Skip to content

inbox: a watch is answered after a look at its object, never by a post - #655

Merged
Japabu merged 21 commits into
mainfrom
wt/toyos-winitstall
Oct 2, 2026
Merged

Japabu merged 21 commits into
mainfrom
wt/toyos-winitstall

Conversation

@Japabu

@Japabu Japabu commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

A reader could stall on readiness it had already used up. A ring answered READABLE for bytes the reader had already read, and its blocking read then waited for good: the terminal stalled this way in Window::recv_event in #638's toolkit_winit_loop. The cause is that an object's post fires every armed poll without reading the object again. So a peer's zero-length write wrote an answer for nothing, and so did a write's post that landed after the reader had taken the bytes and watched again.

That stale answer is now impossible at its source. The kernel writes a watch's answer only in the ring's own inbox_submit, after looking at the object again.

Decisions

  • The fix is in the kernel, not the SDK. A ring carries no readiness, so no userland registry can tell an answer for nothing from an answer for bytes. A token that only a non-blocking read accepts would put the rule on every reader, and would leave libc's poll(), std's Read and every reader outside toyos exposed. The owner ruled that the ABI may change, so the answer is made true where it is written. Struct layouts and syscall numbers are unchanged. OP_WATCH's contract changes (toyos-abi/src/inbox.rs).
  • A post fires a poll, and the submitter answers it (kernel/src/inbox/polls.rs).
    • inbox_submit takes every fired poll and looks at its object. It answers with the directions ready now, or arms a new poll in its place.
    • A blocking read after a ready answer is therefore sound for a handle no other reader drains, so no type has to forbid it.
  • A submitter parks on its polls, and no word beside them records a fire (polls::awake).
    • A fire takes its poll and posts the watch the ring's submitters park on. The submitter, registered on that watch, reads its polls again before it parks. That is the watch's own lost-wake argument (toyos-sched/src/watch.rs).
    • A full completion ring keeps the predicate false: a submitter with a fired poll and no room to answer it would otherwise go round for good.
  • An answer wakes the ring's submitters (polls::complete).
    • A poll one submitter is looking at is hidden from every other submitter of the ring. A second one reads nothing owed and parks, with the fire's wake spent before it registered. The looker's answer is what wakes it.
    • polls::complete writes a completion and then posts the watch the submitters park on. It is the one writer of a completion, for a watch's answer, a NOP, an accept and every refusal; Submitter::answer writes and wakes nobody.
  • A wait that goes round reads its kill (inbox::submit).
    • watch::wait_until returns on a true predicate before it reads the kill, and submit's predicate is true while a poll is owed a look. A peer whose posts kept landing therefore kept a killed thread in the kernel.
    • submit reads the caller's kill beside its deadline, as ops::until_answered does. A peer cannot delay a kill.
  • A handle has one poll, on every path. A watch replaces the handle's earlier poll whether a post has fired it or not, and whether or not a submitter is looking at it: a poll taken for its look stays among the ring's polls, marked, and the look answers nothing and arms nothing once a newer watch holds the handle's place (Polls::settle, Polls::renew). A watch whose object is ready at once is a fired poll like any other. One look therefore answers a handle once, under its newest token.
  • One look is one pass. A pass takes only the polls kept before it began, so a poll it renews waits for the next. A peer that writes no bytes as fast as the submitter looks would otherwise keep the pass on its own handle and off every handle behind it.
  • The log and a console keep the post as the answer (ops::read_posts_are_readiness).
    • The log's unread records belong to its reader's cursor, which the kernel does not hold.
    • A console's watch is the keyboard's while its data comes from the serial line (issues/kernel/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md, whose exit names its test).
    • Poll::posted records which watch posted, so a registrant's own look never counts as a post.
    • Every other kind is looked at through ops::has_data and ops::has_space, which a blocking read or write waits on. A process handle has no read watch, and OP_WATCH on one is refused -NotSupported, as on main.
  • A handle closed since it was watched answers -NotFound at the look. It does not get the handle fault that a submission naming such a handle gets.
  • A full completion ring ends the look. The polls it did not reach stay owed for the next wait, so a ring one thread submits to drops no watch's answer.
  • A ring's completions sit behind a Lock. No interrupt handler reaches them: a handler's fire posts the ring's watch and writes nothing.
  • toyos::poller differs from main's in one doc sentence. fsd's per-acceptor probe, which asked whether an answer was true before accept, is deleted.
  • libc's poll() keeps one watch per fd (userland/libc/src/pollreq.rs). The watch goes under the first entry that names the fd and carries every entry's interest. A second watch on the same fd would replace the first and leave the first entry unanswered.
  • Closed, each by its own exit:
    • issues/kernel/a-zero-byte-pipe-write-wakes-the-readers-watch.md: a wake completes a watch only if its direction is ready, and inbox_empty_write's reader watch stays pending across a write of no bytes.
    • issues/kernel/two-completions-can-name-one-arrival-and-accept-parks.md: a watch withdraws the armed one on its handle whether or not it answers at once (Polls::admit), counted by a_watch_replaces_a_poll_a_post_already_fired.
  • Filed, all four by this diff, and open. Main already has the behaviour the first three describe; the fourth is this change's own:
    • a close of one handle ends every ring's poll on its object, and Poller hands that end out as a token (issues/kernel/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md);
    • a ring handed to another process resolves its watches in the receiver's table (issues/isolation/a-ring-handed-to-another-process-resolves-its-watches-in-the-receivers-table.md);
    • toyos::Poller is Sync, and a watch moves the submission tail in two steps (issues/design-debt/toyos-poller-is-sync-and-a-watch-moves-the-tail-in-two-steps.md);
    • two submitters of one ring race its last completion slot, and the loser's answer is dropped and counted, as every completion written to a full ring is on main (issues/kernel/two-submitters-of-one-ring-race-its-last-completion-slot.md).

Checks

This is a concurrency primitive under the kernel's wait path.

The T14, at b6b61967d

b6b61967d is this branch before the kill's read, the answer's wake in polls.rs and the log's test. The T14 booted three images of it (the boots). The negative control is the whole change reverted: 0c34f5e45 is a31eec595, the main that head merged, plus inbox_empty_write.rs and nothing else.

boot tree members, by name cargo test --test toyos-build -- --metal --metal-readback <dir> <row> over its readback
control 0c34f5e45 inbox_empty_write exit=101, tokens [1, 2] against [2] row inbox_empty_write: exit 1
head b6b61967d abuse_inbox, inbox_cancel_wakes, inbox_empty_write exit=0 row inbox: exit 0
head b6b61967d poll_wake_pipe, poller_capacity exit=0 row poll: exit 0

The T14, at 5f62645ae

The T14 booted four images of 5f62645ae, once each: three of the tree as it stood (the boots) and one with the kill's read reverted (the control). The judge is the same command as in the table above, over each boot's readback.

row tree image sha256 members, by name the judge
kill_ends_every_wait 5f62645ae eba28098612662ece3f4624bd9e3d21d2b4a1de5c3f8cb8f990510c56e557416 kill_ends_every_wait, all six waits, exit=0 exit 0: 1 passed, 0 failed, 1 boot
inbox 5f62645ae 76e624dc09b87214d29967d4cde5bec1c2b07c5617a8f09a7aad15373cb19890 abuse_inbox, inbox_cancel_wakes, inbox_empty_write, inbox_log_post exit=0 exit 0: 4 passed, 0 failed, 1 boot
poll 5f62645ae df3ae50d766dc2d80317655927a1cc8cba06150e47d073e36fa6475a2cf55a64 poll_wake_pipe, poller_capacity exit=0 exit 0: 2 passed, 0 failed, 1 boot
kill_ends_every_wait 5f62645ae and submit reads no kill (k1) 6105b217c4fac8d1087db5c5e33f334159928064374058a4401ca2bd8736c3e6 kill_ends_every_wait exit=101 exit 1: 0 passed, 1 failed, 1 boot

posted-poll's verdict is a duration, which only the T14 may judge, and the fourth boot is its negative control there: k1 reverts the kill's read in inbox::submit and nothing else. From the two kill_ends_every_wait readbacks' kernel.log:

  • With the read: "posted-poll: killing" at 1.221 (:384), exit: … pid=12 code=137 at 1.222 (:396), "posted-poll: a kill ended it" at 1.222 (:401).
  • Without it: "posted-poll: killing" at 1.224 (:384), and at 2.225 "posted-poll: the child still watched its pipe 1s after its kill: the posts held it in its wait" (:390-391).

The T14, at 4695a65fb

The T14 booted six images of the head, once each, staged from the clean worktree and each image's sha256 checked again before it was flashed; each toyos-metal exit 0 (the first three, the second three).

filter boot image sha256 members the row's judge
kill_ends_every_wait shared d926c499877e431159024f2184f886a099f3cdea20bec43c517d25159a399b24 kill_ends_every_wait exit=0 exit 0: 1 passed, 0 failed, 1 boot
inbox shared 420ad0bfc268dfcf38b5fd996f6ed420cec02ae438c9fcbe0af2100ab1e6b737 abuse_inbox, inbox_cancel_wakes, inbox_empty_write, inbox_log_post exit=0 exit 0: 4 passed, 0 failed, 1 boot
poll shared bfb4b2e7dfcedc4ac384687f9cbc24299e6f5fad24b2de67ed507ecf45d93f72 poll_wake_pipe, poller_capacity exit=0 exit 0: 2 passed, 0 failed, 1 boot
sched_stress shared ce24bf2a42f8f6cc901e82cff742abd302cb47c58072971f2102a9650d257518 sched_stress exit=0 exit 0: 1 passed, 0 failed, 1 boot
handle_lifetime shared-debug f30dccabbc1028d60519aad70e60ff5bbeede89e287efc62d812a233793a404a handle_lifetime exit=0 exit 0: 1 passed, 0 failed, 1 boot
soundd_log_stall logstallcase b8813f05efe3140f260ea956f499c82eaf3073674603cd7775831401f0b2964d soundd_log_stall exit=0 exit 0: PASS soundd_log_stall, 1 passed, 0 failed, 1 boot

posted-poll on the head (655-r6/metal-kill/shared/kernel.log:381-401): parked at 1.206, killed at 1.228, "a kill ended it" at 1.229. k1 booted at 5f62645ae alone: the round-6 review's --stat over the wait loop, the park, the scheduler, the post path, kill_process and the test finds the two heads the same source there but for a deleted selftest and the test's one doc clause, and that carries the red to this head (the review).

The head is three commits past 5f62645ae:

This pull request's own diff is the booted one but for that clause: git diff b331934c9 5f62645ae and git diff dd8738302 4695a65fb differ in the comment's line and in three index lines (the diff).

The guest tests

All three are members of the T14's shared boot, by their files alone, and have no QEMU registration.

  • inbox_empty_write: one poller watches two empty pipes, a write of no bytes goes into the first and one byte into the second, and the wait answers the second pipe's token alone.
    • No type reaches it: which answers a wait hands back is a run-time fact.
    • No host test reaches it: sys_write's post, the look's resolve in the process's handle table and pipe::has_data name the kernel and compile in no host crate. The host models drive polls.rs against a fake look.
  • inbox_log_post: a reader arms a watch on the endowed logread capability, ends a process, is handed its token by wait(1, u64::MAX), and reads the kernel's exit: record of that pid.
    • It is the test of the log's arm of the look (ops::read_posts_are_readiness). With SysCap moved to the false arm (e1) the wait never returns.
    • No host test reaches it: klogd's post, the capability's resolve and that match name the kernel. a_post_answers_for_an_object_with_nothing_to_look_at runs the model's own flag.
  • kill_ends_every_wait, the posted-poll wait: a child watches one empty pipe through 256 handles with no deadline. Four threads of its parent write no bytes into the pipe, the roster shows the child out of its park, and it is killed.
    • The posts stop one second after the kill, and the child has to have ended before they do. A kill those posts hold is held for as long as they win a race and no longer, so the harness's deadline cannot see it: with the read reverted, the child ended by itself after seconds.
    • No host test reaches it: the kill mark is the scheduler's, and the loop that reads it is inbox::submit, which names the ring's page and the clock.

Measured in one QEMU guest each

A harness patch, applied, run and reversed by one script, boots tests/testcases with 8 vCPUs and runs one member (local-qemu.patch in the evidence comment). It is in no commit. Each mutation is a checked patch, applied and reversed by the same script, the tree clean after.

member tree result exit of cargo test --test toyos-build -- zz_local_<member>
kill_ends_every_wait 5f62645ae, three runs exit 0 0, 0, 0
kill_ends_every_wait and submit reads no kill (k1), three runs exit 101, "the child still watched its pipe 1s after its kill" 1, 1, 1
inbox_log_post 5f62645ae exit 0 0
inbox_log_post and a log's post is not its readiness (e1) no exit: the harness's 300 s ceiling 1
inbox_empty_write 5f62645ae exit 0 0
inbox_empty_write and the look answers without looking (a1) exit 101, tokens [1, 2] 1
inbox_empty_write and a pipe's post is its readiness (a2) exit 101, tokens [1, 2] 1
kill_ends_every_wait 4695a65fb exit 0, all six waits 0
inbox_log_post 4695a65fb exit 0 0
inbox_empty_write 4695a65fb exit 0 0

The three 4695a65fb rows ran on the same host at load 52 to 57, with the harness patch moved onto #647's tests/toyos.rs (the runs and the patch). The k1 rows read a duration under QEMU. They are the cheap measurement that showed the hold and chose the ceiling; posted-poll's control is the T14's k1 boot above.

How long a peer held a kill, before the arm had its ceiling: the child's roster entry, sampled by a diagnostic that is in no commit, on a 14-core host at load 10.

kernel posting threads the child after its kill
submit reads no kill 6 running in each of 596 samples over 4234 ms, then gone
submit reads no kill 4 running in each of 200 samples over its first 354 ms; that guest's run took 80 s
submit reads the kill 6 gone after 13 ms and 6 samples; that guest's run took 5 s

Commit 5f62645ae's message says the four-thread child was seen running in every roster sample for 70 s; its samples cover the 354 ms of this table, and that is the figure that stands.

Independent oracle: the interleaving models, and Linux's epoll

  • toyos-sched-loom's a_fire_racing_a_submitters_park_is_never_lost covers the park. It compiles the kernel's polls.rs and runs Poll::fire, Polls, deliver and awake against the real watch and park: two interrupt-handler posts fire two polls of one ring while its submitter waits for both answers. commit-ignores-notify is its control, kept red by --ci host.
  • toyos-sched-loom's an_answer_wakes_the_submitter_its_look_hid_the_poll_from covers two submitters. They run on two CPUs against one handler's post of one poll, each waiting for one answer, through the same deliver, awake and complete. A submitter that parks is owed exactly one wake, by the fire or by the answer.
  • kernel-loom's poll_once and toyos-sched-loom's an_end_racing_a_post_* cover the one-shot a fire, a recheck, an end and a withdrawal race for; poll-fire-load-store is their control.
  • kernel-loom's inbox_answer covers which polls are answered and when, in one thread, with a second thread's watch and a peer's post staged inside the look. post-is-an-answer is its control: 3 verdicts red, kept red by --ci host.
  • fs/eventpoll.c's ep_send_events polls every ready item again with ep_item_poll before reporting it, and skips an item whose poll comes back empty; epoll(7): "Modify will reread available I/O." A post with nothing behind it and a post for bytes already taken are the two cases that oracle decides, and inbox_empty_write is the first of them.

Mutations of the host tier

20 runs at 5f62645ae, each a checked patch, built (exit 0), run (exit 101) and reversed, the tree clean after each. b1 ran against both models. The last column names a test each run failed; the patches and every red are in the evidence comments (m1, m2 and the table, the other patches).

what patch red
an answer wakes nobody m1 an_answer_wakes_the_submitter_its_look_hid_the_poll_from
the wake comes before the answer is written m2 an_answer_wakes_the_submitter_its_look_hid_the_poll_from
the park does not read the polls b1 a_fire_racing_a_submitters_park_is_never_lost, a_submitter_parks_only_with_nothing_it_could_answer
a fire posts before it takes its poll b2 a_fire_racing_a_submitters_park_is_never_lost
a replaced poll stays armed c1 a_replaced_poll_is_withdrawn_from_its_watch
a torn-down ring's polls stay armed c2 a_ring_torn_down_withdraws_every_poll
a refusal is not answered c3 a_handle_closed_since_its_watch_is_answered_with_the_refusal
the cap's >= becomes > n5 a_ring_keeps_its_cap_of_polls_and_no_more
a look arms beside a newer watch n4a a_watch_during_a_look_that_finds_nothing_is_the_handles_one_poll
a look answers a replaced poll n4b a_watch_during_a_look_that_finds_bytes_answers_alone
a pass looks again at a poll it renewed pass a_poll_fired_as_it_is_renewed_waits_for_the_next_look
the park ignores a full ring r a_submitter_parks_only_with_nothing_it_could_answer
a renewed poll ends the look p1 a_poll_armed_again_does_not_end_the_look
a watch replaces nothing p2 a_watch_replaces_a_poll_a_post_already_fired
a watch drops every other handle's poll p3 a_poll_left_standing_still_answers
a look has no room check p4 a_full_ring_leaves_a_fired_poll_for_the_next_look
an ended poll is looked at p5 an_ended_source_is_answered_gone_without_a_look
a fire does not record its post p6 a_post_answers_for_an_object_with_nothing_to_look_at
libc's poll watches every entry l1 a_descriptor_named_twice_is_watched_once_for_both_interests

