ADR-023 — Guest ELF64 PT_LOAD loader into user TTBR0
- Status: Accepted (loader mile Verified when the serial / tests pass; Track A / app hosting still Planned)
- Date: 2026-09-12
Context
Track A (issue #31) wants a freestanding image loaded into user maps and entered with ERET. Issue #34 is this mile.
ADR-022 already links user/hello-libctos against libctos. That hello is host-built. build.rs extracts PT_LOAD and the kernel memcpys the blob onto paging::EL0_PAGE. That is not a guest loader.
A3 must parse and map on the guest. The bytes may still be embedded (no VFS yet — A6/A7). The point is map + load + ERET, not host flatten-and-copy onto a fixed probe page alone.
Existing A1 svc:* and A2 libctos:* markers stay. A4–A9 stay out of this PR.
Hypotheses
| ID | Option | Notes |
|---|---|---|
| H1 (chosen) | Minimal ELF64 LE AArch64 ET_EXEC, PT_LOAD only | The in-tree payload is already that ELF. A2 already walks PT_LOAD on the host. Guest parse is the missing mile. |
| H2 (rejected) | Custom raw image (base VA + entry + blob) | Smaller parser, but the host would flatten it — same shape as A2. Would not prove guest ELF walk. |
| H3 (rejected) | Linux ELF ABI / PT_INTERP / ET_DYN / TLS / glibc | Inflates the claim. Fail-closed: reject PT_INTERP. Not Track B. |
Decision
- Guest parser in
src/loader.rs. Accept ELF64 little-endian AArch64ET_EXEC. Walk program headers. LoadPT_LOADonly. IgnorePT_PHDR/PT_GNU_STACK/ notes. RejectPT_INTERP. Reject aPT_LOADthat is W+X (segment flags or two segments that union to W+X on one page).SCTLR.WXNis already on (ADR-015); do not map a writable executable leaf. - User TTBR0 maps. Each page of a
PT_LOADis a freshly allocated frame mapped in the shared map-window L3 (paging::MAP_WINDOW…+ MAP_WINDOW_SIZE). That L3 is already inL1_USER, so EL0 and EL1 both see it. Permissions:PF_X→map_el0_exec(EL0 fetch, no EL0 data; EL1 may write the identity PA before / while mapping);PF_W→map_el0_rw; R-only →map_el0_ro. Page-granular union:.textRX and.rodataR on one 4 KiB page become Exec. Not a new user address space. - Copy + BSS. File bytes are written to the frame’s identity PA.
p_memsz > p_fileszis zeroed. I-cache is synced on executable pages.e_entrymust land in an Exec page. - Stack. A separate EL0-RW NX page at
paging::LOADER_STACK_VA(0x80007000), not the A2 code page. Rustmainstill savesx30onSP_EL0. - Image source. First cut embedded the ELF (
include_bytes!). ADR-032 reads the same bytes from FAT/hello.build.rsstill compiles and publishestarget/hello-libctos.elf. Not a second QEMU-kernel. - ERET.
el0::install_standing(e_entry, stack_top, user_ttbr0)thenexception::eret_to_el0.SYS_EXITreturns to EL1 as today. A2’s memcpy path stays solibctos:*markers remain. - Fail-closed probe. Hello serial
loader: mapped+loader: okafter a successful trip. The loaded payload still printslibctos: hi/libctos: okviauart_write.scripts/qemu-smoke.shgreps those and rejectsloader: probe missed.#[test_case]covers parse,PT_INTERPreject, W+X reject (flags and overlapping pages), RX+R union on the hello page, and the full map +ERETtrip. Existingsvc:/libctos:/el0:markers stay. - Honesty. Say “the guest parsed a freestanding ELF64, mapped
PT_LOADinto user TTBR0, andERETed toe_entry” only when the serial / tests pass. Do not say: Linux ELF ABI,PT_INTERP, glibc, app hosting is done, POSIX, userspace, “EL0 isolated,” or “secure OS.” Standing-as-normal is ADR-024. Track A A5–A9 stay Planned. - NFR-10 text is revised in place (ID unchanged) to name this loader mile. Do not mint FR-16+ or NFR-15+.
Consequences
- Code:
src/loader.rs,paging::map_el0_ro/window_range_ok/LOADER_STACK_VA,build.rsemits the ELF,scripts/qemu-smoke.sh. - Docs: el0.md, syscall.md, building-or-porting.md. Roadmap cites #34 / Track A #31.
- A4 (standing EL0 as normal mode) keeps this loader as the way a payload appears (ADR-024).