Skip to content

netd: a listener's socket that left Listen is handed over or listens again, and a refused accept leaves its owner a wake - #559

Merged
Japabu merged 13 commits into
mainfrom
wt/toyos-netdfix
Sep 27, 2026
Merged

Japabu merged 13 commits into
mainfrom
wt/toyos-netdfix

Conversation

@Japabu

@Japabu Japabu commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

What changed, and why

1. A listener's socket that left Listen is handed over or listens again (userland/netd/src/listen.rs). A netd listener is one smoltcp socket that becomes the connection it accepts. netd woke the owner only when a pass saw the socket Established. A peer whose handshake ACK and FIN land in one pass takes it SynReceived → CloseWait, and one whose ACK and RST do takes it to Closed; neither is ever seen Established. The owner was never woken, the socket never listened again, and the port reset every later SYN with nothing logged. listen::Listening is now the one rule that the pass and accept both use. Established and CloseWait hold a connection, and Closed listens again. settle is the one function that reads the socket's state, and it writes: it puts a Closed socket back into Listen.

2. The pass writes the wake it is handed. Listening::wake(socket, room) returns the bytes to write and records the wake it returns. main.rs reads no socket state and holds no wake bookkeeping. Recording the wake before the write is sound because a pass whose owed write fails ends the listener.

3. A refused accept spends its owner's wake, and the wake is owed again only when there is room. The room refusal and the missing-pipes refusal returned before the accept spent the wake. The owner had already read its wake byte, but netd still counted it as held. A std server refused for room would then block in its next accept for the rest of the boot. Listening::accept(socket, room, pipes) takes the pipes the request carried as an Option and answers Accept::{Take(pipes), NoPipes, NoRoom, Nothing} after it has spent the wake, so no refusal can skip the spend. Room is part of the wake condition: without it, each accept refused for room would be answered by a new wake. An accept with nothing waiting answers Nothing whatever the room, since there is no connection to refuse.

4. The wake is served with the room the pass ends with. process_pending, which frees room when a pending connect ends, returns that room, and serve_piped_listeners takes it as a parameter. Serving before the pending work does not compile.

5. A silent skip becomes a named invariant. handle_tcp_accept_piped looked up its listener with if let Some(pl) = ... a second time, after nothing had removed it from piped_listeners since the first lookup. expect now says why that lookup cannot fail.

6. Issues. issues/hardware/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md names its owner. issues/hardware/an-accept-that-never-reaches-netd-strands-its-listener.md records the client half of the same strand: std's accept reads the wake before it reaches netd, so a failure in between spends a wake netd still counts. issues/design-debt/toyos-has-its-own-network-stack.md names all three listener defects as its own, with a-connect-between-two-accepts-is-reset.md.

Gates

At dbd8b021, measured before the one-line deletion of an unreachable match arm in netd_refused_accept.rs (97eaa363, which builds every guest binary: cargo test --test toyos-build -- --list exit 0).

