Skip to content

SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's - #642

Merged
Japabu merged 10 commits into
mainfrom
wt/toyos-proclife
Oct 2, 2026
Merged

Japabu merged 10 commits into
mainfrom
wt/toyos-proclife

Conversation

@Japabu

@Japabu Japabu commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Stage 0 of issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md: the Process handle is the only control, so the one call that turned a pid into a handle is deleted with every name only it reached. Deleting the reopenable column that call needed exposed a second thing that needed it, #659's spawn, so the order a spawn mints its handles in changes here too.

What changed, per decision

  • SYS_PROCESS_OPEN is deleted outright, and 110 is free. sys_process_open, its dispatch arm, toyos_abi::syscall::{SYS_PROCESS_OPEN, process_open} and SysCap::open_process go. 110 is not added to retired_syscalls! and the ABI carries no comment for it: root CLAUDE.md says a removed number is free, and issues/design-debt/the-abi-still-keeps-retired-syscall-numbers.md owns the table. Rights::MANAGE leaves init's SysCap, where opening by pid was its only use; it stays the right a kill needs.
  • The kobject! sealed/reopenable column is deleted, with its control: process::process_object, object::process::reopen_selftest, sched::kthread::open_selftest, the process-reopen-selftest actuator, its metal row, judge, SELFTESTS arm and FLASHABLE name. ProcessEntry::object goes too: process_object was its last caller on main.
  • A spawn mints its child's own handle with the object, and its caller's from it. On main a spawn puts the child's self in the child's table, schedules the child, and only then installs the spawner's handle (sys_spawn): a child whose table closes in between takes its object's count to zero and back, which only a reopenable row survives. loader::spawn now mints the child's self as it makes the object (start::own_handle) and hands that HandleEntry to the commit. PendingHandles::commit holds no object but the one inside that entry: it mints the caller's handle from it under the caller's lock, installs it, and puts the entry in the child's table. sys_spawn returns the handle the commit answered and installs nothing.
  • Why no gap is left, by every point from the object's creation to the spawn's return at which a handle exists:
    1. ProcessObject::new, then at once start::own_handle: one handle, a value in the spawning thread's kernel frame. No table holds it and no thread can name it.
    2. commit, the caller's lock held. A refusal drops that handle and the count reaches zero for good: the only other reference to the object is spawn's Arc, which goes as spawn returns the refusal.
    3. commit installs the caller's handle: two handles. Once the lock is released every thread of the caller reaches this one, and can close it, dup2 over it, duplicate it or transfer it. Each leaves the count at one or more, because the child's own is not in a table any thread reaches.
    4. The child's own moves into the child's table: a local of commit, then of spawn, then inside the ProcessData spawn builds. Still reached by nobody.
    5. The landing, under PROCESS_TABLE: the entry is inserted and the thread enqueued. From here the child's threads and the child's teardown reach the child's own handle. A count that reaches zero now is final: KObjectRef::Process( is constructed once, in own_handle, every other HandleEntry::new in the kernel names an object of another kind made at its own site, and a duplicate needs a live entry.
    6. spawn answers the caller's handle as a number. Nothing is minted.
  • The room for the caller's handle is checked before anything moves, and that is a behaviour change. A spawn needs a free slot for its answer in its caller's table (has_room(1)), checked before any endowment moves and with no arithmetic over what the endowments would free. main served a caller exactly at its cap whose spawn named an endowment: the move freed the slot the answer then took. This branch refuses that spawn ResourceExhausted, by name, with the table unchanged and no child built. abuse_handle_table's arm at the cap pins the refusal as the rule: it spawns from a full table naming one of its handles as an endowment, and on main's kernel reads Ok(RawHandle(8191)) (the last control row below). toyos_abi::syscall::spawn's doc states the rule. sys_spawn's kill of a landed child it could not name goes. This is the exit of issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md, which is closed here.
  • loader::spawn takes the commit as a closure, generic over what the commit answers beside the pid: () for the boot's init, which no table but its own holds, and a RawHandle for the syscall. PendingHandles loses its Ready variant and is a caller's request alone. The alternative, one enum answering Option<RawHandle>, would have sys_spawn unwrap a None the types could refuse. The kernel ships two copies of spawn for it; the size is under "The kernel's size". The other cut, spawn split at its point of no return into a build and a landing, was not taken: spawn reads 34 of its locals after the commit (counted off kernel/src/loader/mod.rs), an upper bound on what a split carries: at least a dozen of them in a struct declared, built and taken apart once each, against the closure's one parameter and two call sites. That is a count, not a built diff.
  • toyos-proclife models the spawn's two lock sections and a close by the spawner's other thread. The model's spawn is cut where the kernel gives up its caller's lock: the commit mints the child's own handle and the caller's, and the landing is its own section. Op::Close is sys_close by another thread of the spawner on the handle the spawn will answer. L13 refuses a handle minted on an object whose last had gone. mutate-spawner-handle-before-the-childs-own restores 35e5338's order and mutate-spawner-handle-after-the-landing main's; src/ci.rs's CONTROLS runs both.
  • debug_action::HOLD_SPAWN_UNTIL_CHILD_ENDS (23), on the test kernel only: the caller's next spawn whose child lands parks, after the landing, until the child's exit is published. No caller can order a child's end inside the spawn that starts it.
  • Tests. spawn_child_ends_first (new, the shared SYS_DEBUG boot): under the hold, a spawn answers a child that has already ended and its handle reads the code; and a spawn from a full table starts no child, read off a pipe that child would have spoken into. abuse_handle_table gains the arm at its cap. process_lifecycle loses its pid arm, whose callee is gone. The model gains a_sibling_closing_a_spawns_handle_before_the_spawn_returns.
  • Issues. The track's stage 0 is deleted. the-capability-end-state-is-twelve-answers.md question 3 says no arm takes a pid, question 10 drops SYS_PROCESS_OPEN, and the paragraphs that argued from 110 go; the-kernel-keeps-nothing-it-enumerates.md loses its clause about MANAGE. Filed: issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md. The commit still installs the caller's handle before the landing, so that window is not removed.

git grep at this head for sys_process_open, process_open, open_process, SYS_PROCESS_OPEN, reopenable, process_object, reopen_selftest, open_selftest, process-reopen, process_reopen and process-open-kthread finds only toyos-symbols/tests/fixtures/input-test.bin, whose symbols are frozen test data.

Net against origin/main: +473 −400 in 28 files. Production (kernel, toyos-abi, toyos, src) +149 −264; the model and tests +286 −70; issues +38 −66.

Gates at c837976

gate exit
cargo run -- --ci host (66 steps, both spawn controls among them) 0
cargo run -- --build-only 0
cargo test --test toyos-build (the QEMU suite, 21 guests) 0

High-risk checks (ABI, process lifecycle, a refcount under concurrency)

Every run, its log lines and every mutation patch are in #642 (comment); the probe is the one of #642 (comment), a patch that adds one machine test booting tests/testcases under QEMU and running the named shared-boot binaries. None of it lands.

The model, cargo test -p toyos-proclife:

tree feature exit what went red
40d7842, the kernel's order as 35e5338 had it none 101 a_sibling_closing_a_spawns_handle_before_the_spawn_returns: "pid 2: a handle to it was minted after its last one had gone", schedule spawn_under#0 -> spawn_under#0 -> close#1 -> spawn_under#0
c837976 none 0 none of 48
c837976 mutate-spawner-handle-before-the-childs-own 101 the same test on the same schedule
c837976 mutate-spawner-handle-after-the-landing 101 a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it, a_spawn_racing_the_kill_of_its_own_spawner and the new test

The probe at c837976:

run kernel change applied exit what went red
green shipping 110 probe arm 0 none of the 78 shipping members
green test none 0 none of the 5 SYS_DEBUG members
main's order: the spawner's handle installed once loader::spawn returns test mut-old-order.patch 1 PANIC: … src/object/handle.rs:108:9: a handle to a retired Process, from ops::install in sys_spawn
the entry minted in the commit but its slot taken after the landing, no room check test mut-slot-not-taken.patch 1 spawn_child_ends_first: "the refused spawn's child started and spoke"
the same shipping mut-slot-not-taken.patch 1 abuse_handle_table: "a spawn from a full table was not refused"
35e5338's order: the caller's handle installed, its lock released, the child's own minted after test mut-head-order.patch 0 none of the 5
the same shipping mut-head-order.patch, 110 probe arm 0 none of the 78
negative control: kernel/ as on origin/main shipping nc-whole.patch, 110 probe arm 1 2 of 78: process_lifecycle ends 139 at the probe's call of 110, abuse_handle_table on "a spawn from a full table was not refused"
  • The race the review found, a close by the spawner's other thread between the caller's install and the child's own mint: red in the model at 40d7842, before the kernel changed; green at c837976; red again under the feature that restores that order. In the kernel the order is commit's parameter type. mut-head-order.patch, the kernel half of c837976 reverted, stays green in every booted member: no guest arm runs two threads of one spawner inside the commit.
  • The first race, a child's table closing before the spawner's handle exists: at 7fe8c56, before the order changed, the probe ended in the kernel's assert (exit 1); mut-old-order.patch ends in the same assert at this head; mutate-spawner-handle-after-the-landing reds the model.
  • Negative control for the whole change, and for the syscall's deletion: the last probe row. With nc-whole.patch applied, git diff origin/main -- kernel names one line, the constant the reverted dispatch needs. The 110 probe arm calls 110 through arch::bare_syscall and expects InvalidArgument, the answer for a number nothing serves: green at this head, and on main's kernel sys_process_open serves it and ends the caller on the handle it reads. The arm is in the control and not in the tree: no committed test pins a free number.
  • Independent oracles: the interleaving model, which enumerates every ordering of the spawn's sections against a close and a kill; the recorded failure, which is the kernel's assert and not a test's; rustc's name resolution for the deletion; and the T14, below. Outside the tree, rg for the deleted names finds no caller: nothing in ~/Dev/jan/forks (16 clones) or rust/library; in ~/.cargo/git/checkouts a cached copy of this repository at 6115718 that no manifest names; in ~/.cargo/registry/src the published toyos and toyos-abi 0.1.0 and 0.2.0, which are the definitions, and uucore's unrelated Windows process_open.

Tiers

The order a spawn mints its handles in is held on the host: by commit's parameter, a handle and not an object, and by the model's L13 with its two controls. spawn_child_ends_first is a member of the shared SYS_DEBUG boot, which runs on the T14; it adds no QEMU guest test. What the host cannot reach is that a child ending inside its own spawn leaves the spawner a handle that reads its code: that takes a booted kernel and the hold.

The kernel's size

The shipping x86-64 kernel cargo run -- --build-only stages, read with the toolchain's llvm-size -A; origin/main is this worktree with git diff --binary c8379761c origin/main applied, built, and reversed.

file .text every section
origin/main (a31eec5) 3134088 1372619 4115614
c837976 3148648 1383691 4127902
difference +14560 +11072 +12288

loader::spawn is one symbol of 15396 bytes on origin/main and two at this head, 11263 for the syscall's and 11398 for init's, which runs once a boot. The aarch64 kernel was not measured.

T14

