Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-086 — Umbrella “EL0 isolated” checklist audit after B2-P (gap list + milestone plan)

  • Status: Accepted (docs / audit). The umbrella “EL0 isolated” (P-SEC-3l) stays Planned / non-claim. This ADR audits every ADR-055 / ADR-060 checklist item against evidence gathered this session (2026-09-26). It publishes a measured TTBR0 identity inventory, a gap list, and a milestone plan (one milestone per branch/PR). It mints no new Verified for the umbrella, taken SError on the accepted smoke machine, or full identity teardown.
  • Follow-on (2026-09-28, M6): B3 met: the sponsor accepted the ADR-091 revision-3 sentence and scope in writing (2026-09-28 16:41 CEST). ADR-091 is Accepted. “EL0 isolated” is Verified only as that sentence, with its limits. Text in this ADR that says the umbrella is Planned / non-claim is as of this ADR’s date.
  • Date: 2026-09-26
  • Trigger: sponsor relay (DSO, 2026-09-26 13:55 CEST): item 2 (umbrella EL0 isolated) is unlocked now that B2 has a result (ADR-085). “Claim it ONLY when every checklist item is Verified by a probe from this session.”
  • Base: stacked on PR #137 (915796f). #136 / #137 are not merged at the time of writing, and main @ 4ea871e has neither.
  • Numbering: next free after ADR-085.

Context

ADR-055 §B names three Unmet rows that block the umbrella: (1) taken lower-EL SError while standing, (2) full identity teardown including yank _start, (3) a written sponsor accept that the umbrella sentence is in scope. ADR-060 is the sponsor one-pager of the same locks. B1 (ADR-083) and B2-P (ADR-085) have since produced a taken el0: serror, but only on opt-in paths. The identity tears ADR-018…038 plus ADR-049 landed, while _start stays (ADR-042 / ADR-047 §3, ident: start-stay).

This ADR answers: which checklist items are Verified today, which are not, and what exactly is left.

Evidence gathered this session

ProbeWhen (CEST)WhatResult
Agent-box default smoke2026-09-26 13:59–14:04scripts/qemu-smoke.sh on 915796f, stock QEMU 10.0.13 (Debian), virt -cpu cortex-a76 TCG, rustc 1.100.0-nightly 0fc141305 (repo pin; corrected in ADR-087 — this line first said 5ceaf6608, the box’s +nightly)qemu-smoke: ok; force-fail exit 1 (fail-closed ok). All §A markers below present. inject-nmi=>error:machine does not provide NMIs → el0: serror-park.
GHA PR run 36239619910 (+ push run 36239617837)2026-09-26 13:41–13:53smoke.yml on 915796f: ubuntu-24.04-arm + ubuntu-24.04 default jobs; opt-in qemu-nmi-pin (B1)All success. Default: park. qemu-nmi-pin job 108397549350: inject-nmi=>ok → bare el0: serror → qemu-smoke: taken 'el0: serror' present (ADR-083 pinned QEMU).
B2-P run 4 (ADR-085)2026-09-26 13:26–13:33AWS c7g.metal KVM, opt-in b2-serror profile, one-off paidel0: serror / b2: taken / b2-kvm-smoke: PASS. Not CI.
Scratch TTBR0 inventory (not committed to src/)2026-09-26 ~14:10915796f + scratch walker patch: walks kernel and user TTBR0 L1→L3, coalesces leaves, prints just before Hello World! (after every tear). Same QEMU/rustc; smoke rc=0Output. Five identity (VA = PA) ranges remain in kernel TTBR0 and two in user TTBR0. See §Inventory.

Inventory: what is still identity-mapped after every tear

Kernel TTBR0 (the hello boot; the other two boots in the smoke show the same classes, with boundaries shifted by text size):