gate exit
cargo run -- --ci host (50 steps, clippy with warnings denied among them; userland/netd's 19 host tests, 6 of them listen::tests) 0
cargo run -- --build-only 0
cargo test --test toyos-build -- --list: builds every guest binary, netd_refused_accept among them, checks registration and boots nothing 0
netd_refused_accept at head, run by the orchestrator 0, 5 of 5
The Fast tier, run by the orchestrator 1: 396 passed, 1 failed, lan_mdns_answer (a harness socket path over SUN_LEN on macOS, not this diff)
CI host, run 36355783543 success

Negative controls and the oracle

Every patch is applied as a checked patch and not committed. Each mutated tree was shown to build: netd for the host (cargo test --no-run) and the whole image (cargo run -- --build-only), both exit 0. The tree was then restored at the same head.

Host arms: netd's host suite, cargo test --target <host> in userland/netd.

patch mutates suite red test: reason
host-base-rule Listening given the base's rule whole: Established only, no re-listen, room ignored by the wake, refusals before the spend 101 five listen::tests, among them a_peer_that_closes_with_its_last_ack_is_a_connection: "a connection the peer half-closed was never announced"; a_peer_that_resets_before_it_is_taken_frees_the_port: "the port answered the next peer … Rst"; an_accept_refused_for_its_pipes_is_woken_again: "an owner refused for its pipes was never woken again"
host-m1-closewait-not-a-connection CloseWait read like Listen 101 a_peer_that_closes_with_its_last_ack_is_a_connection: "a connection the peer half-closed was never announced"
host-m2-closed-not-relistened Closed not listening again 101 a_peer_that_resets_before_it_is_taken_frees_the_port: "the port answered the next peer … Rst"
host-m3-accept-keeps-the-wake accept not spending the wake 101 an_accept_refused_for_room_is_woken_again_when_room_returns: "an owner refused for room was never woken again"; an_accept_refused_for_its_pipes_is_woken_again; a_wake_spent_on_a_reset_connection_announces_the_next
host-wake-unrecorded self.woken |= owed deleted 101 a_finished_handshake_is_owed_one_wake: "a connection its owner holds a wake for was announced twice"
host-no-room-nothing-waits no room answered NoRoom with nothing waiting 101 an_accept_refused_for_room_is_woken_again_when_room_returns: "an accept with nothing waiting was refused for room"
refused-room-unowed room out of the wake condition 101 an_accept_refused_for_room_is_woken_again_when_room_returns: "the owner was woken for a connection there is no room to take"
refused-room-keeps-wake the room refusal before the spend 101 an_accept_refused_for_room_is_woken_again_when_room_returns: "an owner refused for room was never woken again"
refused-pipes-first the pipes refusal before the spend 101 an_accept_refused_for_its_pipes_is_woken_again: "an owner refused for its pipes was never woken again"
m-room wake's order to room && settle(..) && !self.woken 101 a_peer_that_resets_before_it_is_taken_frees_the_port at listen/tests.rs:263: "the port answered the next peer … Rst"
m-woken wake's order to !self.woken && settle(..) && room 101 a_wake_spent_on_a_reset_connection_announces_the_next, panicking inside syn (listen/tests.rs:196): "a SYN from 5002 was answered … Rst"

Machine arms, run by the orchestrator at dbd8b021; each patch applied as a checked patch and reversed clean.

arm mutates result
head nothing EXIT=0, 5 of 5
refused-revert all of userland/netd back to origin/main EXIT=1 at the harness ceiling; the guest's last line: "waiting for a wake for the connection an accept refused for room left, once room returned"
refused-room-keeps-wake the room refusal before the spend EXIT=1 at the harness ceiling, the same last line
refused-room-unowed room out of the wake condition EXIT=1 at :57: "netd woke its owner for a connection there is no room to take"
pass-room-true the pass served with room replaced by true EXIT=1 at :57: "netd woke its owner for a connection there is no room to take"
accept-room-true the accept handed room replaced by true EXIT=1 at :45: "an accept with every connection taken"

Independent oracle. smoltcp's own Interface and TCP state machine, driven over a hand-played wire (listen/tests.rs): the states are smoltcp's, not ours. RFC 9293 §3.10.7.4 ("Eighth, check the FIN bit"): a FIN in SYN-RECEIVED enters CLOSE-WAIT.

🤖 Generated with Claude Code

Japabu and others added 4 commits September 27, 2026 22:25
…again

A listener is one smoltcp socket that becomes the connection it accepts,
so its port listens only while that socket is in Listen. netd woke the
owner only when a pass saw the socket Established, and accept took only
Established. A peer whose handshake-closing ACK and FIN land in one pass
moves the socket SynReceived -> CloseWait with no pass seeing it
Established; an ACK and a reset in one pass leave it Closed. Either way
the owner is never woken, the socket never listens again, and every later
SYN on the port is answered with a reset, with nothing logged on either
side.

The rule is `listen::Listening`, one type the pass and accept both use. A
socket in Established or CloseWait holds a connection its owner is woken
for and takes; a Closed one listens again. `settle` is the one function
that reads a listener's socket state, and it writes: it is what puts a
Closed socket back into Listen.

Taken from bdfc2c5 (userland/netd only); `connection_waiting` is renamed
`settle` because it re-listens a closed socket.

Host negative controls on listen.rs, each measured on bdfc2c5's tree with
a checked patch that built and was restored: CloseWait answered like
Listen, Closed not re-listened, accept keeping the wake; each exits 101.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eighty peers connect through QEMU's forward and leave at once, half of them
with a zero linger so the close is a reset; then `echo` over ssh must be
answered. The forward finishes the guest's handshake after the host has
closed and sends the FIN straight behind the last ACK, which is how a
listener's socket goes SynReceived -> CloseWait with no pass seeing it
Established. A netd that wakes its owner only for Established leaves port
22 resetting every SYN for the rest of the boot.

It is the committed arm for netd's wiring of `listen::Listening`: the host
tests in userland/netd call `Listening` directly and stay green when the
pass stops using it.

A reset ask is retried at once, since a hasty handshake still in flight
shuts the port without sshd hearing of it; a second reset waits on sshd
saying something, since a connection netd holds for sshd shuts the port
until sshd takes it. A port that stays shut ends the wait as a guest gone
quiet, which is the red. No fixed delay: the measured actuator this comes
from paced its rounds and retried its ask on a one-second sleep, and
neither is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is room

The accept's room refusal and its missing-pipes refusal returned before
the accept spent the owner's wake. The owner had already read its wake
byte, but netd still held it as given, so no pass announced the waiting
connection again: a std server refused for room blocked in its next
accept for the rest of the boot while the connection held the port. sshd
got out only because it rebinds on any accept error.

The rule, in `listen::Listening`: an accept spends the owner's wake
whatever it answers, a refusal included, and a wake is owed only for a
connection there is room to take. `Listening::accept` takes the room and
answers `Accept::{Take, NoRoom, Nothing}`, so the room refusal cannot skip
the spend; the missing-pipes refusal comes after it. Room is in the wake
condition so that an owner refused for room is woken again when room
returns and not on every pass before it, which spending alone would do:
each refused accept would be answered by a wake on the next pass.

`serve_piped_listeners` moves after `process_pending`, which frees room
when a pending connect ends: the wake condition is read after everything
in a pass that changes it, with nothing between it and the wait.

Arms: the host test
an_accept_refused_for_room_is_woken_again_when_room_returns, and the
machine test netd_refused_accept (tests/netcase), whose guest refuses an
accept for its pipes and one for room and waits on the wake each leaves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on smoltcp 0.12's interface over a hand-played wire (the harness
of userland/netd/src/listen/tests.rs): after one SYN and 600 s of silence
the listener's socket was still SynReceived, having sent 72 SYN-ACKs, and
another peer's SYN was answered with a reset. Filed as a defect, and both
listener defects are named under the network-stack track, whose holder
owns them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu marked this pull request as ready for review September 27, 2026 21:12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 of 9ba050e5 (the merge of origin/main 1808fb8d) against origin/main.

