Repository navigation
inbox: a watch is answered after a look at its object, never by a post - #655
Conversation
…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
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
|
Review of 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
NOTE
REMOVE
SEND BACK |
…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
|
Review of Round-1 BLOCKERs
Net lines: +428 −37 (net +391). Production: +133 in BLOCKER
NOTE
REMOVE
SEND BACK |
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
|
Answers to the review of 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 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
Both are green on the branch and red under 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 BLOCKER 3, the two BLOCKER 4, the NOTEs
REMOVEs
Not measured by me: guest runs. The kernel's look has no host test:
|
|
Review of CI: run 36835197361 at Round-2 BLOCKERs
Net: +825 −205 (net +620).
BLOCKER
NOTE
REMOVE
SEND BACK |
…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
|
Mutation patches for the round at
The review's two kernel mutations, and the control, in one QEMU guest. No host test reaches them, so
The control commit is on no branch. It is Its tree is The T14 is the verdict's tier: the control and the head are staged for it, and
|
… 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
|
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 (
|
|
Review of Round-3 BLOCKERs
The two issues the round left open
Net: +1376 −241.
BLOCKER
NOTE
REMOVE
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
|
Evidence for The 19 patches of the earlier evidence comment (
--- 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);
}
--- 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,
--- 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
--- 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
--- 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}")),
}
}
#!/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 ( The host mutation runs ( The Logs: The diagnostic runs behind "how long a peer held a kill" (
|
|
T14 evidence at
|
|
Review of Round-4 BLOCKERs
What this verdict rests on
Net: +1700 −357.
BLOCKER
NOTE
REMOVE
SEND BACK |
|
T14 negative control for
|
…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
|
Evidence for The gates, at each of the three heads (
This branch's three guest tests, one QEMU guest each, on the two merged heads ( At
--- 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}")),
}
}
#!/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: No mutation ran this round and no T14 image was staged. |
|
T14 at
The Readbacks and judge logs: |
|
Review of Round-5 BLOCKER
What this verdict rests on
Net: +1700 −357, as at round 5.
BLOCKER
NOTE
REMOVE
LAND AFTER NAMED CHANGES |
|
T14 at
Readbacks and judge logs: |
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
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
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
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
A reader could stall on readiness it had already used up. A ring answered
READABLEfor bytes the reader had already read, and its blocking read then waited for good: the terminal stalled this way inWindow::recv_eventin #638'stoolkit_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
poll(), std'sReadand every reader outsidetoyosexposed. 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).kernel/src/inbox/polls.rs).inbox_submittakes every fired poll and looks at its object. It answers with the directions ready now, or arms a new poll in its place.polls::awake).toyos-sched/src/watch.rs).polls::complete).polls::completewrites a completion and then posts the watch the submitters park on. It is the one writer of a completion, for a watch's answer, aNOP, an accept and every refusal;Submitter::answerwrites and wakes nobody.inbox::submit).watch::wait_untilreturns on a true predicate before it reads the kill, andsubmit'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.submitreads the caller's kill beside its deadline, asops::until_answereddoes. A peer cannot delay a kill.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.ops::read_posts_are_readiness).issues/kernel/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md, whose exit names its test).Poll::postedrecords which watch posted, so a registrant's own look never counts as a post.ops::has_dataandops::has_space, which a blocking read or write waits on. A process handle has no read watch, andOP_WATCHon one is refused-NotSupported, as on main.-NotFoundat the look. It does not get the handle fault that a submission naming such a handle gets.Lock. No interrupt handler reaches them: a handler's fire posts the ring's watch and writes nothing.toyos::pollerdiffers from main's in one doc sentence. fsd's per-acceptor probe, which asked whether an answer was true beforeaccept, is deleted.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.issues/kernel/a-zero-byte-pipe-write-wakes-the-readers-watch.md: a wake completes a watch only if its direction is ready, andinbox_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 bya_watch_replaces_a_poll_a_post_already_fired.Pollerhands that end out as a token (issues/kernel/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md);issues/isolation/a-ring-handed-to-another-process-resolves-its-watches-in-the-receivers-table.md);toyos::PollerisSync, 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);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
b6b61967db6b61967dis this branch before the kill's read, the answer's wake inpolls.rsand the log's test. The T14 booted three images of it (the boots). The negative control is the whole change reverted:0c34f5e45isa31eec595, themainthat head merged, plusinbox_empty_write.rsand nothing else.cargo test --test toyos-build -- --metal --metal-readback <dir> <row>over its readback0c34f5e45inbox_empty_writeexit=101, tokens[1, 2]against[2]inbox_empty_write: exit 1b6b61967dabuse_inbox,inbox_cancel_wakes,inbox_empty_writeexit=0inbox: exit 0b6b61967dpoll_wake_pipe,poller_capacityexit=0poll: exit 0The T14, at
5f62645aeThe 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.kill_ends_every_wait5f62645aeeba28098612662ece3f4624bd9e3d21d2b4a1de5c3f8cb8f990510c56e557416kill_ends_every_wait, all six waits, exit=0inbox5f62645ae76e624dc09b87214d29967d4cde5bec1c2b07c5617a8f09a7aad15373cb19890abuse_inbox,inbox_cancel_wakes,inbox_empty_write,inbox_log_postexit=0poll5f62645aedf3ae50d766dc2d80317655927a1cc8cba06150e47d073e36fa6475a2cf55a64poll_wake_pipe,poller_capacityexit=0kill_ends_every_wait5f62645aeandsubmitreads no kill (k1)6105b217c4fac8d1087db5c5e33f334159928064374058a4401ca2bd8736c3e6kill_ends_every_waitexit=101posted-poll's verdict is a duration, which only the T14 may judge, and the fourth boot is its negative control there:k1reverts the kill's read ininbox::submitand nothing else. From the twokill_ends_every_waitreadbacks'kernel.log::384),exit: … pid=12 code=137at 1.222 (:396), "posted-poll: a kill ended it" at 1.222 (:401).: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
4695a65fbThe 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-metalexit 0 (the first three, the second three).kill_ends_every_waitsharedd926c499877e431159024f2184f886a099f3cdea20bec43c517d25159a399b24kill_ends_every_waitexit=0inboxshared420ad0bfc268dfcf38b5fd996f6ed420cec02ae438c9fcbe0af2100ab1e6b737abuse_inbox,inbox_cancel_wakes,inbox_empty_write,inbox_log_postexit=0pollsharedbfb4b2e7dfcedc4ac384687f9cbc24299e6f5fad24b2de67ed507ecf45d93f72poll_wake_pipe,poller_capacityexit=0sched_stresssharedce24bf2a42f8f6cc901e82cff742abd302cb47c58072971f2102a9650d257518sched_stressexit=0handle_lifetimeshared-debugf30dccabbc1028d60519aad70e60ff5bbeede89e287efc62d812a233793a404ahandle_lifetimeexit=0soundd_log_stalllogstallcaseb8813f05efe3140f260ea956f499c82eaf3073674603cd7775831401f0b2964dsoundd_log_stallexit=0PASS soundd_log_stall, 1 passed, 0 failed, 1 bootposted-pollon 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.k1booted at5f62645aealone: the round-6 review's--statover the wait loop, the park, the scheduler, the post path,kill_processand 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:fac2352d8deletes one clause of a doc comment in a test binary.git diff --stat 5f62645ae fac2352d8liststests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs, 1 insertion and 1 deletion, and no other file.ce815516fmergesorigin/mainatdc8212c7f(SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642) with no conflict. That is 28 files, +473 −400, and the kernel's spawn path and handle table are among them.4695a65fbmergesorigin/mainatdd8738302(AArch64 stage 5: PSCI resets and powers the machine off, behind one reset and power-off seam #647) with no conflict. That is 42 files, +1188 −354, and the kernel's reset and power-off paths are among them.This pull request's own diff is the booted one but for that clause:
git diff b331934c9 5f62645aeandgit diff dd8738302 4695a65fbdiffer in the comment's line and in threeindexlines (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.sys_write's post, the look'sresolvein the process's handle table andpipe::has_dataname the kernel and compile in no host crate. The host models drivepolls.rsagainst a fake look.inbox_log_post: a reader arms a watch on the endowedlogreadcapability, ends a process, is handed its token bywait(1, u64::MAX), and reads the kernel'sexit:record of that pid.ops::read_posts_are_readiness). WithSysCapmoved to thefalsearm (e1) the wait never returns.klogd's post, the capability'sresolveand that match name the kernel.a_post_answers_for_an_object_with_nothing_to_look_atruns the model's own flag.kill_ends_every_wait, theposted-pollwait: 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.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/testcaseswith 8 vCPUs and runs one member (local-qemu.patchin 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.cargo test --test toyos-build -- zz_local_<member>kill_ends_every_wait5f62645ae, three runskill_ends_every_waitsubmitreads no kill (k1), three runsinbox_log_post5f62645aeinbox_log_poste1)inbox_empty_write5f62645aeinbox_empty_writea1)[1, 2]inbox_empty_writea2)[1, 2]kill_ends_every_wait4695a65fbinbox_log_post4695a65fbinbox_empty_write4695a65fbThe three
4695a65fbrows ran on the same host at load 52 to 57, with the harness patch moved onto #647'stests/toyos.rs(the runs and the patch). Thek1rows 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'sk1boot 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.
submitreads no killsubmitreads no killsubmitreads the killCommit
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'sa_fire_racing_a_submitters_park_is_never_lostcovers the park. It compiles the kernel'spolls.rsand runsPoll::fire,Polls,deliverandawakeagainst the real watch and park: two interrupt-handler posts fire two polls of one ring while its submitter waits for both answers.commit-ignores-notifyis its control, kept red by--ci host.toyos-sched-loom'san_answer_wakes_the_submitter_its_look_hid_the_poll_fromcovers two submitters. They run on two CPUs against one handler's post of one poll, each waiting for one answer, through the samedeliver,awakeandcomplete. A submitter that parks is owed exactly one wake, by the fire or by the answer.kernel-loom'spoll_onceandtoyos-sched-loom'san_end_racing_a_post_*cover the one-shot a fire, a recheck, an end and a withdrawal race for;poll-fire-load-storeis their control.kernel-loom'sinbox_answercovers 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-answeris its control: 3 verdicts red, kept red by--ci host.fs/eventpoll.c'sep_send_eventspolls every ready item again withep_item_pollbefore 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, andinbox_empty_writeis 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.b1ran against both models. The last column names a test each run failed; the patches and every red are in the evidence comments (m1,m2and the table, the other patches).m1an_answer_wakes_the_submitter_its_look_hid_the_poll_fromm2an_answer_wakes_the_submitter_its_look_hid_the_poll_fromb1a_fire_racing_a_submitters_park_is_never_lost,a_submitter_parks_only_with_nothing_it_could_answerb2a_fire_racing_a_submitters_park_is_never_lostc1a_replaced_poll_is_withdrawn_from_its_watchc2a_ring_torn_down_withdraws_every_pollc3a_handle_closed_since_its_watch_is_answered_with_the_refusal>=becomes>n5a_ring_keeps_its_cap_of_polls_and_no_moren4aa_watch_during_a_look_that_finds_nothing_is_the_handles_one_polln4ba_watch_during_a_look_that_finds_bytes_answers_alonepassa_poll_fired_as_it_is_renewed_waits_for_the_next_lookra_submitter_parks_only_with_nothing_it_could_answerp1a_poll_armed_again_does_not_end_the_lookp2a_watch_replaces_a_poll_a_post_already_firedp3a_poll_left_standing_still_answersp4a_full_ring_leaves_a_fired_poll_for_the_next_lookp5an_ended_source_is_answered_gone_without_a_lookp6a_post_answers_for_an_object_with_nothing_to_look_atpollwatches every entryl1a_descriptor_named_twice_is_watched_once_for_both_interestsGates
cargo run -- --ci host, 67 steps4695a65fbcargo run -- --build-only4695a65fbcargo test --test toyos-build, the QEMU suite, 25 tests4695a65fb4695a65fbcargo test -p kernel-loom --test inbox_answer, 16 tests5f62645aecargo test -p toyos-sched-loom --test loom_watch, 12 tests5f62645ae5f62645ae5f62645aerows5f62645aekill_ends_every_wait, 1 member5f62645aeinbox, 4 members5f62645aepoll, 2 members5f62645aekill_ends_every_wait, 1 member5f62645aeandk1kill_ends_every_wait,inbox,poll: 1, 4 and 2 members4695a65fbsched_stress,handle_lifetime: 1 member each4695a65fbsoundd_log_stall4695a65fbThe merges
origin/mainatdd8738302(#647) is merged (4695a65fb): no conflict, andsrc/build.rsis the one file changed on both sides, in separate hunks. Before it,dc8212c7f(#642,ce815516f): no conflict, andsrc/ci.rsis the one file changed on both sides, in separate hunks ofCONTROLS. 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'sStagedring inkernel/src/inbox/mod.rs, its module inkernel/src/watch.rs, and fsd's four test actuators, where both sides' deletions stand.src/ci.rs's control row takes #668'sFailsvalues, and main's two Finder issues stand in place of this branch's one.Not sure of
posted-poll's red is judged at5f62645aealone. No mutation was booted on the merged head; a later merge that moves the wait loop, the park, the post path,kill_processor 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::awakereads 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