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/tests/kconfig_graph_and_syscall_join.rs

//! **Step 2 of `mimir-v2.md`, on a miniature kernel tree** — the Kconfig graph
//! reader, the syscall → CONFIG join, and the first real `EvidenceSource`,
//! `KconfigSource` and `Subtractor` implementations.
//!
//! `tests/fixtures/kernel-mini` is a few dozen files in the SHAPE of a real tree:
//! `Kconfig` sourcing `arch/$(SRCARCH)/Kconfig`, `if`/`menu`/`choice` scopes,
//! help blocks, `select … if`, Makefiles with `obj-$(CONFIG_X)` and composite
//! objects, `.c` files with `SYSCALL_DEFINEn` under `#ifdef CONFIG_X`, and a
//! `syscall_64.tbl`. Every relation in it is copied from the real kernel; only
//! the help text is shortened. The same assertions run against a REAL tree in
//! `against_a_real_kernel_tree.rs`.
//!
//! Each invariant below was written RED first — the assertion existed before the
//! code that satisfies it.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use nornir_purgatory::kconfig::tree::{symbols_in_expr, KconfigTree, Polarity};
use nornir_purgatory::kconfig::{Effective, EffectiveReport, Requested};
use nornir_purgatory::syscall::{
    check_table, object_guards, DefinitionIndex, Probe, ProbeEvidence, ProbedGuard, Resolution,
    Road, SyscallJoin, SyscallTable, TableEntry, CHECKED_TABLE,
};
use nornir_purgatory::{
    immune_from_fragments, propose, Demand, EvidenceSource, Freshness, KconfigAwareSubtractor,
    KconfigSource, Policy, Scope, Subtractor,
};

fn mini() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/kernel-mini")
}

fn tree() -> KconfigTree {
    KconfigTree::read(&mini(), "x86").expect("the fixture tree must read")
}

fn set(syms: &[&str]) -> BTreeSet<String> {
    syms.iter().map(|s| (*s).to_string()).collect()
}

// ── the Kconfig reader ───────────────────────────────────────────────────────

/// `source "arch/$(SRCARCH)/Kconfig"` is followed, nested sources are followed,
/// and a source that does not exist is RECORDED rather than ignored or fatal.
#[test]
fn the_reader_follows_source_directives_and_records_the_missing_one() {
    let t = tree();
    assert_eq!(
        t.release(),
        Some("6.12.104"),
        "VERSION.PATCHLEVEL.SUBLEVEL from the Makefile"
    );
    // Reached through the root, through arch/$(SRCARCH), through security/.
    for sym in [
        "EXPERT",
        "SECCOMP_FILTER",
        "SECURITY_LANDLOCK",
        "BLK_DEV_IO_TRACE",
        "IO_URING",
    ] {
        assert!(
            t.symbol(sym).is_some(),
            "{sym} must be read; got {} symbols",
            t.len()
        );
    }
    assert_eq!(
        t.missing_sources(),
        &["this/one/does/not/exist/Kconfig".to_string()],
        "a missing source is a finding, not silence"
    );
    assert!(t.files_read() >= 13, "files read: {}", t.files_read());
    // Kconfig.include declares macros, never symbols.
    assert!(t.symbol("y").is_none() && t.symbol("success").is_none());
}

/// The relations the plan is built on, read off the fixture exactly as the
/// kernel spells them.
#[test]
fn select_and_depends_are_read_with_their_conditions() {
    let t = tree();
    let blk = t.symbol("BLK_DEV_IO_TRACE").unwrap();
    let selects: Vec<&str> = blk.selects.iter().map(|s| s.target.as_str()).collect();
    assert_eq!(
        selects,
        [
            "RELAY",
            "DEBUG_FS",
            "TRACEPOINTS",
            "GENERIC_TRACER",
            "STACKTRACE"
        ]
    );
    assert_eq!(blk.file, "kernel/trace/Kconfig");
    assert!(blk.line > 0);
    // `depends on` lines AND the enclosing `if FTRACE` block.
    assert_eq!(t.depends("BLK_DEV_IO_TRACE"), ["SYSFS", "BLOCK", "FTRACE"]);

    let sf = t.symbol("SECCOMP_FILTER").unwrap();
    assert_eq!(sf.depends, ["HAVE_ARCH_SECCOMP_FILTER && SECCOMP && NET"]);
    assert_eq!(
        t.depends("SECCOMP_FILTER"),
        ["HAVE_ARCH_SECCOMP_FILTER", "SECCOMP", "NET"]
    );

    // `select X if COND` keeps its condition.
    let x86 = t.symbol("X86").unwrap();
    let huge = x86
        .selects
        .iter()
        .find(|s| s.target == "HAVE_ARCH_HUGE_VMAP")
        .unwrap();
    assert_eq!(huge.cond.as_deref(), Some("X86_64 || X86_PAE"));

    // `bool "…" if EXPERT` — the shape hardened.config:28-36 explains.
    let io = t.symbol("IO_URING").unwrap();
    assert_eq!(io.prompt.as_deref(), Some("Enable IO uring support"));
    assert_eq!(io.prompt_cond.as_deref(), Some("EXPERT"));
    assert!(
        t.symbol("IO_WQ").unwrap().prompt.is_none(),
        "a promptless symbol"
    );

    // `prompt` as its own line, `def_bool` as type + default.
    let sec = t.symbol("SECCOMP").unwrap();
    assert_eq!(
        sec.prompt.as_deref(),
        Some("Enable seccomp to safely execute untrusted bytecode")
    );
    assert_eq!(sec.defaults.len(), 1);

    // menuconfig is a config.
    assert!(t.symbol("NET").is_some() && t.symbol("MODULES").is_some());
}