At 35e5338, run by the orchestrator (#642 (comment)), LENOVO 20W0003AMZ, BIOS N34ET71W (1.71): six boots, each toyos-metal exit 0 with verdict passed, and 9 members ended, 9 with exit 0.

filter / boot image sha256 members, exit
spawn/shared 6d55f4de59ee23a6f04f9b0248204196d97a3f7cbd2a15f933d8bcc6deb1a84b abuse_spawn_argv 0, spawn_image_object 0
spawn/shared-debug e950408bd9bd29eff453d46237a893325e77e4813187ffef6223d73270b43d3f spawn_child_ends_first 0, spawn_lands_claimed 0
handle/shared 70672353b04ff7fa5e45229cba66ac2581e0f698308887f15a3bdd42896335a4 abuse_handle_table 0
handle/shared-debug a73d68406b3c64180062e976384e2264506ce22c511486935b588c84b693409b handle_lifetime 0
process/proctreecase a18c5a34fc6c6130992a2b6b291e20929e886d58d04072711409840957345ed9 process_tree 0
process/shared b7d7759a101234dc18b6755e52f541ffdc7ff872f82d32bc3fd9d0f92a350a47 process_lifecycle 0, std_process 0

The rows' judges over those readbacks, cargo test --test toyos-build -- --metal --metal-readback <dir> <filter> at 35e5338: spawn exit 0 (4 passed, 0 failed, 2 boots), handle exit 0 (2 passed, 0 failed, 2 boots), process exit 0 (PASS process_tree; 3 passed, 0 failed, 2 boots). These are that head's green arms; no mutation was booted on the machine.

At c837976, run by the orchestrator (#642 (comment)), same machine: seven boots, each image's sha256 checked against the request before it was flashed, each toyos-metal exit 0 with verdict passed, and 9 members ended, 9 with exit 0. commit moved since 35e5338, so these are the six boots again and one row of the selftests image, which has one arm fewer.

filter / boot image sha256 members, exit
spawn/shared fc71752979a3016e43e9827aa2582606dcce28ea4fdf000594f41a80ca178944 abuse_spawn_argv 0, spawn_image_object 0
spawn/shared-debug 2eb796a5994c42f0cb518dad181f5302a6a72d1547be66c3e3361f1c023f4378 spawn_child_ends_first 0, spawn_lands_claimed 0
handle/shared 0b5a8c89e3d74d3bcd06932a415ca17f0bb16663be5e9ea3583cbac34faa05cd abuse_handle_table 0
handle/shared-debug 0baa985b23578b82bc8f8bf07dccd17401975f560a895681b40d2630692c66b5 handle_lifetime 0
process/proctreecase d7b73c13e6736fda7ea01a1bcb96a443abccb50fc9d6597f7dd68fa2fad1c90c process_tree 0
process/shared 75eca94fff32d454f6c3384708c9490c502d0a6c230c5d2eea991e1add19e965 process_lifecycle 0, std_process 0
pci_capability_walk/selftests e6289effac6efd34db4212a7a94108071baccc913387f04876c04fe2ae24036f no test-runner job; its ten actuators armed, kernel.log:97 virtio: pci cap selftest 15/15

The rows' judges over those readbacks, cargo test --test toyos-build -- --metal --metal-readback <dir> <filter> at c837976, run by the orchestrator from the clean worktree: spawn exit 0 (4 passed, 0 failed, 2 boots), handle exit 0 (2 passed, 0 failed, 2 boots), process exit 0 (PASS process_tree; 3 passed, 0 failed, 2 boots), pci_capability_walk exit 0 (PASS pci_capability_walk; 1 passed, 0 failed, 1 boot). The spawn, handle and process judgings each offered boot.shared.* rows for tests/metal/lenovo-20w0003amz.toml; none is committed, since shared's whole boot is not this branch's. These are the head's green arms; no mutation was booted on the machine.

🤖 Generated with Claude Code

https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm

Stage 0 of issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md,
ruled by the owner: the `Process` handle is the only control, so the one call
that turned a pid into one is deleted with every name only it reached.

- `sys_process_open`, its dispatch arm, `toyos_abi::syscall::process_open`
  and `SysCap::open_process` go. 110 enters `retired_syscalls!`, so a call of
  it answers `NotSupported` and the kernel logs
  "syscall 110 is retired (formerly SYS_PROCESS_OPEN)"; the ABI keeps the
  number as a comment, as it does 8, 85, 87 and 107.
- `Rights::MANAGE` leaves init's `SysCap`: on a `SysCap` it opened a process
  by pid and nothing else. It stays a `Process` handle's right to kill.
- The `kobject!` `sealed`/`reopenable` column goes. `Process` was its one
  `reopenable` row, there only because 110 could mint a handle after the
  last one had gone; with nothing left that installs a `Process` object but
  the spawn that made it, every row retires on its last handle and
  `HandleEntry` is byte-identical to its form before 1ee9ec9.
- `process::process_object`, `object::process::reopen_selftest`,
  `sched::kthread::open_selftest`, the `process-reopen-selftest` actuator,
  its `process_reopen_selftest` machine test and judge, its metal row, its
  `FLASHABLE` row and its recorded duration go. The two in-kernel controls
  measured `process_object`'s answers, and nothing asks that question now.
- `process_lifecycle`'s pid arm, the only caller, now asks 110 with the
  arguments it took (this process's capability and its own pid) and asserts
  `NotSupported`; `check_process_lifecycle` reads the kernel's retired
  record. `wait_raw` becomes `raw(num, a1, a2)`, which both raw arms share.
- `issues/kernel/the-capability-end-state-is-twelve-answers.md` (questions
  3 and 10, and the enforced-rulings paragraph) and
  `issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md` (stage 2)
  no longer argue from 110, and the track's stage 0 is deleted.

`git grep` for sys_process_open, process_open, open_process, reopenable,
process_object, reopen_selftest, open_selftest, process-reopen,
process_reopen and process-open-kthread finds only
`toyos-symbols/tests/fixtures/input-test.bin`, whose symbols are frozen test
data; `SYS_PROCESS_OPEN` is left only as the retired table's name for 110.

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

Japabu commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator runs at a8acd2a (logs orch/logs/642-642-*.log):

job args patch exit line
green process_lifecycle — 0
negative control (whole change reverted) process_lifecycle negative-control.patch 1 FAIL process_lifecycle: exit code Some(101)
110 not retired process_lifecycle mut-110-unretired.patch 1 FAIL process_lifecycle: exit code Some(101)
whole (none) — 0 test result: ok. 503 passed, 503 total (725.6s)

@Japabu
Japabu marked this pull request as ready for review September 30, 2026 21:47
@Japabu

Japabu commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #642, round 1, at a8acd2a

Gate. CI host at a8acd2a: run 36781659044, conclusion success. Guest runs (issuecomment-5920300921; I read the logs): green process_lifecycle 0; negative control 1 (process_lifecycle.rs:282, 110 answered PermissionDenied, so sys_process_open served it); 110 unretired 1 (InvalidArgument); whole suite 503/503.

Size. +103 −297 in all. Production (kernel, toyos-abi, toyos, src) is +40 −195, tests +49 −66, issues +14 −36.

The brief's questions

  • Retirement. 110 is in retired_syscalls! (kernel/src/syscall/dispatch.rs:98), and the ascending-order const assert holds. It is also a comment at toyos-abi/src/syscall.rs:266, in the same form as 85, 87 and 107. It can be reused exactly as easily as every other retired number; see the first NOTE.
  • Callers. At the head, git grep finds the deleted names only in the retired table, the test, the ABI comment and the issue. rg over ~/Dev/jan/forks (16 clones), ~/.cargo/git/checkouts (54), ~/.cargo/registry/src, rust/library and rust/src/tools finds only ~/.cargo/git/checkouts/toyos-b407e7a14e68aa06/6115718. That is a cached copy of this repository at 6115718 (The ACPI decode becomes a pure crate with a corpus, and the loader's relocation window becomes a refusal #386), and no Cargo.toml or Cargo.lock in the tree or the forks names it as a source. The forks take toyos/toyos-abi from crates.io, and none calls either deleted function.
  • Kept just in case. Nothing:
    • kernel/src/object/handle.rs is blob 4ffe21b7c, the same blob as at 1ee9ec9ad^.
    • kernel/src/object/mod.rs differs from 1ee9ec9ad^ only by the later Held::with.
    • is_kernel_task, ProcessEntry::object, handle_result, Process::from_raw and ObjectCore::retired all still have other callers.
    • KObjectRef::Process( is installed only at kernel/src/syscall/proc.rs:45, so the sealed row's drop assert never sees a second zero crossing.
  • Negative control. I applied negative-control.patch to a8acd2a's tree in a scratch clone. The result differs from 649ea51 only in process_lifecycle.rs, tests/toyos.rs (+23: the judge and its check_for row) and the three issue files. That is the whole change reverted onto the base with the test kept, and it goes red for the right reason.
  • The retired record. It is the general retired-syscall answer. It comes from syscall_dispatch's _ arm (dispatch.rs:667-671), which predates the branch, and log_hold.rs and panic_halts_first.rs already read it. Nothing ships for this test alone. The judge that reads the record here is the second BLOCKER.
  • The owner's rulings of 2026-09-30 written where they govern #645. I measured it, and the two do not conflict. git merge-tree of main e754dc8 with 7eb4428 and a8acd2a is clean in both landing orders, and both orders give the same tree, 9fb4f4626. In the track file, The owner's rulings of 2026-09-30 written where they govern #645's hunks are the header, the ruled paragraph, and stage 2 onward; SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642's only hunk deletes lines 43–56, which is stage 0. The second to land merges main as it stands. If a later push to either branch does make the file conflict, take The owner's rulings of 2026-09-30 written where they govern #645's side of every hunk and delete the stage-0 paragraph.

BLOCKER

  • tests/toyos-rust-tests/src/bin/process_lifecycle.rs:98 — raw(num, a1, a2) is a second raw-syscall entry, with core::arch::asm! in a bin. It duplicates tests/toyos-rust-tests/src/arch/{x86_64,aarch64}.rs's bare_syscall, which base already has for exactly this case: a retired number, which the kernel refuses without reading an argument (panic_halts_first.rs:22 calls 26 through it). Fix it one of two ways:

    • call arch::bare_syscall(110) through #[path = "../arch/mod.rs"] mod arch; and return wait_raw to its base form, or
    • move raw into both arch halves.

    Either way, re-run the negative control on the result.

  • tests/toyos.rs:3234 — check_process_lifecycle's record half has never been seen red, and it can red a correct kernel.

    • Both mutation logs fail in check_rust_result (:3235) and return before the log is read.
    • The record reaches the wire through klogd, which takes 8 records and then 8 queued lines per hold (kernel/src/log/console.rs:119-122, :433-434). The runner's END line can therefore overtake a record backlog and close the window (tests/common/qemu.rs:3459) before the record is sent.
    • The guest half already tells retired (NotSupported) from unassigned (InvalidArgument), and it is measured red both ways.

    Delete check_process_lifecycle, PROCESS_OPEN_RETIRED and the check_for row. If they stay instead, two things are needed: a measured red under the one mutation only they catch (110 => SyscallError::NotSupported.to_u64(), above the _ arm in syscall_dispatch), and the record ordered ahead of the window's close.

NOTE

  • kernel/src/syscall/dispatch.rs:78 — nothing stops a retired number from being reused. A future SYS_* = 110 with a live arm silently shadows the table. The ABI's retired set and the kernel's already disagree: 8 and 107 are "retired and unused" at toyos-abi/src/syscall.rs:8 and :244, are missing from the table, and answer InvalidArgument. This predates the branch. File it, as one declaration that both readers read.
  • kernel/src/object/process.rs:87 — the deletion leaves a trailing blank line at the end of the file.
  • tests/toyos-rust-tests/src/bin/process_lifecycle.rs:280 — const PROCESS_OPEN: u64 = 110 spells the retired name again. The sibling tests call it RETIRED (log_hold.rs:11, panic_halts_first.rs:14).

REMOVE

  • issues/kernel/the-capability-end-state-is-twelve-answers.md:33 — "Four of the rulings are enforced in code" now names two. Delete the paragraph.
  • issues/kernel/the-capability-end-state-is-twelve-answers.md:107-110 — a count and a citation were corrected ("Five …, and 110 SYS_PROCESS_OPEN") instead of deleted.
  • tests/toyos-rust-tests/src/bin/process_lifecycle.rs:275-278 — "110 was the call that turned a pid into a handle …" describes a past implementation, and it points at the judge the second BLOCKER deletes.
  • tests/toyos-rust-tests/src/bin/process_lifecycle.rs:214 — "two arms here want the same cap" is a count, and three arms take it now.
  • PR body — remove all of these:
    • "the record check reds too": no run shows it.
    • "git grep 'KObjectRef::Process(' finds only sys_spawn": it also finds kernel/src/object/ops.rs:60 and the match arms.
    • every /Users/jan/.claude/jobs/… path: main's record would cite files nobody else can read.
    • "This is the first stage that waited on no open question" and "Stages 1 and 4 … come next".
    • the whole "What I am unsure of" section.

SEND BACK

Japabu and others added 2 commits October 1, 2026 00:06
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The owner ruled that the ABI is completely unstable: no stable ABI exists
yet, and toyos-abi and the SDK take breaking changes freely until ToyOS is
adopted. A deleted syscall is deleted and its number is free, so nothing
here retires 110.

- 110 leaves `retired_syscalls!` and the ABI's retired-number comment goes.
  The table itself stays; removing it tree-wide is a separate change.
- process_lifecycle's pid arm is deleted. A call that no longer exists
  needs no runtime test: every caller of the deleted names fails to
  compile. The other arms stay. `wait_raw` is its base form again, so no
  second raw-syscall entry is left in the binary (review B1).
- check_process_lifecycle, its record constant and its check_for row are
  deleted (review B2). No run had seen the record half red, and the
  record could reach the wire after the runner's window had closed.
- Review NOTEs and REMOVEs: the trailing blank line at the end of
  kernel/src/object/process.rs goes. In
  the-capability-end-state-is-twelve-answers.md, the paragraph on which
  rulings are enforced in code goes, and so does question 3's sentence on
  retired pid-addressed numbers. The test loses its count of the arms
  that take the cap, the arm's history comment, and the clauses of the
  module doc and the closing line that spoke for the deleted arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title SYS_PROCESS_OPEN goes: 110 is retired, and a pid reaches nothing SYS_PROCESS_OPEN is deleted: a pid reaches nothing, and 110 is free Sep 30, 2026
Japabu added a commit that referenced this pull request Oct 1, 2026
…d the defects the cut's tests found

- `.claude/agents/reviewer.md`: **Growth** said tests are cut only when they
  test nothing, which sends back every cut the ladder makes. It now reads
  "a test is cut only when it tests nothing, or as **Guest tests** says", and
  **Guest tests** says when: a cheaper tier already holds the behaviour, named
  in the pull request body, or a stage of a track names it in the same diff
  with an exit a build or test can fail. A guest test re-argues the tiers when
  it is new or its behaviour changes, not on every edit.
- The track carries owed work only: the verdict counts, the keep list and
  both cut lists go to this pull request's body, and stage M goes, its
  shard, duration and phase half done here and the rest naming nothing. It
  names its owner. The tests main redlists are not its items: each is red and
  its issue holds it. `late_storage_connect` stays metal, staged by its
  actuator rather than a plug; `https_tls13_e1000e` and
  `blackbox_early_panic_sealed_muted` join the rows that cover them;
  `metal_sim_input` and `virtio_used_ring` become host items and
  `query_pci_agreement` a metal one against Ubuntu's `lspci`; FPU isolation
  goes first in stage C; and each item whose arm needs a deleted feature or
  file says so.
- Defects #654 recorded on main, carried whether or not their test survives:
  `a-ready-marker-read-off-the-16550-file-can-end-the-boot-wait-mid-line.md`
  (`root_candidate_malformed`), `a-test-asserts-a-daemons-line-off-a-boot-log-that-ends-before-it.md`
  (`lan_dhcp_lease` and `iommu_virtio_platform`, one mechanism),
  `an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md`
  (`virt_smp`), `blockd_serves_partitions` folded into
  `qemu-drops-console-output-the-harness-is-slow-to-read.md`, and
  `process_stats`'s two assertions with an exit a test can fail.
  `redirty_mid_flush`'s red was #642's own and is not carried.
- `sys-debug-actions-and-two-loader-words-that-nothing-calls.md` records the
  ABI names the cut left with no caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu
Japabu marked this pull request as draft October 2, 2026 16:13
Japabu and others added 5 commits October 2, 2026 18:29
Six conflicts, each resolved by taking main's side and applying this
branch's deletion to it again:

- kernel/src/actuator.rs: main deleted the rows this branch kept around
  `process-reopen-selftest` and kept that row; the row goes.
- kernel/src/syscall/dispatch.rs: main's import list gained `spawn_place`;
  `sys_process_open` leaves it.
- src/metal.rs: `FLASHABLE` is a list of names on main; the
  `process-reopen-selftest` name goes.
- tests/toyos.rs: main moved the QEMU machine test out, so what is left to
  delete is the metal row, the `SELFTESTS` arm, the `process_reopen` judge
  and the two counts of that image's actuators.
- tests/test-durations: main deleted the file; this branch's one hunk
  removed a row of it, and goes with it.
- the track file: main reworded stage 0 and stage 1; stage 0 is deleted
  and stage 1 is main's.

kernel/src/object/handle.rs and kernel/src/object/mod.rs merge clean, so
the `Process` row is sealed again over #659's spawn, whose two installs
on a child's object are not ordered for that. The commits after this one
make that red and then remove it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…s for it

`debug_action::HOLD_SPAWN_UNTIL_CHILD_ENDS` (23) marks the caller's next
spawn whose child lands: its thread parks in `loader::spawn`, after the
landing and its retires, until the child's exit is published.
`spawn_child_ends_first` spawns a child that exits at once under that
hold and reads the child's code off the handle the spawn answers.

This is the window the merge before this commit left open: #659 installs
the child's own `self` at the commit, schedules the child, and installs
its spawner's handle only once `loader::spawn` has returned. A child
whose table closes in between takes its object's handle count to zero
and back, which `HandleEntry::new` asserts against on every row now that
none is `reopenable`. The binary is the control for the commit that
removes the window.

`ProcessEntry::object` goes: `process::process_object` was its last
caller on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ld lands

Measured first, on the commit before this one: `spawn_child_ends_first`
on the test kernel under QEMU ends in

  PANIC: panicked at src/object/handle.rs:108:9:
  a handle to a retired Process (koid 162)
    kernel::object::ops::install
    kernel::syscall::proc::sys_spawn

The child's table had closed, taking its `self` and the object's count to
zero, before `sys_spawn` installed the spawner's handle.

`PendingHandles::commit` now installs the caller's handle in the hold
that moves the endowments, before the child's own `self` and before the
landing, and answers it; `sys_spawn` returns that handle and installs
nothing. The room for it is checked with the child's table's, before
anything moves, so the refusal a full table draws leaves the caller's
table as it was and no child exists: the kill of a child that had landed
and could not be named goes.

`loader::spawn` takes the commit as a closure and answers what it left
its caller holding beside the pid, so the boot's init, which no table but
its own holds, answers `()` and the syscall a `RawHandle`: `PendingHandles`
loses its `Ready` variant and is the caller's request alone.

A handle the commit installed resolves before the syscall that will
answer it has returned. Only a caller that guessed its number can name
it there, and what it gets is what a process not yet in the table gives:
a wait that parks, `NotFound` for its accounting, `Gone` for a spawn
under it, and a kill that finds nothing to claim.

toyos-proclife: the model's spawn mints both handles in the section that
lands the child, a teardown closes its process's table, and L13 refuses a
handle minted on an object whose last had gone.
`mutate-spawner-handle-after-the-landing` restores the old order and reds
`a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it`
and `a_spawn_racing_the_kill_of_its_own_spawner`: the child lands claimed,
is retired and closes its table, and then its spawner's handle is minted.

Guest arms: `abuse_handle_table` spawns from its full table naming an
endowment, and is refused with the endowment still its own; under the
hold, `spawn_child_ends_first` spawns from a full table a child that
would speak into a pipe, and the pipe ends empty.

Closes issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md:
its exit was the handle in the commit's hold with the room checked before
anything moves, and a guest arm reading `ResourceExhausted` with no child
started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Under the control that lets a spawn from a full table succeed,
`abuse_handle_table` ended with exit 134 and no message: its
`expect_err` panicked with every slot of the table taken. Both arms now
take the spawn's answer, close what filled the table, and assert after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
@Japabu Japabu changed the title SYS_PROCESS_OPEN is deleted: a pid reaches nothing, and 110 is free SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn takes its handle to the child before the child lands Oct 2, 2026
@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Runs at 35e5338 (implementer; QEMU on this host, 14 cores, nothing else of this worktree running).

Each row is one run of the probe: every patch applied as a checked patch (git apply --check, then git apply) on a clean tree, TOYOS_PROBE=<members> [TOYOS_PROBE_DEBUG=1] cargo test --test toyos-build -- shared_probe, the command's own exit code, and the tree restored (git status --porcelain --ignore-submodules=none empty after each). TOYOS_PROBE_DEBUG=1 boots the kernel that carries SYS_DEBUG.

run head kernel patches beside the probe members exit
the hazard 7fe8c56 test none (probe-v1.patch) spawn_child_ends_first 1
green 35e5338 shipping probe-110.patch the 78 shipping members 0
green 35e5338 test none the 5 SYS_DEBUG members 0
old order 35e5338 test mut-old-order.patch spawn_child_ends_first 1
slot not taken 35e5338 test mut-slot-not-taken.patch spawn_child_ends_first 1
slot not taken 35e5338 shipping mut-slot-not-taken.patch abuse_handle_table 1
kernel/ as on origin/main 35e5338 shipping probe-110.patch, nc-whole.patch the 78 shipping members 1

The 78 shipping members: abuse_connect_flood abuse_cwd_growth abuse_elf_loader abuse_elf_segments abuse_gpu_resolution abuse_handle_table abuse_inbox abuse_listener_hijack abuse_page_straddle abuse_pipe_map abuse_pipe_owner abuse_pipe_ring abuse_readonly_copyout abuse_shared_grant abuse_shm_length abuse_spawn_argv abuse_thread_name abuse_tls_alloc allocator_stress blocking_read_stress c_aligned_alloc connect_before_serve cpal_drop_unreleased debug_trap demand_paging_sse demand_window_race disk_backtrace dlopen_dedup empty_dir_stat endowment_denied exit_wait_storm fault_gates file_mtime fs_cache_eviction fs_escape fs_large_file fs_stream_offset fs_truncate_persist fs_turns futex_wake_counts hierarchy_paths home_backing_revoked inbox_cancel_wakes kill_ends_every_wait lseek_past_eof memmap_exec_refused mmap_prot mmap_stress munmap_reissues_read_window munmap_reissues_second_read_window mutual_kill netd_gone_mid_bind nvme_home_roundtrip pipe_flag_forgery poll_wake_pipe poller_capacity process_lifecycle query_modules_size sched_stress spawn_image_object std_alloc std_fs std_fs_write std_io std_mmap std_process std_sync std_threading std_tls std_tls_cranelift std_tls_dlopen std_tls_multi_crate std_unwind std_unwind_so toybox_file_tools user_copy_spans_windows wall_clock_now window_refusal.

The hazard, at 7fe8c56:

kernel panic: PANIC: panicked at src/object/handle.rs:108:9: — the guest went quiet because every CPU is halted, not because it was still working. The panic is the finding and the guard never got to be one.
--- what the kernel said as it died ---
[kernel 0.553 cpu1] PANIC: panicked at src/object/handle.rs:108:9:
a handle to a retired Process (koid 162)
[kernel 0.553 cpu1]   Backtrace:
[kernel 0.554 cpu1]     0xffff80007c5f51ec  core::panicking::panic_fmt+0x2c
[kernel 0.554 cpu1]     0xffff80007c544eb0  kernel::object::ops::install+0x100
[kernel 0.554 cpu1]     0xffff80007c5914a2  kernel::process::with_process_data::<core::result::Result<toyos_abi::handle::RawHandle, toyos_abi::syscall::SyscallError>, kernel::syscall::proc::sys_spawn::{closure#0}>+0x52
[kernel 0.554 cpu1]     0xffff80007c542837  kernel::syscall::proc::sys_spawn+0x57
[kernel 0.555 cpu1]     0xffff80007c52be3a  kernel::syscall::dispatch::syscall_dispatch::{closure#1}+0x287a
[kernel 0.555 cpu1]     0xffff80007c52c274  kernel::syscall::dispatch::syscall_dispatch+0x104
[kernel 0.555 cpu1]     0xffff80007c55dbc3  kernel::arch::x86_64::syscall::syscall_handler+0x33
[kernel 0.555 cpu1]     0xffff80007c55d89c  kernel::arch::x86_64::syscall::syscall_entry+0x70
[kernel 0.555 cpu1]   Contexts: cpu1 crashed at sp=0xffff800006db66f8, asking about ctx 0xffff8000002aa3f0
[kernel 0.555 cpu1]   cpu0 is on ctx 0xffff800000200fb0 (its idle context) stack_top=0x0000000000000000 saved_sp=0xffff800000420ea0
[kernel 0.555 cpu1]   cpu1 is on ctx 0xffff8000002aa3f0 pid=9 tid=0 stack_top=0xffff800006db7000 saved_sp=0xffff800006db5160
[kernel 0.555 cpu1]   Running: pid=9 tid=Some(Tid(0))
[kernel 0.555 cpu1]   Process: test_rs_spawn_child_ends_fi pid=9 state=Live

The old order restored, at 35e5338:

  [probe] spawn_child_ends_first: exit None
FAIL shared_probe: spawn_child_ends_first: exit None
kernel panic: PANIC: panicked at src/object/handle.rs:108:9: — the guest went quiet because every CPU is halted, not because it was still working. The panic is the finding and the guard never got to be one.
--- what the kernel said as it died ---
[kernel 0.522 cpu1] PANIC: panicked at src/object/handle.rs:108:9:
a handle to a retired Process (koid 162)
[kernel 0.522 cpu1]   Backtrace:
[kernel 0.523 cpu1]     0xffff80007c5f846c  core::panicking::panic_fmt+0x2c

Slot not taken, test kernel:

  [probe] spawn_child_ends_first: exit Some(101)
FAIL shared_probe: spawn_child_ends_first: exit Some(101)

--- stdout ---

thread 'main' (1) panicked at src/bin/spawn_child_ends_first.rs:71:5:
assertion `left == right` failed: the refused spawn's child started and spoke
  left: Ok(1)
 right: Ok(0)

Slot not taken, shipping kernel:

  [probe] abuse_handle_table: exit Some(139)
FAIL shared_probe: abuse_handle_table: exit Some(139)

--- stdout ---
an endowment labelled self and 32 entries are refused, and 31 start

thread 'main' (1) panicked at src/bin/abuse_handle_table.rs:170:5:
assertion `left == right` failed: a spawn from a full table was not refused
  left: Ok(RawHandle(8191))
 right: Err(ResourceExhausted)

kernel/ as on origin/main: 76 of 78 exit 0; the two that do not:

  [probe] abuse_handle_table: exit Some(139)
  [probe] process_lifecycle: exit Some(139)
an endowment labelled self and 32 entries are refused, and 31 start

thread 'main' (1) panicked at src/bin/abuse_handle_table.rs:170:5:
assertion `left == right` failed: a spawn from a full table was not refused
  left: Ok(RawHandle(8191))
 right: Err(ResourceExhausted)
stack backtrace:

process_lifecycle: exit Some(139)

--- stdout ---
  the code is read three times and is the same each time
  a wait taken before the exit is woken by it
  a wake meant for something else does not end a wait
  two handles to one process answer the same code
  a kill publishes 137 once, and a second kill changes nothing
  a process that did not start the child waited for it, and so did the one that did
  an undefined WNOHANG-word bit is InvalidArgument, and without it the code comes back

process_lifecycle printed every arm and then ended 139, the handle-fault exit, at the probe arm's call of 110: main's sys_process_open demands a SysCap at whatever its first argument register holds. The kernel's own record of the fault is not in the captured output.

The model at 35e5338: cargo test -p toyos-proclife exits 0 (47 passed); cargo test -p toyos-proclife --features mutate-spawner-handle-after-the-landing exits 101:


---- interleave::tests::a_spawn_racing_the_kill_of_its_own_spawner stdout ----

thread 'interleave::tests::a_spawn_racing_the_kill_of_its_own_spawner' (81962031) panicked at toyos-proclife/src/interleave.rs:510:27:
a lifecycle law broke:
pid 3: a handle to it was minted after its last one had gone
  schedule: spawn_under#1 -> kill#0 -> kill#0 -> kill#0 -> kill#0 -> spawn_under#1 -> spawn_under#1 -> out 2/0 -> out 2/0 -> out 2/0 -> out 2/0 -> out 2/0 -> out 2/0 -> out 3/0 -> out 3/0 -> spawn_under#1

---- interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it stdout ----

thread 'interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it' (81962030) panicked at toyos-proclife/src/interleave.rs:510:27:
a lifecycle law broke:
pid 3: a handle to it was minted after its last one had gone
  schedule: spawn_under#1 -> kill#0 -> kill#0 -> kill#0 -> kill#0 -> spawn_under#1 -> spawn_under#1 -> out 1/0 -> out 1/0 -> out 1/0 -> out 1/0 -> out 2/0 -> out 2/0 -> out 2/0 -> out 2/0 -> out 2/0 -> out 2/0 -> out 3/0 -> out 3/0 -> spawn_under#1

T14, staged and not run: cargo test --test toyos-build -- --metal --metal-readback <dir> <filter> for spawn, handle and process, exit 2 each.

probe.patch — the machine test every row at 35e5338 runs
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 8fa631772..837b0c620 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -170,6 +170,10 @@ const MACHINE_TESTS: &[&str] = &[
     // The nested-NMI report is a raw write to the 16550, which the T14 does not
     // have.
     "nested_nmi_is_loud",
+    // PROBE, not for landing: the shared-boot binaries `TOYOS_PROBE` names, on
+    // one QEMU boot of tests/testcases; `TOYOS_PROBE_DEBUG` picks the kernel
+    // that carries `SYS_DEBUG`.
+    "shared_probe",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2024,6 +2028,39 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        "shared_probe" => {
+            let names = std::env::var("TOYOS_PROBE").expect("TOYOS_PROBE names the binaries to run");
+            let kernel_features: &'static [&'static str] =
+                if std::env::var_os("TOYOS_PROBE_DEBUG").is_some() { ACTUATOR_KERNEL } else { &[] };
+            let rust_bins = qemu::build_toyos_bins(
+                &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/toyos-rust-tests"),
+            );
+            let mut qemu = QemuInstance::boot_with_options(
+                test_config,
+                &[],
+                &rust_bins,
+                BootOptions { kernel_features, ..Default::default() },
+            );
+            let mut failed = Vec::new();
+            for name in names.split(',') {
+                let result = qemu.run_test(&format!("test_rs_{name}"), Duration::from_secs(180));
+                eprintln!("  [probe] {name}: exit {:?}", result.exit_code);
+                if result.exit_code != Some(0) {
+                    let died = result.exit_code.is_none();
+                    failed.push(format!(
+                        "{name}: exit {:?}\n{}\n--- stdout ---\n{}",
+                        result.exit_code,
+                        result.error.map(|e| e.to_string()).unwrap_or_default(),
+                        result.stdout,
+                    ));
+                    // No exit code is a machine that is gone: nothing after it runs.
+                    if died {
+                        break;
+                    }
+                }
+            }
+            if failed.is_empty() { Ok(()) } else { Err(failed.join("\n")) }
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }
probe-v1.patch — the probe as the hazard row ran it, which stops at the first member that does not exit 0
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 8fa631772..5eca9a086 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -170,6 +170,10 @@ const MACHINE_TESTS: &[&str] = &[
     // The nested-NMI report is a raw write to the 16550, which the T14 does not
     // have.
     "nested_nmi_is_loud",
+    // PROBE, not for landing: the shared-boot binaries `TOYOS_PROBE` names, on
+    // one QEMU boot of tests/testcases; `TOYOS_PROBE_DEBUG` picks the kernel
+    // that carries `SYS_DEBUG`.
+    "shared_probe",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2024,6 +2028,33 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        "shared_probe" => {
+            let names = std::env::var("TOYOS_PROBE").expect("TOYOS_PROBE names the binaries to run");
+            let kernel_features: &'static [&'static str] =
+                if std::env::var_os("TOYOS_PROBE_DEBUG").is_some() { ACTUATOR_KERNEL } else { &[] };
+            let rust_bins = qemu::build_toyos_bins(
+                &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/toyos-rust-tests"),
+            );
+            let mut qemu = QemuInstance::boot_with_options(
+                test_config,
+                &[],
+                &rust_bins,
+                BootOptions { kernel_features, ..Default::default() },
+            );
+            for name in names.split(',') {
+                let result = qemu.run_test(&format!("test_rs_{name}"), Duration::from_secs(180));
+                eprintln!("  [probe] {name}: exit {:?}", result.exit_code);
+                if result.exit_code != Some(0) {
+                    return Err(format!(
+                        "{name}: exit {:?}\n{}\n--- stdout ---\n{}",
+                        result.exit_code,
+                        result.error.map(|e| e.to_string()).unwrap_or_default(),
+                        result.stdout,
+                    ));
+                }
+            }
+            Ok(())
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }
probe-110.patch
diff --git a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs
index 5a63f02dd..95110d3fb 100644
--- a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs
+++ b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs
@@ -35,6 +35,8 @@ use toyos_abi::RawHandle;
 
 #[path = "../roster.rs"]
 mod roster;
+#[path = "../arch/mod.rs"]
+mod arch;
 
 const SELF_PATH: &str = "/system/bin/test_rs_process_lifecycle";
 
@@ -62,6 +64,15 @@ fn test() {
     a_kill_publishes_like_an_exit();
     a_handle_is_the_whole_of_the_right();
     an_undefined_wait_flag_bit_is_refused();
+    // PROBE, not for landing: 110 answers as a number nothing serves.
+    // SAFETY: an unassigned number, refused without reading an argument.
+    let answer = unsafe { arch::bare_syscall(110) };
+    assert_eq!(
+        SyscallError::from_u64(answer),
+        Some(SyscallError::InvalidArgument),
+        "syscall 110 answered {answer:#x}, not as an unassigned number"
+    );
+    println!("  [probe] syscall 110 is unassigned");
     println!("a process is a handle: the code is read, not claimed");
 }
 
mut-old-order.patch
diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
index d74b0930d..40cd17d49 100644
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -4,7 +4,7 @@
 
 use alloc::vec::Vec;
 
-use crate::object::{ops, HandleEntry, HandleTable, KObjectRef, Refusal};
+use crate::object::{HandleEntry, HandleTable, KObjectRef, Refusal};
 use crate::process::{
     process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE,
 };
@@ -53,7 +53,7 @@ pub struct PendingHandles {
 impl PendingHandles {
     /// Take the endowed handles out of the caller's table and put its handle to `own`, the child, in it, all under one lock hold: a refusal leaves the table unchanged.
     /// The child's table holds `own` under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
-    pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+    pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, KObjectRef), Refusal> {
         let Self { mut table, endow, mut labels } = self;
         let data_arc = process_data();
         let mut data = data_arc.lock();
@@ -103,8 +103,7 @@ impl PendingHandles {
             entries.push(entry);
         }
         // In this hold, before the child can run: its object's handle count never reaches zero while the spawn is in flight.
-        let held = ops::install(&mut data.handles, own.clone())
-            .expect("a caller's table with verified room refused its child");
+        let held = own.clone();
         drop(data);
         endow_self(&mut table, &mut entries, &mut labels, own);
         Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
index c54120404..4e0163efd 100644
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,12 @@ pub(super) fn sys_spawn(
 ) -> u64 {
     // Nothing to clean up: spawn's frame owns the child's resources on error.
     match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
-        Ok((_, handle)) => u64::from(handle.0),
+        Ok((_, object)) => {
+            match process::with_process_data(|data| crate::object::ops::install(&mut data.handles, object)) {
+                Ok(handle) => u64::from(handle.0),
+                Err(e) => e.to_u64(),
+            }
+        }
         Err(e) => e.refuse(),
     }
 }
mut-slot-not-taken.patch
diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
index d74b0930d..51bcc7ade 100644
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -53,7 +53,7 @@ pub struct PendingHandles {
 impl PendingHandles {
     /// Take the endowed handles out of the caller's table and put its handle to `own`, the child, in it, all under one lock hold: a refusal leaves the table unchanged.
     /// The child's table holds `own` under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
-    pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+    pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, HandleEntry), Refusal> {
         let Self { mut table, endow, mut labels } = self;
         let data_arc = process_data();
         let mut data = data_arc.lock();
@@ -87,7 +87,7 @@ impl PendingHandles {
             moving.push((EndowEntry { label_off, label_len, handle, _pad: 0 }, handle));
         }
         // Checked before any removal, so a failed install can't strand a handle out of a table that never spawned.
-        if !table.has_room(moving.len() + 1) || !data.handles.has_room(1) {
+        if !table.has_room(moving.len() + 1) {
             return Err(SyscallError::ResourceExhausted.into());
         }
 
@@ -103,8 +103,7 @@ impl PendingHandles {
             entries.push(entry);
         }
         // In this hold, before the child can run: its object's handle count never reaches zero while the spawn is in flight.
-        let held = ops::install(&mut data.handles, own.clone())
-            .expect("a caller's table with verified room refused its child");
+        let held = HandleEntry::new(own.clone(), ops::initial_rights(&own));
         drop(data);
         endow_self(&mut table, &mut entries, &mut labels, own);
         Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
index c54120404..648d2bf29 100644
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,10 @@ pub(super) fn sys_spawn(
 ) -> u64 {
     // Nothing to clean up: spawn's frame owns the child's resources on error.
     match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
-        Ok((_, handle)) => u64::from(handle.0),
+        Ok((_, entry)) => match process::with_process_data(|data| data.handles.install(entry)) {
+            Ok(handle) => u64::from(handle.0),
+            Err(crate::object::handle::TableFull) => SyscallError::ResourceExhausted.to_u64(),
+        },
         Err(e) => e.refuse(),
     }
 }

nc-whole.patch is git diff 35e533841 origin/main -- kernel with one hunk more, in kernel/src/syscall/dispatch.rs above retired_syscalls!:

+// CONTROL, not for landing: the ABI at this head no longer declares it.
+const SYS_PROCESS_OPEN: u64 = 110;
+

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Answers to the review at a8acd2a (#642 (comment)), at 35e5338.

BLOCKER

  • process_lifecycle.rs:98, the second raw-syscall entry: the pid arm and raw are deleted. git diff origin/main 35e533841 --numstat -- tests/toyos-rust-tests/src/bin/process_lifecycle.rs is 3 23, all of it the arm's removal; wait_raw is main's. The negative control re-run on the result is the last row of SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642 (comment): exit 1, process_lifecycle ends 139 at a call of 110 on main's kernel and exits 0 at this head.
  • tests/toyos.rs:3234, the record half never seen red: check_process_lifecycle, PROCESS_OPEN_RETIRED and the check_for row are deleted; git grep -n 'check_process_lifecycle\|PROCESS_OPEN' 35e533841 finds nothing.

NOTE

  • dispatch.rs:78, a retired number can be reused: 110 is not in the table, and the table is issues/design-debt/the-abi-still-keeps-retired-syscall-numbers.md's on main.
  • object/process.rs:87, the trailing blank line: gone.
  • process_lifecycle.rs:280, const PROCESS_OPEN: deleted with the arm.

REMOVE

  • the-capability-end-state-is-twelve-answers.md:33 and :107-110: both deleted.
  • process_lifecycle.rs:275-278 and :214: both deleted.
  • The pull request body: rewritten for this head; none of the five named lines is in it.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 evidence at 35e533841, run by the orchestrator: the six boots the round staged (filters spawn, handle, process), each one image flashed and booted once (cargo run --bin toyos-metal -- --image … --readback … --fat32-check from the worktree, clean at the head before and after). Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71).

filter / boot image sha256 armed toyos-metal boot tests ended exit=0 by name
spawn/shared 6d55f4de59ee23a6f04f9b0248204196d97a3f7cbd2a15f933d8bcc6deb1a84b boot-deadline=120000 EXIT=0, verdict passed 1152 ms 2 2 abuse_spawn_argv=0 spawn_image_object=0
spawn/shared-debug e950408bd9bd29eff453d46237a893325e77e4813187ffef6223d73270b43d3f boot-deadline=120000 EXIT=0, verdict passed 1152 ms 2 2 spawn_child_ends_first=0 spawn_lands_claimed=0
handle/shared 70672353b04ff7fa5e45229cba66ac2581e0f698308887f15a3bdd42896335a4 boot-deadline=120000 EXIT=0, verdict passed 1152 ms 1 1 abuse_handle_table=0
handle/shared-debug a73d68406b3c64180062e976384e2264506ce22c511486935b588c84b693409b boot-deadline=120000 EXIT=0, verdict passed 1185 ms 1 1 handle_lifetime=0
process/proctreecase a18c5a34fc6c6130992a2b6b291e20929e886d58d04072711409840957345ed9 boot-deadline=120000 EXIT=0, verdict passed 1158 ms 1 1 process_tree=0
process/shared b7d7759a101234dc18b6755e52f541ffdc7ff872f82d32bc3fd9d0f92a350a47 boot-deadline=120000 EXIT=0, verdict passed 1156 ms 2 2 process_lifecycle=0 std_process=0
  • 9 tests ended across the six boots and 9 exit 0.
  • toyos-fat32-check on each boot: 6 of 6 logs say the log partition's bytes check out.
  • These are the head's green arms; no mutation was booted on the machine, and the harness's judging over these readbacks has not been run.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #642, round 2, at 35e5338

Earlier BLOCKERs

  • process_lifecycle.rs:98, the second raw-syscall entry — CLOSED. git diff --numstat origin/main 35e533841 -- tests/toyos-rust-tests/src/bin/process_lifecycle.rs is 3 23, the arm and raw gone; the core::arch::asm! left at :98 is main's wait_raw. The control re-run on the result: nc-whole-shipping exit 1 with process_lifecycle 139 at the probe's call of 110, green-shipping exit 0 (I read both logs).
  • tests/toyos.rs:3234, the record half never seen red — CLOSED. git grep 'check_process_lifecycle\|PROCESS_OPEN' 35e533841 -- tests finds nothing.

What this verdict rests on. I read, and ran nothing: host.log (created 4 s after the head's commit, Host: 65 step(s), all green, exit 0), suite.log (21 of 21, exit 0), every probe log and its .exit, and the six T14 readbacks behind issuecomment-5957623626, where I found the nine TEST_END … exit=0 lines by name. Those boots are the head's green arms alone and the rows' own judges have not been run over them.

Size. +359 −393 in 27 files: production (kernel, toyos-abi, toyos, src) +135 −261, the model and tests +215 −66, issues +9 −66.

The probe. Sound for what it measured. It applies to this head's tests/toyos.rs, boots the same tests/testcases image through the harness's own run_test, judges each member on its exit code as the shared block does, and every row's applied diff, exit and restored tree are on record. The mutated kernels were really booted: each red is a different failure in the place its patch predicts. Its limits: two CPUs under QEMU, serial members, and nothing in it runs two threads of one spawner, which is where the BLOCKER below is.

BLOCKER

  • kernel/src/loader/start.rs:107-110 — the caller's handle to the child is installed in the caller's live table at :107, the table's lock is released at :109, and the child's own self is minted only at :110 (endow_self, :119). Between the two the object's count is one, and that one handle is in a table every other thread of the caller can reach. LockGuard::drop ends in preempt::enable() (kernel/src/sync.rs:205), which reschedules when a tick is owed, so :109 is a preemption point and the gap is as long as a timeslice. A sibling thread that closes that handle, or replaces its slot (sys_dup2 takes a slot, not a handle), takes the count to zero and sets retired; endow_self then panics at object/handle.rs:108, or, if its load of retired came first, the child's table closing later panics at handle.rs:139. This is the same zero crossing the round set out to remove, moved from the child's side to the caller's, and main does not have it: there Process was reopenable and the child's self was minted first. The claim at :106 and in the body, that the count never reaches zero while the spawn is in flight, is false at this head, and nothing measured can fail on it: the model mints both handles in one atomic section (toyos-proclife/src/interleave.rs:256-258) and has no close but a teardown's, and no guest arm runs a second thread of the spawner. Send back to measure, as the first race was: this head's order shown red, then the child's self minted before any table userland can reach holds a handle to the object. The cheapest tier that reaches it is the model: the spawn's section cut where the kernel drops the caller's lock, and a close of the spawner's handle by another thread of by as a step. The mutation that must then turn L13 red is this head's order, the caller's mint ahead of the child's own.

NOTE

  • PR body, "T14" — it still says the six boots are staged and not run. The exits are in an orchestrator comment; the body is main's record and carries them, with the rows' judges run over the readbacks. The next head moves commit, so it needs the same six boots again.
  • kernel/src/process.rs:1700 — a SYS_PROCESS_KILL on the handle between the commit and the landing claims nothing (teardown::claim_teardown answers false for a pid not in the table) and answers Ok; the child then lands and runs. "Ok for an already-gone process: the caller asked for it to be dead and it is" does not cover a process not yet there. The body's "Unsure" is not a record: file it in issues/kernel/ with its owner, this evidence and an exit, or remove it.
  • kernel/src/loader/mod.rs:351 — spawn is generic over the closure as well as over H, so two whole copies of a 330-line function ship, one of them run once per boot. The body says the size was not measured: measure the kernel's size at this head against origin/main and state it, or cut spawn at its point of no return into a build and a landing, so that neither caller needs a closure or a second copy.
  • tests/toyos.rs:696 — the selftests image has one arm fewer and was neither built nor booted at this head. The name lists are held on the host (src/metal.rs:2735); what is not measured is that the image still builds: stage one of its rows with --metal-readback at the next head.

The five left to the orchestrator

  1. No committed test pins 110. Needs nothing, not a BLOCKER: root CLAUDE.md says a removed number is free, rustc refuses any caller, a reader sees the arm gone, and a pin would red the day 110 is assigned. The probe arm is the measurement: green here, 139 on main's kernel.
  2. The handle resolves before its spawn returns. Handle numbers are deterministic, so "a caller guessing" understates it. The gap before the child's self exists is the BLOCKER above. After that, a close, a dup, a transfer, a wait and a stats read are sound; the kill is the second NOTE. A reserved slot would close both and is not required to land.
  3. The refusal at the cap. Not a BLOCKER, and it needs a ruling rather than code: it is a refusal by name with the table unchanged, and toyos_abi::syscall::spawn states it. But it is a spawn main served: nc-whole-shipping shows abuse_handle_table's new arm answering Ok(RawHandle(8191)) on main's kernel, so that arm pins the over-refusal and not the endowment's safety. If main's answer is to stay, the room after the removals is HandleTable's to answer beside has_room, since a slot at its last generation gives nothing back, and the arm flips.
  4. The generic loader::spawn: the third NOTE.
  5. The selftests image: the fourth NOTE.

REMOVE

  • kernel/src/loader/start.rs:106 — "its object's handle count never reaches zero while the spawn is in flight": false at this head.
  • kernel/src/process.rs:754 — "None only in the window between a live process and its published exit": this change adds a second window, a handle that resolves before its process lands.
  • PR body, "Unsure", first bullet — "Gone for a spawn under it": the spawner's handle carries no WRITE, so spawn_place answers PermissionDenied.
  • PR body — "The count on a child's object never reaches zero while its spawn is in flight, so every row retires when its last handle goes."

SEND BACK

Japabu and others added 2 commits October 2, 2026 19:48
… and a sibling's close is a step

`PendingHandles::commit` installs the caller's handle to the child in the
caller's live table, drops that table's lock, and only then mints the child's
own `self`. The model minted both in the section that lands the child and had
no close but a teardown's, so it could not see a thread of the spawner closing
that handle in the gap.

The spawn's section is now two: the commit under the caller's lock, and the
child's own handle with the landing once that lock is given up, in the order
the kernel has them at this commit. `Op::Close` is `sys_close` by another
thread of the spawner on the handle the spawn will answer.

This commit is red, and is the measurement: `cargo test -p toyos-proclife`
exits 101 on `a_sibling_closing_a_spawns_handle_before_the_spawn_returns`,

    pid 2: a handle to it was minted after its last one had gone
      schedule: spawn_under#0 -> spawn_under#0 -> close#1 -> spawn_under#0

which is `HandleEntry::new`'s assert in `endow_self`. The next commit changes
the kernel's order and the model's with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…takes it

At 35e5338 `PendingHandles::commit` installed the caller's handle to the
child in the caller's live table, released that table's lock, and minted the
child's `self` after it. In between the object had one handle, in a table every
other thread of the caller reaches, and `LockGuard::drop` is a preemption
point: a close or a `dup2` over it there retired the object, and `endow_self`
then panicked in `HandleEntry::new`. 40d7842 shows it in the model.

`loader::spawn` now mints the child's `self` as it makes the object
(`start::own_handle`) and `commit` takes that `HandleEntry`, not the object.
The caller's handle is minted from the entry's object while the entry is held,
and the entry then sits in the child's table, which no thread reaches until
the child lands. `commit` has no object to mint from but the one inside a
handle it holds, so the order that crossed zero is not one it can be written
in.

The model's commit section mints both, the child's own first.
`mutate-spawner-handle-before-the-childs-own` restores 35e5338's order and
`src/ci.rs`'s `CONTROLS` runs it:
`a_sibling_closing_a_spawns_handle_before_the_spawn_returns` fails on L13.

`stats_of`'s doc loses the sentence that named one window in which it answers
`None`: a handle that resolves before its process lands is a second.
`toyos_abi::syscall::spawn`'s doc says the room for the answer is counted
before, and regardless of, what the endowments free. A kill on the handle
before the child lands still claims nothing; that is filed as
issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md.

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

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Runs at c837976 (implementer; QEMU on this host, 14 cores, nothing else of this worktree running). Logs: 642-round/r3/ in the orchestrator's scratchpad.

The model, on the host. cargo test -p toyos-proclife, the command's own exit code:

tree features exit what went red
40d7842: the model cut at the caller's lock, a sibling's close a step, the kernel's order as 35e5338 has it none 101 a_sibling_closing_a_spawns_handle_before_the_spawn_returns, L13
c837976 none 0 48 passed
c837976 mutate-spawner-handle-before-the-childs-own 101 the same test, the same schedule
c837976 mutate-spawner-handle-after-the-landing 101 a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it, a_spawn_racing_the_kill_of_its_own_spawner, and the new test

At 40d7842 and under the first feature alike:

pid 2: a handle to it was minted after its last one had gone
  schedule: spawn_under#0 -> spawn_under#0 -> close#1 -> spawn_under#0

The probe, under QEMU. Each row is one run of the probe of #642 (comment) (probe.patch and probe-110.patch unchanged; both still apply): every patch applied as a checked patch (git apply --check, then git apply) on a clean tree at c837976, TOYOS_PROBE=<members> [TOYOS_PROBE_DEBUG=1] cargo test --test toyos-build -- shared_probe, the command's own exit code, and the tree restored (git status --porcelain --ignore-submodules=none empty after each).

run kernel patches beside the probe members exit
green shipping probe-110.patch the 78 shipping members 0
green test none the 5 SYS_DEBUG members 0
old order: the spawner's handle installed once loader::spawn returns test mut-old-order.patch spawn_child_ends_first 1
slot not taken test mut-slot-not-taken.patch spawn_child_ends_first 1
slot not taken shipping mut-slot-not-taken.patch abuse_handle_table 1
35e5338's order: the caller's handle installed, its lock released, the child's own minted after test mut-head-order.patch the 5 SYS_DEBUG members 0
the same shipping probe-110.patch, mut-head-order.patch the 78 shipping members 0
kernel/ as on origin/main shipping probe-110.patch, nc-whole.patch the 78 shipping members 1

The 5 SYS_DEBUG members: spawn_child_ends_first spawn_lands_claimed handle_lifetime shm_release_reclaims abuse_kernel_addr. The 78 shipping members: abuse_connect_flood abuse_cwd_growth abuse_elf_loader abuse_elf_segments abuse_gpu_resolution abuse_handle_table abuse_inbox abuse_listener_hijack abuse_page_straddle abuse_pipe_map abuse_pipe_owner abuse_pipe_ring abuse_readonly_copyout abuse_shared_grant abuse_shm_length abuse_spawn_argv abuse_thread_name abuse_tls_alloc allocator_stress blocking_read_stress c_aligned_alloc connect_before_serve cpal_drop_unreleased debug_trap demand_paging_sse demand_window_race disk_backtrace dlopen_dedup empty_dir_stat endowment_denied exit_wait_storm fault_gates file_mtime fs_cache_eviction fs_escape fs_large_file fs_stream_offset fs_truncate_persist fs_turns futex_wake_counts hierarchy_paths home_backing_revoked inbox_cancel_wakes kill_ends_every_wait lseek_past_eof memmap_exec_refused mmap_prot mmap_stress munmap_reissues_read_window munmap_reissues_second_read_window mutual_kill netd_gone_mid_bind nvme_home_roundtrip pipe_flag_forgery poll_wake_pipe poller_capacity process_lifecycle query_modules_size sched_stress spawn_image_object std_alloc std_fs std_fs_write std_io std_mmap std_process std_sync std_threading std_tls std_tls_cranelift std_tls_dlopen std_tls_multi_crate std_unwind std_unwind_so toybox_file_tools user_copy_spans_windows wall_clock_now window_refusal.

mut-head-order.patch stays green on both kernels: no guest arm runs a second thread of a spawner inside the commit, so no booted member can fail on the kernel's order. The red for that order is the model's, above.

Old order, m1-old-order.log:

  [probe] spawn_child_ends_first: exit None
FAIL shared_probe: spawn_child_ends_first: exit None
kernel panic: PANIC: panicked at src/object/handle.rs:108:9: — the guest went quiet because every CPU is halted, not because it was still working. The panic is the finding and the guard never got to be one.
--- what the kernel said as it died ---
[kernel 0.513 cpu1] PANIC: panicked at src/object/handle.rs:108:9:
a handle to a retired Process (koid 162)
[kernel 0.513 cpu1]   Backtrace:
[kernel 0.514 cpu1]     0xffff80007c5f816c  core::panicking::panic_fmt+0x2c
[kernel 0.514 cpu1]     0xffff80007c547960  kernel::object::ops::install+0x100
[kernel 0.515 cpu1]     0xffff80007c593b67  kernel::process::with_process_data::<core::result::Result<toyos_abi::handle::RawHandle, toyos_abi::syscall::SyscallError>, kernel::syscall::ipc::sys_accept::{closure#1}>+0x47
[kernel 0.515 cpu1]     0xffff80007c54548b  kernel::syscall::proc::sys_spawn+0x7b
[kernel 0.515 cpu1]     0xffff80007c531583  kernel::syscall::dispatch::syscall_dispatch::{closure#1}+0x2863
[kernel 0.515 cpu1]     0xffff80007c5319c4  kernel::syscall::dispatch::syscall_dispatch+0x104
[kernel 0.515 cpu1]     0xffff80007c4b98f3  kernel::arch::x86_64::syscall::syscall_handler+0x33
[kernel 0.515 cpu1]     0xffff80007c4b964c  kernel::arch::x86_64::syscall::syscall_entry+0x70
[kernel 0.515 cpu1]   Contexts: cpu1 crashed at sp=0xffff800006db66d8, asking about ctx 0xffff8000002ac940
[kernel 0.515 cpu1]   cpu0 is on ctx 0xffff800000200fb0 (its idle context) stack_top=0x0000000000000000 saved_sp=0xffff800000420ea0
[kernel 0.515 cpu1]   cpu1 is on ctx 0xffff8000002ac940 pid=9 tid=0 stack_top=0xffff800006db7000 saved_sp=0xffff800006db5130
[kernel 0.515 cpu1]   Running: pid=9 tid=Some(Tid(0))
[kernel 0.515 cpu1]   Process: test_rs_spawn_child_ends_fi pid=9 state=Live
[kernel 0.516 cpu1] panic: rebooting in 60 s unless a key is pressed, timed by the calibrated clock

Slot not taken, test kernel, m2-slot-debug.log:

  [probe] spawn_child_ends_first: exit Some(101)
thread 'main' (1) panicked at src/bin/spawn_child_ends_first.rs:71:5:
assertion `left == right` failed: the refused spawn's child started and spoke
  left: Ok(1)
 right: Ok(0)

Slot not taken, shipping kernel, m2-slot-shipping.log:

  [probe] abuse_handle_table: exit Some(139)
thread 'main' (1) panicked at src/bin/abuse_handle_table.rs:170:5:
assertion `left == right` failed: a spawn from a full table was not refused
  left: Ok(RawHandle(8191))
 right: Err(ResourceExhausted)

kernel/ as on origin/main, nc-whole-shipping.log: 76 of 78 exit 0; abuse_handle_table ends 139 on the same assertion as above, and process_lifecycle prints every arm and ends 139 at the probe arm's call of 110.

The kernel's size. The shipping x86-64 kernel cargo run -- --build-only stages (target/kernel-x86_64-4be7ccf4859a9aa6), read with the toolchain's llvm-size -A and llvm-nm -S --size-sort -C; the origin/main build is this worktree with git diff --binary c8379761c origin/main applied and then reversed, clean after.

file .text .rodata .data .bss every section
origin/main (a31eec5) 3134088 1372619 266208 552848 1720033 4115614
c837976 3148648 1383691 267224 552800 1720081 4127902
difference +14560 +11072 +1016 -48 +48 +12288

loader::spawn is one symbol of 15396 bytes on origin/main and two at this head: spawn::<RawHandle, sys_spawn::{closure#0}> 11263 and spawn::<(), spawn_init::{closure#0}> 11398. The aarch64 kernel was not measured.

T14, the previous head's boots judged. cargo test --test toyos-build -- --metal --metal-readback <dir> <filter> at 35e5338 over the readbacks the orchestrator's six boots left (#642 (comment)), tree clean before: spawn exit 0 (4 passed, 0 failed, 2 boot(s)), handle exit 0 (2 passed, 0 failed, 2 boot(s)), process exit 0 (PASS process_tree, 3 passed, 0 failed, 2 boot(s)). The spawn run wrote three boot.shared.* numbers into tests/metal/lenovo-20w0003amz.toml; that file is outside this round's brief and was restored, the diff kept as judge-old.record-offered.diff.

T14, staged at c837976 and not run: cargo test --test toyos-build -- --metal --metal-readback <dir> <filter> for spawn, handle, process and pci_capability_walk (the selftests image, which builds with its ten arms), exit 2 each. Image sha256:

0baa985b23578b82bc8f8bf07dccd17401975f560a895681b40d2630692c66b5  handle/shared-debug/image.img
0b5a8c89e3d74d3bcd06932a415ca17f0bb16663be5e9ea3583cbac34faa05cd  handle/shared/image.img
d7b73c13e6736fda7ea01a1bcb96a443abccb50fc9d6597f7dd68fa2fad1c90c  process/proctreecase/image.img
75eca94fff32d454f6c3384708c9490c502d0a6c230c5d2eea991e1add19e965  process/shared/image.img
e6289effac6efd34db4212a7a94108071baccc913387f04876c04fe2ae24036f  selftests/selftests/image.img
2eb796a5994c42f0cb518dad181f5302a6a72d1547be66c3e3361f1c023f4378  spawn/shared-debug/image.img
fc71752979a3016e43e9827aa2582606dcce28ea4fdf000594f41a80ca178944  spawn/shared/image.img
mut-old-order.patch
diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -7,7 +7,7 @@
 use alloc::vec::Vec;
 
 use crate::object::process::ProcessObject;
-use crate::object::{ops, HandleEntry, HandleTable, KObjectRef, Refusal};
+use crate::object::{HandleEntry, HandleTable, KObjectRef, Refusal};
 use crate::process::{
     process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE,
 };
@@ -56,7 +56,7 @@
 impl PendingHandles {
     /// Take the endowed handles out of the caller's table and put its handle to the child in it, all under one lock hold: a refusal leaves the table unchanged.
     /// `own` is the child's handle to itself, which its table holds under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
-    pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+    pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, KObjectRef), Refusal> {
         let Self { mut table, endow, mut labels } = self;
         let data_arc = process_data();
         let mut data = data_arc.lock();
@@ -106,8 +106,7 @@
             entries.push(entry);
         }
         // Minted while `own` is held, and `own` goes into a table no thread reaches until the child lands: another thread of the caller closing this handle never closes the object's last.
-        let held = ops::install(&mut data.handles, own.object().clone())
-            .expect("a caller's table with verified room refused its child");
+        let held = own.object().clone();
         drop(data);
         endow_self(&mut table, &mut entries, &mut labels, own);
         Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,12 @@
 ) -> u64 {
     // Nothing to clean up: spawn's frame owns the child's resources on error.
     match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
-        Ok((_, handle)) => u64::from(handle.0),
+        Ok((_, object)) => {
+            match process::with_process_data(|data| crate::object::ops::install(&mut data.handles, object)) {
+                Ok(handle) => u64::from(handle.0),
+                Err(e) => e.to_u64(),
+            }
+        }
         Err(e) => e.refuse(),
     }
 }
mut-slot-not-taken.patch
diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -56,7 +56,7 @@
 impl PendingHandles {
     /// Take the endowed handles out of the caller's table and put its handle to the child in it, all under one lock hold: a refusal leaves the table unchanged.
     /// `own` is the child's handle to itself, which its table holds under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
-    pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+    pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, HandleEntry), Refusal> {
         let Self { mut table, endow, mut labels } = self;
         let data_arc = process_data();
         let mut data = data_arc.lock();
@@ -90,7 +90,7 @@
             moving.push((EndowEntry { label_off, label_len, handle, _pad: 0 }, handle));
         }
         // Checked before any removal, so a failed install can't strand a handle out of a table that never spawned.
-        if !table.has_room(moving.len() + 1) || !data.handles.has_room(1) {
+        if !table.has_room(moving.len() + 1) {
             return Err(SyscallError::ResourceExhausted.into());
         }
 
@@ -106,8 +106,7 @@
             entries.push(entry);
         }
         // Minted while `own` is held, and `own` goes into a table no thread reaches until the child lands: another thread of the caller closing this handle never closes the object's last.
-        let held = ops::install(&mut data.handles, own.object().clone())
-            .expect("a caller's table with verified room refused its child");
+        let held = HandleEntry::new(own.object().clone(), ops::initial_rights(own.object()));
         drop(data);
         endow_self(&mut table, &mut entries, &mut labels, own);
         Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,10 @@
 ) -> u64 {
     // Nothing to clean up: spawn's frame owns the child's resources on error.
     match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
-        Ok((_, handle)) => u64::from(handle.0),
+        Ok((_, entry)) => match process::with_process_data(|data| data.handles.install(entry)) {
+            Ok(handle) => u64::from(handle.0),
+            Err(crate::object::handle::TableFull) => SyscallError::ResourceExhausted.to_u64(),
+        },
         Err(e) => e.refuse(),
     }
 }
mut-head-order.patch — the kernel half of c837976 reverted
diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs
--- a/kernel/src/loader/mod.rs
+++ b/kernel/src/loader/mod.rs
@@ -337,8 +337,8 @@
 
 /// Load a program and place its main thread under `parent`, answering its pid
 /// and what `commit` left its caller holding of it. `commit` builds the
-/// child's handle table around the child's handle to itself, once nothing is
-/// left to refuse.
+/// child's handle table around the child's own object, once nothing is left
+/// to refuse.
 ///
 /// `image` is the program's bytes when the caller read them itself, and then
 /// `argv[0]` is only its name: nothing opens it, and its libraries come from
@@ -350,7 +350,7 @@
 /// unwinds, so the error must travel out as a value rather than strand it.
 pub fn spawn<H>(
     argv: &[&str],
-    commit: impl FnOnce(crate::object::HandleEntry) -> Result<(HandleTable, Endowments, H), crate::object::Refusal>,
+    commit: impl FnOnce(KObjectRef) -> Result<(HandleTable, Endowments, H), crate::object::Refusal>,
     cwd: String,
     env: Vec<u8>,
     image: Option<Arc<dyn crate::file_backing::FileBacking>>,
@@ -592,7 +592,7 @@
     let object = crate::object::process::ProcessObject::new(pid);
     // The point of no return: every failure above answers the caller with its
     // table untouched.
-    let (handles, endowments, held) = commit(start::own_handle(&object))?;
+    let (handles, endowments, held) = commit(KObjectRef::Process(Arc::clone(&object)))?;
     let proc_data = Arc::new(Lock::new(ProcessData {
         handles,
         cwd,
diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -3,10 +3,8 @@
 //! stack starts from and the trampolines it returns into are the
 //! architecture's (`arch::entry`).
 
-use alloc::sync::Arc;
 use alloc::vec::Vec;
 
-use crate::object::process::ProcessObject;
 use crate::object::{ops, HandleEntry, HandleTable, KObjectRef, Refusal};
 use crate::process::{
     process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE,
@@ -54,9 +52,9 @@
 }
 
 impl PendingHandles {
-    /// Take the endowed handles out of the caller's table and put its handle to the child in it, all under one lock hold: a refusal leaves the table unchanged.
-    /// `own` is the child's handle to itself, which its table holds under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
-    pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+    /// Take the endowed handles out of the caller's table and put its handle to `own`, the child, in it, all under one lock hold: a refusal leaves the table unchanged.
+    /// The child's table holds `own` under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
+    pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
         let Self { mut table, endow, mut labels } = self;
         let data_arc = process_data();
         let mut data = data_arc.lock();
@@ -105,8 +103,8 @@
                 .expect("a child table with verified room refused an endowment");
             entries.push(entry);
         }
-        // Minted while `own` is held, and `own` goes into a table no thread reaches until the child lands: another thread of the caller closing this handle never closes the object's last.
-        let held = ops::install(&mut data.handles, own.object().clone())
+        // In this hold, before the child can run: its object's handle count never reaches zero while the spawn is in flight.
+        let held = ops::install(&mut data.handles, own.clone())
             .expect("a caller's table with verified room refused its child");
         drop(data);
         endow_self(&mut table, &mut entries, &mut labels, own);
@@ -114,16 +112,11 @@
     }
 }
 
-/// A new process's handle to itself, the first its object has: `WRITE` to be named a spawn's place, `DUP` and `TRANSFER` to hand that on.
-pub(super) fn own_handle(object: &Arc<ProcessObject>) -> HandleEntry {
+/// Install `own` in its own table under [`SELF_LABEL`]: `WRITE` to be named a spawn's place, `DUP` and `TRANSFER` to hand that on. Its caller verified the room.
+pub(super) fn endow_self(table: &mut HandleTable, entries: &mut Vec<EndowEntry>, labels: &mut Vec<u8>, own: KObjectRef) {
     let rights = Rights::WRITE.union(Rights::DUP).union(Rights::TRANSFER);
-    HandleEntry::new(KObjectRef::Process(Arc::clone(object)), rights)
-}
-
-/// Install `own` in its own table under [`SELF_LABEL`]. Its caller verified the room.
-pub(super) fn endow_self(table: &mut HandleTable, entries: &mut Vec<EndowEntry>, labels: &mut Vec<u8>, own: HandleEntry) {
     let handle = table
-        .install(own)
+        .install(HandleEntry::new(own, rights))
         .expect("a child table with verified room refused its own handle");
     entries.push(EndowEntry {
         label_off: labels.len() as u32,

nc-whole.patch is git diff c8379761c origin/main -- kernel with one hunk more, in kernel/src/syscall/dispatch.rs above retired_syscalls!:

+// CONTROL, not for landing: the ABI at this head no longer declares it.
+const SYS_PROCESS_OPEN: u64 = 110;
+

@Japabu Japabu changed the title SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn takes its handle to the child before the child lands SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's Oct 2, 2026
@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Answers to the review at 35e5338 (#642 (comment)), at c837976. Measurements: #642 (comment).

BLOCKER

  • kernel/src/loader/start.rs:107-110, the caller's handle the object's only one while its lock is released: measured first, then fixed. 40d7842 cuts the model's spawn where the kernel gives up the caller's lock and adds Op::Close, a sys_close by another thread of the spawner, with the kernel's order as it was: cargo test -p toyos-proclife exits 101 on L13, schedule spawn_under#0 -> spawn_under#0 -> close#1 -> spawn_under#0. c837976 has loader::spawn mint the child's self with the object and hand commit that HandleEntry; the caller's handle is minted from the entry's object while the entry is held, and the entry then sits in the child's table until the landing. The model exits 0, and 101 again under mutate-spawner-handle-before-the-childs-own, which CONTROLS runs. The body lists every point from the object's creation to the spawn's return at which a handle exists and who reaches it. The kernel half reverted (mut-head-order.patch) stays green in every booted member, as the review said of the probe's reach; the body says so under "Unsure".

NOTE

  • PR body, "T14": it carries the six boots' exits as 35e5338's, with the rows' judges run over the readbacks (exit 0 for spawn, handle and process), and names the seven owed at c837976 with each image's sha256.
  • kernel/src/process.rs:1700, a kill before the landing claims nothing: filed as issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md. The fix does not remove the window: the commit still installs the caller's handle before the landing (start.rs:109).
  • kernel/src/loader/mod.rs:351, two copies of spawn: measured. .text is 1383691 bytes at this head against 1372619 on origin/main, +11072; the two copies are 11263 and 11398 bytes against one of 15396. The split was counted, 34 locals read after the commit, and not built; the generic form stays.
  • tests/toyos.rs:696, the selftests image: pci_capability_walk staged with --metal-readback at c837976, exit 2; the image builds with its ten arms.

The five

  1. No pin for 110: nothing done.
  2. The handle resolves before its spawn returns: the gap before the child's self is the BLOCKER, closed; the kill is filed.
  3. The refusal at the cap: the orchestrator ruled it stays. The body says it is a behaviour change and names abuse_handle_table's arm as what pins it; toyos_abi::syscall::spawn's doc now says the room is counted whatever the endowments would have freed.
  4. and 5.: the third and fourth NOTEs.

REMOVE

  • kernel/src/loader/start.rs:106: deleted.
  • kernel/src/process.rs:754: deleted.
  • PR body, "Unsure", first bullet, and "The count on a child's object never reaches zero while its spawn is in flight…": both deleted.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 evidence at c8379761c, run by the orchestrator: the seven boots the round staged, each image's sha256 checked against the request before it was flashed, each flashed and booted once (cargo run --bin toyos-metal -- --image … --readback … --fat32-check from the worktree, clean at the head before and after). Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71).

filter / boot image sha256 armed loop exit, verdict boot tests ended exit=0 by name
spawn/shared fc71752979a3016e43e9827aa2582606dcce28ea4fdf000594f41a80ca178944 boot-deadline=120000 EXIT=0, passed 1152 ms 2 2 abuse_spawn_argv=0 spawn_image_object=0
spawn/shared-debug 2eb796a5994c42f0cb518dad181f5302a6a72d1547be66c3e3361f1c023f4378 boot-deadline=120000 EXIT=0, passed 1153 ms 2 2 spawn_child_ends_first=0 spawn_lands_claimed=0
handle/shared 0b5a8c89e3d74d3bcd06932a415ca17f0bb16663be5e9ea3583cbac34faa05cd boot-deadline=120000 EXIT=0, passed 1152 ms 1 1 abuse_handle_table=0
handle/shared-debug 0baa985b23578b82bc8f8bf07dccd17401975f560a895681b40d2630692c66b5 boot-deadline=120000 EXIT=0, passed 1153 ms 1 1 handle_lifetime=0
process/proctreecase d7b73c13e6736fda7ea01a1bcb96a443abccb50fc9d6597f7dd68fa2fad1c90c boot-deadline=120000 EXIT=0, passed 1153 ms 1 1 process_tree=0
process/shared 75eca94fff32d454f6c3384708c9490c502d0a6c230c5d2eea991e1add19e965 boot-deadline=120000 EXIT=0, passed 1155 ms 2 2 process_lifecycle=0 std_process=0
selftests/selftests e6289effac6efd34db4212a7a94108071baccc913387f04876c04fe2ae24036f pci-cap-selftest, revoked-backing-selftest, leak-rollback-selftest, lapic-spurious-selftest, unclaimed-vector-selftest, xhci-xecp-selftest, xhci-descriptor-selftest, sysret-ss-probe, test-input-merge, sched-operation-nesting, boot-deadline=120000 EXIT=0, passed 1154 ms 0 0
  • 9 tests ended across the seven boots and 9 exit 0.
  • toyos-fat32-check: 7 of 7 logs say the log partition's bytes check out.
  • These are the head's green arms; no mutation was booted on the machine, and the harness's judges have not been run over these readbacks.
  • The selftests image runs no test-runner job; it booted with its ten actuators armed and its log carries the selftest lines, among them kernel.log:97 virtio: pci cap selftest 15/15, the line of the staged row pci_capability_walk. The image builds and boots with one arm fewer.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #642, round 3, at c837976

Earlier BLOCKER

  • kernel/src/loader/start.rs:107-110, the caller's handle installed and its lock released before the child's self was minted — CLOSED. The measurement: cargo test -p toyos-proclife exits 101 at 40d7842 on a_sibling_closing_a_spawns_handle_before_the_spawn_returns ("pid 2: a handle to it was minted after its last one had gone", spawn_under#0 -> spawn_under#0 -> close#1 -> spawn_under#0), exits 0 at c837976 (48 passed), and the same test reds again under mutate-spawner-handle-before-the-childs-own inside this head's --ci host (host.log:6630-6655, one verdict reached). In the kernel, own is a HandleEntry minted at loader/mod.rs:595 before commit takes the caller's lock, and the caller's handle is minted from own.object() at start.rs:109.

The six points, each read against the code

  1. loader/mod.rs:592, :595 — holds. own_handle is the argument of the commit call, so the handle exists before process_data() is taken, and nothing sits between the two lines.
  2. start.rs:72-94 — holds. Every refusal returns with own still the parameter: it drops after the guard, Process is an immediate row so the drop runs no hook, and no table ever held it.
  3. start.rs:109-111 — holds. From drop(data) a sibling's close, a dup2 over the slot (install_at displaces the entry), a duplicate, a transfer, and a second spawn naming the handle in its slot map or its endowments all leave own.
  4. start.rs:112, loader/mod.rs:596-627 — holds. proc_data is a local until :650.
  5. loader/mod.rs:645-668 — holds. git grep 'KObjectRef::Process(' c8379761c finds one construction, start.rs:120, and patterns. ops::install is handed a Process at start.rs:109 alone, inside own's life; every other caller of it, of install_all and of HandleEntry::new builds an object of another kind where it stands; duplicate borrows a live entry.
  6. loader/mod.rs:685, syscall/proc.rs:56 — holds.

Reachable in the window and not on the walk, none of them a gap: a wait parks or answers WouldBlock (syscall/proc.rs:76-95), a stats read answers NotFound (process.rs:765), spawn_place refuses the handle for want of WRITE, and the kill is the filed issue.

Earlier NOTEs

  • PR body, "T14" — OPEN, as the first NOTE below.
  • kernel/src/process.rs:1700, the kill before the landing — CLOSED. issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md carries the frontmatter issues/README.md asks for, an owner, the evidence and an exit a model schedule can fail; claim_teardown answering false for a pid not in the table is toyos-proclife/src/teardown.rs:30.
  • kernel/src/loader/mod.rs:351, two copies of spawn — CLOSED by measurement: .text 1383691 against 1372619, and spawn 15396 bytes on origin/main against 11263 and 11398 here (nm-main.txt:6078, nm-head.txt:6095-6096).
  • tests/toyos.rs:696, the selftests image — CLOSED: staged exit 2, then booted; kernel.log:4 names its ten actuators.

Earlier REMOVEs — all four are gone from the tree and the body.

What this verdict rests on. I read, and ran nothing. Logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/642-round/r3/.

  • gates.head is c837976 with the tree clean before and after. host.log ends [ci] Host: 66 step(s), all green, exit 0; --build-only exit 0; suite.log 21 passed of 21, exit 0, and both AArch64 kernels were rebuilt under it at this head.
  • The eight probe rows, each with its applied diff and its exit: 78 of 78 and 5 of 5 green; mut-old-order red in the kernel's assert at object/handle.rs:108; mut-slot-not-taken red on both kernels; mut-head-order green on both; nc-whole red on 2 of 78.
  • The seven T14 boots at c837976 (SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642 (comment)). In the readbacks I found the seven passed verdicts and the nine TEST_END … exit=0 lines by name, and in spawn/shared-debug the line exit: … pid=10 code=7 ahead of spawn: … pid=10: the hold held on the machine. They are green arms only, and the rows' judges have not been run over them. The lines process_tree and pci_cap_selftest look for are there: two no_such_program refusals, one refusal at depth 65, pci cap split 13/13, pci cap selftest 15/15, PCI: Enumeration complete.
  • main moved to b331934 and GitHub reads the branch mergeable. Its new rule, nothing ships for tests alone, is kept: action 23 compiles only under test-actuators (process.rs:1735-1762, syscall/dispatch.rs:552).

Size. +473 −400 in 28 files: production (kernel, toyos-abi, toyos, src) +149 −264, the model and tests +286 −70, issues +38 −66. Production source shrinks by 115 lines; the shipping x86-64 .text grows by 11072 bytes.

The two judgments the brief asks for

  • Is a count enough to refuse the split? Yes. I counted the same 34 locals off loader/mod.rs:596-685. That is an upper bound on what a split carries: twelve of them fold into an ElfInfo and three into a ThreadData built before the commit, and seven more serve only the one log line. The lower bound is still a dozen fields in a struct declared, built and taken apart, against one type parameter. The ruling's condition was less code, and building the split would not move the sign of that comparison. What the generic form costs is binary, 11398 bytes that run once a boot, and it is measured; what it buys is PendingHandles::Ready gone and no Option for sys_spawn to unwrap. Accepted.
  • May the model alone hold the kernel's order? No, and it does not have to. Its steps are hand-cut and no kernel line reaches it. What it holds is that the order is sufficient: every interleaving of the spawn's two sections with a sibling's close and the place's kill, red when the caller's mint leads. The kernel's conformance is held by commit's signature: the caller's handle cannot be an object's only one unless commit drops own before the window or takes an object again. mut-head-order.patch is the second, five hunks that change that signature at both ends. A reader of the diff catches it, so it is no named mutation and no debug action is owed for it. A commit that takes a KObjectRef again reopens this.

BLOCKER

None.

NOTE

  • PR body, "T14" — "At c837976, owed" is no longer true — the body is main's record: it carries the seven boots' exits and the nine members by name, and the exits of the four judge commands metal/request.txt lists, run over those readbacks. A red judge reopens the round.
  • PR body, the loader::spawn bullet — "34 of its locals … which a split carries in a struct" overstates the split — 34 is what spawn reads after the commit, and a split carries at least a dozen of them: say the count is an upper bound.

REMOVE

  • PR body, "## Unsure" — both bullets restate the mut-head-order.patch row and the size section, and this review rules on both.

LAND AFTER NAMED CHANGES

@Japabu
Japabu marked this pull request as ready for review October 2, 2026 18:31
@Japabu
Japabu enabled auto-merge October 2, 2026 18:31
@Japabu
Japabu added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit dc8212c Oct 2, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-proclife branch October 2, 2026 19:19
Japabu added a commit that referenced this pull request Oct 2, 2026
No conflict. `src/ci.rs` is the one file changed on both sides, in separate
hunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu added a commit that referenced this pull request Oct 2, 2026
#647, #642 and #636's follow-up landed since the last merge. One conflict,
in tests/toyos.rs: #647 made `boot_virt_smp` take a `BootOptions` and
`judge_virt_job` borrow its guest, and this branch had added
`virt_mask_windows` on the old shape. Main's shape is kept whole;
`virt_mask_windows` names its kernel build in the options and lends its
guest like the others. `tests/virtsmpcase` now ends in `shutdown`, which
this test does not wait for: it judges once `unmap_touch` has ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu added a commit that referenced this pull request Oct 2, 2026
Three modify/delete conflicts, each hunk of this branch's side accounted for:

- tests/toyos-rust-tests/src/bin/log_hold.rs, deleted by 520c0d1: the one
  hunk moved its 192 records from syscall 26 to `SYS_DEBUG` `LOG_PATTERNED`.
  The binary and `log_program_line_after_its_records` are gone, so it goes.
- tests/common/origin.rs, deleted by 520c0d1: `staged_job`, `one_job` on it,
  `PATTERNED` in `RETIRED`'s place and `after_records` on the test kernel with
  its per-index count all served that one test. They go.
- issues/build/no-device-class-answers-for-a-block-device.md, deleted on main:
  the one hunk dropped "(3 and 4 are retired.)". It goes.

Content conflicts:

- tests/common/logstream.rs and tests/common/qemu.rs are main's: this branch's
  `stage_on_test_kernel`, `write_staged` and `build_test_kernel_image` had
  `origin::after_records` as their only caller.
- issues/build/the-boot-census-guesses-a-staged-images-kernel.md, which this
  branch filed against `build_test_kernel_image` and a staged
  `BootOptions::boot_image`, goes: main has neither.
- CLAUDE.md and .claude/agents/reviewer.md are main's: #673 landed this
  branch's half of both.
- issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md
  is main's: this branch's hunk edited stage 0, which #642 landed and deleted.
- issues/kernel/the-capability-end-state-is-twelve-answers.md: main deleted the
  sentence this branch's first hunk edited; the second hunk, which drops "85
  `SYS_LISTEN` and 87 `SYS_CONNECT` are retired numbers", is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu added a commit that referenced this pull request Oct 2, 2026
The branch was at 649ea51 (#641). Three landings since sit under it:
#642 (dc8212c), #659 (c1c5048) and #655 (5daab30).

Two content conflicts, each main deleting what this branch's hunk stood
beside:

- kernel/src/object/ops.rs, close_ends_polls: #655 deleted the log's and
  the keyboard's close actuators, whose two arms this branch's
  `Process(_) => false` sat between. Main's two `false` arms stand and
  the process's is a third.
- tests/toyos-rust-tests/src/bin/process_lifecycle.rs, the imports: #642
  deleted `toyos::AsHandle` with the pid arm, its one user; this branch's
  `toyos::poller` import stands alone.

Everything else merged by itself: #642's deletions in
kernel/src/object/process.rs beside this branch's `Arc<Watch>`, #659's
init changes beside the one doc sentence this branch deletes, and the
`rust` gitlink at main's 95960d6c214.

This commit is the resolution and nothing else. What #655's contract
changes in this branch's own lines is the next commit's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
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