plugins/native/znippy-plugin-elf/src/lib.rs
//! znippy handler for **ELF binaries with provenance** — so a built binary can
//! be stored, searched, verified and **refused by name**.
//!
//! It teaches znippy's Arrow-IPC index what each binary in an archive *is*, what
//! it needs to run, what it costs to fetch and **what built it**, so a sealed
//! archive of artifacts is queryable without unpacking it:
//!
//! ```sql
//! -- the question nobody could answer the night this was ordered
//! SELECT relative_path FROM elf WHERE toolchain_image_sha256 <> '<the pin>';
//! -- which binaries link libssl 3.0.x
//! SELECT relative_path FROM elf WHERE elf_needed LIKE '%libssl.so.3(OPENSSL_3.0%';
//! -- which members lie about their architecture
//! SELECT relative_path, arch_claim, elf_machine FROM elf WHERE arch_lies = 1;
//! -- what a fetcher pays, before it fetches
//! SELECT relative_path, text_bytes, debug_bytes FROM elf WHERE has_debug = 1;
//! ```
//!
//! It adds **only metadata columns** — the format, the packing and the sealing
//! stay owned by znippy.
//!
//! Native builtin: registered in `znippy-cli/src/handlers.rs::builtin_handlers`,
//! discovered via [`ArchiveTypePlugin::meta`]. NOT a WASM plugin. Design:
//! `.nornir/elf-design.md`.
//!
//! **`type_id` 7, not a fresh number, and that was deliberate.** `elf` was
//! already taken by a filename-only skeleton at 7
//! (`znippy-common/src/plugins/native/skeletons.rs`). `find_handler` takes the
//! first match in `builtin_handlers()` order and that vector is sorted by
//! `type_id`, so a fresh id named `elf` would sit BEHIND the skeleton and
//! `--format elf` would silently seal with a handler that extracts nothing —
//! the exact `image` collision between docker (12) and media (25) this repo
//! already recorded. The skeleton is deleted in the same change: one id, one
//! handler, no ambiguity.
//!
//! Laws it honours: **P-1** one archive = one ecosystem (`--format elf`);
//! **P-2** writes only the columns it declared; **P-4** never panics — a
//! truncated or hostile binary still writes its row, with every fact it could
//! not prove left null; **E-0** a path may widen candidacy, never assert a fact
//! (the one path-derived column is literally named `arch_claim`).
/// What a **path** claims about architecture — a claim, never a fact.
/// Unconditional and dependency-free, so `--no-default-features` compiles for
/// `wasm32-unknown-unknown`.
pub mod claim;
#[cfg(feature = "znippy-handler")]
pub mod facts;
#[cfg(feature = "znippy-handler")]
pub mod refusal;
/// The DSSE / in-toto / SLSA binding — behind the `provenance` feature, because
/// it turns on `znippy-common/sign`. See the module docs for why it adds no new
/// signing scheme.
#[cfg(feature = "provenance")]
pub mod provenance;
#[cfg(feature = "znippy-handler")]
pub use facts::{ElfFacts, Recipe, is_elf, read};
#[cfg(feature = "znippy-handler")]
pub use refusal::{Coordinate, ElfRefusal, HostFacts, Linkage, RecipeVerdict, check};
#[cfg(feature = "znippy-handler")]
use std::collections::HashMap;
#[cfg(feature = "znippy-handler")]
use znippy_common::plugin::{
default_command, ArchiveTypePlugin, Col, ExtensionRow, ExtensionValue, Claim, HandlerSpec,
};
/// DenseUnion / pkg_type discriminant. The id already reserved for ELF.
pub const ELF_TYPE_ID: i8 = 7;
/// Native ELF handler.
#[cfg(feature = "znippy-handler")]
pub struct NativeElfPlugin;
#[cfg(feature = "znippy-handler")]
impl NativeElfPlugin {
pub fn new() -> Self {
NativeElfPlugin
}
}
#[cfg(feature = "znippy-handler")]
impl Default for NativeElfPlugin {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "znippy-handler")]
fn u32_sat(v: u64) -> ExtensionValue {
ExtensionValue::U32(u32::try_from(v).unwrap_or(u32::MAX))
}
/// The ELF handler, stated once.
///
/// `extensions` is EMPTY on purpose and agrees with `matches_path` (**E-0**):
/// the handler claims by the `\x7fELF` magic, never by name — most binaries in
/// `bin/` have no extension at all, so an extension list was a promise the
/// handler never kept.
#[cfg(feature = "znippy-handler")]
pub static ELF_SPEC: HandlerSpec = HandlerSpec {
name: "elf",
aliases: &["binary", "so"],
type_id: ELF_TYPE_ID,
ecosystem: "Linux ELF binaries / shared objects, with build provenance",
extensions: &[],
// Candidacy, not a fact (**E-0**). `--format elf` selects this handler
// explicitly, so every member is a candidate and the magic bytes decide.
// Claiming by extension would miss every extensionless binary in `bin/`,
// which is most of them.
claim: Claim::Content,
description: "Reads every fact from the binary itself — header, PT_INTERP, \
DT_NEEDED with symbol versions, build-id, ISA level, the \
.nordisk.buildthing receipt, .note.package and .dep-v0 — so an \
archive of artifacts is searchable and a box can refuse one BY NAME",
fields: &[
// identity
("member_kind", Col::Utf8),
("elf_build_id", Col::Utf8),
// header — what it can run on
("elf_machine", Col::Utf8),
("elf_class", Col::Utf8),
("elf_osabi", Col::Utf8),
("elf_type", Col::Utf8),
("elf_interp", Col::Utf8),
("elf_abi_level", Col::Utf8),
// dependencies
("elf_needed", Col::Utf8),
// cost
("text_bytes", Col::U32),
("debug_bytes", Col::U32),
("has_debug", Col::U32),
// recipe
("recipe_coordinate", Col::Utf8),
("recipe_variant", Col::Utf8),
("recipe_features", Col::Utf8),
("recipe_target", Col::Utf8),
("toolchain_image", Col::Utf8),
("toolchain_image_sha256", Col::Utf8),
("source_commit", Col::Utf8),
// the lie
("arch_claim", Col::Utf8),
("arch_lies", Col::U32),
// audit
("auditable_root", Col::Utf8),
],
commands: &[
("facts", "Print every fact read from an ELF file"),
("refusals", "Check an ELF against THIS host and print each refusal by name"),
],
};
#[cfg(feature = "znippy-handler")]
impl ArchiveTypePlugin for NativeElfPlugin {
fn spec(&self) -> &HandlerSpec {
&ELF_SPEC
}
fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
if !matches!(cmd, "facts" | "refusals") {
return default_command(self, cmd, args);
}
let path = args
.first()
.ok_or_else(|| anyhow::anyhow!("usage: elf {cmd} <file>"))?;
let data = std::fs::read(path)?;
let Some(f) = facts::read(&data) else {
anyhow::bail!("'{path}' is not an ELF file (no \\x7fELF magic)")
};
match cmd {
"facts" => {
println!("path: {path}");
println!("member_kind: {}", f.member_kind);
println!("elf_class: {}", f.class);
println!("elf_machine: {}", f.machine);
println!("elf_osabi: {}", f.osabi);
println!("elf_type: {}", f.etype);
println!("elf_interp: {}", f.interp.as_deref().unwrap_or("- (static)"));
println!("elf_abi_level: {}", f.abi_level.as_deref().unwrap_or("-"));
println!("elf_build_id: {}", f.build_id.as_deref().unwrap_or("-"));
println!("elf_needed: {}", f.needed.as_deref().unwrap_or("-"));
println!("text_bytes: {}", f.text_bytes);
println!("debug_bytes: {}", f.debug_bytes);
println!("has_debug: {}", f.has_debug);
println!("recipe_coordinate: {}", f.recipe.coordinate.as_deref().unwrap_or("-"));
println!("recipe_variant: {}", f.recipe.variant.as_deref().unwrap_or("-"));
println!("recipe_features: {}", f.recipe.features.as_deref().unwrap_or("-"));
println!("recipe_target: {}", f.recipe.target.as_deref().unwrap_or("-"));
println!("toolchain_image: {}", f.recipe.toolchain_image.as_deref().unwrap_or("-"));
println!(
"toolchain_image_sha256: {}",
f.recipe.toolchain_image_sha256.as_deref().unwrap_or("-")
);
println!("source_commit: {}", f.recipe.source_commit.as_deref().unwrap_or("-"));
println!("arch_claim: {}", claim::arch_claim(path).unwrap_or("-"));
println!("arch_lies: {}", claim::lies(claim::arch_claim(path), &f.machine));
println!("auditable_root: {}", f.auditable_root.as_deref().unwrap_or("-"));
Ok(())
}
"refusals" => {
let host = refusal::HostFacts::detect();
let coord = refusal::Coordinate { path: path.clone(), ..Default::default() };
let refusals = refusal::check(&f, &host, &coord);
// A check nobody can see not running is worse than no check at
// all: the verdict is printed whether or not anything refused.
println!(
"elf-recipe verdict: {} (toolchain image)",
refusal::recipe_verdict(&f, &coord).word()
);
if refusals.is_empty() {
println!("no refusal — this host can run {path}");
} else {
for r in &refusals {
println!("{r}");
}
}
Ok(())
}
_ => unreachable!("dispatched above"),
}
}
fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
// The ONLY gate. Not the extension, not the directory: the bytes.
let f = facts::read(data)?;
let mut m: HashMap<String, ExtensionValue> = HashMap::new();
let mut put = |k: &str, v: Option<String>| {
if let Some(v) = v {
m.insert(k.into(), ExtensionValue::Str(v));
}
};
put("member_kind", Some(f.member_kind.to_string()));
put("elf_build_id", f.build_id.clone());
put("elf_machine", Some(f.machine.clone()));
put("elf_class", Some(f.class.to_string()));
put("elf_osabi", Some(f.osabi.clone()));
put("elf_type", Some(f.etype.to_string()));
put("elf_interp", f.interp.clone());
put("elf_abi_level", f.abi_level.clone());
put("elf_needed", f.needed.clone());
put("recipe_coordinate", f.recipe.coordinate.clone());
put("recipe_variant", f.recipe.variant.clone());
put("recipe_features", f.recipe.features.clone());
put("recipe_target", f.recipe.target.clone());
put("toolchain_image", f.recipe.toolchain_image.clone());
put("toolchain_image_sha256", f.recipe.toolchain_image_sha256.clone());
put("source_commit", f.recipe.source_commit.clone());
put("auditable_root", f.auditable_root.clone());
m.insert("text_bytes".into(), u32_sat(f.text_bytes));
m.insert("debug_bytes".into(), u32_sat(f.debug_bytes));
m.insert("has_debug".into(), ExtensionValue::U32(u32::from(f.has_debug)));
// The one path-derived pair, and it is labelled as a claim.
let claimed = claim::arch_claim(path);
if let Some(c) = claimed {
m.insert("arch_claim".into(), ExtensionValue::Str(c.into()));
}
m.insert(
"arch_lies".into(),
ExtensionValue::U32(u32::from(claim::lies(claimed, &f.machine))),
);
Some(ExtensionRow { fields: m })
}
}
#[cfg(all(test, feature = "znippy-handler"))]
mod fixture;
#[cfg(all(test, feature = "znippy-handler"))]
mod tests;