Skip to content

A shared region's last handle takes its list of mappings, and a map that finds none is refused - #762

Merged
Japabu merged 5 commits into
mainfrom
wt/toyos-shmretire
Oct 8, 2026
Merged

Japabu merged 5 commits into
mainfrom
wt/toyos-shmretire

Conversation

@Japabu

@Japabu Japabu commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

The defect

SYS_SHM_MAP resolves its handle and clones the object's Arc under the process-data lock (sys_shm_map, kernel/src/syscall/ipc.rs), drops the lock, and calls SharedMemObject::map_into (kernel/src/object/shm.rs). A sibling thread that closes the region's last handle in between retires the object: the zero-handle hook empties the list of mappings, once. map_into checked nothing, installed a writable mapping and appended it to a list nothing reads again. The last Arc's drop then trips SharedMemObject::drop's debug_assert!.

Each step of the audit's trace holds on main at 0174516. Two corrections to the record: the object lives in kernel/src/object/shm.rs; and the kernel has one build profile, toyos, with debug-assertions = true (kernel/Cargo.toml), so on every kernel this tree builds the outcome is the panic — a userland race that panics the kernel — and the freed-while-mapped pages are what the assert stands in front of.

The fix, per decision

  • Who owns what. The handle count owns the mappings; the Arc count owns the pages. That was already the design: a syscall's Arc can be stranded on a killed thread's stack, so the mappings cannot ride it. But its first half was a convention: the list was a Lock<Vec>, and an emptied Vec accepts a push.
  • The structure. The list is now object::Held<Vec<..>>, which every other deferred object already keeps its released resource in. The hook takes the list out, so after it there is no list; map_into reaches the list only through Held::with_mut, under the lock the take used. One lock, no check-then-act: a map lands before the take and is torn down by it, or finds nothing and answers Gone having touched no page table. Held gains take and with_mut; release is now drop(take()).
  • The answer. Gone, as a partition claim whose last handle went answers a transfer (DeviceClaim::partition_view). toyos-abi's shm_map doc says so; a doc comment, no identity change.
  • A region no handle ever named (an inbox's ring page, mapped by inbox::create and unmapped by InboxRef's drop) never retires and keeps its list: unchanged.
  • Drop's assert stays, as the check that a handle-less owner unmapped before letting go.

The same shape elsewhere