Gates

gate at exit
cargo run -- --ci host, 67 steps 4695a65fb 0
cargo run -- --build-only 4695a65fb 0
cargo test --test toyos-build, the QEMU suite, 25 tests 4695a65fb 0
the 3 QEMU runs of this branch's guest tests 4695a65fb 0 each, tree restored
cargo test -p kernel-loom --test inbox_answer, 16 tests 5f62645ae 0
cargo test -p toyos-sched-loom --test loom_watch, 12 tests 5f62645ae 0
the 20 host mutation runs 5f62645ae 101 each, tree restored
the 11 QEMU runs of the table's 5f62645ae rows 5f62645ae as the table says, tree restored
the T14's judge of kill_ends_every_wait, 1 member 5f62645ae 0
the T14's judge of inbox, 4 members 5f62645ae 0
the T14's judge of poll, 2 members 5f62645ae 0
the T14's judge of kill_ends_every_wait, 1 member 5f62645ae and k1 1
the T14's judges of kill_ends_every_wait, inbox, poll: 1, 4 and 2 members 4695a65fb 0 each
the T14's judges of sched_stress, handle_lifetime: 1 member each 4695a65fb 0 each
the T14's judge of soundd_log_stall 4695a65fb 0

The merges

origin/main at dd8738302 (#647) is merged (4695a65fb): no conflict, and src/build.rs is the one file changed on both sides, in separate hunks. Before it, dc8212c7f (#642, ce815516f): no conflict, and src/ci.rs is the one file changed on both sides, in separate hunks of CONTROLS. Before it, b331934c9 (f2c87688c): no file is changed on both sides. Before that, a31eec595 (9063ab60f): three content conflicts, each main deleting a block this branch had edited: handler-post's Staged ring in kernel/src/inbox/mod.rs, its module in kernel/src/watch.rs, and fsd's four test actuators, where both sides' deletions stand. src/ci.rs's control row takes #668's Fails values, and main's two Finder issues stand in place of this branch's one.

Not sure of

  • posted-poll's red is judged at 5f62645ae alone. No mutation was booted on the merged head; a later merge that moves the wait loop, the park, the post path, kill_process or the test owes that boot.
  • k1's red on the T14 is one boot. How long four threads hold a kill there past the second, and how often, is not measured.
  • polls::awake reads every kept poll under the ring's lock, each time a submitter is about to park. A watch already walks the same list. Its cost on the T14 is not measured.

Net lines

git diff --shortstat origin/main...HEAD: 34 files, +1700 −357. Production +546 −200: the answer moved into the kernel, one poll per handle across a look, the pass bound, the kill's read, and libc's watch plan. Tests and their gates +1076 −92. Issues +78 −65.

🤖 Generated with Claude Code

https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm

…dy spent

`toolkit_winit_loop` went red in #638's whole run 638r4 because the
terminal stopped reading its shell's output after stage 1 and never
started again: the app went on through all 23 windows, its lines piled
up unread in the shell's stdout pipe, and the harness waited for a
`WINIT-LOOP CLOSE-ME` that was sitting in that pipe.

## The cause

A poller's READABLE is a cue to look, not a promise that bytes are
there. `process_watch` (`kernel/src/inbox/mod.rs`) completes a handle
that is already ready at once and leaves the poll an earlier round
registered on it armed; `toyos::poller`'s capacity counts that leftover
answer by design. So a round that registers between a writer's data and
that writer's post reads the data on its own answer, and the leftover
poll then answers again for bytes that are gone. The writer's window is
wide: `sys_write_nonblock` releases its locks after the copy, which is a
preemption point, and only then posts.

The terminal read every answer with a call that waits: `recv_event` on
its window, std's `read` on the shell's stdout and stderr. On the
window that wait is for good. The compositor writes to a window only
after it presents or on input, so after stage 1's present and its frame
event, a spent answer parked the terminal in `recv_event` with nothing
coming; stage 2's line and every one after it stayed in the pipe. Every
CPU reading `ready=0` from 11.4 s to 42.8 s is that: the terminal was
parked in a connection read, and nothing else had work.

Ruled out from the code: logd says every loss by name (a full ring, an
allowance, the console's unshown count) and said none; klogd's queue
counts what it refuses and counted none; the harness reads one serial
stream, which carried the compositor's lines the whole time; and the
terminal's log ring has one writer, its one thread, so no slot it
reserved can stay unpublished.

## Decisions

- **toyos-window reads every event through one `FrameRx`, without
  waiting.** `try_event` is the read for a loop that waits on the
  window's handle itself; `recv_event` and `poll_event` wait on readiness
  and look again, so a spent answer is waited past. `poll_event(0)` now
  looks without registering a poll. winit's ToyOS backend drains with
  `poll_event(0)`, so the same hang is closed for every winit app.
- **The terminal reads its shell's pipes with `read_nonblock`.** std
  gives a child's pipe no `IntoRawFd` on ToyOS, and its end is the
  handle and nothing more, so the terminal forgets std's owner and takes
  the handle as a `toyos::Pipe`. It depends on `toyos-abi` for
  `SyscallError`, as the console does.
- **The kernel is unchanged.** The leftover answer is the poller's
  documented contract, and closing it would not close staleness: a post
  that claims a poll before the replacing registration withdraws it
  writes its completion after.
- **The console and `surface::Host::accept` wait the same way** and are
  off this path; `issues/design-debt/console-and-surface-host-wait-on-a-spent-readiness.md`
  holds them. `Host::accept` is under `toyos/src`.

## Tests

`toolkit_window_spent` (`tests/toyos-rust-tests/src/bin/window_spent.rs`)
leaves a spent answer in a window's own ring — a timed `poll_event` arms
the poll, the frame event answers it, `recv_event` reads the frame — and
requires `poll_event(0)` and a timed `poll_event` to answer `None` and
the next frame to be read. Before this change `poll_event(0)` parks
there.

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
650-libcllvm-whole.log, wt/toyos-libcllvm, whose diff is userland/libc
alone; load average 66-87, with the run reporting ceilings at 1.01x.

- metal_sim_window_drag and usb_boot_stick_pulled timed out their boots at
  21 and 22 s in the serial tail, the 20 s a phase of one guest gets. Each
  had the loader still printing its segments. They go under the harness-wait
  issue, which now also says why the run's 1.01x could not see the load.
- blockd_serves_partitions: the bench passed and its process exited at
  35.978 s, then no ===TEST_END=== came for 3559 s, while the kernel kept
  logging with every CPU idle. Either test-runner's wait on the job missed
  the exit's post, or logd stopped forwarding. The new issue says which
  evidence the next sighting needs, and names #655's poller shape as the
  logd reading.

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

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 85cd3b614 against origin/main (merge base 49f493832), round 1.

Net: +254 −49 (net +205). Production +116 −47 (toyos-window +66 −26, terminal +48 −21, manifest and lock +2). Tests +109 −2. Issue +29.

BLOCKER

  • PR inbox: a watch is answered after a look at its object, never by a post #655 @ 85cd3b614 — CI host is SKIPPED: the PR is a draft, and .github/workflows/ci.yml:21 runs only when draft == false. toolkit_window_spent has no green run and no negative-control red, and the probe has no run — nothing this branch claims is measured.

  • kernel/src/inbox/mod.rs:595-598, toyos/src/poller.rs:181-184 — the contract is at the wrong layer. "Every reader re-checks a readiness answer" leaves the class representable at every reader. This branch patches two of them. The tree already carries a different workaround for the same hazard: userland/fsd/src/main.rs:525-539's probe poller. These still park on an answer from a superseded registration:

    • init's three acceptors, userland/init/src/main.rs:938 (PID 1)
    • netd :1845
    • blockd :640
    • filepicker :536
    • logd inspect.rs:159
    • soundd control.rs:182
    • the console's pipes, console/src/main.rs:244,266
    • surface::Host::accept, toyos/src/surface.rs:182
    • surface::Keys::next, toyos/src/surface.rs:397 — the exact wait-then-recv_header shape this branch removes from Window::poll_event

    Fix it once, where every reader inherits it: toyos::poller hands out no completion from a registration that a later registration of the same handle replaced. That is the dedup key the kernel's register path already uses (Poll::handle) and needs no ABI change. It deletes fsd's probe and most of this diff. If the per-reader contract is kept instead, the issue file must list every site above, not "two readers".

  • userland/terminal/src/main.rs:153-155 — host.accept() is the blocking Acceptor::accept (toyos/src/port.rs:40) answering a TOKEN_LISTEN completion. The terminal therefore still parks on a spent answer until the next client connects; the branch's own issue file says so. The title "the terminal never blocks on a readiness answer already spent" is false at this head.

  • userland/toyos-window/src/lib.rs:657-659 — this patch keeps toolkit_window_spent green, because every poll_event in it must answer None:

    -            if !self.wait_readable(wait) {
    -                return None;
    -            }
    +            self.wait_readable(wait);
    +            return None;
    

    It breaks the "None when the timeout passes" contract for finite-timeout callers (editor/src/main.rs:1469, paint/src/main.rs:1082). Whatever test pins the read loop must turn red on it: after present(), one finite poll_event must deliver Frame.

  • tests/toyos-rust-tests/src/bin/window_spent.rs, tests/toyos.rs:305,1174,1423,7970-7994,10502 — this is a guest test for a decision a host test reaches. With the fix in toyos::poller, it is a FakePage test beside toyos/src/poller.rs:506: post the completion of a superseded registration; drain hands out nothing, and the live completion still arrives. --ci host's "the toyos SDK" step (src/ci.rs:490) runs it. toyos-window cannot hold a gated test: its arch/mod.rs cfg(target_arch) is an escape under src/userlandhost.rs:254. A kept per-reader loop moves into toyos beside FrameRx and is tested there. Under the owner's ladder, the guest test goes.

NOTE

  • userland/toyos-window/src/lib.rs:651-653 — u64::try_from(..).map_or(u64::MAX - 1, |n| n.min(u64::MAX - 1)) clamps nothing, because left ≤ timeout_nanos < u64::MAX whenever deadline is Some. Dead code; write left.as_nanos() as u64.
  • userland/terminal/src/main.rs:48-58 — pipe_end's unsafe and mem::forget rely on std's private ToyOS Pipe { fd } layout (rust/library/std/src/sys/pipe/toyos.rs:7). The safe toyos_abi::syscall::read_nonblock(RawHandle(stdout.as_raw_fd() as u32), buf) (toyos-abi/src/syscall.rs:2031) gives the same non-blocking read on std's own handle, as watch_raw already does. Delete pipe_end; no IntoRawFd is needed in the fork.
  • userland/terminal/src/main.rs:121-175 — nothing pins the terminal's own change: toolkit_window_spent never runs the terminal, so reverting userland/terminal/ alone keeps it green.
  • PR body, oracle — select(2) BUGS describes a kernel withdrawing readiness it already reported (a datagram dropped on checksum), not an answer from a replaced registration. epoll(7) level-triggered mode re-polls at delivery (ep_send_events → ep_item_poll), so a single reader never sees a spent answer there. The oracle does not support the contract chosen.
  • PR body — neither negctl-revert-fix.patch nor probe-poll-spent.patch is in the tree or the body, and the probe's count is not stated. The "measurement of this kernel" has no number.

REMOVE

  • issues/design-debt/console-and-surface-host-wait-on-a-spent-readiness.md:22-23 — "It is under toyos/src, so it is an ABI brief's." This is false: toyos/ is the SDK, toyos-abi/ is the ABI, and the inbox already answers OP_ACCEPT with WouldBlock (kernel/src/inbox/mod.rs:698).
  • issues/design-debt/console-and-surface-host-wait-on-a-spent-readiness.md:14 — "Two readers still block on one" is false; see the second BLOCKER's list.
  • userland/toyos-window/src/lib.rs:601-604 — "A poll left armed … is never sent." restates the poller's contract away from its header and rots once that contract changes.
  • userland/terminal/src/main.rs:61-62 — "as a blocking read's was" describes a past implementation.
  • tests/toyos.rs:1170-1172 — the rewritten comment goes with the test.
  • PR body — "The issue this answers lives on Every QEMU ceiling is at most three times what its test takes; wedges end in seconds; two LAN boots ride the talking boot #638's branch … lands second deletes it." and the whole "## Unsure" section are coordination that is false in main's record once merged.

SEND BACK

Japabu and others added 8 commits October 1, 2026 08:13
…tion

A watch is one-shot, and watching a handle again replaces its registration:
`process_watch` withdraws the armed poll whose `Poll::handle` matches. Two
answers escape that withdrawal and stay in the ring under the caller's token:
one the replaced registration posted before the replacement was processed,
and one it posts after a replacement that found the handle ready and was
answered at once (that path returns before the withdrawal). Either reads as
news about the handle. A reader that answers it with a blocking call waits
for bytes it already read, or for a connection it already took: the terminal
parked in `Window::recv_event` in #638's `toolkit_winit_loop` run, and the
same shape sits in init's acceptors, netd, blockd, filepicker, logd, soundd,
the console and `toyos::surface`.

The poller now submits every registration under a number of its own and keeps
a registry of the live ones, at most one per handle. An answer whose number is
not its handle's latest is dropped in `drain`; a live one hands the caller its
token and ends the registration. No ABI change: the kernel echoes whatever
token a submission carries.

`wait` went on believing the kernel. The kernel counts a dropped answer
towards `min_complete` and returns for it, so `wait` now loops, for what is
left of its timeout on the clock page, until the answers it handed out make
up the count. A caller's `wait(1, u64::MAX)` returns only with an answer.

The registry is a fixed array, because the SDK is a dependency of std and
links no allocator: 2 * MAX_HANDLES entries, with a per-poller limit of twice
the declared capacity (the handles watched, and as many closed since the
last wait whose end is still in the ring), past which a registration panics
by name. `Poller` stops being `Sync`: the registry is a `RefCell`, and a
watch already moved the submission tail in two steps.

fsd's probe poller existed only to second-guess a doubled answer before its
blocking `accept`; it goes.

Host tests drive the poller over a fake page and a fake kernel that answers
as `process_watch` and a post do, and reports readiness the reader has
already spent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
With toyos::poller handing out only a handle's latest registration's answer,
and `wait` returning only with one or at its timeout, a blocking read on an
answer finds what it was told of. The per-reader workarounds of 85cd3b6 go
back to main's code: toyos-window's `FrameRx` read path, `try_event` and its
deadline loop, the terminal's `pipe_end` and `read_ready`, and its `toyos-abi`
dependency.

The console and `surface::Host::accept`, which the issue this branch filed
named, read on answers of registrations they renew before every wait, so the
poller's rule covers them and the issue is deleted. `toolkit_window_spent` goes
with the per-reader loop it pinned: the decision it reached is the poller's,
and `toyos/src/poller.rs`'s host tests reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Met by this branch's `cargo run -- --build-only`; the rerun passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
So a red names the token that escaped instead of `is_empty()` failing bare.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Pins the removal in `Registry::answer`: without it a poller that watches one
handle after another reaches its bound and panics a long-running server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
After a wait every live registration is on a handle still open, because a
close answers the poll on it and that wait handed the answer out, except a
close `ops::close_ends_polls` says ends nothing; so at most the declared set
stands, and one round adds at most the declared set again.

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 07:00
@Japabu Japabu changed the title The terminal and toyos-window never block on a readiness answer already spent toyos::poller never hands out an answer that a later watch of the same handle replaced Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 4aac65723 against origin/main, round 2.

Round-1 BLOCKERs

  • CI host skipped on a draft: CLOSED. Run 36827911199 at 4aac65723: job host success, step cargo run -- --ci host success.
  • Wrong layer: CLOSED as prescribed. The poller drops the answers of replaced registrations, and negctl-today.patch gives EXIT=101 with 3 red on main's poller. The class it named is still open: BLOCKER 1.
  • Terminal host.accept() parks, and the title was false: CLOSED for the title. The park is still reachable: BLOCKER 1.
  • toyos-window/src/lib.rs:657-659 mutation: CLOSED. The loop is gone, and mutation-one-pass.patch gives EXIT=101 with 2 red.
  • Guest test: CLOSED. The guest test is deleted, and the FakePage tests run in --ci host's "the toyos SDK" step (src/ci.rs:490), green in run 36827911199.

Net lines: +428 −37 (net +391). Production: +133 in toyos/src/poller.rs, −19 in fsd. Tests: +261. Issue: +16.

BLOCKER

  • toyos/src/poller.rs:269-277 — the stall is only less likely, not impossible. The registry drops the answers of replaced registrations. But a post can fire the latest registration when there is nothing to read, and the poller hands that answer out.

    • Why: Watch::post fires every armed poll without re-reading the object (toyos-sched/src/watch.rs:157-176, kernel/src/inbox/mod.rs:185-191). kernel/src/watch.rs:9 says so: "a waiter re-reads the object, never the post".
    • Trigger (a), a race: a write posts only after it has released the lock that published it (kernel/src/syscall/io.rs:56-66; connect at kernel/src/syscall/ipc.rs:259 then :276). A frame is two writes (toyos/src/ipc.rs:298-299). Delay the compositor between the payload's publish and its post (an interrupt, a preemption, a descheduled vCPU). The terminal reads header and payload, watches again, the payload's post fires that new watch, and recv_event parks in recv_header. That is the original failure, traced through this poller.
    • Trigger (b), deterministic: a peer's zero-length write posts with nothing written. View::window accepts len 0 (kernel/src/user_ptr.rs:190), pipe::try_write answers Wrote(0) (kernel/src/pipe.rs:250-251), and sys_write then runs wake_pipe_readers.
    • Still exposed: the terminal's shell_stdout.read; fsd's accept, whose probe this branch deletes; init's, netd's and blockd's acceptors; Keys::next; Host::accept.
    • The PR's own oracle refuses this: epoll re-polls each item at delivery (ep_send_events → ep_item_poll).
    • This test is red at 4aac65723 (it hands out [7]). No registry can turn it green, because the ring carries no readiness. The guarantee belongs where the answer is read (BLOCKER 2), or in a kernel that re-polls before it answers. Until then main has no record of the class, because the round-1 issue this branch deleted never reached main.
    #[test]
    fn an_answer_with_nothing_to_read_is_not_handed_out() {
        let (mut kernel, poller) = pair(1);
        poller.watch_raw(H, READABLE, 7);
        kernel.submit();
        kernel.post(H); // a zero-length write's post, or a late post for bytes already read
        assert_eq!(drained(&poller), [0u64; 0]);
    }
  • toyos/src/poller.rs:275-277, and the PR body's "Not taken" — readers must follow the rule "watches it again before it waits", and no type enforces it. The reason given for not enforcing it is false:

    • Acceptor::accept (toyos/src/port.rs:40), Connection::recv_header (toyos/src/ipc.rs:216) and FrameRx (toyos/src/ipc.rs:466) are in toyos, the poller's own crate.
    • Each already has a non-blocking form with no ABI change: read_nonblock (toyos/src/ipc.rs:284, toyos-abi/src/syscall.rs:2020), and OP_ACCEPT's WouldBlock (kernel/src/inbox/mod.rs:698).
    • std's Read is the only reader outside the crate. The terminal already holds its pipes' raw handles (userland/terminal/src/main.rs:84-85). std's own ToyOS net code already works this way: a non-blocking op, then a wait, then a retry (rust/library/std/src/sys/net/connection/toyos.rs:63-80).
    • Fix: wait hands out an answer that only a non-blocking read accepts. That closes BLOCKER 1 for every reader in toyos. Once a stale answer costs one WouldBlock, decide what the registry still earns.
  • toyos/src/poller.rs:427-428 — nothing pins a min_complete above 1 across a dropped answer, and poller_capacity calls wait(CAP, …). Each of these two mutations keeps every test green:

    • submit(min_complete - handed, nanos) → submit(min_complete, nanos)
    • handed += self.drain(f) → handed = self.drain(f)

    This test is green at 4aac65723, and each mutation turns it red (mins becomes [2, 2] or [2, 1, 1]):

    #[test]
    fn a_wait_for_two_asks_the_kernel_only_for_what_is_missing() {
        let (mut kernel, poller) = pair(2);
        poller.watch_raw(G, READABLE, 4);
        kernel.submit();
        kernel.arrive(G);
        kernel.take(G);
        kernel.fill(H);
        poller.watch_raw(H, READABLE, 3);
        poller.watch_raw(G, READABLE, 4);
        let clock = Cell::new(0);
        let mut mins = Vec::new();
        let mut seen = Vec::new();
        let submit = |min, nanos| {
            kernel.submit();
            mins.push(min);
            if mins.len() == 2 {
                kernel.arrive(G);
            }
            clock.set(clock.get() + if kernel.posted() >= min { 1 } else { nanos });
        };
        poller.wait_on(2, 1_000, &mut |token| seen.push(token), submit, || clock.get());
        assert_eq!((seen, mins), (vec![3, 4], vec![2, 1]));
    }
  • toyos/src/poller.rs:236 — this mutation keeps every test green, yet it loses a live registration: when a registration that is not the last one answers, the last one is dropped, and its handle is never answered again.

             let token = self.live[at].token;
             self.len -= 1;
    -        self.live[at] = self.live[self.len];
             Some(token)

    Append to a_registration_left_standing_still_answers. It is green at 4aac65723 and red under the mutation ([]):

        kernel.arrive(G);
        assert_eq!(drained(&poller), [4]);

NOTE

  • toyos/src/poller.rs:433 — 0 => return, is dead code. With a zero timeout the deadline arm returns on the same pass, because checked_sub gives Some(0) or None. Delete it: a_wait_of_zero_looks_once stays green.
  • issues/build/a-finder-file-in-the-c-runtime-scratch-panics-its-removal.md — this is the same class as issues/build/a-finder-file-in-a-store-directory-panics-its-sweep.md: a Finder .DS_Store written into a build directory fails a build step. Keep one issue, not two.
  • userland/libc/src/posix_io.rs:559-566 — when a pollfd array names one fd twice, the earlier entry now loses its answer even if the fd is ready, because the registry replaces it. Before this branch, an immediate answer reached both entries.
  • PR body, Gates — the --build-only and cargo check --bins rows have no log, and CI's host job builds no guest userland. Poller: !Sync rests on those two rows. In the tree I found no unsafe impl Sync around a Poller, and std's wait_ready and the socket2 fork both build their pollers inside the call. Attach the logs.

REMOVE

  • toyos/src/poller.rs:272-274 — "So a caller that watches … what it is told of is there to read." This is false (BLOCKER 1).
  • userland/fsd/src/main.rs:517-518 — "this process is the port's one acceptor, so it is still queued." This is false (BLOCKER 1).
  • PR body, "## The cause" — it names two escapes as the cause of a stall that was never reproduced, and its "all had that shape" claims coverage the branch does not have.
  • PR body, "Not taken: a readiness token …" — its premise is false (BLOCKER 2).
  • PR body, "## Review of 85cd3b614", "## Unsure", and the Finder paragraph under "## Gates" — these coordinate this round and are not main's record. The issue file already carries the Finder story.

SEND BACK

Japabu and others added 4 commits October 1, 2026 09:23
A reader stalled when a ring answered READABLE for bytes it had already
read, and its blocking read then waited for good. The previous round's
registry in toyos::poller dropped the answers of replaced registrations,
but a post fires every armed poll without reading the object again, so a
peer's zero-length write, or a write's post that lands after the reader
took its bytes and watched again, still wrote an answer for nothing. The
ring carries no readiness, so nothing in userland could filter that out;
a token that only a non-blocking read accepts would have moved the burden
onto every reader, and left libc's poll(), std's Read and every reader
outside the SDK exposed.

So the kernel no longer writes an answer when a post fires. A post fires
the poll, which owes the ring a look (polls::Wake::owe), and the ring's
own inbox_submit looks at the object again before it writes anything
(polls::deliver): an object ready at that look is answered with the
directions it is ready in, and one that is not is armed again. That is
epoll's model: ep_send_events polls every ready item again (ep_item_poll)
before it reports it.

- A handle has one poll that may answer. A watch now replaces its
  handle's earlier poll on both paths, armed or fired, and a watch whose
  object is ready at once is a fired poll like any other, so one look
  answers a handle once, under its newest token.
- The log and a console keep their post as the answer
  (ops::read_posts_are_readiness): the log's unread records are its
  reader's cursor's, and a console's watch is the keyboard's while its
  data is the serial line's, an open issue. Poll::posted records which
  watch posted, so a registrant's own look is never taken for a post.
- A handle the process closed since it watched it answers -NotFound at
  the look; it is not the handle fault a submission naming one is.
- A full completion ring stops the look and leaves the rest owed for the
  next wait, so a readiness answer is never dropped.
- inbox_submit parks until a poll is owed a look as well as on its count.
  The debt is an AtomicBool, stored before the ring's watch is posted and
  swapped clear before each look.

Deleted with it: toyos::poller's registry, its wait loop and their tests.
The poller is main's again but not Sync, since a watch moves the
submission tail in two steps. fsd's per-reader probe stays deleted, and
the comment that claimed its acceptor was still queued goes. libc's
poll() keeps one watch per fd, under the first entry naming it with every
entry's interest, because a second watch on one fd replaces the first
and its entry would go unanswered even when the fd was ready.

kernel-loom/tests/inbox_answer.rs drives polls.rs against fake objects.
post-is-an-answer is its negative control (a fired poll is answered with
no look) and is red on the zero-length write, the late post and a look
that re-arms; --ci host runs it.

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

The C++ runtime's scratch removal and the store sweep fail on the same
host writer, a Finder .DS_Store in a directory the build takes as wholly
its own, so they are one issue under a slug that names the class.

A ring's completions keep their IrqLock although no interrupt handler
writes a completion now that a post only owes a look; recorded with its
exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
--ci host was red on two steps at 7d31aca:

- the build system's the_kernel_declares_only_the_builds_that_earned_one
  did not list post-is-an-answer, which the kernel declares only so cfg
  checking knows the name;
- clippy: toyos-sched/loom's loom_watch compiles inbox/once.rs and took
  neither end nor ended, and inbox_answer's fake ring held an Rc, so an
  Arc of its poll was neither Send nor Sync.

loom_watch's two ring entries now do what the kernel's PollEntry does,
firing the poll on readiness and ending it on Gone, and the end-racing
models assert that the one-shot names which of the two took it. The
model of a ring whose fire takes the ring's lock no longer claims to be
the kernel's, and the issue that records that lock names it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title toyos::poller never hands out an answer that a later watch of the same handle replaced inbox: a watch is answered after a look at its object, never by a post Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Answers to the review of 4aac65723, at 67881a654.

The owner ruled during this round that the ABI may change, so spent readiness is made impossible where an answer is written, in the kernel, instead of with a token in toyos::poller. A post now only owes a poll a look. The ring's own inbox_submit looks at the object again before it writes an answer (kernel/src/inbox/polls.rs).

BLOCKER 1, a post fires the newest registration with nothing to read. Fixed in the kernel, so it holds for every reader. Your test, restated against polls.rs with a fake object:

  • a_post_with_nothing_to_read_answers_nothing covers the zero-length write.
  • a_post_that_lands_after_its_bytes_were_read_answers_nothing covers the late post.

Both are green on the branch and red under post-is-an-answer (EXIT=101). That control answers a fired poll without a look, which is what a ring did before this branch. It is a CONTROLS row, so --ci host keeps it red. Every reader in your "still exposed" list is its handle's only reader, so an answer is still true when it reads.

BLOCKER 2, no type enforces the rule. I did not take the token. With the answer checked when it is written, a blocking read after an answer is sound for a handle no other reader drains, and a type that forbade it would forbid correct programs. A token would also bind only readers inside toyos; libc's poll() and std's Read would stay exposed. The registry earns nothing now and is deleted.

BLOCKER 3, the two min_complete mutations. Their code is gone. wait is main's submit + drain, because no answer is dropped in userland any more.

BLOCKER 4, the swap_remove mutation. The registry is gone. Its kernel counterpart is Polls::admit. a_poll_left_standing_still_answers reds when admit drops another handle's poll (patch built EXIT=0, run EXIT=101).

NOTEs

  • 0 => return,: deleted along with the loop.
  • The two Finder issues are now one: issues/build/a-finder-file-in-a-build-directory-fails-the-build.md.
  • posix_io.rs, one fd named twice: the kernel now replaces a handle's poll on the ready path as well, so libc keeps one watch per fd. The watch goes under the first entry that names the fd and carries every entry's interest, and every entry gets the shared answer. This also fixes main's armed case, which already lost the earlier entry.
  • Poller: !Sync logs are below. The image build and a cargo check --bins of every guest test binary both ran against this head's sysroot, 09e5db9ed92ece02.

REMOVEs

  • poller.rs:272-274: removed. The poller doc is main's apart from the Sync line and the false clause about two completions per handle.
  • fsd/src/main.rs:517-518: removed.
  • PR body: rewritten as main's record. "The cause", "Not taken", the review section, "Unsure" and the Finder paragraph are all gone.

Not measured by me: guest runs. The kernel's look has no host test: readiness_of, the log and console exception, and the refusals for a closed handle. Guest coverage comes from kbd_close (a keystroke must answer a console poll, which this branch keeps), inbox_cancel_wakes, poll_wake_pipe, poller_capacity, the log gates and every server loop.

cargo run -- --build-only, tail
Signed with this checkout's throwaway key SHA256:tcWkBR5gzJixvS0AaqBh4X04V11mbV8faI1Y74f5Kd4 at version 1790842508
Build finished.
Boot image: /Users/jan/Dev/jan/toyos-winitstall/target/bootable.img
EXIT=0

guest test binaries, cargo check --bins --target x86_64-unknown-toyos, tail
    Checking toyos-rust-tests v0.1.0 (/Users/jan/Dev/jan/toyos-winitstall/tests/toyos-rust-tests)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.18s
EXIT=0

That run checked the same toyos, toyos-abi and libc sources the head carries. The rerun at the head found nothing to recheck: Finished in 0.22s, EXIT=0.

@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 67881a654 against origin/main (fa134154c), round 3.

CI: run 36835197361 at 67881a654: job host success, and step cargo run -- --ci host success. In its log, inbox_answer gives "8 passed; 0 failed", and control post-is-an-answer gives "3 verdict(s) reached".

Round-2 BLOCKERs

  • 1, a post fires the newest registration with nothing to read: OPEN at the kernel. inbox_answer (EXIT 0) and post-is-an-answer (EXIT 101, 3 red) measure polls.rs against the test's own fake look (kernel-loom/tests/inbox_answer.rs:160-167). The code that decides the answer, Submitter::look (kernel/src/inbox/mod.rs:644-669), is run by no test that can fail. See BLOCKER A.
  • 2, no type enforces "watch again before you wait": CLOSED. The answer's contract is now the kernel's (toyos-abi/src/inbox.rs:13-21), so no reader is asked to keep a rule. The residual end-answer path is a NOTE below.
  • 3, the two min_complete mutations: CLOSED. The loop is deleted, and wait is main's submit + drain (the toyos/src/poller.rs diff is doc and Sync only).
  • 4, the swap_remove mutation: CLOSED. The registry is deleted, and admit dropping another handle's poll reds a_poll_left_standing_still_answers (PR body, EXIT 101). admit's withdrawal is unpinned: BLOCKER C.

Net: +825 −205 (net +620).

  • Production: +395 −180 (net +215). Kernel and toyos-sched +366 −140, toyos-abi +11 −6, toyos +4 −6, libc +14 −7, fsd −21. The growth is accepted for moving the answer into the kernel.
  • Tests and their gates: +383 −9.
  • Issues: +47 −16.

BLOCKER

  • A. kernel/src/inbox/mod.rs:656-664, kernel/src/object/ops.rs:788-796: the kernel's claim has no test that can fail.
    • Either of these mutations keeps every host test green: replace the look body after resolve with return Look::Ready(poll.flags);, or add KObjectRef::PipeRead(_) | KObjectRef::Connection(_) to read_posts_are_readiness's true arm.
    • Every guest test the PR names (poll_wake_pipe, inbox_cancel_wakes, poller_capacity, keyboard_claim_close_spares_stdin, log_conservation_smp2) asserts that an answer arrives. None asserts that one does not.
    • post-is-an-answer reverts only polls.rs. It keeps the deferral to submit and the replacement of fired polls: a_watch_replaces_a_poll_a_post_already_fired is green under it, where main answers [1, 2]. So it stands in for main on the two headline cases only. It is not the whole change reverted onto the base, which CLAUDE.md requires for a negative control.
    • The epoll oracle (ep_send_events → ep_item_poll) is independent. It decides those two cases' expected outcomes, but it only ever judges the fake.
    • Required: one guest test. One poller watches pipes H (token 1) and G (token 2). wait(0, 0) registers both. Then a raw zero-length write to H, then one byte to G, then wait(1, u64::MAX); assert the tokens are [2]. It must be red at fa134154c and green at the head, with both runs in the PR body. No cheaper tier runs sys_write's post or pipe::has_data.
  • B. kernel/src/inbox/mod.rs:465, :495: the new lost-wake protocol (the owed mark, its swap, and the park predicate) is modelled nowhere.
    • Either of these mutations keeps every test green, and each parks a submitter whose poll fired between take_owed's scan and the park: (1) delete inbox.owed.load(Ordering::Acquire) || from the predicate at :495; (2) move the swap at :465 below polls::deliver(..) at :466.
    • inbox_answer is one thread, and its owe is a counter (:49-53).
    • toyos-sched/loom/tests/loom_watch.rs:661-778 (PollRing, two_posts_through_one_rings_lock_lose_no_wake) is edited in this diff, yet still models a fire that writes a completion under the ring's lock. The branch's own issue says no fire does that.
    • Required: that model, rewritten to the owed protocol: a fire marks and posts the ring's watch; the submitter swaps the mark, takes the fired entries, and parks on owed || taken == 2. It must run the kernel's own sequence, not a transliteration, and both mutations must be shown red in it.
  • C. kernel/src/inbox/polls.rs:106, :126, :169: three mutations keep all 8 inbox_answer tests green.
    • (1) Delete p.withdraw(); at :106. A replaced armed poll then stays live on its object's watch, because toyos-sched/src/watch.rs:317-318 prunes only dead entries. Every re-watch of an idle handle then grows that watch's list until the object posts, and mio's selector re-arms every registration on every select. That is kernel memory a process chooses.
    • (2) Delete poll.withdraw(); at :126. A torn-down ring's polls then stay live on every watch.
    • (3) Change Look::Refused(e) => -(e as i32), to Look::Refused(_) => continue, at :169. A poll whose handle was closed is then never answered. The fake look never refuses.
    • Required tests: watch H 1, submit, watch H 2, submit, then assert that the first poll is not armed (or that a post of H owes one look, not two); a ring torn down by withdraw_all, then assert that no poll on H is armed; a fake look that refuses with NotFound for a closed handle, then assert the answer (token, -NotFound).
  • D. PR body: post-is-an-answer adds a feature and a cfg arm (kernel/Cargo.toml:41, kernel-loom/Cargo.toml:61, kernel/src/inbox/polls.rs:185-188). The body does not show a mem::forget planted in that arm turning cargo run -- --clippy red.

NOTE

  • kernel/src/object/ops.rs:788-796: per object kind, the answer to the brief.
    • Pipes, connections (sockets ride them), acceptors and devices: the look reads ops::has_data/has_space, the predicate a blocking read waits on, so a single reader's answer holds.
    • The log and the console: not closed. A post is still the answer there; the console's is tracked in its issue, and logd reads non-blocking.
    • Process handles: read_watch is None and has_data is false (:293, :779), so OP_WATCH on one is refused -NotSupported, on main and here alike.
  • kernel/src/object/ops.rs:186-192, toyos/src/poller.rs:340-348: an end answer is handed out as a bare token.
    • A close of any one handle to a pipe read end or an acceptor ends every ring's poll on that watch with -NotFound. So "a blocking read after an answer is sound for a handle no other reader drains" is false when a sibling handle closes.
    • fsd's deleted probe (userland/fsd/src/main.rs:534-539 on main) covered that case for its acceptors.
    • I found it unreachable in the tree: fsd's acceptors are endowed and never dup'd. It is pre-existing; file it.
  • kernel/src/inbox/mod.rs:581-583, :645: the look resolves poll.handle in the submitter's table, not the registrant's.
    • An Inbox handle carries DUP and TRANSFER (ops.rs:46-48). A process handed a ring therefore re-resolves the owner's handle numbers in its own table and re-arms the owner's tokens on its own objects.
    • Main's answers never read the submitter's table. It grants no authority that OP_WATCH into a held ring did not already grant.
  • kernel/src/inbox/polls.rs:118-121, mod.rs:665: a poll taken out for its look is invisible to a concurrent admit.
    • With a second thread submitting to the same ring, the look's re-arm withdraws the newer watch, or answers the older token beside it: two answers for one arrival.
    • OP_WATCH's doc (toyos-abi/src/inbox.rs:21) promises otherwise. Poller is !Sync, and mio's selector submits from one thread.
  • kernel/src/inbox/polls.rs:110: >= → > keeps every test green. The tests' cap is 16 and never reached.
  • userland/libc/src/posix_io.rs:570: dropping .filter(|&i| first(i) == i) keeps every test green. A C case that polls one fd twice pins it.
  • issues/design-debt/a-rings-completions-are-behind-an-irq-lock-no-handler-takes.md: the deletion it names is open work: the IrqLock becomes a Lock, and handler-post's in_a_ring arm, Staged::complete and the raise_if_staged call in Inbox::complete go. status: open names no owner.
  • Userland-first: re-reading readiness when the answer is written is the one thing only the kernel can do, and ENDED, posted and the log/console exception are what keeping main's other answers costs. Poller: !Sync is outside the stall.
  • Guest gap: of The guest suite keeps the 21 tests only a booted machine answers; the rest are metal, host or tracked #660's 21, none asserts on this path.
    • The 16 virt_* rows and the three screen_* rows boot init, logd and the compositor, whose loops wait through OP_WATCH, so a lost answer hangs them. That is a smoke test only.
    • iommu_virtio_platform and nested_nmi_is_loud do not reach the path.
    • A host model suffices for polls.rs, and for the owed protocol once B lands. It cannot reach look's per-kind readiness.
    • Required guest runs at the final head:
      • A's test, at both commits;
      • keyboard_claim_close_spares_stdin, the console exception;
      • log_conservation_smp2, the log exception;
      • poll_wake_pipe, a cross-thread fire against the park;
      • abuse_inbox, the result word;
      • one virt_* boot.

REMOVE

  • kernel/src/inbox/mod.rs:313-314: "an interrupt handler's post reaches here through the poll it fires". This is false: a fire reaches owe (:155-161).
  • kernel/src/watch.rs:292-295: "raised inside a completion written into a ring that polls the watch … fires that poll". This was rewritten in this diff for a nesting no post reaches.
  • kernel/src/inbox/polls.rs:178, kernel-loom/Cargo.toml:53-54, kernel-loom/tests/inbox_answer.rs:21: "what a ring did before this file" and its two copies. This is false (see A) and describes a past implementation.
  • toyos/src/poller.rs:307-308: "a handle that was ready when the kernel looked". This is false for the -NotFound and refusal tokens that drain hands out (:340-348).
  • PR body, "Not host-tested … The guest suite covers it through …": none of those tests has run, and none can red on the claim (A).
  • PR body, "The userland mechanism from the previous rounds goes" and "The two Finder issues are now one": these describe this branch's rounds. Main never had the registry, and it had one Finder issue.

SEND BACK

@Japabu
Japabu marked this pull request as draft October 2, 2026 16:13
Japabu and others added 2 commits October 2, 2026 18:25
…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
…s handle's place, and the answer has a guest test

Answers the review of 67881a6.

A. `tests/toyos-rust-tests/src/bin/inbox_empty_write.rs`: one poller watches
   two pipes, a write of no bytes goes into the first and one byte into the
   second, and the wait answers the second's token alone. It is the one test
   that runs `sys_write`'s post, the look's `resolve` and `pipe::has_data`.

B. The `owed` mark and its swap are gone. A fire takes its poll and posts the
   ring's watch; the submitter, registered on that watch, reads its polls
   again before it parks (`polls::awake`). That is the watch's own lost-wake
   argument, with no word beside the poll to keep in step with it. The mark
   was first moved into `polls.rs` so `toyos-sched-loom` could run it: loom
   then parked the submitter over two fires, because it lets a plain store
   land before another thread's earlier swap in modification order, which no
   swap allows. Reading the polls needs no such argument.
   `toyos-sched/loom/tests/loom_watch.rs`'s ring model is rewritten over the
   kernel's own `polls.rs`: `Poll::fire`, `Polls`, `deliver` and `awake`,
   against the real watch and park. `two_posts_through_one_rings_lock_lose_no_wake`
   becomes `a_fire_racing_a_submitters_park_is_never_lost`, and
   `commit-ignores-notify` names it.
   A full completion ring keeps `awake` false: a submitter with a fired poll
   and no room to answer it would otherwise go round for good.

C. `inbox_answer` gains a replaced poll's withdrawal, a torn-down ring's, and
   a closed handle's refusal as the answer.

Notes:
- A poll taken for its look stays among the ring's polls, marked, so a watch
  another thread submits during the look replaces it (`Polls::settle`,
  `Polls::renew`): the look then answers nothing and arms nothing beside the
  newer watch. Two cases stage the watch inside the look.
- The cap is tested at the cap.
- libc's `poll` plan is `pollreq.rs`, pure, and `toyos-libc-copies` tests a
  descriptor named twice.
- A ring's completions sit behind a `Lock`: no interrupt handler reaches
  them, and the issue that said so closes.
- `toyos::Poller` is `Sync` again, as on main; filed.
- Filed: a close of one handle ends every ring's poll on its object; a ring
  handed to another process resolves its watches in the receiver's table.

Removed: the comment in `Inbox::complete` about a handler's post, "what a ring
did before this file" and its two copies, and `Poller::wait`'s clause about a
handle that was ready.

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

Mutation patches for the round at b6b61967d, each git apply --check, git apply, built, run and git apply -R by one script, the tree clean before and after each.

patch command build run red
b1-park-without-the-polls cargo test -p toyos-sched-loom --test loom_watch a_fire_racing_a_submitters_park_is_never_lost 0 101 a_fire_racing_a_submitters_park_is_never_lost
b1-park-without-the-polls cargo test -p kernel-loom --test inbox_answer 0 101 a_poll_fired_as_it_is_renewed_waits_for_the_next_look a_watch_during_a_look_that_finds_bytes_answers_alone a_submitter_parks_only_with_nothing_it_could_answer
b2-fire-wakes-before-it-takes-the-poll cargo test -p toyos-sched-loom --test loom_watch a_fire_racing_a_submitters_park_is_never_lost 0 101 a_fire_racing_a_submitters_park_is_never_lost
c1-admit-leaves-the-replaced-poll-armed cargo test -p kernel-loom --test inbox_answer 0 101 a_replaced_poll_is_withdrawn_from_its_watch
c2-teardown-leaves-polls-armed cargo test -p kernel-loom --test inbox_answer 0 101 a_ring_torn_down_withdraws_every_poll
c3-a-refusal-is-not-answered cargo test -p kernel-loom --test inbox_answer 0 101 a_handle_closed_since_its_watch_is_answered_with_the_refusal
n5-cap-off-by-one cargo test -p kernel-loom --test inbox_answer 0 101 a_ring_keeps_its_cap_of_polls_and_no_more
n4a-renew-beside-a-newer-watch cargo test -p kernel-loom --test inbox_answer 0 101 a_watch_during_a_look_that_finds_nothing_is_the_handles_one_poll
n4b-settle-answers-a-replaced-poll cargo test -p kernel-loom --test inbox_answer 0 101 a_watch_during_a_look_that_finds_bytes_answers_alone
r-park-ignores-a-full-ring cargo test -p kernel-loom --test inbox_answer 0 101 a_submitter_parks_only_with_nothing_it_could_answer
p1-a-renewed-poll-ends-the-look cargo test -p kernel-loom --test inbox_answer 0 101 a_poll_fired_as_it_is_renewed_waits_for_the_next_look a_poll_armed_again_does_not_end_the_look
p2-admit-replaces-nothing cargo test -p kernel-loom --test inbox_answer 0 101 a_replaced_poll_is_withdrawn_from_its_watch a_ring_keeps_its_cap_of_polls_and_no_more a_watch_during_a_look_that_finds_bytes_answers_alone a_watch_during_a_look_that_finds_nothing_is_the_handles_one_poll a_watch_replaces_a_poll_a_post_already_fired
p3-admit-drops-every-other-poll cargo test -p kernel-loom --test inbox_answer 0 101 a_poll_fired_as_it_is_renewed_waits_for_the_next_look a_ring_keeps_its_cap_of_polls_and_no_more a_full_ring_leaves_a_fired_poll_for_the_next_look a_poll_left_standing_still_answers a_submitter_parks_only_with_nothing_it_could_answer
p4-deliver-has-no-room-check cargo test -p kernel-loom --test inbox_answer 0 101 a_full_ring_leaves_a_fired_poll_for_the_next_look a_submitter_parks_only_with_nothing_it_could_answer
pass-a-renewed-poll-is-looked-at-again cargo test -p kernel-loom --test inbox_answer 0 101 a_watch_during_a_look_that_finds_bytes_answers_alone a_poll_fired_as_it_is_renewed_waits_for_the_next_look
p5-an-ended-poll-is-looked-at cargo test -p kernel-loom --test inbox_answer 0 101 an_ended_source_is_answered_gone_without_a_look
p6-fire-does-not-record-its-post cargo test -p kernel-loom --test inbox_answer 0 101 a_post_answers_for_an_object_with_nothing_to_look_at
l1-poll-watches-every-entry cargo test -p toyos-libc-copies poll_requests 0 101 poll_requests::a_descriptor_named_twice_is_watched_once_for_both_interests

The review's two kernel mutations, and the control, in one QEMU guest. No host test reaches them, so local-qemu.patch below adds one machine test to the harness that boots tests/testcases and runs test_rs_inbox_empty_write. One script applied it with the mutation, ran cargo test --test toyos-build -- zz_local_empty_write, reversed both and showed the tree clean. The patch is a measurement and is in no commit.

tree mutation guest exit
b6b61967d none inbox_empty_write: a write of no bytes answered no watch, exit 0 0
b6b61967d a1-the-look-answers-without-looking exit 101: left: [1, 2], right: [2] 1
b6b61967d a2-a-pipes-post-is-its-readiness exit 101: left: [1, 2], right: [2] 1
0c34f5e45, the control none exit 101: left: [1, 2], right: [2] 1

The control commit is on no branch. It is a31eec595's tree and one file:

GIT_INDEX_FILE=/tmp/i git read-tree a31eec595
GIT_INDEX_FILE=/tmp/i git update-index --add --cacheinfo \
  100644,$(git rev-parse b6b61967d:tests/toyos-rust-tests/src/bin/inbox_empty_write.rs),tests/toyos-rust-tests/src/bin/inbox_empty_write.rs
git commit-tree $(GIT_INDEX_FILE=/tmp/i git write-tree) -p a31eec595

Its tree is cfd13548ecc6fd7ecd34dd3b5a5fc4e596fd58aa.

The T14 is the verdict's tier: the control and the head are staged for it, and a1 and a2 are here for a metal mutation loop.

local-qemu.patch
--- a/tests/toyos.rs	2026-10-02 18:52:53
+++ b/tests/toyos.rs	2026-10-02 18:52:53
@@ -167,6 +167,7 @@
     // The nested-NMI report is a raw write to the 16550, which the T14 does not
     // have.
     "nested_nmi_is_loud",
+    "zz_local_empty_write",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2027,6 +2028,22 @@
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        "zz_local_empty_write" => {
+            let crate_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/toyos-rust-tests");
+            let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &crate_path, "inbox_empty_write");
+            let mut guest = QemuInstance::boot_with_options(
+                test_config,
+                &[],
+                &[("inbox_empty_write".to_string(), bin)],
+                BootOptions::default(),
+            );
+            let result = guest.run_test("test_rs_inbox_empty_write", Duration::from_secs(120));
+            eprintln!("  [local] exit {:?} error {:?}\n{}", result.exit_code, result.error, result.stdout);
+            match (result.exit_code, result.error) {
+                (Some(0), None) => Ok(()),
+                (code, error) => Err(format!("inbox_empty_write: exit {code:?}, {error:?}")),
+            }
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }
a1-the-look-answers-without-looking.patch
--- a/kernel/src/inbox/mod.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/mod.rs	2026-10-02 19:07:46
@@ -608,6 +608,9 @@
             Err(HandleError::Rights { .. }) => return Look::Refused(SyscallError::PermissionDenied),
             Err(HandleError::TableFull) => return Look::Refused(SyscallError::ResourceExhausted),
         };