Gate. CI host succeeded at 9ba050e5 (run 36350863799, conclusion success), and its log shows all five listen::tests passing. No guest test has run at this head. sshd_hasty_peers, netd_refused_accept and the Fast tier are unmeasured, so every machine claim is NOT READY FOR REVIEW. The code is reviewed anyway because the brief asked for it.

BLOCKER

  • userland/netd/src/main.rs:1439-1443 — The pass's half of the rule is still in main.rs: it calls owes_wake, then woke() on Ok(_) if owed. No host test reaches that code, and its only arm is sshd_hasty_peers (next BLOCKER).

    • Fix. Fold owes_wake and woke into one Listening::wake(&mut self, socket, room) -> &'static [u8] that records the wake it returns (self.woken |= owed). This is sound because every failed owed write already ends the listener (:1444-1450).
    • Effect. main.rs then reads no socket state and writes what it is handed. The hasty-wiring patch has no site left; its only equivalent is an edit to listen.rs, which m1 and m2 turn red.
    • Required host red. Deleting self.woken |= owed must turn a_finished_handshake_is_owed_one_wake red ("announced twice"). Today, dropping the woke() call from main.rs passes every committed test.
  • tests/common/ssh.rs:630-711, tests/toyos.rs:979-981, :12264 — Delete sshd_hasty_peers. Once the fold above lands, it catches nothing the host suite misses. As written:

    • Green arm. It can go red on a correct netd. A hasty peer's handshake left in SynReceived clears only on smoltcp's SYN-ACK retransmit and the forward's reset, and sshd says nothing when it does. Two asks inside that window lead to a wait for an sshd: line that never comes. The first such wait can be satisfied by the hasty phase's own buffered lines, so it is a later one that goes red. Whether the port reopens between two back-to-back asks is timing, and here that timing decides the verdict.
    • Red arm. Its red is the quiet guard: GUEST_QUIET, or GUEST_WEDGED if the guest keeps talking. tests/common/qemu.rs defines that guard as not a verdict.
    • Prefix predicate. log[from..].contains("sshd: ") is keyed on a program prefix, which tests/CLAUDE.md says the wrong speaker satisfies. Any sshd line satisfies it, including session error and listening on port 22 after a rebind.
    • Reset text. why.contains("Connection reset by peer") (:690) matches error text from a host client that has never been seen reset by this forward. If the forward answers EOF instead, a busy port is a red on a correct netd.
    • Cost. Its unpaced stimulus has no measured red rate, and it takes a Fast-tier boot of its own.

    Moving it to Nightly keeps all of these defects and only runs it less often. If it is kept instead of the fold, it lands only after head green 10 of 10 and hasty-wiring and hasty-revert red, with the reset text quoted from those logs.

  • userland/netd/src/main.rs:1743-1747 — The move of serve_piped_listeners after process_pending has no arm, and no QEMU arm could be anything but a timing verdict: the old order only delays the wake until netd's next unrelated event. Make the order unrepresentable instead. process_pending ends by returning self.piped_room(), and serve_piped_listeners takes that room as a parameter and drops its own :1435. Serving before process_pending then does not compile.

  • Gate — The guest runs below, at the head that closes these BLOCKERs.

NOTE

  • userland/netd/src/listen.rs:57 — (_, false) => NoRoom answers ERR_RESOURCE_EXHAUSTED and logs "refusing accept" even when no connection waits. The mutation (_, false) → (true, false) plus (false, false) => Nothing passes every test. Decide which answer is right and pin it with one assert in an_accept_refused_for_room_is_woken_again_when_room_returns.

  • rust/library/std/src/sys/net/connection/toyos.rs:417-422 — std reads the wake byte before NetdConn::connect and DataPath::create. If either fails, the byte is spent but no accept reaches netd, woken stays set, and the owner blocks for the rest of the boot. This is the same strand on the client side. File it under the network-stack track.

  • userland/netd/src/main.rs:1266 — Where the pipes refusal sits relative to the spend is main.rs ordering, and its only arm is netd_refused_accept's first phase. Handing accept an Option<P> of the pipes and answering Take(P) or NoPipes would move it into the host suite. Optional.

  • userland/netd/src/listen/tests.rs:215-229 — Four one-line wrappers (owes_wake, owes_wake_with, accept, accept_with) exist for two calls. Pass room at the call sites instead.

  • Net lines, from git diff --numstat: +732 −34.

    part lines net
    production (listen.rs +84, main.rs +34 −31) +118 −31 +87
    tests (listen/tests.rs 307, netd_refused_accept.rs 161, ssh.rs 83, toyos.rs +30 −3) +581 −3 +578
    issues +33 +33

    The fold deletes woke() and the Ok(_) if owed arm. Deleting sshd_hasty_peers removes about 87 test lines. Keep the 230-line wire in listen/tests.rs: it is the oracle. netd_refused_accept earns its boot. It is the only arm for main.rs's room wiring (room replaced by true in the pass or in the accept) and for the pipes ordering.

REMOVE

  • PR body §1: "taken from bdfc2c5, netd only" and "settle, renamed from connection_waiting," — the history of an unmerged branch has no place in main's record.
  • PR body §2 in full, and the sshd_hasty_peers rows of both arm tables — they go with the test.
  • PR body Gates: "Logs are in the job's netdfix-r1/ scratch directory." — a scratch path has no place in main's record.
  • PR body, oracle: "QEMU's slirp is an independent TCP peer that produces the ACK-then-FIN pattern in the machine tests." — unmeasured.
  • PR body, Unsure bullet 4: "is woken again on every pass" — false: it is woken once per refused request.
  • PR body, Unsure bullets 1–3 — delete them once the guest arms are measured and the reorder is a data dependency.
  • userland/netd/src/main.rs:1745-1746 — once room is passed from process_pending, the data dependency states this comment's invariant.

The brief's questions

  • One rule, one function. Not yet. listen.rs holds the accept's answers for room and for an empty socket. The pass's wake bookkeeping (first BLOCKER) and the place of the pipes refusal (NOTE) are still in main.rs.

  • Completeness. The rule is complete for smoltcp 0.12. A listener netd has not touched reaches only these states:

    • Listen;
    • SynReceived, where an RST returns it to Listen (tcp.rs:1738; listen_endpoint.port != 0 holds);
    • Established;
    • CloseWait;
    • Closed.

    No timeout or keep-alive is set on a listener, so only netd itself can reach settle's panic arm.

  • Does the reorder need an arm? It needs a check. It cannot be proven on the host while main's loop is shaped as it is, and a QEMU arm would be a timing verdict. That is the third BLOCKER, closed by a compile-time data dependency.

  • Timing. netd_refused_accept's WITHIN of 20 s is a hang ceiling, not a verdict. Each green wake is caused by the pass after a request, and in each red arm the wake never comes. Its "no wake waiting" check is ordered by the answer to a later connect, not by time: netd reads that request on a pass whose start ran the listener service. sshd_hasty_peers is not a hang ceiling (second BLOCKER).

  • The unmeasured red rate. Delete the test, after the fold.

  • Is +578 proportionate? See the NOTE on net lines. About 87 lines go, and the remaining 491 are the oracle and the only arm for main.rs's wiring.

