ADR-024 — Standing EL0 as normal mode
- Status: Accepted (standing-task mile Verified when the serial / tests pass; Track A / app hosting still Planned)
- Date: 2026-09-12
Context
Track A (issue #31) wants a loaded freestanding image to run at EL0, not only enter and leave. Issue #35 is this mile.
ADR-013 already has a bounded standing context: SVC #1 stays at EL0, a user MOVZ runs, SVC #2 restores EL1. is_active() is true only for that lifetime. That is a smoke probe, not the supported path for a loaded app.
ADR-021 / ADR-022 / ADR-023 already ERET to a payload and come back on SYS_EXIT. They reuse install_standing as a flag. A leftover is_active() after ERET fails the probe. That is still a one-shot trip, not “standing EL0 is how a loaded image runs.”
A4 keeps the A3 loader as the way a payload appears. It does not add VFS, slots, PAN, or a Linux exec. Existing svc:* / libctos:* / loader:* / el0: standing / el0: restored markers stay.
Hypotheses
| ID | Option | Notes |
|---|---|---|
| H1 (chosen) | Distinguish probe vs task standing. A task is the supported path: load (A3) → is_active() true → stay at EL0 for yield / uart_write → exit restores. Unexpected lower-EL sync while a task is standing restores fail-closed instead of parking. | Proves real-task is_active and a restore that cannot leave a dangling standing context. |
| H2 (rejected) | Treat the ADR-013 dual-SVC as “normal mode.” | Still a one-shot SVC #1 / #2 trampoline. Does not run a loaded app until exit. |
| H3 (rejected) | Claim isolation / PAN / process table while promoting standing. | Inflates the mile. Isolation is A5 (issue #36). |
Decision
- Two standing kinds.
install_standingremains the ADR-013 / A1–A3 probe (dual-SVC or ABI trampoline).install_taskis the A4 task.is_active()is true for both.is_task()is true only for a task. Saved user PC / SP / TTBR0 live only whileis_active(). - Supported path. The A3 guest ELF loader maps
PT_LOADfrom FAT/hello(ADR-032), theninstall_task(e_entry, stack_top, user_ttbr0)andERET. The loaded image runs at EL0 untilSYS_EXIT.SYS_YIELD/SYS_UART_WRITEstay at EL0 and must observeis_active()(el0: task-active). A2’s memcpy path stays solibctos:*markers remain (same FAT file; no embed). - Fail-closed restore.
SYS_EXITon a task printsel0: task-exit/el0: task-restoredand clearsis_active(). An unexpected lower-EL sync (unhandled SVC, translation/permission abort, and anything that would otherwise park) while a task is standing printsel0: restore-fail, clearsis_active(), and returns to the EL1 caller. It does not park. Probe standing still parks on unexpected sync (A1–A3 unchanged). AfterERETreturns, leftoveris_active()is Failed (caller force-clears and fails the probe). - Fail-closed probe. Hello serial
el0: task-enter+el0: task-active+el0: task-exit+el0: task-restored+el0: restore-fail+el0: task-ok.scripts/qemu-smoke.shgreps those and rejectsel0: task missed.#[test_case]covers the loaded-hello task trip and the fault-restore trip. Existingsvc:/libctos:/loader:/el0: standing/el0: restoredmarkers stay. - Honesty. Say “a loaded freestanding image stood at EL0 until
exit, and an unexpected fault restored fail-closed” only when the serial / tests pass. Do not say: app hosting is done, POSIX, userspace, “EL0 isolated,” Linuxexec, or “secure OS.” Track A A5–A9 stay Planned. - NFR-10 text is revised in place (ID unchanged) to name this standing-task mile. Do not mint FR-16+ or NFR-15+.
Consequences
- Code:
src/el0.rs(install_task,restore_exit,restore_fault),src/loader.rs(run_hello_as_task),src/exception.rs(task restore instead of park),src/syscall.rs(note_active_while_standing),scripts/qemu-smoke.sh. - Docs: el0.md, syscall.md, track-a.md. Roadmap cites #35 / Track A #31.
- A5 (isolation completion — remaining identity
.rodata/.data/ heap tear; PAN only if the probe CPU implements it and an ADR says so) is not this PR.