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

nordisk/edda

crates/nornir-purgatory/src/lib.rs

//! **The purgatorium** — the evidence half of a two-stage kernel minimiser.
//!
//! ```text
//! stage 1  build-thing variant, COMMITTED fragments  =  POLICY    a floor, cannot regress
//! stage 2  purgatorium, MEASURED reachability        =  EVIDENCE  a subtraction on that floor
//! ```
//!
//! Stage 2 runs **before** the kernel is configured, because its output is an
//! *input* to kconfig selection — the same shape `data.io = "uring"` already has
//! in `nornir-build-thing`'s `data.rs`.
//!
//! The plan is `workspace_nordisk/mimir-v2.md`. **What is here:**
//!
//! * step 1 — the four view modes ([`kconfig::effective`]), pure set arithmetic;
//! * step 3 — the three invariants ([`subtract`]), each a test;
//! * the first real implementations of the three traits:
//!   [`kconfig::tree::KconfigTree`] is a [`KconfigSource`] over a real kernel
//!   tree (the `select`/`depends on` graph, read, not solved);
//!   [`syscall::ProbeEvidence`] is an [`EvidenceSource`] — a measured syscall
//!   list joined to CONFIG symbols through [`syscall::SyscallJoin`], derive >
//!   probe > checked table (plan §2, step 5);
//!   [`subtract::KconfigAwareSubtractor`] is a [`Subtractor`] whose immunity is
//!   transitive through the graph and which reports a futile subtraction by
//!   naming the `select`or that would undo it.
//!
//! What is NOT here, and why:
//!
//! * **The gate itself** (`dwarves purgatorium`, the `buildthing` step) — a
//!   dwarves path. The evidence it needs from the CALL GRAPH is still absent in
//!   one direction: nornir `dbdea8a` REFUSES `ra_ingest`'s def-site-keyed scans
//!   from the cross-repo join (so the 2026-09-07 fabricated edge can no longer
//!   happen), but `ra_ap_ide::moniker` is not wired, so calls that cross a
//!   workspace member are MISSING rather than wrong. An [`EvidenceSource`] over
//!   that graph must report [`Freshness::Unsound`] until it is — an incomplete
//!   demand set subtracts symbols another member reaches. The syscall probe
//!   here does not have that problem: it measures the whole process.
//! * **`RECIPE_SCHEMA` 4→5** and **whether stage 2 may subtract at all on the
//!   assured appliance** — both explicitly the owner's (plan §8), so neither is
//!   decided here. [`subtract::Policy::report_only`] carries the second as a
//!   switch with no default.
//!
//! Dep-light by design: consumed by dwarves, nornir and dwarves-gui, so it takes
//! nothing but `std`. A kernel tree is read from disk; a warehouse is behind
//! [`EvidenceSource`].

pub mod kconfig;
pub mod subtract;
pub mod syscall;

pub use kconfig::{
    Directive, Effective, EffectiveReport, KconfigTree, Requested, Unavailable, Value,
};
pub use subtract::{
    propose, Decision, Demand, Freshness, KconfigAwareSubtractor, Policy, PolicySubtractor, Scope,
    Subtraction, Subtractor,
};

use std::collections::BTreeSet;

/// The warehouse side, so the gate is testable without one.
pub trait EvidenceSource {
    /// Kconfig symbols the measured code reaches, for `repo`.
    fn demand(&self, repo: &str) -> Demand;
    /// How much that evidence can be trusted — drives FAIL CLOSED.
    fn freshness(&self) -> Freshness;
}

/// The kernel-tree side: Kconfig's own `select`/`depends on` graph, which is why
/// no C scan is needed (plan §2 — *"Kconfig itself IS a declarative dependency
/// graph"*).
pub trait KconfigSource {
    fn selects(&self, symbol: &str) -> Vec<String>;
    fn depends(&self, symbol: &str) -> Vec<String>;
}

/// Stage 1's symbol set: everything a committed fragment named, and therefore
/// everything invariant 2 makes immune.
#[must_use]
pub fn immune_from_fragments<'a, I>(fragments: I) -> BTreeSet<String>
where
    I: IntoIterator<Item = (&'a str, &'a str)>,
{
    Requested::from_fragments(fragments)
        .symbols()
        .into_iter()
        .map(str::to_string)
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Immunity is derived FROM the fragments, never hand-listed — so a symbol
    /// added to `hardened.config` tomorrow is immune the same day, with nobody
    /// remembering to update a second list.
    #[test]
    fn every_symbol_a_fragment_names_is_immune_including_its_vetoes() {
        let im = immune_from_fragments([(
            "hardened.config",
            "CONFIG_SECCOMP=y\nCONFIG_STACKPROTECTOR_STRONG=y\n# CONFIG_DEVMEM is not set\n",
        )]);
        assert!(im.contains("SECCOMP"));
        assert!(im.contains("STACKPROTECTOR_STRONG"));
        assert!(
            im.contains("DEVMEM"),
            "a VETO is policy too — evidence must not be able to re-open the question"
        );
        assert_eq!(im.len(), 3);
    }
}