Skip to content

The loader is the program's, and the kernel parses no ELF - #1189

Merged
Japabu merged 16 commits into
mainfrom
kernel-no-tls
Nov 5, 2026
Merged

Japabu merged 16 commits into
mainfrom
kernel-no-tls

Conversation

@Japabu

@Japabu Japabu commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The kernel laid out every thread's TLS block and DTV, loaded and relocated dlopened libraries, answered the module list, and parsed every program's ELF at spawn and its own at boot. All of that is input parsed in Ring 0, most of it written by userland. This is Move 1 of issues/the-kernel-still-parses-what-userland-writes.md. The owner's decision of 2026-11-05 extends it: nothing in the kernel parses an ELF, and toyos-elf is not published.

What changes

The kernel parses no ELF

  • Spawn takes ranges. SpawnArgs carries the child's memory as SpawnMaps: an address, a length, an object, an offset, the bytes copied and a protection. It also carries the entry, the image base and the build-id that a crash record names.
    • The kernel checks each range as an ordinary mapping, all or nothing: whole pages of the user half, a VmProt, and a copy only of ordinary memory the object holds (MAP) from a page boundary.
    • A range that meets another range or the clock page is refused, and so is an entry in no executable range.
    • A copied range uses the kernel's existing private kind, whose pages are copied out of the object at first touch, so page sharing is unchanged.
    • No VmProt is writable and executable, so W^X holds by type.
  • The parent lays out the program. toyos::elf::program::lay_out does it. Helper::spawn uses it, and so does the build for the supervisor. It refuses a segment both writable and executable by name, as the library loader does.
  • Boot is precomputed. The signed image's kernel section is toyos_kimage, carried in the slot as toyos/kernel.img. It holds the kernel at its own addresses, its RELATIVEs, its ranges and RELRO, a symbol table, and the supervisor's program with the length of the section it was laid out of. The build writes it (build::kimage). The loader copies the image and applies the relocations. The kernel takes its page protections, its symbols and the supervisor's ranges from it, and checks the supervisor section's length against the section the loader handed it.
  • The kernel's frames name the same functions as before. The symbol table resolves exactly as the ELF symbol table does: on a real binary at every sampled offset, and on a crafted tie.
  • The kernel's spawn path and kernel/src/file_backing.rs are gone. The loader and the kernel link no ELF code.

One ELF parser, in toyos

  • toyos::elf holds lib/elf's source and tests. The SDK's loader, the build's gates, build::kimage and /system/bin/symbolize read with it.
  • lib/symbols keeps what reads no ELF: the frame records and the name budget. The kernel keeps using it.
  • toyos-elf is no crate. It leaves PUBLISHED, std's patched crates and the rust fork's lock (a189b3a6b33), so nothing new needs publishing.

TLS is the program's (toyos::tls)

  • There is one process-wide module list, the executable first, which every thread's DTV names.
  • Each thread's area sits below its stack. Library blocks are made the first time a thread asks for one, and the DTV grows.
  • The kernel's only part is SYS_SET_THREAD_POINTER. It refuses a non-canonical address, a kernel address and u64::MAX (abuse_kernel_addr).

The loader is the SDK's (toyos::dl)

  • std's and libc's starts call toyos::dl::start. libc's dlopen, dlsym, dlclose and dlerror are thin wrappers, and libloading calls toyos::dl.
  • A load is one set. It covers the libraries named and every library they name that is not yet published, found breadth first, so a cycle loads once. The set is relocated and protected, and only then published. A refusal releases all of it. Constructors run dependencies first.
  • Refusals:
    • an unknown relocation type, by number;
    • a strong undefined symbol, by name;
    • a writable-and-executable library segment;
    • an empty DT_NEEDED name.
  • What binds without refusal:
    • ABS64 is applied as S + A.
    • A weak symbol nothing defines binds to 0.
    • A lazy call slot nothing defines binds to a trap, which fails at the call as a lazy loader does. With BIND_NOW, it is refused.
  • The executable's binding pass also writes into the main thread's TLS block whatever it writes into the template.

Retired

  • Syscalls: SYS_DLOPEN, SYS_DLSYM, SYS_DLCLOSE, SYS_TLS_ALLOC_BLOCK and SYS_QUERY_MODULES.
  • SpawnArgs::image.
  • Kernel code: kernel/src/elf/, loader/tls.rs, file_backing.rs, KernelSlice and Rollback.
  • lib/elf and RelaCounts.

Forks, all pushed to their branch heads:

  • rust main a189b3a6b33;
  • backtrace-rs 742eb4b;
  • rust_libloading toyos-sdk-0.12 1eb1665;
  • core_affinity 61cf0af.

Size

  • Kernel: kernel/src goes from 224 files and 58,050 lines at the merge base ec8912181 to 218 files and 56,392 lines (−6 files, −1,658 lines).
  • Loader in every std binary: about 58 KB. A hello-world goes from 643,480 to 702,928 bytes (+9.2%).

Tests

  • Host:
    • toyos::elf: the moved corpus and fuzz, and elf_program, which compares lay_out with object's reading of lld's images and checks its refusals.
    • toyos::dl: relocations, refusals, sets, cycles and constructor order.
    • toyos-kimage: the format's round trip, its fences, symbol resolution, and stack placement.
    • build::kimage: the table names what the ELF table names.
    • symbolize's namer, toyos::relocate, kernel/pure's W^X and libc's copies.
  • Guest:
    • abuse_spawn_maps (new) holds the kernel's spawn boundary: every refusal above, including a copy from no page boundary and one whose end wraps, PermissionDenied without MAP, and an honest spawn after.
    • start_refuses_itself (new) holds a real program refusing itself at start, for a RELR table and an empty DT_NEEDED name.
    • spawn_image_object keeps the object's rules.
    • The crafted-ELF guest tests are deleted. Their headers are the parent's layout now, which host tests hold.

Evidence

All of it is published at https://github.com/ToyOSOrg/evidence/tree/faa22272c8eaa7e0900a2dc1cbc87dfce8798f71/pr-1189, with round 3 under round3/.

  • Round 4 (the review of round 3) at 61152480a:
    • Host CI exited 0, all 108 steps green, and --build-only exited 0.
    • Guest rows run 42103839687 passed 19 of 19, the ten x86-64 rows and the nine aarch64 rows.
    • The W+X refusal's control (mut15) went red.
    • None of the 221 programs the build made has a writable-and-executable PT_LOAD. The command, its per-file output and a control that it detects one are in round4/wx-scan*.
    • The T14 testcases boot at this revision, image hash as staged, passed 16 of 16 (round4/metal-61152480a). abuse_spawn_maps refused both new cases InvalidArgument, and C cases 204, 206 and 207 passed.
  • Host, round 3: cargo run -- --ci host at bb1e9ada6 exited 0, all 108 steps green. --build-only at b36b16e12 exited 0.
  • Negative controls, each applied as a patch, built, run and restored:
    • round 2: seven on the loader;
    • round 3:
      • the table keeps the first symbol at an address, not the sized one;
      • a writable-executable segment is laid out writable;
      • holes are left unreserved;
      • the section's reader accepts a relocation past the image.
    • All went red. The first round-3 one initially passed, which exposed a missing tie case; it was added and the control then went red.
  • Guest rows:
    • At b36b16e12, run 42080820355: 18 of 19. virt_random_differs was red because the loader refused a slot whose kernel was not a section ("not TOYOSKRN"). A second boot in one process took build_test_parts' memo path, which returned the kernel's ELF. That was a defect of this change, fixed in bb1e9ada6.
    • At bb1e9ada6, run 42089538985: 18 of 19. Every aarch64 row is green, virt_random_differs among them, and every x86-64 row but one. session_lifecycle stalled at the desktop reopening.
      • Cause: the supervisor handed the compositor the session whose owner had exited 6 ms earlier, before it read the exit.
      • Nothing this PR changes is on that path, and the row passed at this branch in three earlier runs.
      • Recorded, with the log, as issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md.
    • The rows cover the ten x86-64 rows that boot std and libc programs (libc_children and hosted_clang_hello among them) and the nine aarch64 virt_* rows.
  • T14:
    • Round 3's boots at bb1e9ada6, both images' hashes as staged, passed 17 of 17 (evidence).
      • testcases, 16 members: the loader tests, start_refuses_itself, abuse_spawn_maps (each refusal answered as named), spawn_image_object, mmap_stress, disk_backtrace, and C cases 204, 206 and 207.
      • shared-debug: abuse_kernel_addr.
    • Round 2's boots at 125b8929f passed 16 of 16.
    • Guest negative control on the T14, a separate boot because its kernel is mutated: lay() ignores its vacancy check, on a never-pushed commit ae8139783 over bfb75f056, booting abuse_spawn_maps alone. The image hash was as staged. The member went red at "a range on the clock page": the spawn was not refused, the child ran (pid 16, exit 0), and the member exited 101. Every refusal before it was still answered (evidence).

Risks

  • The __tls_get_addr DTV layout changed (a three-word header).
  • AArch64 TLSDESC is refused.
  • A Rust library carries its std's _start. Its main slot binds to the lazy trap.
  • A concurrent dlopen can return a library whose constructors are still running. This predates the PR and is recorded in an issue.
  • A spawned program's base is fixed at toyos::elf::program::BASE, as before. There is no randomization.
  • The slot's kernel file is renamed (toyos/kernel.img) and holds a new format. An image older than this one cannot update in place to it: its updater writes the slot under the old name, and the loader on the ESP stays the old one, which parses an ELF. Such a machine is flashed. ToyOS keeps no compatibility, and the T14 is flashed for every run.

🤖 Generated with Claude Code

Japabu and others added 5 commits November 5, 2026 11:14
Stage 2 of Move 1 of issues/the-kernel-still-parses-what-userland-writes.md.

A program lays out its own thread-local storage (toyos::tls): one
process-wide module list, the executable first, which every thread's DTV
names so every copy of the toyos crate in a process finds it; each
thread's area (DTV, the executable's block, the TCB) below its stack,
released with it; a dlopen'd module's block made on first use by
__tls_get_addr's slow path, in the process's own memory; a DTV that
grows. The kernel's only part is SYS_SET_THREAD_POINTER (171), which
points the caller's FS base or TPIDR_EL0 at a user-half address and
answers its tid; a spawned thread starts with no thread pointer and
names its own first. FSGSBASE stays forbidden, so x86-64 needs the call.

libc's loader (sdk/libc/src/dl.rs over dlimage.rs) reads a library
wherever it is served, lays it out in private memory, relocates it with
toyos-elf against every published module (the executable first), gives
each segment its protection and PT_GNU_RELRO read-only, publishes it and
runs its constructors. dlopen, dlsym (RTLD_DEFAULT included), dlerror,
dladdr and dl_iterate_phdr read the list; std's unwinder and the fork's
backtrace crate read it too. An executable naming DT_NEEDED is no longer
refused by toyos::relocate: it applies its RELATIVE entries, and its
start loads the libraries and binds the rest before main
(__toyos_load_needed). A C program always links the loader; std reaches
it through a weak symbol, since libtoyos_c is built from std, and a Rust
program naming a library links libtoyos_c itself or is refused at start
by name, as std_tls (restored) and the track record.

Retired: SYS_DLOPEN, SYS_DLSYM, SYS_DLCLOSE, SYS_TLS_ALLOC_BLOCK and
SYS_QUERY_MODULES, with DlopenArgs, ModuleInfo, MAX_LIBRARIES and the
ABI's TCB items (current_tid moves to toyos::tls). The kernel loses
kernel/src/elf/, loader/tls.rs, ElfInfo, a thread's TLS mapping, the
dynamic TLS blocks, KernelSlice, Rollback and OwnedAlloc's views:
kernel/src goes from 223 files and 56,800 lines to 218 and 55,286.
toyos-elf loses the static-TLS arithmetic only the kernel used.

A program may protect its own memory read and execute (vm_prot), which
dlopen already effectively allowed through any bytes it was handed; no
protection allows a write and an execution at once, held by
kernel/pure/vm.rs's no_protection_allows_a_write_and_an_execution, and an
object's mapping still never becomes executable. libc's mmap and
mprotect follow: read+exec answered, write+exec ENOTSUP.

