Skip to content

An IPC setup no longer takes back a handle it installed: connect, inbox setup, pipe and port - #758

Merged
Japabu merged 3 commits into
mainfrom
wt/toyos-ipcrollback
Oct 8, 2026
Merged

Japabu merged 3 commits into
mainfrom
wt/toyos-ipcrollback

Conversation

@Japabu

@Japabu Japabu commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes a traced-but-unexecuted kernel defect from the kernel audit (pull request #754, closed without landing): an IPC setup rollback that panicked the kernel when a sibling thread closed the freshly installed handle. The issue file is added and deleted inside the branch, so the net diff carries none. Head 86cdd0c09; the gates were measured on base origin/main 017451624.

The defect

sys_namespace_open's connect and sys_inbox_setup each installed a handle under the process-data lock, released the lock, ran a step that could fail (a queue push; a copy-out to a user address), and on failure closed the handle with ops::close(..).expect(..). The install was visible to sibling threads the instant the lock dropped. A sibling of the same process on another CPU closing or moving that handle in the window turned the rollback's correctly detected stale handle into a kernel panic.

What changed, per decision

  • connect (kernel/src/syscall/ipc.rs, kernel/src/object/port.rs): one with_process_data hold asks has_room(1), pushes the server's half onto the port, then installs the client's half. Nothing is taken back on any arm. An expect remains ("room was asked for first"): it was an expect on a state a sibling thread reaches, and is now one on a state the hold owns. A refused push hands the PendingConnection back (Connector::push returns it), so both halves drop after the hold is given up, their pipes with them.
  • inbox setup (ipc.rs, dispatch.rs): the dispatcher takes the answer's user window before the inbox exists, as the other copy-out syscalls do; sys_inbox_setup takes &mut UserBytesMut, and once the handle is installed it is never taken back.
  • The answer's refusals moved (dispatch.rs, the SYS_INBOX_SETUP arm), because the window is ctx.user_bytes_mut and no longer UserAddr::checked then copy_out:
    • an out outside the user half was InvalidArgument and is now BadAddress;
    • an out that is unaligned for InboxSetup, or whose 16 bytes straddle a 2 MiB page, was BadAddress (copy_out's is_user_object) and is now written.
      Both are what SYS_DEVICE_DMA_ALLOC's window answers.
  • sys_pipe and sys_port_create (review round 1's NOTE, folded in): both installed one handle and took it back when the second was refused, sys_pipe through the tree's last ops::close(..).expect("…installed a moment ago"). Both now call one install_pair, which asks has_room(2) before installing either. A refused pair installs nothing, takes nothing back and spends no slot generation, and its two objects drop after the hold. No handle in kernel/src/syscall/ipc.rs is installed and then taken back any more.
  • abuse_handle_table gains the arm that reaches that refusal, which no test did: at the cap and again with one slot free, a pipe and a port are each ResourceExhausted; then the one slot is still free, and is the only one. It is an arm of an existing member of the T14's shared boot, not a new guest test.

No model or actuator in the tree reaches the old race (loom compiles neither the handle table nor Lock's spin; no SYS_DEBUG action holds a thread between install and the fallible step), and the window is gone by construction, so no red test of the race itself exists.

Gates, at 86cdd0c09

Logs are in the round's scratchpad, orch/ipcrollback-r1/.

command exit log
cargo run -- --ci host 0 — [ci] Host: 77 step(s), all green ci-host.log
cargo test --test toyos-build -- machine_shutdown 0 — machine_shutdown, machine_shutdown_short_stop: 2 passed, 2 total guest-machine_shutdown.log
CI run 37756991371 at 86cdd0c09, merged into main 2e781a49d (checkout 2c68eae) host success (job 113243871684), toolchain / build success (113243872163), guest / suite success (113245223481): test result: ok. 30 passed, 30 total, [ci] Guest: 5 step(s), all green the run

The whole guest suite ran in CI at this head (the row above), not locally. The two tests above are the QEMU tests that reach these paths by name: every boot's supervisor and servers connect through sys_namespace_open and make their Poller's inbox through sys_inbox_setup, on the success arm only.

The refusal arms run in no test the QEMU suite boots. abuse_connect_flood, connect_before_serve, abuse_inbox, abuse_handle_table and handle_lifetime are members of the T14's shared and shared-debug boots, and ran there at this head: see "Independent oracle" below.

The pair refusal's mutation and control, on a kernel that is not the shipping one

Not a gate, not the T14 and not what the negative control rests on: two runs of cargo test --test toyos-build -- --debug, which boots one x86-64 TCG guest on the debug-wait kernel, its debugger gate released with lldb.

arm member result file
mutation: install_pair asks has_room(1) abuse_handle_table red: kernel panic: PANIC: panicked at src/syscall/ipc.rs:42:62: room was asked for first: ResourceExhausted, from sys_pipe debug-mutation-results.txt
control: both pair rollbacks restored (ipc.rs as at e556d5955) under the new arm abuse_handle_table exit_code: Some(0): the refusal answers what it did before the fold-in debug-control-results.txt

Each patch was applied with git apply --check, built, run and reversed by one script (arm.sh), which printed RESTORED=0 status:[]. The patches are posted as a comment on this pull request.

High-risk checks (syscalls, concurrency)

  • Negative control: for the race, none executes: the fix reverted restores the expect across a dropped lock, and nothing in the tree orders a sibling's close inside that window; review round 1 read the window gone by construction. For the pair refusal no mutation is owed (has_room(1) before two installs is what a reader of the diff catches); the control above says the refusal's answer did not change.
  • Independent oracle: real hardware. The orchestrator booted the T14's shared and shared-debug images, staged from the clean tree at this head (sha256 dce10535…0bcc7a41 and 47334de3…5145d8ad), each toyos-metal exit 0 and verdict.txt passed: shared 62 of 62 and shared-debug 7 of 7 ===TEST_END lines exit=0, among them abuse_connect_flood, connect_before_serve, abuse_inbox, abuse_handle_table, abuse_listener_hijack, process_lifecycle and, on shared-debug, handle_lifetime; the lines connect refused after 32 unaccepted, and resumed once one was drained and handle table capped at 4096 on every insert path (refused at 4096); no PANIC record and no room was asked for first in either boot (An IPC setup no longer takes back a handle it installed: connect, inbox setup, pipe and port #758 (comment)). Read whole by exit and TEST_END lines; the harness's by-name judge was not run over them.

Unsure

  • connect_before_serve's Gone may be answered by connect_through's closed() check before the hold and not by the push's Closed arm; the two answer the same word and no test tells them apart, so the push's Closed arm is read, not measured.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A

Japabu and others added 2 commits October 8, 2026 10:08
…ew handle

Carried from the kernel audit's branch (pull request 754), which traced it
without executing it, so the record lands with the commit that closes it.

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

`sys_namespace_open`'s connect and `sys_inbox_setup` installed a handle under
the process-data lock, released it, ran a step that could fail, and on failure
closed the handle with `ops::close(..).expect(..)`. The install was visible to
sibling threads the instant the lock dropped, so a sibling closing or moving
that handle in the window turned the rollback's correctly detected stale handle
into a kernel panic — untrusted ordering crashing the kernel where it must
refuse.

Both now commit all-or-nothing under one hold, so there is no window and no
rollback:

- connect reserves a handle slot (`has_room`), pushes the server's half onto the
  port, and installs the client's half — all inside one `with_process_data`. The
  room is reserved before the fallible push, so the install cannot fail; a
  refused push drops both halves after the hold is given up, their pipes with
  them. The dropped `expect` is replaced by an invariant, not a second code path.
- inbox setup takes the answer's user window in the dispatcher (as the other
  copy-out syscalls do), before the inbox exists, so a bad address refuses
  before anything is installed. The handle, once installed, is never taken back.

`Connector::push` now hands a refused `PendingConnection` back so its pipe ends
are not dropped under the caller's handle-table lock.

Closes the carried issue; deletes its file.

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

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 of wt/toyos-ipcrollback at e556d5955 against origin/main 017451624. Read only: no test or build run.

Net: 3 files, +38 −41. Production −3, tests 0. The issue file is added and deleted inside the branch, so the net diff carries none.

BLOCKER

  • PR body, Gates — cargo run -- --ci host is absent — the body carries a narrowed cargo clippy on the kernel alone, and its log has no exit line; the host check is owed with command, exit code and log at this head.
  • PR body, Gates — the guest suite is absent — connect is on every boot's path and the body carries two tests (machine_shutdown, machine_shutdown_short_stop); guest / suite at this head is owed.
  • kernel/src/syscall/ipc.rs:263-268 — the refused-push arm, the one piece of logic this branch rewrote, has never executed at this head — nothing the QEMU suite boots fills a port's queue or connects to a closed port; abuse_connect_flood (QueueFull, depth 32, accept after drain) and connect_before_serve (Gone) run only on the T14's shared boots. A T14 run is owed before landing and is the orchestrator's: the whole shared boot (its members are not filtered by name; it carries abuse_connect_flood, abuse_inbox, connect_before_serve, port_badge, abuse_listener_hijack, abuse_handle_table, process_lifecycle, ring_park_herd) and shared-debug (handle_lifetime, whose census reads what a refused or closed inbox and connection give back). The implementer's own attempt, cargo test --test toyos-build -- abuse, exited 1 with "No test matches filter" and is not in the body.

NOTE

  • kernel/src/syscall/dispatch.rs:536-541 — the answer's refusals moved and the body does not say so — an out outside the user half was InvalidArgument (UserAddr::checked) and is now BadAddress; an out that is unaligned or straddles a 2 MiB page was BadAddress (copy_out's is_user_object) and is now written. Both match SYS_DEVICE_DMA_ALLOC's window and are right; the record should name them.
  • kernel/src/syscall/ipc.rs:31-41, 95-107 — sys_pipe and sys_port_create still install one handle and take it back when the second is refused, sys_pipe through the last ops::close(..).expect("…installed a moment ago") in the tree — safe only because the hold is unbroken, and the idiom this branch exists to remove; has_room(2) first deletes both rollbacks and the expect, and stops a refused sys_pipe spending a slot generation. Outside this brief's two sites: file it in issues/ or fold it in, not left unrecorded.
  • PR Three audited kernel races recorded, traced and unexecuted: an IPC rollback's expect, chdir against open, an shm map against the last close #754 — it carries this issue's file and two others (an-shm-map-racing-the-last-close-…, chdir-holds-the-vfs-lock-…) — landed after this branch it re-adds a closed issue; closed, the other two need a home first.
  • PR body, "the expect is replaced by an invariant" — an expect remains at ipc.rs:264 ("room was asked for first"); it is unreachable (below), and the sentence should say an expect on a sibling-reachable state became one on a state the hold owns.

What the brief asked to be ruled

  1. No red test is owed for the race; the window is gone by construction. Read line by line, not on the body's word:
    • connect, ipc.rs:259-270: one with_process_data hold spans has_room(1), Connector::push and ops::install. HandleTable::has_room(1) (handle.rs:177) is free.len() + (MAX_HANDLES - slots.len()) >= 1, and install (handle.rs:181) succeeds exactly when the free list is non-empty or slots.len() < MAX_HANDLES; the push touches no table and every table mutation in the kernel goes through this lock (PendingHandles::commit included), so the expect at :264 cannot fire. ops::install adds only HandleEntry::new, whose assert is on a retired object and this one is fresh. Nothing is taken back on any arm.
    • under the hold now and not before: one acquisition of the port's queue lock and a push_back on a deque bounded at 32. Every holder of that lock (port.rs:96,100,110,145,173) holds it for one statement, calls nothing under it and never parks or copies user memory, so userland can neither stall nor grow the hold. Order is process-data → queue, and the queue lock is a leaf: no site takes anything under it but the heap, so no inverse exists. watch().post() stays outside the hold. No VFS lock is on this path.
    • a refused connection: push returns the PendingConnection (port.rs:144-151), the closure returns it and the client's ConnectionEnd out, and both drop at ipc.rs:274 after the guard is gone; an uninstalled ConnectionEnd gives its pipe ends back through Held's plain drop, as the install-refused path on main already did.
    • the removed ordering ("client's end installed first") is not load-bearing: a server that pops between push and install holds its own end and can name nothing in the client's table; a close it makes then is indistinguishable from one made a moment later.
    • inbox setup, ipc.rs:448-462: no handle is taken back at all, and sys_inbox_setup takes &mut UserBytesMut, so the type refuses a call without a pinned, write-proved window; write_at on pinned frames cannot fail. A sibling's munmap of out lands the write in a frame the pin keeps from reissue.
      A loom or host model of reserve-push-install would model one lock hold; none is owed. What is owed is execution of the rewritten refusal arm, the third BLOCKER.
  2. The two-instance claim holds for handles. Every ops::install, HandleTable::install*, remove, transfer and ops::close under kernel/src read: sys_pipe and sys_port_create take back under the installing hold; PendingHandles::commit (loader/start.rs) is one hold and the spawn has no failure after it; sys_handle_recv, install_all and remint reserve first; io.rs:139,248 re-resolve under the same hold; every other install is a lone install answered by handle_result. Nearest non-handle relative: sys_device_dma_alloc takes a grant back with dma_undo after a dropped lock, but finds it by Arc identity, tolerates its absence, and no sibling can remove an Allocated grant (dma_unmap matches Origin::Mapped only) — not this defect.
  3. A T14 run is owed — the third BLOCKER names the boots.
  4. The proposed rule is declined for kernel/CLAUDE.md and for reviewer.md. It has its home already: this diff put it in kernel/src/syscall/ipc.rs's module header, which is where issues/README.md sends a closed issue's durable line; reviewer.md's Edges already names check-then-act races, and a CLAUDE.md does not grow for a rule a reader of one module header meets at the site.

SEND BACK

sys_pipe and sys_port_create installed one handle and took it back when
the second was refused, sys_pipe through the tree's last
`ops::close(..).expect("…installed a moment ago")`. Both now go through
one install_pair that asks has_room(2) first, so a refused pair installs
nothing, takes nothing back and spends no slot generation, and its two
objects drop once the hold is given up.

abuse_handle_table gains the arm that reaches the refusal: at the cap
and with one slot free a pipe and a port are each ResourceExhausted, and
the one slot is still free afterwards and the only one.

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

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1's checked patches, each applied to 86cdd0c09 with git apply --check, run on the harness's --debug guest against abuse_handle_table and reversed (RESTORED=0, tree clean).

Mutation, red — kernel panic src/syscall/ipc.rs:42:62: room was asked for first: ResourceExhausted from sys_pipe:

--- a/kernel/src/syscall/ipc.rs
+++ b/kernel/src/syscall/ipc.rs
@@ -35,7 +35,7 @@
 fn install_pair(first: KObjectRef, second: KObjectRef) -> u64 {
     let installed = process::with_process_data(|data| {
         // Room for both before either: a refused pair installs nothing, so nothing is taken back.
-        if !data.handles.has_room(2) {
+        if !data.handles.has_room(1) {
             return Err((first, second));
         }
         let mut install =

Control, green (exit_code: Some(0)) — both pair rollbacks restored, kernel/src/syscall/ipc.rs as at e556d5955:

diff --git b/kernel/src/syscall/ipc.rs a/kernel/src/syscall/ipc.rs
index b16b2760c..35a92811e 100644
--- b/kernel/src/syscall/ipc.rs
+++ a/kernel/src/syscall/ipc.rs
@@ -28,27 +28,17 @@ pub(super) fn sys_pipe() -> u64 {
     let (reader, writer) = pipe::create();
     let read_end = KObjectRef::PipeRead(crate::object::pipe::PipeReadEnd::new(reader));
     let write_end = KObjectRef::PipeWrite(crate::object::pipe::PipeWriteEnd::new(writer));
-    install_pair(read_end, write_end)
-}
-
-/// Install two objects or neither; the two handles come back packed into one word, which cannot be read as an error.
-fn install_pair(first: KObjectRef, second: KObjectRef) -> u64 {
-    let installed = process::with_process_data(|data| {
-        // Room for both before either: a refused pair installs nothing, so nothing is taken back.
-        if !data.handles.has_room(2) {
-            return Err((first, second));
-        }
-        let mut install =
-            |object| ops::install(&mut data.handles, object).expect("room was asked for first");
-        let first = install(first);
-        let second = install(second);
-        Ok(((first.0 as u64) << 32) | second.0 as u64)
-    });
-    match installed {
-        Ok(word) => word,
-        // A refused pair drops here, with the hold given up.
-        Err(_pair) => SyscallError::ResourceExhausted.to_u64(),
-    }
+    process::with_process_data(|data| {
+        let Ok(read_h) = ops::install(&mut data.handles, read_end) else {
+            return SyscallError::ResourceExhausted.to_u64();
+        };
+        let Ok(write_h) = ops::install(&mut data.handles, write_end) else {
+            ops::close(&mut data.handles, read_h, &mut data.pipe_maps)
+                .expect("the read end this call installed a moment ago");
+            return SyscallError::ResourceExhausted.to_u64();
+        };
+        ((read_h.0 as u64) << 32) | write_h.0 as u64
+    })
 }
 
 /// Map a pipe's ring page into the caller, tracked against the pipe so
@@ -99,10 +89,22 @@ pub(super) fn sys_connection_join(rx_h: RawHandle, tx_h: RawHandle) -> u64 {
     process::with_process_data(|data| handle_result(ops::install(&mut data.handles, object)))
 }
 
-/// Make a port and install both ends; needs no right and grants none, since a port with no clients is not authority.
+/// Make a port and install both ends; needs no right and grants none, since a port with no clients is not authority. The two handles come back packed into one word, which cannot be read as an error.
 pub(super) fn sys_port_create() -> u64 {
     let (acceptor, connector) = port::create();
-    install_pair(KObjectRef::Acceptor(acceptor), KObjectRef::Connector(connector))
+    process::with_process_data(|data| {
+        let Ok(a) = ops::install(&mut data.handles, KObjectRef::Acceptor(acceptor)) else {
+            return SyscallError::ResourceExhausted.to_u64();
+        };
+        let install_c =
+            ops::install(&mut data.handles, KObjectRef::Connector(connector));
+        let Ok(c) = install_c else {
+            // The acceptor goes back so a refused pair leaves no orphaned port half.
+            drop(data.handles.remove(a));
+            return SyscallError::ResourceExhausted.to_u64();
+        };
+        ((a.0 as u64) << 32) | c.0 as u64
+    })
 }
 
 /// Build a namespace from a base's kept names plus new bindings; a refusal leaves the caller's table unchanged (every name resolved and connector checked first).

@Japabu Japabu changed the title An IPC setup rollback no longer panics when a sibling closed its new handle An IPC setup no longer takes back a handle it installed: connect, inbox setup, pipe and port Oct 8, 2026
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

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

boot image sha256 toyos-metal verdict ===TEST_END lines of them exit=0
shared dce10535…0bcc7a41 exit 0 passed 62 62
shared-debug 47334de3…5145d8ad exit 0 passed 7 7

The rows the request names, each exit=0: test_rs_abuse_connect_flood, test_rs_connect_before_serve, test_rs_abuse_inbox, test_rs_abuse_handle_table, test_rs_abuse_listener_hijack, test_rs_process_lifecycle (shared); test_rs_handle_lifetime (shared-debug). The two lines it asks for are in shared's log: connect refused after 32 unaccepted, and resumed once one was drained and handle table capped at 4096 on every insert path (refused at 4096). Neither kernel log has a PANIC record, and room was asked for first occurs in no log of either boot.

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2 of wt/toyos-ipcrollback at 86cdd0c09 against origin/main 2e781a49d (merge base 017451624; main has since gained five issues/ files and nothing else, and the branch merges onto it without conflict). Read only: no test or build run. Logs read are the round's own, under orch/ipcrollback-r1/.

Net: 4 files, +93 −66. Production +61 −66 (−5), tests +32. This round alone: ipc.rs +23 −25, abuse_handle_table.rs +32.

Round 1's BLOCKERs

  1. cargo run -- --ci host — CLOSED. ci-host.log ends [ci] Host: 77 step(s), all green and EXIT=0; it ran 09:05–09:14 UTC, after the head's commit at 08:46 UTC, from a tree arm.sh and git status both show clean. Every FAILED line in it sits inside a control step, which is judged by its verdict.
  2. The guest suite — OPEN. The body carries machine_shutdown and machine_shutdown_short_stop (2 passed, EXIT=0) and says the rest is CI's. CI has not run: host, toolchain and guest are all SKIPPED on this head because the pull request is a draft, and a skipped check is not a green one. This round widened what the suite reaches: every pipe() and every port_create() in every guest now goes through install_pair, on top of every boot's connect and inbox setup. The T14's 69 members are hardware's reading and do not stand in for the QEMU suite's tests, which are other tests. Closes with no code change: guest / suite green at 86cdd0c09, or the whole suite run locally, recorded in the body with its exit and log.
  3. The refused-push arm on the T14 — CLOSED. Read from the readbacks, not from the comment: metal/shared/verdict.txt passed, 62 ===TEST_END lines and none without exit=0; metal/shared-debug/verdict.txt passed, 7 of 7. kernel.log:366 is connect refused after 32 unaccepted, and resumed once one was drained, :678 is handle table capped at 4096 on every insert path (refused at 4096); test_rs_abuse_connect_flood, connect_before_serve, abuse_inbox, abuse_handle_table, abuse_listener_hijack, process_lifecycle and, on shared-debug, handle_lifetime each end exit=0. Neither kernel log holds PANIC or room was asked for first. The image hashes in t14-run.out are the request's. The push's Closed arm stays unexecuted, as the body says (connect_through's own closed() answers first in every test); it is the QueueFull arm's twin, differing in one word, and is read, not measured.

Round 1's NOTEs

BLOCKER

  • PR body, Gates — the guest suite at this head is absent — round 1's second BLOCKER, open as stated above.

NOTE

  • PR body, "A T14 run is requested and has not happened", "real hardware, owed", "Nothing here has run on the T14" — false of the record since the run at this head — replace all three with the reading: shared 62 of 62, shared-debug 7 of 7, the seven named rows, the two lines, no PANIC.
  • PR body, "The refusal arms under QEMU, through the harness's --debug guest" — presents a debug-wait kernel under TCG, its gate released by a debugger's memory write, beside the gates — ruled below: the four head rows go; the mutation and control rows are not what "High-risk checks → Negative control" may rest on.
  • PR body, "Head 86cdd0c09, on origin/main 017451624 (merged, already up to date)" — main is 2e781a49d — the base the gates were measured on is 017451624; say that and drop "already up to date".
  • PR body, Unsure, third bullet — Three audited kernel races recorded, traced and unexecuted: an IPC rollback's expect, chdir against open, an shm map against the last close #754 is closed and will not re-add the issue — delete the bullet.

What the brief asked to be held

  1. has_room(2) then two installs is one unbroken hold with nothing fallible between (kernel/src/syscall/ipc.rs:35-52). One with_process_data closure spans the question and both installs. has_room(n) (handle.rs:177) is free.len() + (MAX_HANDLES - slots.len()) >= n; install (handle.rs:181) takes one unit of exactly that sum, a free-list pop or a push below MAX_HANDLES, and refuses only when the sum is zero. The free list never names a retired or occupied slot (retire pushes only a vacated, unspent slot; install_at removes its own), so the sum never over-counts. Between the question and the second install run initial_rights, a total match, and HandleEntry::new, whose one assert is on a retired object and both objects are fresh from pipe::create or port::create with no handle ever made. Nothing parks, copies user memory or takes another lock but the heap's.
  2. A refused pair answers what it answered before. The word is ResourceExhausted on both arms, as before. Nothing is installed: the refusal returns before any &mut use of the table, and has_room takes &self, which is also why no slot generation is spent; the old sys_pipe spent one through ops::close → retire. No test reads the generation and none is owed: an &self method followed by a return is what a reader checks. What the objects give back is unchanged in effect and changed in route: main built a HandleEntry for the refused object and dropped it, so the pipe reference went back through the deferred zero-handles hook; here no entry is ever built, the last Arc drops at ipc.rs:50 after the guard is gone, and Held (object/mod.rs:33) has no Drop of its own, so its PipeReader and PipeWriter run close_read and close_write there. A never-installed Acceptor needs no hook: its queue is empty and its Connector drops beside it.
  3. Each remaining expect("room was asked for first") (ipc.rs:42, :262, :433) is reachable only by a kernel bug: every mutation of a table goes through the lock the closure holds, so a refusal there means has_room and install disagree about one table under one hold.
  4. install_pair is not a sibling of HandleTable::install_all (handle.rs:204, one caller, device.rs:56), though both ask has_room and then install: install_all drops what it refuses inside the hold, takes rights from its caller and allocates two vectors, and a fresh pipe end's last drop takes PIPES and wakes the other end, which this module now says is not done under a table's lock.
  5. The new arm of abuse_handle_table can fail on what it claims. A pair that left its first half installed with one slot free makes the following dup refuse (:207); a pair that freed a slot makes the second dup succeed (:208); a wrong word fails :193-202. refused at 4096 on the T14 says the fill reached the cap with no retired slot in its way. It is an arm of an existing metal row, the cheapest tier that reaches a syscall on the kernel binary's handle table, which no host test builds.
  6. The --debug guest. Nothing of it is in the tree, and the landing rests on none of it. The four head rows (abuse_handle_table, abuse_connect_flood, connect_before_serve, abuse_inbox) are superseded by the same four on the shipping kernel on hardware and leave the body. The mutation and the control are real runs of the patches in comment 6056728877 (debug-mutation-results.txt: panic at src/syscall/ipc.rs:42:62 from sys_pipe; debug-control-results.txt: exit_code: Some(0)), and may stay as two lines under their true label — a debug-wait kernel under TCG, not the shipping kernel, not a gate. "Negative control: for the pair refusal, the mutation above" is reworded: no mutation is owed there, has_room(1) before two installs being what a reader of the diff catches, and the control's green says what it should, that the refusal's answer did not change. For the race the body's sentence stands: none executes, and round 1 ruled the window gone by construction.
  7. Checks the landing rests on. None has run. host and guest / suite (through toolchain) on the ready pull request and again in the merge queue, on the branch merged with 2e781a49d; and the T14 reading above, which is the only execution of the refusal arms and is not repeated by either check.

SEND BACK

@Japabu
Japabu marked this pull request as ready for review October 8, 2026 09:29
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3 of wt/toyos-ipcrollback at 86cdd0c09 against origin/main b6bcb9691 (merge base 017451624). Read only: no test or build run. The head is unchanged since round 2 (git rev-parse HEAD and origin/wt/toyos-ipcrollback both 86cdd0c094409631e6cf57ea39dd6268dc184ad5, the pull request's headRefOid the same, tree clean), so the change was not re-read; this round judges round 2's open BLOCKER, the rewritten body, and the merge onto a main that has since gained #749 and #757.

Net, unchanged: 4 files, +93 −66. Production +61 −66 (−5), tests +32.

Round 2's BLOCKER

  1. The guest suite at this head — CLOSED. Run 37756991371 is completed/success with headSha 86cdd0c09: host (113243871684), toolchain / build (113243872163) and guest / suite (113245223481) each success. The guest job's saved log (ci-guest-86cdd0c09.log, 886 lines, read whole) checks out 2c68eae, "Merge 86cdd0c… into 2e781a4…", restores sysroot 6a6e3fbcd05fbd1c, and runs cargo run -- --ci guest. Both instruments are named and real: x86_64 under KVM (/dev/kvm opens, QEMU 11.1.1) and aarch64 emulated. running 30 tests, 1 wide; 30 RUN lines and 30 PASS lines, name for name — nine x86_64 (iommu_virtio_platform, nested_nmi_is_loud, machine_shutdown, acpi_power_button, machine_shutdown_short_stop, screen_panic_muted, screen_fatal_behind_a_painter, screen_fatal_halt_composited, screen_loader_clears) and twenty-one virt_* — and no FAIL, no PANIC outside the tests whose subject is one. It ends test result: ok. 30 passed, 30 total, [ci] Guest: 5 step(s), all green, and the job's cleanup follows with no error step. The suite built the kernel from the merged source three ways ("", boot-actuators,test-actuators, mask-windows) on both architectures, so install_pair, the connect hold and the inbox window ran on every one of the 32 guests' boots.

Round 2's NOTEs, against the body as it stands

The merge onto b6bcb9691

BLOCKER

None.

NOTE

  • PR body, line 1, "the gates were measured on base origin/main 017451624" — true of the first two rows and not of the third: run 37756991371 checked out 2c68eae, this head merged into 2e781a49d — say so in the CI row.

LAND

@Japabu
Japabu added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 16b219b Oct 8, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-ipcrollback branch October 8, 2026 10:13
Japabu added a commit that referenced this pull request Oct 8, 2026
Clean. This branch names no dependency and no lockfile, so the root lock is
main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant