Skip to content

A return to userland takes an entry inside the user half, and nothing is placed in its last page - #757

Merged
Japabu merged 3 commits into
mainfrom
wt/toyos-threadentry
Oct 8, 2026
Merged

Japabu merged 3 commits into
mainfrom
wt/toyos-threadentry

Conversation

@Japabu

@Japabu Japabu commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

The defect

SYS_THREAD_SPAWN bounded the stack and passed the entry on unread, so the new thread's first return to userland carried whatever address the caller wrote. What that does on a real processor is read, not seen: Intel's SDM has IRET to an address that is not canonical fault in the returning instruction, in the kernel's ring, and fatal_exception halts every CPU on a fault taken there, so one process could take the machine. What was seen is the unfixed kernel under TCG, which completes the return and faults at the fetch in Ring 3: the caller ends and nothing else does. Root CLAUDE.md: input that crossed a trust boundary is refused.

What changed, per decision

  • toyos_userbound::Entry is an instruction pointer inside the user half. It is decided by is_user_addr, the tree's one notion of a user address, and has one constructor.
  • loader::Start is the only way to a trampoline. alloc_kernel_stack took a trampoline and three bare words; it now takes Start::{Process, Thread, Kernel}, whose two userland arms carry an Entry, and loader no longer re-exports the trampolines. An unchecked address cannot be handed to a return to userland without naming arch::entry directly.
  • sys_thread_spawn refuses what Entry refuses with InvalidArgument. That covers the kernel half too, which is canonical and only ever ended the caller, because one definition is cheaper than two.
  • Nothing is placed in the last 2 MiB page of the user half. PLACED_TOP is that bound, one declaration with two readers: rebase_base refuses an image that reaches past it, and Window::new asserts a placement window's ceiling against it. The kernel's window is a const, so one above it does not compile. Before round 1 the window's half of this rested on ALLOC_CEILING happening to be STACK_BASE.

Every other path by which a user-chosen instruction pointer reaches a return

path held by
a process's entry (e_entry) toyos-elf holds it inside the image and rebase_base holds the image below PLACED_TOP. It is now made an Entry as well, because the type asks.
a saved user frame No syscall writes one. There is no signal delivery, no resumption at a registered address and no context to set, and the frame lives on a kernel stack.
the stack pointer of a new thread Neither return instruction checks it. It is the thread's own to fault on, as it is for any running thread.
AArch64 A bad ELR_EL1 is taken as an abort from EL0 after the ERET. Entry applies there all the same.
an instruction ending at the last byte of the user half Nothing held this. Its successor address is the first past the user half, which is not canonical, and that is what a syscall or an interrupt taken there returns to. The syscall return is the worse half: kernel/src/arch/x86_64/syscall.rs runs sysretq after pop rsp, so on an Intel processor the fault a noncanonical rcx raises is taken in Ring 0 on the stack pointer the caller chose. That is a kernel exception frame written wherever a process points, which is more than a halt. Read from the SDM and the code, executed by nobody. Executable memory comes from two places, an image's segments and a library placed through the kernel's window, and PLACED_TOP now bounds both; sys_mmap makes no executable mapping.

Round 1

  • The standalone guest test is gone: spawn_noncanonical_entry.rs, its RUST_SKIP entry, the MACHINE_TESTS entry and the arm. The three refusals are in tests/toyos-rust-tests/src/bin/abuse_thread_table.rs, the T14's process_bound_threads row.
  • Why not abuse_tls_alloc, which the brief preferred so that CI would hold the word. No QEMU boot runs a discovered Rust binary. The suite's QEMU half is MACHINE_TESTS and SCREEN_TESTS (select, tests/toyos.rs), and shared_metal is the only runner of the rest. Measured at the round's first commit: cargo test --test toyos-build -- abuse_tls_alloc exits 1 with No test matches filter Some("abuse_tls_alloc"), and the same for abuse_elf_loader, std_t, std_sync, 124_atomic_counter and 202_errno_per_thread. So either binary holds the word on the T14 and neither in CI; abuse_thread_table has a row of its own to ask for by name, and its boot carries three jobs where a shared boot carries a chunk.
  • The named binaries are the T14's; the QEMU suite is CI's, and both have run. The binaries the first BLOCKER names are T14 shared-boot members; CI's suite never runs them, and round 2's review ruled that no T14 boot beyond process_bound_threads is owed for them. What CI's suite reaches is every boot's process and thread spawns, through rebase_base and alloc_kernel_stack, on both architectures: guest / suite at this head is green, 30 of 30 (run 37752723738, job 113230606172; x86-64 under KVM, AArch64 emulated).

Gates, at b00560c4b

command exit
cargo run -- --ci host 0 (Host: 77 step(s), all green)
cargo run -- --build-only 0
cargo test -p toyos-userbound 0 (43 passed)
cargo test --test toyos-build -- machine_shutdown 0 (2 passed, 2 total: machine_shutdown, machine_shutdown_short_stop)
cargo test --test toyos-build -- --metal --metal-readback <dir> process_bound_threads staged by the implementer (exit 2, one image, testcases-bounds), then booted on the T14 by the orchestrator at this head: boot exit 0, judge exit 0, [metal] 1 passed, 0 failed, 1 boot(s), ===TEST_END test_rs_abuse_thread_table exit=0===, image sha256 db1f156d…47e10b07 (#757 (comment))
CI run 37752723738 at b00560c4b host success (job 113229625347), toolchain / build success (113229626035), guest / suite success (113230606172): test result: ok. 30 passed, 30 total, [ci] Guest: 5 step(s), all green

Where the moved assertions have run: on the T14, in the process_bound_threads row above, on the fixed kernel. The whole QEMU suite ran in CI at this head, not locally. The machine_shutdown filter is the one local boot of the fixed kernel at this head: x86-64 under TCG, tests/testcases, every server spawned.

The checks of high-risk code (the syscall ABI, a security boundary)

Negative control, measured at a45d98269 with the standalone binary and standing as the record of the defect. The whole fix reverted (kernel/, toyos-userbound/, toyos-abi/ back to origin/main, the tests kept), as a checked patch applied and restored by one script:

arm command exit
fix reverted cargo test --test toyos-build -- thread_entry_noncanonical --nocapture 1
Entry::new accepts every address (m1) the same 1
FAULT rip=0x0100000000000000 cr2=0x0000000000000000 err=0x0000000000000000 ... tid=1
SIGBUS tid=1: general protection fault (error_code=0x0)
===TEST_END test_rs_spawn_noncanonical_entry exit=-1===

Host mutations, at b00560c4b, each checked, applied, run and reversed by one script, the tree clean after (git status --porcelain empty):

mutation cargo test -p toyos-userbound red test
m1: Entry::new accepts every address 101 an_entry_is_an_address_of_the_user_half_and_nothing_else
m2: rebase_base bounds by USER_TOP 101 an_image_that_reaches_the_last_page_of_the_user_half_is_refused
m3: Window::new admits a ceiling of USER_TOP 101 a_window_reaching_the_last_page_of_the_user_half_is_a_kernel_bug

m1 was not booted on the T14, and round 2's review ruled it is not owed. m1 reds on the host at this head, and removing the check from sys_thread_spawn does not compile, since spawn_thread takes an Entry. On the T14 an m1 kernel is the removed defect on an Intel processor; the likely reading is a halted machine, which measures the severity of what was removed and nothing about this change.

Independent oracle. Intel's SDM, for IRET and for SYSRET. CI's runner is not an instrument for either claim: the workflow does not choose its processor's vendor, and where a return to a noncanonical address faults is vendor behaviour, documented so for SYSRET. The machine that could execute the dangerous half is the T14, which the issue named, with control-revert.patch applied. The review rules that reading is not required to land, since the fixed kernel executes no return to an address Entry refuses.

Why the behaviour is on a metal row

The type holds that no unchecked entry reaches a trampoline, and the host table holds the predicate. Neither reaches the word the syscall answers its caller: kernel/src/syscall/ links into no host test, and the refusal, as opposed to a kill or another error word, is what std and libc will read. A metal row reaches it, in a binary that already spawns raw threads on mapped stacks and asserts a refusal by name, on a boot the table already has. No guest test is added or changed.

What I am unsure of

  • The last-page hole is read off the code and the architecture's documents and was executed by nobody. No image or library that reaches the last page was built. The refusal is tested at rebase_base and Window::new on the host and nowhere else.
  • The bound is 2 MiB although regions are granted at 4 KiB. I took the kernel's one user page size over the smallest sufficient guard.
  • The loader's new Entry::new refusal is unreachable while toyos-elf and rebase_base hold. It is there because the type has no unchecked constructor, and it refuses instead of panicking because the number is the file's.
  • The moved assertions spawn without waiting on an accepted thread. On a regressed kernel the job is red either way, by the assertion or by the fault, but which comes first is a race.

The issue

issues/a-thread-entry-is-unchecked-and-a-noncanonical-one-is-measured-only-under-tcg.md was filed on wt/toyos-audit-kernel (pull request #754, since closed without landing) and is not on main. This branch carries it unchanged in its first commit and deletes it in the fix, with its one durable line at sys_thread_spawn. No other file cites the slug.

Growth

git diff --shortstat origin/main...b00560c4b: 10 files changed, 123 insertions(+), 40 deletions(-). Tests are +38 −7 (span.rs tests +18 −3, place.rs tests +7 −3, abuse_thread_table.rs +13 −1); production is the rest, +85 −33: the type and the bound in toyos-userbound, Start in the loader, the refusal.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A

Japabu and others added 2 commits October 8, 2026 10:06
The file is the audit's, filed on wt/toyos-audit-kernel (draft pull request
754) and not yet on main. It is carried here unchanged so that it lands with
its fix and is deleted by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…e stops a page short of its top

SYS_THREAD_SPAWN bounded the stack and passed the entry on unread, so the
first return to userland of the new thread carried whatever the caller
wrote. For an address that is not canonical the architecture faults in the
returning instruction itself, in the kernel's ring, where the fatal path
halts every CPU. Under QEMU's TCG the emulator completes the return and
faults at the fetch in Ring 3, which is why the audit that found this
(issues/a-thread-entry-is-unchecked-and-a-noncanonical-one-is-measured-only-under-tcg.md,
deleted here) saw only the one process end.

toyos_userbound::Entry is an instruction pointer inside the user half, by
is_user_addr and by no second definition, and it has one constructor.
loader::Start is now the only way to a trampoline: its Process and Thread
arms carry an Entry, its Kernel arm a kernel thread's body, and the
trampolines are no longer re-exported from loader. sys_thread_spawn refuses
what Entry refuses with InvalidArgument; the loader's entry, already inside
an image rebase_base placed, is made one the same way.

The other paths by which a user-chosen instruction pointer reaches a return:

- a process's entry is e_entry, which toyos-elf holds inside the image, and
  the image is held inside the user half by rebase_base;
- no syscall writes a saved user frame: there is no signal delivery, no
  resumption at a registered address and no context to set, and a saved
  frame lives on a kernel stack;
- the stack pointer is not checked by either return instruction and is the
  thread's own to fault on;
- AArch64 takes a bad ELR_EL1 as an abort from EL0 after the ERET;
- an instruction that ends at the last byte of the user half leaves the first
  address past it, which is not canonical, as its successor: what a syscall
  placed there returns to through SYSRET, and what an interrupt taken after
  any instruction there returns to through IRET. Nothing held this.
  rebase_base allowed an image to end exactly at USER_TOP, and the image is
  the only executable mapping that can reach it (libraries and anonymous
  memory are placed below ALLOC_CEILING, and anonymous memory is never
  executable). It now refuses an image that reaches the last 2 MiB page.
  This one is read off the code and the architecture's documents; nothing
  here executed it, and TCG would not show it.

Tests: the host table in toyos-userbound holds Entry's edges and the image
bound; thread_entry_noncanonical boots tests/testcases alone and runs
spawn_noncanonical_entry, which asks for a thread at an address that is not
canonical, at the first past the user half and at the first of the kernel
half, and wants InvalidArgument for each. The binary is on RUST_SKIP: a
kernel that took the entry would take a shared boot with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Control and mutation patches, as run against a45d98269

Each was checked (git apply --check), applied, run, and reversed by one script; exits are in the body.

control-revert.patch

diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs
index cfa90ffcb..deb308799 100644
--- a/kernel/src/loader/mod.rs
+++ b/kernel/src/loader/mod.rs
@@ -15,7 +15,8 @@ mod symbols;
 mod tls;
 
 pub use start::{build_child_handles, PendingHandles, SLOT_PAIR_LEN};
-pub(crate) use start::{alloc_kernel_stack, Start};
+pub(crate) use start::alloc_kernel_stack;
+pub(crate) use crate::arch::entry::{kernel_start, process_start, thread_start};
 pub use tls::{TlsBlock, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT};
 
 use alloc::string::String;
@@ -567,12 +568,7 @@ pub fn spawn<H>(
         return Err(SyscallError::ResourceExhausted.into());
     };
 
-    // Inside the image, which `rebase_base` placed inside the user half.
-    let Some(entry) = toyos_userbound::Entry::new((image_start + layout.entry().get()).raw())
-    else {
-        log!("spawn: {}: the entry is outside the user half", path);
-        return Err(SyscallError::InvalidArgument.into());
-    };
+    let entry = (image_start + layout.entry().get()).raw();
     let image_end = (image_start + layout.span()).raw();
     let sp = user_stack.write_argv(argv);
     let t_tls = crate::clock::nanos_since_boot();
@@ -589,7 +585,7 @@ pub fn spawn<H>(
         bias: base,
     });
 