/// A help block is prose. The fixture's `CC_VERSION_TEXT` help contains a line
/// beginning `select DEBUG_FS`; it must not become a select.
#[test]
fn help_text_is_never_parsed_as_attributes() {
    let t = tree();
    assert!(t.symbol("CC_VERSION_TEXT").unwrap().selects.is_empty());
    assert!(t
        .selected_by("DEBUG_FS")
        .iter()
        .all(|s| s.by != "CC_VERSION_TEXT"));
    // and the block after the help resumed parsing.
    assert!(t.symbol("IRQ_WORK").is_some());
}

/// Scoping: `if X … endif`, `menu` and `choice` deps reach the entries inside
/// them and stop at the closer.
#[test]
fn if_menu_and_choice_scopes_apply_to_what_they_enclose_and_nothing_else() {
    let t = tree();
    assert_eq!(
        t.depends("MODULE_UNLOAD"),
        ["MODULES"],
        "inside `if MODULES`"
    );
    assert!(
        !t.depends("SYSFS").contains(&"MODULES".to_string()),
        "after `endif`"
    );
    assert_eq!(
        t.depends("PARAVIRT"),
        ["HAVE_STATIC_CALL", "HYPERVISOR_GUEST"]
    );
    assert!(
        t.depends("HYPERVISOR_GUEST").is_empty(),
        "after `endif` and `endmenu`"
    );
    assert_eq!(
        t.depends("DEBUG_FS_ALLOW_ALL"),
        ["DEBUG_FS"],
        "a choice's `depends on`"
    );
    assert!(
        t.depends("STACKTRACE").iter().all(|d| d != "DEBUG_FS"),
        "after `endchoice`"
    );
    assert_eq!(t.depends("INET"), ["NET"], "inside `if NET`");
    assert!(t.depends("HAVE_CBPF_JIT").is_empty());
}

/// Expression polarity: `!X` and `X=n` are NEGATIVE dependencies. Subtracting a
/// negatively-depended symbol cannot hurt the dependant, so it must not be made
/// immune by it — `HARDENED_USERCOPY depends on !SLUB_TINY` is the real case
/// (`hardened.config:53`).
#[test]
fn polarity_is_read_so_a_negative_dependency_does_not_protect_its_symbol() {
    let pos = |e: &str| -> Vec<String> {
        symbols_in_expr(e)
            .into_iter()
            .filter(|(_, p)| *p == Polarity::Positive)
            .map(|(s, _)| s)
            .collect()
    };
    let neg = |e: &str| -> Vec<String> {
        symbols_in_expr(e)
            .into_iter()
            .filter(|(_, p)| *p == Polarity::Negative)
            .map(|(s, _)| s)
            .collect()
    };
    assert_eq!(
        pos("HAVE_ARCH_SECCOMP_FILTER && SECCOMP && NET"),
        ["HAVE_ARCH_SECCOMP_FILTER", "SECCOMP", "NET"]
    );
    assert_eq!(neg("!SLUB_TINY"), ["SLUB_TINY"]);
    assert!(pos("!SLUB_TINY").is_empty());
    assert_eq!(neg("CFG80211=n"), ["CFG80211"]);
    assert_eq!(
        pos("IPV6 || IPV6=n"),
        ["IPV6"],
        "one positive mention is enough to protect"
    );
    assert_eq!(pos("KUNIT=y"), ["KUNIT"]);
    assert_eq!(
        neg("BATMAN_ADV && !(CFG80211=m && BATMAN_ADV=y)"),
        ["CFG80211", "BATMAN_ADV"]
    );
    assert_eq!(
        pos("$(cc-option,-fstack-protector-strong)"),
        Vec::<String>::new(),
        "a macro is not a symbol"
    );
    assert_eq!(
        pos("INIT_STACK_NONE || !CC_IS_CLANG || CLANG_VERSION >= 140000"),
        ["INIT_STACK_NONE", "CLANG_VERSION"]
    );
    assert!(
        pos("y").is_empty() && pos("m").is_empty() && pos("n").is_empty(),
        "constants"
    );
    assert_eq!(
        pos("\"$(ARCH)\" = \"x86\""),
        Vec::<String>::new(),
        "quoted strings are not symbols"
    );
    let t = tree();
    assert!(
        t.depends("HARDENED_USERCOPY").is_empty(),
        "the only dep is negative"
    );
}

