57c56ee4e2
The load seam's aura-core stamp becomes AURA_CORE_FINGERPRINT: an FNV-1a
64 hash over aura-core's sorted src/**/*.rs (prefix-free records: path,
NUL, u64-LE content length, contents), emitted by build.rs beside the
rustc stamp and baked into the descriptor by aura_project!. validate_c_tier
compares fingerprints ('aura-core build'); a stale dylib — same rustc,
different aura-core sources — is refused instead of trusted at the
Rust-ABI tier. RED-first: the headline test pins that the frozen crate
version is never again the host stamp (plus a version-bump-proof twin and
an aura-core shape pin: 16 hex, never CARGO_PKG_VERSION).
Descriptor field renamed aura_core_version -> aura_core_fingerprint
(engine-internal; field names are not ABI, order is). Documented accepted
C30 limit: source-level identity only — the consuming build's lockfile/
features stay outside the stamp. C13/C30 prose aligned.
closes #348
173 lines
6.5 KiB
Rust
173 lines
6.5 KiB
Rust
//! The project-cdylib descriptor contract (C13/C16, cycle 0102).
|
|
//!
|
|
//! A research project compiles to a cdylib exporting ONE symbol,
|
|
//! `AURA_PROJECT`, a [`ProjectDescriptor`]. The descriptor has two ABI tiers:
|
|
//! a **C tier** (`magic`, `descriptor_version`, the build stamps, the
|
|
//! namespace — all C-compatible field types) the host validates BEFORE
|
|
//! trusting anything, and a **Rust tier** (the vocabulary resolver and the
|
|
//! enumerable type-id list) the host touches only after both stamps match.
|
|
//! The stamp cannot certify the channel it rides on, so it must not itself
|
|
//! depend on the Rust ABI it certifies.
|
|
//!
|
|
//! The stamps are baked at *aura-core compile time* — per consuming build —
|
|
//! so host and dylib each carry their own side's truth.
|
|
|
|
use crate::node::PrimitiveBuilder;
|
|
|
|
/// First 8 bytes of the C tier; ASCII "AURAPROJ".
|
|
pub const AURA_DESCRIPTOR_MAGIC: u64 = 0x4155_5241_5052_4F4A;
|
|
/// Bumped on any layout change of [`ProjectDescriptor`].
|
|
pub const AURA_DESCRIPTOR_VERSION: u32 = 1;
|
|
/// The one exported symbol name (NUL-terminated for the symbol lookup).
|
|
pub const AURA_PROJECT_SYMBOL: &[u8] = b"AURA_PROJECT\0";
|
|
|
|
/// The compiling rustc's `--version` line (via build.rs).
|
|
pub const RUSTC_VERSION: &str = env!("AURA_RUSTC_VERSION");
|
|
/// Build-identity fingerprint of aura-core's own sources (via build.rs),
|
|
/// per ledger contract C30: the load seam commits to a source-derived
|
|
/// fingerprint, never the frozen crate version (a fixed 0.1.0 under the
|
|
/// no-semver discipline would disarm the handshake it's meant to guard).
|
|
pub const CORE_FINGERPRINT: &str = env!("AURA_CORE_FINGERPRINT");
|
|
|
|
/// C-ABI string slice: pointer + length over `'static` UTF-8 bytes.
|
|
/// No NUL convention needed; readable without trusting the Rust ABI.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct StrSlice {
|
|
pub ptr: *const u8,
|
|
pub len: usize,
|
|
}
|
|
|
|
impl StrSlice {
|
|
pub const fn new(s: &'static str) -> Self {
|
|
Self { ptr: s.as_ptr(), len: s.len() }
|
|
}
|
|
|
|
/// Read the slice back as UTF-8.
|
|
///
|
|
/// # Safety
|
|
/// `ptr`/`len` must describe live UTF-8 bytes — true for any descriptor
|
|
/// built by [`aura_project!`](crate::aura_project), whose strings are
|
|
/// `'static` and whose library is loaded-and-held (never unloaded).
|
|
pub unsafe fn as_str(&self) -> Option<&str> {
|
|
if self.ptr.is_null() {
|
|
return None;
|
|
}
|
|
let bytes = unsafe { core::slice::from_raw_parts(self.ptr, self.len) };
|
|
core::str::from_utf8(bytes).ok()
|
|
}
|
|
}
|
|
|
|
/// The exported project descriptor. Field ORDER is ABI: C tier first,
|
|
/// Rust tier last; validated front-to-back.
|
|
#[repr(C)]
|
|
pub struct ProjectDescriptor {
|
|
// ---- C tier: validated BEFORE any Rust-ABI field is touched ----
|
|
pub magic: u64,
|
|
pub descriptor_version: u32,
|
|
pub rustc_version: StrSlice,
|
|
pub aura_core_fingerprint: StrSlice,
|
|
pub namespace: StrSlice,
|
|
// ---- Rust tier: only after both stamps match ----
|
|
pub vocabulary: fn(&str) -> Option<PrimitiveBuilder>,
|
|
pub type_ids: fn() -> &'static [&'static str],
|
|
}
|
|
|
|
// The raw pointers are to 'static data; the fn pointers are Sync by nature.
|
|
unsafe impl Sync for ProjectDescriptor {}
|
|
|
|
/// Declare a project crate's export in one place. Emits the `AURA_PROJECT`
|
|
/// static with this build's stamps baked in.
|
|
#[macro_export]
|
|
macro_rules! aura_project {
|
|
(namespace: $ns:literal, vocabulary: $vocab:path, type_ids: $ids:path $(,)?) => {
|
|
#[unsafe(no_mangle)]
|
|
pub static AURA_PROJECT: $crate::project::ProjectDescriptor =
|
|
$crate::project::ProjectDescriptor {
|
|
magic: $crate::project::AURA_DESCRIPTOR_MAGIC,
|
|
descriptor_version: $crate::project::AURA_DESCRIPTOR_VERSION,
|
|
rustc_version: $crate::project::StrSlice::new(
|
|
$crate::project::RUSTC_VERSION,
|
|
),
|
|
aura_core_fingerprint: $crate::project::StrSlice::new(
|
|
$crate::project::CORE_FINGERPRINT,
|
|
),
|
|
namespace: $crate::project::StrSlice::new($ns),
|
|
vocabulary: $vocab,
|
|
type_ids: $ids,
|
|
};
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn fake_vocab(type_id: &str) -> Option<PrimitiveBuilder> {
|
|
let _ = type_id;
|
|
None
|
|
}
|
|
fn fake_ids() -> &'static [&'static str] {
|
|
&["t::A"]
|
|
}
|
|
|
|
aura_project! {
|
|
namespace: "t",
|
|
vocabulary: fake_vocab,
|
|
type_ids: fake_ids,
|
|
}
|
|
|
|
#[test]
|
|
fn stamps_are_baked_and_nonempty() {
|
|
assert!(RUSTC_VERSION.starts_with("rustc "));
|
|
assert!(!CORE_FINGERPRINT.is_empty());
|
|
}
|
|
|
|
/// C30/#348: the stamp is a source-derived hash, never a constant that
|
|
/// would disarm the load-seam refusal — pin its mechanical shape (the
|
|
/// 16-hex FNV-1a rendering) and that it is not the crate version.
|
|
#[test]
|
|
fn core_fingerprint_is_a_source_hash_not_the_crate_version() {
|
|
assert_eq!(CORE_FINGERPRINT.len(), 16);
|
|
assert!(CORE_FINGERPRINT.chars().all(|c| c.is_ascii_hexdigit()));
|
|
assert_ne!(CORE_FINGERPRINT, env!("CARGO_PKG_VERSION"));
|
|
}
|
|
|
|
#[test]
|
|
fn str_slice_round_trips() {
|
|
let s = StrSlice::new("hello");
|
|
assert_eq!(unsafe { s.as_str() }, Some("hello"));
|
|
}
|
|
|
|
/// `as_str` refuses a null pointer instead of dereferencing it — the
|
|
/// loader's boundary-safety primitive for a descriptor field it hasn't
|
|
/// otherwise validated yet.
|
|
#[test]
|
|
fn str_slice_null_ptr_is_none() {
|
|
let s = StrSlice { ptr: std::ptr::null(), len: 0 };
|
|
assert_eq!(unsafe { s.as_str() }, None);
|
|
}
|
|
|
|
/// `as_str` refuses non-UTF-8 bytes rather than producing a mangled or
|
|
/// panicking read, so a corrupt/foreign descriptor field degrades to
|
|
/// `None` instead of undefined behaviour downstream.
|
|
#[test]
|
|
fn str_slice_invalid_utf8_is_none() {
|
|
static BAD: [u8; 2] = [0xFF, 0xFE];
|
|
let s = StrSlice { ptr: BAD.as_ptr(), len: BAD.len() };
|
|
assert_eq!(unsafe { s.as_str() }, None);
|
|
}
|
|
|
|
#[test]
|
|
fn macro_emits_a_valid_descriptor() {
|
|
let d = &AURA_PROJECT;
|
|
assert_eq!(d.magic, AURA_DESCRIPTOR_MAGIC);
|
|
assert_eq!(d.descriptor_version, AURA_DESCRIPTOR_VERSION);
|
|
assert_eq!(unsafe { d.namespace.as_str() }, Some("t"));
|
|
assert_eq!(unsafe { d.rustc_version.as_str() }, Some(RUSTC_VERSION));
|
|
assert_eq!(unsafe { d.aura_core_fingerprint.as_str() }, Some(CORE_FINGERPRINT));
|
|
assert!((d.vocabulary)("nope").is_none());
|
|
assert_eq!((d.type_ids)(), &["t::A"]);
|
|
}
|
|
}
|