check result
one rule, one function no: the pass's bookkeeping is in main.rs (first BLOCKER)
no silently dropped input yes: every accept path answers the client. The answer when there is no room and nothing waits is undecided (NOTE)
every claimed property has a red arm no: the reorder has none, and the pass wiring rests on an unmeasured test
no unmeasured numbers slirp's ACK-then-FIN is unmeasured (REMOVE); 80 peers is a chosen constant
no review chronology none in the source; the body's §1 has it (REMOVE)
PR body fit for main's record not until the REMOVEs and the measured guest arms

Runs required at the new head

Run each with guest-arm.sh, and record its exit code and log.

  • netd_refused_accept:
    • at head: green 5 of 5;
    • refused-revert, refused-pipes-first, refused-room-keeps-wake and refused-room-unowed: each red, with the reason the PR names;
    • new: the pass's room replaced by true: red, "netd woke its owner for a connection there is no room to take";
    • new: the accept's room replaced by true: red, "an accept with every connection taken".
  • The Fast tier at head, once. The sshd_* tests and lan_swap are the arm showing that main.rs writes the wake it is handed.
  • Host arms, re-cut after the fold: m1, m2, m3, refused-room-unowed, refused-room-keeps-wake, and self.woken |= owed deleted. Each must exit 101.

SEND BACK

Japabu and others added 4 commits September 27, 2026 23:40
…sten.rs

`Listening::wake` returns the bytes the pass writes and records the wake it
returns, so `main.rs` reads no socket state and writes what it is handed. That
is sound because a pass whose owed write fails ends the listener.

`Listening::accept` takes the pipes the request carried as an `Option` and
answers `NoPipes` after spending the wake, so the pipes refusal is ordered
against the spend inside the one function and is host-tested
(`an_accept_refused_for_its_pipes_is_woken_again`). An accept with nothing
waiting answers `Nothing` whatever the room: there is no connection to refuse,
and netd no longer says it refused one.

`process_pending` returns the room the pass ends with and
`serve_piped_listeners` takes it, so serving before the pending work is freed
does not compile; the comment that stated the order goes.

`netd_refused_accept` loses its pipes phase, which the host suite now
carries; it keeps the room phase, the one arm for `main.rs`'s room wiring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its red is the harness's quiet guard, which is no verdict, and its green can
go red on a correct netd: a hasty peer's handshake left in `SynReceived`
shuts port 22 until smoltcp retransmits, and the test's reset text and
`sshd: ` prefix predicate decide the outcome on timing. The listener's wake
rule it guarded is now whole in `listen.rs`, where the host suite reddens on
every mutation of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
std's accept reads the wake before it reaches netd, so a failure in between
spends a wake netd still counts as held. Filed under the network-stack track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2 of ac948e6a (the merge of origin/main e5ffe950) against origin/main.

Round 1 BLOCKERs

  • The pass's wake folded into listen.rs: CLOSED. Listening::wake (userland/netd/src/listen.rs:49-53) records the wake it returns, and serve_piped_listeners writes what it is handed. host-wake-unrecorded exits 101: a_finished_handshake_is_owed_one_wake goes red at listen/tests.rs:237, "announced twice" (netdfix-r2/arm-host-wake-unrecorded.host.log).
  • sshd_hasty_peers deleted: CLOSED. Commit 19ae86cd. tests/common/ssh.rs has no diff against origin/main.
  • The reorder as a data dependency: CLOSED. process_pending returns the room and serve_piped_listeners(&mut socket_set, room) takes it (main.rs:1739-1740). Swapping the two lines no longer compiles.
  • Gate: OPEN for CI only.
    • Guest runs at ac948e6a, from the orchestrator: netd_refused_accept at head EXIT=0, 5 of 5. Every arm is red with its named reason: refused-revert and refused-room-keeps-wake at netd_stream.rs:70 ("not within 20s"), refused-room-unowed and pass-room-true at :64, accept-room-true at :52.
    • Fast tier: 395 passed and 2 failed, and neither failure is about this diff. lan_mdns_answer fails on SUN_LEN and quiesce_wakes_on_the_last_park on its stop budget. netd_refused_accept, sshd_* and lan_swap pass.
    • Host arms: nine patches, each exit 101 at its named line.
    • CI at ac948e6a: run 36353385756 is queued with no conclusion. The only --ci host exit 0 is the implementer's local one.