+        if true {
+            return Look::Ready(poll.flags);
+        }
         let flags = WatchFlags(poll.flags);
         let mut now = readiness_of(&object, flags);
         // A post on its read watch is the object's readability, and nothing
a2-a-pipes-post-is-its-readiness.patch
--- a/kernel/src/object/ops.rs	2026-10-02 19:07:46
+++ b/kernel/src/object/ops.rs	2026-10-02 19:07:46
@@ -784,8 +784,8 @@
 /// line's (`issues/kernel/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md`).
 pub fn read_posts_are_readiness(object: &KObjectRef) -> bool {
     match object {
-        KObjectRef::SysCap(_) | KObjectRef::Console(_) => true,
-        KObjectRef::PipeRead(_) | KObjectRef::PipeWrite(_) | KObjectRef::Connection(_)
+        KObjectRef::SysCap(_) | KObjectRef::Console(_) | KObjectRef::PipeRead(_) | KObjectRef::Connection(_) => true,
+        KObjectRef::PipeWrite(_)
         | KObjectRef::Acceptor(_) | KObjectRef::File(_) | KObjectRef::Device(_)
         | KObjectRef::Inbox(_) | KObjectRef::Connector(_) | KObjectRef::Namespace(_)
         | KObjectRef::SharedMem(_) | KObjectRef::Process(_) => false,
b1-park-without-the-polls.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
@@ -250,7 +250,8 @@
 /// registered on the watch a fire posts: it does not park over a poll
 /// [`deliver`] would answer.
 pub fn awake<W: Wake>(ring: &impl Submitter<W>, enough: impl FnOnce() -> bool) -> bool {
-    ring.room() && ring.polls(|polls| polls.owed()) == Some(true) || enough()
+    let _ = ring;
+    enough()
 }
 
 /// `post-is-an-answer` is the negative control: a fired poll is answered with
b2-fire-wakes-before-it-takes-the-poll.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
@@ -66,9 +66,8 @@
     pub fn fire(&self, posted: u32) {
         // Before the exchange, so the look that the winning fire owes sees it.
         self.posted.fetch_or(posted, Ordering::Release);
-        if self.state.fire() {
-            self.ring.wake();
-        }
+        self.ring.wake();
+        let _ = self.state.fire();
     }
 
     /// The object's source ended: the poll is answered as gone, with no look.
c1-admit-leaves-the-replaced-poll-armed.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
@@ -132,9 +132,6 @@
     pub fn admit(&mut self, poll: Arc<Poll<W>>, cap: usize) -> bool {
         self.polls.retain(|kept| {
             let other = kept.poll.handle != poll.handle;
-            if !other {
-                kept.poll.withdraw();
-            }
             other
         });
         if self.polls.len() >= cap {
c2-teardown-leaves-polls-armed.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
@@ -186,9 +186,7 @@
 
     /// The ring is going: no poll answers.
     pub fn withdraw_all(&mut self) {
-        for kept in self.polls.drain(..) {
-            kept.poll.withdraw();
-        }
+        self.polls.clear();
     }
 }
 
c3-a-refusal-is-not-answered.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
@@ -236,7 +236,7 @@
         } else {
             match look(ring, &poll) {
                 Look::Ready(flags) => flags as i32,
-                Look::Refused(e) => -(e as i32),
+                Look::Refused(_) => continue,
                 Look::Waits => continue,
             }
         };
l1-poll-watches-every-entry.patch
--- a/userland/libc/src/pollreq.rs	2026-10-02 19:07:46
+++ b/userland/libc/src/pollreq.rs	2026-10-02 19:07:46
@@ -32,7 +32,6 @@
         }
     }
     (0..entries.len())
