ADR-033 — Linux ELF / auxv / PT_INTERP vs the freestanding loader
- Status: Accepted (gap ADR; no Linux ELF / dynamic-linker claim)
- Date: 2026-09-12
Context
Track B child B4 / #44 (parent #40) asks what would be needed to load a musl-linked binary versus today’s freestanding image, and to keep the Track A loader reusable.
Frame: ADR-031. That frame says B4 must not accept PT_INTERP, must not claim Linux userspace, and must not replace the freestanding Track A path. Syscall convention is already a gap (linux-aarch64-syscall-gap.md / B2). Process execve is B3 / #43. This ADR does not write B3, B5, or B6.
Track A A3 (ADR-023) already parses ELF64 little-endian AArch64 ET_EXEC, maps PT_LOAD only, rejects PT_INTERP, rejects W+X, and ERETs to e_entry. A2–A4 and A9 reuse that parser on FAT /hello (ADR-030, ADR-032). The in-tree payload is libctos, not musl or glibc.
This mile is docs only. It does not change src/loader.rs. File presence of this page is not a Linux ELF probe and not dynamic-linker support.
Probe (what Verified means here)
| Claim | Probe | Status |
|---|---|---|
Linux ELF types / PT_* | Read Linux v6.10 include/uapi/linux/elf.h (ET_EXEC 2, ET_DYN 3, PT_LOAD 1, PT_DYNAMIC 2, PT_INTERP 3, PT_PHDR 6, PT_TLS 7) | Document inspection |
| Linux auxv keys | Read Linux v6.10 include/uapi/linux/auxvec.h (AT_NULL … AT_EXECFN, AT_MINSIGSTKSZ) | Document inspection |
| ctos freestanding loader | Read src/loader.rs (parse_elf64, PT_INTERP reject, LOADER_STACK_VA) + ADR-023 on this tree | Document inspection |
| Dynamic Linux ELF / musl / glibc on the guest | No such serial marker | Not claimed. Ledger “Guest runs host apps” stays Planned. |
A later kernel qemu-smoke is a different row. This mile did not edit src/.
Hypotheses
| ID | Option | Notes |
|---|---|---|
| H1 (chosen) | Document the Linux vs ctos gap. Keep the ADR-023 loader reusable as the freestanding contract. Do not accept PT_INTERP. Do not claim musl/glibc/dynamic ELF. | Matches #44 / ADR-031. Leaves B6 room to choose never. |
| H2 (rejected) | Accept PT_INTERP / ET_DYN / auxv in this mile, or rewrite parse_elf64 as a Linux binfmt_elf. | ADR-031 forbids accepting PT_INTERP here. Would break A3 Interp tests and A2–A9 markers. |
| H3 (rejected) | Treat A3 PT_LOAD + A9 FAT /hello as Linux execve / dynamic linking. | The loader rejects PT_INTERP. Standing EL0 is not execve. Honesty fail (NFR-06). |
| H4 (rejected) | Promise a musl or glibc port, “runs Alpine,” or a guest ld-linux / ld-musl. | ADR-031 H2. No probe exists. |
| H5 (rejected) | Fork a second incompatible parser this mile so Track A and “Linux ELF” diverge silently. | #44 asked to keep the Track A loader reusable. A later Linux path, if B6 ever names one, is a new layer that may call the PT_LOAD helpers — not a silent retarget. |
Decision
-
What Linux
execof an ELF does (inspection). Linuxfs/binfmt_elf.c(not copied here) loadsET_EXECorET_DYN(PIE). If aPT_INTERPstring is present, it also maps that interpreter (typically glibcld-linux-aarch64.so.1or muslld-musl-aarch64.so.1) and transfers control to the interpreter entry, not the binarye_entry. The kernel builds a user stack:argc→argv[]→NULL→envp[]→NULL→auxv[](AT_*key/value pairs) →AT_NULL.The auxiliary vector (Linux v6.10
auxvec.h) is how the dynamic linker and libc CRT find program headers, page size, interpreter base, the real entry, and entropy. Typical keys a musl/glibc_startreads:AT_PHDR/AT_PHENT/AT_PHNUM,AT_PAGESZ,AT_BASE,AT_ENTRY,AT_UID/AT_EUID/AT_GID/AT_EGID,AT_SECURE,AT_RANDOM(16 bytes),AT_HWCAP/AT_HWCAP2,AT_CLKTCK,AT_PLATFORM,AT_EXECFN, oftenAT_SYSINFO_EHDR(vDSO) andAT_MINSIGSTKSZ.After that, the interpreter walks
PT_DYNAMIC(DT_NEEDED,DT_SYMTAB,DT_RELA/DT_REL,DT_INIT_ARRAY), maps shared objects, applies relocations, sets upPT_TLS, then jumps toAT_ENTRY. That is dynamic linking. It is notPT_LOADcopy. -
What the ctos loader does (inspection).
src/loader.rs/ ADR-023:Item ctos today File type ELF64 LE AArch64 ET_EXEConly (NotExecon anything else, includingET_DYN)Segments PT_LOADonly.PT_PHDR/PT_GNU_STACK/ notes ignored.PT_INTERPParseError::Interp. Fail-closed. Smoke/tests require the reject.W+X Reject (segment flags or page-union). SCTLR.WXNis on (ADR-015).VA window paging::MAP_WINDOW0x8000_0000…+ MAP_WINDOW_SIZE(512 × 4 KiB). Caps: 8PT_LOADs, 8 pages.Stack One zeroed EL0-RW NX page at LOADER_STACK_VA(0x80007000). Noargc/argv/envp/ auxv.Entry ERETto the binarye_entry. No interpreter.CRT libctoswrappers +_start. Does not read auxv. CallsSVC #<n>(ADR-021).Image source FAT /hello(A9 / ADR-032). Notexecve.No Linux ELF feature in the gap table below is
present.PT_LOADof a freestandingET_EXECis partial (related map, different contract). -
Status words (same idea as B2).
Status Means present Same Linux ELF/auxv/ PT_INTERPmeaning, so a musl/glibc AArch64 binary could start with no translation.partial Track A has a related PT_LOAD/ stack / entry behavior. Type, stack, or control flow still differ.absent No analog. B6 may still discuss a named subset. Not a promise to implement. stay-rejected A3 fail-closed reject. This ADR does not flip it. Reopen only after B6 names a path, in a later epic. never-per-ADR-031 Would imply a Linux distro / glibc-musl product / guest interpreter that ADR-031 already ruled out as a Track B promise. -
Gap table.
Image type and program headers
Linux / ELF item ctos status Notes ELF64 LE AArch64 magic partial Same header walk. Not a Linux ABI. ET_EXECPT_LOADmap + BSS zeropartial A3 does this inside the 2 MiB window, 8 segments / 8 pages. ET_DYN/ PIE / loader-chosen baseabsent parse_elf64returnsNotExec. Linux PIE isET_DYN.PT_INTERPstay-rejected A3 tests require Interp. ADR-031: do not accept it in this child.Load the interpreter ELF absent No second image, no AT_BASE.Transfer to interpreter entry absent ERETis always binarye_entry.PT_DYNAMIC/DT_NEEDEDabsent Ignored (not PT_LOAD). No shared-object search.Relocations ( DT_RELA/REL/RELR/ IFUNC)absent Freestanding image is already linked. PT_TLS/ TPIDR_EL0absent PT_GNU_STACKexec-stackabsent Ignored. Stack is NX. A Linux exec-stack request would fight W^X. PT_GNU_RELROabsent PT_PHDRusable at runtimeabsent No AT_PHDR. Headers are not guaranteed mapped at a libc-known VA.Stack and auxv (Linux v6.10
auxvec.h)Item ctos status Notes argc/argv[]/envp[]absent Stack page is zeros. libctosdoes not readSP.AT_NULLterminator + key/value pairsabsent AT_PHDR/AT_PHENT/AT_PHNUMabsent musl/glibc ldso needs these. AT_PAGESZabsent Guest pages are 4 KiB; nothing publishes that to EL0. AT_BASEabsent Interpreter load base. AT_ENTRYabsent Real program entry after ldso init. AT_RANDOM(16 bytes)absent Typical stack canary / SSP. No getrandomeither (B2).AT_UID/AT_EUID/AT_GID/AT_EGID/AT_SECUREabsent No uid model. AT_HWCAP/AT_HWCAP2/AT_PLATFORMabsent AT_EXECFNabsent A9 path is FAT /hello, not a Linux pathname.AT_SYSINFO_EHDR(vDSO)absent No vDSO mapping. AT_MINSIGSTKSZabsent No POSIX signals (B2). musl-linked vs freestanding (the #44 question)
Workload What it still needs on ctos Honesty In-tree libctosET_EXECAlready the A3/A9 path. Keep it. Not Linux. Static musl ( -static, often noPT_INTERP)Linux stack + auxv; Linux svc #0/x8(exit_group,set_tid_address,brk/mmap,writeon fd 1 — B2); oftenET_DYNstatic-PIE; VAs usually not in0x8000_0000…+2 MiB; image larger than 8 pages.Not a small subset. ADR-031 rejected “runs musl” as a promise. Dynamic musl ( PT_INTERP→ld-musl-aarch64.so.1)Everything static needs, plus accept PT_INTERP, load the interpreter,AT_BASE/AT_PHDR/AT_ENTRY,PT_DYNAMIC+DT_NEEDEDfiles on a real FS, relocations, TLS.stay-rejected + absent. Do not claim dynamic Linux ELF. glibc dynamic Same class as dynamic musl, usually heavier ( ld-linux, vDSO, more CRT syscalls).Same non-claim. -
Keep the Track A loader reusable. A2 flatten, A3 map+
ERET, A4 standing task, and A9 FAT/helloall callparse_elf64/run_image. This ADR does not widen that function. A later Linux ELF path — only if B6 / #46 chooses a compat layer, and only with a probe — would be a new module that may reuse page-map helpers. It must not silently retargetparse_elf64to acceptPT_INTERPorET_DYN. Track A tests that rejectInterpstay. -
Not claiming dynamic Linux ELF support. Say that sentence on the Track B page. Today:
- A3 maps freestanding ELF64
ET_EXECPT_LOADand rejectsPT_INTERP. - The stack has no auxv.
ERETis not interpreter start.- A9 FAT
/hellois notexecve. - The ledger row “Guest runs host apps (Linux ELF / shell / Python)” stays Planned.
- A3 maps freestanding ELF64
-
If a subset is ever useful (not a decision). Research hint for B6, not a backlog:
- In-tree
libctosapps need no Linux ELF features. Keep ADR-023. - Static musl is already a libc-shaped surface (B2) plus auxv/stack/
ET_DYN/VA-window work. That is not a B4 implementation item. - Dynamic musl/glibc starts with accepting
PT_INTERP. This track’s frame forbids that in B4. B6 may choose never. - Do not start with relocations, TLS, or a guest
ld.so.
- In-tree
-
Non-goals. Full Linux ELF ABI; glibc/musl ports; accepting
PT_INTERPinsrc/; rewriting Track A probes; guest containers (ADR-029); new FR/NFR IDs; author self-merge (ADR-002).
Consequences
- Docs: this ADR. track-b.md B4 becomes Documented. SUMMARY.md and roadmap.md link here. Site extras point at the gap without a userspace claim.
- B3 / B5 / B6 stay Planned research. No
src/change in this PR. - Track A A3 remains the product loader for in-tree freestanding apps.
PT_INTERPstays rejected. - Honesty: document inspection only. Do not add a Verified Linux-ELF or dynamic-linker ledger row because this file exists.