BLOCKER

  • tests/toyos-rust-tests/src/bin/netd_refused_accept.rs:29,38,81,110 — Ruling: the 20 s must go. Under the owner's ruling the test may carry no in-guest deadline.
    • Why. WITHIN is not only a hang ceiling. In refused-revert and refused-room-keeps-wake, "not within 20s" is the verdict itself. The two connect deadlines of 30 000 ms (:38, :110) are also in-guest deadlines, with netd acting as the guest's timer: a slow host answers TimedOut, and :51 goes red on a correct netd.
    • Fix. wake prints what it waits for, then blocks on listener.notify.read(&mut byte). It requires Ok(1), and Ok(0) means netd closed the listener. Both connects pass 0: main.rs:1181 sets no deadline for 0. Delete WITHIN, Duration, await_until and READABLE.
    • Re-measure. Head green 5 of 5. refused-revert and refused-room-keeps-wake red at the harness ceiling, with the waiting line as the guest's last line, quoted from those logs.
  • userland/netd/src/listen.rs:50 — A reset socket may stay shut, and no test catches it. The module contract says a socket that left Listen is handed over or listens again. That holds only because settle runs first in a short-circuit &&. Two mutants pass every committed test:
    • M-room: let owed = room && settle(socket, self.port) && !self.woken;. With no room, a reset socket stays Closed, and the port resets every SYN until room returns.
    • M-woken: let owed = !self.woken && settle(socket, self.port) && room;. A socket reset while its owner holds a wake stays Closed until the accept.
    • Required reds. At listen/tests.rs:262, write !net.wakes(false) instead of !net.wakes(true), so that M-room reds at :263. In a_wake_spent_on_a_reset_connection_announces_the_next, move let isn = net.syn(5002); (:279) to before the accept at :278, so that M-woken reds in syn with "a SYN from 5002 was answered … Rst". Head stays green, because settle relistens at :277 and the accept then finds SynReceived and answers Nothing. Each mutant must exit 101.

NOTE

  • userland/netd/src/main.rs:1291-1293 — The if let Some(pl) = self.piped_listeners.get_mut(..) is a silent skip on a path that cannot happen, because the listener was looked up at :1243 and nothing removed it. Write through expect with the invariant, or restructure so that listener.handle = new_handle is written directly.

  • PR body — The machine-arm table and the Gates row still say run by the orchestrator. At the next head, the body carries each arm's exit code and quoted reason, CI's run and conclusion, and the Fast tier's result.

  • Net lines, from git diff --numstat (+662 −44):

    part lines net
    production (listen.rs 88, main.rs +37 −41) +125 −41 +84
    tests (listen/tests.rs 314, netd_refused_accept.rs 138, toyos.rs +26 −3) +478 −3 +475
    issues +59 +59

    The production growth is the one rule in one file, and I accept it. The first BLOCKER shrinks the guest test.

REMOVE

  • tests/toyos.rs:922 — "; its clocks are liveness guards": once the first BLOCKER lands, the test has no clocks.
  • tests/toyos-rust-tests/src/bin/netd_refused_accept.rs:27-28 — The WITHIN doc goes with the constant.
  • PR body, "Unsure" — this is resolved by measurement; delete it.
  • PR body, the machine-arm rows' "not within 20s" — false after the first BLOCKER. Replace it with the measured ceiling red, quoted from the log.

The brief's questions

  • Is accept(socket, room, pipes) one rule in one function? Yes. self.woken = false precedes one exhaustive match over (connection, room, pipes) at listen.rs:57-65. Every refusal is an arm of that match, and refused-pipes-first, refused-room-keeps-wake and host-no-room-nothing-waits are red in the host suite. main.rs only maps each answer to its error. The wake is one function too. What remains is that its evaluation order is load-bearing and unpinned (second BLOCKER).
  • Is anything left to delete? Yes: WITHIN, the 30 s connect deadlines, the "clocks" clause, and the dead if let (NOTE). Accept<P> earns its generic, because it is what carries the pipes refusal into the host suite.
  • Timing ruling. The in-guest 20 s bound is not acceptable under the strict ruling. It becomes a plain wait, and a lost wake reds by the harness ceiling (first BLOCKER).
check result
one rule, one function yes, but the order inside wake is unpinned (BLOCKER)
no silently dropped input yes: every accept path answers the client
every claimed property has a red arm no: "a reset socket listens again" survives M-room and M-woken
no in-guest deadline no: WITHIN and two 30 s connect deadlines (BLOCKER)
no unmeasured numbers yes in source
no review chronology none
PR body fit for main's record not until the arms' measurements replace "run by the orchestrator" and the REMOVEs are applied
CI green at head no: queued

SEND BACK

Japabu and others added 3 commits September 28, 2026 00:27
`wake` blocked on `read_nonblock` against a poller timeout, and the two
connects passed a 30s deadline: three clocks inside a test whose verdict a
lost wake must fail by hanging the harness, not by racing a guest clock
against netd. `wake` now blocks on `listener.notify.read`, refusing `Ok(0)`
by name (netd closed the listener), and both connects pass `timeout_ms: 0`
(`main.rs:1181` sets no deadline for that). `WITHIN`, `Duration`,
`await_until` and `READABLE` go with them, and so does the doc's claim that
netd_refused_accept's clocks are liveness guards: it has none now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`settle` must run before `room` and `woken` are read, because it is what
relists a socket its peer reset: skip it under `room` false and a reset
socket never listens again while room is out, and skip it under `woken` true
and a socket reset while its owner holds a wake stays closed until the
accept reaches it. Both orders passed every committed test, because `accept`
calls `settle` again on its own path and papered over `wake`'s skip.

`a_peer_that_resets_before_it_is_taken_frees_the_port` now calls `wakes`
with no room, which only reaches the next peer's SYN if `wake` relisted the
port; `a_wake_spent_on_a_reset_connection_announces_the_next` now sends the
next peer's SYN before the accept, so the accept's own `settle` cannot mask
a `wake` that left the socket closed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`handle_tcp_accept_piped` looks up the listener once and holds no borrow of
it across the intervening inserts, so the second lookup before the
replacement handle is written cannot fail. The `if let` treated that as a
skippable case; `expect` says why it never is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3 of dbd8b021 against origin/main (e5ffe950, the merge base).

Gate at dbd8b021

  • CI: run 36355783543 (pull_request, headSha dbd8b021), job host, conclusion success.
  • Guest runs, from the orchestrator:
    • netd_refused_accept at head: EXIT=0, 5 of 5, "PASS netd_refused_accept (4s)".
    • Fast tier: 396 passed and 1 failed. The failure is lan_mdns_answer on SUN_LEN (connect to QEMU's …/tap-out-0.sock: path must be shorter than SUN_LEN). It is a harness socket path, and this diff touches no lan code.

Round 2 BLOCKERs

  • The in-guest deadlines: CLOSED. WITHIN, await_until, READABLE and Duration are gone. wake blocks on listener.notify.read (netd_refused_accept.rs:75), and both connects pass timeout_ms: 0, which main.rs:1181 maps to no deadline. The blocking read cannot be ended early by netd's zero-byte probe writes: sys_read loops on try_read until data or EOF (kernel/src/syscall/io.rs:125-163), so only a real wake or netd's close ends the wait.
    • Head: EXIT=0, 5 of 5.
    • refused-revert: EXIT=1, "timed out after 300s, with the guest still talking 8s ago (360 console line(s) while it ran)".
    • refused-room-keeps-wake: EXIT=1, "timed out after 393s … (478 console line(s) …)".
    • In both logs the guest's last line is netd_refused_accept: waiting for a wake for the connection an accept refused for room left, once room returned. That is the second wait. The brief says the last line is "…for the host's dial", and that is wrong: the dial's wake arrived in both arms.
    • refused-room-unowed and pass-room-true: EXIT=1 at :57, "netd woke its owner for a connection there is no room to take".
    • accept-room-true: EXIT=1 at :45, "an accept with every connection taken".
  • wake's evaluation order unpinned: CLOSED.
    • m-room: HOST_TEST=101 at listen/tests.rs:263, "the port answered the next peer Some(Sent { control: Rst, … to: 5002 })".
    • m-woken: HOST_TEST=101 in syn at listen/tests.rs:196, "a SYN from 5002 was answered … Rst".
    • Both logs are netdfix-r3/arm-m-{room,woken}.host.log. Head is green in CI.

The brief's questions

  • Is a lost wake reddened by the hang ceiling acceptable? Yes. It is the only clock the owner's rule allows, and the head can no longer go red because the host is slow. A red costs the 300 s backstop, not the 120 s budget, because the kernel's 10 s line keeps a parked guest "talking". GUEST_WEDGED (tests/common/qemu.rs:520-529) already documents this for every failing wait.
  • Is the failure message clear enough to diagnose? Yes, from the guest's last line, which names the wake that never came. The harness's own clause "it was working and did not finish" is false for a parked guest. That is the harness's standing property, outside this fence, and not this branch's to fix.
  • Is anything left to delete? One dead match arm (NOTE). The PR body's round narration (REMOVE).
  • Is the PR body fit for main's record? No. It still says the guest numbers are "to be re-measured" and that CI has "no run completed", and it narrates review rounds. See NOTE and REMOVE.

BLOCKER

None.

NOTE

  • tests/toyos-rust-tests/src/bin/netd_refused_accept.rs:78 — Ok(n) => panic!("… read {n} bytes") is unreachable, because a one-byte buffer reads 0 or 1. Delete it, and let Ok(0) become Ok(_) with the "netd closed the listener" message.

  • PR body, Gates and machine-arm tables — Replace them with the measurements at dbd8b021:

    • CI run 36355783543, host success.
    • Head EXIT=0, 5 of 5.
    • Each arm EXIT=1 with the reason quoted above. The two ceiling arms quote the guest's real last line.
    • Fast tier 396/1, with lan_mdns_answer on SUN_LEN.
  • Net lines, from git diff --numstat origin/main...HEAD (+658 −47):

    part lines net
    production (listen.rs +88, main.rs +41 −44) +129 −44 +85
    tests (listen/tests.rs +314, netd_refused_accept.rs +130, toyos.rs +26 −3) +470 −3 +467
    issues +59 +59

    Round 3 is +17 −24. The production growth is unchanged from round 2, which accepted it.

REMOVE

  • PR body, item 5 — This narrates the test's past waits (a 20 s poller wait and 30 s connects), which never existed on main.
  • PR body, item 6 — "both orders passed every committed test …" and the "now asks / now sends" sentences are review chronology. The m-room and m-woken rows already record what the item pins.
  • PR body, the paragraph "m-room and m-woken are round-3 verification patches … the two the review named" — this is review chronology.
  • PR body, the "round-2 result at ac948e6a" column, including the "superseded" cells — false at this head.
  • PR body, "Fast tier at ac948e6a: 395 passed, 2 failed …" — false at this head.
  • PR body, the bold paragraph "All of the above guest numbers are round-2's … The orchestrator's guest run list for this head …" — false at this head.

LAND AFTER NAMED CHANGES

…ytes goes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu enabled auto-merge September 27, 2026 23:21
@Japabu
Japabu added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 614698a Sep 27, 2026
1 check passed
Japabu added a commit that referenced this pull request Sep 28, 2026
Brings in #537, #553 (every kernel panic halts, usbd and poison deleted,
the no-kernel-threads track), #559 and #561. The kernel now spawns one
thread outside the actuator build, klogd: this branch deleted iod and
main deleted usbd, and neither side's replacement is a kthread.