-        .filter(|&entry| watch_of(entries, entry) == entry)
         .map(|entry| (entry, entries[entry].0, interest[entry]))
         .collect()
 }
n4a-renew-beside-a-newer-watch.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -170,8 +170,11 @@
     /// Keep `again` in `poll`'s place once its look found nothing; `false` as
     /// [`Self::settle`] says it, and `again` is not kept.
     pub fn renew(&mut self, poll: &Arc<Poll<W>>, again: Arc<Poll<W>>) -> bool {
-        let Some(at) = self.place(poll) else { return false };
-        self.polls[at] = self.keep(again);
+        let kept = self.keep(again);
+        match self.place(poll) {
+            Some(at) => self.polls[at] = kept,
+            None => self.polls.push(kept),
+        }
         true
     }
 
n4b-settle-answers-a-replaced-poll.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -162,7 +162,7 @@
     /// Let go of `poll` once its look has an answer. `false` if a newer watch
     /// replaced it during the look or the ring went: it answers nothing.
     pub fn settle(&mut self, poll: &Arc<Poll<W>>) -> bool {
-        let Some(at) = self.place(poll) else { return false };
+        let Some(at) = self.place(poll) else { return true };
         self.polls.remove(at);
         true
     }
n5-cap-off-by-one.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:45
@@ -137,7 +137,7 @@
             }
             other
         });
