ADR-090 — Taken-SError evidence class for the umbrella = B1 pin + B2-P (M4, sponsor D1)
- Status: Accepted (docs + CI workflow). Records sponsor decision D1 (DSO relay 2026-09-26 ~14:35 CEST). The taken lower-EL SError evidence that counts for the umbrella is the B1 pinned-QEMU CI job plus the B2-P run, stated verbatim below and copied into the draft accept ADR (ADR-091). Makes the
qemu-nmi-pinjob unconditional. Nosrc/logic change; one stale M2 doc comment insrc/teardown.rsis fixed (the allowlist has been stub-only since ADR-088). The umbrella “EL0 isolated” stays Planned / non-claim: the sponsor’s written accept (B3) is still absent. - 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
- Base: stacked on PR #141 (ADR-089, M3).
Context
ADR-055 §B row 1 required “honest host inject that delivers async SError on the accepted smoke machine” (ADR-060). ADR-086 found two taken-SError sources, neither on that machine as worded. B1 (ADR-083) is a CI job on a locally patched QEMU, gated by repo variable CTOS_BUILD_QEMU_NMI=1, so it is opt-in. B2-P (ADR-085) is one paid real-hardware KVM run, not CI. ADR-086 asked D1: which evidence counts? The sponsor accepted the recommendation (i)+(ii): both together, stated verbatim, with no silent redefinition.
Decision
-
D1 evidence class (verbatim; ADR-091 must quote this paragraph unchanged):
Taken lower-EL SError while standing at EL0 is evidenced by two sources together. (i) B1: the GitHub Actions job
qemu-nmi-pin(“QEMU NMI pin smoke (taken SError)”) in.github/workflows/smoke.yml. It builds QEMU v10.0.0 (7c949c53e936aa3a658d84ab53bae5cadaa5d59c) with reviewed local patchresearch/qemu-nmi/0001-hw-arm-virt-TYPE_NMI-raise-SError.patch(TCG:virtTYPE_NMI→ async SError, masked byPSTATE.A), boots the default image onvirt -cpu cortex-a76TCG, issues QMPstop→inject-nmi→conton the guest’sel0: serror-armcue, and withCTOS_REQUIRE_TAKEN_SERROR=1fails closed unless the bareel0: serrorline (printed only by the lower-EL SError vector while the standing-EL0 expectation is armed) appears. It runs on every push and pull request. (ii) B2-P: ADR-085 run 4, 2026-09-26 13:26–13:33 CEST, AWS Graviton3c7g.metal(eu-north-1b, Spot), ctos commit159b178, QEMU-accel kvmwith patch 0002 (inject-nmi→KVM_SET_VCPU_EVENTS{serror_pending=1}→HCR_EL2.VSE), opt-inb2-serrorprofile:el0: serror+b2: taken, fail-closedb2-kvm-smoke.pyPASS. It is a one-off real-hardware run, not CI. Stock distro QEMU (the default smoke) cannot inject and continues to requireel0: serror-park. -
Make B1 unconditional. Remove
if: ${{ vars.CTOS_BUILD_QEMU_NMI == '1' }}fromqemu-nmi-pinand rename the job from “(opt-in)” to “QEMU NMI pin smoke (taken SError)”. Why: D5 makes the smoke a GitHub-required check before the flip. GitHub treats a job skipped byif:as passing a required check, so a variable-gated job would let a PR pass without taking an SError. This reverses ADR-083 §6 (“do not force multi-minute QEMU compile on every PR”) on purpose. Cost: ≈ 10–15 min on a free GitHub-hostedubuntu-24.04runner per push/PR. The repo is public, so standard runners are free. The repo variable becomes unused and can be deleted by the sponsor at any time; its value no longer matters. -
Reworded requirement (ADR-055 §B row 1, ADR-060 taken-SError row): “Taken lower-EL SError while standing, on the D1 evidence class (ADR-090): B1
qemu-nmi-pingreen, unconditional on every push/PR, plus the B2-P one-off KVM run (ADR-085 run 4).” -
Row status: reworded row 1 = Met (D1 evidence class). The B1 leg is green after the boot-path changes of this session: #139 (post-M1) pin jobs 108406107228 / 108406127586 and #140 (post-M2) pin job 108408283187 (
inject-nmi=>ok→ bareel0: serror, 2026-09-26 14:56–15:10 CEST). The first unconditional run is on this PR’s CI. This is not the umbrella: row 3 (sponsor accept) remains, see M5 → ADR-091. -
Not changed: the default smoke on stock QEMU still requires
el0: serror-park(ADR-053/081). ADR-053’s hard-stop stays true for stock virt TCG. DockerfileARG CTOS_BUILD_QEMU_NMI=0stays opt-in (local images only). No KVM in CI.
Caveats stated with the evidence (must travel with any quote of §1)
- B1 is a locally patched emulator. The SError is raised by QEMU TCG code this repo carries (patch 0001), not by stock upstream QEMU and not by hardware. It proves that the guest’s lower-EL SError path takes the exception; it does not prove any CPU’s RAS behaviour.
- B2-P is historical. It ran once at
159b178, before this session’s M1/M2 boot-path changes (identity leftover tears, MMIO high alias). Theb2-serrorprofile still TCG-boots through its markers after M2 (agent box, ~14:44 CEST), but it was not re-run under KVM. A re-verify is one more paidc7g.metalSpot run, ≈ $0.2 (ADR-085 cost basis). D1 does not require it; it is a sponsor option. - B2-P used a reduced profile (no GIC / timer / virtio / FAT), per ADR-085.
- Neither leg is real hardware running the full default image.
Honesty
Say: “per sponsor D1 (ADR-090), taken lower-EL SError is evidenced by the B1 pinned-QEMU CI job, which is unconditional and fail-closed on every push/PR, plus the one-off B2-P Graviton3 KVM run (ADR-085 run 4). Stock QEMU parks.” Do not say “taken SError on stock QEMU”, “taken SError Verified on real hardware in CI”, “SError hard-stop lifted” (it stands for stock TCG), or “EL0 isolated”.
Consequences
.github/workflows/smoke.yml:qemu-nmi-pinunconditional and renamed. docs/dev/qemu-nmi-pin-ci.md updated.- D5 / M6 required-check names (branch protection is not changed here; the sponsor/DSO sets it at M6):
QEMU aarch64 smoke (ubuntu-24.04-arm),QEMU aarch64 smoke (ubuntu-24.04),QEMU NMI pin smoke (taken SError). - ADR-055 §B row 1, ADR-060 and ADR-083 follow-ons; ledger, roadmap P-SEC-3r/3l, threat model v1.65, el0.md, SUMMARY, MOC.
- Next: M5 (ADR-091 draft accept). Stop before M6.