/// **§2's question answered from Kconfig alone: what does `CONFIG_X` pull in?**
/// The transitive `select` closure, no C scan.
#[test]
fn the_select_closure_is_transitive_and_is_the_debugfs_story() {
    let t = tree();
    let pulled = t.select_closure("BLK_DEV_IO_TRACE");
    for s in [
        "RELAY",
        "DEBUG_FS",
        "TRACEPOINTS",
        "GENERIC_TRACER",
        "STACKTRACE",
        "IRQ_WORK",
        "TRACING",
        "RING_BUFFER",
    ] {
        assert!(pulled.contains(s), "{s} missing from {pulled:?}");
    }
    assert!(
        !pulled.contains("BLK_DEV_IO_TRACE"),
        "a symbol does not pull itself"
    );
    assert!(!pulled.contains("SYSFS"), "depends on is not select");
    // The KconfigSource view is one hop.
    assert_eq!(t.selects("RELAY"), ["IRQ_WORK"]);
    assert_eq!(t.selects("GENERIC_TRACER"), ["TRACING"]);
}

/// **ADDED, explained.** An enabled symbol nobody asked for is explained by the
/// enabled symbol that `select`ed it, with the file it happened in. This is the
/// 2026-09-07 defect found by hand, made routine.
#[test]
fn an_arrival_is_explained_by_the_enabled_symbol_that_selected_it() {
    let t = tree();
    let requested = Requested::from_fragments([("nuked.config", "# CONFIG_DEBUG_FS is not set\n")]);
    let eff = Effective::from_produced_config(
        "# Linux/x86 6.12.104 Kernel Configuration\nCONFIG_BLK_DEV_IO_TRACE=y\nCONFIG_DEBUG_FS=y\nCONFIG_RELAY=y\nCONFIG_IRQ_WORK=y\n",
    );
    let report = EffectiveReport::compute(requested, eff);
    // DEBUG_FS was asked `n` and came back `y`: DROPPED. RELAY nobody named: ADDED.
    assert!(report.dropped.iter().any(|d| d.symbol == "DEBUG_FS"));
    assert!(report.added.iter().any(|a| a.symbol == "RELAY"));
    let enabled = report.effective.enabled();
    let why = t.explain_arrival("RELAY", &enabled);
    assert_eq!(why.len(), 1, "{why:?}");
    assert_eq!(why[0].by, "BLK_DEV_IO_TRACE");
    assert_eq!(why[0].file, "kernel/trace/Kconfig");
    // And the defeated veto is explained the same way.
    let why = t.explain_arrival("DEBUG_FS", &enabled);
    assert_eq!(why[0].by, "BLK_DEV_IO_TRACE");
    // A selector that is itself OFF does not explain anything.
    let off: BTreeSet<&str> = BTreeSet::from(["DEBUG_FS"]);
    assert!(t.explain_arrival("DEBUG_FS", &off).is_empty());
    // IRQ_WORK arrived two hops away: explained by RELAY, which is on.
    assert_eq!(t.explain_arrival("IRQ_WORK", &enabled)[0].by, "RELAY");
}

// ── the Kconfig-aware subtractor: invariant 2, transitively ─────────────────

/// **INVARIANT 2, TRANSITIVE — the new red.** The fragment names only
/// `STACKPROTECTOR_STRONG`. It `depends on STACKPROTECTOR`. A subtractor that
/// drops `STACKPROTECTOR` because "nothing reaches it" silently loses the
/// hardening symbol at `olddefconfig` — policy weakened by evidence, through one
/// hop of Kconfig. The bare `propose` does exactly that; the Kconfig-aware one
/// must not.
#[test]
fn a_dependency_of_a_hardening_symbol_is_immune_too() {
    let t = tree();
    let seeds = immune_from_fragments([("hardened.config", "CONFIG_STACKPROTECTOR_STRONG=y\n")]);
    let eff = Effective::from_produced_config(
        "# Linux/x86 6.12.104 Kernel Configuration\nCONFIG_HAVE_STACKPROTECTOR=y\nCONFIG_STACKPROTECTOR=y\n\
         CONFIG_STACKPROTECTOR_STRONG=y\nCONFIG_SLUB_TINY=y\nCONFIG_EXPERT=y\n",
    );
    let demand = Demand {
        symbols: BTreeSet::new(),
        scope: Scope::Whole,
        freshness_note: "test".into(),
    };
    let fresh = Freshness::Fresh {
        revision: "t".into(),
    };

    // The bare subtractor — the pre-existing behaviour — cuts the dependency.
    let bare = propose(
        &eff,
        &demand,
        &Policy {
            immune: seeds.clone(),
            report_only: false,
        },
        &fresh,
    );
    assert!(
        bare.subtractions
            .iter()
            .any(|s| s.symbol == "STACKPROTECTOR"),
        "this is the defect"
    );

    let sub = KconfigAwareSubtractor::new(&t, seeds, false);
    assert!(sub.immune().contains("STACKPROTECTOR"), "one hop");
    assert!(sub.immune().contains("HAVE_STACKPROTECTOR"), "two hops");
    assert!(!sub.immune().contains("SLUB_TINY"), "not related");
    let d = sub.propose(&eff, &demand, &fresh);
    let cut: Vec<&str> = d.subtractions.iter().map(|s| s.symbol.as_str()).collect();
    assert!(!cut.contains(&"STACKPROTECTOR"), "{cut:?}");
    assert!(!cut.contains(&"HAVE_STACKPROTECTOR"), "{cut:?}");
    assert!(
        cut.contains(&"SLUB_TINY"),
        "and it still subtracts what it may: {cut:?}"
    );
    assert!(d
        .spared
        .iter()
        .any(|s| s.symbol == "STACKPROTECTOR" && s.why.contains("IMMUNE")));
}