Conflicts, each resolved against both sides' hunks:

- kernel/src/drivers/nvme.rs, kernel/src/iod.rs (modify/delete): deleted.
  Main's changes to them were the `mm::policy::MmioPolicy` import rename
  and dropping `OnPanic::Recover` from iod's spawn, adaptations to code
  this branch removes; neither carries behaviour to move elsewhere.
- kernel/src/sched/kthread.rs: main's row table without panic policy.
  MAX_KERNEL_TASKS is 1 (klogd), and 2 + MAX_LOG_SHARDS in the actuator
  build (klogd, lognest, one logstorm per shard, which can run in one boot).
- kernel/src/main.rs: neither `usbd::start()` nor `iod::start()`; main's
  panic handler without recovery, this branch's storage phase.
- kernel/src/quiesce.rs: main's header, which names no kernel thread.
- kernel-loom/src/lib.rs: neither `poison` (main) nor `durability` (here).
- tests/toyos.rs: `heap_ceiling_bounds` (main's rename) without
  `cache_eviction` (deleted here); `blocked_dump` and `klogd_hosted` ask
  for klogd alone; `klogd_panic_halts` is main's, whose usbd arm and
  recover row are gone with usbd and poison.
- issues/build/a-lane-s-tap-socket-path-outgrows-sun-len-on-the-dev-host.md:
  main's body and `kind: tooling`, this branch's two-shapes measurement;
  `status: open`, since no disabled row names `lan_mdns_answer` on either
  side and `--known-red` answers NO, so the quarantine paragraph goes.

Beyond the conflicts: toyos-inventory gets the `description` main's
hostws gate now requires of every workspace package, and the
no-kernel-threads track loses K5, which this branch meets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Japabu added a commit that referenced this pull request Sep 28, 2026
The branch continues from a worktree made from main, so main is this
merge's first parent and every hash of wt/toyos-install stays reachable.
netd takes main's side whole: #559 landed the listener fix and its machine
test on its own, which supersedes this branch's copy of listen.rs, its
tests and its main.rs wiring. The listener issue takes main's text, which
names its owner and `listen::settle`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu
Japabu deleted the wt/toyos-netdfix branch September 28, 2026 09:46
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