plugins/native/znippy-plugin-elf/src/refusal.rs
//! The four named refusals.
//!
//! **None of them excludes hardware.** Every one of them turns a box that starts
//! and dies into a box that says *why*, before it starts.
//!
//! House style, verbatim from the estate (`gunnar/xtask/src/isoinstaller.rs`,
//! `edda/crates/korp-installer/src/installer_source.rs:257`):
//!
//! ```text
//! REFUSED [<kebab-name>] <one sentence naming BOTH sides and the fix>
//! ```
//!
//! [`check`] is a **pure function** of `(facts, host, coordinate)`, so every arm
//! is testable without a box, a binary or a boot.
use std::fmt;
use std::path::Path;
use crate::claim;
use crate::facts::ElfFacts;
/// What the box running the check actually is. Every field is *measured*, never
/// configured — `detect` reads CPU features in-process through `std::arch` and
/// the filesystem through `std::fs`. No subprocess, no `/proc` text parsing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostFacts {
/// Canonical machine name, same vocabulary as `elf_machine`.
pub machine: String,
/// The highest x86-64 ISA level this CPU provides, or the aarch64 features.
pub abi_level: Option<String>,
/// Absolute interpreter paths that exist on this box.
pub interpreters: Vec<String>,
}
impl HostFacts {
/// Measure this box.
pub fn detect() -> Self {
HostFacts {
machine: host_machine().to_string(),
abi_level: host_abi_level(),
interpreters: KNOWN_INTERPRETERS
.iter()
.filter(|p| Path::new(p).exists())
.map(|p| (*p).to_string())
.collect(),
}
}
}
/// The interpreters a nordisk box can plausibly have. The list is only used to
/// answer "does the one this binary asks for exist" — an interpreter NOT in this
/// list is still checked, by `Path::exists` on the binary's own `PT_INTERP`.
const KNOWN_INTERPRETERS: &[&str] = &[
"/lib64/ld-linux-x86-64.so.2",
"/lib/ld-linux-aarch64.so.1",
"/lib/ld-musl-x86_64.so.1",
"/lib/ld-musl-aarch64.so.1",
"/lib/ld-linux-riscv64-lp64d.so.1",
];
fn host_machine() -> &'static str {
match std::env::consts::ARCH {
"x86_64" => "x86-64",
"x86" => "x86",
"aarch64" => "aarch64",
"arm" => "arm",
"riscv64" => "riscv64",
"powerpc64" => "powerpc64",
"s390x" => "s390x",
"loongarch64" => "loongarch64",
other => other,
}
}
/// The host's ISA level, read from the CPU in-process.
#[cfg(target_arch = "x86_64")]
fn host_abi_level() -> Option<String> {
let v2 = is_x86_feature_detected!("popcnt")
&& is_x86_feature_detected!("sse4.2")
&& is_x86_feature_detected!("ssse3");
let v3 = v2
&& is_x86_feature_detected!("avx2")
&& is_x86_feature_detected!("bmi1")
&& is_x86_feature_detected!("bmi2")
&& is_x86_feature_detected!("fma");
let v4 = v3 && is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512bw");
Some(
if v4 {
"x86-64-v4"
} else if v3 {
"x86-64-v3"
} else if v2 {
"x86-64-v2"
} else {
"x86-64-baseline"
}
.to_string(),
)
}
#[cfg(target_arch = "aarch64")]
fn host_abi_level() -> Option<String> {
let mut f: Vec<&str> = Vec::new();
if std::arch::is_aarch64_feature_detected!("bti") {
f.push("bti");
}
if std::arch::is_aarch64_feature_detected!("paca") {
f.push("pac");
}
if f.is_empty() { None } else { Some(f.join("+")) }
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
fn host_abi_level() -> Option<String> {
None
}
/// The rank of an x86-64 ISA level, for comparison. `None` for anything that is
/// not an x86-64 level — an unranked level is never "above" another one, because
/// guessing an ordering is how a working box gets refused.
fn x86_rank(level: &str) -> Option<u8> {
match level {
"x86-64-baseline" => Some(0),
"x86-64-v2" => Some(1),
"x86-64-v3" => Some(2),
"x86-64-v4" => Some(3),
_ => None,
}
}
/// Whether the ARCHIVE's coordinate says this member should be static.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Linkage {
/// The coordinate says static; a `PT_INTERP` is then a contradiction.
Static,
/// The coordinate says dynamic.
Dynamic,
/// The coordinate does not say. Silence is not a contradiction.
Unstated,
}
/// The three states a recipe check can end in — the same shape
/// `installer-source-guard` proved (`Verdict { Match, Unchecked, Unpinned }`):
/// an unchecked build is **recorded**, never silent, and only a genuine
/// contradiction refuses.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecipeVerdict {
/// Checked and agreed.
Match,
/// Nothing to check against. Printed as a NAMED skip, never as silence — a
/// check nobody can see not running is worse than no check at all.
Unchecked,
}
impl RecipeVerdict {
pub fn word(self) -> &'static str {
match self {
RecipeVerdict::Match => "match",
RecipeVerdict::Unchecked => "unchecked",
}
}
}
/// What the archive's coordinate and its signature claim about this member.
/// Every field is optional: the coordinate saying nothing is `Unchecked`, not a
/// refusal.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Coordinate {
/// The member's stored path — the only thing that carries an arch CLAIM.
pub path: String,
/// What the coordinate says the linkage should be.
pub linkage: Option<Linkage>,
/// The toolchain image digest the archive's signed statement vouches for.
pub expect_toolchain_image_sha256: Option<String>,
/// The source commit the archive's signed statement vouches for.
pub expect_source_commit: Option<String>,
/// The recipe coordinate (`krate:version:hash`) the statement vouches for.
pub expect_recipe_coordinate: Option<String>,
}
/// One named refusal.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ElfRefusal {
/// The header's machine disagrees with the coordinate, or with the host.
Machine { header: String, other: String, whose: &'static str },
/// Dynamic where the coordinate says static.
LibcDynamicWhereStatic { interp: String },
/// The interpreter this binary asks for is not on this box.
LibcInterpreterMissing { interp: String },
/// The micro-architecture level is above the host's.
Cpu { needs: String, host: String },
/// The build coordinate is missing entirely.
RecipeMissing { field: &'static str },
/// The binary's own receipt contradicts the archive's signed statement.
RecipeContradicts { field: &'static str, binary: String, statement: String },
}
impl ElfRefusal {
/// The kebab-case refusal name — the estate's `[name]`.
pub fn name(&self) -> &'static str {
match self {
ElfRefusal::Machine { .. } => "elf-machine",
ElfRefusal::LibcDynamicWhereStatic { .. }
| ElfRefusal::LibcInterpreterMissing { .. } => "elf-libc",
ElfRefusal::Cpu { .. } => "elf-cpu",
ElfRefusal::RecipeMissing { .. } | ElfRefusal::RecipeContradicts { .. } => "elf-recipe",
}
}
}
impl fmt::Display for ElfRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "REFUSED [{}] ", self.name())?;
match self {
ElfRefusal::Machine { header, other, whose } => write!(
f,
"the ELF header says {header} and the {whose} says {other} — one of the two is \
wrong, and the header is the one that was measured; rebuild for {other} or file \
this member under {header}"
),
ElfRefusal::LibcDynamicWhereStatic { interp } => write!(
f,
"this member asks for the interpreter {interp}, but the coordinate says it is \
statically linked — the binary is dynamic; either build it with a static libc \
or stop calling it static"
),
ElfRefusal::LibcInterpreterMissing { interp } => write!(
f,
"this member asks for the interpreter {interp} and this box does not have that \
file — it would start and die; install that libc, or ship the static build"
),
ElfRefusal::Cpu { needs, host } => write!(
f,
"this member requires {needs} and this CPU provides {host} — it would SIGILL on \
the first instruction it cannot run; this is a host limit, not a policy, so \
rebuild at {host} or run it on a {needs} box"
),
ElfRefusal::RecipeMissing { field } => write!(
f,
"this member carries no {field} — neither its .nordisk.buildthing receipt nor a \
.note.package note says how it was built, so nothing can vouch for it; rebuild \
it through build-thing so the receipt is linked in"
),
ElfRefusal::RecipeContradicts { field, binary, statement } => write!(
f,
"the binary's own receipt says {field} is {binary} and the archive's signed \
statement says {statement} — two records disagree about one artifact; the \
statement is the authority, so this binary is not the one that was attested"
),
}
}
}
/// Run every check. Pure: no filesystem, no clock, no network — `host` was
/// already measured by [`HostFacts::detect`].
///
/// Returns every refusal, not just the first: a box that has to be rebooted once
/// per problem is a box nobody keeps.
pub fn check(facts: &ElfFacts, host: &HostFacts, coord: &Coordinate) -> Vec<ElfRefusal> {
let mut out = Vec::new();
// ── elf-machine ───────────────────────────────────────────────────────────
// The archive-wide `arch_lies = 1` query and this refusal are ONE rule seen
// from two vantage points: the column sweeps the archive, this checks a start.
if let Some(claimed) = claim::arch_claim(&coord.path) {
if claim::lies(Some(claimed), &facts.machine) {
out.push(ElfRefusal::Machine {
header: facts.machine.clone(),
other: claimed.to_string(),
whose: "coordinate",
});
}
}
if facts.machine != host.machine && facts.machine != "none" {
out.push(ElfRefusal::Machine {
header: facts.machine.clone(),
other: host.machine.clone(),
whose: "host",
});
}
// ── elf-libc ──────────────────────────────────────────────────────────────
if let Some(interp) = &facts.interp {
if coord.linkage == Some(Linkage::Static) {
out.push(ElfRefusal::LibcDynamicWhereStatic { interp: interp.clone() });
}
if !host.interpreters.iter().any(|p| p == interp) {
out.push(ElfRefusal::LibcInterpreterMissing { interp: interp.clone() });
}
}
// ── elf-cpu ───────────────────────────────────────────────────────────────
if let (Some(needs), Some(have)) = (&facts.abi_level, &host.abi_level) {
if let (Some(n), Some(h)) = (x86_rank(needs), x86_rank(have)) {
if n > h {
out.push(ElfRefusal::Cpu { needs: needs.clone(), host: have.clone() });
}
}
}
// ── elf-recipe ────────────────────────────────────────────────────────────
if facts.recipe.is_empty() {
out.push(ElfRefusal::RecipeMissing { field: "build coordinate" });
} else {
contradiction(
"toolchain_image_sha256",
facts.recipe.toolchain_image_sha256.as_deref(),
coord.expect_toolchain_image_sha256.as_deref(),
&mut out,
);
contradiction(
"source_commit",
facts.recipe.source_commit.as_deref(),
coord.expect_source_commit.as_deref(),
&mut out,
);
contradiction(
"recipe_coordinate",
facts.recipe.coordinate.as_deref(),
coord.expect_recipe_coordinate.as_deref(),
&mut out,
);
}
out
}
/// A contradiction needs BOTH sides. One side missing is [`RecipeVerdict::Unchecked`]
/// — recorded by [`recipe_verdict`], never refused here.
fn contradiction(
field: &'static str,
binary: Option<&str>,
statement: Option<&str>,
out: &mut Vec<ElfRefusal>,
) {
if let (Some(b), Some(s)) = (binary, statement) {
if b != s {
out.push(ElfRefusal::RecipeContradicts {
field,
binary: b.to_string(),
statement: s.to_string(),
});
}
}
}
/// Was the toolchain image actually checked against anything?
///
/// Until `BuildProvenance` carries the toolchain image (`.nornir/elf-design.md`
/// §6.3(b), an owner decision in edda), this is `Unchecked` for every artifact —
/// and it must be PRINTED as a named skip rather than pass silently.
pub fn recipe_verdict(facts: &ElfFacts, coord: &Coordinate) -> RecipeVerdict {
match (&facts.recipe.toolchain_image_sha256, &coord.expect_toolchain_image_sha256) {
(Some(_), Some(_)) => RecipeVerdict::Match,
_ => RecipeVerdict::Unchecked,
}
}