Kernel tooling v2: the design as built
This is the current design of the nordisk kernel tooling, as the code on main has it on 2026-11-04. It supersedes ../KERNEL-TOOLING-MAP.md, the read-through of 2026-11-03 that found the gaps. That document is kept as history. Every claim here names the file that does it. Paths are repo-relative, and the repo is named first: edda, tunnr, heimdall, dwarves, nornir or holger. Planned items are listed only in the short "Next" section at the end.
1. What it is for
The tooling builds the smallest kernel a row can boot. A row is one appliance medium. The kernel must still carry everything the row's programs need, and every CONFIG_X=n must be backed by evidence. The evidence comes from four places:
| evidence | question it answers | who writes it |
| measured syscalls | which calls the programs made while a workload ran | Heimdall tooling (the heimdall probe, run by Build Thing's measure step) |
| possible syscalls | which calls the code can reach at all (the max) | Mímir (mimir-room possible) |
| kernel coverage | which CONFIG_* code ran while the row's suite ran |
Build Thing (a gcov probe kernel under KVM) |
| runtime needs | which kconfig lines the Heimdall runner needs | Heimdall (heimdall kernel needs --json) |
Build Thing joins these files to the kernel tree. It then decides what the row's kernel must keep, and what a veto may drop.
2. The three homes
The tooling lives in three homes, and the homes share nothing but files. Each file has a schema and a validator. No home links another home's code. Build Thing is MIT and never links Heimdall, which is AGPL. Mímir's walk is the owner's engine, and no lane writes a second one.
| home | repo, crates | owns |
| Build Thing | edda crates/nornir-build-thing (the engine), <repo>-build-thing per product (tunnr crates/tunnr-build-thing), dwarves buildthing |
kernel rows, fragments, pins, the ledger key, compose and build in a container, what-if, candidates, coverage, vetoes and their receipts, the syscall→CONFIG join, requires rows from runtime needs, the measure step, kernel facts for AI |
| Heimdall tooling | heimdall (heimdall profile probe/gen/check, heimdall kernel needs), dwarves tune and dwarves-tune-graph, edda measure.rs (it runs the heimdall binary) |
measured syscalls, seccomp profiles, approval, drift, the runner's kernel needs, the graph cross-check |
| Mímir | nornir crates/{mimir-driver,mimir-ingest,mimir-room} |
the code graph of a program and its whole dependency universe; the walk to the syscall boundary; graph.json (the possible set) |
The files all have one definition, in the MIT crate nordisk-kernel-evidence (edda crates/nordisk-kernel-evidence; see section 8).
3. Every schema'd file
| file | schema | writer | readers |
.nornir/build-variants.toml [[build.kernel]], [[build.installer]] (kernel_donor, requires) |
the Build Thing recipe schema (edda src/schema.rs) |
<repo>-build-thing emit (edda src/emit.rs) |
edda variant.rs BuildVariantSet::load_file, kernel_facts |
.nornir/profiles/<row>/measure.toml |
hand-written manifest (programs, workloads, [program.vm] legs) |
the row's owner | edda measure.rs MeasureManifest::load |
.nornir/profiles/<row>/measure.json + <program>/<arch>-<libc>.{probe.json,json,oci.json} |
the measure step's evidence; Heimdall's probe JSON | measure syscalls <row> (edda measure.rs, runs heimdall profile probe) |
edda syscall_join.rs MeasuredRow::load, dwarves tune, dwarves-tune-graph --profile |
.nornir/profiles/<row>/<program>/graph.json |
mimir.possible-syscalls/1 (dwarves-tune-graph Graph plus the source graph's fields) |
nornir mimir-room <lake> possible --bin … --out (crates/mimir-room/src/possible.rs) |
edda syscall_join.rs load_possible (through Graph::parse), dwarves-tune-graph --graph, dwarves tune show |
.nornir/profiles/<row>/kernel-needs.json |
nornir.kernel-needs/1 |
kernel needs <row> --save (edda kernel_trim.rs → KernelNeeds::save) |
dwarves tune show (tune/review.rs), edda kernel_facts |
.nornir/kernel-runtime-needs/<name>/heimdall.json |
nornir.kernel-runtime-needs/1 |
heimdall kernel needs --json (heimdall crates/heimdall-cli/src/main.rs, crates/heimdall-container/src/kernel.rs) |
kernel requires-from-needs (edda kernel_requires/from_needs.rs), tunnr tests heimdall_needs_rows.rs, kernel_facts |
.nornir/kernel-coverage/<row>/<sha12>.json |
nornir.kernel-coverage/1 |
kernel coverage <row> --run <suite> (edda kernel_trim/veto.rs run_coverage → coverage::store) |
kernel candidates, kernel veto (coverage::load_for), kernel_facts |
.nornir/kernel-vetoes/<row>/protect.list |
PATTERN reason lines |
the row's owner | coverage::ProtectList::load |
.nornir/kernel-vetoes/<row>/receipts/<date>-<CONFIG_X>.json |
nornir.kernel-veto/1 |
kernel veto <row> CONFIG_X (edda kernel_trim/veto.rs run_veto) |
veto::check_list (the build), kernel vetoes, kernel_facts |
the row's veto list: .nornir/kernel-vetoes/<row>/vetoes.list (RowDirVetoStore), or tunnr's crates/tunnr-build-thing/kernel-vetoes/<row>.list (DirVetoStore) |
CONFIG_X <receipt> <reason> lines (veto::parse_vetoes) |
kernel veto |
the recipe (veto_requirements → =n requires), veto::check_list |
.kernel-out/<name>/{bzImage,kernel.config,<name>.kernel-pin.json,kernel.why.toml} |
the pin and the why index | the kernel build (edda kernel.rs) |
the installer (resolve_kernel_for), the candidates verb (kernel.why.toml) |
| ledger key of a built kernel | nornir.kernel-ledger-key/2 |
edda kernel.rs ledger_key (LedgerKeyInputs::key) |
dwarves kernel_ledger.rs |
nornir.kernel-facts/1 (stdout, MCP, the AI pack) |
nornir.kernel-facts/1 |
edda kernel_facts.rs gather |
nornir MCP kernel_facts, nornir docs ai-context |
4. Data flow
Mímir (nornir) Heimdall tooling Heimdall runtime
──────────────── ───────────────── ────────────────
consume (driver dumps every crate, measure.toml heimdall kernel needs --json
registry crates from a crates.io │ measure syscalls <row> │
mirror: MIMIR_CRATE_MIRROR) │ (edda measure.rs runs heimdall) ▼
│ ▼ .nornir/kernel-runtime-needs/
▼ .nornir/profiles/<row>/ <name>/heimdall.json
lake ──► mimir-room possible ──► measure.json, <prog>/*.probe.json │
graph.json ──────► <prog>/graph.json │ kernel requires-from-needs
│ │ ▼
│ └► dwarves-tune-graph [[build.installer.requires]]
│ --graph --profile (generated rows)
▼ (cross-check) │
Build Thing (edda) syscall_join (measured ⊆ possible) │
─────────────────── │ kernel needs <row> --save │
[[build.kernel]] + preset + profile ▼ │
+ requires (consumers, vetoes =n) ──► kernel-needs.json ──► dwarves tune show │
merged fragment ◄──────────────────────────────────────────────────────────────┘
│ container build (draupnir), pin, ledger key
▼
.kernel-out/<name>/bzImage ──► installer medium ──► kernel-suite.sh (KVM, gcov probe)
│ kernel coverage <row> --run <suite>
▼
.nornir/kernel-coverage/<row>/<sha12>.json
│ kernel candidates / kernel veto CONFIG_X
▼
receipt (nornir.kernel-veto/1) + veto list line
│ veto::check_list (every verb, the build)
▼
=n requires in the next merged fragment
every file above ──► kernel_facts::gather ──► nornir.kernel-facts/1 ──► MCP kernel_facts, docs/ai-context.md
5. The verbs
| verb | door | code |
kernel what-if <row> --drop CONFIG_X … [--measure] [--json] |
tunnr tunnr-build-thing |
edda kernel_trim.rs run_kernel_verb_in |
kernel candidates <row> [--top N] [--json] |
tunnr | the same; with coverage it marks each candidate veto-ready or not (veto::annotate) |
kernel needs <row> [--json] [--save] |
tunnr | syscall_join::needs, KernelNeeds::save |
kernel coverage <row> --run <suite> [--suite-script <file>] |
tunnr | veto::run_coverage (ScriptSuite runs the row's scripts/kernel-suite.sh) |
kernel veto <row> CONFIG_X --run <suite> |
tunnr | veto::run_veto |
kernel vetoes <row> |
tunnr | veto::render_receipts_checked (BACKED or UNBACKED per line) |
kernel requires-from-needs <row> --needs <file> [--edge kernel] |
tunnr | kernel_requires/from_needs.rs |
| `kernel-probe [--repo ] [--probe gcov | off]` | tunnr |
preflight [<repo-root>] [--strict] |
tunnr | tunnr src/main.rs (pins, the veto lists through check_row_vetoes) |
| `measure syscalls [--check | --gen], measure gate [--row ]… [--build]` |
tunnr, dwarves tune |
| `tune probe | gen | check |
dwarves-tune-graph --graph <graph.json> --profile <probe.json> [--json] |
dwarves | crates/dwarves-tune-graph/src/xcheck.rs |
| `heimdall profile probe | gen | check ` |
heimdall kernel needs [--json] [--mode strict] |
heimdall | heimdall crates/heimdall-cli/src/main.rs kernel_needs, heimdall-container/src/kernel.rs strict |
mimir-room <lake> possible [ENTRY…] [--bin NAME] [--target T] [--out F] |
nornir | crates/mimir-room/src/possible.rs |
consume <repo> <url> <branch> <state> <dump> <nightly> [--std] [--boundary T], the code room consumer |
nornir | crates/mimir-ingest/examples/consume.rs, src/code_room/consumer_loop.rs |
MCP kernel_facts <repo>, nornir docs ai-context <repo> |
nornir | src/mcp_stdio.rs, src/bin/nornir.rs run_docs_ai_context |
6. The veto and receipt rule
A veto is one row-level CONFIG_X=n, applied by one run of kernel veto <row> CONFIG_X (edda kernel_trim/veto.rs run_veto). The run goes in this order:
- The symbol must be a candidate the static what-if calls clean. A drop that takes away a measured call's option is refused by name (
syscall_join::vetoes_for). So is a drop that takes away a possible call's option (possible_vetoes_for). - The coverage on the row's current fragment must say the symbol is never executed. It must also not be protected:
protect.list, and the probe's own symbols (PROBE_ALTERED). - The vetoed kernel is composed, and the inverse check must hold.
- The row's suite runs again on the vetoed kernel. A green rerun appends the list line and writes an applied receipt. A red rerun writes a refused receipt and leaves the list unchanged.
The build trusts the list only through its receipts. veto::check_list runs before every kernel verb except vetoes and requires-from-needs. tunnr's preflight and kernel-probe run it too, through check_row_vetoes. Each line must name an applied receipt for its row and symbol. That receipt's before and after fragments must be this row's merged fragment without the line and with it. Otherwise the line is refused by name: [veto-no-receipt], [veto-not-applied], [veto-receipt-mismatch] or [veto-stale]. A --repo veto lands in the other repo's .nornir/kernel-vetoes/<row>/vetoes.list (RowDirVetoStore). Both the list and its receipts are committed.
7. Possible versus measured
| set | from | meaning |
| possible | graph.json (Mímir) |
the max: every call the program's code can reach, its dependencies included |
| measured | *.probe.json (Heimdall) |
what a run made: a subset of what is possible |
The rules are in edda syscall_join.rs needs. They are shown by dwarves tune show and dwarves-tune-graph:
- Measured ⊆ possible. A measured call outside the possible set is listed as
outside_possible. That is a graph gap to review, because the probe is the truth. - Possible but not measured is a call no test drove. Its options are listed with the class
possible, unexercised, and the path to it is given. - A veto may not drop an option a possible call needs (
possible_vetoes_for). That holds even when no run made the call. - A generated seccomp profile holds only what was measured. The graph never widens a filter (dwarves-tune-graph's rule).
Mímir widens rather than drops. An unresolved edge (dyn dispatch, a call through a value, a fn pointer) is widened to every target the room holds, and it is listed in unresolved_sites. So the max stays a max (nornir docs/v2/mimir/README.md §10.2). The dependency crates come from their exact sources. With MIMIR_CRATE_MIRROR=sparse+<holger>/…, the dumper's cargo takes them from Holger's crates.io mirror through its normal sparse registry door (nornir crates/mimir-ingest/src/crate_mirror.rs).
8. The evidence crate
edda crates/nordisk-kernel-evidence, licence MIT, depends only on serde, serde_json, anyhow and nornir-hash. It lives in edda because edda, tunnr and dwarves already depend on edda by path. That avoids a new repo and a new release line. Heimdall takes it from crates.io, since it is MIT and may be used under the AGPL.
| module | types | validator |
needs |
KernelNeeds, CallNeed, Caller, OptionNeed, PossibleNeed, NoCaller, kernel_needs_path, runs_note |
KernelNeeds::validate(row), save, load |
runtime_needs |
RuntimeNeeds, RuntimeNeed, NeedOp, NeededBy |
RuntimeNeeds::validate, parse, read, render |
graph |
Graph, ModuleInfo, Reach, Site (with the source graph's optional format, uncertain, callee, reason, targets, reached_functions) |
Graph::validate, parse |
coverage |
CoverageReport, Totals, SymbolCoverage, Status, StageResult, NotRun, KernelProbe, GCOV_PROBE_FRAGMENT |
CoverageReport::validate (totals are the symbols' counts) |
veto |
VetoReceipt, VetoVerdict |
VetoReceipt::validate (an applied veto had a green rerun) |
ledger |
LedgerKeyInputs, KERNEL_LEDGER_KEY_SCHEMA |
validate_ledger_key |
roundtrip |
rewrite, check |
a file writes back byte for byte |
These users have no copy of their own:
- edda
nornir-build-thingre-exports the crate's types under its old paths (syscall_join,kernel_trim::{coverage,veto},kernel_requires::from_needs,kernel::KERNEL_LEDGER_KEY_SCHEMA). - dwarves
dwarves-tune-graphre-exportsGraph,ModuleInfo,ReachandSite. - tunnr
tunnr-build-thingreads the probe word, receipts and runtime needs through it. - heimdall
heimdall-containerre-exportsRuntimeNeedsunder its own names. This is on branchktool/evidence-crate, which waits for the crate on crates.io and for Heimdall 0.3.2.
The round-trip tests use real files:
- edda
crates/nordisk-kernel-evidence/tests/real_files.rscovers holger's kernel-needs, tunnr's coverage and receipt, heimdall's runtime needs, and a cut of holger's graph.json. - tunnr
tests/evidence_files_round_trip.rscovers every committed coverage report, receipt and runtime needs file.
9. AI visibility
edda crates/nornir-build-thing/src/kernel_facts.rs gather(repo_root) reads every file in section 3 through its own reader. The result is one nornir.kernel-facts/1 document with a validator: kernel rows, and per row the saved needs, each program's measured and possible syscalls, coverage totals, the veto list with BACKED/UNBACKED per receipt, every receipt, and the protect list; plus Heimdall's runtime needs. A broken file fails with its path. Nothing in the document is typed by hand.
It reaches AI in two ways:
- nornir MCP
kernel_facts <repo>(src/mcp_stdio.rs) returns the JSON document. It is covered by the MCP matrix harness. nornir docs ai-context <repo>writesdocs/ai-context.md, which carries a "Kernel evidence" section. nornir injectskernel_facts::ai_sectioninto nornir-docgen'sCtx::with_kernel_facts, so the docgen leaf links no build engine.
The public site's llms.txt lists each repository's README and file tree, and the committed docs/ai-context.md is in that tree.
Example: tunnr, cargo run -p nornir-build-thing --example kernel_facts -- <tunnr>, cut:
## Kernel evidence
### Kernel rows
| kernel | version | arch | profile | preset | toolchain | solver | lto |
|---|---|---|---|---|---|---|---|
| tunnr-kernel-k8s | 6.12.104 | x86_64 | k8s-hardened | slim | gcc | - | false |
…
### Row `rusternetes-node` (kernel tunnr/tunnr-kernel-k8s)
Needs: no saved `kernel-needs.json` (`kernel needs rusternetes-node --save`).
- program node-pid1 (x86_64 musl): measured 17: arch_prctl brk close exit_group fcntl getdents64 …
possible: no graph.json
- coverage `.nornir/kernel-coverage/rusternetes-node/58a245df9d77.json`: tunnr-kernel-k8s 6.12.104 suite bridge GREEN on 2026-11-03T14:29:51Z: 993 symbols, 311 executed, 54 never executed, 628 unmeasured; 10 checks not run; fragment 58a245df9d77
- veto CONFIG_MOUSE_PS2_BYD BACKED (from build-variants.toml, receipt 2026-11-03T144637Z-CONFIG_MOUSE_PS2_BYD.json): never executed under suite bridge (65536 B bzImage)
- receipt 2026-11-03T144637Z-CONFIG_MOUSE_PS2_BYD.json: CONFIG_MOUSE_PS2_BYD applied on suite bridge, bzImage -65536 B, fragment after 333150ec4493, 10 checks not run
- protect.list: 47 pattern(s) never offered as veto candidates
### Runtime needs `.nornir/kernel-runtime-needs/tunnr-kernel-sandbox/heimdall.json` (heimdall 0.3.1, mode strict)
- CONFIG_SECCOMP=y (strict seccomp_filter: Heimdall strict: every job runs under a seccomp filter …)
- CONFIG_LSM∋landlock (strict landlock: Heimdall strict: Landlock must be in the LSM boot order by name …)
…
10. Node profiles and Landlock (tunnr)
Measured profiles. The rusternetes-node and rusternetes-node-aya rows pack measured seccomp profiles for every program that a probe can run under: Rusternetes, the youki shim's wait phase, the CSI node plugin and tunnr-cni. Each one is a VM leg of its row ([program.vm] how + approved in tunnr .nornir/profiles/<row>/measure.toml, read by edda measure.rs). measure syscalls <row> --check holds each leg's KVM evidence to the approved file the row packs. scripts/kernel-suite.sh <row> <suite> heimdall <out> composes the DEV probe medium (--opt heimdall-probe=on). It cuts the evidence after the suite (scripts/heimdall-evidence.py) and prints STAGE heimdall-<program> PASS|FAIL against the packed profile.
The floor stays. korp-start loads the hand-written profile (rusternetes-node/approved-x86_64.oci.json, the 361-call floor) onto the node's PID 1 and execs it. No probe can measure either of them. Each must be PID 1, and the kernel allows one seccomp listener per process tree. The floor is also the ceiling of the whole tree, pods included: privileged and Unconfined pods start below it through the shim door. tunnr crates/tunnr-build-thing/tests/measured_profiles.rs holds the floor above every measured profile of its tree.
Landlock. Rusternetes takes no Landlock domain (payloads/rusternetes-node/src/landlock.rs RUSTERNETES). The kubelet opens every pod's /proc/<pid>/ns and kills pods through libcontainer. A Landlock domain may not reach a process outside it (the ptrace rule), and the pods start outside Rusternetes. The pods take the domain instead. PID 1 starts the shim keeper (tunnr-shim-keeper, src/door.rs) under landlock::pods_rules. These rules scope signals and abstract unix sockets, and they handle neither TCP nor the filesystem. The keeper starts every shim, so all pods share ONE domain. A pod can no longer signal or ptrace PID 1, Rusternetes, the CSI plugin or the signer, nor reach their abstract sockets. Containers of the same pod still can reach each other. Every suite holds both serial logs to the filter and Landlock self-tests and to no SIGSYS kill (STAGE filter-selftest, landlock-selftest, no-sigsys). The aya suite also runs the NetworkPolicy cases (STAGE netpol).
Next (planned, not built)
- Holger's
rust-remotedownload base from the index'sconfig.jsondl(holger branchfix/rust-remote-dl, to be merged by Holger's lane). With it, the Mímir step 3 proof runs against Holger's/cache. - Heimdall on the evidence crate (branch
ktool/evidence-crate), afternordisk-kernel-evidence0.1.0 is on crates.io and Heimdall 0.3.2 is out. - A
graph.jsonbeside each measured program of tunnr's rows (the mimir-sys bench step 5 onrusternetes-nodeandtunnr-cni). Holger's scan worker keeps its graph gzipped under.nornir/kernel-usage/, sosyscall_joindoes not read it yet. - One
.probe.jsonreader in the evidence crate. Heimdall, edda and dwarves each still parse the probe by field name.