#RangeSizeAttrsWhat it isStatus vs “full identity teardown”
I10x0000_0000–0x4000_00001 GiB L1 blockDevice-nGnRnE, EL1 RW, PXN+UXNMMIO identity: GIC 0x0800_0000, PL011 0x0900_0000, virtio-mmio 0x0a00_0000 (all accessed through identity VAs)Not torn. Needs high MMIO alias in TTBR1 first.
I20x4000_0000–0x4008_0000512 KiBNormal, EL1 RW, XNRAM below the image (QEMU DTB hole). ctos never reads the DTB (frame.rs: “never handed out”)Not torn. Nothing uses it.
I30x4008_0000–0x4008_10001 pageNormal, EL1 RO+XBoot stub page: _start + exception_vectors (identity copy; VBAR is high)Stays by decision (ident: start-stay, ADR-042 / ADR-047 §3).
I4[.ident_tear end, 0x4020_1000)≈ 1.06 MiB (e.g. 0x400f1000–0x40201000)Normal, EL1 RO+XLinker padding between the last torn RO section and .data (pinned at 0x4020_1000). No live code.Not torn. The ADR-019/020/025 tears cover .text after the stub, .rodata, and .ident_tear only.
I5__kernel_end page (0x4025d000)1 pageNormal, EL1 RW, XNThe page between __fatal_stack_top / __kernel_end and the heap (ident: heap lo=0x4025e000)Not torn. Falls between the ADR-037 data tear (hi=__kernel_end) and the ADR-038 heap tear.

User TTBR0 (what EL0 runs on; all EL1-only, el0=false): I3 (boot stub, RO+X) and I4 (padding tail, RO+X). Kernel .text/.rodata/.data/heap/RAM are already absent from user TTBR0.

Everything else (live .text after the stub, .rodata, .ident_tear, .data/.bss/stacks, heap, and the frame pool up to 0x4800_0000) is torn, which matches the Verified tear rows. The inventory is a scratch probe: it is Documented evidence for this audit, not a committed fail-closed ratchet. That ratchet is milestone M1.

Checklist audit

Status words: Verified = backed by a probe from this session (box and/or CI above). CI-gated = scripts/qemu-smoke.sh fails closed without the marker on the default GHA jobs. Note that main has no branch protection (GET /branches/main/protection → 404), so “CI-gated” means the sponsor does not merge red. No check is GitHub-required.

ADR-055 §A — Verified miles (necessary, not sufficient)

#ItemStatusProbe / marker (this session)RecordedCI-gated?
A1EL0 first mile + NX kernel dataVerifiedel0: ok, el0: nx kernelLedger ADR-013 rows; box 14:04 + run 36239619910Yes
A2User TTBR0 omits kernel dataVerifiedel0: no kernel read (ADR-055 lists el0: no data, which is stale wording: the code and smoke use el0: no kernel read)Ledger ADR-013Yes
A3Standing EL0 + standing taskVerifiedel0: standing, el0: restored, el0: task-enter/active/exit/restored, el0: task-okLedger ADR-013/024Yes
A4ASID isolationVerifiedasid: dual / asid: conflict / asid: okLedger ASID rowYes
A5TTBR1 private page + EL1 high fetchVerifiedttbr1: el1 / ttbr1: no el0 / ttbr1: ok, ttbr1: el1 exec / ttbr1: vbarLedger ADR-016/017Yes
A6Identity tears text / rodata / data / heap / RAMVerified (as specific tears)ident: live/range/text, ident: rodata(+-fault/-high), ident: data(+…), ident: heap(+…), ident: ram(+-fault/-high), ident: ok; smoke rejects ident: heap-stay / ident: ram-stayLedger ADR-018…020/025/037/038/049Yes
A7EL0 entry without TLBI VMALLE1Verifiedel0: no-vmalle1 + smoke greps src/exception.rs for no tlbi vmalle1Ledger ADR-039Yes
A8Lower-EL IRQ while standing + default I clearVerifiedel0: irq, el0: irq-defaultLedger ADR-040/041Yes
A9Lower-EL FIQ while standingVerifiedel0: fiqLedger ADR-043Yes
A10SError park honesty (default machine)Verifiedel0: serror-arm → inject-nmi=>error:machine does not provide NMIs → el0: serror-parkLedger ADR-043/045/053Yes
A11PAN ID fieldVerifiedpan: id= / pan: present (cortex-a76)Ledger ADR-079Yes
A12PAN enable + EL1-vs-EL0 faultVerifiedpan: enabled, pan: el1-faultLedger ADR-080Yes
A13_start stay honestyVerified (honesty; not a teardown)ident: start-stay lo=0x40080000 hi=0x40081000Ledger ADR-042Yes

ADR-055 §B / ADR-060 — umbrella blockers