Read: every deferred row of kobject! and every syscall that acts through a reference cloned out of the table.

  • Pipes, connections, inboxes, device claims: the resource is in Held, reached under its lock. Holds.
  • Ports: closed and pending share one lock (PortQueue); Connector::push checks and inserts under it. Holds.
  • A region lent to a device (pcidev::dma_map) or read as a program image (file_backing::SharedImage) after its last handle went: both hold the Arc, which owns the pages, and neither makes a user mapping. Holds.
  • Does not hold, PCI: a device call resolves its claim to a slot index, drops the lock, and pcidev::with_bound(slot) acts on whoever is bound there by then. That is issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md, which main has from Three audit claims traced: a BAR asked for twice panics the kernel, netstack keeps a dead client's socket, and a PCI call's slot is not its claim #755; this branch files nothing on it and does not fix it.
  • ISA and ACPI rows, traced. The calls on the claim do not have the window: a read and a write (read_device, write_device, kernel/src/object/ops.rs) run whole under the process-data lock of the table holding the claim's one handle (sys_read, sys_read_nonblock, with_object_ref), and a claim carries no Rights::DUP, so the step that fails is "lets the lock go": nothing can release the claim inside a call, and after the close the handle refuses it. The acpi claim's mediated access and the Global Lock, and acpiserver loading the machine's tables through them #749's reviews reached the same answer from the binding's side (claim_row mints nothing while a process is bound, and process_ends runs after the last thread left). A poll does have it, and it is issues/a-poll-registered-racing-its-own-close-outlives-the-release.md, which main already carries with an exit written for every claim's watch; this branch files nothing on it and does not edit that file. What the trace adds to it: inbox::resolve clones the claim out and arm then reads isa::has_irq(row) and registers on isa::watch(row), neither of which asks whose the row is, and the ISA and ACPI rows have the window only where the binder moved the handle on and ended (isa::process_ends takes the binding back while the claim stays minted in the receiver's table, so after the receiver's close claim_row mints the row again). Where the poller is the process that bound the row, claim_row answers Owned until its last thread has left. Read from the code, not run.
  • A process that ends with a region mapped (the review's off-fence note): holds. Nothing but an inbox's teardown calls unmap_from, and teardown_resources touches no region's list, so the entry's PageTables Arc keeps the dead process's address space, its page-table pages and its user PCID, until the region's last handle goes anywhere; one entry per such process, unbounded, against a pool of MAX_USER_PCID tags whose exhaustion refuses every spawn. Pids are never reissued, so no later process is answered a dead one's address. Filed as issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md. Read from the code, not run; not fixed here.

For wt/toyos-barremap (#761): a BAR window is its own SharedMemObject and goes with that object's last handle, not with the claim's (sys_device_bar_map, kernel/src/syscall/device.rs). After this change a retired region has no mapping list and can never be mapped again, so a retired object must never be handed out a second time. This branch does not touch that path.

Checks (high-risk: memory management, concurrency)

The race itself is covered by the type, and no test is added for it. The window is closed by where the list lives, which a reader checks in map_into's last line and the hook's first. The loom models and the process-life simulator compile no object-layer file, and the kernel has no actuator that holds sys_shm_map between its clone and its map; none is added.

Negative control, executed. The bad interleaving needs no concurrency to state: it is clone, last handle dropped, zero-handle queue drained, map_into. A probe patch (posted below as a comment, never landed) runs exactly that at boot against the real object, allocator and a fresh user address space. Both arms on 8340830, cargo test --test toyos-build -- --nocapture machine_shutdown_short_stop, the tree restored and git status --porcelain empty after each:

arm exit the kernel's words
the fix's kernel/ diff reverted + probe 1 shm-probe: a map after the last handle's teardown answered Ok(ffff400000) then PANIC: panicked at src/object/shm.rs:186:9: shm koid 4 freed with a live mapping
the fix + probe 0 shm-probe: a map after the last handle's teardown answered Err(Gone) then shm-probe: the region was freed and the kernel is running

This is the first execution of the defect. Independent oracle: none exists beyond the kernel's own pre-existing assert, which the fix did not write and which is what fires in the red arm.

Gates

The head is 933d291; every gate in this table was measured at 8340830. Since then the branch dropped the duplicate issue from its fix commit (now 73fd889, the same source tree), merged origin/main at 2e781a4, and filed, narrowed and deleted issue files. git diff 83408305e 933d291e7 -- . ':(exclude)issues' and git diff 58b768a03 933d291e7 -- . ':(exclude)issues' both print nothing, exit 0: every byte outside issues/ is what was measured here and what the T14 booted, and the merge moved no code, so nothing in this table was run again. At 933d291, cargo test --test toyos-checks: exit 0, test result: ok. 36 passed; 0 failed.

command exit its log's closing line
cargo run -- --build-only 0 08:33:23 Build finished.
cargo run -- --ci host 0 08:50:36 [ci] Host: 77 step(s), all green
cargo test --test toyos-build -- machine_shutdown 0 08:39:23 test result: ok. 2 passed, 2 total (17.9s; workers: 15s building, 15s testing)
cargo test --test toyos-build -- screen_fatal_halt_composited 0 08:40:17 test result: ok. 1 passed, 1 total (53.8s; workers: 44s building, 9s testing)
cargo test --test toyos-build -- virt_user_mode (the AArch64 kernel) 0 08:41:01 test result: ok. 1 passed, 1 total (43.7s; workers: 40s building, 3s testing)

The guest tests chosen are the QEMU boots that map and release regions: every boot's programs map their log ring, and the composited screen test maps the scanout buffers.

The T14, at 58b768a

The binaries that exercise region lifetime directly ride the T14's shared boots and no QEMU registration, so the T14 is their only reading. Three boots, each whole, since every member that makes an inbox goes through map_into and InboxRef's unmap_from; the shipping set is chunked into two images, so the review's two boots are three. Staged by the implementer from the clean tree at 58b768a with cargo test --test toyos-build -- --metal --metal-readback <dir> (exit 2, closing line [metal] staged 30 image(s); no filter selects the shared boots whole, so the whole profile was staged and the 27 images nobody asked for were removed). Booted by the orchestrator, one boot per image, each image's sha256 checked against the request before it was flashed:

boot command exit image sha256 ===TEST_END lines of them exit=0 the rows the review named, each exit=0
shared cargo run --bin toyos-metal -- --image <dir>/shared/image.img --readback <dir>/shared --fat32-check 0 fe56f9ae0929006d6cf3b35ddc5fd08c5e6956eb69f3ce89243330fa37271cca 62 62 test_rs_abuse_shared_grant
shared-2 cargo run --bin toyos-metal -- --image <dir>/shared-2/image.img --readback <dir>/shared-2 --fat32-check 0 789cd7391c6837977677a834a4e95b72ad9b746c7e01d4acb54ceb208c7f1dea 22 22 test_rs_spawn_image_object
shared-debug cargo run --bin toyos-metal -- --image <dir>/shared-debug/image.img --readback <dir>/shared-debug --fat32-check 0 d81a0458f52bd0215877fcbcdc62d5a15621fd21fa03e4a319a23d7771aeb376 7 7 test_rs_handle_lifetime, test_rs_shm_release_reclaims

The orchestrator's record of the run is #762 (comment), and round 2's review read the three readbacks themselves: each verdict.txt is passed, and the names on each boot's exit=0 lines are exactly the test_rs_* jobs its image's system-*.toml declares.

Landing order, as the orchestrator ruled it: this pull request lands before #761. Its T14 reading is then of the kernel that lands: no byte outside issues/ has moved since 58b768a, and this branch does not merge origin/main again. #761 then merges main and owes its own readings at its merged head.

Unsure of

  • The filed issue is a trace, and so is what this body adds to main's poll issue: neither interleaving was executed, and neither has an actuator that could.
  • The harness's by-name judge was not run over the three readbacks, and is not owed. For a shared member the judge is Readback::job_passed (tests/common/metal.rs), the exit code off that member's ===TEST_END line, which is the line read here, against a set the review compared with each image's own job list. What the judge adds per boot over toyos-metal's verdict is stop_completed and log_reached_the_stick, both read by the review off each loader.log, and the boot and panel timings against the machine's record, which this change makes no claim about.
  • cargo run -- --ci host was not run again at 933d291: the commit since moves two files under issues/ and nothing else. CI's host, toolchain and guest are skipped on a draft, and a skipped check is not a reading.

Size

git diff --shortstat origin/main...933d291e7: 4 files, +112 −45. Production: 3 files, +67 −45 (Held's two verbs +10, the module header +4, the rest is map_into and unmap_from re-nested under with_mut). Tests: 0. Issues: one filed, +45.

Closes issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md, carried here from #754's branch in the first commit and deleted in the second; #754 was closed without landing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A

Carried from the kernel audit's branch, where it was traced and not
executed, so that the fix that follows closes a file this branch holds.

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

The negative control's probe, applied as a checked patch to 8340830 and restored after each arm. Never landed.

diff --git a/kernel/src/main.rs b/kernel/src/main.rs
index d9efe43de..9f0326cdb 100644
--- a/kernel/src/main.rs
+++ b/kernel/src/main.rs
@@ -497,6 +497,22 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! {
     gpt::probe_usb_disks();
     rootfs::hold_source();
 
+    // PROBE, never landed: `SYS_SHM_MAP`'s steps with the last close inside its window. `shm` is
+    // the reference the syscall cloned out of the table; the one handle goes and its hook runs,
+    // as a sibling thread's close does; then the syscall maps.
+    {
+        use crate::object::{shm::SharedMemObject, HandleEntry, KObjectRef};
+        let shm = SharedMemObject::create(crate::mm::PAGE_2M).expect("probe: a region");
+        let space = crate::mm::paging::AddressSpace::new_user().expect("probe: an address space");
+        let pt: crate::process::PageTables = alloc::sync::Arc::new(crate::sync::Lock::new(space));
+        drop(HandleEntry::new(KObjectRef::SharedMem(shm.clone()), toyos_abi::handle::Rights::MAP));
+        crate::object::drain_zero_handles();
+        let answer = shm.map_into(toyos_abi::Pid(1), &pt);
+        log!("shm-probe: a map after the last handle's teardown answered {answer:x?}");
+        drop(shm);
+        log!("shm-probe: the region was freed and the kernel is running");
+    }
+
     #[cfg(feature = "boot-actuators")]
     if actuator::leak_rollback_selftest() {
         leak_selftest::run();

Red arm (the fix's kernel/ diff reverted, then the probe): cargo test --test toyos-build -- --nocapture machine_shutdown_short_stop, EXIT=1. The guest's serial log:

[ 3.992 cpu0 kernel] shm-probe: a map after the last handle's teardown answered Ok(ffff400000)
[ 3.993 cpu0 kernel alert] PANIC: panicked at src/object/shm.rs:186:9:
shm koid 4 freed with a live mapping: the zero-handle hook did not run, so the pages go back to the PMM under somebody's window
[ 3.995 cpu0 kernel]   Backtrace:
[ 3.996 cpu0 kernel]     0xffff80007afdca5c  core::panicking::panic_fmt+0x2c
[ 3.997 cpu0 kernel]     0xffff80007af26794  core::ptr::drop_glue::<kernel::object::shm::SharedMemObject>+0x144
[ 3.997 cpu0 kernel]     0xffff80007aeece72  <alloc::sync::Arc<kernel::object::shm::SharedMemObject>>::drop_slow+0x12
[ 3.997 cpu0 kernel]     0xffff80007af86d28  kernel::kernel_main+0x1568
[ 3.998 cpu0 kernel]     0xffff80007af5d411  _start+0x21
[ 3.998 cpu0 kernel]   Contexts: cpu0 crashed at sp=0xffff80007bb625b0, asking about ctx 0x0
[ 3.998 cpu0 kernel]   cpu0 is on ctx 0x0 (never switched, or not a context)
[ 3.999 cpu0 kernel alert] panic: rebooting in 60 s, timed by the calibrated clock

Green arm (the probe on the fix): same command, EXIT=0. The guest's serial lines:

08:38:32 [serial 0] [ 4.819 cpu0 kernel] shm-probe: a map after the last handle's teardown answered Err(Gone)
08:38:32 [serial 0] [ 4.819 cpu0 kernel] shm-probe: the region was freed and the kernel is running
08:38:32 test result: ok. 1 passed, 1 total (18.7s; workers: 13s building, 6s testing)

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 of #762 at 83408305e, against origin/main 017451624. Read only: the diff, the four changed files whole, every caller of the mapping list, the implementer's logs on disk. Nothing was run.

Net lines (git diff --shortstat origin/main...83408305e): 4 files, +108 −45. Production +67 −45 (kernel/src/object/shm.rs, kernel/src/object/mod.rs, a doc comment in toyos-abi/src/syscall.rs); tests 0; issues +41. The production growth is accepted: it is the Option around the list and the two verbs that reach it.

BLOCKER

NOTE

  • Brief (1), the window, read line by line: closed. The list has four touch points and all four take the one Held lock: map_into (shm.rs:163), unmap_from (:170), the hook's take (:184), mapped_nowhere (:134). A map either pushes before the take and is torn down and flushed by the hook, or finds None and answers Gone before pt.lock(). The hook's take is the only one, and HandleEntry::drop's retired.swap assert (handle.rs:138) is what makes expect("the zero-handle hook runs once") a kernel-bug panic and not a userland one. The batch's KObjectRef holds an Arc across the hook (mod.rs:287), so the pages outlive the teardown and its shootdown. A map in another process holding its own handle keeps the count above zero, a handle in a connection's queue is a HandleEntry and counts, and a region no handle named (the inbox's) never retires. Lock order is what main has: list lock, then a page-table lock, in map_into and unmap_from; the hook takes the page-table locks with the list lock released; no path takes the process-data lock under either (sys_shm_map drops it before map_into, HandleEntry::drop only enqueues).
  • Brief (2), Held: not weakened. release is still take-under-the-lock, drop outside it. take and with_mut are pub(crate) with one caller each; no other Held user's resource is reachable through them that was not through release and with.
  • Brief (3), no landed test: none is owed. A host test of take-then-with_mut asserts Option::take and Option::as_mut. The probe does not land: it is kernel code shipped for a test alone, guarding two lines a reader checks (shm.rs:163, :184). The negative control CLAUDE.md asks of high-risk code is in the PR's comment at this head, both arms, with the red arm's revert byte-identical to the branch's kernel/ diff (compared against fix-kernel.patch), and the oracle is main's own assert.
  • PR body, "Gates" — the five rows give command and exit code and no log. The logs are on the implementer's disk at this head and say what the table says ([ci] Host: 77 step(s), all green; machine_shutdown 2 passed; screen_fatal_halt_composited 1 passed; virt_user_mode 1 passed); the body quotes each one's closing line.
  • PR body, "Who owns what" — "a claim's BAR windows must go when the claim's handle goes" is false of the tree: a BAR window is its own SharedMemObject and goes with that object's last handle (sys_device_bar_map, kernel/src/syscall/device.rs:193), which is the subject A claim keeps where its BARs are and no object over them: a BAR asked for again after its handle closed no longer panics the kernel, and two answers held at once map apart #761 files.
  • For the record, wt/toyos-barremap (A claim keeps where its BARs are and no object over them: a BAR asked for again after its handle closed no longer panics the kernel, and two answers held at once map apart #761): this change requires only that a retired SharedMemObject is never handed out again, and A claim keeps where its BARs are and no object over them: a BAR asked for again after its handle closed no longer panics the kernel, and two answers held at once map apart #761 at its present head already mints one per ask and deletes Bound::bars. On main the cached object's second handle dies in HandleEntry::new's assert before any map, so this branch changes nothing there. The two branches touch kernel/src/object/mod.rs and toyos-abi/src/syscall.rs in different hunks; whichever lands second rebases and re-measures.
  • Off the fence, unchanged by this branch, not found in issues/: nothing but InboxRef's drop calls unmap_from, so a process that exits with a region mapped leaves its (Pid, PageTables, UserAddr) entry in the list, holding that address space's Arc, until the region's last handle goes anywhere. To be traced and filed by whoever owns the object layer, not fixed here.

SEND BACK

Japabu and others added 3 commits October 8, 2026 10:58
…hat finds none is refused

SYS_SHM_MAP resolves its handle and clones the object out of the table,
drops the process-data lock, and maps. A sibling thread closing the
region's last handle in between retired the object: the zero-handle hook
emptied the list of mappings, once, and returned. The map then installed
a writable mapping and appended it to a list nothing would ever look at
again. With debug assertions the last Arc's drop panicked the kernel; on
a shipping kernel the pages went back to the allocator still present in
the caller's page table.

The handle count owns the mappings; the Arc count owns the pages. The
first half was a convention: the list was a Lock<Vec>, and an emptied
Vec accepts a push. It is now the object layer's Held<Vec>, which every
other deferred object already keeps its released resource in: the hook
takes the list out and nothing is left to push to, and a map takes the
same lock to find it, so the map either lands before the take and is
torn down by it, or finds nothing and answers Gone having touched no
page table. Held gains the two verbs that needs, take and with_mut.

The other objects with a zero-handle hook were read for the same shape.
Pipes, connections, inboxes and device claims hold their resource in
Held; a port checks closed and queues under one lock. The device calls
have a different one, a slot index that outlives its claim, which is
issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md.

Closes issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…m, and a dead process's address space kept by a region

The device-slot issue this branch filed was one subject with
issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md,
which main has; it is gone from the branch. Its untraced sentence, the ISA
and ACPI row, is traced: a read and a write run whole under the lock of the
table that holds the claim's one handle, so nothing releases the claim inside
one; a poll clones the claim out and then answers from the row, and a handle
moved on by a binder that ended lets the row be claimed again in between.

The second file is the review's note: nothing takes a process's entry out of
a region's list of mappings when the process ends, so the entry's Arc keeps
the address space, and its PCID, until the region's last handle goes.

Both are read from the code and not run.

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

T14 at head 58b768a03 (orchestrator's run; the three images staged by the implementer from the clean tree, each sha256 checked against the request and printed by the command that flashed it). Each boot is read whole, by toyos-metal's exit and the ===TEST_END lines of its readback; the harness's by-name judge was not run over them.

boot image sha256 toyos-metal ===TEST_END lines of them exit=0
shared fe56f9ae…37271cca exit 0 62 62
shared-2 789cd739…8c7f1dea exit 0 22 22
shared-debug d81a0458…71aeb376 exit 0 7 7

The four rows the review named, each exit=0: test_rs_abuse_shared_grant (shared), test_rs_spawn_image_object (shared-2), test_rs_handle_lifetime and test_rs_shm_release_reclaims (shared-debug).

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2 of #762 at 58b768a03, against origin/main 2e781a49d. Read only: the diff, the changed files whole, the two new issues against the code they cite, issues/ on origin/main, the three T14 readbacks on disk, the harness's judge. Nothing was run.

Net lines (git diff --shortstat origin/main...58b768a03): 5 files, +157 −45. Production +67 −45, unchanged since round 1 and accepted there; tests 0; issues +90.

Round 1's BLOCKERs

  • The T14 boots: CLOSED. Read from the readbacks, not from the comment's table. Each boot's verdict.txt is passed; the names on its ===TEST_END … exit=0=== lines are exactly the test_rs_* jobs its image's system-*.toml declares (62 of 62, 22 of 22, 7 of 7, comm empty both ways), none with another exit; the four named rows are among them (abuse_shared_grant in shared, spawn_image_object in shared-2, handle_lifetime and shm_release_reclaims in shared-debug). No kernel panic line in any log; the six panicked at lines in shared-2 are std_unwind's own. Each pass after the reset reads DONE and a tail ending Rebooting. with stop: 19 of 19 userland thread(s) stopped … 0 of N userland block operation(s) still open. shared-debug booted a different kernel (2756600 bytes against 2416840), as it must.
  • The duplicate device-slot issue: CLOSED. issues/a-device-call-acts-on-whoever-holds-its-claims-slot-by-then.md is in no tree of the branch's diff, and git grep at the head finds neither its slug nor the closed shm issue's; the fix commit's message and the body point at Three audit claims traced: a BAR asked for twice panics the kernel, netstack keeps a dead client's socket, and a PCI call's slot is not its claim #755's file.
  • git diff 83408305e 58b768a03 -- . ':(exclude)issues' is empty, as claimed: the code round 1 read line by line is the code at this head.

BLOCKER

  • issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md — one subject with issues/a-poll-registered-racing-its-own-close-outlives-the-release.md, which main has had since 2026-09-26 — the same window (the handle resolved and the object cloned out in kernel/src/inbox/mod.rs, then registered on a per-device static watch with no lock held), the same sibling close and release's cancel_polls inside it, the same harm (the next holder's interrupt completes the old poll: another process's device activity across the boundary), and an exit already written for every claim, not for PCI alone: "a poll cannot reach a watch whose object has ended", with a guest test in which a second claim "sees no completion arrive in the first one's ring", which also covers the at-once fire through has_data. The new file is true of the code (DeviceClaim::isa_row is a plain field the release does not clear, ops.rs:284 and :801 answer from the row, isa::release clears MINTED, claim_row mints again once BOUND is NOBODY, a claim's handle carries TRANSFER), and its sentence "the PCI half of the shape is …a-pci-call-in-flight…" is not: the PCI half of the poll is the older file. This file goes, as round 1's did. What it adds to main's file — the ISA and ACPI rows have the window only where the binder moved the handle on and ended, and the calls on the claim do not have it — stays in the body's bullet, which then cites main's slug; the branch does not edit a file it does not own. Round 1 offered "its own narrow file" without having searched for the poll's; that offer was wrong.

NOTE

  • PR body, "Owed: the T14" and "Not run" — stale: the boots were run at this head. The body takes the reading, per boot the cargo run --bin toyos-metal -- --image … --readback … --fat32-check command, its exit 0, the image sha256 and the 62 / 22 / 7 exit=0 counts, and loses "Not run". With the BLOCKER it is the body's only other change.
  • Whether the by-name judge is owed: no. For a shared member the judge is Readback::job_passed (tests/common/metal.rs:482), the exit code off that member's ===TEST_END line, which is the line read here, against a set I compared with the image's own job list. What the judge adds per boot over toyos-metal's verdict is stop_completed and log_reached_the_stick, both read above off each loader.log, and the boot and panel timings against the machine's record, which this change makes no claim about. The body's "Unsure of" line can say so.
  • issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md — true of the code: unmap_from's one caller is inbox/mod.rs:84; teardown_resources (process.rs:1028) closes handles and touches no list; the entry's PageTables Arc holds the AddressSpace whose drop is the only return of its PcidGuard (arch/x86_64/paging.rs:322); new_user answers None at exhaustion and loader/mod.rs:435 refuses the spawn ResourceExhausted; sys_shm_create's handle carries MAP, DUP and TRANSFER; pids are never reissued. Not a duplicate of issues/the-compositor-keeps-a-committed-regions-mapping-for-as-long-as-the-client-does.md: that exit ends a view at a close, this one at a process's end, and its harm is every spawn on the machine refused. Owner and exit stand; a test can fail the exit.
  • Same file, last sentence — narrower than the cited exit: the compositor issue's exit also reaches a process that closed its handle before it ended (unmapped at that close); the residue is a handle moved on by SYS_HANDLE_SEND, which is no close.
  • CI at this head: host, toolchain and guest are SKIPPED (run 37754056566, draft). A skipped check is not a reading.

The sequencing with #761

#761 lands first; this branch then merges origin/main. At the merged head the body owes, each with command, exit code and log at that head:

The landing then rests on host and guest / suite concluding success on the ready pull request at that head and again in the merge queue, and on the T14 reading in the body, which no check runs.

SEND BACK

…names what the compositor's exit leaves

The poll file was one subject with
issues/a-poll-registered-racing-its-own-close-outlives-the-release.md, which
main already carries and whose exit covers every claim's watch, not PCI's
alone. What the trace added, that the ISA and ACPI rows have the window only
where the binder moved the handle on and ended and that the calls on the
claim do not have it, stays in the pull request's body, citing main's file.

The region issue's owner line said the compositor issue's exit reaches it
only where the process still held a handle when it ended. That exit unmaps at
the close of the last handle, so it also reaches a process that closed before
it ended; what it leaves is a handle moved on by SYS_HANDLE_SEND, which is no
close.

Nothing outside issues/ moves.

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

Review round 3 of #762 at 933d291e7, against origin/main 2e781a49d (fetched now; it is also the merge base). Read only: the one commit since 58b768a03, the remaining issue whole, the body, the T14 comment, issues/README.md, #761's diff at 8a6dc0be3. Nothing was run.

Net lines (git diff --shortstat origin/main...933d291e7): 4 files, +112 −45. Production 3 files, +67 −45, unchanged since round 1 and accepted there; tests 0; issues +45.

Round 2's BLOCKER

  • The duplicate poll issue: CLOSED. git diff 58b768a03 933d291e7 deletes issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md whole; git grep at 933d291e7 for its slug, for poll-in-flight and for its heading finds nothing (exit 1); git diff --name-status origin/main...933d291e7 -- issues is one line, the region issue added. The body's ISA and ACPI bullet cites main's issues/a-poll-registered-racing-its-own-close-outlives-the-release.md, which exists at the head, and the branch does not edit it.

What the T14 reading is of

  • git diff 58b768a03 933d291e7 -- . ':(exclude)issues' prints nothing, exit 0; one commit, parent 58b768a03, no merge. git diff 83408305e 933d291e7 -- . ':(exclude)issues' prints nothing either. The kernel booted three times at 58b768a03 (comment 6056578038) is this head's kernel, and the code the gates and both arms of the negative control measured at 83408305e is this head's code.
  • It stays the kernel that lands only while main is 2e781a49d when the queue takes this: the queue's merge is then this head's tree. Anything that lands first and moves a byte under kernel/, toyos-abi/ or what the three images are built from voids the reading, as round 2 said of a merge.

BLOCKER

None.

NOTE

The order: this lands before #761

Nothing is wrong with it, and it is the cheaper order.

What the landing rests on

  • host, toolchain and guest / suite concluding success, not skipped, on the ready pull request at 933d291e7, and again in the merge queue. All three are SKIPPED today (run 37755710416, draft), which is no reading.
  • The T14 reading in the body, which no check runs, under the condition above.
  • The negative control in the body, both arms on 83408305e.
  • --ci host not rerun since 83408305e: accepted. The diff since is issues/ alone. The host steps that read that directory resolve a manifest's or a build reason's cited issue (src/userlandhost.rs, src/build.rs:2908), and nothing in the tree cites either file this branch's issue commits touched; toyos-checks is reported at the head, exit 0, 36 passed, and tests/checks.rs and tests/checks/ hold 36 #[test]s. CI's host on the ready pull request is the reading at this head, and a red there sends the branch back whatever this line says.

LAND

@Japabu
Japabu marked this pull request as ready for review October 8, 2026 09:23
@Japabu
Japabu enabled auto-merge October 8, 2026 09:23
@Japabu
Japabu added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit fb00abf Oct 8, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-shmretire branch October 8, 2026 10:02
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
Japabu added a commit that referenced this pull request Oct 8, 2026
Clean. This branch names no dependency and no lockfile, so the root lock is
main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: 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