Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

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;