#ItemStatusEvidenceHonest readingCI-gated?
B1Taken lower-EL SError while standingUnmet as worded (Verified only on two opt-in, non-default paths)Default machine: park (A10, this session). B1 pin: GHA qemu-nmi-pin job 108397549350 (this session) inject-nmi=>ok → bare el0: serror. B2-P: ADR-085 run 4 (this session) el0: serror under KVM.ADR-060 requires “honest host inject that delivers async SError on the accepted smoke machine; serial el0: serror under EXPECT”. The accepted smoke machine is still stock distro QEMU virt TCG, which parks. B1 is CI-runnable but uses a locally patched emulator (0001 TYPE_NMI→SError). Its job runs only while repo variable CTOS_BUILD_QEMU_NMI=1, so it is opt-in, not a gate. B2-P is the only real-hardware source (KVM serror_pending), but it was one paid run on the b2 profile (no GIC/timer/virtio/FAT) and is not CI-runnable without paid metal or a self-hosted arm64 KVM runner. Neither meets the row as worded. It can count only if the sponsor redefines the accepted evidence (decision D1).Row wording implies yes (default smoke + EXPECT). B1 is CI but opt-in. B2-P: no.
B2Full identity teardown including yank _startUnmet, and unmeetable as wordedScratch inventory: I1, I2, I4, I5 still identity-mapped in kernel TTBR0; I3 + I4 in user TTBR0.The row demands yanking _start, while ADR-047 §3 decides “never yank _start” and the sponsor repeated “never yank _start at 0x4008_0000”. As written the row cannot be met. Decision D2 must redefine it. Independently of _start, four other identity ranges (I1, I2, I4, I5) are not torn, so even “full teardown except the stub” is Unmet today.Needs a new fail-closed ratchet (M1). None exists today.
B3Written sponsor accept that the umbrella sentence is in scopeUnmet (intent Documented)DSO relays 2026-09-25 23:44 (“Go with B2 to unlock 1 and 2”) and 2026-09-26 13:55 (“item 2 unlocked … claim ONLY when every item Verified”)This is direction, not an ADR-048/052-style accept that names the exact sentence and its scope (machine, CPU, profile). Decision D4.n/a (docs)
B4PAN enable (ADR-060 row)Verified= A12Met (ADR-054 gates 1–4 via ADR-079/080).Yes
B5Yank _start (ADR-060 row)Decided: never (not a Verified target)ident: start-stayConflicts with B2 wording. See D2.Yes (stay marker)
B6Guest Linux / containers / immutable-OS marketingNon-goalADR-029 / ADR-036Out of scope. Not required for the umbrella, and not unlocked by it.n/a

Roadmap P-SEC-3l also paraphrases the umbrella as “standing + PAN + full TTBR1 / identity teardown”: standing (A3) and PAN (A12) are Verified. “Full TTBR1 / identity teardown” = B2 (Unmet).

Bottom line: 13 of 13 §A miles plus PAN enable are Verified this session. All three §B blockers are not Met. The umbrella stays Planned / non-claim.

Gap list (ordered; identity teardown first)

GapWhatSizeNeeds sponsor?
G1No committed, fail-closed identity inventory ratchet. Today “what is left” is a scratch probe.Small: walker + allowlist + smoke line + #[test_case]No
G2I2 low RAM 0x4000_0000–0x4008_0000 (DTB hole) identity RWSmall (unused range)No
G3I4 padding tail [.ident_tear end, 0x4020_1000) identity RO+X in kernel and user TTBR0Small (no live code)No
G4I5 __kernel_end page identity RWSmallNo
G5I1 MMIO 1 GiB identity Device block: GIC / PL011 / virtio must move to a TTBR1 high alias before the tearMedium (UART is used pre-MMU and in fault paths; the b2 profile shares the UART)Scope confirm (D3)
G6I3 boot-stub page (_start + identity vectors) in kernel + user TTBR0Small code if allowed; policy-blockedYes (D2)
G7Taken SError on the accepted machinePolicy / CIYes (D1)
G8Umbrella accept ADR with the exact sentenceDocsYes (D4)
G9Doc drift: ADR-055 §A el0: no data ≠ code el0: no kernel readTrivialNo (noted here; fix in M1)

Milestone plan (one milestone per branch / PR; all TCG, no AWS)