-        if self.polls.len() >= cap {
+        if self.polls.len() > cap {
             return false;
         }
         let kept = self.keep(poll);
p1-a-renewed-poll-ends-the-look.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -237,7 +237,7 @@
             match look(ring, &poll) {
                 Look::Ready(flags) => flags as i32,
                 Look::Refused(e) => -(e as i32),
-                Look::Waits => continue,
+                Look::Waits => return,
             }
         };
         if ring.polls(|polls| polls.settle(&poll)) == Some(true) {
p2-admit-replaces-nothing.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -131,7 +131,7 @@
     /// a post has fired it or not.
     pub fn admit(&mut self, poll: Arc<Poll<W>>, cap: usize) -> bool {
         self.polls.retain(|kept| {
-            let other = kept.poll.handle != poll.handle;
+            let other = kept.poll.handle != poll.handle || true;
             if !other {
                 kept.poll.withdraw();
             }
p3-admit-drops-every-other-poll.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -131,7 +131,7 @@
     /// a post has fired it or not.
     pub fn admit(&mut self, poll: Arc<Poll<W>>, cap: usize) -> bool {
         self.polls.retain(|kept| {
-            let other = kept.poll.handle != poll.handle;
+            let other = kept.poll.handle != poll.handle && false;
             if !other {
                 kept.poll.withdraw();
             }
p4-deliver-has-no-room-check.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -229,7 +229,7 @@
 /// object as fast as it is looked at holds nobody.
 pub fn deliver<W: Wake>(ring: &impl Submitter<W>) {
     let Some(pass) = ring.polls(|polls| polls.pass()) else { return };
-    while ring.room() {
+    loop {
         let Some(poll) = ring.polls(|polls| polls.take_owed(pass)).flatten() else { return };
         let result = if poll.state.ended() {
             -(SyscallError::NotFound as i32)
p5-an-ended-poll-is-looked-at.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -231,7 +231,7 @@
     let Some(pass) = ring.polls(|polls| polls.pass()) else { return };
     while ring.room() {
         let Some(poll) = ring.polls(|polls| polls.take_owed(pass)).flatten() else { return };
-        let result = if poll.state.ended() {
+        let result = if false {
             -(SyscallError::NotFound as i32)
         } else {
             match look(ring, &poll) {
p6-fire-does-not-record-its-post.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -65,7 +65,7 @@
     /// posted, or with `0` its registrant saw it ready. Owes a look, once.
     pub fn fire(&self, posted: u32) {
         // Before the exchange, so the look that the winning fire owes sees it.
-        self.posted.fetch_or(posted, Ordering::Release);
+        let _ = posted;
         if self.state.fire() {
             self.ring.wake();
         }
pass-a-renewed-poll-is-looked-at-again.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -154,7 +154,7 @@
     /// The oldest poll owed a look among those kept before `pass`, kept while
     /// it is looked at so a watch on its handle still replaces it.
     pub fn take_owed(&mut self, pass: u64) -> Option<Arc<Poll<W>>> {
-        let kept = self.polls.iter_mut().find(|kept| kept.order < pass && kept.owed())?;
+        let kept = self.polls.iter_mut().find(|kept| { let _ = pass; kept.owed() })?;
         kept.looking = true;
         Some(kept.poll.clone())
     }
r-park-ignores-a-full-ring.patch
--- a/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
+++ b/kernel/src/inbox/polls.rs	2026-10-02 19:07:46
@@ -250,7 +250,7 @@
 /// registered on the watch a fire posts: it does not park over a poll
 /// [`deliver`] would answer.
 pub fn awake<W: Wake>(ring: &impl Submitter<W>, enough: impl FnOnce() -> bool) -> bool {
-    ring.room() && ring.polls(|polls| polls.owed()) == Some(true) || enough()
+    ring.polls(|polls| polls.owed()) == Some(true) || enough()
 }
 
 /// `post-is-an-answer` is the negative control: a fired poll is answered with

… submitter

`polls::deliver` took the oldest poll owed a look until none was. A poll it
renewed could be fired again before the next take, by a peer writing no bytes
as fast as the submitter looked, and the pass then never reached the handles
behind it nor returned. Each kept poll now carries its order among every poll
its ring kept, and a pass takes only those kept before it began: a renewed
poll, and a watch that lands during the pass, wait for the next, which
`submit`'s loop makes at once because `awake` sees them owed.

`a_poll_fired_as_it_is_renewed_waits_for_the_next_look` stages the peer inside
the look. `a_watch_during_a_look_that_finds_bytes_answers_alone` now reads the
newer watch's answer from the pass after the one it landed in.

Filed, neither removed here:
- two submitters of one ring race its last completion slot, and the loser's
  answer is dropped and counted;
- a peer that posts as fast as a submitter looks keeps it from parking, so a
  kill waits for the first park. Not measured.

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, run by the orchestrator: the three boots the round staged, each image's sha256 checked against the request before it was flashed, each flashed and booted once (cargo run --bin toyos-metal -- --image … --readback … --fat32-check from the worktree at b6b61967d, clean before and after). Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71).

boot tree image sha256 toyos-metal boot tests, by name
control 0c34f5e45: main a31eec595 plus inbox_empty_write alone d1f2b2e323d69449ad7abfda81331acfa4b3f948f9cae9697fed6799f3652e31 EXIT=0, verdict passed 1152 ms inbox_empty_write exit=101
head, inbox b6b61967d 7cbf44a9746348a6437e02a720557953a4f23a2d5953c8fdf6ac218d379d398b EXIT=0, verdict passed 1151 ms abuse_inbox exit=0, inbox_cancel_wakes exit=0, inbox_empty_write exit=0
head, poll b6b61967d 957cbe5f47c7d9103a149c89450030f0e0dd1d3afea0833147c026288f02c8ff EXIT=0, verdict passed 1152 ms poll_wake_pipe exit=0, poller_capacity exit=0
  • The control is red as predicted, on the real machine: kernel.log:338-339, "panicked at src/bin/inbox_empty_write.rs:33:5: assertion left == right failed: the wait answered a pipe with nothing to read", then exit: test_rs_inbox_empty_write pid=9 code=101.
  • toyos-metal's own exit and verdict speak for the boot, not for a test in it: the control's boot is passed with its test at 101. The rows are judged by name above; the harness's --metal-readback judging over these readbacks has not been run.
  • toyos-fat32-check: the log partition's 35651584 bytes check out, on all three. All three images were armed with boot-deadline=120000.
  • The request's two kernel mutations were not booted.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of b6b61967d against origin/main (a31eec595), round 4. Read, not run: the diff, every changed file whole, the round's logs under /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-round/, and the three T14 readbacks there.

Round-3 BLOCKERs

  • A, the kernel's claim has no test that can fail: CLOSED. The verdict rests on the three T14 boots of inbox: a watch is answered after a look at its object, never by a post #655 (comment), read by name from their readbacks.
    • Control 0c34f5e45 (git diff --stat a31eec595 0c34f5e45: the one test, +39): ===TEST_END test_rs_inbox_empty_write exit=101===, left: [1, 2], right: [2] (metal-base/shared/kernel.log:338-341, :382).
    • Head: inbox_empty_write exit=0 (metal-head-inbox/shared/kernel.log:383, :396).
    • a1 and a2: exit 101 with [1, 2], one QEMU guest each (local-qemu-a1.log, local-qemu-a2.log); neither was booted on the T14.
  • B, the lost-wake protocol is modelled nowhere: CLOSED for one submitter. The ring model compiles the kernel's polls.rs; b1 and b2 exit 101 on a_fire_racing_a_submitters_park_is_never_lost (mutations/results.txt); commit-ignores-notify reds it in --ci host (ci-host-b6b61967d.log:6259-6286, EXIT=0 at :7386); poll_wake_pipe exit=0 on the T14, 300 edges (metal-head-poll/shared/kernel.log:345, :359). The swap mutation has no subject. Two submitters are not covered: BLOCKER 2.
  • C, three mutations green: CLOSED. c1, c2, c3 exit 101, each on its named test (mutations/results.txt).
  • D, a planted mem::forget: CLOSED. Worktrees and the primary's sync are git commands in the role prompts, a sysroot placement sweeps its store, and the prompts take the owner's decisions #678 took the rule out of reviewer.md; post-is-an-answer is a CONTROLS row (src/ci.rs:288) that control_features hands the $CONTROLS shape (src/clippy.rs:53).

The two issues the round left open

  • issues/kernel/two-submitters-of-one-ring-race-its-last-completion-slot.md: may land open. Main drops and counts every completion written to a full ring; here only two threads of one process racing their own ring's last slot do. post_completion rechecks under the lock, the drop is counted and Poller panics on the count. mio's selector and std's connection each submit from one thread, and the filed search finds no Poller shared between threads.
  • issues/kernel/a-peer-posting-as-fast-as-a-submitter-looks-keeps-it-from-parking.md: must close here. BLOCKER 1.

Net: +1376 −241.

  • Production +512 −184 (net +328), accepted for moving the answer into the kernel. BLOCKER 1's first arm deletes more than it adds.
  • Tests and their gates +770 −57. Issues +94; BLOCKER 4 takes two files out.

BLOCKER

    1. kernel/src/inbox/mod.rs:404-443, kernel/src/watch.rs:260-262 — a wait that goes round reads no kill — wait_until returns on a true ready() before it reads the kill, so with no deadline a peer holding a write end decides how long a killed thread stays in the kernel.
    • Main returns to Ring 3 on every post. kill_ends_every_wait holds "a kill ends every wait" for the parked state only.
    • The issue says "Not measured". Root CLAUDE.md lets a compromise stay only with its evidence, and a guess that one measurement would settle goes back to measure.
    • Required, either arm of the issue's own exit:
      • the loop reads the caller's kill beside the deadline at :421, as ops.rs:669 does, and the issue file goes. Its test is a kill_ends_every_wait arm: a child waiting with no deadline on a pipe its parent's threads write no bytes into, killed, then waited for. It is green with the read, and the body carries its run with the read reverted.
      • or that arm's run at this head, QEMU first and the T14 for the verdict, showing no kill is held, written into the issue in place of "Not measured".
    • Either way kill_ends_every_wait runs whole on the T14 at the final head: its poll arm is the one test of submit's kill path, and it was not among the five run here.
    1. kernel/src/inbox/polls.rs:114-116, kernel/src/inbox/mod.rs:298-303, toyos-sched/loom/tests/loom_watch.rs:699-701 — the models run one submitter — with two, a parked submitter is owed its wake by an answer and not by a fire.
    • Kept::owed hides a poll another submitter is looking at, so a second submitter parks over a fired poll. A thread that registers a ready handle with inbox_submit(1, 0, 0) while another waits is enough.
    • What wakes it is the looker's answer: Inbox::complete's post. The model runs one submitter and its answer posts nothing; no guest runs a second thread parked on one ring.
    • Mutation m1: delete self.watch.post_in_place() at mod.rs:300-302, keeping the write. I expect every model and guest green. By reading, the same deletion on main leaves poll_wake_pipe's watcher parked.
    • Required: the ring model runs two submitters against one fire with the answer's wake in code it compiles; or a shared-boot member in which a sibling's submission of a ready watch returns a thread parked in wait(1, u64::MAX) on the same ring. m1, or the same deletion in the code the model compiles, reds it. polls.rs:16-19 and Submitter::answer (:217) then say who owes that wake.
    1. kernel/src/object/ops.rs:787, kernel/src/inbox/mod.rs:615-617 — the log's arm has no test that can fail, as the body says — it is new kernel code on the wait path, and The guest suite keeps the 21 tests only a booted machine answers; the rest are metal, host or tracked #660 deleted the two guest tests round 3 named for it.
    • Mutation e1: move KObjectRef::SysCap(_) to the false arm. logd then reads the kernel's records on its cadence alone, up to 250 ms late (userland/logd/src/main.rs:123-124). I expect every model, guest and T14 boot green.
    • a_post_answers_for_an_object_with_nothing_to_look_at runs the fake's own flag.
    • Required: a shared-boot member that watches the endowed logread capability, causes a record, and is handed its token by wait(1, u64::MAX); under e1 the wait hangs and the harness's ceiling reds it.
    • The console's arm rides issues/kernel/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md, whose exit names its test.
    1. issues/kernel/a-zero-byte-pipe-write-wakes-the-readers-watch.md, issues/kernel/two-completions-can-name-one-arrival-and-accept-parks.md — this diff meets both exits and deletes neither file — main's tracker would say two things this merge makes false (issues/README.md, "Closing one").
    • The first's exit is "a wake completes a watch only if its direction is ready, with a test whose reader watch stays pending across a zero-byte write": the look, and inbox_empty_write.
    • The second's is "a watch that withdraws the armed one on its handle whether or not it answers at once", with a count: Polls::admit, and a_watch_replaces_a_poll_a_post_already_fired. Its body says fsd "now asks the acceptor with a zero-timeout watch on a probe ring", which this diff deletes.
    • With them goes tests/toyos-rust-tests/src/netd_stream.rs:54-56, the first defect said in a doc comment.

NOTE

  • PR body, Checks, Gates and "Not sure of" — "staged and waits for the machine", three gate rows at "2, staged" and "The T14 has not run any of the three boots" are false since the boots ran — the body takes their rows, and the exits of request.txt's three --metal-readback judge commands, which touch no machine and have not been run.
  • issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md:13-15, :28-29 — "its completions sit behind an IrqLock" and "each taking that ring's completions lock" are false at this head (mod.rs:188, polls.rs:66-72) — the file is assigned to the small-kernel track, so its holder corrects it in this merge.

REMOVE

  • PR body, "A mark that a fire stores and the submitter swaps … and the polls need none." and "Why loom let a store land before an earlier swap is my reading of its trace, not of its source." — an attempt main never had, and a claim about loom its author does not stand behind.
  • PR body, "The review's second B mutation … there is no swap.", the "BLOCKER D" paragraph, "a1 and a2 are the two kernel mutations the review named …", "The guest runs the review asked for." and "the six of the review before" — this branch's rounds, not main's record.

SEND BACK

No file is changed on both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
… model's, and the log's arm has a guest test

The fourth review of #655 (issuecomment-5958166619).

A wait that goes round reads no kill. `watch::wait_until` returns on a true
predicate before it reads the kill, and `submit`'s predicate is true whenever a
poll is owed a look, so a peer whose posts keep landing kept a killed thread in
the kernel. `submit` now reads the caller's kill beside the deadline, as
`ops::until_answered` does. Measured before the read went in, in one QEMU guest
of 8 vCPUs on a 14-core host at load 10: a child watching one empty pipe through
256 handles, four and six threads of its parent writing no bytes into it, was
seen running in every roster sample for 70 s and for 4.2 s after its kill, and
ended only when the posts left a gap; with the read it was gone in 13 ms.
`kill_ends_every_wait` gains that child as `posted-poll`. Its posts stop one
second after the kill, since a held kill is held for as long as its peers win a
race and no longer, which the harness's deadline cannot see.
`issues/kernel/a-peer-posting-as-fast-as-a-submitter-looks-keeps-it-from-parking.md`
is closed by this.

An answer's wake had no test. `Kept::owed` hides a poll a submitter is looking
at, so a second submitter of the ring parks over it, the fire's wake spent
before it registered; the looker's answer is what wakes it. That wake was
`Inbox::complete`'s, in code no model compiles. It is now `polls::complete`,
the one writer of a completion for every op, and `Submitter::answer` writes and
wakes nobody. `toyos-sched-loom`'s
`an_answer_wakes_the_submitter_its_look_hid_the_poll_from` runs two submitters
on two CPUs against one fire of one poll.

The log's arm of the look (`ops::read_posts_are_readiness`) had no test that
could fail. `inbox_log_post` is a shared-boot member that arms a watch on the
endowed `logread` capability, ends a process, is handed its token by
`wait(1, u64::MAX)` and reads the kernel's `exit:` record of that pid.

Two issues this branch already met are closed:
`a-zero-byte-pipe-write-wakes-the-readers-watch.md`, by the look and
`inbox_empty_write`, and `two-completions-can-name-one-arrival-and-accept-parks.md`,
by `Polls::admit` and `a_watch_replaces_a_poll_a_post_already_fired`. The doc
comment in `netd_stream.rs` that said the first goes with it. The
interrupts-off-walk issue no longer says a ring's completions sit behind an
`IrqLock` or that a fire takes their lock.

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

Evidence for 5f62645ae: the patches this head's runs applied, and what each run came to. Every run was made by one script that checks the patch, applies it, runs, reverses it and shows the tree clean.

The 19 patches of the earlier evidence comment (a1, a2, b1, b2, c1 to c3, l1, n4a, n4b, n5, p1 to p6, pass, r) apply to this head unchanged (git apply --check, exit 0 each) and were run again. Four are new.

k1-the-wait-loop-reads-no-kill.patch

--- a/kernel/src/inbox/mod.rs
+++ b/kernel/src/inbox/mod.rs
@@ -424,7 +424,7 @@
 
         // Read here and not only at the park: a peer whose posts keep this
         // thread looking keeps it from the park, and may not keep its kill.
-        if crate::sched::driver::current_kill_pending() {
+        if false && crate::sched::driver::current_kill_pending() {
             return Err(SyscallError::Gone);
         }
 

e1-a-log-post-is-not-its-readiness.patch

--- a/kernel/src/object/ops.rs
+++ b/kernel/src/object/ops.rs
@@ -784,8 +784,8 @@
 /// line's (`issues/kernel/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md`).
 pub fn read_posts_are_readiness(object: &KObjectRef) -> bool {
     match object {
-        KObjectRef::SysCap(_) | KObjectRef::Console(_) => true,
-        KObjectRef::PipeRead(_) | KObjectRef::PipeWrite(_) | KObjectRef::Connection(_)
+        KObjectRef::Console(_) => true,
+        KObjectRef::SysCap(_) | KObjectRef::PipeRead(_) | KObjectRef::PipeWrite(_) | KObjectRef::Connection(_)
         | KObjectRef::Acceptor(_) | KObjectRef::File(_) | KObjectRef::Device(_)
         | KObjectRef::Inbox(_) | KObjectRef::Connector(_) | KObjectRef::Namespace(_)
         | KObjectRef::SharedMem(_) | KObjectRef::Process(_) => false,

m1-an-answer-wakes-nobody.patch

--- a/kernel/src/inbox/polls.rs
+++ b/kernel/src/inbox/polls.rs
@@ -239,7 +239,6 @@
 /// while a look hid it.
 pub fn complete<W: Wake>(ring: &impl Submitter<W>, user_data: u64, result: i32) {
     ring.answer(user_data, result);
-    ring.wake();
 }
 
 /// Answer every poll owed a look, oldest first, while the ring has room; a

m2-the-wake-before-the-answer.patch

--- a/kernel/src/inbox/polls.rs
+++ b/kernel/src/inbox/polls.rs
@@ -238,8 +238,8 @@
 /// may wait for this count, and one may have parked over the poll it answers
 /// while a look hid it.
 pub fn complete<W: Wake>(ring: &impl Submitter<W>, user_data: u64, result: i32) {
-    ring.answer(user_data, result);
     ring.wake();
+    ring.answer(user_data, result);
 }
 
 /// Answer every poll owed a look, oldest first, while the ring has room; a

local-qemu.patch, the harness patch that boots tests/testcases with 8 vCPUs and runs one shared-boot member; in no commit.

--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -167,6 +167,9 @@
     // The nested-NMI report is a raw write to the 16550, which the T14 does not
     // have.
     "nested_nmi_is_loud",
+    "zz_local_kill_ends_every_wait",
+    "zz_local_inbox_log_post",
+    "zz_local_inbox_empty_write",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2027,6 +2030,24 @@
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        local if local.starts_with("zz_local_") => {
+            let member = &local["zz_local_".len()..];
+            let crate_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/toyos-rust-tests");
+            let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &crate_path, member);
+            let mut guest = QemuInstance::boot_with_options(
+                test_config,
+                &[],
+                &[(member.to_string(), bin)],
+                BootOptions { smp: 8, ..BootOptions::default() },
+            );
+            let result =
+                guest.run_test(&format!("test_rs_{member}"), Duration::from_secs(120));
+            eprintln!("  [local] exit {:?} error {:?}\n{}", result.exit_code, result.error, result.stdout);
+            match (result.exit_code, result.error) {
+                (Some(0), None) => Ok(()),
+                (code, error) => Err(format!("{member}: exit {code:?}, {error:?}")),
+            }
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }

local-qemu.sh

#!/bin/sh
# One QEMU guest (tests/testcases, 8 vCPUs) runs one shared-boot member on the tree as it stands plus the
# named mutation ("none" for none). The harness patch and the mutation are applied, run and reversed here.
# usage: local-qemu.sh <label> <member> <mutation|none> [strict]
#   strict: refuse a tree that is not clean before, and show it clean after.
set -u
W=/Users/jan/Dev/jan/toyos-winitstall
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r5
cd "$W"
label=$1; member=$2; mutation=$3; strict=${4:-loose}
if [ "$strict" = strict ] && [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "RESULT $label: TREE NOT CLEAN BEFORE" >> "$D/local-qemu-results.txt"; exit 1; fi
git apply --check "$D/local-qemu.patch" || { echo "RESULT $label: HARNESS PATCH DOES NOT APPLY" >> "$D/local-qemu-results.txt"; exit 1; }
if [ "$mutation" != none ]; then git apply --check "$D/mutations/$mutation.patch" || { echo "RESULT $label: MUTATION DOES NOT APPLY" >> "$D/local-qemu-results.txt"; exit 1; }; fi
git apply "$D/local-qemu.patch"
if [ "$mutation" != none ]; then git apply "$D/mutations/$mutation.patch"; fi
cargo test --test toyos-build -- "zz_local_$member" > "$D/local-qemu-$label.log" 2>&1; ran=$?
if [ "$mutation" != none ]; then git apply -R "$D/mutations/$mutation.patch"; fi
git apply -R "$D/local-qemu.patch"
clean=dirty; [ -z "$(git status --porcelain --ignore-submodules=none)" ] && clean=clean
echo "RESULT $label | HEAD $(git rev-parse --short HEAD) ($clean after) | mutation $mutation | cargo test --test toyos-build -- zz_local_$member | EXIT=$ran" >> "$D/local-qemu-results.txt"
touch "$D/local-qemu-$label.done"

The QEMU runs (local-qemu-results.txt)

HEAD 5f62645aeb32614b51080d81788ff360919141d3
RESULT kill-head-1 | HEAD 5f62645ae (clean after) | mutation none | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=0
RESULT kill-head-2 | HEAD 5f62645ae (clean after) | mutation none | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=0
RESULT kill-head-3 | HEAD 5f62645ae (clean after) | mutation none | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=0
RESULT kill-k1-1 | HEAD 5f62645ae (clean after) | mutation k1-the-wait-loop-reads-no-kill | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=1
RESULT kill-k1-2 | HEAD 5f62645ae (clean after) | mutation k1-the-wait-loop-reads-no-kill | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=1
RESULT kill-k1-3 | HEAD 5f62645ae (clean after) | mutation k1-the-wait-loop-reads-no-kill | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=1
RESULT log-head | HEAD 5f62645ae (clean after) | mutation none | cargo test --test toyos-build -- zz_local_inbox_log_post | EXIT=0
RESULT empty-head | HEAD 5f62645ae (clean after) | mutation none | cargo test --test toyos-build -- zz_local_inbox_empty_write | EXIT=0
RESULT empty-a1 | HEAD 5f62645ae (clean after) | mutation a1-the-look-answers-without-looking | cargo test --test toyos-build -- zz_local_inbox_empty_write | EXIT=1
RESULT empty-a2 | HEAD 5f62645ae (clean after) | mutation a2-a-pipes-post-is-its-readiness | cargo test --test toyos-build -- zz_local_inbox_empty_write | EXIT=1
RESULT log-e1 | HEAD 5f62645ae (clean after) | mutation e1-a-log-post-is-not-its-readiness | cargo test --test toyos-build -- zz_local_inbox_log_post | EXIT=1
status after all: []

The host mutation runs (mutations/results.txt)

HEAD 5f62645aeb32614b51080d81788ff360919141d3
RESULT m1-an-answer-wakes-nobody | cargo test -p toyos-sched-loom --test loom_watch an_answer_wakes_the_submitter_its_look_hid_the_poll_from | build EXIT=0 | run EXIT=101 | red: an_answer_wakes_the_submitter_its_look_hid_the_poll_from 
RESULT m2-the-wake-before-the-answer | cargo test -p toyos-sched-loom --test loom_watch an_answer_wakes_the_submitter_its_look_hid_the_poll_from | build EXIT=0 | run EXIT=101 | red: an_answer_wakes_the_submitter_its_look_hid_the_poll_from 
RESULT b1-park-without-the-polls | cargo test -p toyos-sched-loom --test loom_watch a_fire_racing_a_submitters_park_is_never_lost | build EXIT=0 | run EXIT=101 | red: a_fire_racing_a_submitters_park_is_never_lost 
RESULT b1-park-without-the-polls | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_poll_fired_as_it_is_renewed_waits_for_the_next_look a_watch_during_a_look_that_finds_bytes_answers_alone a_submitter_parks_only_with_nothing_it_could_answer 
RESULT b2-fire-wakes-before-it-takes-the-poll | cargo test -p toyos-sched-loom --test loom_watch a_fire_racing_a_submitters_park_is_never_lost | build EXIT=0 | run EXIT=101 | red: a_fire_racing_a_submitters_park_is_never_lost 
RESULT c1-admit-leaves-the-replaced-poll-armed | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_replaced_poll_is_withdrawn_from_its_watch 
RESULT c2-teardown-leaves-polls-armed | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_ring_torn_down_withdraws_every_poll 
RESULT c3-a-refusal-is-not-answered | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_handle_closed_since_its_watch_is_answered_with_the_refusal 
RESULT n5-cap-off-by-one | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_ring_keeps_its_cap_of_polls_and_no_more 
RESULT n4a-renew-beside-a-newer-watch | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_watch_during_a_look_that_finds_nothing_is_the_handles_one_poll 
RESULT n4b-settle-answers-a-replaced-poll | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_watch_during_a_look_that_finds_bytes_answers_alone 
RESULT r-park-ignores-a-full-ring | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_submitter_parks_only_with_nothing_it_could_answer 
RESULT p1-a-renewed-poll-ends-the-look | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_poll_fired_as_it_is_renewed_waits_for_the_next_look a_poll_armed_again_does_not_end_the_look 
RESULT p2-admit-replaces-nothing | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_ring_keeps_its_cap_of_polls_and_no_more a_replaced_poll_is_withdrawn_from_its_watch a_watch_during_a_look_that_finds_bytes_answers_alone a_watch_during_a_look_that_finds_nothing_is_the_handles_one_poll a_watch_replaces_a_poll_a_post_already_fired 
RESULT p3-admit-drops-every-other-poll | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_ring_keeps_its_cap_of_polls_and_no_more a_poll_fired_as_it_is_renewed_waits_for_the_next_look a_full_ring_leaves_a_fired_poll_for_the_next_look a_poll_left_standing_still_answers a_submitter_parks_only_with_nothing_it_could_answer 
RESULT p4-deliver-has-no-room-check | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_submitter_parks_only_with_nothing_it_could_answer a_full_ring_leaves_a_fired_poll_for_the_next_look 
RESULT pass-a-renewed-poll-is-looked-at-again | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_watch_during_a_look_that_finds_bytes_answers_alone a_poll_fired_as_it_is_renewed_waits_for_the_next_look 
RESULT p5-an-ended-poll-is-looked-at | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: an_ended_source_is_answered_gone_without_a_look 
RESULT p6-fire-does-not-record-its-post | cargo test -p kernel-loom --test inbox_answer | build EXIT=0 | run EXIT=101 | red: a_post_answers_for_an_object_with_nothing_to_look_at 
RESULT l1-poll-watches-every-entry | cargo test -p toyos-libc-copies poll_requests | build EXIT=0 | run EXIT=101 | red: poll_requests::a_descriptor_named_twice_is_watched_once_for_both_interests 
DONE
EXIT=0

The --metal-readback judges over b6b61967d's T14 readbacks (judge-results.txt)

HEAD b6b61967db1a427de1f76272544cb199bd724eb6
status before: []
RESULT base | cargo test --test toyos-build -- --metal --metal-readback /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-round/metal-base inbox_empty_write | EXIT=1
RESULT head-inbox | cargo test --test toyos-build -- --metal --metal-readback /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-round/metal-head-inbox inbox | EXIT=0
RESULT head-poll | cargo test --test toyos-build -- --metal --metal-readback /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-round/metal-head-poll poll | EXIT=0
status after: [ M tests/metal/lenovo-20w0003amz.toml]
DONE
restored tests/metal/lenovo-20w0003amz.toml (the head-inbox judge added boot.shared.* = 1151/3781/21245; not this brief's to commit)

Logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r5/.

The diagnostic runs behind "how long a peer held a kill" (explore/, on trees that were not clean: the arm with roster sampling added, in no commit)

explore-kill-noread-2  b6b61967d's kernel, 4 posting threads   DIAG before kill: parked=0 running=50   DIAG after kill: parked=0 running=200 gone=0, 354 ms   PASS (80s)
explore-kill-noread-3  f2c87688c's kernel, 6 posting threads   DIAG before kill: parked=0 running=50   DIAG after kill: parked=0 running=596 held_ms=4234   PASS (11s)
explore-kill-read-1    f2c87688c and the kill's read, 6 threads DIAG before kill: parked=0 running=50   DIAG after kill: parked=0 running=6 held_ms=13       PASS (5s)

5f62645ae's commit message says the four-thread child "was seen running in every roster sample for 70 s". The samples cover its first 354 ms; the 70 s is the length of that guest's run against a run with the read. The pull request body's table is the measurement.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 evidence at 5f62645ae, run by the orchestrator: the three boots the round staged, all of the head, each image's sha256 checked against the request before it was flashed, each flashed and booted once (cargo run --bin toyos-metal -- --image … --readback … --fat32-check from the worktree, clean before and after). Then the request's three judge commands over the readbacks, from the same clean worktree. Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71).

boot image sha256 loop boot tests, by name judge
kill_ends_every_wait, whole eba28098612662ece3f4624bd9e3d21d2b4a1de5c3f8cb8f990510c56e557416 EXIT=0, passed 1156 ms kill_ends_every_wait exit=0 … --metal-readback … kill_ends_every_wait: EXIT=0, 1 passed, 0 failed, 1 boot
inbox 76e624dc09b87214d29967d4cde5bec1c2b07c5617a8f09a7aad15373cb19890 EXIT=0, passed 1152 ms abuse_inbox 0, inbox_cancel_wakes 0, inbox_empty_write 0, inbox_log_post 0 … inbox: EXIT=0, 4 passed, 0 failed, 1 boot
poll df3ae50d766dc2d80317655927a1cc8cba06150e47d073e36fa6475a2cf55a64 EXIT=0, passed 1151 ms poll_wake_pipe 0, poller_capacity 0 … poll: EXIT=0, 2 passed, 0 failed, 1 boot
  • The new posted-poll arm on the machine, metal-kill_ends_every_wait/shared/kernel.log: :384 "posted-poll: killing" at 1.221, :401 "posted-poll: a kill ended it" at 1.222, :469 "kill_ends_every_wait: every wait a kill reached, it ended".
  • toyos-fat32-check: the log partition's bytes check out, on all three. Each image was armed with boot-deadline=120000.
  • These are the head's green arms. None of the request's four mutation patches was staged or booted.
  • Each judging of a green shared boot offered boot.shared.* rows for tests/metal/lenovo-20w0003amz.toml; the diffs are kept beside the readbacks and none is committed.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 5f62645ae against origin/main (b331934c9), round 5. Read, not run: the diff, every changed file whole, the round's logs under /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r5/, and the three T14 readbacks of #655 (comment).

Round-4 BLOCKERs

  • 1, a wait that goes round reads no kill: CLOSED for the code and the issue. Its test's control is the BLOCKER below.
    • submit reads current_kill_pending() beside the deadline (kernel/src/inbox/mod.rs:416-420). The issue file is gone and its slug is cited nowhere at the head.
    • T14 at this head: kill_ends_every_wait whole, exit=0 (metal-kill_ends_every_wait/shared/kernel.log:481). posted-poll is killed at 1.221 and exit: … pid=12 code=137 is at 1.222 (:384, :396, :401). Judge exit 0.
  • 2, the models run one submitter: CLOSED.
    • polls::complete writes and then wakes (kernel/src/inbox/polls.rs:234-237), and post_completion has one caller, Submitter::answer (mod.rs:592).
    • an_answer_wakes_the_submitter_its_look_hid_the_poll_from is green at the head and in --ci host (ci-host-5f62645ae.log:4237). m1 and m2 each exit 101 on it: "parked with 1 answer(s) written and no wake owed", left: [] (mutations/m1-…run.log, m2-…run.log).
  • 3, the log's arm has no test that can fail: CLOSED. inbox_log_post exit=0 on the T14 (metal-inbox/shared/kernel.log:416, :429). Under e1 it never exits and the 300 s ceiling ends it (local-qemu-log-e1.log); a hang is a completion verdict, which QEMU may give.
  • 4, two met issues not deleted: CLOSED. Both files and netd_stream.rs's paragraph are gone, and neither slug is cited at the head.
  • Round 4's NOTE on the interrupts-off issue and its REMOVEs are done. Its NOTE on the body's T14 lines recurs below.

What this verdict rests on

  • The three T14 boots at 5f62645ae, each image's hash equal to gates-results.txt's, and their judges, exit 0 each:
    • kill_ends_every_wait, 1 member;
    • inbox: abuse_inbox, inbox_cancel_wakes, inbox_empty_write, inbox_log_post exit=0 (metal-inbox/shared/kernel.log:359, :378, :396, :429);
    • poll: poll_wake_pipe, poller_capacity exit=0 (metal-poll/shared/kernel.log:359, :409).
  • Round 4's control of the whole change on the T14: 0c34f5e45, inbox_empty_write exit=101.
  • cargo run -- --ci host exit 0, "Host: 65 step(s), all green" (ci-host-5f62645ae.log:7190); the QEMU suite, 21 tests, exit 0 (guest-suite-5f62645ae.log). CI's host is skipped on a draft.

Net: +1700 −357.

  • Production +546 −200 (net +346); +34 −16 since round 4, the kill's read and complete moved into polls.rs. Accepted.
  • Tests and their gates +1076 −92. Issues +78 −65.

BLOCKER

    1. tests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs:57, :194-203, kernel/src/inbox/mod.rs:418 — posted-poll's verdict is a duration, and its red has been read only under QEMU — root CLAUDE.md lets only metal give a timing verdict, so the arm has no control on the one machine that judges it.
    • It is a duration. With the read reverted the child still ended by itself, posts still landing: after 4234 ms with six posters, and inside an 80 s run with four (explore/local-qemu-explore-kill-noread-3.log, -2.log). What the second separates is 13 ms from seconds.
    • In the tree it is judged where the rule allows it. shared_metal alone stages a shared-boot member (tests/toyos.rs:719), and the QEMU suite registers none (guest-suite-5f62645ae.log). The T14's green arm is 1 ms against the second.
    • The six QEMU runs (0, 0, 0 and 1, 1, 1) are a timing reading taken under QEMU. They are the cheap measurement that showed the hold and chose the ceiling. They are not the arm's control, and the body's "Not sure of" says what four threads hold on the T14 is not measured.
    • No model compiles submit's loop, so the T14 is the kill's read's only oracle.
    • Required: k1-the-wait-loop-reads-no-kill.patch on 5f62645ae, staged as the kill_ends_every_wait row and booted once on the T14. It must turn posted-poll red: TEST_END test_rs_kill_ends_every_wait exit=101, "the child still watched its pipe 1s after its kill", judge exit 1.
    • By reading I expect red: four posters on eight CPUs leave no gap as long as a pass of 256 looks. If it is green the arm cannot fail where it runs, and the arm changes, never the ceiling.
    • e1, a1 and a2 are owed no boot. e1's red is a hang and a1's and a2's are the tokens [1, 2]: completion and content, which QEMU gave (exit 1 each). What a1 and a2 mutate has its metal control in 0c34f5e45's boot.

NOTE

  • PR body, "The T14, at this head", Gates' three "2, staged" rows with "A staging run exits 2 …", and "Not sure of" 1 and 2 — false since the boots ran — the body takes the rows and judge exits of comment 5959249651, and the k1 boot's.
  • PR body, the hold table — 5f62645ae's message says the four-thread child was "seen running in every roster sample for 70 s"; the samples cover 354 ms of an 80 s run. The table is the measurement and no claim rests on the 70 s, so the figures are enough. The commit lands in main's history and the comment that corrects it does not: one sentence under the table names the commit and says which figure stands.
  • PR body, "Filed. The first three are on main already" — all four files are added by this diff (git cat-file -e origin/main:<file> fails for each); what main already has is the behaviour the first three describe.
  • tests/metal/lenovo-20w0003amz.toml — the boot.shared.* rows stay out of this pull request — nothing in this diff reads or moves them, the three judges offer three different triples for one key off boots of 1, 4 and 2 members, and main lacks the rows since da9e8c075 took a failed boot's numbers out. The holder of the metal suite records them from a whole green shared boot on main; a gap worth tracking is one issues/build/ file.

REMOVE

  • PR body, "That second is a hang ceiling and no measure of the kill." — the same paragraph says the child ended by itself after seconds, so the second reads how long.
  • tests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs:56 — ": a hang ceiling, for a hang that ends when its peers do" — the hold ended with its peers still posting (explore/), and the file's header already says what the ceiling is for.
  • PR body, "Not sure of", the boot.shared.* line — the harness's gap, not this change's record.

SEND BACK

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 negative control for posted-poll, run by the orchestrator as a metal mutation loop on 5f62645ae: clean worktree at the head; git apply --check and git apply of k1-the-wait-loop-reads-no-kill.patch; the kill_ends_every_wait row staged (cargo test --test toyos-build -- --metal --metal-readback <dir> kill_ends_every_wait, exit 2, staged); git apply -R, tree clean; the image flashed and booted once; the row's judge over its readback. Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71).

  • image sha256 6105b217c4fac8d1087db5c5e33f334159928064374058a4401ca2bd8736c3e6, armed with boot-deadline=120000; the boot itself: toyos-metal EXIT=0, passed, 1152 ms.
  • Red, as required: ===TEST_END test_rs_kill_ends_every_wait exit=101===. kernel.log:384 "posted-poll: killing" at 1.224; :390-391 at 2.225, "panicked at src/bin/kill_ends_every_wait.rs:199:13: posted-poll: the child still watched its pipe 1s after its kill: the posts held it in its wait".
  • The judge: EXIT=1, FAIL test_rs_kill_ends_every_wait: test_rs_kill_ends_every_wait exited 101 on the T14, [metal] 0 passed, 1 failed, 1 boot(s).
  • Against the head's own boot of the same row (comment 5959249651): killed at 1.221, ended at 1.222, exit=0, judge 0.
  • The worktree is clean at 5f62645ae afterwards; nothing is committed.

Japabu and others added 3 commits October 2, 2026 21:15
…y carries

The fifth review of #655 (issuecomment-5959593490) names the clause for
removal: ": a hang ceiling, for a hang that ends when its peers do". With the
kill's read reverted the hold ended with its peers still posting, and the file's
header already says what the ceiling is for. A comment alone; no behaviour
changes.

The same review's BLOCKER is answered by a boot and by the pull request's body,
not by the tree: the T14 booted `5f62645ae` with
`k1-the-wait-loop-reads-no-kill.patch` applied as the `kill_ends_every_wait`
row, and `posted-poll` went red there (issuecomment-5959632256).

That commit's message says the four-thread child was seen running in every
roster sample for 70 s. The samples cover the first 354 ms after its kill, in a
guest whose run took 80 s; the pull request's hold table is the figure that
stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
No conflict. `src/ci.rs` is the one file changed on both sides, in separate
hunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
No conflict. `src/build.rs` is the one file changed on both sides, in separate
hunks.

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

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Evidence for 4695a65fb: the round that answers the review of 5f62645ae (issuecomment-5959593490). The head is three commits past 5f62645ae: fac2352d8 (one clause of a doc comment), ce815516f (merge of dc8212c7f, #642) and 4695a65fb (merge of dd8738302, #647). Every result is the command's own exit, written by one script from a clean committed tree.

The gates, at each of the three heads (gates-results-<head>.txt)

HEAD fac2352d84bf28b1f524f1c50dc20bf77a62b4dd
status before: []
load before: 21:15  up 3 days, 9 hrs, 3 users, load averages: 3.39 2.61 3.30
RESULT cargo run -- --ci host | EXIT=0
RESULT cargo test --test toyos-build | EXIT=0
status after: []
HEAD ce815516f3d1e33ff8712e534ea2ec90e3306ffb
status before: []
load before: 21:21  up 3 days,  9:05, 3 users, load averages: 4.08 4.35 3.95
RESULT cargo run -- --ci host | EXIT=0
RESULT cargo run -- --build-only | EXIT=0
RESULT cargo test --test toyos-build | EXIT=0
status after: []
HEAD 4695a65fb6436e0005090206813d0c159ec63310
status before: []
load before: 21:32  up 3 days,  9:16, 3 users, load averages: 49.82 28.25 15.67
RESULT cargo run -- --ci host | EXIT=0
RESULT cargo run -- --build-only | EXIT=0
RESULT cargo test --test toyos-build | EXIT=0
status after: []

--ci host: "Host: 65 step(s), all green" at fac2352d8, "Host: 67 step(s), all green" at ce815516f and 4695a65fb. The QEMU suite: "21 passed, 21 total" at fac2352d8 and ce815516f, "25 passed, 25 total" at 4695a65fb.

This branch's three guest tests, one QEMU guest each, on the two merged heads (local-qemu-results-<head>.txt)

RESULT kill-merged | HEAD ce815516f (clean after) | load 28.15 20.00 11.43 | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=0
RESULT log-merged | HEAD ce815516f (clean after) | load 26.32 19.88 11.48 | cargo test --test toyos-build -- zz_local_inbox_log_post | EXIT=0
RESULT empty-merged | HEAD ce815516f (clean after) | load 25.58 20.04 11.69 | cargo test --test toyos-build -- zz_local_inbox_empty_write | EXIT=0
status after all: []
RESULT kill | HEAD 4695a65fb (clean after) | load 57.18 43.85 28.35 | cargo test --test toyos-build -- zz_local_kill_ends_every_wait | EXIT=0
RESULT log | HEAD 4695a65fb (clean after) | load 57.02 44.27 28.68 | cargo test --test toyos-build -- zz_local_inbox_log_post | EXIT=0
RESULT empty | HEAD 4695a65fb (clean after) | load 52.20 43.66 28.65 | cargo test --test toyos-build -- zz_local_inbox_empty_write | EXIT=0
status after all: []

At ce815516f the harness patch is local-qemu.patch, byte for byte the one in issuecomment-5959109143. #647 moved both of its sites in tests/toyos.rs, so at 4695a65fb it is the same two hunks on the new context:

local-qemu-4695a65fb.patch, in no commit

--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -177,6 +177,9 @@
     // no way to turn it back on, so only a machine QEMU reports stopping can
     // be asked. `machine_soft_off_decoded` reads the T14's own decode.
     "machine_shutdown",
+    "zz_local_kill_ends_every_wait",
+    "zz_local_inbox_log_post",
+    "zz_local_inbox_empty_write",
 ];

 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2246,6 +2249,24 @@
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
         "machine_shutdown" => power::machine_shutdown(test_config),
+        local if local.starts_with("zz_local_") => {
+            let member = &local["zz_local_".len()..];
+            let crate_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/toyos-rust-tests");
+            let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &crate_path, member);
+            let mut guest = QemuInstance::boot_with_options(
+                test_config,
+                &[],
+                &[(member.to_string(), bin)],
+                BootOptions { smp: 8, ..BootOptions::default() },
+            );
+            let result =
+                guest.run_test(&format!("test_rs_{member}"), Duration::from_secs(120));
+            eprintln!("  [local] exit {:?} error {:?}\n{}", result.exit_code, result.error, result.stdout);
+            match (result.exit_code, result.error) {
+                (Some(0), None) => Ok(()),
+                (code, error) => Err(format!("{member}: exit {code:?}, {error:?}")),
+            }
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }

local-qemu.sh and its caller

#!/bin/sh
# One QEMU guest (tests/testcases, 8 vCPUs) runs one shared-boot member on the clean committed head.
# The harness patch is checked, applied, run and reversed here, and the tree is shown clean after.
# usage: local-qemu.sh <label> <member> <harness patch>
set -u
W=/Users/jan/Dev/jan/toyos-winitstall
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r6
cd "$W"
label=$1; member=$2; patch=$3
h=$(git rev-parse --short=9 HEAD)
R="$D/local-qemu-results-$h.txt"
if [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "RESULT $label: TREE NOT CLEAN BEFORE" >> "$R"; exit 1; fi
git apply --check "$patch" || { echo "RESULT $label: HARNESS PATCH DOES NOT APPLY" >> "$R"; exit 1; }
git apply "$patch"
cargo test --test toyos-build -- "zz_local_$member" > "$D/local-qemu-$label-$h.log" 2>&1; ran=$?
git apply -R "$patch"
clean=dirty; [ -z "$(git status --porcelain --ignore-submodules=none)" ] && clean=clean
echo "RESULT $label | HEAD $h ($clean after) | load $(uptime | sed 's/.*load averages: //') | cargo test --test toyos-build -- zz_local_$member | EXIT=$ran" >> "$R"

#!/bin/sh
# The gates, then this branch's three guest tests one QEMU guest each, at one committed clean head.
set -u
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r6
W=/Users/jan/Dev/jan/toyos-winitstall
sh "$D/gates.sh"
cd "$W"
h=$(git rev-parse --short=9 HEAD)
P="$D/local-qemu-$h.patch"
sh "$D/local-qemu.sh" kill kill_ends_every_wait "$P"
sh "$D/local-qemu.sh" log inbox_log_post "$P"
sh "$D/local-qemu.sh" empty inbox_empty_write "$P"
echo "status after all: [$(git status --porcelain --ignore-submodules=none)]" >> "$D/local-qemu-results-$h.txt"
touch "$D/round-$h.done"

What this pull request's own diff changed since the T14 booted it: diff <(git diff b331934c9 5f62645ae) <(git diff dd8738302 4695a65fb), exit 1

1749c1749
< index 722d44ea5..58c5dfaf3 100644
---
> index 34429b4ad..c97643b86 100644
1764c1764
< index c1f902458..d9e4158b6 100644
---
> index 4ff74000b..2538f7a4b 100644
1920c1920
< index 3417cb5ac..e68635496 100644
---
> index 3417cb5ac..13cd5d9c2 100644
1978c1978
< +/// child outlived it: a hang ceiling, for a hang that ends when its peers do.
---
> +/// child outlived it.

No mutation ran this round and no T14 image was staged.
The ce815516f runs came from local-qemu.sh before it took the patch as an argument; it named local-qemu.patch itself and did the same steps.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 at 4695a65fb, run by the orchestrator. The head is 5f62645ae plus one comment clause and the merges of #642 and #647; #642 changed how a spawn mints handles, and kill_ends_every_wait spawns every child it kills, so the head's three boots are run again on the merged head. Staged from the clean worktree with cargo test --test toyos-build -- --metal --metal-readback <dir> <filter> (exit 2 each), each image's sha256 checked again before it was flashed, each toyos-metal exit 0; the worktree was clean before and after.

filter image sha256 members, exit judge
kill_ends_every_wait d926c499877e431159024f2184f886a099f3cdea20bec43c517d25159a399b24 kill_ends_every_wait 0 exit 0, 1 passed, 0 failed
inbox 420ad0bfc268dfcf38b5fd996f6ed420cec02ae438c9fcbe0af2100ab1e6b737 abuse_inbox 0, inbox_cancel_wakes 0, inbox_empty_write 0, inbox_log_post 0 exit 0, 4 passed, 0 failed
poll bfb4b2e7dfcedc4ac384687f9cbc24299e6f5fad24b2de67ed507ecf45d93f72 poll_wake_pipe 0, poller_capacity 0 exit 0, 2 passed, 0 failed

The posted-poll arm on this head (metal-kill/shared/kernel.log:381-401): parked at 1.206, looking at 1.228, killed at 1.228, "a kill ended it" at 1.229. Its red on this machine, with the wait loop reading no kill, is the k1 boot at 5f62645ae (#655 (comment)); no mutation was booted on the merged head.

Readbacks and judge logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r6/ (metal-kill, metal-inbox, metal-poll, judge-*.log).

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 4695a65fb against origin/main (dd8738302), round 6. Read, not run: the diff, every changed file at the head, both merges, the logs under /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r6/, the k1 readback under …/655-r5/metal-k1/, and the head's three T14 readbacks of #655 (comment).

Round-5 BLOCKER

  • 1, posted-poll has no control on the machine that judges it: CLOSED.
    • k1-the-wait-loop-reads-no-kill.patch on 5f62645ae, booted once on the T14, image 6105b217…c3e6: ===TEST_END test_rs_kill_ends_every_wait exit=101=== (655-r5/metal-k1/shared/kernel.log:435).
    • "posted-poll: killing" at 1.224 (:384); "the child still watched its pipe 1s after its kill" at 2.225 (:391); the child ends only once the posters have stopped, exit: … pid=12 code=137 cpu=1011ms (:404).
    • The judge: FAIL test_rs_kill_ends_every_wait, 0 passed, 1 failed, 1 boot(s), exit status 1 (655-r5/judge-metal-k1.log).
    • Its green arm on the same tree: killed at 1.221, ended at 1.222, judge exit 0 (comment 5959249651).
  • That red stands for 4695a65fb. No k1 boot of the merged head is required.
    • What the red runs through is the same source at both heads. git diff --stat 5f62645ae 4695a65fb -- kernel/src/inbox kernel/src/watch.rs kernel/src/sched toyos-sched kernel/src/syscall/io.rs kernel/src/object/ops.rs toyos/src/poller.rs rust tests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs lists two files: kernel/src/sched/kthread.rs, −17, open_selftest under boot-actuators; and the test's one doc clause.
    • process::kill_process (kernel/src/process.rs:1702) lies outside SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642's three hunks of that file, and kernel/src/syscall/dispatch.rs lost SYS_PROCESS_OPEN's arm and gained a test-actuators action. toyos-proclife, which the kernel compiles, changed in two #[cfg(test)] modules and two mutation features.
    • What SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642 did change, a spawn's commit, runs before the state the red needs. The head's own boot goes through it for every child and reaches that state: "waiting for the roster to show it looking" and "killing" at 1.228, exit: … pid=12 code=137 cpu=12ms at 1.229 (655-r6/metal-kill/shared/kernel.log:383-384, :396).
    • git apply --check of the patch at the head exits 0, its hunk at kernel/src/inbox/mod.rs:415: it would mutate the line it mutated.
    • It would take one boot, the patch on the head staged as the kill_ends_every_wait row, judge exit 1, if a merge had moved a path of that --stat, kill_process or the test's code. A later merge that does owes that boot.
  • Round 5's NOTEs and REMOVEs are done: the body's 5f62645ae rows and the k1 row, the 70 s sentence, "Filed, all four by this diff", no tests/metal/ file in the diff, the two body lines gone, and fac2352d8's one clause. Its first NOTE recurs below for this head.

What this verdict rests on

  • cargo run -- --ci host exit 0 at 4695a65fb, "Host: 67 step(s), all green" (ci-host-4695a65fb.log:7237). In it: inbox_answer 16 passed (:757), loom_watch 12 passed (:4229), post-is-an-answer 3 verdicts (:5726), commit-ignores-notify 2 (:6124), and SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642's two toyos-proclife controls red on the merged tree (:6632, :6658).
  • cargo run -- --build-only exit 0; the QEMU suite exit 0, 25 passed (gates-results-4695a65fb.txt, guest-suite-4695a65fb.log). CI's three checks are skipped on a draft.
  • The T14 at 4695a65fb, each image's hash in metal-*.sha256 the one the comment names, each judge without a failure:
    • kill_ends_every_wait exit=0 (metal-kill/shared/kernel.log:481), 1 passed;
    • abuse_inbox, inbox_cancel_wakes, inbox_empty_write, inbox_log_post exit=0 (metal-inbox/shared/kernel.log:359, :378, :396, :429), 4 passed;
    • poll_wake_pipe, poller_capacity exit=0 (metal-poll/shared/kernel.log:359, :409), 2 passed.
  • Both merges are merges and nothing else: git show --cc of ce815516f and of 4695a65fb is empty, and each commit's tree is the one in merge-tree.txt and merge-tree-2.txt. src/ci.rs and src/build.rs are the only files changed on both sides.
  • This pull request's own diff since round 5: diff <(git diff b331934c9 5f62645ae) <(git diff dd8738302 4695a65fb) is the doc clause and three index lines.

Net: +1700 −357, as at round 5.

  • Production +546 −200 (net +346), accepted there.
  • Tests and their gates +1076 −92. Issues +78 −65.

BLOCKER

  • None.

NOTE

  • PR body, "No image of the head has booted on the T14.", the three bullets under it, and "Not sure of", first bullet — false since comment 5960216590 — the body takes the head's three rows with their image hashes and judge exits, three Gates rows at 4695a65fb, and in the bullets' place one sentence: k1 booted at 5f62645ae alone, and the --stat above is what carries it to the head.
  • tests/toyos-rust-tests/src/bin/sched_stress.rs:49, handle_lifetime.rs:140, :299, :320, soundd_log_stall.rs:94 — three more binaries name the poller or the ring, and no head of this branch has booted one of them (the ten readbacks under 655-round/, 655-r5/ and 655-r6/ hold seven names) — each is booted at the head before landing and the body takes its row.
    • sched_stress on the shared boot: the one assertion that an acceptor's watch answers for its own port's connection and no other's.
    • handle_lifetime on shared-debug: a ring under one close, under both, and eight of them under a kill, against InboxRef's drop as this diff leaves it.
    • The soundd_log_stall row: a watch on a connection under a deadline.
    • A NOTE and no BLOCKER: found outside what changed since round 5, the QEMU suite is whole and green, and by reading each passes. An acceptor's look reads has_pending, which a queued connection holds until pop (kernel/src/object/ops.rs:757, kernel/src/object/port.rs:72, :86); a submit of nothing returns at kernel/src/inbox/mod.rs:399. A red there is a defect, and the branch comes back with it.

REMOVE

  • None.

LAND AFTER NAMED CHANGES

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 at 4695a65fb, the round-6 review's NOTE 2, run by the orchestrator: the three rows that name the poller or the ring and that no head of this branch had booted. Each staged from the clean worktree with cargo test --test toyos-build -- --metal --metal-readback <dir> <row> (exit 2), each image's sha256 checked again before it was flashed, each toyos-metal exit 0; the worktree was clean before and after.

row boot image sha256 member, exit judge
sched_stress shared ce24bf2a42f8f6cc901e82cff742abd302cb47c58072971f2102a9650d257518 sched_stress 0 exit 0, 1 passed, 0 failed
handle_lifetime shared-debug f30dccabbc1028d60519aad70e60ff5bbeede89e287efc62d812a233793a404a handle_lifetime 0 exit 0, 1 passed, 0 failed
soundd_log_stall logstallcase b8813f05efe3140f260ea956f499c82eaf3073674603cd7775831401f0b2964d soundd_log_stall 0 exit 0, PASS soundd_log_stall, 1 passed, 0 failed

Readbacks and judge logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/655-r6/ (metal-sched_stress, metal-handle_lifetime, metal-soundd_log_stall, judge-metal-*.log).

@Japabu
Japabu marked this pull request as ready for review October 2, 2026 20:11
@Japabu
Japabu enabled auto-merge October 2, 2026 20:11
@Japabu
Japabu added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 5daab30 Oct 2, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-winitstall branch October 2, 2026 21:02
Japabu added a commit that referenced this pull request Oct 2, 2026
No conflict: git merged both files #655 and this branch share on its own.
#655 adds `OP_WATCH`'s doc in `toyos-abi/src/inbox.rs` directly above the
`Op code 2 unused` comment this branch deletes, and moves lines above
`Op::from_raw` in `kernel/src/inbox/mod.rs`, whose `2 is retired` comment this
branch deletes. Both of #655's additions and both of this branch's deletions
are in the result; `git diff origin/main` over the merged tree is the 23 files
the branch changed before the merge, 103 insertions and 269 deletions.

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
Japabu added a commit that referenced this pull request Oct 2, 2026
60ec86d was written against a kernel where an object's post completed
every poll on its watch: the publish's post was the answer to a watch on a
child, and has_data's Process arm mattered only to a registration made
after the end. #655 took the answer from the post. A post fires a poll,
which owes it a look, and the ring's own submitter writes the completion
after reading ops::has_data again; an object not ready at the look is
armed again (kernel/src/inbox/mod.rs, Submitter::look).

Nothing this branch added became unnecessary. What each line is for
moved:

- has_data's Process arm, ProcessObject::finished, is what the look of
  every watch on a process reads, not the late registration's alone.
- read_watch's Process arm is what lets a watch on a live child register:
  inbox::arm refuses NotSupported a poll with no readiness and no watch.
- read_posts_are_readiness keeps a Process in main's false arm, with no
  change here: the kernel holds the fact the look reads, so the post is
  not the readiness.
- close_ends_polls answers false for a Process, as before: ops::close
  cancels every poll on the read watch of a kind it answers true for.

publish_exit stores `finished` before it posts, as it already did for
SYS_PROCESS_WAIT's predicate; the look rests on the same order.

The prose that had the post answer goes: read_watch's comment, and the
sentence of process.rs's header in which the publish answered the watch.
The watch's field doc is main's sentence with the reason for its own Arc.

process_lifecycle's first arm watches each child once, not again each
round. On #655's kernel a watch replaces its handle's earlier poll, so
the re-watch made every completion the newest registration's; watched
once, the arm also holds that a standing poll outlives two other
children's ends.

The track's stage 1 is cut to what is left of it: init's waiter threads.

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 cut the guest suite to 26 tests, deleted the harness that served the
rest, moved CI onto one guest workflow and landed #655, so most of what
this branch tightened no longer exists. Every hunk of the branch is
accounted for below: one whose subject main deleted goes, one whose
subject is still there stays.

Modify/delete, thirteen files, each deleted with main. Every hunk in
them is a ceiling of a test main deleted:
- tests/common/blockd.rs, tests/toyos-rust-tests/src/bin/blockd_io.rs:
  the role ceilings and BENCH_BLOCKS of blockd_serves_partitions,
  blockd_survives_its_death, blockd_serves_nothing and
  blockd_lends_within_its_bound.
- tests/common/clang.rs (c_hello), console.rs (CEILING), https.rs
  (run_guest), logread.rs (CEILING), origin.rs (twelve waits of the
  log_program tests), partclaim.rs (four run bounds), pkg.rs (two),
  storage.rs (five), swap.rs (CEILING), wallclock.rs (two).
- tests/common/orphan.rs: guest_dies_with_its_harness's wait behind the
  owner's boot ceiling, the one caller of `qemu::backstop` outside the
  harness.

Content conflicts, taken from main and the surviving hunks applied again:
- .github/workflows/nightly.yml: main's. The macOS `host` job and the
  `tcg` job the branch bounded are gone; `host` is the cache's writer on
  Linux and `tcg` calls guest.yml, whose TCG arm has not run yet.
- tests/common/devices.rs, iommu.rs, logstream.rs, power.rs, usb.rs,
  volumes.rs: main's. WAIT, CHAIN_WAIT, WEDGE_DEADLINE, LOCKUP_DEADLINE,
  FLOOD_CEILING and every run bound the branch moved belonged to QEMU
  tests main deleted.
- tests/common/qemu.rs: the width goes (`WIDTH`, `set_width`, the free
  `budget`), `budget_smp` states the rule, the boot ceiling is
  `BOOT_CEILING`, and the backstop is `(ceiling * 2).max(ceiling +
  GUEST_QUIET)` with a death named at it. `qemu::Ceiling` and
  `backstop()` go: main deleted every wait that ended through them
  (toolkit_winit_loop, toolkit_winit_pace, lan_no_lease, kernel_log_file,
  usb_flush_optional).
- tests/checks/qemu.rs, tests/checks.rs: the backstop's cases and the
  two walks stay; `capture_ceiling_self_check` goes with `Ceiling`.
- tests/toyos.rs: the LAN rows, the shared boots' sizes, `set_width`'s
  call and the summary's "x width" stay. The ceilings of the tests main
  kept are main's numbers here, no longer multiplied by the width; the
  next commit sets them by measurement. Everything else the branch
  changed in this file was a deleted test's.
- tests/common/lan.rs, tests/common/metal.rs: `lan_dhcp_lease` and
  `lan_message_delivery` ride the talking boot, and an image's bound is
  `metal::bound_for`. The swap's kill goes: main deleted the swap.
- src/build.rs: `tests/lancase` and `tests/lanicscase` leave
  ALL_CONFIGS, and the Intel-actuator gate is one flag.
- tests/metal/lenovo-20w0003amz.toml: the rows of `ccorpus-2` and
  `lanicscase` go, `boot.shared.*` is added.

What changed against the branch because main moved:
- `tests/lanleasecase`, its `lan_lease_report` row, `leased_on_metal`,
  `Readback::log_volume_file` and `lan_hold` stay as main has them. The
  branch deleted the boot and kept netd's `--exit-with-lease` for a QEMU
  registration main has since deleted, so deleting the boot now means
  deleting the probe, `toyos_i219::lease` and `phy::Outcome` with the
  driver tests that assert its codes. The issue is renamed and says so.
  `deaf_cpu_hold` is not made: `lan_hold` still holds two boots.
- Two issue files the branch filed are not carried:
  client-death-ends-before-its-reaped-creators-request-is-served.md is
  about a test main deleted, and
  winit-loops-output-stopped-reaching-the-log-after-stage-1.md is the
  stall #655 found the cause of and fixed.
- tests/lantalkcase/system.toml loses the sentence about
  `tests/e1000talkcase`, which main deleted.

Clean merges kept: `--provoke-message` out of netd and toyos-i219,
`toyos_tco::STAGED_BOUND_MS`, the metal loop's port poll and its wait
after a refusal, readdir_bound's `/home` arm with its issue, the
`HARD_LOCKUP_BOUND_MS` issue, and the two issue files the LAN fold
closes, the lanicscase boot's and the cable judge's unreadable records.

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