/// What policy `select`s is policy: SECCOMP_FILTER=y in the fragment implies
/// SECCOMP and NET (depends) — and a fragment's `select` targets likewise.
#[test]
fn what_policy_implies_is_policy() {
    let t = tree();
    let im = t.policy_closure(&set(&["SECCOMP_FILTER", "BLK_DEV_IO_TRACE"]));
    for s in [
        "SECCOMP_FILTER",
        "SECCOMP",
        "NET",
        "HAVE_ARCH_SECCOMP_FILTER",
        "HAVE_ARCH_SECCOMP",
        "NLATTR",
        "DEBUG_FS",
        "RELAY",
        "IRQ_WORK",
        "SYSFS",
        "BLOCK",
        "FTRACE",
    ] {
        assert!(im.contains(s), "{s} missing from {im:?}");
    }
    assert!(!im.contains("IO_URING"));
}

/// **Futility is reported, not hidden.** Subtracting `DEBUG_FS` while an enabled
/// survivor `select`s it is a directive `olddefconfig` will put straight back —
/// the exact 2026-09-07 defect. The subtractor says so and spares it; when the
/// selector is itself subtracted, the subtraction stands.
#[test]
fn a_subtraction_that_a_surviving_selector_would_undo_is_spared_and_says_who() {
    let t = tree();
    let eff = Effective::from_produced_config(
        "# Linux/x86 6.12.104 Kernel Configuration\nCONFIG_BLK_DEV_IO_TRACE=y\nCONFIG_DEBUG_FS=y\nCONFIG_RELAY=y\n",
    );
    let fresh = Freshness::Fresh {
        revision: "t".into(),
    };
    let sub = KconfigAwareSubtractor::new(&t, BTreeSet::new(), false);
    // The evidence says the code reaches BLK_DEV_IO_TRACE, so it survives.
    let demand = Demand {
        symbols: set(&["BLK_DEV_IO_TRACE"]),
        scope: Scope::Whole,
        freshness_note: "test".into(),
    };
    let d = sub.propose(&eff, &demand, &fresh);
    assert!(
        d.subtractions
            .iter()
            .all(|s| s.symbol != "DEBUG_FS" && s.symbol != "RELAY"),
        "{:?}",
        d.subtractions
    );
    let sp = d
        .spared
        .iter()
        .find(|s| s.symbol == "DEBUG_FS")
        .expect("spared");
    assert!(
        sp.why.contains("SELECTED BY")
            && sp.why.contains("BLK_DEV_IO_TRACE")
            && sp.why.contains("kernel/trace/Kconfig"),
        "{}",
        sp.why
    );
    // Nothing is demanded: the selector goes, and so may what it selected.
    let none = Demand {
        symbols: BTreeSet::new(),
        scope: Scope::Whole,
        freshness_note: "test".into(),
    };
    let d = sub.propose(&eff, &none, &fresh);
    let cut: Vec<&str> = d.subtractions.iter().map(|s| s.symbol.as_str()).collect();
    assert_eq!(cut, ["BLK_DEV_IO_TRACE", "DEBUG_FS", "RELAY"], "{cut:?}");
}

/// **INVARIANT 3 on the Kconfig side.** A graph read from one kernel says
/// nothing about a `.config` produced by another: release mismatch is STALE
/// and subtracts nothing.
#[test]
fn a_tree_from_another_kernel_release_is_stale_evidence() {
    let t = tree();
    let eff = Effective::from_produced_config(
        "# Linux/x86 6.6.1 Kernel Configuration\nCONFIG_SLUB_TINY=y\n",
    );
    assert_eq!(eff.release(), Some("6.6.1"));
    let sub = KconfigAwareSubtractor::new(&t, BTreeSet::new(), false);
    let demand = Demand {
        symbols: BTreeSet::new(),
        scope: Scope::Whole,
        freshness_note: "test".into(),
    };
    let d = sub.propose(
        &eff,
        &demand,
        &Freshness::Fresh {
            revision: "t".into(),
        },
    );
    assert!(d.subtractions.is_empty());
    let r = d.refused.expect("must say why");
    assert!(r.contains("6.6.1") && r.contains("6.12.104"), "{r}");
    // Same release: it works. No banner at all: the .config cannot be dated, and
    // that too is doubt.
    let same = Effective::from_produced_config(
        "# Linux/x86 6.12.104 Kernel Configuration\nCONFIG_SLUB_TINY=y\n",
    );
    assert!(!sub
        .propose(
            &same,
            &demand,
            &Freshness::Fresh {
                revision: "t".into()
            }
        )
        .subtractions
        .is_empty());
    let undated = Effective::from_produced_config("CONFIG_SLUB_TINY=y\n");
    assert!(sub
        .propose(
            &undated,
            &demand,
            &Freshness::Fresh {
                revision: "t".into()
            }
        )
        .refused
        .is_some());
}