Closes the DTV-capacity defect (the DTV grows in the process's memory),
the twice-zeroed TLS block, the contiguous dlopen run, dlopen's
write_at contract, std's query-modules miscount, RTLD_DEFAULT's panic
(207 holds it) and KernelSlice's lifetime. The crafted-ELF guest cases
keep what the kernel still parses, the program headers; a library's are
the program's loader's, held by toyos-libc-copies' dlimage tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g unbundled

toyos::tls's start publishes the executable through the list's one
writer rather than forgetting a Loading guard; libc's loader marks a
library's memory published rather than forgetting its reservation.
sourcegate's Send/Sync registry loses kernel/src/mm/region.rs, which is
gone. The layout tests read alignment with is_multiple_of. libloading's
fork names libtoyos_c unbundled (fbfc60c): a library build does not
search the sysroot the final link does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…left

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The T14 testcases boot at f66dd9a red abuse_elf_loader's tls_template_2m
and abuse_elf_segments's tls_huge: each asserted the kernel refuses a PT_TLS
its TLS block allocation could not hold, and the kernel lays out no TLS now,
so it spawns them and the child dies. tls_crowds_dtv went with them, a
survival case over the kernel's DTV. The PT_TLS rules the program-header
parse keeps (p_align, filesz > memsz, vaddr) stay asserted at the spawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1189 at 42ed99c14 (stage 2 of Move 1: the loader is the program's)

Architecture

The direction matches the owner's plan and the orchestrator's rulings. The kernel now maps PT_LOADs and jumps. The module list, the TLS areas, the DTV and the loader belong to the program. The only new kernel surface is SYS_SET_THREAD_POINTER, which FSGSBASE being forbidden makes necessary. The deletion is real: kernel/src goes from 223 files and 56,800 lines to 218 and 55,286, about +91/−1,589 across kernel/. Production code is about −815 lines net and tests about −781. The added code is sdk/toyos/src/tls (~590 lines with its layout tests) and libc's dl.rs/dlimage.rs (~700). That is a fair price for removing the kernel's ELF/TLS/dlopen code and its six syscalls' worth of bounds.

Security (what you asked first)

  • What the kernel still parses: Layout::parse reads the program headers, including PT_TLS placement (p_align, filesz <= memsz, the template inside the image). It also reads the build-id note. The kernel no longer reads any PT_TLS size into an allocation. toyos::tls::start's executable() relies on exactly this validated placement, so the kernel's check is still worth keeping. The host fuzz (lib/elf/tests/fuzz.rs) still draws hostile TLS sizes and alignments through the parse. In the guest, tls_filesz, tls_vaddr and tls_align_wrap still assert the spawn refusal.
  • W^X: it holds on every path I traced.
    • kernel::vm::Prot has no write+execute variant, and both arch encodings agree: x86 ReadWrite sets NX and ReadExec clears W; AArch64 ReadWrite sets UXN|PXN and ReadExec is AP_READ_ONLY.
    • sys_shm_map refuses ReadExec. Every other space.map caller passes Read or ReadWrite.
    • protect of an object mapping is bounded by within(max), and ReadExec is within neither Read nor ReadWrite.
    • remap refuses ReadExec. Private and Copy pages own their frames, so one cannot alias another's.
    • AArch64 I-cache maintenance runs on every leaf that becomes ReadExec (edit/write → make_executable).
    • The negative control (mut1) is real. Prot::allows is what user_ptr checks before a kernel copy-out into user memory, so the mutation it controls is the kernel-write path into code pages. It does not cover the PTE encodings, which the enum shape covers.
  • SYS_SET_THREAD_POINTER: it accepts zero or < USER_TOP (UserAddr::checked) and refuses anything else with InvalidArgument. A non-canonical or kernel FS base therefore never reaches wrmsr.
    • The switch path saves the live register on both arches (read_fs_base / TPIDR_EL0) and restores it for the incoming thread, so the next switch and return keep what the thread set.
    • The kernel never dereferences it. dump_crash_diagnostics uses dump_region, which reads user memory safely.
    • On AArch64 any value would be harmless, and refusing kernel-half values costs nothing.
    • One thread cannot set another's: the call writes the calling CPU's register for the current thread only. Another thread can write a sibling's TCB memory, which is the same process's own memory and acceptable.
  • Forks: the pins match the pushed heads.
    • rust main = fd00d661c36, which bumps library/backtrace from f8a3e68 to 742eb4b (backtrace-rs master head).
    • rust_libloading toyos-sdk-0.12 = fbfc60c.
    • core_affinity toyos-0.8.3 = 61cf0af.
    • The only lock consuming the latter two is tests/guest/rust-tests/Cargo.lock.
  • Deletion: complete. Outside issues/, nothing in the tree names SYS_DLOPEN/DLSYM/DLCLOSE/TLS_ALLOC_BLOCK/QUERY_MODULES, DlopenArgs, ModuleInfo, MAX_LIBRARIES, TCB_TID, toyos_abi::current_tid, KernelSlice or Rollback. OwnedAlloc and KernelMapping remain, each with live users.

The coordinator's question: 42ed99c14

Deleting tls_template_2m, tls_huge and tls_crowds_dtv is right, and no replacement case is needed.

  • Each asserted a kernel bound over a quantity the kernel no longer acts on: the TLS block allocation, the DTV header in the same block, and OwnedAlloc near 2 MiB.
  • The part of PT_TLS the kernel still parses is held in the guest by the three surviving cases and on the host by the fuzz.
  • A "the kernel survives, the child exits non-zero" case would test the child's own start, not a kernel boundary. That refusal is layout::area overflowing (host-tested in an_alignment_that_is_no_power_of_two_or_a_size_past_the_space_is_refused) or vm_reserve refusing a huge length (an existing, checked boundary).
  • One consequence: both binaries asserted in order, so the T14 run at f66dd9a22 never reached the cases after the red ones (abuse_elf_loader 8–22, abuse_elf_segments 3+). Those need a run at this head (see B4).

Findings

BLOCKER B1 — libc's loader hides failures it should refuse (sdk/libc/src/dlimage.rs, sdk/libc/src/dl.rs)

  • dlimage.rs:300 let Some(r) = rela::parse(..)? else { continue };: an unknown relocation type is skipped silently because rela::parse answers Ok(None) for Other. Skipped types include R_X86_64_64 / R_AARCH64_ABS64 against a symbol (S + A, common in data that points at an imported function), IRELATIVE and COPY. The slot keeps its link-time value and the program runs with a wrong pointer. toyos::relocate refuses an unknown type by name, and the loader must too. Either implement the absolute 64-bit type, which is the common one, or refuse it. Only R_*_NONE may be skipped.
  • dlimage.rs:307-313, 316-326, 334-339: a symbol nothing defines is written as 0, or as a DTPMOD, the image's own id. That aliases another module's thread-local onto this module's block at offset 0. The count is returned and then discarded at both callers (dl.rs:129, dl.rs:256), so the computed answer is dead output and nothing reports the failure. The kernel at least logged unresolved=N. Refuse a strong undefined reference (dlerror naming it, exit 127 at start). Bind a weak one to 0. Then drop the count, or use it.
  • dl.rs:239, 243: __toyos_load_needed returns silently when the executable's plan or tables are refused. An executable that names libraries then runs main with unbound GOT slots. Once the image names a library, every failure must take the exit-127 path.

BLOCKER B2 — a failed dlopen publishes dependencies whose constructors never run (sdk/libc/src/dl.rs:107-156, 287-303)

  • open publishes each dependency as soon as it loads (loading.add) and queues its .init_array in inits.
  • If the parent then fails (relocation, protect, a later sibling), dlopen drops inits on its Err arm.
  • The dependency stays in the list, and a later dlopen of it answers it through loading.find with an empty inits: a module handed out whose constructors never ran.
  • Fix: run the constructors of everything published even when the top-level load fails. The modules are published either way.

BLOCKER B3 — a DT_NEEDED cycle recurses until the stack overflows (dl.rs:124-127)

  • A module is published only after its dependencies, so with A → B → A, open(A) is not yet findable when B names it, and the recursion never ends.
  • Cycles are legal ELF, and the file is the loader's untrusted input.
  • Track the names being opened and refuse a cycle by name (or support it). A crash on the guard page is not a refusal.

BLOCKER B4 — symbol writes into the executable's TLS template miss the main thread's block, and toyos::relocate's comment and refusal are now wrong (sdk/toyos/src/relocate/mod.rs:23-25, 436-437; dlimage.rs Pass::Executable)

  • The kernel no longer copies the template from the file. tls::start copies it from the mapped, already-relocated image, and every later thread's block is copied after that too.
  • So a RELATIVE in the exe's .tdata now reaches every block, and Refusal::InTlsTemplate refuses something that would now work. The new comment ("which a write here would never reach in a block already made") is false for every block relocate precedes.
  • The hazard has moved. __toyos_load_needed's Pass::Executable writes symbol values after tls::start made the main thread's area, and nothing stops such a write from landing in the template. The main thread then sees the stale value and every later thread the bound one.
  • Fix: refuse a Pass::Executable write into the executable's PT_TLS file bytes, or copy it into the main thread's block too. Delete or restate the relocate refusal and its comment to match.

BLOCKER B5 — guest evidence for the central claims is missing at the head

  • The only guest coverage of DT_NEEDED, libc's dlopen, __tls_get_addr's slow path, DTV growth, std_unwind_so, the C cases 204/206/207 and the kernel's crafted-ELF cases is the T14 testcases boot.
  • No result of that boot is published. The f66dd9a22 run was red in abuse_elf_loader/abuse_elf_segments, which also skipped their later cases.
  • Needed: the readback of that boot at the final head (after B1–B4), published with the PR, every listed member green.
  • libc_children's red in run 42018194698 I accept as independent. It is the recorded stall, and main reproduces it locally at 1 in 7.

BLOCKER B6 — a stale record directly affected by the change (issues/the-capability-end-state-is-twelve-answers.md:163)

  • The PR rewrites lines 141–160, but the recommendation two lines below still says to keep "SYS_DLOPEN and SYS_SPAWN ambient", naming a syscall this PR deletes. Drop SYS_DLOPEN there.

SUGGESTION S1 — the libtoyos_c limit: my view of the cleanest fix

  • The track records the exit as "the target links its C runtime into every executable". That is an agent proposal, not an owner ruling, and I think it is the wrong exit. libtoyos_c is built from std, so it would put a second std (allocator, stdio, panic runtime) into every Rust binary.
  • The loader's subject is the module list that toyos::tls already owns, beside toyos::relocate, which already runs in every start. Move dl.rs/dlimage.rs's loader into the toyos crate (e.g. toyos::dl, depending on the pure, no_std toyos-elf).
    • std's start calls it directly, so the weak symbol and the 127-for-want-of-a-loader path disappear.
    • libc's dlopen/dlsym/dlerror become C-ABI shims, keeping errno and dlerror's text.
    • libloading's ToyOS backend calls toyos again and stops linking toyos_c unbundled.
    • std_tls loses its #[link(name = "toyos_c")] plus #[used] workaround.
  • Cost: the probe of the dynamic section in programs that name no library, and the loader's text in each std binary unless it is dead-stripped. Measure the binary size before deciding. Either way, label the recorded exit as a proposal until the owner rules.

SUGGESTION S2 — test SYS_SET_THREAD_POINTER's refusal. Inspection establishes it, but the consequence of losing it is a userland-triggered #GP in Ring 0. Adding 0x0000_8000_0000_0000, a kernel-half address and u64::MAX to abuse_kernel_addr, which already runs in the shared boot, costs one boot's seconds.

SUGGESTION S3 — protect (dl.rs:190-196) silently makes a PF_W|PF_X segment read+execute, so its writes fault later. Refuse the segment by name instead, as the loader's own W^X statement implies.

SUGGESTION S4 — dlsym(handle, …) (dlimage.rs:244, defined) answers base + st_value for an STT_TLS symbol, which is not the calling thread's datum. Filter TLS symbols out, as address already does through exported.

SUGGESTION S5 — history in test comments. The reworded comments in 42ed99c14 ("u64::MAX once wrapped the kernel's TLS block size", "16 KiB copied into a 16-byte kernel heap allocation while the kernel laid out…", and the abuse_elf_segments header paragraph) narrate history. Each case's claim is that the spawn refuses the header. The history belongs in the commit.

Evidence checked

  • Guest run 42018194698: head c6c2c3dd2, 18 of 19 rows passed, and the one red is libc_children at tty_interrupt, as claimed.
  • Host CI log hostci2.log: 108 steps, all green, exit 0. Its FAILED lines are the CI's own negative controls. Everything after 96cbf3838 touches only tests/guest/rust-tests and issues/, so the host result stands for the head.
  • Negative controls mut1 and mut2: each patch is shown red with exit 101.

SEND BACK (B1–B6).

Japabu and others added 8 commits November 5, 2026 14:28
main allocates tids from the process's own pool; the thread's id no
longer goes into its TCB, which the program now owns
(toyos::tls::current_tid asks SYS_SET_THREAD_POINTER).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loader moves out of libc into `toyos::dl`, beside the module list it
publishes into (`toyos::tls`) and the self-relocation that precedes it.
std's and libc's starts both call `toyos::dl::start`, so a Rust program
that names a library no longer has to link libtoyos_c: the weak symbol,
std's "links no loader" refusal and `std_tls`'s link workaround go.
libc keeps `dlopen`, `dlsym`, `dlclose` and `dlerror` as wrappers over it,
with errno and a per-thread `dlerror` text that names the reason. The
libloading fork calls `toyos::dl` again; `toyos` depends on `toyos-elf`,
which joins std's patched crates and the published crates ahead of
`toyos`, and the rust fork's library lock carries it.

A load is one set: the libraries named and every library they name not
yet published, found breadth first, so a cycle of `DT_NEEDED` names loads
once instead of recursing off the stack. Each is relocated against the
published modules and the set, all are protected, and only then is the set
published, so a refused load leaves nothing half-loaded; constructors run
afterwards, each library's after those of the libraries it names.

Nothing is skipped silently any more. Every relocation type but NONE that
the loader does not apply (COPY, IRELATIVE, any unknown number) is refused
by number; R_X86_64_64 and R_AARCH64_ABS64 are applied as S + A. A symbol
nothing defines is refused by name, but a weak one, which is 0, and a
call slot in an image not bound now, which is bound to a trap that fails
at the call as a lazy loader would (a Rust library's `main`, which its
std's `_start` names). A DT_NEEDED naming nothing, and a library segment
both writable and executable, are refused by name.

The executable relocates itself before its main thread's thread-local
storage is copied from its template, so `toyos::relocate` no longer
refuses a write into the template; the start's executable pass, which
runs after that copy, writes what lands in the template into the main
thread's block too. `RelaCounts`, whose only user was the kernel loader,
goes with its comment about shared read-only pages.

`abuse_kernel_addr` holds SYS_SET_THREAD_POINTER's refusal of a
non-canonical, a kernel and the last address. The crafted-ELF cases'
comments lose their history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fork's ToyOS backend (1eb1665) calls toyos::dl again instead of
linking libtoyos_c, and toyos brings toyos-elf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's decision: a parent lays out its child's program, the build
lays out the kernel and the supervisor, and the ELF decoder is a module of
`toyos`, so `toyos-elf` is no crate and is never published.

Spawn: `SpawnArgs` carries the child's memory as ranges (`SpawnMap`), the
entry, the image base and the build-id, in place of an image the kernel
parsed. Each range is checked as an ordinary mapping is, all or nothing:
whole pages of the user half, a `VmProt`, and a copy only of ordinary
memory the object, held with `MAP`, holds from a page boundary; a range
meeting another or the clock page, and an entry in no executable range,
are refused. A copied range is the existing private kind whose pages are
copied out of the object at their first touch, so pages are shared as
before. `toyos::elf::program::lay_out` is the layout, which `Helper::spawn`
and the build both use; the kernel's `file_backing` goes.

Boot: the signed image's kernel section is `toyos_kimage`: the kernel at
its own addresses, its RELATIVE relocations, its ranges and RELRO, a symbol
table that resolves as the ELF table does, and the supervisor's program.
The build writes it (`build::kimage`); the loader copies the image and
applies the relocations; the kernel takes its page protections, its
symbols and the supervisor's ranges from it.

`toyos::elf` holds `lib/elf`'s source and tests. `lib/symbols` keeps what
reads no ELF (frame records, the name budget); finding an image's symbol
tables and build-id is `toyos::elf`'s, and `symbolize` names a recorded
frame with it. The build's gates read the kernel with it too.
`toyos-elf` leaves `PUBLISHED`, std's patched crates and the rust fork's
lock (a189b3a6b33).

The crafted-ELF guest tests go with the kernel's parse: their headers are
the parent's layout, which the host tests hold. `abuse_spawn_maps` holds
the kernel's new boundary, `start_refuses_itself` a program's refusal of
itself, and `spawn_image_object` the object's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main reworked sys_query_modules, which this branch deletes, and moved
the capability record into the manual, which drops this branch's edit
to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lints

The merge left vm.rs's conflict markers in, where main reworked code
this branch deletes. toyos-kimage's writer is part of the crate rather
than a feature the model-control gate would read as a control, and the
kernel's symbol table, now a borrow of the section, needs no
hand-written Send or Sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_test_parts returned the memoized kernel ELF on a full memo hit,
so the second boot of one config in a process signed an ELF into the
slot's kernel section, which the loader refuses (not TOYOSKRN). Guest
rows run 42080820355 red virt_random_differs's second boot so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guest rows run 42089538985 stalled session_lifecycle so: the supervisor
answered the compositor's ask with the desktop session whose owner had
just exited, before it read the exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu Japabu changed the title The loader is the program's: TLS and dlopen leave the kernel The loader is the program's, and the kernel parses no ELF Nov 5, 2026
@Japabu

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review r3 of #1189 at bfb75f056 (nothing in the kernel parses an ELF)

Architecture

Round 3 implements the owner's decision of 2026-11-05 as ruled. Each part has the right owner:

  • The parent lays out its child (toyos::elf::program::lay_out).
  • The build lays out the kernel and the supervisor (build::kimage, lib/kimage).
  • The kernel only checks ranges as ordinary mappings.

The kernel and the loader link no ELF code. In kernel/src and loader/src, the only ELF matches are in comments, listed under B4. kernel/Cargo.toml and loader/Cargo.toml name toyos-kimage (which depends only on toyos-abi) and no toyos-elf. The kernel does not reach toyos: toyos-symbols depends on toyos-abi, toyos-elide and rustc-demangle.

The forks are coherent:

  • The rust gitlink is a189b3a6b33, which origin/main of the fork contains and which descends from main's pin aa0d9be2c. Its library/Cargo.lock names toyos and toyos-abi only.
  • toyos-elf appears in PUBLISHED, SYSROOT_SOURCES and SYSROOT_MANIFESTS neither at this head nor on main, so its round-2 addition is fully reverted.

Sizes, measured at origin/main and at this head:

  • kernel/src goes from 224 files and 58,050 lines to 218 and 56,392, as claimed.
  • The branch as a whole is +4,691/−5,925. Of that, tests are +1,280/−2,188 and production plus records +3,411/−3,737.
  • The consequential additions are lib/kimage (~380 lines), build/kimage.rs (169) and toyos::elf::program (116). They replace the loader's relocation pass, the kernel's spawn-time ELF parse, file_backing.rs and mm::image's ELF reading.

Spawn boundary (security)

I traced every check in spawn_program and Map::checked (kernel/src/syscall/proc.rs:72, kernel/src/loader/mod.rs:62), then lay and spawn.

  • Counts and lengths: the count is 1..=64. i * size_of::<SpawnMap>() is therefore bounded, and maps_ptr + … is checked_add; copy_in validates the user pointer. build_id_len is ≤ 32 before user_vec.
  • Integer overflow:
    • addr + len is checked_add and ≤ USER_TOP.
    • offset + copied is checked_add and ≤ object.size(). Unchecked, an offset near u64::MAX would wrap past the size check, and the first-touch SharedMemObject::read would hit each_piece's assert: a userland-triggered kernel panic. The check is present.
    • vacant → pages is checked_add again, and the entry test's at + len cannot overflow after the USER_TOP bound.
  • W^X: it holds by type. vm_prot maps only None/Read/ReadWrite/ReadExec, and the new kernel/pure test pins the four-way match.
  • Copy kind: it is the existing Kind::Copy. Its authority is MAP on a SharedMemObject in the caller's own table, which is the same right shared_image demanded on main. is_ordinary refuses an aperture, and the offset must be a page boundary. Semantics are unchanged from file_backing::SharedImage: each page is read once at first touch, and a later write by a holder reaches only that holder's own child.
  • No cross-process right: objects are looked up in the caller's table and cloned as Arcs before spawn_place or an endowment can move a handle. place keeps its WRITE check.
  • Clock page and overlap: vacant refuses both, through holds (the space's floor and top) and is_free, before every At::Fixed placement. Without it, put would replace what is there, and the T14 control proves that.
  • Entry: it must lie inside a ReadExec range, and Entry::new re-checks it.
  • image_base: it is a label only. frame_offset uses checked_sub.

abuse_spawn_maps covers every refusal class the PR names. The T14 control mut14 is real: with the vacancy check disabled, "a range on the clock page" was answered None, the child ran as pid 16, and the member exited 101. The earlier cases were still refused, so Map::checked alone refuses "past the user half". Three refusals have no guest case: an unaligned offset, a wrapping offset + copied, and an aperture object (S1).

lib/kimage

  • Parse: it reads its counts against the section with checked arithmetic (take). It refuses unordered or overlapping ranges, ranges past memory, not exactly one executable range, an entry past memory, RELRO outside memory, a relocation whose offset + 8 or addend lies past memory, unsorted symbols, and names past the table or not in UTF-8. Both the loader and the kernel check machine.
  • Relocation: for each entry the loader writes PHYS_OFFSET + process_mem + addend at offset. On main it wrote PHYS_OFFSET + process_mem + (min + target) at r_offset, and build::kimage emits exactly (r_offset, min + target), so the two agree. Every store lies in bounds because parse checked it, and slice indexing would panic otherwise.
  • Symbol table: chosen keeps, per address, the first STT_FUNC with a nonzero value, replaced only by the first sized one. That is the order SymTab::resolve prefers: highest value, sized over unsized, first among equals. Both answer None past a sized symbol's end and for an empty name. The host test over 0x6_0000 sampled offsets plus the crafted tie case (mut10) establishes equivalence. The kernel's base is unchanged (PHYS_OFFSET + kernel_memory_addr).

The supervisor's precomputed program

lay_out runs once in the build over the same userland.supervisor bytes that are signed as the supervisor section. Both shipped_parts and both build_test_parts arms produce the section, and the memo arm is the virt_random_differs fix. The kernel feeds each map through Map::checked and lay, exactly as a spawn's, and panics by name on a refusal. That is the right fence for signed input.

Earlier findings

The r1 fixes hold at this head:

  • B1: rela::parse answers Ok(None) only for NONE and refuses Other by number. A strong undefined symbol is Refused::Undefined, and a weak one binds to 0.
  • B2 and B3: the set is published only after the whole load, breadth first with a name check, and constructors run after publication.
  • B4: Pass::Executable mirrors template writes into the main thread's block.
  • B6: the file is gone on main.

S2 was adopted in abuse_kernel_addr. I found no r2 review text to check beyond these.

session_lifecycle: I accept it as independent

teardown closes a process's handles (teardown_resources → close_all) before it publishes the Exit, and it holds that exit until every end below it is published. So the compositor can see the session's controls Gone while the supervisor's exit_of (a WNOHANG process_wait) still answers "running". That window exists on main.

This PR touches neither sessions.rs, signout.rs nor that ordering. Its only teardown edits remove TLS work before close_all and ELF frees after it, which if anything narrow the window. The row passed in three earlier runs at this branch.

Findings

BLOCKER B1: stale or false comments and docs left by the move

  • sdk/toyos/src/dl/image/mod.rs:2,7, sdk/toyos/src/dl/mod.rs:52, sdk/libc/src/elfsym.rs:2, sdk/libc/arch/src/dladdr_symbols.rs:2 and sdk/toyos/tests/elf_note.rs:50 (an assertion message) still name toyos-elf, a crate that no longer exists.
  • sdk/toyos/tests/elf_real.rs:7-11:
    • "the shape the loader actually meets at every boot" is no longer true: the boot loader parses no ELF.
    • The refresh command writes to lib/elf/tests/fixtures/lld-headers.bin, a directory that no longer exists. The fixture now lives at sdk/toyos/tests/elf/fixtures/.
  • lib/symbols/src/frame.rs:4-6:
    • "[crate::name] names the function": toyos_symbols::name moved to system/symbolize, so this is a broken intra-doc link.
    • "the build-id the file carries" is now the build-id the parent handed the spawn (SpawnArgs::build_id_ptr).
  • lib/symbols/src/lib.rs:6-9: "a SymbolTable holding raw pointers into the kernel image". It now holds a toyos_kimage::Symbols<'static> slice, and the raw-pointer Send/Sync pair was deleted, as sourcegate's table records.
  • kernel/src/loader/mod.rs:47,176 reference syscall::proc::spawn_maps. The function is spawn_program.

BLOCKER B2: lay_out's rationale for W+X is false, and the policy contradicts toyos::dl (sdk/toyos/src/elf/program.rs:49-50)

  • The comment says "taking the execution away would let a hostile file run as data instead". Mapping such a segment ReadWrite would not let anything run. Either choice only makes the program fault later: a write under ReadExec or a fetch under ReadWrite.
  • In the same crate, toyos::dl refuses the identical input by name ("a library's PT_LOAD is both writable and executable"), as round 2 decided.
  • One toyos::elf should have one policy. The simpler fix, and the one that matches "fail clearly", is for lay_out to refuse a W+X segment by name and for its test and mut11 to assert the refusal. If keeping main's old map-it-RX behaviour is intended, replace the comment with that true contract and state why executables and libraries differ.

BLOCKER B3: a dead dependency (lib/symbols/Cargo.toml:20-22, Cargo.lock)

  • object remains a dev-dependency of toyos-symbols, with its fixture-alignment comment, but the tests that used it moved to sdk/toyos/tests/elf_symbols.rs. The only remaining test, lib/symbols/tests/frame.rs, does not use it.
  • Delete the dependency.

BLOCKER B4: the slot's toyos/kernel.elf is no longer an ELF (lib/update/src/slots.rs:36, KERNEL_FILE)

  • The file now holds the TOYOSKRN section. Anyone who opens the slot, a person or a tool, is told the wrong format.
  • Rename it to something like toyos/kernel or toyos/kernel.img: one constant, with no compatibility obligation.
  • Correct the comments that still describe the handed bytes as an ELF:
    • loader/src/main.rs:459 ("beside its ELF");
    • loader/src/main.rs:893 ("its ELF, its parameter…");
    • loader/src/arch/x86_64.rs:136 ("kernel.elf's entry point");
    • sdk/abi/src/boot.rs:52.

SUGGESTION S1: three cheap abuse_spawn_maps cases

  • An offset that is not page-aligned.
  • offset = 0xffff_ffff_ffff_f000 with copied = 0x1000, the wrap that would otherwise reach each_piece's assert at first touch.
  • Optionally a handle to a non-SharedMemObject.

Inspection establishes all three today. Each is one line in a boot that already runs, and the second guards a kernel panic.

SUGGESTION S2: kernel/src/loader/mod.rs:173-185, 290. layout={}ms now times only the entry check. Drop t1 and the field rather than log a number that measures nothing.

SUGGESTION S3: e_machine is declared as 62/183 in build/kimage.rs:17-22, loader/src/arch/*.rs and kernel/src/arch/*/mod.rs, beside toyos::elf::header. toyos_kimage could own the two constants (or a small enum) that the writer and both readers share.

SUGGESTION S4: toyos_kimage could carry the length of the supervisor section it laid out. spawn_supervisor would then assert that images.supervisor matches, a one-word fence against a section built from other bytes. Today only offset + copied ≤ size catches such a mismatch.

Evidence checked

  • Guest rows: run 42089538985 has headSha bb1e9ada6 and 18 of 19 rows passed. The red is session_lifecycle, assessed above, and virt_random_differs is green.
  • Host CI: round3/hostci.log names bb1e9ada6. The local run started after that commit and records 108 steps all green with rc=0. bfb75f056 adds only an issue record.
  • T14: round3/metal-bb1e9ada6/judge.log shows 17 passed, 0 failed over 2 boots.
  • Guest control: control-mut14/member.txt shows the expected red, and the patch weakens only lay's vacancy check.
  • Note for the orchestrator: round3/README.md still says of the control "Not yet run" in the bullet above its own result.

SEND BACK (B1–B4). Each is a comment, manifest or name fix. None touches the spawn boundary's logic, so no further guest or metal run is needed unless B2 is resolved by refusing, which a host test covers.

Japabu and others added 2 commits November 5, 2026 21:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The slot's kernel file is `toyos/kernel.img`: it holds the kernel
  section (`toyos_kimage`), not an ELF. The build and the loader name it
  through `slots::KERNEL_FILE` alone; the comments that called the handed
  bytes an ELF say what they are.
- `toyos::elf::program::lay_out` refuses a segment both writable and
  executable by name, as the loader refuses a library's: one policy, and
  either protection would only have made the program fault later.
- `toyos_kimage` owns the `e_machine` numbers the build, the loader and the
  kernel share, and records the supervisor section's length, which the
  kernel checks against the section the loader handed it.
- `abuse_spawn_maps` refuses a copy from no page boundary of its object,
  and one whose end wraps, which unchecked would read past the object in
  the kernel at the first touch.
- The spawn record drops `layout=`, which timed nothing any more.
- `toyos-symbols` drops its dead `object` dev-dependency, and the comments
  that still named `toyos-elf`, `lib/elf`, `crate::name`, raw-pointer symbol
  tables or `spawn_maps` say what is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review r4 of #1189 at 61152480a (follow-up to r3)

Scope: bfb75f056..61152480a, which is the merge 7a72146db plus 61152480a (26 files, +72/−52). The main-side content is excluded except where it touches this PR.

Architecture

The round-3 shape is unchanged and still right. The two additions fit their owners:

  • toyos_kimage now owns the e_machine numbers that its writer and both readers share. The kernel's and the loader's arch E_MACHINE remain the per-arch selector, which is the right place for it.
  • The section records the supervisor section's length. The kernel asserts it at boot (kernel/src/main.rs:321), before anything is spawned from it. That is earlier than the spawn_supervisor placement S4 proposed, and equally simple.

All three kimage::section call sites (build/build.rs:2085, 2259, 2309) pass the same userland.supervisor bytes that are signed as the section. The loader's BootImage.len is the exact payload length (loader/src/payload.rs:61), not a page-rounded one, so the equality is the right comparison.

r3 blockers

  • B1 (stale comments): resolved.
    • Every listed site is fixed.
    • The elf_real.rs refresh path points at sdk/toyos/tests/elf/fixtures/lld-headers.bin, which exists.
    • frame.rs no longer links to crate::name, and /system/bin/symbolize is in images/system.toml.
    • lib/symbols' header names toyos_kimage::Symbols, which kernel/src/symbols.rs:16 holds.
    • Outside issues/, spawn_maps survives only as abuse_spawn_maps. One new stale record is listed under B6 below.
  • B2 (W+X): resolved.
    • lay_out refuses a W+X segment by name (sdk/toyos/src/elf/program.rs:81). The doc states the true contract.
    • The host test asserts the refusal. The object comparison no longer maps 3 to ReadExec, which is unreachable now anyway, since such an image is refused before the comparison.
    • The control round4/controls/mut15 removes exactly the refusal and goes red on a_writable_executable_segment_is_refused.
    • The policy now matches toyos::dl. Two one-line checks with their own messages are fine.
  • B3 (dead dependency): resolved. The [dev-dependencies] section and the Cargo.lock edge are both gone.
  • B4 (kernel.elf): resolved.
    • KERNEL_FILE is toyos/kernel.img. Every consumer (build/image.rs, loader/src/slot.rs, system/update) names the constant, and no literal remains.
    • The four comments are corrected. boot.rs' "beside the loader" is accurate: create_esp_volume writes toyos/log.guid on the ESP with the loader (build/image.rs:593).
    • The kernel.elf: prefixes left in build/kimage.rs describe the linked ELF, which is still an ELF. See S5.

Suggestions from r3

  • S1, adopted.
    • "A copy from no page boundary" and "a copy whose end wraps" are each refused at the intended line.
      • The wrap case: 0xffff_ffff_ffff_f000 + 0x1000 passes the alignment and copied <= len checks and then fails checked_add in Map::checked (kernel/src/loader/mod.rs:78), so it guards the line that matters.
      • The T14 log at 61152480a (round4/metal-61152480a/kernel.log:499-500) shows both answered InvalidArgument.
    • The wrong-kind-handle case was declined, but I found no recorded reason on the PR or in round4/README.md. I accept the decline on inspection:
      • The lookup is the typed handles.get::<SharedMemObject> that every handle syscall shares.
      • A wrong type goes to HandleError::WrongType, and from there to the process's handle-fault policy (kernel/src/object/handle.rs:54), not to an error this test could assert.
      • It was optional in r3.
  • S2, adopted. t1 and layout= are gone. tests/checks/claims.rs:37 still carries a sample line with layout=0ms tls=0ms. Its check keys on the prefix only, so this is cosmetic, and it was already stale before this round.
  • S3, adopted.
  • S4, adopted.
    • PROGRAM is 72. len is read at offset 64, after the maps count at 56.
    • The writer emits it in that order, and the round-trip test reads back a non-default 0x1234.
    • FORMAT stays 1. That is fine, because format 1 has never landed.

The merge 7a72146db

git show --remerge-diff is empty. The 26 files main brought in (#1197's records and docs, and kernel/pure/sched/cpu.rs) do not intersect the PR's 176 files, so there was no textual conflict to resolve.

Semantically:

  • docs/ has no ELF, TLS or dlopen claim that this PR falsifies. Only docs/src/using/interface.md:48's list of what works mentions thread-locals and dynamic loading, and it remains true.
  • One issue record that main rewrote keeps a name this PR renamed (B6). The same phrase was already there before the merge, so the merge did not cause it.

Update compatibility statement

The Risks paragraph is accurate:

  • system/update/src/install/mod.rs:108-114 writes only the slot's volume (signed header, kernel, cmdline, supervisor, fileserver) and never the ESP's loader.
  • An older updater would therefore write the new section under toyos/kernel.elf, and the old loader would parse it as an ELF and refuse it.
  • The metal loop flashes the whole image every run (tests/metal/src/lib.rs FLASHABLE and the flash phase; the round-4 judge log shows flash 14.9 s (dd 10.6 s)).
  • I found no guest test, workflow or nightly step that installs an image from an earlier revision.

Nothing on the bench depends on an in-place update across this change.

Findings

BLOCKER B5: false comment on the wrap case (tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs:85-86)

  • The comment says that, unchecked, "the first touch reads past the object's end in the kernel". It would not read past the end. SharedMemObject::each_piece asserts offset + len <= size (kernel/src/object/shm.rs:244), so the first touch would panic the kernel: a userland-triggered kernel panic, as r3 described.
  • That panic is the reason the case exists. The comment misstates the stake.
  • Fix: say it would panic the kernel at the first touch, or delete the comment, since the case's name already says what is refused.
  • The commit message repeats the claim. History can stay as it is.

BLOCKER B6: an issue record names the function this PR renamed (issues/the-loader-does-only-what-must-precede-the-handover.md:212)

  • Stage 5 says its PR lists "every load_kernel_elf refusal of the kernel's bytes". This PR replaced load_kernel_elf with load_kernel(section) (loader/src/main.rs:252), which parses no ELF and fences the signed section through toyos_kimage::Kernel::parse.
  • The record is directly affected by this change and now points at a function and a format that no longer exist.
  • Fix: name load_kernel and the section, or the parse it applies. This is one phrase.

SUGGESTION S5: build/kimage.rs:27-78. The kernel.elf: panic prefixes now name no file anywhere: the build's artifact has no such name, and the slot file is kernel.img. "the kernel's ELF:" would not send a reader looking for a file.

SUGGESTION S6: lib/kimage/src/lib.rs:39-40. "what the build writes and the loader and the kernel each check is their own" does not parse. Something like "what the build writes, and what the loader and the kernel each check against their own" would.

SUGGESTION S7 (evidence, for the orchestrator): round4/README.md states the 221-program readelf -lW scan result, but publishes neither its command line nor its output. The claim matters, because an image with a W+X PT_LOAD is now refused at spawn, and the guest and metal rows run only a subset of the 221. Publishing the command and its per-file output would make the claim checkable.

Evidence checked

  • Guest rows: run 42103839687 is a workflow_dispatch of guest-rows with headSha 61152480a, conclusion success. rows-42103839687.txt shows 19 of 19, session_lifecycle and virt_random_differs among them.
  • Host CI: round4/revision is 61152480a, and the README records 108 steps green with exit 0, plus --build-only exit 0.
  • T14: round4/metal-61152480a/judge.log shows 16 passed, 0 failed in 1 boot. The kernel log shows every abuse_spawn_maps refusal, the two new cases included, and the spawn line in its new (total=…) form.
  • Control: mut15.patch deletes only the three-line refusal. The log shows the expected assertion failure (Ok(Program{… prot: 3 …}) against the refusal) with test rc 101, followed by the restore.
  • What a fix needs: neither blocker touches code that runs. Fixing B5 and B6 changes a comment and a record, so no build or test rerun is needed.

SEND BACK (B5, B6). Both are one-line text fixes, and the r3 blockers are all resolved.

- `abuse_spawn_maps`' wrapping copy would panic the kernel at its first
  touch, where `SharedMemObject` bounds every read: the comment says so.
- The loader track's stage 5 names `load_kernel` and the kernel section's
  parse, not the `load_kernel_elf` this change replaced.
- `build::kimage`'s refusals say "the kernel's ELF", which names no file.
- `toyos_kimage::EM_*`'s comment parses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the r4 fix (6115248..842f595), which changes no behaviour:

  • The diff touches four files. The .rs changes are panic-message strings in build/kimage.rs (kernel.elf: → the kernel's ELF:) and comments; the rest is the loader-track record.
  • B5 is resolved: the wrap case's comment states the real stake, a kernel panic at first touch.
  • B6 is resolved: stage 5 names load_kernel and toyos_kimage::Kernel::parse.
  • S5 and S6 are taken. S7: the W+X scan's command, per-program output and positive control are published under faa22272 round4/, after the first detector was found not to fire and was replaced.
  • The round-4 evidence (host CI 108/108, guest 19/19, T14 16/16, controls) applies unchanged.

Landing.

@Japabu
Japabu marked this pull request as ready for review November 5, 2026 21:31
@Japabu
Japabu enabled auto-merge November 5, 2026 21:31
@Japabu
Japabu added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 0b2dec6 Nov 5, 2026
10 checks passed
@Japabu
Japabu deleted the kernel-no-tls branch November 5, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant