Skip to content

A parent's end takes its children down: every end walks its subtree and is published after it, a spawn past its commit lands, and a refused spawn spends no pid - #659

Merged
Japabu merged 22 commits into
mainfrom
wt/toyos-proclife4
Oct 2, 2026

Conversation

@Japabu

@Japabu Japabu commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Stage 4 of issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md, as ruled: every process has one parent, and an end (exit, kill, CPU fault, handle fault) takes its whole subtree. The stage is deleted from the track.

What a user sees today

A program started by another is that program's child, and ends when it ends. On main a process has no parent, and nothing ends a program because the one that started it ended.

  • Closing a terminal ends its shell and every program started from that shell that is still running, including one the user meant to leave running. detach <program> at the shell starts a program under init, which outlives the shell and the terminal.
  • If the compositor ends, every terminal and app started from the desktop ends with it.
  • A program started over ssh ends when sshd ends. sshd itself is started by hand with detach /system/bin/sshd and outlives the shell that started it.
  • A program that starts another and exits takes the other with it. That is what pkg_install_gbae showed: its client launched gbae and exited, and gbae ended as killed.

The track's stage 5 gives each login a session under init, which is what separates a login's programs from the compositor and sshd. Until that session lands, a launched program dies with its launcher. The owner has ruled that the track's own interim, which stage 5's negative control states: "the stage reverted whole, where the compositor's programs die with it". The owner was not asked to accept anything further. Nothing here changes how the compositor, the terminal and sshd launch.

What changed, per decision

The decisions are toyos-proclife's tree and pids modules. Each process carries a Node: its parent, its depth below init, the children placed under it, and a count of what holds its publication. The count is its own teardown, plus every child admitted under it and not yet published or refused. The kernel only performs the answers (kernel/src/process.rs, kernel/src/loader/mod.rs).

  • Admission. tree::admit_child runs at the top of loader::spawn, before anything is built, and takes the child's pid.
    • A place being torn down answers Gone.
    • A child more than MAX_DEPTH (64) below init answers ResourceExhausted, and the kernel logs spawn: refused under pid N at depth D, more than 64 below init.
    • With every pid issued, it answers ResourceExhausted, and the kernel logs spawn: refused, every pid below 4294967295 is issued.
    • Admission raises the place's count. A spawn refused after it (the build, or the commit's own refusals) lowers the count and gives the pid back, through the Admission guard's drop (tree::refuse_child).
  • Pids (toyos_proclife::pids::Pids, held by the process table beside its entries; ProcessTable is a struct now).
    • Every pid the kernel issues comes from it: init's, every spawn's, and the kernel thread's.
    • A refused spawn's pid is the next one taken, so a refused spawn spends none. The pids waiting to be reused are at most the spawns in flight at once.
    • Pid::MAX is never issued, because it is the per-CPU word for no process. On main, IdMap issued it, and the next Pid + Pid panicked the kernel.
    • Pid is no IdKey and has no Add: no IdMap is keyed by a pid, and IdMap's step was the only place two pids were added.
    • Giving a pid back is sound because no entry ever held it. The ProcessObject made before the commit dies with the refused spawn, so no handle, no node's children and no walk names that pid. A pid an entry held is never issued again.
    • Why the pid is not taken at the point of no return: commit itself refuses after that point (a self label, a missing TRANSFER, no room), so each refused commit would still spend a pid, without bound. Taking it after the commit leaves exhaustion to refuse after the caller's handles moved. A 64-bit pid was not taken, because std's process::id() and Child::id() answer u32.
  • Past the commit, a spawn lands. The commit moves the caller's handles. Then tree::land_child inserts the child in one hold of the table lock, and the caller fills the entry and schedules the thread inside that hold, through a closure.
    • A place claimed since the admission has already read its children in its walk. So the child is claimed (137) in the hold that lands it.
    • The landing answers the threads its spawner retires: every thread of such a child, and none of any other. Admission::land maps them through scheds, which every claim's retires also go through. The loader posts them after the table lock is given up.
  • The walk. An end claims its top, then walks: one claim per hold of the table lock. Each claim reads the children it owes in the same hold. Between two claims, a reschedule owed is served by yield_now with nothing held. The walk runs in process::exit, which serves exit, CPU fault and handle fault, and in process::kill, which kill_process calls, on the ending or killing thread. A process another end claimed first is that end's to walk.
  • Publication after every end below. A teardown keeps its Exit on the entry and lowers its own share of the count. Whoever lowers a count to zero publishes, then lowers the parent's count. Publication goes child before parent, at most MAX_DEPTH + 1 times, each with the table lock given up.
  • self. Every process starts holding a handle to itself under SELF_LABEL ("self"), with WRITE, DUP and TRANSFER. The commit that moves the endowments installs it.
    • A spawn endowing a self of its own is refused InvalidArgument, because the child's lookup would find it first.
    • MAX_SPAWN_ENDOWMENTS and MAX_SPAWN_LABELS_LEN (toyos-abi) are what a spawn may carry: one entry and four label bytes under what the table holds. SYS_SPAWN's decode and build_child_handles both read them.
    • toyos::endow::this_process answers the handle, and panics when it is gone, since the kernel puts it in every table.
  • SpawnArgs::place (112 bytes to 120). It is a handle carrying WRITE, or HANDLE_INVALID for the caller.
    • The place and SYS_NAMESPACE_BUILD's connector are both handles a peer sent. They resolve through one lookup, HandleTable::get_sent, which answers a wrong type InvalidArgument. HandleError::WrongType documents the exception once, and the connector's own match in ipc.rs is gone.
    • A handle without WRITE, such as a MANAGE-only one, is PermissionDenied.
  • The launch wire names the parent. The header's ninth word is either a place or init. A place travels as the batch's last handle and counts against MAX_LAUNCH_EXTRAS.
    • init refuses a launch that names neither, and spawns under the place (CommandExt::under).
    • init answers a place being torn down with the new MSG_GONE, which it reads from its spawn's BrokenPipe, std's word for the kernel's Gone. Nothing else in that path answers BrokenPipe: the command is prepared, so the spawn calls no file server, and every refusal start makes before the spawn is Other.
  • std. The rust gitlink is 6c7f996a4fe: main's pin 3f6050fc829 (libc: the POSIX surface LLVM compiles against, and the headers held to the definitions #650) with this branch's three commits (336c8a8e141, 40470840283, d1b9f2eae3c) merged over it, and nothing else. The two sides share no file. It is on the fork's main, through the merge 012fdce3c79, and the fork branch this pull request had of its own is deleted. No lockfile names the fork.
    • A launch carries a copy of the caller's self.
    • CommandExt::under_init asks for init, and is a launch or nothing. No launcher, a program the launcher answers as undeclared, an endowment and an extra slot each answer PermissionDenied and start nothing.
    • A launch that cannot carry its place or its connectors does not turn into a direct spawn. A place or a stdio slot whose duplicate the kernel refuses answers that refusal. More connectors provided than fit beside the place answer InvalidInput.
    • A launch whose argv and environment do not fit one frame still turns into one, as on main: Launch::encode refuses it, std answers LaunchError::NotSent with the direct spawn, and that child holds its caller's namespace and not its row (issues/isolation/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md).
    • A direct spawn's refusal goes through to_io_error.
    • The endowment table and its namespace copy are made after the routing. A launched spawn had leaked the namespace copy made for a direct spawn.
  • The shell gains detach <program>, which starts a program under init and does not wait for it. system.toml's note on starting sshd by hand now names it.
  • A test actuator reaches the landing. debug_action::KILL_PLACE_AS_SPAWN_LANDS (22, never assigned) marks the caller's next spawn. On the test-actuators kernel the loader kills that spawn's place after the commit and before the landing, a window no caller can order a kill inside. The static, the mark and the hook are each #[cfg(feature = "test-actuators")], which src/clippy.rs's boot-actuators,test-actuators shapes lint on both architectures.

Gates, at 3fd1419

3fd1419 merges main at 80bc97b (#643), which shares no file with this branch. rust is at 6c7f996a4fe and the tree is clean after each. Logs and gates.exits are under /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/659-named/; the same gates at 517d8c1, before that merge, exited the same and are in at-517d8c16c/ beside them.

  • cargo run -- --ci host: EXIT=0, "Host: 64 step(s), all green". Its clippy step runs every kernel shape. Its controls step reached every toyos-proclife control's verdict, the five this branch adds among them.
  • cargo run -- --build-only: EXIT=0. The sysroot is built from the fork at 6c7f996a4fe.
  • Those two are what show impl IdKey for Pid and impl Add for Pid dead. With both deleted, the kernel builds on both architectures and in every clippy shape, and so do the std fork, the SDK, libc and every userland crate. The oracle is rustc's name resolution, which fails the build of any caller left. The guest suite and the metal staging below built every test binary.
  • cargo test --test toyos-build, the whole guest suite: EXIT=0, "21 passed, 21 total". This branch adds no test to it.

The T14

  • Three boots, at 1e9cb8c, run by the orchestrator (A parent's end takes its children down: every end walks its subtree and is published after it, a spawn past its commit lands, and a refused spawn spends no pid #659 (comment); LENOVO 20W0003AMZ, BIOS N34ET71W (1.71)). Each is one image flashed and booted once by cargo run --bin toyos-metal -- --image … --readback … --fat32-check: proctreecase EXIT=0, shared carrying abuse_handle_table alone EXIT=0, shared-debug carrying spawn_lands_claimed alone EXIT=0. Each verdict.txt reads "passed".
  • The three rows, judged over those readbacks. With a readback in place the command only judges, and touches no machine. R is /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/659-round. Each was run by the harness at 1e9cb8c and again by the harness at 3fd1419, with the same exit and lines:
    • cargo test --test toyos-build -- --metal --metal-readback $R/metal-process_tree process_tree: EXIT=0, "PASS process_tree", "1 passed, 0 failed, 1 boot(s)".
    • cargo test --test toyos-build -- --metal --metal-readback $R/metal-abuse_handle_table abuse_handle_table: EXIT=0, "shared: 1 member(s)", "1 passed, 0 failed, 1 boot(s)".
    • cargo test --test toyos-build -- --metal --metal-readback $R/metal-spawn_lands_claimed spawn_lands_claimed: EXIT=0, "shared-debug: 1 member(s)", "1 passed, 0 failed, 1 boot(s)".
  • Those readbacks are 1e9cb8c's, not this head's. No image of 3fd1419 has booted on the T14. Since 1e9cb8c the head:
    • deletes the two impls, in the kernel and the ABI. The four kernels the gates rebuilt, two per architecture, each hash differently from the one the tree held at 1e9cb8c, and the shipping x86-64 one is 176 bytes smaller;
    • merges main's blockd sets its ring cursors before it answers an open #643, which changes blockd, a program in every image;
    • deletes a comment line in abuse_handle_table.rs, which moves that binary's panic line numbers;
    • moves process_tree's judge within tests/toyos.rs, its text unchanged;
    • records the proctreecase boot in tests/metal/lenovo-20w0003amz.toml, and files two issues.
  • The record. Judging process_tree wrote the proctreecase boot's three numbers (complete 1153 ms, panel_max 3773 us, panel 21279 us), and they are committed: this branch adds that boot. Judging abuse_handle_table wrote three boot.shared rows off a boot carrying one member. They are left out, because shared's whole boot is not this branch's.
  • Staged at 3fd1419, the machine untouched: cargo test --test toyos-build -- --metal --metal-readback /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/659-named/metal-whole, EXIT=2, "staged 28 image(s)". No filter selects the shared boots alone, so the whole profile was staged and the images nobody asked for were removed. Six remain: shared (38 members), shared-2 (38), shared-3 (2), shared-debug (4), ccorpus and proctreecase.

Tests

  • Host (toyos-proclife; the model carries the count, the climb, the pids and laws L9 to L12):

    • A spawn racing its place's kill leaves nothing under it and publishes it, in every ordering. The spawner is either init serving a launch or the place's own thread, and its build either lands or fails. The spawner retires what the landing answers.
    • An exit publishes after every end below it.
    • A child's own exit races its parent's kill.
    • A spawn under an unrelated process lands between two claims of one walk (explore_any: some ordering has it).
    • tree::tests: a chain is refused at MAX_DEPTH + 1 below init; a place being torn down admits nothing; a child landed under a place claimed since its admission is claimed with it, its thread is answered for its spawner to retire, and it holds the place's publication; a published child leaves its parent's children; with two pids left, a refused spawn gives the last back three times over, the landing takes it, the next admission answers NoPid and raises no hold, and init publishes once its child does.
    • pids::tests: pids issue in order from 0; the last is u32::MAX - 1 and none follows it; a pid given back is the next taken.
    • The SDK: a request's ninth word names a place, init, or (any other word) neither.
  • process_tree, a METAL row booting tests/proctreecase (a launcher, cat and the shell). For B killed and B taking a CPU fault:

    • B's subtree is C, spawned; D, launched under B; and E, under init.
    • Once A's wait on B answers, a non-blocking wait on C and on D reads 137, and E still runs.
    • After the kill, a spawn and a launch under B's self answer Gone.
    • B's first act is a spawn the loader refuses after admission.

    Then:

    • A MANAGE-only handle as place answers PermissionDenied.
    • A pipe as a launch's place is refused by init, which answers the launches after it.
    • A launch under this process's self narrowed to WRITE answers PermissionDenied and starts nothing.
    • under_init is refused for an undeclared program and for an extra slot.
    • A chain alternating launch and spawn stops at a depth refusal and dies whole with its first link (roster). The chain reads its stop off the shell, which says : not found for every spawn error (issues/diagnostics/the-shell-says-not-found-for-every-spawn-error.md), so the depth is the judge's to show.
    • A cat a shell detaches outlives the shell (roster, at the instant the shell's wait answers).

    The row's judge also reads two kernel records off the stick: spawn: /system/bin/no_such_program: , once per B, showing B's first act reached the loader rather than failing in std; and the one depth refusal, naming 65.

  • abuse_handle_table, a shared-boot member, gains two arms beside its repeated-endowment refusal:

    • an entry labelled self naming a pipe's write end answers InvalidArgument, and a byte written to that end still arrives;
    • MAX_ENDOWMENTS entries naming distinct pipe ends answer InvalidArgument, and the first MAX_ENDOWMENTS - 1 start a child that exits 0.
  • spawn_lands_claimed, a shared-boot member on the kernel that carries SYS_DEBUG. The test starts a place that hands it its self, marks its next spawn, and spawns a child that parks under that place.

    • The spawn answers the child.
    • A second spawn under the place answers Gone, which is the kill having happened.
    • The wait on the child and the wait on the place both answer 137. A child claimed and never retired parks on and holds its place unpublished, so neither wait answers.
  • Where they run, and why no cheaper tier. All three run on the T14: one row and two shared-boot members. None is a QEMU guest test.

    • A type cannot hold them: each is what the kernel does at run time.
    • A host test holds the decisions, in toyos-proclife. No host harness reaches what performs them: kill's walk, the loader's Admission and its retire posts, spawn_place and get_sent, init spawning under a place, and std's ToyOS code run only on a booted machine.
    • process_tree has a boot of its own because tests/testcases has no launcher and declares no shell.

High-risk checks

  • Negative control, the whole stage. The orchestrator ran it under QEMU at d69b80a, before The guest suite keeps the 21 tests only a booted machine answers; the rest are metal, host or tracked #660 moved the test to the T14: the stage reverted onto main's 1e4d3e0, with process_tree cut to its first arm in main's API, exits 1 on "C was not ended, as killed, once B's end was published" (A parent's end takes its children down: every end walks its subtree and is published after it, a spawn past its commit lands, and a refused spawn spends no pid #659 (comment)). It has not been run on the T14.
  • The whole suite before the merges of main, at ad21ebf: cargo test --test toyos-build EXIT=0, "471 passed, 471 total". It is the one QEMU verdict over the shared boot's members on this kernel.
  • Mutations, each a checked patch, shown to build, and the tree restored. The landing's ran under QEMU at 0ee4b44 and the endowments' at d62803b; A parent's end takes its children down: every end walks its subtree and is published after it, a spawn past its commit lands, and a refused spawn spends no pid #659 (comment) carries those patches with each command, exit and line. No mutation has been booted on the T14.
    • The landing's kernel half. Admission::land answering no thread (mut-landing-land) and the loader posting no retire (mut-landing-post): spawn_lands_claimed exits 1 under each, at the suite's 300 s ceiling, because the wait on the child never answers. The actuator killing nothing (mut-landing-nokill): exit 1, the guest's 101 on "the kernel did not kill the place of the marked spawn".
    • The endowment refusals. start.rs's self check deleted: abuse_handle_table exits 1, the guest's 101 on "an endowment labelled self must be refused". The one-entry margin deleted, MAX_SPAWN_ENDOWMENTS = MAX_ENDOWMENTS: exit 1, the guest's 101 on "a spawn carrying MAX_ENDOWMENTS entries must be refused".
    • mutate-landed-child-retires-nothing, the host control for the landing's decision: --ci host reached its two verdicts, the interleaving and the tree landing test both FAILED.
    • refuse_child without give_back: exit 101, the pid exhaustion test FAILED. Pids::take without its Pid::MAX stop: exit 101, both pids tests and that test FAILED. Both were measured at cc49e13; since then toyos-proclife has changed by one line of its crate doc.
  • The track's models. toyos-proclife's interleavings cover the protocol this stage changes, and this round changes none of it: kill_process's body moved to process::kill(pid) unchanged. They ran in --ci host.
  • Oracle: Linux's cgroup v2 (Documentation/admin-guide/cgroup-v2.rst):
    • cgroup.kill kills every process in the cgroup and all its descendants, which is what the walk does from an end.
    • cgroup.events' populated turns 0 only when the cgroup and every descendant holds no live process, which is the publication order the host model checks.
    • cgroup.max.depth refuses a new descendant at or past the limit, which is the depth refusal the chain checks.
    • For pids, Linux's pid_max: alloc_pid answers EAGAIN once no pid is free, a refusal and never a panic, and fork frees the pid of a child it fails to finish (kernel/pid.c, free_pid on copy_process's error path).

Unsure of

  • Six images of this head booted on the T14, run by the orchestrator at 3fd1419: shared (38 tests ended, 38 exit 0), shared-2 (38, 38), shared-3 (2, 2), shared-debug (4, 4), ccorpus (90, 90, tinycc 206 to 208 among them) and proctreecase (1, 1); each toyos-metal EXIT=0 with verdict passed, in A parent's end takes its children down: every end walks its subtree and is published after it, a spawn past its commit lands, and a refused spawn spends no pid #659 (comment). They are green arms only, and the harness has not judged these readbacks: the judging exits above are over the boots at 1e9cb8c.
  • The harness cannot judge those boots alone. A filter is a substring of a member's name, and none selects the shared boots without the rest of the profile, so what those boots answer with is toyos-metal's exit and the kernel's exit: record of each member.
  • A stdio slot whose duplicate the kernel refuses answering that refusal is unmeasured: no test makes that dup fail. The place's half is measured, by process_tree's launch under a self narrowed to WRITE.
  • The fork's main holds more than this pin. Its tip, 012fdce3c79, also carries M2: CMake's ToyOS in every C sysroot, LLVM's two ToyOS arms, and LLVM, clang and LLD built for a ToyOS host by unchanged bootstrap as far as libc lets it #661's five commits (25da73337eb to 7cd37350b51), which nothing on ToyOS's main consumes. This pull request pins 6c7f996a4fe, which has none of them.
  • A child claimed as it lands may run user code between its enqueue and its spawner's retire, as any killed process does before its retire lands. Nothing can land under it, because it is claimed in the hold that inserts it.
  • spawn_lands_claimed reds a broken landing by a hang, at the runner's deadline, not by a named assertion: a child that is never retired answers no wait.
  • Pids are no longer issued in landing order. A spawn refused after admission hands its pid to the next admission, so a later child can carry a lower pid. Three places read pid order: sys_sysinfo's roster, which then lists such a child among older ones and, cut short by a small buffer, keeps the lowest pids; reap::finished_pids, which sorts for determinism; and the metal judge's Readback::exit_code, which takes a name's lowest pid for the job (issues/build/a-metal-judge-takes-a-names-lowest-pid-for-the-job.md).
  • A caller that provides five connectors can no longer launch: the place takes one of the five.
  • yield_now between two claims of a walk is unmeasured: no test reaches it deterministically. It runs on the exit, kill and CPU-fault paths; the CPU-fault entry runs at BASELINE_TRAP, which yield_now asserts.
  • Two commit messages name the wrong tree. 54c375a says it merged main at de5f63c and merged 74a2e70: the shared origin/main ref had moved. ad21ebf says pkg_install_gbae was red at d62803b; it was measured at d69b80a, whose tree differs only in abuse_handle_table.rs.

What remains

  • Nothing of stage 4's own text. When issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md stage 2 moves launch resolution into toyos-supervisor, init's refusal of a launch naming no parent moves with it.
  • issues/kernel/a-process-that-starts-four-billion-threads-panics-the-kernel.md: a thread id still overflows into a kernel panic, as a pid did.
  • issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md: sys_spawn still installs the child's handle after the commit, so a full caller table refuses a spawn whose endowments have moved. Every other refusal leaves the caller's table as it was.
  • issues/isolation/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md and issues/diagnostics/the-shell-says-not-found-for-every-spawn-error.md: main's behaviour, filed here and not changed.
  • Two sentences this branch made stale and no review named: kernel/src/sleeplock.rs's doc on FREE says id_map never issues u32::MAX in the pid half, and issues/kernel/a-double-fault-on-cpu-1-under-a-wide-suite.md says pids come from IdMap. Pids come from toyos_proclife::Pids.

🤖 Generated with Claude Code

https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm

Stage 4 of issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md,
as ruled: every process has one parent, and an end -- exit, kill, CPU fault,
handle fault -- takes its whole subtree.

The decisions are toyos-proclife's new `tree` module. Each process carries
a `Node`: its parent, its depth below init, the children placed under it,
and a count of what holds its publication -- its own teardown and every
child admitted under it and not yet published or refused.

- A spawn is admitted under its place at the top of the loader, before
  anything is built: a place being torn down answers `Gone`, and a child more
  than MAX_DEPTH (64) below init `ResourceExhausted`, which the kernel logs
  with the depth. Admission raises the place's count. The insert asks again
  under the lock that inserts, and a place claimed since refuses it (`Gone`)
  and lowers the count, as every failed build does through the `Admission`
  guard's drop.
- An end claims its top, then walks: one claim per hold of the table lock,
  each claim reading the children it owes in the same hold, so a child either
  landed before the claim and is walked or is refused at its insert. Between
  two claims a reschedule owed is served by `yield_now` with nothing held.
  The walk runs in `process::exit` (exit, CPU fault, handle fault) and in
  `kill_process`, on the ending or killing thread. A process another end
  claimed first is that end's to walk.
- A teardown no longer publishes at once. It keeps its `Exit` on the entry
  and lowers its own share of the count; whoever lowers a count to zero
  publishes, then lowers the parent's: child before parent, at most
  MAX_DEPTH + 1 publications, with preemption off and the table lock given
  up for each publication.

The ABI: `SpawnArgs` gains `place` (112 to 120 bytes): a handle carrying
`WRITE` to the process the child goes under, or HANDLE_INVALID for the
caller. Every process starts holding a handle to itself under `self`
(`WRITE`, `DUP`, `TRANSFER`), installed by the commit that moves the
endowments, so a table and a label blob hold one entry and four bytes more
than a spawn may carry. The place and SYS_NAMESPACE_BUILD's connector are
both handles a peer sent, and resolve through one lookup,
`HandleTable::get_sent`, which answers a wrong type `InvalidArgument`; the
connector's own match in ipc.rs goes.

The launch wire names the parent: its header's ninth word is a place, which
travels as the batch's last handle and counts against MAX_LAUNCH_EXTRAS, or
init. A launch naming neither is refused. init spawns under the place
(`CommandExt::under`) and answers a place being torn down with the new
MSG_GONE. std launches with a copy of the caller's `self`; `under_init`
asks for init and is a launch or nothing -- no launcher, a program no row
declares, an endowment or an extra slot answers `PermissionDenied` and starts
nothing. std's direct spawn maps the kernel's word through `to_io_error`, so
init can tell `Gone` (BrokenPipe) from a refusal, and makes its endowment
table after the routing: a launched spawn had leaked the namespace copy made
for a direct one. The shell gains `detach`, which starts a program under
init and does not wait for it.

`compositor_client_death`'s relay outlived its creator; it is now placed
under the test's root, through the root's `self`, so the creator's end does
not take it down.

Host: four new interleavings and a depth test, each with a feature control
in src/ci.rs's CONTROLS. Guest: `process_tree` on tests/proctreecase, and two
new arms in `launcher_refusals`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Japabu and others added 2 commits October 1, 2026 05:55
The first sshd is killed as soon as the roster shows it, which can be
before it binds its port, so a count of its listening lines is not a
fact the guest arranged. What makes the arm about a kill rather than an
sshd that ended by itself is the kernel's line for its end: one, and
code 137.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
`every_shipped_boot_config_is_covered` found the new case config and no
row for it in ALL_CONFIGS, so none of the gates over configs read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title A parent's end takes its children down A parent's end takes its children down: every end walks its subtree and is published after it Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 81ff55022 against .claude/agents/reviewer.md. This is round 1.

CI: host was skipped at 81ff550 because the PR is a draft; ci.yml runs host only on a ready PR. No guest has run at any head. Every host gate in the body is a local measurement.

Growth: git diff --shortstat origin/main...HEAD gives 43 files, +1768 −215.

  • Production (kernel, ABI, SDK, init, shell, toybox, system.toml, and toyos-proclife's tree/lib/table): +697 −99.
  • Tests, model and harness: +1069 −63.
  • Track: +1 −52.
  • std fork 336c8a8e141: +147 −47.

The production growth is the ruled stage. The deletions are named below.

BLOCKER

  1. kernel/src/loader/mod.rs:605,654-661: commit moves the caller's endowments before land. A place claimed after Admission::ask is then refused Gone, and the moved handles drop with the unbuilt child.
    • Two readers assume a refused spawn moved nothing: init's start at userland/init/src/main.rs:2126, whose held drop closes them, and std (library/std/src/sys/process/toyos.rs:401, "An endowment moves only on a spawn that happened").
    • So init closes handles it no longer holds, takes a handle fault and exits 139. Under this stage its end then takes every process on the machine.
    • Any launcher holder can trigger this by ending while init builds its launch. A terminal closed during ls is enough.
    • Required: a spawn that answers a refusal leaves the caller's table as it was. Either decide the place before anything moves, or a child that landed and was then claimed answers success.
    • Test that must turn red at this head: at toyos-proclife/src/interleave.rs:217, the SpawnUnder insert section marks the caller's handles moved before tree::insert_child, which is the kernel's order. final_faults gains the law "a refused spawn moved its caller's handles". a_spawn_racing_its_places_kill_lands_nothing_under_it_and_publishes_it reds now and is green after the fix.
  2. rust, gitlink 336c8a8e141: 336c8a8e141 was merged into the fork's main as 2df0b60efd0, beside libc: the POSIX surface LLVM compiles against, and the headers held to the definitions #650's 9151571cae9, while ToyOS main has neither half.
  3. library/std/src/sys/process/toyos.rs:510 (fork 336c8a8e141): None | Some(Err(_)) => return direct(None) turns a launch into a direct spawn. The child then holds the caller's namespace instead of its row, and nothing says so.
    • This happens for a missing self, which is a kernel-contract violation or a caller that takes "self" first.
    • It also happens for a place whose dup failed.
    • :496 does the same to a caller with five provides, which launched before this branch.
    • Required: fail fast on a missing self, and refuse the other two.
  4. toyos-proclife/src/tree.rs:178: this mutation passes every host test: delete proc.node_mut().children.retain(|&c| c != child); in published. A published or reaped pid only loses its claim. Yet init's children then grows by one pid per process ever started under it, and an end walks every dead pid in it.
    • Required: a tree test that a published child has left its parent's children. It must red under this patch.
  5. toyos-proclife/Cargo.toml:66-83: four new features, with #[cfg(not(feature = …))] arms at tree.rs:123 and :225-227. The PR body does not show a planted mem::forget in each arm turning cargo run -- --clippy red.

NOTE

  • kernel/src/process.rs:1198: the walk's yield_now is reached deterministically by no test, and deleting it passes everything. It also runs on the CPU-fault path. Either name it unmeasured or give it a metal row.
  • kernel/src/process.rs:1072: preempt_off around the climb has no stated reason. The publish it replaces ran without one.
  • kernel/src/process.rs:888: process::Refused wraps tree::Refused for a single caller.
  • kernel/src/syscall/dispatch.rs:230 and kernel/src/loader/start.rs:135,143: "one fewer for self" is worked out at two sites. Make it one ABI constant for what a spawn may carry.
  • kernel/src/loader/start.rs:124 and toyos/src/endow.rs:133: a caller may endow a self label. index_of answers the first match, so the caller's label shadows the kernel's. Refuse SELF_LABEL in a spawn's endowment by name.
  • userland/init/src/main.rs:1600: init reads the kernel's Gone as std's BrokenPipe. Any BrokenPipe a spawn answers is therefore reported as the place ending.
  • tests/toyos-rust-tests/src/bin/process_tree.rs:414: ud2 is written as core::arch asm in a generic test binary. A write through a null pointer gives the same CPU fault without it.
  • tests/toyos-rust-tests/src/bin/process_tree.rs:344: this is a spin-poll on the roster, not a wait on the event. It goes with the sshd arm below.

REMOVE

  • kernel/src/loader/mod.rs:599-604: "and fatal to it … and so is a parent claimed while this was built" is false, because the spawn answers Gone.
  • toyos-proclife/src/teardown.rs:13,60: "publishes its exit" and "publishes code" are false now that publication waits for every end below.
  • userland/toybox/src/ps.rs:114: rewritten instead of deleted.
  • issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md:132: "as an end does" is a rewritten citation. Delete the clause.
  • tests/toyos-rust-tests/src/bin/process_tree.rs:10 and tests/toyos.rs:852: "so B's end was published after theirs" and "publication order" are claims the test cannot back. A reads C only after it wakes, so the guest cannot see order.
  • tests/toyos-rust-tests/src/bin/launcher_refusals.rs:37-42,273-276: a module paragraph that repeats the two function docs, and a rewritten doc.
  • PR body: "The first run, at the merge 6c38904, was red…" is chronology. jobs.txt and negative-control.patch name files no tree will hold.

The ladder, arm by arm (host = toyos-proclife at this head)

process_tree

  • B killed. Host reaches the claim, the children read in the same hold, and the count. It does not reach:

    • kill_process calling walk;
    • init spawning D under the launch's place;
    • under_init placing E under init;
    • Gone across kernel, init and std;
    • the loader's Admission drop (B's first act).

    Keep. This arm shows the kernel performs the tree.

  • B's exit, CPU fault and handle fault all enter process::exit. Keep CPU fault only: it is the one entry from an exception frame. Cut the exit and handle-fault arms.

  • The order assertion in each arm is owned by host's an_exit_publishes_after_every_end_below_it. The guest only shows that C and D ended, as killed.

  • under_b_is_gone after publication (:186): an absent place is tree::tests::a_place_being_torn_down_admits_nothing. Keep the after-kill call only.

  • MANAGE-only place: this is the kernel's spawn_place, and no host harness reaches kernel syscall code. Keep. It is the refusal that stops a MANAGE holder from placing children under a process.

  • under_init ×3 (:266): std's ToyOS code runs only on a booted machine. Keep the undeclared program and one of endowment or extra slot, since those two share one if.

  • Chain: host reaches MAX_DEPTH. It does not reach a depth counted from the place through init, or the deepest subtree there is, ended by one kill. Keep.

  • sshd (:342): D and E already show a launch dying with its caller and init's child outliving it. This arm adds netd, a NIC, a staged key, and a dependence on netd freeing the port before the detached sshd binds. The deferred-release defect makes that last part racy. Cut it, along with proctreecase's netd and sshd rows and the harness's exit: sshd check. Cover detach with shell -c "detach /system/bin/cat".

launcher_refusals

  • No parent (:290): the decision is Request::parent() on one wire word, which is pure. Host-test it in the toyos SDK step. The guest arm protects nothing, because any client may send PARENT_INIT. Cut.
  • Pipe as place (:318): this is the kernel's get_sent, and init surviving it. Keep.

Where it runs: nothing above needs QEMU rather than the T14. Under #660's ladder this is a metal row. The trimmed proctreecase (launcher, cat, shell) is what that row boots.

Guest runs

  • Required:

    • the negative control;
    • one whole-suite green run.

    Run both at the head that closes 1–5, not at 81ff550. The green run's console must carry the loader's spawn: /system/bin/no_such_program: line. That line is what shows B's first act reached admission rather than failing in std.

  • Dropped, with the reason for each:

    • D under init: the D assertion is red by construction, and the negative control already reds the arm.
    • Publish-before-children in the kernel: the host control owns the decision, and the guest cannot see order.
    • Walk from sys_exit alone: every end enters process::exit.
    • Refused-insert count in the kernel: the host control owns the decision, and the console line above shows the arm reaches it.
    • Place looked up with get: the pipe arm's next answer fails if init dies.
    • No depth check, and depth counted from the spawner: the harness's one-refusal-at-65 check reds both by construction.
    • No-parent launch under init: that arm is cut.
    • std's NotDeclared fallback, and std's early return: one if each, red by construction in init_is_asked_only_by_a_launch.

SEND BACK

Japabu and others added 3 commits October 1, 2026 07:32
…d the tree's tests reach what they claim

- A spawn that answers a refusal leaves its caller's table as it was.
  Admission is the last refusal: past it the loader moves the caller's
  handles, and `tree::land_child` lands the child whatever happened since.
  A child whose place was claimed since its admission is claimed in the hold
  that inserts it, its retires posted by its spawner, and the spawn answers
  it. `tree::insert_child`, its `Refused` and the kernel's wrapper go.
  The model moves the caller's handles before the landing, as the kernel
  does, and gains L12: a refused spawn moved none of them. At 81ff550,
  with only that law added, the race test reds on it.
- The race test runs a failed build beside a landing, which is what
  `mutate-refused-spawn-keeps-the-count` (renamed from the refused insert's)
  now reds through. No test is gated on a control feature.
- `tree::tests` gains a published child leaving its parent's children, and
  a child landed under a place claimed since its admission.
- std (fork 40470840283): a launch that cannot carry its place is refused,
  never spawned directly. `toyos::endow::this_process` answers the handle
  and panics when it is gone.
- The kernel refuses a spawn endowing `self`; `MAX_SPAWN_ENDOWMENTS` and
  `MAX_SPAWN_LABELS_LEN` are what a spawn may carry, read at both sites.
- The climb runs without `preempt_off`.
- The toyos SDK host-tests `Request::parent`.
- process_tree keeps the killed and CPU-fault arms, the MANAGE-only place,
  two `under_init` refusals, the chain, and a detached `cat`; the harness
  requires the loader's refusal of each B's first spawn. sshd, netd and the
  staged key leave proctreecase. launcher_refusals loses the no-parent arm.
- Prose the review flagged is deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
`ptr::write_volatile` checks alignment only, so a null write reaches the CPU
under the guest profile's debug assertions; the unmapped constant and its
claim otherwise go.

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 ready for review October 1, 2026 06:43
@Japabu Japabu changed the title A parent's end takes its children down: every end walks its subtree and is published after it A parent's end takes its children down: every end walks its subtree and is published after it, and a spawn past its admission lands Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 6cb8071c7 against .claude/agents/reviewer.md, round 2. Delta 81ff55022..6cb8071c7, merge 90a9937 excluded.

CI: no check at 6cb8071. The PR is CONFLICTING with main, so ci.yml's pull_request run never fired. host was skipped (draft) at 672f04b and 81ff550, so it has never run on this PR, and no guest has run at any head. That alone is NOT READY FOR REVIEW; reviewed as briefed.

Growth: git diff --shortstat origin/main...HEAD: 44 files, +1826 −242.

  • Production +694 −125: kernel +291 −86; toyos-proclife tree/lib/table/teardown +223 −6; ABI, SDK, init, shell, toybox and system.toml +180 −33.
  • Tests, model, harness and controls +1129 −63. Track +2 −53.
  • Fork: 40470840283 adds +15 −9 to 336c8a8e141's +147 −47.

Round 1

  1. CLOSED. Admission::land (process.rs:868) has no error arm, so nothing refuses after commit moves handles. The body measures L12 red at 81ff550 (exit 101) and green now. The kernel half is BLOCKER 1.

  2. CLOSED on its merits.

    • compare/aca5f527fcb...40470840283: ahead 2, behind 0. wt-toyos-proclife4's tip is the gitlink, and no lockfile names the fork. A pin of main's pin plus this PR's own commits couples no other PR's std.
    • Fork main still carries 336c8a8e141, through 2df0b60efd0, without 40470840283 (compare/40470840283...main: ahead 10, behind 1).
    • A bump to fork main after this lands fails to compile, because 336c8a8e141 calls .map on this_process(), which now answers &'static Process. That leftover state is loud, not silent. See NOTE.
  3. CLOSED (fork 40470840283):

    • a missing self panics (toyos/src/endow.rs:323);
    • a refused place dup is answered (toyos.rs:515);
    • five provides plus a place answer InvalidInput (:497).

    The fallbacks left are NOTEs.

  4. CLOSED. a_published_child_leaves_its_parents_children reds under the retain deletion: exit 101, the one failure, per the body.

  5. CLOSED. No #[cfg] arm of the four features remains. The forget planted in the five cfg!() arms gave --clippy exit 1 at the five named sites, per the body.

BLOCKER

  1. kernel/src/process.rs:876 and kernel/src/loader/mod.rs:676-678: no test reaches the claimed-as-it-lands path, round 2's kernel answer to B1.
    • The model computes its own retires (interleave.rs:258), and no guest orders a kill between admission and landing.
    • Patch: (inserted, tree::Landed::Claimed) => (inserted, Vec::new()),. Under it the child runs on, claimed and never retired, and its place is never published. Every test stays green.
    • Required: a test that reds under that patch. One way: a guarded boot actuator that kills the place between commit and land for one marked spawn, and a guest arm whose waits on the child (137) and on the place both answer.
  2. kernel/src/loader/start.rs:80-83 and toyos-abi/src/syscall.rs:471,484: the endowment-vector refusals this change adds have no test, on the ABI. They are a caller's self label and the 32nd entry.
    • These patches each pass every test: delete start.rs:80-83; set MAX_SPAWN_ENDOWMENTS = MAX_ENDOWMENTS.
    • Required: arms in abuse_handle_table, beside its repeated-endowment refusal (:120), each red under its patch:
      • an entry labelled self naming a fresh pipe end answers InvalidArgument, and the end is still held;
      • 32 entries naming distinct TRANSFER handles answer InvalidArgument, and 31 start.
    • This is the brief's question: BLOCKER, not NOTE.
  3. kernel/src/process.rs:860: Admission::ask reserves the pid before the build, so every spawn the build refuses burns one.
    • A spawn of a missing path costs one VFS lookup. Pid + Pid (toyos-abi/src/lib.rs:53) panics at u32::MAX under the kernel's overflow-checks = true (kernel/Cargo.toml:387).
    • On main, only a landed spawn consumes a pid (insert_with, origin/main loader/mod.rs:649). This branch moves a userland-driven kernel panic from 2^32 completed spawns to 2^32 failed ones.
    • Required: take the pid at the point of no return (loader/mod.rs:597), in a hold before ProcessObject::new, not in ask. Record the pre-existing overflow in issues/.

NOTE

  • Fork main (dc36cdce3b1) holds 336c8a8e141 without 40470840283. Whoever bumps it next merges 40470840283 in. .claude/agents/implementer.md:41,44 ("never a new branch"; "lands in the same pull request as the bump") contradicts the per-PR fork branch this pin sits on. That is the orchestrator's to reconcile, outside this fence.
  • library/std/src/sys/process/toyos.rs:503-505 (fork 40470840283): slot_map.len() > MAX_LAUNCH_SLOTS => direct(None) is dead by the body's own argument. Delete it.
  • library/std/src/sys/process/toyos.rs:528-535: a stdio slot whose dup fails still turns a launch into a direct spawn, B3's class. It is pre-existing: refuse it or record it.
  • library/std/src/sys/process/toyos.rs:515: putting direct(None) back for a refused place dup passes every test. Test it with under(h) on a process handle carrying WRITE and not DUP: refused now, spawned directly before.
  • toyos-proclife/src/tree.rs:82: Admitted::place has no caller. Delete it.
  • kernel/src/syscall/proc.rs:64-70: an install failure kills the child and answers a refusal after commit moved the endowments. The body's "leaves its caller's table as it was" does not hold there. It is pre-existing and rare, since the moves free slots.

REMOVE

  • toyos-proclife/src/tree.rs:12-13: "It is the last refusal: past it the spawn moves its caller's handles, so it lands." False: the build and commit refuse past admission.
  • toyos-proclife/src/tree.rs:31: ", which runs with preemption off". False since preempt_off went.
  • kernel/src/loader/mod.rs:600-601: "commit's own ? is different — reachable only if the caller raced its own spawn, and fatal to it, not a refusal". False: start.rs:81 refuses with no race.
  • PR body: "Admission is the last refusal" (false as above); "## Round 1, finding by finding" and "## Other open PRs", which are review chronology and cross-PR status in main's merge record.

Asked

  • Walk: no defect found.
    • Every kernel claim reads children in its own hold (tree::claim) or claims a fresh child (land_child). claim_teardown has no other production caller.
    • A walker's own retire is sticky and taken only at exit_to_user (scheduler.rs:378), so a yield never abandons a walk.
    • Pids never reissue, so a stale owed pid claims nothing.
  • "No test reaches that refusal": BLOCKER 2.
  • Ladder (host = toyos-proclife and --ci host's SDK step):
    • process_tree, B killed: host owns the claim, the children, the count, the order and admission Gone.
      • Host does not reach kill_process→walk, init spawning under a launch's place, under_init, Gone across kernel/init/std, or the loader's Admission drop.
      • Keep.
    • B CPU fault: no host reaches fatal_exception→exit→walk, the one entry from an exception frame. Keep.
    • MANAGE-only place: kernel spawn_place/get_sent; no host. Keep.
    • under_init undeclared and extra slot: std's ToyOS code; no host. Keep.
    • Chain: host reaches MAX_DEPTH. It does not reach depth through launch places, or one kill ending 64 levels. Keep.
    • detach: shell builtin to std under_init; no host. Keep: it is the only coverage of detach.
    • launcher_refusals pipe as place: kernel get_sent and init surviving; no host. Keep.
    • No-parent launch: host now (a_request_names_its_parent_by_one_word). Nothing reaches init's refusal of it, as round 1 ruled.
    • BLOCKERs 1 and 2 add guest arms: no host harness reaches commit or the loader.

SEND BACK

#639 deleted three guest tests this branch had changed, each with the issue
that records the commit restoring it. Every hunk of this branch's side is
accounted for:

- `device_claim_lifetime.rs` (51cc87f) and `handle_kill_policy.rs`
  (6b7da44): this branch's one hunk in each set the new `SpawnArgs::place`
  to `HANDLE_INVALID`. It goes with the file. Reverting either deletion onto
  this tree fails to compile until that field is set, which is loud.
- `launcher_refusals.rs` (4c19146): this branch's hunks were
  `parent: Parent::Init` in its three `Launch` literals, the paragraph it
  deleted from `the_kernel_answers_rather_than_faults`, and the arm
  `a_place_that_is_a_pipe`. The first two go with the file. The arm moves to
  `process_tree` as `a_pipe_is_no_place`: `tests/proctreecase` has the
  launcher it needs, and the arms after it are init answering the next
  launches, which is what the old arm's position asserted.
- `src/build.rs`: main took `tests/quiescecase` and `tests/quiescelastcase`
  out of `ALL_CONFIGS`; this branch's `tests/proctreecase` row stays.
- `tests/toyos.rs`: main took `esp_files` out of `RUST_SKIP` and
  `launcher_refusals` out of `MACHINE_TESTS` and `CARRIES`; this branch's
  `process_tree` rows stay.

`git grep` finds no `SpawnArgs` literal without `place` and no reference to
the three deleted binaries this branch added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Japabu added a commit that referenced this pull request Oct 1, 2026
…arms alone

The previous pin, f066099a351, merged the fork's moved main and with it
#659's std commit 336c8a8e141, which builds `SpawnArgs { place, .. }` and
calls `toyos::endow::this_process()`: neither exists in this tree, so the
toolchain did not build here.

The pin is now 61adcea7362 on the fork's `wt-toyos-m2`: main's pin
aca5f527fcb, #650's 9151571cae9 (std's `aligned_alloc`, which this branch's
libc relies on), the `configure_cmake` arm that names CMake's system `ToyOS`
(25da73337eb), `src/llvm-project` at ceaf0fbb8 (`bit.h`'s `<endian.h>` and
`is_local_impl`'s arms), and two reverts. `clang-tblgen` from an external
host LLVM (61f2a91fffa, df7bd9c942e) and LLD only beside the target's own
`llvm-config` (d622a37189e) were cross-platform changes to bootstrap, made
to suit the store's host LLVM: a bootstrap build that names no
`llvm-config` for the build triple needs neither, because it builds that
triple's LLVM itself, with `clang-tblgen` in its build directory and no
`lld` beside its `llvm-config`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Japabu and others added 4 commits October 1, 2026 09:32
… landing answers what its spawner retires

Answers BLOCKERs 1 and 3 of the second review of #659.

**Pids (BLOCKER 3).** `toyos_proclife::pids::Pids` issues every pid the
kernel hands out, init's, every spawn's and the kernel thread's, and the
process table holds it beside its entries: `ProcessTable` becomes a struct,
and `IdMap::reserve` and `IdMap::fill` go back out.

- A spawn takes its pid at admission (`tree::admit_child`), with the other
  refusals and before anything is built. A spawn refused after that gives it
  back (`tree::refuse_child`), and the next admission takes it. A refused
  spawn spends none, and the pids given back at once are at most the spawns
  in flight at once.
- `Pid::MAX` is never issued: it is the per-CPU word for no process
  (`percpu::current_pid`). Once every pid below it is issued, admission
  answers `Admit::NoPid`, the spawn answers `ResourceExhausted`, and the log
  says `spawn: refused, every pid below 4294967295 is issued`. Before this,
  the counter issued `Pid(u32::MAX)` and the next `Pid + Pid` panicked the
  kernel under `overflow-checks`.
- Why give back, rather than take the pid at the point of no return as the
  review proposed: `PendingHandles::commit` itself refuses (a `self` label, a
  missing `TRANSFER`, no room), so a pid taken before the commit is still
  spent by every refused commit, without bound. Taking it after the commit
  leaves exhaustion to refuse after the caller's handles moved, unless
  admission also counts the spawns owed a pid. A 64-bit pid was not taken:
  std's `process::id()` and `Child::id()` answer `u32`.
- Giving back is sound because no entry ever held a refused spawn's pid. The
  `ProcessObject` the loader makes before the commit dies with the refused
  spawn, so no handle, no node's children and no walk names that pid. A pid
  an entry held is never issued again, so a pid a walk carries across a lock
  release still names its process or nothing.

**The landing answers the retires (BLOCKER 1).** `tree::land_child` now
answers the threads its spawner retires: every thread of a child claimed as
it lands, and none of any other. `tree::Landed` goes. The model's spawner
posts what the landing answers instead of computing its own, so the decision
is the host's to check. `Admission::land` has no branch left: it maps the
answer through `scheds`, which every claim's retires also go through, and the
loader posts the result after the table lock is given up.

- New control `mutate-landed-child-retires-nothing`: the landing claims the
  child and answers nothing to retire. `cargo test -p toyos-proclife --features
  mutate-landed-child-retires-nothing` exits 101, with three FAILED:
  `a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it` and
  `a_spawn_racing_the_kill_of_its_own_spawner` ("pid 1 was claimed for
  teardown and never published an exit", and pid 3), and
  `a_child_landed_under_a_place_claimed_since_its_admission_is_claimed_with_it`.
  `src/ci.rs`'s `CONTROLS` row demands the first.
- The review's kernel patch has no site left. Its nearest forms are measured
  as checked patches against `cd kernel && cargo check`, which builds with
  `-Dwarnings`: `(inserted, Vec::new())` in `Admission::land` exits 101
  (unused `retire` and `pid`), and the loader's post loop deleted exits 101
  (unused `retire`). The tree was restored clean after each.

**Tests.** `pids::tests`: pids from 0 in order; the last is `u32::MAX - 1`
and none follows it; a pid given back is the next taken. `tree::tests`'
`a_spawn_past_the_last_pid_is_refused_and_a_refused_spawn_spends_none`: with
two pids left, a refusal after the last gives it back three times, the
landing takes it, `NoPid` raises no hold, and init publishes once its child
does. As checked patches, each restored:

- `refuse_child` without its `give_back`: `cargo test -p toyos-proclife`
  exits 101, that test FAILED.
- `Pids::take` without its `Pid::MAX` stop: exit 101, both `pids` tests and
  that test FAILED, each on the overflow panic.

**REMOVE.** The review's three lines go: `tree.rs`'s "It is the last refusal"
sentence and "which runs with preemption off", and the loader's clause on
`commit`'s `?`. `tree::Admitted::place` (NOTE, no caller) goes; `Admitted`
gains `pid`.

`issues/kernel/a-process-that-starts-four-billion-threads-panics-the-kernel.md`
records the same overflow for a thread id, as the review asked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…and std refuses a launch whose stdio it cannot duplicate

Answers BLOCKER 2 and the NOTEs of the second review of #659.

**`abuse_handle_table` (BLOCKER 2).** Two arms beside the repeated-endowment
refusal, each spawning a copy of the test binary that exits at once:

- An entry labelled `self` naming a fresh pipe's write end answers
  `InvalidArgument`, and that end is still this process's: a byte written to
  it is read from the other end. Under the review's patch (`start.rs`'s
  `self` check deleted) the spawn starts and `expect_err` panics.
- `MAX_ENDOWMENTS` entries naming 32 distinct pipe ends, which carry
  `TRANSFER`, answer `InvalidArgument`; the first `MAX_ENDOWMENTS - 1` of them
  start a child that exits 0. Under the review's patch
  (`MAX_SPAWN_ENDOWMENTS = MAX_ENDOWMENTS`) the 32 start and `expect_err`
  panics. The arm counts with `MAX_ENDOWMENTS`, the table's size, so that
  patch cannot move the test with it.

**`process_tree`.** `a_place_without_dup_is_refused` (NOTE on fork
`toyos.rs:515`): a launch under this process's `self` narrowed to `WRITE`
answers `PermissionDenied` and starts nothing. With `direct(None)` back for a
refused place duplicate, the child would be spawned directly under the
place, which the kernel accepts, and the arm panics.

**std** (fork `wt-toyos-proclife4`, d1b9f2eae3c on 40470840283):

- A stdio slot whose duplicate the kernel refuses answers that refusal
  instead of turning the launch into a direct spawn (NOTE on `toyos.rs:528-535`).
  No arm tells the two apart: for a stdio handle without `DUP` the direct
  spawn's own slot duplicate is refused `PermissionDenied` too, so the
  difference shows only to a caller whose table is full, where the direct
  spawn started a child holding the caller's namespace.
- The `slot_map.len() > MAX_LAUNCH_SLOTS` fallback goes (NOTE on
  `toyos.rs:503-505`): `launch` is reached only with no extra slot.
- `CommandExt::under` says a launched child's place needs `DUP` too.

`issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md`
records the NOTE on `proc.rs:64-70`: a pre-existing refusal after the
commit, off this stage's path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The doc says its questions are the whole of the crate; `pids` is one now.

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

`ProcessTable` holds its entries in a `HashMap<Pid, ProcessEntry>` of its own
now, where the `IdMap` it replaced was declared in `id_map.rs`.
`every_hashed_kernel_container_is_declared_with_a_kernel_minted_key` was
red on it in `cargo run -- --ci host` (exit 1, "kernel/src/process.rs holds
[\"HashMap<Pid, ProcessEntry>\"] and src/kernelkeys.rs declares no hashed
container there").

The trace the row owes: `ProcessTable::insert` keys by the entry's pid,
which is its `ProcessObject`'s. The two `ProcessObject::new` calls take that
pid from `toyos_proclife::Pids::take`: `loader::spawn` from its admission
(`tree::admit_child`), and `sched::kthread::spawn` directly. No caller
chooses one; a pid given back is one `take` issued.

`cargo test --lib kernelkeys`: exit 0, 4 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title A parent's end takes its children down: every end walks its subtree and is published after it, and a spawn past its admission lands A parent's end takes its children down: every end walks its subtree and is published after it, a spawn past its commit lands, and a refused spawn spends no pid Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator guest runs at d69b80a75 (logs orch/logs/proclife4-*.log):

job expected exit
negative control (process_tree) 1 1
whole suite (pre-#660, 470 tests) 0 1

Whole: 468 passed, 2 failed. Both reds are this branch's:

  • abuse_handle_table exit 139: handle fault: pid=20 tid=0 syscall=10 a handle closed at an earlier generation, right after the new arm (an endowment labelled self and 32 entries are refused, and 31 start).
  • pkg_install_gbae: pkg-launch: started /apps/gbae/gbae as pid 21, the launcher exits at 1.918 and exit: gbae pid=21 code=137 follows at once, so the compositor never counts a window. A launched app now dies with the program that started it; pkg-launch (and any real launcher) must launch through init or detach, or the design must say why not.

Japabu and others added 2 commits October 2, 2026 10:10
The dup2 arm filled every slot from 3 up and then closed `RawHandle(slot)`,
on the premise that each of those slots was still at generation 0. The two
endowment arms added before it (a `self` label refused, MAX_ENDOWMENTS entries
refused and one fewer started) open and close pipe ends in exactly those
slots, so the slots dup2 fills are past generation 0: dup2 answers the slot's
own generation (`HandleTable::install_at`), the bare index named an earlier
one, and the first close was a handle fault, exit 139.

The arm now keeps each handle dup2 answers and closes those.

Measured under QEMU, `cargo test --test toyos-build -- abuse_handle_table`:
exit 1 at d69b80a (the guest's exit 139, "handle fault: ... syscall=10 a
handle closed at an earlier generation"), exit 0 with this commit.

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

The round-2 review's first BLOCKER: nothing booted reaches the path where a
spawn's place is claimed between the spawn's commit and its landing, so the
kernel's half of it (`Admission::land` mapping the answered threads to their
scheduler records, and the loader posting those retires) could be deleted with
every test green. The decision is `toyos_proclife::tree::land_child`'s and is
host-tested; the kernel performing it was not.

That window is the loader's own and no caller can order a kill inside it, so a
test actuator does: `debug_action::KILL_PLACE_AS_SPAWN_LANDS` (22, never
assigned) marks the caller's next spawn, and on the `test-actuators` kernel the
loader kills that spawn's place after the commit and before the landing. The
kill is `kill_process`'s own body, now `process::kill(pid)`, and the landing is
the shipped one.

`spawn_lands_claimed` (an `ACTUATOR_TESTS` member, so it rides the shared boot
on the kernel that carries `SYS_DEBUG`): the test starts a place that hands it
its `self`, marks its next spawn, and spawns a child that parks under that
place. The spawn answers the child; a second spawn under the place answers
`Gone`, which is the kill having happened; the wait on the child and the wait
on the place both answer 137. A child claimed and never retired parks on, and
holds its place unpublished, so neither wait answers.

Measured under QEMU: `cargo test --test toyos-build -- spawn_lands_claimed`
exit 0.

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
…o is the tooling track's

Answers the review of c280ece and takes two answers the owner gave on
2026-10-02, after that commit was written.

The owner's answers:

- The development host builds cargo from the Rust fork and uses no other.
  The notes this branch was written from recorded the opposite as a
  consequence of accepting cargo's C dependencies, and the owner has overruled
  it. The bullet "The host builds with the cargo rustup ships. #629 ... is
  reworked to keep rustup's" goes; the tooling track's cargo sentences on main
  stand and are not repeated here.
- OpenSSL's build on the host runs under the host's own make and shell, and
  openssl-src is not forked for it. The track says so in one sentence, beside
  the question it bounds: what is open until M4 is how cargo's OpenSSL is
  built for ToyOS. The host tools themselves are declared in
  the-build-runs-host-tools-outside-rust-and-qemu.md, which is #629's change.

The review's findings:

- "cargo keeps its eight C libraries ... and nothing replaces them with Rust"
  said more than was decided and contradicted the open rustls-backend
  question. The bullet is now "cargo's C dependencies are accepted", and the
  eight are named by the stage that cross-builds them.
- flock leaves the libc stage. No library cargo builds for ToyOS calls it:
  over the eight crates' sources, `rg '\bflock\s*\('` finds a call in two
  places. SQLite's is inside `#if SQLITE_ENABLE_LOCKING_STYLE`, which
  sqlite3.c defines 1 only under __APPLE__ and libsqlite3-sys 0.38.1's
  build.rs never sets; nghttp2's is in third-party/mruby, which
  libnghttp2-sys 0.1.13's build.rs does not name.
- setvbuf gets the caller the stage did not name: curl's TLS key log
  (lib/vtls/keylog.c).
- The landing queue (#650, #659, #661 "open", "in this order") and "which
  main does not have" turn false at a landing that need not touch this file,
  and go. The stage keeps the libc its gaps were measured against, pull
  request #650's at 15625e0, and says of that libc what it said of main's:
  `git grep -w` for setvbuf, socketpair and select over userland/libc at
  15625e0 matches nothing.
- "The plan's stages", "the plan sets no order" and "the plan does not say"
  pointed at a document the tree does not hold. Each is written as open.
- Waiting on stage 3 of the child-process track and on /dev/null is said of
  the tools written in C or C++, as it was measured, and no longer of brush
  and uutils, which are Rust.
- "C and C++ are accepted" becomes the owner's word, C.
- The child-process track's sentence no longer repeats why select passes
  FD_SETSIZE; this track says it beside the open question.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu and others added 3 commits October 2, 2026 10:39
`pkg_install_gbae`'s client launched gbae and exited at once, on the premise
that a launched program outlives the program that started it. Under this
branch a launch's child is its launcher's, so gbae ended (137) with the client
and the compositor never counted its window.

The client now launches with `CommandExt::under_init`, the one way to outlive
a starter. The launch still goes through init's launcher and the `/apps` row,
so the test's subject and its refusal arms are as they were.

Measured under QEMU, `cargo test --test toyos-build -- pkg_install_gbae`:
exit 1 at d62803b ("gbae started and the compositor never counted a
window", `exit: gbae pid=21 code=137`), exit 0 with the launch under init.

Whether the shipped launchers (the compositor, the terminal, sshd) should do
the same until the track's stage 5 gives a login its session is the owner's
question, and nothing of theirs changes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
#660 cut the guest suite to what no cheaper tier reaches and moved the shared
boot to the T14; #678 rewrote the role prompts and the worktree commands.

Conflicts, hunk by hunk:

- `tests/toyos.rs`: main's harness, with this branch's registrations re-made in
  its shape. `process_tree` was a QEMU machine test on `tests/proctreecase`; it
  is now a `METAL` row on that config (`PROCTREECASE`), its binary on
  `RUST_SKIP`, and its judge (`process_tree`) reads off the stick what the QEMU
  arm read off the console: the job's exit, the loader's refusal of
  `/system/bin/no_such_program` once per B, and one depth refusal naming 65.
  `spawn_lands_claimed` stays an `ACTUATOR_TESTS` member, so it rides the
  shared boot on the kernel that carries `SYS_DEBUG`. The `Sched` and `CARRIES`
  rows have no table left to sit in.
- `src/build.rs`: main's list of boot configs, plus `tests/proctreecase`.
- The child-process track: main's text, with stage 4 deleted as this branch
  deletes it. Main's stage 6 gained "A quit reaches the process's subtree by
  stage 4's walk"; the citation of the deleted stage goes and the sentence
  stays.
- `blockd_io.rs`, `spawn_cwd.rs` (modify/delete): this branch's hunk in each
  was the `place` field `SpawnArgs` gained. Main deleted both tests; the hunk
  has nothing left to adapt.
- `compositor_client_death.rs` (modify/delete): this branch's hunk placed the
  test's relay under the test's root, because the relay outlived the creator
  that started it. Main deleted the test; nothing is left to adapt.
- `pkg_launch_gbae.rs` (modify/delete): this branch's hunk has the client start
  gbae under init. Main deleted the client with `pkg_install_gbae`, which the
  guest-suite track brings back as a metal row from main before the cut, where
  the client still exits over a child that now ends with it. That track's
  `pkg_install_gbae` item now says the client starts gbae under init.

Not a conflict, and broken by the merge: `src/ci.rs`'s `CONTROLS` rows take a
`Verdict` on main, so this branch's five `toyos-proclife` rows name their
tests as `Fails(...)` with the module path, and the landed-child control names
its `tree` test as well.

Filed: `issues/build/a-metal-judge-takes-a-names-lowest-pid-for-the-job.md`.
`Readback::exit_code` takes a name's lowest pid for the job, and this branch's
kernel gives a refused spawn's pid to the next admission.

Before this commit, on the merged tree: `cargo run -- --build-only` exit 0,
`cargo test --test toyos-build -- --list` exit 0, and `--metal --list` for
`process_tree`, `abuse_handle_table` and `spawn_lands_claimed` exit 0 each
(one boot each: `proctreecase`, `shared`, `shared-debug`), which builds every
guest binary.

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

Main moved under this round three times: #669, #650 and #653. The merge before
this one, 54c375a, took main at 74a2e70 (#669), not at de5f63c as its
title says: the shared `origin/main` ref had been fetched forward between this
round's fetch and its merge. This one names its commit.

The one conflict is the `rust` gitlink. Main pins 3f6050fc829 (#650); this
branch pinned d1b9f2eae3c, its three std commits over the pin before, on a
fork branch of its own. Neither contained the other. The gitlink is now
6c7f996a4fe, the merge of d1b9f2eae3c onto 3f6050fc829: main's pin and this
branch's three commits, and nothing else. The two sides share no file. It is
on the fork's `main` through the merge 012fdce3c79, which also brings that
branch the two of this branch's commits it lacked.

`kernel/src/loader/mod.rs`, `tests/toyos.rs` and `toyos-abi/src/syscall.rs`
merged without a conflict.

Before this commit, on the merged tree: `cargo run -- --build-only` exit 0,
with the sysroot built from the fork at 6c7f996a4fe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
The first merge of this round wrote what the restored `pkg_install_gbae` row
needs into the guest-suite track's item for it. That track is the
orchestrator's, and `issues/README.md` files a new fact as a new file: the
track is as main has it again, and
`issues/build/pkg-install-gbaes-launch-client-exits-over-a-child-that-ends-with-it.md`
carries the fact, its two QEMU exits and the commit that holds the fixed
client.

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

Round 3's QEMU arms, taken before the merge of #660 moved the shared boot to the T14. Each is cargo test --test toyos-build -- <filter> in /Users/jan/Dev/jan/toyos-proclife4; a mutation is a checked patch (git apply --check), run, and reverted, the tree clean after. Logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/659-round/.

arm tree filter exit what it said
red d69b80a abuse_handle_table 1 guest exit 139, handle fault: pid=9 tid=0 syscall=10 a handle closed at an earlier generation
green d62803b abuse_handle_table 0 PASS abuse_handle_table
mut-selfcheck d62803b abuse_handle_table 1 guest exit 101, an endowment labelled self must be refused: RawHandle(4103)
mut-spawnmax d62803b abuse_handle_table 1 guest exit 101, a spawn carrying MAX_ENDOWMENTS entries must be refused: RawHandle(4133)
green 0ee4b44 spawn_lands_claimed 0 PASS spawn_lands_claimed
mut-landing-nokill 0ee4b44 spawn_lands_claimed 1 guest exit 101, the kernel did not kill the place of the marked spawn
mut-landing-land 0ee4b44 spawn_lands_claimed 1 timed out after 300s: the wait on the child never answers
mut-landing-post 0ee4b44 spawn_lands_claimed 1 timed out after 300s: the wait on the child never answers
red d69b80a pkg_install_gbae 1 gbae started and the compositor never counted a window, exit: gbae pid=21 code=137
green ad21ebf pkg_install_gbae 0 [pkg] gbae opened a window through the /apps row alone
whole suite ad21ebf none 0 test result: ok. 471 passed, 471 total (416.6s)

Under each of the three mut-landing patches, cd kernel && cargo check --features test-actuators exits 0: each builds.

mut-selfcheck (the review's first endowment patch):

--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -77,10 +77,6 @@
             if end > labels.len() {
                 return Err(SyscallError::InvalidArgument.into());
             }
-            // The kernel's own, which a caller's of that name would shadow in the child's lookup.
-            if &labels[label_off as usize..end] == SELF_LABEL.as_bytes() {
-                return Err(SyscallError::InvalidArgument.into());
-            }
             // Checked before any removal, so a missing `TRANSFER` refuses the spawn instead of leaving a hole.
             let rights = data.handles.rights_of(handle)?;
             if !rights.contains(Rights::TRANSFER) {

mut-spawnmax (the review's second):

--- a/toyos-abi/src/syscall.rs
+++ b/toyos-abi/src/syscall.rs
@@ -468,7 +468,7 @@
 /// truncated — the widest manifest row plus stdio.
 pub const MAX_ENDOWMENTS: usize = 32;
 /// `(label, handle)` pairs one spawn may carry: the kernel adds [`SELF_LABEL`].
-pub const MAX_SPAWN_ENDOWMENTS: usize = MAX_ENDOWMENTS - 1;
+pub const MAX_SPAWN_ENDOWMENTS: usize = MAX_ENDOWMENTS;
 /// `(child slot, parent handle)` pairs one spawn may carry.
 ///
 /// **Derived rather than chosen.** A slot map installs into the child's table,

mut-landing-nokill (the actuator marks and kills nothing):

--- a/kernel/src/process.rs
+++ b/kernel/src/process.rs
@@ -1748,7 +1748,7 @@
     use core::sync::atomic::Ordering::Relaxed;
     let Parent::Under(place) = parent else { return };
     if MARKED_SPAWNER.compare_exchange(current_process().0, Pid::MAX.0, Relaxed, Relaxed).is_ok() {
-        kill(place);
+        let _ = place;
     }
 }
 

mut-landing-land (Admission::land answers its spawner no thread to retire):

--- a/kernel/src/process.rs
+++ b/kernel/src/process.rs
@@ -908,7 +908,8 @@
         let admitted = self.0.take().expect("Admission: landed once");
         let pid = admitted.pid();
         let (inserted, retire) = tree::land_child(table, admitted, KILLED_EXIT_CODE, insert);
-        (inserted, scheds(table, pid, retire))
+        let _ = (pid, retire);
+        (inserted, Vec::new())
     }
 }
 

mut-landing-post (the loader posts no retire):

--- a/kernel/src/loader/mod.rs
+++ b/kernel/src/loader/mod.rs
@@ -675,9 +675,7 @@
     drop(guard);
     // Its parent was claimed while it was built, and its walk has passed: the
     // child is ended as that walk would have ended it, and the spawn answers it.
-    for sched in &retire {
-        scheduler::post_retire(sched);
-    }
+    let _ = retire;
 
     let t3 = crate::clock::nanos_since_boot();
     log!("spawn: {} pid={} tid={} dst={} base={:#x} entry={:#x} root={:#x} symbols={}KiB (layout={}ms relocs={}ms deps={}ms tls={}ms total={}ms)",

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 evidence at 1e9cb8c77, run by the orchestrator: the three boots the round requested, 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).

boot image sha256 armed toyos-metal boot test
proctreecase 475380c696a0323291a4b63e0b2b39a7247ce23dbed64ba1a51a627734bb0f1d boot-deadline=120000 EXIT=0, verdict passed 1153 ms test_rs_process_tree exit=0
shared 3a8b770fc281d34a864be5d46ae9f6c5d45722cb01cd8b784f7ea5ba4677fa75 boot-deadline=120000 EXIT=0, verdict passed 1152 ms test_rs_abuse_handle_table exit=0
shared-debug 9984c2ea236327ba513b1832f46d8378ef6eb18973899e8a77df44d9473bc490 boot-deadline=120000 EXIT=0, verdict passed 1152 ms test_rs_spawn_lands_claimed exit=0
  • toyos-fat32-check: the log partition's 35651584 bytes check out, on all three.
  • These are the head's green arms only; no mutation was booted on the machine.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 1e9cb8c77 against .claude/agents/reviewer.md, round 3. Delta 6cb8071c7..1e9cb8c77; the merges 80be946, 54c375a and 94d22fe read for their resolutions, and the fork at 3f6050fc829..6c7f996a4fe.

CI: host is skipped at 1e9cb8c (draft) and has never run on this PR. Local at this head (659-round/final-gates.exits, tree clean before and after): cargo run -- --ci host EXIT=0 ("Host: 64 step(s), all green", the five toyos-proclife controls each reaching its verdicts), cargo run -- --build-only EXIT=0, cargo test --test toyos-build EXIT=0 (21 passed).

Growth: git diff --shortstat origin/main...1e9cb8c77: 44 files, +2344 −255.

  • Production +852 −143: kernel +358 −105; toyos-proclife tree/pids/lib/table/teardown +308 −6; ABI, SDK, init, shell, toybox and system.toml +186 −32.
  • Tests, model, harness and controls +1394 −59. Issues +97 −52. Fork 3f6050fc829..6c7f996a4fe: +152 −50.
  • Production is 158 lines over round 2's +694: Pids, the table struct, NoPid and the actuator, which are the answers to round 2's BLOCKERs 3 and 1. Accepted. The deletion owed is the first NOTE.

Round 2

  1. CLOSED. spawn_lands_claimed behind KILL_PLACE_AS_SPAWN_LANDS. QEMU at 0ee4b44 (comment 5948957905, 659-round/pre-landing-muts.exits): green exit 0; mut-landing-land exit 1 and mut-landing-post exit 1, each at the 300 s ceiling; mut-landing-nokill exit 1 (guest 101); each patch builds (cargo check --features test-actuators exit 0). T14 at this head, shared-debug: the place (pid 10) and the child (pid 11) both code=137, test_rs_spawn_lands_claimed exit=0 (kernel.log:368-383).
  2. CLOSED. The two arms in abuse_handle_table. QEMU at d62803b (659-round/pre-arms.exits): green exit 0; mut-selfcheck exit 1 (101, "an endowment labelled self must be refused"); mut-spawnmax exit 1 (101, "a spawn carrying MAX_ENDOWMENTS entries must be refused"). T14 at this head, shared: test_rs_abuse_handle_table exit=0 (kernel.log:362-377).
  3. CLOSED, by another fix than the one named. The named fix is refuted by reading: commit refuses after ProcessObject::new(pid) (loader/mod.rs:590-593, start.rs:81,86,91,97), so a pid taken there is still spent by a refused commit. Instead a refused spawn gives its pid back and Pids::take stops below Pid::MAX.
    • Host at this head: pids::tests and a_spawn_past_the_last_pid_is_refused_and_a_refused_spawn_spends_none ok (final-ci-host.log:3788-3809). At cc49e13, refuse_child without give_back exits 101 and take without its stop exits 101 (proclife4/r3/mut-giveback.log, mut-pidmax.log).
    • T14 at this head: each B (pids 9 and 13) is followed by the loader's refusal of /system/bin/no_such_program, and the next spawn lands as pid 10 and pid 14 (proctreecase/kernel.log:334-339,396-400).
    • Thread ids are recorded in issues/kernel/a-process-that-starts-four-billion-threads-panics-the-kernel.md.

BLOCKER

None.

NOTE

  • kernel/src/id_map.rs:17-20, toyos-abi/src/lib.rs:51-54 — impl IdKey for Pid and impl Add for Pid have no user left — no IdMap<Pid, _> remains and K::ONE was the only place two pids were added; delete both.
  • tests/toyos.rs:2239-2240 — process_reopen's doc now heads process_tree, and process_reopen has none — 54c375a put the new judge between a function and its doc; move process_tree above line 2239.
  • tests/toyos.rs:2244 (process_tree), and the two shared members — no row has been judged: the T14 comment carries toyos-metal's exit and the job's, not the harness's verdict over the readback. With a readback in place cargo test --test toyos-build -- --metal --metal-readback <dir> <row> only judges (tests/common/metal.rs:889-894) and touches no machine; run it for the three rows and put each exit in the body. By reading, process_tree's own three conditions hold on its readback: kernel.log:335,397 (two no_such_program refusals), :709 (one depth refusal, naming 65), :1427 (pid 8, code=0).
  • PR body:60,120 — "No T14 run has been made" and "its judge has never read a stick" are false since comment 5949071455 — replace them with the three boots' exits and the judging exits above.
  • PR body:12,121 — "is the owner's question" and "is the owner's to accept or not" are stale — the owner has ruled it the track's interim until stage 5; say the ruling.
  • PR body:47 — "A launch never turns into a direct spawn when it cannot carry what it was asked to" is false — a request Launch::encode refuses (argv and env past one frame) is LaunchError::NotSent, and library/std/src/sys/process/toyos.rs:568-570 (fork 6c7f996a4fe) answers direct(None), so the child holds its caller's namespace and not its row. It is main's behaviour and round 1's BLOCKER 3's class: file it in issues/, and make the sentence true. Its stdio half ("a stdio slot whose duplicate the kernel refuses") has no measurement: say so under "Unsure of".
  • tests/toyos-rust-tests/src/bin/process_tree.rs:340 — the chain's stop accepts : not found, which is the shell's word for every spawn error (userland/shell/src/main.rs:672,685), so a depth refusal, Gone and PermissionDenied all read "not found" to a user; the T14 boot printed it for the depth refusal (kernel.log:1395). The judge's kernel record is what carries the depth claim. File the shell's defect in issues/.
  • For the orchestrator — the shared boot's other members have no verdict on this kernel at this head. Their one verdict is QEMU at ad21ebf (pre-whole.exit EXIT=0, 471 passed), before the merges brought main's c_aligned_alloc and tinycc 206 to 208, which have never run on this kernel. One whole shared and one whole shared-debug boot at the landing head is that verdict.
  • For the orchestrator — fork main's tip 012fdce3c79 is nine commits past this pin: it also carries M2: CMake's ToyOS in every C sysroot, LLVM's two ToyOS arms, and LLVM, clang and LLD built for a ToyOS host by unchanged bootstrap as far as libc lets it #661's unlanded commits (25da73337eb to 7cd37350b51). The merge 012fdce3c79 itself lands in M2: CMake's ToyOS in every C sysroot, LLVM's two ToyOS arms, and LLVM, clang and LLD built for a ToyOS host by unchanged bootstrap as far as libc lets it #661's bump, not this one, so implementer.md's "lands in the same pull request as the bump" is unmet for that one commit; its delta over the pin is M2: CMake's ToyOS in every C sysroot, LLVM's two ToyOS arms, and LLVM, clang and LLD built for a ToyOS host by unchanged bootstrap as far as libc lets it #661's alone. The pin 6c7f996a4fe is main's 3f6050fc829 plus this branch's three commits (library/std/src/os/toyos/process.rs and sys/process/toyos.rs, nothing else), wt-toyos-proclife4 is gone from the fork, and no lockfile names it. Order the two landings.

REMOVE

  • tests/toyos-rust-tests/src/bin/abuse_handle_table.rs:157-158 — "A slot the arms above closed is past generation 0, so each is closed by the handle dup2 answered." — a false comment corrected instead of deleted; the loop under it says it.
  • PR body:28 "as the second review proposed"; :107 "which the second review's first BLOCKER found no test reached"; :108 "The second review's two endowment patches." — review chronology in main's merge record. Name each patch by what it deletes.
  • PR body:102-104, "This round's two reds…" — the story of two reds this branch made and fixed before landing; d62803b's message and issues/build/pkg-install-gbaes-launch-client-exits-over-a-child-that-ends-with-it.md carry them.

What this verdict rests on

  • The T14 at 1e9cb8c (comment 5949071455; readbacks under 659-round/metal-*/, each verdict.txt "passed"): proctreecase with test_rs_process_tree exit=0, shared with test_rs_abuse_handle_table exit=0, shared-debug with test_rs_spawn_lands_claimed exit=0. They are the head's green arms only.
  • Every red arm is QEMU on a tree before the merges: the three landing mutations at 0ee4b44, the two endowment mutations at d62803b, and the stage's negative control at d69b80a (comment 5927569746, exit 1 on "C was not ended, as killed"). Since d69b80a the branch's own kernel change is kill_process's body moved to kill(pid) and the test-actuators hook.
  • No mutation and no negative control has been booted on the T14.

LAND AFTER NAMED CHANGES

Japabu and others added 4 commits October 2, 2026 11:47
No IdMap is keyed by a pid since the process table took its pids from
toyos_proclife::Pids, and IdMap's `next + K::ONE` was the only place two
pids were added. `impl IdKey for Pid` (kernel/src/id_map.rs) and
`impl Add for Pid` (toyos-abi/src/lib.rs) have no user left and go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…reecase boot is recorded

The merge 54c375a put process_tree's judge between process_reopen and
its doc, so the doc headed process_tree and process_reopen had none.
process_tree, with its own doc, now stands above it.

abuse_handle_table's comment on the closing loop loses the sentence about
generations: the loop closes what dup2 answered and says so itself.

The record gains the proctreecase boot's three numbers. The harness wrote
them judging process_tree over the T14's readback of 1e9cb8c
(`cargo test --test toyos-build -- --metal --metal-readback <dir>
process_tree`, exit 0): complete_ms 1153, panel_max_us 3773, panel_us
21279. This branch is what adds that boot, so its record lands here. The
judging of abuse_handle_table over the same head's `shared` readback also
wrote three `boot.shared` rows, off a boot carrying that one member; they
are not this branch's and are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
… says "not found" for every spawn error

Both are main's behaviour, found by the third review of #659 and not
changed here.

std answers `LaunchError::NotSent` with the direct spawn, and the one
`encode` refusal std can reach is a request past MAX_FRAME_LEN: that
child holds its caller's namespace and not its row.

The shell discards the error of every spawn and prints `not found`. The
T14's proctreecase boot at 1e9cb8c printed it for the depth refusal the
kernel recorded beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Main gained #643, blockd setting its ring cursors before it answers an
open, with its issues. It shares no file with this branch and the merge
has no conflict. Main's `rust` gitlink is unchanged at 3f6050fc829, so
the branch's pin 6c7f996a4fe stands.

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

T14 evidence at 3fd141985, run by the orchestrator: the six boots the round staged — the whole shared set (shared, shared-2, shared-3), shared-debug, ccorpus and proctreecase — 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).

boot image sha256 armed toyos-metal boot tests ended exit=0
shared 1980bd07136a6adc1a748b7e8299f0b325f08e4e4d17ab6c2ae07d44166b36bd boot-deadline=120000 EXIT=0, verdict passed 1153 ms 38 38
shared-2 c0d0f02523a7710f3ce9c5912069a5f55caea99e01d8bcdc6f5e08df72e30b4b boot-deadline=120000 EXIT=0, verdict passed 1152 ms 38 38
shared-3 5c026c759dbf1fb4ded6924a9807dfeee51625d8d6e049753f83abe6c006a9b8 boot-deadline=120000 EXIT=0, verdict passed 1153 ms 2 2
shared-debug bdbf033d48febde3c10aad8caa8253a55156ce2f43e9adea1435a3c03ba03bae boot-deadline=120000 EXIT=0, verdict passed 1157 ms 4 4
ccorpus cf57d1d158beb37aca5adca21b69371ddb061b759883e061e7b4b08097600763 boot-deadline=120000 EXIT=0, verdict passed 1153 ms 90 90
proctreecase 435583c7aa9404d22ea26e24bb23bfaeea41ead96775fe631c6cb10c06f37227 boot-deadline=120000 EXIT=0, verdict passed 1152 ms 1 1
  • 173 tests ended across the six boots and 173 exit 0; no ===TEST_END line carries another exit.
  • toyos-fat32-check: the log partition's 35651584 bytes check out, on all six.
  • These are the head's green arms; no mutation was booted on the machine. The harness's judging over these readbacks has not been run.

@Japabu
Japabu marked this pull request as ready for review October 2, 2026 11:10
@Japabu
Japabu added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit c1c5048 Oct 2, 2026
2 checks passed
@Japabu
Japabu deleted the wt/toyos-proclife4 branch October 2, 2026 11:20
Japabu added a commit that referenced this pull request Oct 2, 2026
Cargo.lock alone conflicted: both sides kept, main's pcap-file and
byteorder_slice beside this branch's ureq, flate2 and tar trees, and cargo
left the result as it resolves the merged manifests. Against origin/main the
lockfile differs by what it did before the merge, +801 -6.

#659 moves the rust gitlink and the kernel, so the freestanding and sysroot
keys move with it.

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
Main's nightly 36985427800, at 74a2e70, ended its three-hour toolchain step
red: "crates.io holds no toyos-abi of this tree, so no sdk alias can name
it". #650 and #659 had landed meanwhile and their pushes had put newer
toyos-abi versions up, so the tree the nightly checked out was no longer the
one crates.io's newest named. This branch deleted that step and kept the red:
`release_as` ran behind `ci::at_tip`, which refuses with "HEAD ... is not
main's tip" whenever a landing precedes the `release` job, and `alias` kept
the crates.io refusal for a landing whose crates went up after that check.

`sdk_at_tip` is now the release's one decision, taken before anything is laid
out, packed or put up: it reads crates.io, then main's tip. A tree main has
moved past puts nothing up and the job is green, saying so; the tip's nightly
publishes. At the tip the plan it read is the one the alias is written from,
so nothing read later can disagree with it. crates.io before the tip is the
order that matters: a landing whose crates the first read shows has moved the
tip the second read sees, and the other order leaves a landing between the
two reads refused.

What stays refused is the tip's own crates not being up, which publish.yml
owes, and a remote that names no main. A run of an older tree still moves no
alias back, which is what `at_tip` was put there for; `at_tip` is `publish`'s
alone again, and private as on main.

`a_landing_during_the_nightly_puts_nothing_up_and_is_no_failure` lands once
before the release's first read, between its two reads and not at all, with
and without newer SDK crates.

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
Six conflicts.

- rust: the gitlink is 01b8626673f, the fork's main (012fdce3c79, which holds
  main's pin 6c7f996a4fe) with this branch's 61adcea7362 merged into it. Over
  6c7f996a4fe it carries `configure_cmake`'s `toyos` arm and `src/llvm-project`
  at ceaf0fbb844, and nothing else: 012fdce3c79 still held the three
  cross-platform bootstrap commits 61adcea7362 reverts.
- src/sysroot.rs: main's two keys. `clang::CMAKE` joins the sysroot's, beside
  the C++ runtime's options, and `RECIPE` is main's text with CMake's
  description of ToyOS named in it, at a number neither side had.
- src/libc.rs: `build_c` writes the CMake files and then links main's probe.
- src/libcxx.rs: main moved n2 out of the file; the configure through the
  sysroot's toolchain file is this branch's.
- issues/build/toyos-builds-itself.md: main's Decided and To build sections
  whole. Main dropped the signal-set calls from the Compile bullet this
  branch had already replaced, with Configure and Link, by its pointer to the
  bootstrap issue; the pointer stays.
- issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md:
  this branch's body. Main's one hunk took `alarm` and the signal-set calls
  out of a paragraph this branch had deleted; what the build stops on at this
  tree is measured and written in the commits that follow.

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
…among them) into wt/toyos-winitstall

Three content conflicts, each a deletion on main's side of a block this
branch had edited:

- kernel/src/inbox/mod.rs: main deleted `Staged`, `handler-post`'s ring,
  with the actuator (#660). This branch's hunks inside it — the `owed`
  field, `Poll::new`, the `WatchFlags` direction and "fires" for
  "completes" in its doc — adapted it to the ring's new fields and go with
  it. `Inbox::complete` keeps this branch's wording and loses main's
  `raise_if_staged` call.
- kernel/src/watch.rs: main deleted the `handler_post` module, `holding`,
  `note_post` and the `raise_if_staged` call in `IrqLock::with`. This
  branch's one hunk inside it was "fires" for "completes" in the module's
  doc, which goes with it.
- userland/fsd/src/main.rs: main deleted the four test actuators
  (`--end-on`, `--end-at-read`, `--end-at-mount`, `--let-go-at-read`) and
  kept the acceptor probe; this branch deleted the probe and kept the
  actuators. Both deletions stand: no `caps_len`, no `probe` field, no
  actuator field, and `accept` is this branch's.

Two resolutions no marker asked for:

- src/ci.rs: #668 made a control's verdicts `Fails(..)` values, so
  `post-is-an-answer`'s three verdict strings become three `Fails`.
- issues/build: main filed the C++ runtime's scratch removal as an issue
  of its own (`the-cxx-runtimes-scratch-removal-dies-on-a-finder-file.md`)
  beside the sweep's, which this branch had merged into one file. Main's
  two files stand and this branch's file goes.

The `rust` gitlink is main's, `95960d6c2`.

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
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
Japabu added a commit that referenced this pull request Oct 2, 2026
…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
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