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

nordisk/heimdall

docs/v2/kernel-tooling.md

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:

  1. 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).
  2. 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).
  3. The vetoed kernel is composed, and the inverse check must hold.
  4. 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:

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:

The round-trip tests use real files:

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:

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)