MBranch (proposed)ScopeFail-closed markersGate
M0 (this PR)isolation/adr-086-el0-isolated-auditThis audit + inventory evidence + plan + sponsor decisions. Docs only.n/a—
M1isolation/adr-087-identity-inventory-ratchetCommit the TTBR0 walker as ident: inv + tear G2, G3, G4 (RAM-side leftovers, kernel and user TTBR0) + fix G9ident: inv k=<n> u=<n> + ident: inv-ok. Smoke rejects any identity leaf outside the allowlist {I1 MMIO block, I3 stub page} (ident: inv-leak …). ident: low / ident: tail / ident: kend tears with -fault probes. Keeps ident: start-stay.None
M2isolation/adr-088-mmio-high-aliasMap GIC / PL011 / virtio-mmio in TTBR1 (Device, EL1-only), switch base VAs after the high split, tear I1ident: mmio-high, ident: mmio-fault (identity MMIO VA → DABORT), allowlist shrinks to {I3}D3 (scope). Optional b2-profile KVM re-check is not required. If wanted: ≈ $0.2, report cost first.
M3isolation/adr-089-boot-stub-policyPer D2: (a) docs ADR that defines “full identity teardown” as all identity except the _start stub page (start-stay remains), or (b) code: after the high jump, unmap the identity mapping of the stub page (the image and _start stay physically at 0x4008_0000, and QEMU -kernel only needs that PA at entry), print ident: stub-torn, retire ident: start-stay(b): ident: stub-torn / ident: stub-fault, allowlist becomes {} → ident: inv k=0 u=0D2
M4isolation/adr-090-serror-accepted-machinePer D1: close ADR-055 §B row 1 under the sponsor-chosen evidence class (see D1). A workflow edit (e.g. making qemu-nmi-pin unconditional) is pushed from the amd64 workstation.Taken el0: serror required under the chosen gateD1
M5docs/adr-091-umbrella-acceptADR-048/052-style sponsor accept: exact sentence, machine/CPU/profile scope, the list of Met rowsn/aD4
M6isolation/adr-092-umbrella-flipOnly if M1–M5 are Met: fresh-session re-probe of every row, ledger/roadmap flip, sponsor mergesAll of the above in one runSponsor merge (ADR-002)

ADR numbers in M1–M6 are proposed. Each PR takes the next free number at open time.

  • D1 — Which taken-SError evidence counts for the umbrella? Options: (i) the B1 pinned-QEMU CI job as the accepted smoke machine for this row (make it unconditional; honest caveat: TCG emulator patched locally, not stock); (ii) the B2-P one-off real-hardware run as sufficient (not CI; b2 profile only); (iii) require a CI-gated real-KVM run (self-hosted arm64 KVM runner = security change, or paid metal per gate run ≈ $0.2 each, tagged + self-terminating); (iv) keep taken SError a non-goal → the umbrella is permanently non-claim. Agent recommendation: (i)+(ii) together as the evidence class, stated verbatim in the accept ADR, or (iv). Not a silent redefinition.
  • D2 — _start and “full identity teardown”. ADR-055 §B row 2 (yank _start) contradicts ADR-047 §3 (never yank). Options: (a) the umbrella accepts start-stay as the single documented identity exception; (b) authorize unmapping the stub’s identity mapping after boot while _start stays at 0x4008_0000 (no image move, no -kernel change). ADR-047 §3 would then read as “never move or remove _start”.
  • D3 — Is device MMIO in scope of identity teardown? Recommendation: yes (I1 is the largest identity range left).
  • D4 — Umbrella sentence + accept ADR. Exact wording and scope (e.g. “on QEMU virt -cpu cortex-a76 TCG smoke, plus [D1 evidence]”).
  • D5 — CI gating. main has no branch protection. Should the flip require GitHub-required checks, or is “sponsor does not merge red” enough?

Honesty

Say: “ADR-086 audited the ADR-055/060 checklist on 2026-09-26: every §A mile and PAN enable are Verified this session. Taken SError is Verified only on opt-in paths (B1 pin, B2-P one-off), not on the accepted smoke machine. Identity teardown still leaves five identity ranges (one is _start by decision). No umbrella accept exists. ‘EL0 isolated’ stays Planned / non-claim.”

Do not say:

  • “EL0 isolated” / “secure OS” / “hardened isolation complete”
  • taken lower-EL SError Verified on the default smoke machine
  • identity mappings fully torn down / _start yanked / the kernel moved
  • that the scratch inventory is a CI-gated probe (it is Documented research until M1)

Consequences

  • Ledger, roadmap P-SEC-3l, threat model v1.61, el0.md pointer, SUMMARY, second brain cite this audit.
  • ADR-055 / ADR-060 get a follow-on pointer. Their decisions are not rewritten.
  • No src/ change. No AWS. No CloudAgent. Do not self-merge.