-    let (ks_alloc, ks_sp) = match alloc_kernel_stack(Start::Process { entry, sp }) {
+    let (ks_alloc, ks_sp) = match alloc_kernel_stack(process_start, entry, sp, 0) {
         Some(ks) => ks,
         None => {
             log!("spawn: {}: failed to allocate kernel stack", path);
@@ -687,7 +683,7 @@ pub fn spawn<H>(
 
     let t3 = crate::clock::nanos_since_boot();
     log!("spawn: {} pid={} tid={} dst={} base={:#x} entry={:#x} root={:#x} (layout={}ms relocs={}ms deps={}ms tls={}ms total={}ms)",
-        path, pid, tid, dst.0, base, entry.addr(), child_pt.lock().root().phys(),
+        path, pid, tid, dst.0, base, entry, child_pt.lock().root().phys(),
         (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000, (t_deps - t2) / 1_000_000,
         (t_tls - t_deps) / 1_000_000, (t3 - t0) / 1_000_000);
 
diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
index 4d67e159b..5d42b4c7a 100644
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -1,9 +1,7 @@
 //! Loads a built process onto a CPU, builds the handle table it starts with
 //! and puts its spawner's handle to it in the spawner's table. The frame a new
 //! stack starts from and the trampolines it returns into are the
-//! architecture's (`arch::entry`), and [`Start`] is the only way to one: a
-//! trampoline that returns to userland is reached with an
-//! [`Entry`](toyos_userbound::Entry) and with nothing else.
+//! architecture's (`arch::entry`).
 
 use alloc::sync::Arc;
 use alloc::vec::Vec;
@@ -23,24 +21,13 @@ use toyos_abi::syscall::{
 /// One `[child_slot, parent_handle]` pair of `SpawnArgs::slot_map_ptr`, in bytes.
 pub const SLOT_PAIR_LEN: usize = 8;
 
-/// Where a new context first runs.
-pub(crate) enum Start {
-    /// A program's first instruction, on the stack its loader wrote.
-    Process { entry: toyos_userbound::Entry, sp: u64 },
-    /// A thread's, on a stack its process chose, with its argument.
-    Thread { entry: toyos_userbound::Entry, sp: u64, arg: u64 },
-    /// A kernel thread's body, which never returns to userland.
-    Kernel { body: extern "C" fn(u64) -> !, arg: u64 },
-}
-
 /// Allocate a kernel stack and lay out the frame `context_switch` will restore.
-pub(crate) fn alloc_kernel_stack(start: Start) -> Option<(OwnedAlloc, u64)> {
-    use crate::arch::entry::{kernel_start, process_start, thread_start};
-    let (trampoline, user_entry, user_sp, arg): (unsafe extern "C" fn(), _, _, _) = match start {
-        Start::Process { entry, sp } => (process_start, entry.addr(), sp, 0),
-        Start::Thread { entry, sp, arg } => (thread_start, entry.addr(), sp, arg),
-        Start::Kernel { body, arg } => (kernel_start, body as usize as u64, 0, arg),
-    };
+pub(crate) fn alloc_kernel_stack(
+    trampoline: unsafe extern "C" fn(),
+    user_entry: u64,
+    user_sp: u64,
+    arg: u64,
+) -> Option<(OwnedAlloc, u64)> {
     let alloc = OwnedAlloc::new(KERNEL_STACK_SIZE, 4096)?;
     scheduler::write_stack_canary(&alloc);
     let top = alloc.ptr() as u64 + KERNEL_STACK_SIZE as u64;
diff --git a/kernel/src/process.rs b/kernel/src/process.rs
index 79b7a85a7..78cfa1ded 100644
--- a/kernel/src/process.rs
+++ b/kernel/src/process.rs
@@ -21,7 +21,7 @@ use crate::sched::payload::ThreadSched;
 use crate::time::{Deadline, Duration};
 use crate::{elf, pipe, scheduler};
 use crate::UserAddr;
-use crate::loader::{alloc_kernel_stack, Start, TlsBlock};
+use crate::loader::{alloc_kernel_stack, thread_start, TlsBlock};
 
 pub use toyos_abi::{Pid, Tid};
 pub use crate::scheduler::TaskId;
@@ -923,12 +923,7 @@ impl Drop for Admission {
 }
 
 /// Spawn a thread within the current process.
-pub fn spawn_thread(
-    entry: toyos_userbound::Entry,
-    stack_ptr: u64,
-    arg: u64,
-    stack_base: u64,
-) -> Option<Tid> {
+pub fn spawn_thread(entry: u64, stack_ptr: u64, arg: u64, stack_base: u64) -> Option<Tid> {
     // Phase 1: parent's data + address space (table lock dropped after).
     let parent_process = current_process();
     let (parent_addr_space, process_data_arc) = {
@@ -967,7 +962,7 @@ pub fn spawn_thread(
     };
     let tls_alloc_tcb = tls_alloc.ptr().wrapping_add(tp_offset);
 
-    let (ks_alloc, ks_sp) = match alloc_kernel_stack(Start::Thread { entry, sp: stack_ptr, arg }) {
+    let (ks_alloc, ks_sp) = match alloc_kernel_stack(thread_start, entry, stack_ptr, arg) {
         Some(ks) => ks,
         None => {
             tls_alloc.release(&parent_addr_space);
diff --git a/kernel/src/sched/kthread.rs b/kernel/src/sched/kthread.rs
index 55a711902..e63843293 100644
--- a/kernel/src/sched/kthread.rs
+++ b/kernel/src/sched/kthread.rs
@@ -1,5 +1,5 @@
 //! Kernel threads: ordinary tasks that name `mm::paging::kernel` as their
-//! address space, enter through `arch::entry::kernel_start`, and hold a process-table
+//! address space, enter through `loader::kernel_start`, and hold a process-table
 //! entry. One is preempted or stolen only at a preemption point its body reaches,
 //! and a Ring 0 loop reaches none. [`ROWS`] holds every one.
 
@@ -74,8 +74,13 @@ pub fn is_kernel_task(id: TaskId) -> bool {
 
 /// Start a kernel thread running `body(arg)` on its own kernel stack and return its scheduler faces.
 pub fn spawn(name: &str, body: extern "C" fn(u64) -> !, arg: u64) -> ThreadSched {
-    let (stack, entry_sp) = crate::loader::alloc_kernel_stack(crate::loader::Start::Kernel { body, arg })
-        .unwrap_or_else(|| panic!("kthread: no kernel stack for {name}"));
+    let (stack, entry_sp) = crate::loader::alloc_kernel_stack(
+        crate::loader::kernel_start,
+        body as usize as u64,
+        0,
+        arg,
+    )
+    .unwrap_or_else(|| panic!("kthread: no kernel stack for {name}"));
 
     // Before the table lock: a panic holding the process table hangs the machine.
     let claim = Claim::take(name);
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
index 3251510ea..e856d42a1 100644
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -128,16 +128,11 @@ pub(super) fn sys_endowments(out: &mut crate::user_ptr::UserBytesMut) -> u64 {
     needed as u64
 }
 
-/// Spawn a thread; refuses a `stack_base` above `stack_ptr` (no stack to clamp to)
-/// and an `entry` outside the user half, which the return to it would fault on
-/// in the kernel's ring. The stack pointer is the thread's own to fault on.
+/// Spawn a thread; refuses a `stack_base` above `stack_ptr` (no stack to clamp to).
 pub(super) fn sys_thread_spawn(entry: u64, stack_ptr: u64, arg: u64, stack_base: u64) -> u64 {
     if stack_base > stack_ptr {
         return SyscallError::InvalidArgument.to_u64();
     }
-    let Some(entry) = toyos_userbound::Entry::new(entry) else {
-        return SyscallError::InvalidArgument.to_u64();
-    };
     // A None here is a resource failure or teardown race, never a bad argument.
     process::spawn_thread(entry, stack_ptr, arg, stack_base)
         .map_or(SyscallError::ResourceExhausted.to_u64(), |t| t.raw() as u64)
diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs
index 259fe56cf..3e3260d78 100644
--- a/toyos-abi/src/syscall.rs
+++ b/toyos-abi/src/syscall.rs
@@ -990,8 +990,7 @@ pub fn mark_tty(handle: RawHandle) {
 pub const MAX_THREADS: usize = 4096;
 
 /// Spawn a new thread with the given entry point, stack pointer, argument, and stack base.
-/// `stack_base` is the bottom of the user stack (for stack info queries). An
-/// `entry` outside the user half is `InvalidArgument`.
+/// `stack_base` is the bottom of the user stack (for stack info queries).
 ///
 /// # Safety
 /// `entry` must be a valid function pointer and `stack`/`stack_base` must
diff --git a/toyos-userbound/src/lib.rs b/toyos-userbound/src/lib.rs
index fe80d070c..bf63df0e2 100644
--- a/toyos-userbound/src/lib.rs
+++ b/toyos-userbound/src/lib.rs
@@ -38,6 +38,6 @@ pub use place::{PageSpan, Window};
 pub use port::{port_access, IoBitmap, PortAccess, Ports, Reserved, Undeclared, IO_PORTS};
 pub use segment::{pieces, segments, Pinned, Pins, Segment};
 pub use span::{
-    align_2m_checked, in_user_half, is_user_addr, is_user_object, rebase_base, Access, Entry,
-    PAGE_2M, PAGE_4K, USER_TOP,
+    align_2m_checked, in_user_half, is_user_addr, is_user_object, rebase_base, Access, PAGE_2M,
+    PAGE_4K, USER_TOP,
 };
diff --git a/toyos-userbound/src/span.rs b/toyos-userbound/src/span.rs
index e94514fc5..e7ef60af5 100644
--- a/toyos-userbound/src/span.rs
+++ b/toyos-userbound/src/span.rs
@@ -40,33 +40,6 @@ pub fn is_user_addr(addr: u64) -> bool {
     addr < USER_TOP
 }
 
-/// An instruction pointer the kernel may return to userland at.
-///
-/// A return to an address that is not canonical faults in the returning
-/// instruction itself, in the kernel's ring and not the thread's, so one that
-/// crossed the trust boundary is refused before any frame carries it. No
-/// other constructor: a first return to userland takes this and nothing else.
-#[derive(Clone, Copy, PartialEq, Eq, Debug)]
-pub struct Entry(u64);
-
-impl Entry {
-    /// `None` for an address outside the user half; what is mapped at one
-    /// inside it is the thread's own fault to take.
-    pub fn new(addr: u64) -> Option<Self> {
-        is_user_addr(addr).then_some(Self(addr))
-    }
-
-    pub const fn addr(self) -> u64 {
-        self.0
-    }
-}
-
-/// One past the highest address an image may claim: the user half less its
-/// last page. An instruction that ends at [`USER_TOP`] leaves that address, no
-/// canonical one, as where a syscall or an interrupt taken after it returns
-/// to, so nothing executable is placed where one could end there.
-const IMAGE_TOP: u64 = USER_TOP - PAGE_2M;
-
 /// Whether `[ptr, ptr + len)` is entirely in the user half.
 ///
 /// Also the bound `sys_mmap` applies to a range it will install rather than
@@ -81,13 +54,12 @@ pub fn in_user_half(ptr: u64, len: u64) -> bool {
 
 /// The load base an `ET_DYN` image rebases to `vm_base` (`vm_base - vaddr_min`),
 /// or `None` when it cannot: a `vaddr_min` above `vm_base` underflows the
-/// subtraction, and a `span` reaching from `vm_base` past [`IMAGE_TOP`] does not
+/// subtraction, and a `span` reaching from `vm_base` past the user half does not
 /// fit. The ELF spec leaves an `ET_DYN` `p_vaddr` unconstrained, so the kernel
 /// that picks `vm_base` is the only place that can refuse one.
 pub fn rebase_base(vm_base: u64, vaddr_min: u64, span: u64) -> Option<u64> {
     let base = vm_base.checked_sub(vaddr_min)?;
-    let end = vm_base.checked_add(span)?;
-    (end <= IMAGE_TOP).then_some(base)
+    in_user_half(vm_base, span).then_some(base)
 }
 
 /// Whether the kernel may read or write a `size`-byte value of alignment
@@ -139,19 +111,6 @@ mod tests {
         assert!(is_user_addr(0));
     }
 
-    /// The last address of the user half is one, and the first that is not
-    /// canonical, the first of the kernel half and the last of all are not.
-    #[test]
-    fn an_entry_is_an_address_of_the_user_half_and_nothing_else() {
-        assert_eq!(Entry::new(USER_TOP - 1).map(Entry::addr), Some(USER_TOP - 1));
-        assert_eq!(Entry::new(0).map(Entry::addr), Some(0));
-        assert_eq!(Entry::new(USER_TOP), None);
-        assert_eq!(Entry::new(0x0100_0000_0000_0000), None);
-        assert_eq!(Entry::new(0xFFFF_7FFF_FFFF_FFFF), None);
-        assert_eq!(Entry::new(0xFFFF_8000_0000_0000), None);
-        assert_eq!(Entry::new(u64::MAX), None);
-    }
-
     #[test]
     fn a_range_ending_past_the_bound_is_refused_and_a_wrapping_one_too() {
         assert!(in_user_half(USER_TOP - 8, 8));
@@ -226,11 +185,9 @@ mod tests {
     }
 
     #[test]
-    fn an_image_that_reaches_the_last_page_of_the_user_half_is_refused() {
-        assert_eq!(rebase_base(USER_VM_BASE, 0, IMAGE_TOP - USER_VM_BASE), Some(USER_VM_BASE));
-        assert_eq!(rebase_base(USER_VM_BASE, 0, IMAGE_TOP - USER_VM_BASE + 1), None);
-        assert_eq!(rebase_base(USER_VM_BASE, 0, USER_TOP - USER_VM_BASE), None);
+    fn an_image_that_rebases_past_the_user_half_is_refused() {
+        assert_eq!(rebase_base(USER_VM_BASE, 0, USER_TOP - USER_VM_BASE), Some(USER_VM_BASE));
+        assert_eq!(rebase_base(USER_VM_BASE, 0, USER_TOP - USER_VM_BASE + 1), None);
         assert_eq!(rebase_base(USER_VM_BASE, 0, u64::MAX), None);
-        assert_eq!(rebase_base(u64::MAX, 0, 1), None);
     }
 }

m1-entry-accepts-all.patch

--- a/toyos-userbound/src/span.rs
+++ b/toyos-userbound/src/span.rs
@@ -54,3 +54,3 @@
     pub fn new(addr: u64) -> Option<Self> {
-        is_user_addr(addr).then_some(Self(addr))
+        Some(Self(addr))
     }

m2-image-to-user-top.patch

--- a/toyos-userbound/src/span.rs
+++ b/toyos-userbound/src/span.rs
@@ -88,3 +88,3 @@
     let end = vm_base.checked_add(span)?;
-    (end <= IMAGE_TOP).then_some(base)
+    (end <= USER_TOP).then_some(base)
 }

The script's own output

head a45d98269a65b5ad6281307d7939760a89a40200
control-revert APPLY=0
guest with fix reverted EXIT=1
control-revert RESTORE=0
m1-entry-accepts-all APPLY=0
m1-entry-accepts-all host EXIT=101
m1-entry-accepts-all RESTORE=0
m2-image-to-user-top APPLY=0
m2-image-to-user-top host EXIT=101
m2-image-to-user-top RESTORE=0
m1 APPLY=0
guest with m1 EXIT=1
m1 RESTORE=0
status: []
userbound host EXIT=0
guest fixed EXIT=0
build-only EXIT=0
ci host EXIT=0
DONE

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 of a45d98269 against origin/main 017451624, by .claude/agents/reviewer.md. Read only: nothing was built or run here; the implementer's logs were read at their files.

Growth. git diff --shortstat origin/main...a45d98269: 10 files, +156 −32. Production +80 −29 (kernel +46 −24, toyos-abi +2 −1, toyos-userbound outside mod tests +32 −4). Tests +76 −3 (span.rs tests +18 −3, the new binary +37, tests/toyos.rs +21). The production growth is accepted: the type, Start and the image bound. The test growth is not, see the second BLOCKER.

BLOCKER

  • PR body, "Gates" — no guest test the change reaches was run but the branch's own; the body says so ("No other guest test was run") — rebase_base decides every process spawn and alloc_kernel_stack every thread, process and kernel thread, so the change reaches the whole suite, and by name abuse_elf_loader (the image bound's guest cases), abuse_tls_alloc, and every std and pthread thread. Evidence asks for each green at the head, with command, exit code and log. Closed by guest / suite green at the landing head, or the same from a local run.
  • tests/toyos.rs:251, tests/toyos.rs:2720-2734, tests/toyos.rs:71-73, tests/toyos-rust-tests/src/bin/spawn_noncanonical_entry.rs — a boot of its own, a MACHINE_TESTS entry whose machine shape is not the test, a 15-line arm and a new binary, for three refusals by name — the body's reason for the boot ("a kernel that regressed here takes its boot with it") is one the type answers: Start's userland arms take an Entry and Entry has one constructor, so no kernel that builds returns to an unchecked entry, and a shared row a regressed kernel took down would be red all the same. The body's reason against metal ("would need a boot of its own") is contradicted by the tree: tests/toyos.rs:915-921 already boots tests/testcases on the T14 for abuse_thread_table, a binary that spawns raw threads on mapped stacks and asserts a refusal by name. What is left for a test once the type holds is the word the syscall answers, and that is three assert_eq! lines in a binary that exists. Move the three entries into tests/toyos-rust-tests/src/bin/abuse_thread_table.rs (the process_bound_threads row, the cheaper tier, and a real Intel processor running the fixed kernel), or into a shared-boot binary that already spawns raw threads (abuse_tls_alloc.rs) if CI is to hold the word; delete spawn_noncanonical_entry.rs, its RUST_SKIP entry, the MACHINE_TESTS entry and the arm. The negative control already measured with the standalone binary stands as the record of the defect; the moved assertions need their own red arm only for m1.

NOTE

  • toyos-userbound/src/place.rs:55 — Window::new still admits a placement window whose ceiling is USER_TOP, while the body's claim that the image is the only executable mapping able to reach the last page rests on ALLOC_CEILING happening to be STACK_BASE — libraries are placed through that window (kernel/src/elf/mod.rs:153-155) and are executable. Bound the window by the same constant rebase_base uses (IMAGE_TOP, under a name that says what it is for both), so the invariant is one declaration a const assertion holds and not two values that agree today.
  • PR body, "The defect" — "a fault there halts every CPU: one process could take the machine" is stated as seen; it is read from Intel's SDM and fatal_exception, as the body's own oracle section says further down. Say so where it is first claimed.
  • PR body, "Independent oracle" — CI's guest / suite is called the instrument that could execute the dangerous half. It is not that instrument for the claim made: the runner's processor vendor is not chosen by the workflow, and where a return to a noncanonical address faults is vendor behaviour (documented so for SYSRET). The issue this branch closes named the T14.
  • PR body, last-page row — the worse half is unsaid: kernel/src/arch/x86_64/syscall.rs:87-88 runs sysretq after pop rsp, so on Intel the fault a noncanonical rcx raises is taken in Ring 0 on the caller's stack pointer, which is more than a halt. The refusal closes it; the record should say what it closed.

What was checked, at its lines

  • Entry is the only road, within loader. alloc_kernel_stack has three callers (kernel/src/loader/mod.rs:592, kernel/src/process.rs:970, kernel/src/sched/kthread.rs:77) and is the only caller of arch::entry::initial_frame (kernel/src/loader/start.rs:48); the trampolines are named nowhere else. They stay pub(crate) in arch::entry, as the body says.
  • No other road found. Every iretq, sysretq and eret in kernel/src returns to a frame the processor pushed or the kernel built; no site assigns a saved rip, rcx or elr (searched: none), kernel/src/syscall/debug.rs and the actuators set none, and no fault path advances or redirects one. The thread pointer context_switch writes to IA32_FS_BASE is the kernel's own (kernel/src/loader/tls.rs:57), no syscall sets it, and CR4.FSGSBASE is forbidden, so that neighbouring road is shut as well.
  • Process entry. toyos-elf/src/layout.rs:408-409 holds e_entry inside the extent; kernel/src/loader/mod.rs:571 now makes it an Entry.
  • Stack pointer. A thread can load any rsp and enter the kernel with it today; neither return checks it. True as claimed.
  • Last page. Prot::ReadExec is made at two sites only: kernel/src/loader/mod.rs:174 (image segments, placed from USER_VM_BASE inside the span rebase_base bounds, 4 KiB rounding and the 2 MiB window both staying under the 2 MiB-aligned IMAGE_TOP) and kernel/src/elf/mod.rs:127 (libraries, placed by alloc_region under ALLOC_CEILING). sys_mmap makes Read or ReadWrite and its FIXED arm refuses an end above ALLOC_CEILING (kernel/src/syscall/vm.rs:47-64); the stack is ReadWrite. Two mebibytes is the right grain because a window is mapped whole. A placement check costs nothing per syscall, where a canonical test before sysretq would cost every return, so the boundary is in the right place.
  • Nothing legal today is refused. Every image is placed at 0x100_0000_0000; one reaching the last page spans more than 127 TiB of address space. No test in the tree expects an image ending at USER_TOP to load: the one assertion that did is the host line this branch changes.
  • Controls. controls.out at the head: revert arm exit 1, m1 host 101 and guest 1, m2 host 101, restore clean, green arms 0; ci-host.log ends Host: 77 step(s), all green and its FAILED lines are its own controls. Neither host mutation is one a reader of the diff would miss the absence of, and both boundary values are pinned from each side.
  • The close. The issue's exit was a metal row deciding between a refusal and a doc line. The branch takes the refusal without the row, which meets the stricter arm; the durable line is at sys_thread_spawn; no citation of the slug remains at the head (git grep).

Ruling on the KVM reading of the unfixed kernel

Not required for landing, and not worth a CI run. The fixed kernel executes no return to an address Entry refuses, on any machine, so the reading would measure how bad a removed defect was and nothing about the change. The independent oracle the high-risk rule asks for exists and is cited (the SDM), and the negative control is measured. The run would also be the wrong instrument: it reads whatever processor the runner has, not the Intel machine the issue named. If the owner wants the severity on record, it is the T14's to give, with control-revert.patch applied, and it gates nothing here.

SEND BACK

…a window stops where an image does

The three entries a thread spawn refuses move into `abuse_thread_table`, which
already drives `SYS_THREAD_SPAWN` raw on stacks it mapped and asserts a refusal
by name, on the T14's `process_bound_threads` row. The type already keeps an
unchecked entry from a trampoline, so what the test holds is the word the
caller reads, and a metal row reaches that: the tier before a QEMU guest test.
The wait for an accepted thread goes with the standalone binary: a kernel that
accepted one is red on the assertion or on the fault, whichever comes first.
`spawn_noncanonical_entry`, its `RUST_SKIP` entry, the `MACHINE_TESTS` entry
and the arm are deleted.

Not `abuse_tls_alloc`: no QEMU boot runs a discovered Rust binary. The suite's
QEMU half is `MACHINE_TESTS` and `SCREEN_TESTS`, and `shared_metal` is the only
runner of the rest, so that binary would have held the word on the T14 as well,
under the shared boot's name and not a row's.

`IMAGE_TOP` becomes `PLACED_TOP` and `Window::new` asserts its ceiling against
it. The claim that nothing executable reaches the last page of the user half
rested on the kernel's window happening to end at `STACK_BASE`; libraries are
placed through that window and are executable. It is now one declaration, and
the kernel's window is a `const`, so one above it does not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu Japabu changed the title A return to userland takes an entry inside the user half, and an image stops a page short of its top A return to userland takes an entry inside the user half, and nothing is placed in its last page Oct 8, 2026
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1 mutation patches, as run against b00560c4b

Each was checked (git apply --check), applied, run with cargo test -p toyos-userbound and reversed by one script; all three exit 101, and the body names the test each reds. m1 is unchanged from the comment above; m2 is that comment's after the rename to PLACED_TOP; m3 is new, for the window's bound.

m1.patch

--- a/toyos-userbound/src/span.rs
+++ b/toyos-userbound/src/span.rs
@@ -54,3 +54,3 @@
     pub fn new(addr: u64) -> Option<Self> {
-        is_user_addr(addr).then_some(Self(addr))
+        Some(Self(addr))
     }

m2.patch

--- a/toyos-userbound/src/span.rs
+++ b/toyos-userbound/src/span.rs
@@ -90,3 +90,3 @@
     let end = vm_base.checked_add(span)?;
-    (end <= PLACED_TOP).then_some(base)
+    (end <= USER_TOP).then_some(base)
 }

m3.patch

--- a/toyos-userbound/src/place.rs
+++ b/toyos-userbound/src/place.rs
@@ -55,1 +55,1 @@
-        assert!(ceiling <= PLACED_TOP, "a placement window stops below the last page of the user half");
+        assert!(ceiling <= PLACED_TOP + PAGE_2M, "a placement window stops below the last page of the user half");
head b00560c4bc1d0dc6a46af0286593411630be0c88
m1 CHECK=0
m1 APPLY=0
m1 host EXIT=101
m1 RESTORE=0
m2 CHECK=0
m2 APPLY=0
m2 host EXIT=101
m2 RESTORE=0
m3 CHECK=0
m3 APPLY=0
m3 host EXIT=101
m3 RESTORE=0
status: []

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

T14 at head b00560c4b (orchestrator's run; the image staged by the implementer from the clean tree, sha256 db1f156d…47e10b07 printed by the command that flashed it):

row boot judge (--metal --metal-readback … process_bound_threads, clean tree at the head)
process_bound_threads exit 0 exit 0, [metal] 1 passed, 0 failed, 1 boot(s), PASS process_bound_threads

test_rs_abuse_thread_table, which now carries the three entry refusals, ended exit=0 in that boot (===TEST_END test_rs_abuse_thread_table exit=0===). This is the fixed kernel on an Intel processor refusing the entries; the unfixed kernel was not booted there, and mutation m1 was not run on the machine.

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2 of b00560c4b against origin/main 017451624, by .claude/agents/reviewer.md. Read only: nothing was built or run here; the round's logs were read at their files.

Growth. git diff --shortstat origin/main...b00560c4b: 10 files, +123 −40. Production +85 −33 (kernel +46 −24, toyos-abi +2 −1, toyos-userbound outside mod tests +37 −8). Tests +38 −7 (span.rs +18 −3, place.rs +7 −3, abuse_thread_table.rs +13 −1). Against round 1 the branch is 33 lines smaller in tests and tests/toyos.rs is back to main's bytes. Accepted.

Round 1's BLOCKERs

  1. Guest evidence — OPEN, restated; round 1's wording of it was wrong and is withdrawn where it was wrong.
    • Withdrawn: that guest / suite or a local filter could show abuse_elf_loader, abuse_tls_alloc and the std and pthread thread binaries. The implementer's refutation holds on the tree: select (tests/toyos.rs:4563-4568) takes MACHINE_TESTS and SCREEN_TESTS and nothing else, shared_metal (:1022) is the only runner of a discovered binary and is reached only under --metal (:4722-4739), and norun/ has the six filters exiting 1 with No test matches filter. Those binaries are the T14's.
    • Open: the QEMU suite is the guest tests there are, the change reaches every one of them (each boot spawns its servers through rebase_base and alloc_kernel_stack), and at this head two of its thirty boots have run: machine_shutdown and machine_shutdown_short_stop, exit 0, both x86-64. No AArch64 boot of this kernel exists anywhere in the record: the twenty-one virt_* rows are the only ones that architecture has, Start feeds arch::entry::initial_frame on both, and userland/kernelprobe/src/arch/aarch64.rs:201 spawns a raw thread through the new refusal. Thirty boots, twelve wide, is a cheap measurement the implementer may take himself. Closed by cargo test --test toyos-build exit 0 with its log at the landing head, or guest / suite green there, recorded in the body. No change to the code is asked.
  2. The standalone guest test — CLOSED. spawn_noncanonical_entry.rs, its RUST_SKIP entry, the MACHINE_TESTS entry and the arm are gone (git diff a45d98269 b00560c4b); git grep at the head finds none of spawn_noncanonical_entry, thread_entry_noncanonical, IMAGE_TOP or the issue's slug; tests/toyos.rs is absent from the diff against main. The three refusals are at tests/toyos-rust-tests/src/bin/abuse_thread_table.rs:87-94. Measured: the orchestrator's T14 run at this head, boot exit 0, judge exit 0, [metal] 1 passed, 0 failed, 1 boot(s), ===TEST_END test_rs_abuse_thread_table exit=0===, the first thread the kernel recorded being tid=1, so the three refused calls made none.

Round 1's NOTEs

  • Window::new's ceiling — closed. PLACED_TOP is one pub(crate) declaration with two readers (toyos-userbound/src/span.rs:91, toyos-userbound/src/place.rs:55); the kernel's only window is the const at kernel/src/vma.rs:20, so a ceiling above it does not compile; m3 exits 101 on a_window_reaching_the_last_page_of_the_user_half_is_a_kernel_bug, and m2 after the rename still exits 101 on its test.
  • The three body corrections — made: "read, not seen" where the halt is first claimed; CI's runner named as no instrument for a vendor's fault site; the sysretq after pop rsp in the last-page row.

Ruling 1: which T14 boots the landing is owed

None beyond the one that ran. The QEMU suite above plus process_bound_threads is enough, for these reasons, each read off the tree:

  • The changed paths have already run on that machine. testcases-bounds and shared are the same config (tests/testcases) and the same shipping kernel (features: &[], tests/toyos.rs:910-915 and :1027-1031). That boot took every server and three test processes through rebase_base and Start::Process, and 4095 threads through Entry::new and Start::Thread with a live argument each thread dereferences, so a slot swapped in the Start match would have faulted there.
  • abuse_elf_loader (shared) cannot tell the old bound from the new. Its cases that reach rebase_base are 3, 4 and 4b (abuse_elf_loader.rs:383-418): two spans that end in or beyond the kernel half and one vaddr_min above the base. Each is refused by either bound, and each has its class in the host table at this head (USER_TOP - USER_VM_BASE, u64::MAX, a_vaddr_min_above_the_base_cannot_rebase). Nothing in it reaches the 2 MiB the change took. The host tier holds the function; a boot would re-read it.
  • abuse_tls_alloc (shared), std_sync, std_threading, std_tls* (shared-2) and the pthread cases (ccorpus) spawn at entries inside their own image or a library. Entry::new admits those by the predicate the 4095 raw spawns went through; std and userland/libc/src/pthread.rs:213 call the same syscall with the same argument order.
  • Libraries placed through the window are bounded at compile time and need no boot.

For the record, should the orchestrator want the named binaries read anyway: unfiltered, the shipping set is two flashes, shared (the first 62 members in name order, abuse_connect_flood to query_modules_size, holding abuse_elf_loader and abuse_tls_alloc) and shared-2 (the other 22, sched_stress to window_refusal, holding every std_*); the pthread cases ride ccorpus, and spawn_lands_claimed and spawn_child_ends_first ride shared-debug. That run gates nothing here.

Ruling 2: the m1 boot on the T14

Not owed, and round 1's sentence asking for it ("the moved assertions need their own red arm only for m1") is withdrawn. It named a mutation this prompt says not to name:

  • m1 does not land unseen. It exits 101 on the host at this head (m1-host.log, an_entry_is_an_address_of_the_user_half_and_nothing_else), and removing the check from sys_thread_spawn instead does not compile, since spawn_thread takes an Entry.
  • The moved assertions can fail, by reading and by the row's own log. The refused calls use the stack shape and argument order of spawn() at line 67, whose 4095 calls the kernel accepted in the same run, so the other InvalidArgument in that syscall (stack_base > stack_ptr) is not what answered. The same expression went red under TCG at a45d98269 with m1 (exit 1, in the comment of 08:23).
  • The boot would not read what it is for. An m1 kernel there is the removed defect on an Intel processor: the accepted thread is runnable on another CPU before the caller's assert_eq! unwinds, the body's own fourth uncertainty, so the likely reading is a halted machine and the owner's hand on its power button. Round 1 already ruled that the severity of the removed defect gates nothing.

What the row holds beyond the type and the host table is the word the syscall answers, and a wrong word there is one a reader of kernel/src/syscall/proc.rs:138-140 catches.

BLOCKER

  • PR body, "Gates" — the QEMU suite has not run at b00560c4b: 2 of 30 boots, none AArch64 — Evidence asks for every guest test the change reaches, green, with command, exit code and log; the change reaches all thirty. This is round 1's first BLOCKER as restated above, and the only thing open.

NOTE

  • PR body, "Not run, and not claimed" and the --metal row of "Gates" — "the moved assertions have executed nowhere" and "staged, not run" are false of the record since the T14 run; the reading (boot exit 0, judge exit 0, the TEST_END line, the image's hash) belongs in the body, where Evidence looks for it.
  • PR body, "m1 against the moved assertions is owed and is the T14's" — false of the record after ruling 2.
  • PR body, "The first BLOCKER's close is therefore not guest / suite" — true of the named binaries only; the QEMU suite is still owed.
  • PR body, "The issue" — draft Three audited kernel races recorded, traced and unexecuted: an IPC rollback's expect, chdir against open, an shm map against the last close #754 still carries the issue file this branch deletes; if it lands after this one it puts a closed defect back in issues/. The orchestrator's to hold at that merge; nothing in this diff.

SEND BACK

@Japabu
Japabu marked this pull request as ready for review October 8, 2026 08:52
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3 of b00560c4b against origin/main 2e781a49d, by .claude/agents/reviewer.md. Read only: nothing was built or run here. The head is unchanged since round 2 (refs/pull/757/head and refs/heads/wt/toyos-threadentry both b00560c4bc1d0dc6a46af0286593411630be0c88), so the change was not read again; this round judges round 2's one BLOCKER and the edited body.

Growth. git diff --shortstat origin/main...b00560c4b: 10 files, +123 −40, as round 2 stated and accepted. main has moved from 017451624 to 2e781a49d since the CI run; what it gained is five files under issues/ and no source, test or manifest, and none of the ten files this branch changes.

Round 2's BLOCKER

  1. The QEMU suite had not run at b00560c4b — CLOSED. Measured by CI run 37752723738, job 113230606172 (guest / suite, conclusion success), its log read whole at its saved file:
    • The head. The job checked out 3aa67c31f, Merge b00560c4bc1d0dc6a46af0286593411630be0c88 into 0174516240c1e7e7b8b692d30b4093a485a36757, and compiled kernel and toyos-userbound from it.
    • The command and its result. cargo run -- --ci guest; running 30 tests, 1 wide; test result: ok. 30 passed, 30 total (705.6s; workers: 572s building, 134s testing); [ci] Guest: 5 step(s), all green. Thirty PASS lines counted by name, not taken from the result line.
    • The instruments. the x86_64 instrument: QEMU 11.1.1, firmware /usr/share/OVMF/OVMF_CODE_4M.fd, /dev/kvm opens; the aarch64 instrument: QEMU 11.1.1, firmware /usr/share/AAVMF/AAVMF_CODE.no-secboot.fd, another architecture's machine: emulated.
    • x86-64, nine rows: iommu_virtio_platform, nested_nmi_is_loud, machine_shutdown, acpi_power_button, machine_shutdown_short_stop, screen_panic_muted, screen_fatal_behind_a_painter, screen_fatal_halt_composited, screen_loader_clears.
    • AArch64, twenty-one rows, the gap round 2 named: virt_early_panic, virt_early_fault, virt_el2_drop, virt_user_mode, virt_timer_preempts, virt_irq_storm, virt_timer_floor, virt_fp_isolation, virt_first_entry, virt_unmap_touch, virt_debug_refused, virt_readonly_copyout, virt_ring0_timer_in_syscall, virt_mask_windows, virt_smp, virt_el1_smp, virt_failed_ap_leaves_no_hole, virt_fatal_halts_the_others_first, virt_reboot, virt_off_names_the_cpus_left_on, virt_reboot_refused_without_psci. virt_first_entry is the row that reads a new thread's first instruction through Start::Thread on that architecture: x1-x30 were zero at a new thread's first instruction.
    • The other two checks of the same run: host success (job 113229625347), toolchain / build success (113229626035), read off the pull request's check rollup.

Round 2's NOTEs

  • The T14 reading — in the body's --metal row and "Where the moved assertions have run": boot exit 0, judge exit 0, [metal] 1 passed, 0 failed, 1 boot(s), the TEST_END line, the image's hash. Closed.
  • m1 on the T14 — the body now says it was not booted and is not owed, with ruling 2's reasons. Closed.
  • The guest suite — the body's "Round 1" and "Gates" name run 37752723738 and its three jobs, and the quoted lines are the log's. Closed.
  • PR Three audited kernel races recorded, traced and unexecuted: an IPC rollback's expect, chdir against open, an shm map against the last close #754 — closed without landing (state: CLOSED, mergedAt: null), and main at 2e781a49d has no issues/a-thread-entry-is-unchecked-and-a-noncanonical-one-is-measured-only-under-tcg.md; the slug is cited nowhere at the head. Nothing is left to hold at a later merge. Closed.

BLOCKER

None.

NOTE

LAND

@Japabu
Japabu added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit b6bcb96 Oct 8, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-threadentry branch October 8, 2026 09:38
Japabu added a commit that referenced this pull request Oct 8, 2026
Brings in #749, #757, #759 and #762. #762 changes kernel/src/object/mod.rs
and toyos-abi/src/syscall.rs in hunks disjoint from this branch's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
github-merge-queue Bot pushed a commit that referenced this pull request Oct 8, 2026
…nd the SDK resolve together (#746)

Stage 3 of `issues/the-tree-says-who-uses-each-thing.md`. The root,
`kernel/`, `bootloader/`, `userland/` and `toyos/` were five Cargo
resolutions; they are one workspace with one `Cargo.lock`, one
`[profile.toyos]`, one `[patch]` table and one tracked
`.cargo/config.toml`. No directory moves.

Head `dd12c0b32`, on `origin/main` `6f87cdb9c` (#749; none of #757, #759
or #762 had landed when it was merged and measured). It is `9ef866436`,
where the CI readings of the fold itself were taken, plus two merges of
`main` and the close of the stage's issue. Everything owed at the merged
head is in the next section, measured at `dd12c0b32`.

## The merge of #749, measured at `dd12c0b32`

#749 wrote its new dependency edges into `kernel/Cargo.lock` and
`userland/Cargo.lock`, which this branch deletes. Both modify/delete
conflicts are resolved by deleting the file and re-resolving the root
lock. Git merged the root `Cargo.lock` without a conflict into a lock
that is wrong, as the review found: `cargo metadata --locked` on it
exits 101 (`cannot update the lock file … because --locked was passed`).
It had `toyos-userbound`'s edges to `toyos-abi` and `toyos-bootmap`,
which #749 also wrote into the root lock, and not `acpiserver`'s to
`toyos-acpi` and `toyos-aml`, which #749 wrote into userland's alone.
`cargo metadata --offline` re-resolved it. `diff` of git's merged lock
against the re-resolved one is those two lines under `acpiserver` and
nothing else: no package added, no version moved.

| Owed | Command | Result |
|---|---|---|
| The lock resolves as committed | `cargo metadata --locked
--format-version 1` | exit 0 at this head by the host suite's step "the
licences of what ships", which runs `cargo metadata --locked` for every
shipped crate's manifest and is green in `host.log` and in run
37757675374; the hand run's empty stderr (`metadata-locked.err`)
predates the merge commit and recorded no exit |
| The lock's (name, version) pairs are the union of `main`'s five |
`pairs.sh <worktree> 6f87cdb`: the pairs of `Cargo.lock`, `kernel/`,
`bootloader/`, `userland/` and `toyos/Cargo.lock` at `6f87cdb9c`, `sort
-u`, against the root lock's | 692 against 692, `diff` exit 0
(`pairs.out`) |
| The folded kernel and loader are the control's bytes | `prove.sh
6f87cdb dd12c0b …`, the round 3 script unchanged | exit 0; all four
`control vs fold` byte rows `cmp` exit 0; every row in "The checks"
below (`prove.out`) |
| No new reader of a compiled-in path | `git diff -U0 e3bdff8
dd12c0b -- tests src toyos-blackbox toyos-symbols userland/symbolize`,
its added lines searched for `\.rs`, `taken at`, `panicked at`,
`Location`, `file()`, `PREVIOUS_PANIC`, `strip_prefix`, `src/`, `pure/`
| the merge touches five files there, all under `tests/`; 13 hits, of
which 4 are diff headers and 9 the field `info.rsdp`; none reads a path.
`tests/common/power.rs:429` is still the one reader outside fixtures,
and reads `taken at kernel/src/hardlockup/probe.rs`
(`readers-merge.diff`, `readers-hits.txt`) |
| `cargo run -- --ci host`, once, on the development machine | at
`dd12c0b32`, `cargo run -- --ci host > host.log 2>&1; echo EXIT=$?` |
exit 0; the log ends `[ci] Host: 77 step(s), all green`; 1-minute load
26.60 when it started (`host.log`) |

The logs are in the round's scratch directory (`orch/oneworkspace-r4/`),
which a reader of this pull request cannot reach; `prove.out` and
`pairs.sh` are in the round 4 comment.

## What changed, per decision

- **Members.** `kernel`, `bootloader`, `toyos` and userland's 40
packages join the root `[workspace]`. `userland/Cargo.toml`, four locks,
three `rust-toolchain.toml` and three per-directory `.cargo/config.toml`
are deleted. The toolchain files chose nothing the build read: every
guest `cargo` already runs under `RUSTUP_TOOLCHAIN` naming its sysroot.
- **Flags.** `build.target` is gone, since every guest build already
passes `--target`. The root config holds one `[target.<triple>]` table
per guest triple, six, each with the flags its directory's config gave
it. A host build takes none, as before. Two things do change:
- The guest crates outside the workspace that are built from their own
directory for a ToyOS triple (`tests/toyos-rust-tests` and its `tls-*`
crates) now take `-Dwarnings`, because cargo reads the tracked root
config from above them. On a checkout without a local config they took
no flags.
- The one build that sets `RUSTFLAGS` itself (the test binaries linked
against a `cdylib`, `src/build.rs`) takes none of the table: the
variable replaces it.
- **Profiles.** The root's `[profile.dev]` is `opt-level = 2`. The
kernel library's host tests, its model controls, the SDK's tests and
every surveyed userland crate's host tests used to resolve in their own
workspaces and ran at `opt-level = 0`; they now run at 2.
- **What stays apart** (the root manifest's `exclude` says why at each
entry): `rust/`; `tests/toyos-rust-tests` and its `tls-*` crates and
`tests/ssh-client-host`, because `[patch]` is workspace-wide and they
patch or refuse what the root patches; and `userland/libc`. libc keeps
its own lock because that lock is an input of the sysroot key: as a
member it would be resolved by the root lock, and every dependency
change of any member would move the key and rebuild every sysroot. The
price is a sixth resolution of `toyos`, `toyos-abi`, `toyos-elf`,
`toyos-osrelease` and `dlmalloc` that nothing holds to the root's (both
carry `dlmalloc` 0.2.13 today).
- **The lock** is every `[[package]]` of the five locks, deduplicated
and resolved by `cargo metadata`. Nothing was `cargo update`d. Since the
lock reviewed at `88bcbf4d3` it has changed by #749's four edges alone
(see the section above); `cargo metadata --locked` exits 0 at this head.
- **One target directory.** Every guest is built at the root with `-p`
into `target/`. A stale sysroot used to `cargo clean` a crate's own
target; that would now empty the build system's own, so `Stale::All`
removes `target/toyos` and the guest triples' directories instead, and
the `cargo clean` path, its member assertion and its test are deleted.
- **Kernel and loader build one after the other.** They were built on
two threads. In one target directory cargo serialises them anyway
(measured in round 1: the second prints `Blocking waiting for file lock
on artifact directory`), so the thread scope is deleted.
- **The host suite** can no longer be `--workspace`: the kernel binary,
the loader and most of userland do not build for a host. `src/hostws.rs`
says which members a host tests, and the workspace test and clippy runs
`--exclude` the rest by package name.
- **Fork clones.** The tracked config includes the gitignored
`.cargo/local.toml` when it exists, and `implementer.md` names it.
- **The merge of #745.** `src/sysroot.rs` keeps both sides:
`SYSROOT_SOURCES` carries `"sdk/std"` and `SYSROOT_MANIFESTS` ends in
`".cargo/config.toml"`; its test keeps both loops;
`issues/toyos-has-its-own-allocator.md` keeps `sdk/std/sys/alloc.rs` and
"from the kernel's graph in `Cargo.lock`". Git merged all three without
a conflict.
- **The host's own apps build where the userland tests build.**
`src/ci.rs`'s apps step passes `--target` only where it checks another
host's triple. On `main` the `userland/*` test steps and the host's apps
step both named the host triple and shared `userland/target/<host
triple>`. The fold took `--target` off the test steps, which no longer
need it to keep a guest triple out, and left it on the apps step: the
tests filled `target/debug`, the apps `target/<host triple>`, and every
dependency was compiled twice. That is what made the sealed tree larger
than `main`'s (see CI).
- **The merges of `main`.** #747, #748, #750, #751, #753 and #755 merged
without a conflict. #749 did not: see the section above.
- **The merge of #752.** Git merged it without a conflict: both
workflows' `host` jobs keep `CARGO_PROFILE_DEV_DEBUG: line-tables-only`
in `env:`, and `carry()` no longer sets it. No manifest and no lock
moved in the merge.
- **Issues.**
`issues/the-tree-resolves-in-five-cargo-locks-not-one.md` is deleted:
the one thing it named as left, the T14's run of the metal profile on
the folded build, ran green at `db55db96a`, and review round 2 ruled no
boot owed for what followed on two conditions, both in the section
above. Stages 2 and 3 of `issues/the-tree-says-who-uses-each-thing.md`
now read "Landed in #724, #732 and #738" and "Landed in #746". What the
file carried that stays true is the root manifest's `exclude`, which
says why each excluded directory keeps its own resolution; the deleting
commit's message carries the rest (libc's second resolution of five
crates, and `miniz_oxide` 0.8.9 beside 0.9.1 until `png` takes 0.9).
`issues/cargo-run-inside-kernel-loom-or-kernel-sim-builds-for-a-bare-target.md`
is closed on the two in-directory runs at `9ef866436`.
`issues/the-sdk-is-linted-by-no-clippy-run.md` is filed and names its
owner, the build system.

## The fold changed the kernel's source paths, and the proof did not see
it

The T14's run of the whole metal profile at `88bcbf4d3` exited 1: 295
passed, 1 failed, 30 boots. The red row was
`hard_lockup_ends_a_deaf_cpu`. Its judge looked for `taken at
src/hardlockup/probe.rs` in the previous boot's panic record, and the
readback's loader log says `taken at
kernel/src/hardlockup/probe.rs:145:29`.

**What changed in the kernel's strings.** Cargo hands rustc a workspace
member's source by its path from the workspace root, and rustc writes
that path into every panic and `Location`. The kernel's root was
`kernel/`; it is now the repository. So `src/...` became
`kernel/src/...`, and a path dependency outside the old root, which the
base named by the checkout's absolute path, is now named from the
repository root (`toyos-abi/src/...`). Read from the actuator kernel
staged at this head: 254 distinct `.rs` paths, 158 under `kernel/`, 38
under a `toyos-*` crate or `bcachefs`, none bare `src/` or `pure/`, none
naming the worktree.

**Why the proof did not see it.** Both of its oracles were blind to it
by construction:
- The byte row compared the fold against a control that is the base with
its workspace root moved up. The control moved the root too, so it
carries the same new paths and the bytes agree.
- The `rustc`-lines row compared base against fold after a `sed` that
rewrites `(kernel/|bootloader/)?(src|pure)/x.rs` and
`ROOT/<crate>/src/lib.rs` to one form. That rewrite is needed, or every
path crate's line differs and the row can show nothing else; but it
absorbed the change without reporting it.

`prove.sh` now reports what that rewrite absorbs: per artifact, how many
crates' source arguments were renamed, and a diff of the `.rs` paths the
artifact carries, base against fold and control against fold. The script
is in the round 3 comment and has run twice since, at `9ef866436` and at
this head.

**Every reader of a compiled-in path.** I searched the harness, the
guest tests, the build system, `toyos-blackbox`, `toyos-symbols`,
`userland/symbolize`, the loader and the kernel's panic path for path
literals, prefix strips and `Location` readers. One reader matches a
compiled-in path by its prefix: `tests/common/power.rs`, the red row's
judge, now fixed to the path the kernel records. Everything else is
prefix-blind (`panicked at`, a file name with its line) or a synthetic
fixture. The kernel's panic slot keeps the last 96 bytes of a path; the
longest kernel path is 46, so nothing is cut. Userland's panic sites
gain a `userland/` prefix the same way; no test reads one.

**The record rows.** The judging asked to record three
`boot.testcases-bounds.*` rows. They are not this change's: that boot
was already staged on the base and unrecorded, and `main` recorded it in
#745. They arrive with the merge and nothing is committed here.

## What the fold changes in what is built

The lock row was measured at `dd12c0b32` against `6f87cdb9c`, the
`rustc` row by `prove.sh` at the same pair; the two `cargo tree` rows at
`88bcbf4d3`, and were not taken again.

| Measured | Result |
|---|---|
| Lock: (name, version) pairs, the fold's against the union of
`origin/main`'s five | identical, 692 pairs, `diff` exit 0 |
| Lock: sources | registry `getrandom` 0.2.17, 0.3.4, 0.4.2 are gone;
the forks at the same versions remain |
| Kernel and loader, both arches: every `rustc` command line of `cargo
build -v`, base against fold, path and cargo's path-derived hashes taken
out | identical, `diff` exit 0: 31 units per kernel, 55 and 37 per
loader |
| Userland, both triples: `cargo tree -e features` over every program,
base against fold | identical, `cmp` exit 0 |
| Host members: the same | `diff` exit 1, on `getrandom`'s source alone
|

So one resolved crate changes: the build system and the other host
members compile the ToyOS forks of `getrandom` 0.2.17, 0.3.4 and 0.4.2
instead of the registry's, same versions, same features. And every
source path compiled into the kernel and the loader changes, as above.

## The checks (high-risk: build system)

Measured at `dd12c0b32` against `origin/main` `6f87cdb9c` by `prove.sh`
(the script of the round 3 comment, unchanged), exit 0; its output is in
the round 4 comment. Round 3 measured the same rows at `9ef866436`
against `b432ed21c`, round 1 at `88bcbf4d3` against `e7010129f`.

**Negative control.** The whole change reverted is the base. A second
control is the base with only its workspace root moved up, keeping the
crate's own base lock and its profile. It is given the fold's root
`.cargo/config.toml`, so the control does not hold the flags: the one
row that does is base against fold on normalised `rustc` lines.

**Oracle.** Bytes and cargo's own command lines. Each cell is its own
`cmp` or `diff` exit:

| | kernel x86_64 | kernel AArch64 | loader x86_64 | loader AArch64 |
|---|---|---|---|---|
| control vs fold, bytes | 0 | 0 | 0 | 0 |
| fold vs fold rebuilt, bytes | 0 | 0 | 0 | 0 |
| base vs fold, bytes | 1 | 1 | 1 | 1 |
| base vs fold, `rustc` lines normalised | 0 | 0 | 0 | 0 |
| control vs fold, `rustc` lines verbatim | 0 | 0 | 0 | 0 |

What the normalisation absorbs, reported by the three `paths` rows: base
against fold, cargo hands rustc another source path for 29 of 31 crates
of each kernel and for 35 of 50 and 13 of 35 crates of the loaders
(`diff` exit 1 each, as expected); the `.rs` paths the x86-64 kernel
carries are 250 on both sides, of which the base has 39 under the tree's
absolute path and 150 from the crate's own root and the fold none of
either (`diff` exit 1); control against fold the artifacts' paths are
identical (`diff` exit 0, all four).

**Mutations**, each
on a fresh copy of the fold: M1 (drop the `[target.x86_64-unknown-uefi]`
table) loader build exit 101; M2 (lock `dlmalloc` at 0.2.12) `cmp` exit
1 and lines `diff` exit 1; M3 (select `bcachefs` beside the kernel in
one `cargo`) kernel build exit 101.

## Gates

The rows of the section "The merge of #749" were read at `dd12c0b32`.
Every row below was read at `9ef866436` unless it says otherwise, each
once, the narrowest that judges it. `ci.yml` runs on the push of
`dd12c0b32`; its result is not in this body.

| Gate | Result |
|---|---|
| `cargo run -- --ci host` | at `dd12c0b32`, development machine: exit
0, `[ci] Host: 77 step(s), all green`. Linux runner at `9ef866436`:
`ci.yml` run 37740454881 `host` success; cold inside `--ci seal`,
nightly run 37740449787: `[ci] Seal: 80 step(s), all green`; on macOS,
the same nightly's `portability-macos`: success |
| `cargo test --lib ci::tests` (the changed step's own test) | exit 0,
13 passed |
| The images and the guest suite | at `9ef866436`, run 37740454881:
`toolchain / build` and `guest / suite` success (KVM); run 37740449787:
`toolchain / build` and `tcg / suite` success. At `e3bdff8af`, run
37755369755: `host` and `toolchain / build` success, `guest / suite`
still running when read. Not run locally, and not read at `dd12c0b32` |
| `prove.sh 6f87cdb dd12c0b …` | exit 0; every row as in the table
above |
| `cargo test` inside `kernel/loom` | exit 0 |
| `cargo test` inside `kernel/sim` | exit 0 |
| Cold wall clock, x86-64 kernel and loader (`wall.sh`, one run) | base,
two cargos side by side: 24 s, 1-minute load 34.92 before it. Fold, one
after the other: 20 s, load 42.23. Other agents' builds were running, so
the two are not a controlled pair; the fold was not slower |
| Metal profile | every row green at `db55db96a` (comment 6048782042).
Since then the branch changed `src/ci.rs` and the root lock's two
`acpiserver` edges; the kernel sources that moved are `main`'s own
landings (#747, #748, #749), merged in. Review round 2, ruling (3), owes
no boot for the merge of #749 on two conditions, both met above |
| `cargo test --manifest-path userland/acpiserver/aml/Cargo.toml` at
`e3bdff8af` | exit 0 |
| `git status --porcelain --ignore-submodules=none` at `dd12c0b32` |
empty |

`issues/cargo-run-inside-kernel-loom-or-kernel-sim-builds-for-a-bare-target.md`'s
close now stands on the two in-directory runs at `9ef866436`.

The logs of these rows are files in the scratch directory of the round
that took them (`orch/oneworkspace-r3/`), which a reader of this pull
request cannot reach; the proof's and the measurements' outputs are in
the round 3 comment.

## CI

**Why the sealed tree was larger than `main`'s, measured.** A
`workflow_dispatch` of `nightly.yml` at `88bcbf4d3` (run 37685714260)
sealed `9348536345 B in 20064 files`, red. Units compiled per step,
counted from that log and from `main`'s nightly at `b432ed21c` (run
37717000719, sealed `18708 files, 7664839895 B`):

| step | `88bcbf4d3` | `main` |
|---|---|---|
| the driver's own build | 203 | 203 |
| the build system | 207 | 207 |
| the workspace's host members | 99 | 99 |
| clippy, warnings denied | 412 | 417 |
| the controls | 56 | 56 |
| `userland/*` | 225 | 289 |
| the apps for linux | 282 | 47 |
| the apps for macos | 248 | 248 |
| the apps for windows | 239 | 239 |

Of the 256 distinct crates the apps-for-linux step compiled at
`88bcbf4d3`, 226 had been compiled by an earlier step of the same run;
30 by none. The cause is the target directory: the test steps built
without `--target` into `target/debug`, the apps step with `--target
x86_64-unknown-linux-gnu` into `target/x86_64-unknown-linux-gnu`.
Profile, features and `RUSTFLAGS` are the same in both.

**The fix, measured once on the development machine** (`share.sh` in the
round 3 comment; cold, a target of its own, `aarch64-apple-darwin`):
after the fourteen test steps' builds (271 units), the ten apps with
`--target <host>` compile 270 units and add 616,616 KiB under
`target/<host>` and 135,064 KiB under `target/debug`; the same ten
without `--target` then compile 47 units and add 107,856 KiB. The 47 are
the same crates `main`'s step compiles on the runner.

**The seal at `9ef866436`, nightly run 37740449787** (conclusion
success: `host`, `portability-linux`, `portability-macos`, `toolchain /
build`, `tcg / suite`):

```
the cache entry, read by content: none restored: the run is cold
the apps for linux: 10 app(s) pass `cargo build`; …
the tree, sealed as the host cache's entry: 17010 files, 7493968284 B of the 8000000000 B an entry may hold; sealed: 2496 sources, built on Linux X64 ubuntu24 20261004.327.1, every target dated as built
[ci] Seal: 80 step(s), all green
```

Units per step in its log, against the two columns above:

| step | `9ef866436` | `88bcbf4d3` | `main` |
|---|---|---|---|
| `userland/*` | 225 | 225 | 289 |
| the apps for linux | 42 | 282 | 47 |
| the apps for macos | 264 | 248 | 248 |
| the apps for windows | 240 | 239 | 239 |

Every other step compiles what it did at `88bcbf4d3`. I expected 47 for
the Linux apps: it is `main`'s 47 less `crc32fast`, `log`, `memchr`,
`smallvec` and `toyos-keymap`, which an earlier step had compiled. I did
not expect the macOS step's 16 more: all are host-side units (`syn`,
`thiserror-impl`, `tokio-macros`, `futures-macro`, `autocfg` and the
like), which the Linux step's `--target` build used to compile for the
host and which the first `--target` step now compiles instead. The four
steps together compile 771 units against 994 at `88bcbf4d3` and 823 on
`main`.

- Margin: 506,031,716 B under the limit, 6.3 %, on image 20261004.327.1.
`main`'s 7,664,839,895 B was sealed on 20260927.320.1. The one pair of
figures there is for the two images is `f260e0b98`, built with full
debuginfo before #752 cut it to line tables: 8,540,783,725 B on
20260927.320.1 (run 37292450697) against 8,182,940,473 B on
20261004.327.1 (run 37601225884), 357,843,252 B or 4.2 % less on the
newer. So this head's figure and `main`'s are not a pair, and this head
is unmeasured on the older image.
- Against the same branch before the fix and before #752: 9,348,536,345
B at `88bcbf4d3` on 20260927.320.1.

**`ci.yml` run 37740454881 at `9ef866436`:** `host`, `toolchain / build`
and `guest / suite` success.

After this lands every `host` check runs cold until the first nightly on
`main` seals and saves: the path list is the cache's version.

**Toolchain keys.** The fold moves the sysroot key once:
`userland/.cargo/config.toml` was one of its inputs and
`.cargo/config.toml` replaces it. From now on a change to any guest
triple's flags moves that key. The merges of #747 and #749 moved
`toyos-abi` and `toyos`, so the sysroot key moved with `main`; the key
at this head was not read here.

## No new gate, test or dependency

No guest test is added or changed. No dependency is added. The proof is
a one-off script because its subject is this one change against its
base.

## Size

`git diff --shortstat origin/main...HEAD` at `dd12c0b32`: 53 files,
+5454 −7765. Without the locks: 48 files, +446 −704. `src/`,
`tests/toyos.rs` and `tests/common/`: 12 files, +237 −360, of which
tests are roughly +65 −115 by my reading of the hunks (an estimate, not
a count). `issues/`: 16 files, +49 −115.

## What I am unsure of

- **The seal on the older runner image.** GitHub serves two; this branch
was sealed on the newer one, at `9ef866436`. The only pair of figures
for the two is `f260e0b98` with full debuginfo (above): the older image
sealed it 357,843,252 B larger. Carried unscaled onto this tree that
leaves 148,188,464 B under the limit on the older image; scaled by the
pair's ratio, about 178 MB. Both are arithmetic, not a run.
- **`dd12c0b32` itself:** `ci.yml` run 37757675374 has `host` and
`toolchain / build` success at it; its `guest / suite` is what the
landing waits on. #757 (`b6bcb9691`) landed on `main` after this head
was merged and measured: ten source files under `kernel/src`,
`toyos-abi/src`, `toyos-userbound/src` and `tests/toyos-rust-tests`, no
manifest and no lock; `git merge-tree --write-tree dd12c0b b6bcb96`
exits 0. Nothing here was measured with it in. It differs from the
sealed head by `main`'s #749, #750, #753 and #755 and the root lock's
two edges; the seal's byte count at this head is unmeasured.
- **Build wall clock.** One run under load; the fold was not slower.
- **Clippy reaches further.** The bare-target shapes now also lint the
kernel's and the loader's path dependencies for those targets.
- **The licence gate reads a superset.** `--all-features` for the kernel
now turns on every member's features. It judges more than ships.
- **The runner's cargo.** The nightly's `host` at `88bcbf4d3` parsed the
optional `include`, so the runner's cargo accepts it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
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