// ── the syscall → CONFIG join ────────────────────────────────────────────────

#[test]
fn the_syscall_table_parses_and_names_its_file() {
    let t = SyscallTable::read(&mini(), "x86").unwrap();
    assert_eq!(t.source(), "arch/x86/entry/syscalls/syscall_64.tbl");
    assert!(t.contains("seccomp") && t.contains("bpf") && t.contains("io_uring_enter"));
    assert!(!t.contains("frobnicate"));
    assert_eq!(t.number("seccomp"), Some(317));
    assert!(t.len() >= 16, "{}", t.len());
    // Comment lines are not entries.
    assert!(!t.contains("The"));
}

/// The Makefile chain is a CONJUNCTION of guards down the directory path, and a
/// composite object (`landlock-y := … syscalls.o`, `$(mmu-y)`) resolves through
/// its parent.
#[test]
fn makefile_guards_compose_down_the_path_and_through_composites() {
    let root = mini();
    assert_eq!(
        object_guards(&root, "kernel/seccomp.o").unwrap(),
        set(&["SECCOMP"])
    );
    assert_eq!(
        object_guards(&root, "kernel/bpf/syscall.o").unwrap(),
        set(&["BPF", "BPF_SYSCALL"])
    );
    assert_eq!(
        object_guards(&root, "kernel/bpf/log.o").unwrap(),
        set(&["BPF", "BPF_SYSCALL"]),
        "a continuation line"
    );
    assert_eq!(
        object_guards(&root, "io_uring/io_uring.o").unwrap(),
        set(&["IO_URING"])
    );
    assert_eq!(
        object_guards(&root, "io_uring/register.o").unwrap(),
        set(&["IO_URING"]),
        "deep continuation"
    );
    assert_eq!(
        object_guards(&root, "fs/read_write.o").unwrap(),
        BTreeSet::new(),
        "obj-y is unconditional"
    );
    assert_eq!(
        object_guards(&root, "kernel/sys.o").unwrap(),
        BTreeSet::new()
    );
    assert_eq!(
        object_guards(&root, "security/landlock/syscalls.o").unwrap(),
        set(&["SECURITY_LANDLOCK"]),
        "composite"
    );
    assert_eq!(
        object_guards(&root, "fs/userfaultfd.o").unwrap(),
        set(&["USERFAULTFD"])
    );
    let e = object_guards(&root, "kernel/nowhere.o").unwrap_err();
    assert!(
        e.to_string().contains("nowhere.o") && e.to_string().contains("kernel/Makefile"),
        "{e}"
    );
    let e = object_guards(&root, "drivers/gpu/thing.o").unwrap_err();
    assert!(e.to_string().contains("Makefile"), "{e}");
}

/// `SYSCALL_DEFINEn(name, …)` is found across the tree, with the `#ifdef
/// CONFIG_X` nesting at the definition — the guard a Makefile cannot see.
#[test]
fn definitions_are_indexed_with_their_preprocessor_guards() {
    let idx = DefinitionIndex::scan(&mini()).unwrap();
    let s = idx.sites("seccomp");
    assert_eq!(s.len(), 1);
    assert_eq!(s[0].file, "kernel/seccomp.c");
    assert!(s[0].cpp_guards.is_empty());
    let s = idx.sites("setuid");
    assert_eq!(s[0].file, "kernel/sys.c");
    assert_eq!(
        s[0].cpp_guards,
        set(&["MULTIUSER"]),
        "the guard the Makefile cannot see"
    );
    assert!(
        idx.sites("getpid")[0].cpp_guards.is_empty(),
        "after the #endif"
    );
    assert_eq!(idx.sites("io_uring_enter")[0].file, "io_uring/io_uring.c");
    assert_eq!(idx.sites("read")[0].file, "fs/read_write.c");
    assert!(
        idx.sites("kexec_load").is_empty(),
        "no .c in the fixture defines it"
    );
    // COND_SYSCALL is a stub, not a definition.
    assert!(idx.sites("landlock_create_ruleset").is_empty());
    assert!(idx.len() >= 9, "{}", idx.len());
}

/// **derive > probe > table**, each answer tagged with the road it came by.
#[test]
fn the_join_prefers_derive_then_probe_then_the_checked_table() {
    let k = tree();
    let probes = vec![
        // A probe CONTRADICTING the source for seccomp: derive must still win.
        ProbedGuard {
            syscall: "seccomp".into(),
            config: "SOMETHING_ELSE".into(),
            evidence: "a bad probe".into(),
        },
        // kexec_load has no .c in the fixture; the probe should answer before the table.
        ProbedGuard {
            syscall: "kexec_load".into(),
            config: "KEXEC".into(),
            evidence: "ENOSYS on a KEXEC=n boot".into(),
        },
    ];
    let j = SyscallJoin::build(&mini(), "x86", &k, &probes).unwrap();

    match j.resolve("seccomp") {
        Resolution::Gated(g) => {
            assert_eq!(g.road, Road::Derived);
            assert_eq!(g.configs, set(&["SECCOMP"]));
            assert!(
                g.evidence.contains("kernel/seccomp.c") && g.evidence.contains("kernel/Makefile"),
                "{}",
                g.evidence
            );
        }
        other => panic!("{other:?}"),
    }
    match j.resolve("setuid") {
        Resolution::Gated(g) => {
            assert_eq!((g.road, g.configs), (Road::Derived, set(&["MULTIUSER"])))
        }
        other => panic!("{other:?}"),
    }
    match j.resolve("bpf") {
        Resolution::Gated(g) => assert_eq!(
            (g.road, g.configs),
            (Road::Derived, set(&["BPF", "BPF_SYSCALL"]))
        ),
        other => panic!("{other:?}"),
    }
    assert!(
        matches!(
            j.resolve("read"),
            Resolution::Unconditional {
                road: Road::Derived
            }
        ),
        "{:?}",
        j.resolve("read")
    );
    match j.resolve("kexec_load") {
        Resolution::Gated(g) => assert_eq!((g.road, g.configs), (Road::Probed, set(&["KEXEC"]))),
        other => panic!("{other:?}"),
    }
    match j.resolve("memfd_create") {
        Resolution::Gated(g) => {
            assert_eq!((g.road, g.configs), (Road::Table, set(&["MEMFD_CREATE"])))
        }
        other => panic!("{other:?}"),
    }
    // In the table, no road answers: Unknown, and it says so.
    assert!(matches!(
        j.resolve("epoll_pwait"),
        Resolution::Unknown { .. }
    ));
    // Not even in the syscall table.
    match j.resolve("frobnicate") {
        Resolution::Unknown { why } => assert!(
            why.contains("frobnicate") && why.contains("syscall_64.tbl"),
            "{why}"
        ),
        other => panic!("{other:?}"),
    }
    // The scope is every CONFIG any road can name.
    let scope = j.scope();
    for s in [
        "SECCOMP",
        "BPF_SYSCALL",
        "MULTIUSER",
        "KEXEC",
        "MEMFD_CREATE",
        "IO_URING",
    ] {
        assert!(scope.contains(s), "{s} missing from {scope:?}");
    }
    assert!(
        !scope.contains("SLUB_TINY"),
        "a syscall probe cannot see the allocator"
    );
}

/// **The table is checked against the tree, or it is not used.** An entry the
/// tree contradicts is REJECTED loudly; one the tree cannot judge is set aside;
/// only verified entries join. This is plan §2's condition on road 3.
#[test]
fn a_checked_table_entry_the_tree_contradicts_goes_red_instead_of_lying() {
    let k = tree();
    let t = SyscallTable::read(&mini(), "x86").unwrap();
    let idx = DefinitionIndex::scan(&mini()).unwrap();
    let entries = [
        TableEntry {
            syscall: "seccomp",
            file: "kernel/seccomp.c",
            config: Some("SECCOMP"),
        },
        TableEntry {
            syscall: "seccomp",
            file: "kernel/seccomp.c",
            config: Some("SECCOMP_FILTER"),
        }, // wrong guard
        TableEntry {
            syscall: "bpf",
            file: "kernel/bpf/syscall.c",
            config: Some("BPF_SYSCALL"),
        },
        TableEntry {
            syscall: "bpf",
            file: "kernel/bpf/syscall.c",
            config: Some("BPF_SYSCALL_OLD_NAME"),
        }, // no such symbol
        TableEntry {
            syscall: "frobnicate",
            file: "kernel/frob.c",
            config: None,
        }, // no such syscall
        TableEntry {
            syscall: "read",
            file: "fs/read_write.c",
            config: None,
        },
        TableEntry {
            syscall: "read",
            file: "fs/read_write.c",
            config: Some("BLOCK"),
        }, // claims a guard obj-y has not
        TableEntry {
            syscall: "seccomp",
            file: "kernel/bpf/syscall.c",
            config: Some("BPF_SYSCALL"),
        }, // wrong file: it exists, guard matches, no such definition
        TableEntry {
            syscall: "epoll_create1",
            file: "fs/eventpoll.c",
            config: Some("EPOLL"),
        },
        TableEntry {
            syscall: "acct",
            file: "kernel/acct.c",
            config: Some("BSD_PROCESS_ACCT"),
        }, // fixture Kconfig lacks it
    ];
    let c = check_table(&entries, &mini(), &t, &k, Some(&idx));
    let ok: Vec<&str> = c.accepted.iter().map(|g| g.syscall.as_str()).collect();
    assert_eq!(
        ok,
        ["seccomp", "bpf", "read", "epoll_create1"],
        "{:?}",
        c.accepted
    );
    let bad: Vec<&str> = c.rejected.iter().map(|(e, _)| e.as_str()).collect();
    assert_eq!(bad.len(), 5, "{:?}", c.rejected);
    assert!(
        c.rejected
            .iter()
            .any(|(e, why)| e.contains("SECCOMP_FILTER") && why.contains("kernel/Makefile")),
        "{:?}",
        c.rejected
    );
    assert!(
        c.rejected
            .iter()
            .any(|(e, why)| e.contains("BPF_SYSCALL_OLD_NAME")
                && why.contains("not among the guards")),
        "{:?}",
        c.rejected
    );
    assert!(
        c.rejected
            .iter()
            .any(|(e, why)| e.contains("frobnicate") && why.contains("syscall_64.tbl")),
        "{:?}",
        c.rejected
    );
    assert!(
        c.rejected
            .iter()
            .any(|(e, why)| e.contains("read") && why.contains("obj-y")),
        "{:?}",
        c.rejected
    );
    assert!(
        c.rejected
            .iter()
            .any(|(e, why)| e.starts_with("seccomp → kernel/bpf/syscall.c")
                && why.contains("SYSCALL_DEFINE")),
        "{:?}",
        c.rejected
    );
    // acct: the fixture has no kernel/acct.o rule and no BSD_PROCESS_ACCT — the
    // tree cannot judge it, which is different from contradicting it.
    assert_eq!(c.unverifiable.len(), 1, "{:?}", c.unverifiable);
    assert!(c.unverifiable[0].0.contains("acct"));
    // The real table on the fixture: nothing contradicted. (Complete verification
    // is the real-tree test's job; the fixture models only part of the kernel.)
    let real = check_table(CHECKED_TABLE, &mini(), &t, &k, Some(&idx));
    assert!(
        real.rejected.is_empty(),
        "the shipped table contradicts the fixture: {:?}",
        real.rejected
    );
    assert!(real.accepted.iter().any(|g| g.syscall == "memfd_create"));
}

// ── the first real EvidenceSource: a probe ───────────────────────────────────

fn tunnr_measured() -> BTreeSet<String> {
    // tunnr 961d534, `sandbox.rs` ALLOWED — the 21 syscalls measured under
    // strace on the real wg-init, plus `seccomp` from the fatal tier.
    set(&[
        "poll",
        "read",
        "write",
        "recvfrom",
        "sendto",
        "getrandom",
        "close",
        "fcntl",
        "futex",
        "pause",
        "mmap",
        "munmap",
        "mprotect",
        "brk",
        "rt_sigaction",
        "rt_sigprocmask",
        "sigaltstack",
        "prctl",
        "arch_prctl",
        "gettid",
        "getpid",
        "seccomp",
    ])
}

/// A probe's demand is the CONFIGs its observed syscalls are gated by, and its
/// scope is the CONFIGs a syscall can be gated by at all.
#[test]
fn a_probe_demands_what_its_syscalls_are_gated_by_and_nothing_else() {
    let k = tree();
    let j = SyscallJoin::build(&mini(), "x86", &k, &[]).unwrap();
    let probe = Probe {
        observed: set(&["read", "write", "seccomp", "setuid", "getpid", "prctl"]),
        revision: "abc123".into(),
        how: "strace -f".into(),
    };
    let ev = ProbeEvidence::new(probe, &j, "abc123");
    assert_eq!(
        ev.freshness(),
        Freshness::Fresh {
            revision: "abc123".into()
        }
    );
    let d = ev.demand("tunnr");
    assert_eq!(d.symbols, set(&["SECCOMP", "MULTIUSER"]), "{d:?}");
    match &d.scope {
        Scope::Only(s) => assert!(s.contains("BPF_SYSCALL") && !s.contains("SLUB_TINY")),
        Scope::Whole => panic!("a syscall probe is never whole-scope"),
    }
    assert!(d.freshness_note.contains("strace -f") && d.freshness_note.contains("abc123"));
}

/// **INVARIANT 3, three ways, on a real evidence source.** An observed syscall
/// the join cannot place is UNSOUND — the probe or the table is wrong and we do
/// not know which. A probe from another revision is STALE. No observations is
/// ABSENT. Each yields zero subtractions.
#[test]
fn a_probe_that_cannot_be_placed_or_dated_fails_closed() {
    let k = tree();
    let j = SyscallJoin::build(&mini(), "x86", &k, &[]).unwrap();
    let eff = Effective::from_produced_config(
        "# Linux/x86 6.12.104 Kernel Configuration\nCONFIG_BPF_SYSCALL=y\nCONFIG_SECCOMP=y\n",
    );
    let sub = KconfigAwareSubtractor::new(&k, BTreeSet::new(), false);

    let unsound = ProbeEvidence::new(
        Probe {
            observed: set(&["read", "frobnicate"]),
            revision: "r".into(),
            how: "t".into(),
        },
        &j,
        "r",
    );
    match unsound.freshness() {
        Freshness::Unsound { why } => assert!(why.contains("frobnicate"), "{why}"),
        f => panic!("{f:?}"),
    }
    let stale = ProbeEvidence::new(
        Probe {
            observed: set(&["read"]),
            revision: "old".into(),
            how: "t".into(),
        },
        &j,
        "new",
    );
    assert!(matches!(stale.freshness(), Freshness::Stale { .. }));
    let absent = ProbeEvidence::new(
        Probe {
            observed: BTreeSet::new(),
            revision: "r".into(),
            how: "t".into(),
        },
        &j,
        "r",
    );
    assert_eq!(absent.freshness(), Freshness::Absent);
    for ev in [&unsound, &stale, &absent] {
        let d = sub.propose(&eff, &ev.demand("x"), &ev.freshness());
        assert!(d.subtractions.is_empty() && d.refused.is_some(), "{d:?}");
    }
    // In the table but on no road (epoll_pwait): the honest answer is still "we
    // cannot place it", and that is unsound too.
    let unplaced = ProbeEvidence::new(
        Probe {
            observed: set(&["epoll_pwait"]),
            revision: "r".into(),
            how: "t".into(),
        },
        &j,
        "r",
    );
    assert!(matches!(unplaced.freshness(), Freshness::Unsound { .. }));
}

/// **The new red that made `Scope` exist.** A syscall probe knows nothing about
/// a NIC driver. Before `Scope`, fresh probe evidence subtracted every enabled
/// symbol it had not seen — which was everything. Out of scope is SPARED and
/// says so; in scope and unobserved is subtracted; observed is spared.
#[test]
fn a_syscall_probe_must_not_subtract_a_nic_driver_it_cannot_see() {
    let k = tree();
    let j = SyscallJoin::build(&mini(), "x86", &k, &[]).unwrap();
    let eff = Effective::from_produced_config(
        "# Linux/x86 6.12.104 Kernel Configuration\nCONFIG_SECCOMP=y\nCONFIG_BPF_SYSCALL=y\nCONFIG_BPF=y\n\
         CONFIG_MEMFD_CREATE=y\nCONFIG_E1000=y\nCONFIG_SLUB_TINY=y\nCONFIG_NET=y\nCONFIG_SECCOMP_FILTER=y\n",
    );
    let ev = ProbeEvidence::new(
        Probe {
            observed: tunnr_measured(),
            revision: "r".into(),
            how: "strace".into(),
        },
        &j,
        "r",
    );
    // arch_prctl IS in the fixture x86 table but no road places it: unsound.
    assert!(
        matches!(ev.freshness(), Freshness::Unsound { .. }),
        "{:?}",
        ev.freshness()
    );
    let mut observed = tunnr_measured();
    observed.remove("arch_prctl");
    let ev = ProbeEvidence::new(
        Probe {
            observed,
            revision: "r".into(),
            how: "strace".into(),
        },
        &j,
        "r",
    );
    assert_eq!(
        ev.freshness(),
        Freshness::Fresh {
            revision: "r".into()
        }
    );

    let seeds = immune_from_fragments([("hardened.config", "CONFIG_SECCOMP_FILTER=y\n")]);
    let sub = KconfigAwareSubtractor::new(&k, seeds, false);
    let d = sub.propose(&eff, &ev.demand("tunnr"), &ev.freshness());
    let cut: Vec<&str> = d.subtractions.iter().map(|s| s.symbol.as_str()).collect();
    assert_eq!(
        cut,
        ["BPF", "BPF_SYSCALL", "MEMFD_CREATE"],
        "in scope, unobserved, not policy: {cut:?}"
    );
    let spared = |sym: &str| {
        d.spared
            .iter()
            .find(|s| s.symbol == sym)
            .map(|s| s.why.clone())
            .unwrap_or_default()
    };
    assert!(
        spared("E1000").contains("OUT OF SCOPE"),
        "{}",
        spared("E1000")
    );
    assert!(spared("SLUB_TINY").contains("OUT OF SCOPE"));
    assert!(
        spared("SECCOMP").contains("IMMUNE"),
        "policy first: {}",
        spared("SECCOMP")
    );
    assert!(
        spared("NET").contains("IMMUNE"),
        "SECCOMP_FILTER depends on NET"
    );
    // BPF is `select`ed by BPF_SYSCALL — but BPF_SYSCALL is being cut too, so no
    // SURVIVOR selects BPF and its own subtraction stands.
    assert!(d.rows().iter().all(|r| !r.contains("E1000")));
}

/// Invariant 1 survives the new machinery: a `Demand` naming a symbol that is
/// OFF changes nothing.
#[test]
fn demand_for_a_vetoed_symbol_still_cannot_turn_it_on() {
    let k = tree();
    let eff = Effective::from_produced_config("# Linux/x86 6.12.104 Kernel Configuration\n# CONFIG_BPF_SYSCALL is not set\nCONFIG_SECCOMP=y\n");
    let sub = KconfigAwareSubtractor::new(&k, BTreeSet::new(), false);
    let demand = Demand {
        symbols: set(&["BPF_SYSCALL"]),
        scope: Scope::Whole,
        freshness_note: "t".into(),
    };
    let d = sub.propose(
        &eff,
        &demand,
        &Freshness::Fresh {
            revision: "t".into(),
        },
    );
    assert!(d
        .subtractions
        .iter()
        .chain(d.spared.iter())
        .all(|s| s.symbol != "BPF_SYSCALL"));
}