Skip to content

toyos-net-node: listeners on the own stack's accept queue, and one bound on the streams, listeners and datagram sockets clients make the node hold (stage E) - #777

Merged
Japabu merged 12 commits into
mainfrom
wt/toyos-ownstack-e
Oct 9, 2026
Merged

Japabu merged 12 commits into
mainfrom
wt/toyos-ownstack-e

Conversation

@Japabu

@Japabu Japabu commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Stage E of the cut of issues/toyos-has-its-own-network-stack.md's stage 5: netstack's listeners on ToyOS's own TCP, and the one bound on the streams, listeners and datagram sockets clients make the node hold, in toyos-net-node, host-tested and shipped in nothing. Base: main at 5f2657703, which carries stage D (#775, landed as d5de6fabe), #779, #782 and #780, merged here at f0c483ea2. Head: f0c483ea2; the reviewed head is e737052a5. Stage E is git diff --shortstat origin/main...f0c483ea2: 20 files changed, 1943 insertions(+), 102 deletions(-). Production +476, -39 (listeners.rs 222, places.rs 54, streams.rs +56 -24, lease/tcp.rs +40 -4, lib.rs +19 -4, datagram.rs +15 -3, the shard +46 -4, [tcp] 24); tests +1,385, -59 (tests/listeners.rs 1,031, tests/lease.rs +133 -2, tcp/tests/held.rs 83, tests/common/mod.rs +58 -5, tests/host.rs 54, tests/streams.rs +22 -51, tests/resolve.rs +4 -1); the track, two issues, the manifest and two lockfile lines +82, -4. Against main the diff is those 20 files and no others. No image, no shipped module and no guest test changes.

The merge of main at 5f2657703 (f0c483ea2)

No file stopped the merge and none was resolved by hand. One file is both sides', the track, merged by git and read against both parents: #779 removes two bullets from the node's list and adds two to stage 3's lists, and every line of this stage stands.

git diff e737052a5 f0c483ea2 --stat over userland/netstack/node, toyos-net-shard, toyos-dns, userland/netstack/src/resolve.rs, Cargo.lock and the track is two files, both main's and both #779's (47d8a6baf):

Everything else there is e737052a5's byte for byte: the node's src, its other tests, the shard, [tcp], toyos-dns, resolve.rs and both lockfile lines.

What #779 and #782 change under this stage: nothing it rests on. #779's rules read 169.254/16 alone (a route to it on the link, its two edges no source, an ARP sender in it a neighbour); no source or test of this stage names that prefix, and its peers are 192.0.2.0/24's. The silent-next-hop rule is a test of [ip]'s that #779 adds, with no change of [ip]'s behaviour. #782 is userland/netstack/src and toyos-i219, the shipped netstack and its drivers, which nothing here depends on.

Mutations not run again: the merge touches none of the eight files the 39 patches name, and each patch still applies to f0c483ea2 (git apply --check, step 40-mutations-apply-check, 39 of 39 exit 0).

Round 3 of the review, finding by finding (e737052a5)

BLOCKER 1, evidence. Still open and not mine to close: a draft, and no host job has run. What I ran is in "Gates": round 6, one log a step, in the orchestrator's scratchpad.

BLOCKER 6, the wire ended a program's listener for good. Removed whole, and nothing replaces it in the node. Gone: the check in wake_each; ListenerEnd (the drain yields (ListenerId, WriteRefusal) again; it keeps the name drain_ended_listeners, so the test below is the same text against both sources); Shard::listens_at, back inside Shard::listen, its one caller; Listener::at, now port, which the one-listener-a-port rule reads; the header's paragraph; the track's sentence. listeners.rs against 643584d4b differs by the port rule, its field, its header paragraph, one sentence on the lost address and the drain's name, and by nothing else; the shard's lib.rs is 643584d4b's byte for byte. A listener whose address the machine loses stands as a datagram socket bound to it does: [ip] takes no segment for an address it does not hold, and [tcp] still has the listener when the address is back.

  • The test, beside the lease's: a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back, over a NAK, expiry and a second conflict. Before the loss, a renewal and a link down and up (round 2's second test, folded in): both listeners stand. After it: the named listener and the one at every address stand, two listeners and two places, nothing drained, neither owner's end dropped, no wake; a SYN to the lost address makes the node send nothing and wakes nobody; a listen there on another port is NotLocal. Then the server offers and acknowledges the same address, conflict detection ends, and a peer's handshake at port 22 completes: the SYN-ACK is read by etherparse (<SEQ=ISS><ACK=5001><CTL=SYN,ACK> from port 22) and the named owner holds one wake, the other none.
  • Its control: red against c27cc36aa (round 6, steps red-first-build, exit 0, and red-first--a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back, exit 101): userland/netstack/node/tests/lease.rs:282:13: assertion left == right failed: a NAK, the address lost; left: (1, 1, 1), right: (2, 2, 0). It has no mutation at this head: the rule it guards against is the code that was removed.
  • It replaces round 2's two tests; lease is 19 tests. common::outside reads a TCP segment (Seen::Tcp), so the lease's harness can carry the handshake; no other harness changed.
  • The track says the listener stands as a datagram socket does, names the test, says no host was read, and keeps the exit it had.

NOTE, tests/host.rs. An interrupted poll is one more turn of the loop; any other error still panics, and so does the ceiling.

NOTE, the body. "It is the brief's ruling" is gone with the code, and difference 9 loses its last clause.

The patches. e32 and e33 are gone with what they mutated. The other 39 are regenerated against e737052a5's blobs, each applying with git apply --check exactly, and posted once in the newest patch comment.

Round 2 of the review, finding by finding (c27cc36aa)

BLOCKER 1, evidence. Open then as now; that round's steps are round 5's logs.

BLOCKER 5, a port is one listener's. Node::listen refuses a named port any listener of the node holds, whichever address either named, ListenRefused::InUse, before [tcp] is asked: the rule [udp] has for its ports. [tcp]'s preference for the listener that named a SYN's address (LS-09) stays [tcp]'s and is not reachable from the node, since every listener [tcp] holds is one the node made. A drawn port needs no check: [tcp] draws only a port no listener holds at any address (port_listened).

  • The tests, one helper in three orders: a_listen_at_the_machines_address_takes_no_port_held_at_every_address (0.0.0.0:22 then 192.0.2.1:22), a_listen_at_every_address_takes_no_port_held_at_the_machines_address (the other order) and a_second_listen_at_the_machines_address_takes_no_port_held_there. Each: the second listen is InUse, its owner's pipe end is dropped, one listener and one place are held, and the next handshake wakes the first owner and is accepted there.
  • Red first, the three tests added to the source of 643584d4b before the rule (steps red-first-build, exit 0, and red-first--<test>): the two cross-address orders exit 101, userland/netstack/node/tests/listeners.rs:766:23: called Result::unwrap_err() on an Ok value: (ListenerId(1), Port(22)); the same-address one exits 0, which [tcp] already refused.
  • Mutation e31 (the review's: the new check removed) is red on both cross-address tests at the same line.
  • a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds no longer puts a named listener beside one at every address; it keeps the refused listen at 192.0.2.7, the reset, and a listener at 192.0.2.1 that is woken.
  • The track says what is true and what a listen that carries its program would allow: a second listener on a held port only for the program that holds every listener on it. Exit: a test of the node with one program's pair standing, a SYN handed to the one that named its address, and another program's listen on that port refused.

NOTE, tests/host.rs, all three. One assert_eq! over the pair, its message the host's name (std::env::consts::OS), so a red log is the reading. The second arm now waits on the event it depends on. A connect returns at the SYN-ACK and the listener's end is established by the ACK after it; the arm rested on loopback delivering that ACK first. What a portable program can observe is that the listener is readable, which is the host saying a connection waits to be accepted: the test registers the listener with mio before the connect and sets the option only after the poll reports it, under a 60 s ceiling that panics. Both arms take the same path, so the accept never waits either. The module header says what the arm rests on, and the issue says it too. mio is new as a direct dev-dependency of toyos-net-node: general and widely used, in the lock already through the workspace's patch (one line added to Cargo.lock, no new package; default features off, so its own dependency list in the lock is unchanged), on Linux, macOS and Windows alike. std cannot learn that a connection waits except by accepting it, and socket2 has no poll; the alternative was unsafe libc poll, Unix only.

NOTE, a listener whose address the machine loses. Round 2 ended it; round 3 found that wrong (BLOCKER 6, above) and it is removed.

NOTE, the body's logs. Every step of this round has its log, named below.

The merge of 49e38ca4a (fdfcf6576), hunk by hunk

Three files stopped the merge; Cargo.lock did not, and cargo, given stage D's lockfile and this manifest, writes the same bytes git merged (cargo metadata --locked, step gate-lock, exit 0).

  • userland/netstack/node/src/lib.rs, the header's paragraph on who owns what: one sentence with datagram, name and resolve, then places said to count what it counts: streams, listeners and clients' datagram sockets, the responder's socket and a lookup's outside it.
  • lib.rs, the module list: both sides whole: datagram, lease, listeners, name, places, resolve, streams.
  • userland/netstack/node/Cargo.toml, description: the three kinds and their one bound, the name and the resolver. The dependencies merged by themselves: toyos-dns, mio, socket2.
  • The track, the stage 5 paragraph: in the tree are the lease, the datagram sockets, the name, the resolver, streams, listeners and the bound; still to build is the move.

Merged by git alone and read at the merge: the three pub use lines (listeners, the widened streams, resolve); Node and Node::new with name, resolver, streams, listeners, sockets, places once each; next_deadline is stage D's five terms and gains none (a listener keeps no deadline); settle ends in serve_name then the one resolver.pass; receive and fire end in bridge after settle, transmit in pass(now, true); lease.rs keeps Stack::connect and Stack::close [udp]'s and unprefixed, every forward of this stage is tcp_-prefixed. lib.rs against 49e38ca4a differs only in this stage's lines. d21, n2 and r16, the chain without each joined term, are red after the merge (table).

A lookup's sockets hold no place: decided, in words and by a test. resolve.rs binds and closes through Stack::bind and Stack::close, past Node::udp_bind, the one call in which a datagram socket takes a place. They are the node's own, as the responder's is, and stand outside the places, bounded by toyos_dns::MAX_LOOKUPS and each lookup's rounds: so clients at the bound refuse no lookup. places.rs's header opens "the one bound on the streams, listeners and datagram sockets clients make the node hold" and has the paragraph; issues/netstack-datagram-sockets-and-listeners-have-no-bound.md and the track say the same. One test of the resolver's changed, and had to: a_query_with_no_port_to_leave_from_ends_its_lookup_by_name binds every dynamic port but one as a client, 16,383 sockets, which 32 places refuse; it now gives its node exactly 16,383 places and asserts that the next client bind is ResourceExhausted and the lookup still starts and sends. e25 (a socket holds no place) is red there too. The other 22, the burst of MAX_LOOKUPS among them, are green under PLACES = 32 unchanged.

The earlier merges (b230f88a5, fa6f350ff) were reviewed in rounds 1 and 2.

What differs from what ships today

Nothing ships this yet. These are the behaviours that will differ from netstack today once it moves onto the node:

  1. A listener has a queue. Today a listener is one socket that becomes the connection it accepts: one handshake in progress or one connection waiting, and every other peer's SYN is reset. On the node the queues are [tcp]'s: up to 256 handshakes in progress and 128 finished connections waiting, each listener, oldest accepted first.
  2. A handshake nobody finishes is given up after [tcp]'s SYNACK_GIVE_UP (60 s) and holds nothing shut meanwhile. Today it holds the port shut for the rest of the boot.
  3. Wakes are counted, not flagged. Today the owner holds at most one wake, and a second is written only after an accept spent the first. On the node the owner holds one unspent wake for each connection that waits and that there is a place for, written from what [tcp] and the bound say whenever a pass ends. An accept still spends one whatever it answers.
  4. A wake the owner's pipe refuses, for any reason, ends the listener, a full pipe included, as today; but the node writes one only when one is owed, where today's netstack writes an empty wake to every listener on every pass to learn its owner left. The shell tells the node the owner left (Node::close_listener).
  5. One bound, and it counts more. Today max_piped_connections counts established and connecting streams, and nothing bounds listeners (issues/netstack-datagram-sockets-and-listeners-have-no-bound.md). On the node a stream, a listener and a connection [tcp] is finishing alone each hold a place, and a connect, a listen and an accept past the number are refused with nothing made. The number stays the shell's (memory and poller slots are its to know); a new node has none until Node::set_places.
  6. An accept with no place left is answered Full whatever waits; today an accept with nothing waiting is answered "nothing" first. The mapping onto the pipe ABI's codes is the move's.
  7. A listen on a drawn port draws it at random in 49152 to 65535 ([tcp], RFC 6056) where today's counts up from 49152.
  8. A listener takes nodelay, and a connection that begins afterwards has it, as on the host measured above. Today the request is refused for a listener, and every accepted stream starts with Nagle's algorithm on.
  9. A listener is at the address its bind named. Today a listener answers on every address whatever it named. On the node a SYN to another address of the machine is not its to answer, and a named address the machine does not hold is refused. One whose address the machine loses afterwards stands, and answers again when the address is back.
  10. A client's datagram socket holds a place, and a bind past the places is refused. Today nothing bounds datagram sockets. A lookup's sockets hold none: toyos_dns::MAX_LOOKUPS bounds them.
  11. A port is one listener's, whatever address either listen named. Today each listener is a socket of its own and nothing compares ports across addresses.

Unchanged: a listen before the lease listens; an accept without pipes is refused; closing a listener resets what waits at it.

What a peer can make the node hold

Without any client asking, at each listener: 256 handshakes in progress, each a control block with no buffer, each for at most 60 s; and 128 finished connections waiting to be accepted, each with at most one receive buffer of text (65,535 bytes as netstack configures it). Past either, a SYN or a final ACK is dropped and counted (tcp.listen-overflow, tcp.accept-queue-full). Listeners are at most places. The product: 128 waiting connections of 65,535 bytes are 8.4 MB a listener, which no place counts and the listener's one place stands for (decided above, with the reason).

With a client's help: a datagram socket is one place and [udp]'s two queues of 16 datagrams; an accepted or connected stream is one place, two of the client's pipes and [tcp]'s two buffers; a connection the client let go keeps its place until [tcp] has finished it, which the peer can stretch (60 s idle, restarted by every acknowledgment). So a peer holds at most places places, and holding them all denies every connect, listen and accept: recorded in the track with the per-program share that would end it.

How this bound meets stage D's. Stage D lets one peer address keep at most 16 streams their clients can see no more alive by taking their bytes (OWNERLESS_PER_PEER), and counts no addresses; one past the 16 has 100 s from the pass that found it so, until one of the 16 is done. Such a connection holds a place through both of its lives: it is a stream while its pipe holds bytes, and [tcp]'s to finish after. So stage D's rule bounds how long a peer holds a departed client's place while bytes are left, places bound how many a peer holds in all, and neither replaces the other: a_departed_clients_connection_holds_its_place_until_it_is_cut holds one at the bound, sees a connect refused Full, and sees the place go to a connection that waited when stage D's rule cuts it, and peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them holds the places against peers at more addresses than there are places. Still open, in the track: a connection [tcp] is finishing is reset after 60 s idle and every acknowledgment restarts that, so its place is the peer's for as long as it has bytes to acknowledge.

Not bounded by this stage, and recorded in the track: a SYN flood of 256 a minute from addresses that never answer shuts a port to other peers ([tcp] has no SYN cookie); TIME-WAIT is [tcp]'s 16,384 entries.

What changed, per decision

  • listeners.rs: Node::listen, set_listener_nodelay, listener_nodelay, accept, close_listener, listeners, drain_ended_listeners; the trait Wake is the owner's pipe, as stage D's pipe traits are a client's. The queues stay [tcp]'s: the node keeps no second queue of accepted-but-unclaimed connections, which would be a sibling of [tcp]'s and would turn its reset-before-accept rule off.
  • The wake pass is not a pass over streams. It runs where stage D's pass ends (Node::pass, which a frame, a deadline, a transmit opportunity and each call end in), and costs one lookup in [tcp] a listener: Tcp::ready moves nothing, re-files no deadline and offers no flow, which is what toyos-net-node: streams, a connection of the own stack's bridged to its client's two pipes (stage D) #775's review asks of this stage.
  • places.rs: Node::set_places, held, and the one room every admission reads. A setter and not a parameter of Node::new: the node is born with its link down, no address and no place, and the shell tells it each; it also leaves Node::new as stages B and C call it.
  • Edits to stage D's files, each because the bound or an accept needs it: streams.rs gained ConnectRefused (so Node::connect now answers Result<StreamId, ConnectRefused>, the one signature of an earlier stage that changed), Stream::established and Streams::hold/accepted (one constructor for a connect's stream and an accept's), and three wake_owners lines where a place comes back (pass, a closed connect, a stream reset for its pipe). tests/streams.rs: one assertion and one import follow the new refusal type. tests/common/mod.rs: the test node gets 32 places, above the 17 streams stage D's per-peer test holds.
  • lease/tcp.rs: five forwards beside stage D's, tcp_-prefixed as toyos-net-node: streams, a connection of the own stack's bridged to its client's two pipes (stage D) #775's review asks of that file. shard stays private to lease.
  • toyos-net-tcp (named, as the brief asks): Tcp::ready(ListenerId), how many connections wait at a listener, and Tcp::orphans(), the connections close left it to finish, a count kept in close and free. Tests in tcp/tests/held.rs. This round adds Tcp::options(ConnId), the read of a connection's options.
  • toyos-net-shard: four forwards, listener_port, ready, close_listener, orphans. None settles: none makes a flow eligible, and a closed listener's resets leave outside the round. This round adds set_listener_options and options, and Shard::listen answers Result<ListenerId, ListenError> with its NotLocal.
  • Neither listener issue closes. a-handshake-nobody-finishes…'s exit is a test this stage has, and a-connect-between-two-accepts…'s first half too, but both defects are still true of what ships until the move.

The checks (a trust boundary)

Wire input. No received byte is read in the node. no_cut_of_a_syn_is_a_syn delivers every prefix of a peer's SYN and no_flipped_bit_of_a_syn_wakes_an_owner_or_ends_a_listener every single-bit flip of it to a listening node: no wake, no stream, the listener stands, and a whole handshake after it completes. The new unreachable!s (lease/tcp.rs) are on [tcp]'s answers about a listener or a connection the node holds; none is reachable from the wire that I can construct.

Time is an argument of every call; the tests' clock moves only to next_deadline. No wait anywhere.

Negative control. 39 named mutations in 45 runs at e737052a5, each applied as a checked patch, built (step mutation-<name>--build), run against one test by exact name (step mutation-<name>--<test>), and reversed with the tree clean after. Red first for BLOCKER 5 (round 5's steps) and for BLOCKER 6 (above). No whole-change revert: the tests call signatures the base lacks. The patches are in the newest patch comment.

mutation test, by exact name (running 1 test) build test panicked at the panic's first line
t1-an-orphan-that-ends-stays-counted an_orphan_is_counted_until_both_fins 0 101 toyos-net-shard/tcp/tests/held.rs:17:5 assertion left == right failed: TIME-WAIT is no connection
t2-a-closed-connection-is-not-counted an_orphan_is_counted_until_both_fins 0 101 toyos-net-shard/tcp/tests/held.rs:15:5 assertion left == right failed
t3-a-close-in-syn-received-is-not-counted a_close_before_the_handshake_ends_leaves_an_orphan_until_it_gives_up 0 101 toyos-net-shard/tcp/tests/held.rs:40:5 assertion left == right failed
t4-ready-counts-handshakes-in-progress ready_counts_the_connections_accept_has_yet_to_return 0 101 toyos-net-shard/tcp/tests/held.rs:58:5 assertion left == right failed: a handshake in progress is not ready
t5-options-are-the-defaults options_are_the_ones_the_connection_has 0 101 toyos-net-shard/tcp/tests/held.rs:77:5 assertion left == right failed: its listener's, as its SYN found them
o1-a-listener-is-one-connection a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up 0 101 userland/netstack/node/tests/listeners.rs:471:41 a SYN-ACK to the second peer, not [Segment { to_mac: [2, 0, 0, 0, 0, 11], to: 192.0.2.2, from_port: 22, to_...
o1-a-listener-is-one-connection a_connect_between_two_accepts_is_queued_not_reset 0 101 userland/netstack/node/tests/listeners.rs:306:64 a SYN-ACK to the peer
e01-a-pass-wakes-nobody a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 0 101 userland/netstack/node/tests/listeners.rs:442:5 assertion left == right failed
e02-an-accept-ends-in-no-pass a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 0 101 userland/netstack/node/tests/listeners.rs:449:5 assertion left == right failed: what arrived before the accept moves in it
e03-an-owner-holds-one-wake-at-most a_connect_between_two_accepts_is_queued_not_reset 0 101 userland/netstack/node/tests/listeners.rs:502:5 assertion left == right failed: one wake a connection
e04-only-an-accept-that-takes-spends-a-wake an_accept_spends_a_wake_whatever_it_answers 0 101 userland/netstack/node/tests/listeners.rs:527:5 assertion left == right failed: the wake that accept spent is not counted against the next connection
e19-a-drawn-port-reads-the-draws-high-half a_listen_on_a_taken_port_is_refused_and_a_drawn_port_listens 0 101 userland/netstack/node/tests/listeners.rs:729:5 assertion left == right failed
e05-a-wake-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 0 101 userland/netstack/node/tests/listeners.rs:556:5 assertion left == right failed: two wait, and there is a place for one
e06-an-accept-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 0 101 userland/netstack/node/tests/listeners.rs:558:31 called Result::unwrap_err() on an Ok value: (Accepted { id: StreamId(1), remote: Endpoint { addr: 192.0...
e07-a-finishing-connection-holds-no-place a_wake_is_owed_only_for_a_connection_there_is_a_place_for 0 101 userland/netstack/node/tests/listeners.rs:564:5 assertion left == right failed: a connection [tcp] is finishing holds its place
e13-a-connect-ignores-places a_connect_past_the_places_is_refused_and_sends_nothing 0 101 userland/netstack/node/tests/listeners.rs:640:5 assertion left == right failed: a connect not yet answered holds a place
e18-a-stream-holds-no-place a_connect_past_the_places_is_refused_and_sends_nothing 0 101 userland/netstack/node/tests/listeners.rs:640:5 assertion left == right failed: a connect not yet answered holds a place
e14-a-listen-ignores-places a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 0 101 userland/netstack/node/tests/listeners.rs:659:23 called Result::unwrap_err() on an Ok value: (ListenerId(1), Port(23))
e15-a-listener-holds-no-place a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 0 101 userland/netstack/node/tests/listeners.rs:659:23 called Result::unwrap_err() on an Ok value: (ListenerId(1), Port(23))
e09-a-closed-connect-wakes-nobody closing_a_connect_gives_its_place_to_a_connection_that_waits 0 101 userland/netstack/node/tests/listeners.rs:597:5 assertion left == right failed
e10-a-stream-reset-for-its-pipe-wakes-nobody a_stream_reset_for_its_pipe_gives_its_place_to_a_connection_that_waits 0 101 userland/netstack/node/tests/listeners.rs:609:5 assertion left == right failed
e11-a-closed-listener-wakes-nobody closing_a_listener_gives_its_place_to_a_connection_that_waits_at_another 0 101 userland/netstack/node/tests/listeners.rs:620:5 assertion left == right failed
e12-more-places-wake-nobody more_places_wake_the_owner_of_a_connection_that_waits 0 101 userland/netstack/node/tests/listeners.rs:630:5 assertion left == right failed
e16-a-closed-listener-stays-in-the-stack closing_a_listener_resets_what_waits_and_frees_its_port 0 101 userland/netstack/node/tests/listeners.rs:687:9 Segment { to_mac: [2, 0, 0, 0, 0, 11], to: 192.0.2.2, from_port: 22, to_port: 40001, seq: 530926582, ack: S...
e17-a-refused-wake-is-ignored a_wake_the_owners_pipe_refuses_ends_the_listener 0 101 userland/netstack/node/tests/listeners.rs:707:9 assertion left == right failed
e20-a-listeners-option-does-not-reach-tcp a_stream_starts_with_the_options_its_connection_took_from_its_listener 0 101 userland/netstack/node/tests/listeners.rs:856:5 assertion left == right failed
e21-an-accepted-stream-holds-no-option a_stream_starts_with_the_options_its_connection_took_from_its_listener 0 101 userland/netstack/node/tests/listeners.rs:856:5 assertion left == right failed
e22-an-accepted-stream-is-counted-by-the-nodes-address an_accepted_stream_is_one_of_its_peers_addresss_sixteen 0 101 userland/netstack/node/tests/listeners.rs:908:5 assertion left == right failed
e23-an-id-is-used-twice a_request_for_a_stream_that_was_cut_names_nothing 0 101 userland/netstack/node/tests/listeners.rs:935:5 assertion left != right failed
e05-a-wake-ignores-places peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 0 101 userland/netstack/node/tests/listeners.rs:964:5 assertion left == right failed: sixty-four wait, and there are places for two
e06-an-accept-ignores-places peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 0 101 userland/netstack/node/tests/listeners.rs:974:39 called Result::unwrap_err() on an Ok value: (Accepted { id: StreamId(2), remote: Endpoint { addr: 192.0...
e18-a-stream-holds-no-place peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 0 101 userland/netstack/node/tests/listeners.rs:974:39 called Result::unwrap_err() on an Ok value: (Accepted { id: StreamId(2), remote: Endpoint { addr: 192.0...
e24-a-bind-ignores-places a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101 userland/netstack/node/tests/listeners.rs:824:5 assertion left == right failed
e25-a-datagram-socket-holds-no-place a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101 userland/netstack/node/tests/listeners.rs:823:5 assertion left == right failed
e26-a-closed-socket-wakes-nobody a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101 userland/netstack/node/tests/listeners.rs:831:5 assertion left == right failed: its place is back, and the connection that waited is announced
e27-a-closed-socket-keeps-its-place a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101 userland/netstack/node/tests/listeners.rs:831:5 assertion left == right failed: its place is back, and the connection that waited is announced
e28-a-listen-is-at-every-address a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds 0 101 userland/netstack/node/tests/listeners.rs:744:23 called Result::unwrap_err() on an Ok value: (ListenerId(0), Port(22))
e30-a-listen-takes-an-address-nobody-holds a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds 0 101 userland/netstack/node/tests/listeners.rs:744:23 called Result::unwrap_err() on an Ok value: (ListenerId(0), Port(22))
e29-a-listener-ended-in-a-pass-ends-the-pass a_listener_ended_for_its_wake_gives_its_place_to_another_in_the_same_pass 0 101 userland/netstack/node/tests/listeners.rs:810:5 assertion left == right failed
e31-a-port-is-held-at-one-address-only a_listen_at_the_machines_address_takes_no_port_held_at_every_address 0 101 userland/netstack/node/tests/listeners.rs:766:23 called Result::unwrap_err() on an Ok value: (ListenerId(1), Port(22))
e31-a-port-is-held-at-one-address-only a_listen_at_every_address_takes_no_port_held_at_the_machines_address 0 101 userland/netstack/node/tests/listeners.rs:766:23 called Result::unwrap_err() on an Ok value: (ListenerId(1), Port(22))
e25-a-datagram-socket-holds-no-place a_query_with_no_port_to_leave_from_ends_its_lookup_by_name 0 101 userland/netstack/node/tests/resolve.rs:575:5 assertion left == right failed: every place is a client's socket
d21-the-nodes-deadline-leaves-streams-out a_connect_past_its_deadline_is_timed_out_at_the_deadline 0 101 userland/netstack/node/tests/streams.rs:566:5 assertion left == right failed
n2-deadline-without-the-name a_held_lease_is_announced_at_once_and_a_second_later 0 101 userland/netstack/node/tests/name.rs:170:5 2s apart
r16-deadline-without-the-lookups a_resolver_that_never_answers_is_asked_at_each_waits_end 0 101 userland/netstack/node/tests/resolve.rs:615:5 assertion left == right failed

Gates

At the merged head f0c483ea2, by me, in this worktree, tree clean before and after, on macOS under load (1-minute load average 35 to 107 as sampled over the steps); step names are the log names.

head step command exit
f0c483ea2 10-build-only cargo run -- --build-only 0
20-gate-lock cargo metadata --locked --format-version 1 0
21-gate-tcp-tests cargo test --locked -p toyos-net-tcp (21 targets with tests, 5 to 42 each; held 6, take 5) 0
22-gate-shard-tests cargo test --locked -p toyos-net-shard (acquisition 5, drr 6, egress 19, icmp 4, log 3, net 5, udp 3, 2 unit tests) 0
23-gate-node-tests cargo test --locked --manifest-path userland/netstack/node/Cargo.toml (datagram 5, host 1, lease 19, listeners 28, name 13, resolve 23, slirp 3, streams 34) 0
24-gate-dns-tests cargo test --locked -p toyos-dns (44) 0
25-gate-ip-tests cargo test --locked -p toyos-net-ip (acd 18, addr 37, clk 10, icmp 53, igmp 35, io 52, nbr 23, nud 35, 8 doc tests) 0
26-gate-tcp-clippy, 27-gate-shard-clippy, 28-gate-node-clippy, 29-gate-dns-clippy cargo clippy --locked ... --all-targets -- <the adopted lints> -D warnings, this machine's toolchain 0, 0, 0, 0
30-ci-host cargo run -- --ci host (Host: 78 step(s), all green; clippy, warnings denied: clean; the node's eight targets with the counts above) 0
40-mutations-apply-check git apply --check of each of the 39 patches 0 each

tests/host.rs on this machine: test a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok, green, so its one assert_eq! held (true, false) on macos, in 23-gate-node-tests and again in 30-ci-host.

Round 6, at the reviewed head; round 5's steps (at d1d940dfd and c27cc36aa) are in the earlier body and their logs are kept.

head step command exit
c27cc36aa + the new test red-first-build, red-first--<test> as above 0; 101
e737052a5 gate-lock cargo metadata --locked --format-version 1 0
gate-tcp-tests cargo test --locked -p toyos-net-tcp (21 targets with tests, 5 to 42 each; held 6, take 5) 0
gate-shard-tests cargo test --locked -p toyos-net-shard (acquisition 5, drr 6, egress 19, icmp 4, log 3, net 5, udp 3, 2 unit tests) 0
gate-node-tests cargo test --locked --manifest-path userland/netstack/node/Cargo.toml (datagram 5, host 1, lease 19, listeners 28, name 13, resolve 23, slirp 3, streams 34) 0
gate-root-lib cargo test --locked --lib -- hostws:: userlandhost:: sourcegate:: licence:: (45 tests) 0
gate-tcp-clippy, gate-shard-clippy, gate-node-clippy cargo clippy --locked ... --all-targets -- <the adopted lints> -D warnings, this machine's toolchain (1.98.1) 0, 0, 0
mutations-summary 45 runs of 39 patches: build 0 each, named test 101 each, running 1 test each 0
after-mutations-tcp-held, after-mutations-node-tests held and the node's tests again, unmutated 0, 0

Not run, and not claimed: the host job on ubuntu-24.04 and guest / suite, which a draft skips; clippy on CI's toolchain; tests/host.rs on Linux or Windows; the 39 mutations at f0c483ea2 (above). No guest test reaches the change: no shipped package depends on toyos-net-node, toyos-net-shard or toyos-net-tcp.

What I am unsure of

  • Linux's answers in tests/host.rs. Unread; macOS answers (true, false). The arm no longer rests on delivery order, so a red on Linux is Linux's answer, printed with its name.
  • mio on Windows is read from its API (AsSocket for SockRef), not run.
  • A listener at a lost address stands and is told nothing, as a datagram socket is; no host was read for it. A connection that waited at it when the address went is [tcp]'s to give up by its own rules, which this stage does not test.
  • InUse is checked before NotLocal in Node::listen, where [udp]'s bind checks the address first: a listen at an address the machine does not hold on a held port answers InUse.
  • ListenRefused::NotLocal counts nothing; in the track with the other unspecified additions.
  • A lookup's bound in sockets is MAX_LOOKUPS times what a lookup keeps out over its rounds; I did not derive the product.
  • Places are not shared out among programs; in the track, with its exit.
  • a_wake_left_by_a_connection_its_peer_reset_stands_for_the_next has no mutation of its own: no line writes a wake for an arrival.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A

Japabu and others added 2 commits October 8, 2026 20:33
A listener's owner is woken for the connections that finished their
handshake, and nothing told a caller how many wait short of accepting
them: `Tcp::ready` does. A connection `close` leaves [tcp] to finish
alone is still two buffers and its peer decides for how long, so a
caller that bounds what it holds has to count them: `Tcp::orphans`,
kept as the user changes hands and as the connection is freed.

The shard forwards both, and `listener_port` and `close_listener`,
which it had left out. None of them settles: no flow becomes eligible,
and the resets a closed listener owes leave outside the round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…hat clients make the node hold

Stage E of the cut of the track's stage 5. A listener is a passive open
of [tcp]'s and the pipe its owner reads its wakes from; the queues stay
[tcp]'s. The owner holds one unspent wake for each connection that
waits and that there is a place for, written from what [tcp] and the
bound say when a pass ends, so no wake depends on the pass that saw a
connection arrive. An accept makes the oldest waiting connection a
stream on its pipes.

Places are the stream cap stage D left out, and the listeners' too: a
stream, a listener and a connection [tcp] is finishing alone each hold
one, and a connect, a listen and an accept past the number are refused
with nothing made. The number is the shell's and a new node has none,
so `connect` answers a refusal of the node's own beside the stack's.

The two recorded listener failures of the stack this replaces are
tests here that read the other way: a second peer is answered while a
first handshake hangs, and a connect between two accepts is queued.
Neither issue closes: what ships still has both until the move.

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

The 23 mutations of stage E, in 24 runs

Run by the orchestrator at 1e97631c0 (orch/builds/ownstack-e-r1.log), each by build-request.sh's mutation: git apply --check, git apply, cargo test --no-run (exit 0 every time), then the one test by --exact name, then git apply -R and a clean-tree check. t patches run cargo test --locked -p toyos-net-tcp --test held, the rest cargo test --locked --manifest-path userland/netstack/node/Cargo.toml --test listeners.

mutation test that turned red exit
t1-an-orphan-that-ends-stays-counted an_orphan_is_counted_until_both_fins 101
t2-a-closed-connection-is-not-counted an_orphan_is_counted_until_both_fins 101
t3-a-close-in-syn-received-is-not-counted a_close_before_the_handshake_ends_leaves_an_orphan_until_it_gives_up 101
t4-ready-counts-handshakes-in-progress ready_counts_the_connections_accept_has_yet_to_return 101
o1-a-listener-is-one-connection a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up 101
o1-a-listener-is-one-connection a_connect_between_two_accepts_is_queued_not_reset 101
e01-a-pass-wakes-nobody a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 101
e02-an-accept-ends-in-no-pass a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 101
e03-an-owner-holds-one-wake-at-most a_connect_between_two_accepts_is_queued_not_reset 101
e04-only-an-accept-that-takes-spends-a-wake an_accept_spends_a_wake_whatever_it_answers 101
e19-a-drawn-port-reads-the-draws-high-half a_listen_on_a_taken_port_is_refused_and_a_drawn_port_listens 101
e05-a-wake-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e06-an-accept-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e07-a-finishing-connection-holds-no-place a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e13-a-connect-ignores-places a_connect_past_the_places_is_refused_and_sends_nothing 101
e18-a-stream-holds-no-place a_connect_past_the_places_is_refused_and_sends_nothing 101
e14-a-listen-ignores-places a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 101
e15-a-listener-holds-no-place a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 101
e09-a-closed-connect-wakes-nobody closing_a_connect_gives_its_place_to_a_connection_that_waits 101
e10-a-stream-reset-for-its-pipe-wakes-nobody a_stream_reset_for_its_pipe_gives_its_place_to_a_connection_that_waits 101
e11-a-closed-listener-wakes-nobody closing_a_listener_gives_its_place_to_a_connection_that_waits_at_another 101
e12-more-places-wake-nobody more_places_wake_the_owner_of_a_connection_that_waits 101
e16-a-closed-listener-stays-in-the-stack closing_a_listener_resets_what_waits_and_frees_its_port 101
e17-a-refused-wake-is-ignored a_wake_the_owners_pipe_refuses_ends_the_listener 101
The patches

e01-a-pass-wakes-nobody.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index c142eb558..450c0f39e 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -296,7 +296,6 @@ impl Node {
         let Streams { live, events, .. } = &mut self.streams;
         let stack = &mut self.stack;
         live.retain(|id, stream| stream.pass(*id, now, stack, events));
-        self.wake_owners(now);
     }
 
     /// The client lets go of the stream: nobody reads it, and what its pipe still holds is sent

e02-an-accept-ends-in-no-pass.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..8e7032e24 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -106,7 +106,6 @@ impl Node {
         listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
-        self.bridge(now);
         answer
     }
 

e03-an-owner-holds-one-wake-at-most.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..999eaf136 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -150,7 +150,7 @@ impl Node {
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
-            while listener.unspent < owed {
+            while listener.unspent < owed.min(1) {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
                 }

e04-only-an-accept-that-takes-spends-a-wake.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..695fc1c84 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -103,9 +103,11 @@ impl Node {
     /// and what it already received moves at once.
     pub fn accept(&mut self, now: Instant, id: ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return Err(AcceptRefused::NoListener) };
-        listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
+        if let (Ok(_), Some(listener)) = (&answer, self.listeners.live.get_mut(&id)) {
+            listener.unspent = listener.unspent.saturating_sub(1);
+        }
         self.bridge(now);
         answer
     }

e05-a-wake-ignores-places.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..ab4900dc6 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -149,7 +149,7 @@ impl Node {
         let room = self.room();
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
-            let owed = stack.tcp_ready(listener.bound).min(room);
+            let owed = stack.tcp_ready(listener.bound);
             while listener.unspent < owed {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));

e06-an-accept-ignores-places.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..00914dadc 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -112,9 +112,6 @@ impl Node {
 
     fn take(&mut self, bound: toyos_net_tcp::ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(pipes) = pipes else { return Err(AcceptRefused::NoPipes) };
-        if self.room() == 0 {
-            return Err(AcceptRefused::Full);
-        }
         let Some((conn, tuple)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         let id = self.streams.accepted(conn, pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })

e07-a-finishing-connection-holds-no-place.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 4f0a8362d..9220d676e 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -25,7 +25,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners())
     }
 
     pub(crate) fn room(&self) -> usize {

e09-a-closed-connect-wakes-nobody.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index c142eb558..d5cdaec26 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -307,7 +307,6 @@ impl Node {
             self.stack.abort(now, stream.conn);
             self.streams.live.remove(&id);
             self.streams.events.push_back(StreamEvent::Closed { id });
-            self.wake_owners(now);
             return;
         }
         stream.to_client = None;

e10-a-stream-reset-for-its-pipe-wakes-nobody.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index c142eb558..2bbf70a7b 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -358,7 +358,6 @@ impl Node {
         if held {
             self.stack.abort(now, stream.conn);
             self.streams.live.remove(&id);
-            self.wake_owners(now);
         }
     }
 

e11-a-closed-listener-wakes-nobody.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..f95f2d48a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -124,9 +124,6 @@ impl Node {
     /// says nobody holds the other end of the wake pipe. `false` is an id that names no listener.
     pub fn close_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let closed = self.end_listener(now, id);
-        if closed {
-            self.wake_owners(now);
-        }
         closed
     }
 

e12-more-places-wake-nobody.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 4f0a8362d..e2efd9b4c 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -20,7 +20,6 @@ impl Node {
     /// How many places the node has from here on. Nothing held is let go for a smaller number.
     pub fn set_places(&mut self, now: Instant, places: usize) {
         self.places = places;
-        self.wake_owners(now);
     }
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.

e13-a-connect-ignores-places.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index c142eb558..39315b7a2 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -283,9 +283,6 @@ impl Node {
     /// An active open to `remote`, answered by a [`StreamEvent`] once the handshake ends or
     /// `timeout` passes. Refused, nothing was sent and the pipe ends are dropped.
     pub fn connect(&mut self, now: Instant, remote: Endpoint, timeout: Option<Duration>, pipes: Pipes) -> Result<StreamId, ConnectRefused> {
-        if self.room() == 0 {
-            return Err(ConnectRefused::Full);
-        }
         let conn = self.stack.connect(now, remote).map_err(ConnectRefused::Stack)?;
         let deadline = timeout.map(|within| now.after(within));
         Ok(self.streams.hold(Stream { connecting: true, deadline, ..Stream::established(conn, pipes) }))

e14-a-listen-ignores-places.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..e0caa7e1e 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -85,9 +85,6 @@ impl Node {
     /// the interface holds or comes to hold. Answers the listener and its port. Refused, nothing
     /// was made and `owner` is dropped.
     pub fn listen(&mut self, port: Option<Port>, owner: Box<dyn Wake>, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> {
-        if self.room() == 0 {
-            return Err(ListenRefused::Full);
-        }
         let candidate = || {
             let [low, high, ..] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])

e15-a-listener-holds-no-place.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 4f0a8362d..fc53aa263 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -25,7 +25,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {

e16-a-closed-listener-stays-in-the-stack.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..e40b919ad 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -132,7 +132,6 @@ impl Node {
 
     fn end_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let Some(listener) = self.listeners.live.remove(&id) else { return false };
-        self.stack.tcp_close_listener(now, listener.bound);
         true
     }
 

e17-a-refused-wake-is-ignored.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..59fcf045c 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -151,8 +151,8 @@ impl Node {
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
             while listener.unspent < owed {
-                if let Err(refusal) = listener.owner.wake() {
-                    return Some((*id, refusal));
+                if listener.owner.wake().is_err() {
+                    break;
                 }
                 listener.unspent = listener.unspent.saturating_add(1);
             }

e18-a-stream-holds-no-place.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 4f0a8362d..133ee496d 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -25,7 +25,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.listeners().saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {

e19-a-drawn-port-reads-the-draws-high-half.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index ced370d03..680d269f2 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -89,7 +89,7 @@ impl Node {
             return Err(ListenRefused::Full);
         }
         let candidate = || {
-            let [low, high, ..] = draw().to_le_bytes();
+            let [.., low, high] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
         };
         let Some((bound, port)) = self.stack.tcp_listen(port, candidate) else { return Err(ListenRefused::InUse) };

o1-a-listener-is-one-connection.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..d7fd6b6b5 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -749,7 +749,7 @@ impl Tcp {
             return;
         }
         let Some(listener) = value(&mut self.listeners, index) else { return };
-        if listener.pending.len() >= limits::LISTEN_PENDING || listener.ready.len() >= limits::LISTEN_READY {
+        if !listener.pending.is_empty() || !listener.ready.is_empty() {
             self.log.count(Counter::ListenOverflow);
             return;
         }

t1-an-orphan-that-ends-stays-counted.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..996fc0cd7 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -449,9 +449,6 @@ impl Tcp {
     fn free(&mut self, index: u32) {
         let Some(generation) = self.conns.get(usize::try_from(index).unwrap_or(usize::MAX)).map(|s| s.generation) else { return };
         let Some(conn) = release(&mut self.conns, &mut self.free_conns, index) else { return };
-        if conn.user == User::Orphan {
-            self.orphans = self.orphans.saturating_sub(1);
-        }
         let remote = conn.tuple.remote.addr;
         let parked = self.parked.get(&remote).is_some_and(|p| p.conns.contains(&index));
         // Its index may name another connection next.

t2-a-closed-connection-is-not-counted.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..9856e5858 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1085,7 +1085,6 @@ impl Tcp {
                 sync.shutdown_write(now);
                 sync.orphan(now);
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
         }

t3-a-close-in-syn-received-is-not-counted.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..65ba099a1 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1072,7 +1072,6 @@ impl Tcp {
             Tcb::SynRcvd(rcvd) => {
                 rcvd.shutdown_write();
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
             Tcb::Sync(sync) if sync.rx.unread() > 0 => {

t4-ready-counts-handshakes-in-progress.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..178b9d5a8 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -585,7 +585,7 @@ impl Tcp {
 
     /// How many children completed their handshake and wait for [`Self::accept`].
     pub fn ready(&mut self, id: ListenerId) -> Result<usize, Error> {
-        slot(&mut self.listeners, id.index, id.generation).map(|l| l.ready.len()).ok_or(Error::NoSuchSocket)
+        slot(&mut self.listeners, id.index, id.generation).map(|l| l.pending.len()).ok_or(Error::NoSuchSocket)
     }
 
     /// The oldest child that completed its handshake.

The wake hook moves to the end of `Node::pass`, which a frame, a
deadline and now a transmit opportunity end in, so a connect that fails
inside an opportunity gives its place to a connection that waits. A
wake's refusal is a write's (`WriteRefusal`), an accepted stream
carries its peer's address as a connected one does, and the test node
has 32 places, above the 17 streams stage D's per-peer test holds.

A departed client's connection its peer keeps alive holds its place
through both of its lives, as a stream while its pipe holds bytes and
as [tcp]'s to finish after: a test here holds one at the bound until
stage D's rule cuts it.

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

The 23 mutations of stage E, in 24 runs, at 1c38aed59

Regenerated after stage D's fix round (ddf479738) was merged; these replace the patches of the comment above, which were against 1e97631c0. Run by the orchestrator (orch/builds/ownstack-e-r2.log), each by build-request.sh's mutation: git apply --check, git apply, cargo test --no-run (exit 0 every time), then the one test by --exact name, then git apply -R and a clean-tree check. t patches run cargo test --locked -p toyos-net-tcp --test held, the rest cargo test --locked --manifest-path userland/netstack/node/Cargo.toml --test listeners.

mutation test that turned red exit
t1-an-orphan-that-ends-stays-counted an_orphan_is_counted_until_both_fins 101
t2-a-closed-connection-is-not-counted an_orphan_is_counted_until_both_fins 101
t3-a-close-in-syn-received-is-not-counted a_close_before_the_handshake_ends_leaves_an_orphan_until_it_gives_up 101
t4-ready-counts-handshakes-in-progress ready_counts_the_connections_accept_has_yet_to_return 101
o1-a-listener-is-one-connection a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up 101
o1-a-listener-is-one-connection a_connect_between_two_accepts_is_queued_not_reset 101
e01-a-pass-wakes-nobody a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 101
e02-an-accept-ends-in-no-pass a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 101
e03-an-owner-holds-one-wake-at-most a_connect_between_two_accepts_is_queued_not_reset 101
e04-only-an-accept-that-takes-spends-a-wake an_accept_spends_a_wake_whatever_it_answers 101
e19-a-drawn-port-reads-the-draws-high-half a_listen_on_a_taken_port_is_refused_and_a_drawn_port_listens 101
e05-a-wake-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e06-an-accept-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e07-a-finishing-connection-holds-no-place a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e13-a-connect-ignores-places a_connect_past_the_places_is_refused_and_sends_nothing 101
e18-a-stream-holds-no-place a_connect_past_the_places_is_refused_and_sends_nothing 101
e14-a-listen-ignores-places a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 101
e15-a-listener-holds-no-place a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 101
e09-a-closed-connect-wakes-nobody closing_a_connect_gives_its_place_to_a_connection_that_waits 101
e10-a-stream-reset-for-its-pipe-wakes-nobody a_stream_reset_for_its_pipe_gives_its_place_to_a_connection_that_waits 101
e11-a-closed-listener-wakes-nobody closing_a_listener_gives_its_place_to_a_connection_that_waits_at_another 101
e12-more-places-wake-nobody more_places_wake_the_owner_of_a_connection_that_waits 101
e16-a-closed-listener-stays-in-the-stack closing_a_listener_resets_what_waits_and_frees_its_port 101
e17-a-refused-wake-is-ignored a_wake_the_owners_pipe_refuses_ends_the_listener 101
The patches

e01-a-pass-wakes-nobody.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..8d9618378 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -353,7 +353,6 @@ impl Node {
             *peers = peers.saturating_add(1);
         }
         live.retain(|id, stream| (connects && !stream.connecting) || stream.pass(*id, now, stack, events, &mut extended));
-        self.wake_owners(now);
     }
 
     /// The client lets go of the stream: nobody reads it, and what its pipe still holds is sent

e02-an-accept-ends-in-no-pass.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..abb6b1e40 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -106,7 +106,6 @@ impl Node {
         listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
-        self.bridge(now);
         answer
     }
 

e03-an-owner-holds-one-wake-at-most.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..da391fd88 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -150,7 +150,7 @@ impl Node {
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
-            while listener.unspent < owed {
+            while listener.unspent < owed.min(1) {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
                 }

e04-only-an-accept-that-takes-spends-a-wake.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..f131032af 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -103,9 +103,11 @@ impl Node {
     /// and what it already received moves at once.
     pub fn accept(&mut self, now: Instant, id: ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return Err(AcceptRefused::NoListener) };
-        listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
+        if let (Ok(_), Some(listener)) = (&answer, self.listeners.live.get_mut(&id)) {
+            listener.unspent = listener.unspent.saturating_sub(1);
+        }
         self.bridge(now);
         answer
     }

e05-a-wake-ignores-places.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..efa6d38aa 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -149,7 +149,7 @@ impl Node {
         let room = self.room();
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
-            let owed = stack.tcp_ready(listener.bound).min(room);
+            let owed = stack.tcp_ready(listener.bound);
             while listener.unspent < owed {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));

e06-an-accept-ignores-places.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..7b914b1dc 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -112,9 +112,6 @@ impl Node {
 
     fn take(&mut self, bound: toyos_net_tcp::ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(pipes) = pipes else { return Err(AcceptRefused::NoPipes) };
-        if self.room() == 0 {
-            return Err(AcceptRefused::Full);
-        }
         let Some((conn, tuple)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         let id = self.streams.accepted(conn, tuple.remote.addr, pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })

e07-a-finishing-connection-holds-no-place.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..a9b8d34d1 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -28,7 +28,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners())
     }
 
     pub(crate) fn room(&self) -> usize {

e09-a-closed-connect-wakes-nobody.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..d7bd4849d 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -364,7 +364,6 @@ impl Node {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
             self.streams.events.push_back(StreamEvent::Closed { id });
-            self.wake_owners(now);
             return;
         }
         stream.to_client = None;

e10-a-stream-reset-for-its-pipe-wakes-nobody.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..0669d43a0 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -419,7 +419,6 @@ impl Node {
         if held {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
-            self.wake_owners(now);
         }
     }
 

e11-a-closed-listener-wakes-nobody.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..cf1255229 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -124,9 +124,6 @@ impl Node {
     /// says nobody holds the other end of the wake pipe. `false` is an id that names no listener.
     pub fn close_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let closed = self.end_listener(now, id);
-        if closed {
-            self.wake_owners(now);
-        }
         closed
     }
 

e12-more-places-wake-nobody.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..e66c87073 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -23,7 +23,6 @@ impl Node {
     /// How many places the node has from here on. Nothing held is let go for a smaller number.
     pub fn set_places(&mut self, now: Instant, places: usize) {
         self.places = places;
-        self.wake_owners(now);
     }
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.

e13-a-connect-ignores-places.patch

diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..43accad69 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -330,9 +330,6 @@ impl Node {
     /// An active open to `remote`, answered by a [`StreamEvent`] once the handshake ends or
     /// `timeout` passes. Refused, nothing was sent and the pipe ends are dropped.
     pub fn connect(&mut self, now: Instant, remote: Endpoint, timeout: Option<Duration>, pipes: Pipes) -> Result<StreamId, ConnectRefused> {
-        if self.room() == 0 {
-            return Err(ConnectRefused::Full);
-        }
         let conn = self.stack.tcp_connect(now, remote).map_err(ConnectRefused::Stack)?;
         let deadline = timeout.map(|within| now.after(within));
         Ok(self.streams.hold(Stream { connecting: true, deadline, ..Stream::established(conn, remote.addr, pipes) }))

e14-a-listen-ignores-places.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..b51dc693a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -85,9 +85,6 @@ impl Node {
     /// the interface holds or comes to hold. Answers the listener and its port. Refused, nothing
     /// was made and `owner` is dropped.
     pub fn listen(&mut self, port: Option<Port>, owner: Box<dyn Wake>, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> {
-        if self.room() == 0 {
-            return Err(ListenRefused::Full);
-        }
         let candidate = || {
             let [low, high, ..] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])

e15-a-listener-holds-no-place.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..1f6665268 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -28,7 +28,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {

e16-a-closed-listener-stays-in-the-stack.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..341d5ee6a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -132,7 +132,6 @@ impl Node {
 
     fn end_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let Some(listener) = self.listeners.live.remove(&id) else { return false };
-        self.stack.tcp_close_listener(now, listener.bound);
         true
     }
 

e17-a-refused-wake-is-ignored.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..43e18cff3 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -151,8 +151,8 @@ impl Node {
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
             while listener.unspent < owed {
-                if let Err(refusal) = listener.owner.wake() {
-                    return Some((*id, refusal));
+                if listener.owner.wake().is_err() {
+                    break;
                 }
                 listener.unspent = listener.unspent.saturating_add(1);
             }

e18-a-stream-holds-no-place.patch

diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..5cd96add3 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -28,7 +28,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.listeners().saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {

e19-a-drawn-port-reads-the-draws-high-half.patch

diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..269f61d2b 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -89,7 +89,7 @@ impl Node {
             return Err(ListenRefused::Full);
         }
         let candidate = || {
-            let [low, high, ..] = draw().to_le_bytes();
+            let [.., low, high] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
         };
         let Some((bound, port)) = self.stack.tcp_listen(port, candidate) else { return Err(ListenRefused::InUse) };

o1-a-listener-is-one-connection.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..d7fd6b6b5 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -749,7 +749,7 @@ impl Tcp {
             return;
         }
         let Some(listener) = value(&mut self.listeners, index) else { return };
-        if listener.pending.len() >= limits::LISTEN_PENDING || listener.ready.len() >= limits::LISTEN_READY {
+        if !listener.pending.is_empty() || !listener.ready.is_empty() {
             self.log.count(Counter::ListenOverflow);
             return;
         }

t1-an-orphan-that-ends-stays-counted.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..996fc0cd7 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -449,9 +449,6 @@ impl Tcp {
     fn free(&mut self, index: u32) {
         let Some(generation) = self.conns.get(usize::try_from(index).unwrap_or(usize::MAX)).map(|s| s.generation) else { return };
         let Some(conn) = release(&mut self.conns, &mut self.free_conns, index) else { return };
-        if conn.user == User::Orphan {
-            self.orphans = self.orphans.saturating_sub(1);
-        }
         let remote = conn.tuple.remote.addr;
         let parked = self.parked.get(&remote).is_some_and(|p| p.conns.contains(&index));
         // Its index may name another connection next.

t2-a-closed-connection-is-not-counted.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..9856e5858 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1085,7 +1085,6 @@ impl Tcp {
                 sync.shutdown_write(now);
                 sync.orphan(now);
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
         }

t3-a-close-in-syn-received-is-not-counted.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..65ba099a1 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1072,7 +1072,6 @@ impl Tcp {
             Tcb::SynRcvd(rcvd) => {
                 rcvd.shutdown_write();
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
             Tcb::Sync(sync) if sync.rx.unread() > 0 => {

t4-ready-counts-handshakes-in-progress.patch

diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..178b9d5a8 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -585,7 +585,7 @@ impl Tcp {
 
     /// How many children completed their handshake and wait for [`Self::accept`].
     pub fn ready(&mut self, id: ListenerId) -> Result<usize, Error> {
-        slot(&mut self.listeners, id.index, id.generation).map(|l| l.ready.len()).ok_or(Error::NoSuchSocket)
+        slot(&mut self.listeners, id.index, id.generation).map(|l| l.pending.len()).ok_or(Error::NoSuchSocket)
     }
 
     /// The oldest child that completed its handshake.

Japabu and others added 2 commits October 8, 2026 21:29
Stage D at 47231f0 carries origin/main (26f5ef2, stage A landed) and
its rounds 2 and 3: Stream::reader_left is gone, the cut arms on a stream
its client can see no more, and `extended` is decided in every pass.

Resolved by hand:

- streams.rs, Node::connect: stage E's body (the places check, then
  Streams::hold of Stream::established) over stage D's, which built the
  Stream field by field without reader_left. Stage E's
  Stream::established, merged without a conflict, still named
  reader_left: that line is deleted.
- the track, "What the node does not yet meet": stage E's three lines
  (the places are one number, a listener's handshakes in progress, the
  unspent wake) are kept; stage D's line on a node without a bound is
  dropped, since the places are that bound; stage D's rewritten lines on
  the stream its client can see no more, the reset stream's options and
  the pass over every stream replace stage E's copies of the older ones.
  Two sentences are rewritten for the merged tree: the places line points
  at the line it means by name, and the departed-client line names
  places.rs as the bound across addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…, and the places bound is tested across peer addresses

Stage D's round 2 times any stream its client can see no more, an
accepted one included, by its peer's address, and writes a stream's whole
option set from the copy the stream holds. Three things about a stream an
accept makes follow.

Its options. Stream::established seeded every stream with
Options::default(). That was true only while nothing gave a listener an
option. Stream::established now takes the options, and Streams::accepted
writes the listener's to [tcp] and seeds the stream with what it wrote,
so the copy set_nodelay rewrites is the connection's by construction.
The listener's options are the node's (Node::set_listener_nodelay,
listener_nodelay): [tcp] hands its own copy for a listener to a
connection at the SYN and has no call that reads a connection's back, so
a seed taken from there is wrong for a connection begun before the option
was set. A listening socket that takes TCP_NODELAY and hands it to what
it accepts is what a host does; netstack on smoltcp refuses the request
for a listener.

Its peer address. Already tuple.remote.addr; the test that says so
accepts seventeen from one address and one from another.

Its id after a cut. A stream can now be cut while its client holds it.
Every request for that id names nothing, and no id is used twice, so not
the stream that took its place either: tested, with the place the cut
gave back going to the connection that waited.

The track's exit for the bound across addresses is the last test: peers
at four addresses bring sixteen connections each to a node with three
places. The track line and places.rs's header say what holds.

The listeners tests' peers have an address: a segment to an address no
peer is at fails the test, as in the streams tests.

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

The 27 mutations of stage E, in 31 runs, at 42a8fb25b

Regenerated after stage D's reviewed head (47231f0d7) was merged; these replace the patches of the comments above. e02, e04, e06 and e13 are rewritten for lines that moved, e20 to e23 are new, and e05, e06 and e18 are also run against the test of the places across peer addresses. Run by the orchestrator (orch/builds/ownstack-e-r3.log), each by build-request.sh's mutation: git apply --check, git apply, cargo test --no-run (exit 0 each), the one test by --exact name, git apply -R, tree clean.

mutation test that turned red exit
t1-an-orphan-that-ends-stays-counted an_orphan_is_counted_until_both_fins 101
t2-a-closed-connection-is-not-counted an_orphan_is_counted_until_both_fins 101
t3-a-close-in-syn-received-is-not-counted a_close_before_the_handshake_ends_leaves_an_orphan_until_it_gives_up 101
t4-ready-counts-handshakes-in-progress ready_counts_the_connections_accept_has_yet_to_return 101
o1-a-listener-is-one-connection a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up 101
o1-a-listener-is-one-connection a_connect_between_two_accepts_is_queued_not_reset 101
e01-a-pass-wakes-nobody a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 101
e02-an-accept-ends-in-no-pass a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 101
e03-an-owner-holds-one-wake-at-most a_connect_between_two_accepts_is_queued_not_reset 101
e04-only-an-accept-that-takes-spends-a-wake an_accept_spends_a_wake_whatever_it_answers 101
e19-a-drawn-port-reads-the-draws-high-half a_listen_on_a_taken_port_is_refused_and_a_drawn_port_listens 101
e05-a-wake-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e06-an-accept-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e07-a-finishing-connection-holds-no-place a_wake_is_owed_only_for_a_connection_there_is_a_place_for 101
e13-a-connect-ignores-places a_connect_past_the_places_is_refused_and_sends_nothing 101
e18-a-stream-holds-no-place a_connect_past_the_places_is_refused_and_sends_nothing 101
e14-a-listen-ignores-places a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 101
e15-a-listener-holds-no-place a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 101
e09-a-closed-connect-wakes-nobody closing_a_connect_gives_its_place_to_a_connection_that_waits 101
e10-a-stream-reset-for-its-pipe-wakes-nobody a_stream_reset_for_its_pipe_gives_its_place_to_a_connection_that_waits 101
e11-a-closed-listener-wakes-nobody closing_a_listener_gives_its_place_to_a_connection_that_waits_at_another 101
e12-more-places-wake-nobody more_places_wake_the_owner_of_a_connection_that_waits 101
e16-a-closed-listener-stays-in-the-stack closing_a_listener_resets_what_waits_and_frees_its_port 101
e17-a-refused-wake-is-ignored a_wake_the_owners_pipe_refuses_ends_the_listener 101
e20-an-accept-writes-its-stream-no-option a_stream_starts_with_the_options_its_listener_has_when_it_is_accepted 101
e21-an-accepted-stream-holds-no-option a_stream_starts_with_the_options_its_listener_has_when_it_is_accepted 101
e22-an-accepted-stream-is-counted-by-the-nodes-address an_accepted_stream_is_one_of_its_peers_addresss_sixteen 101
e23-an-id-is-used-twice a_request_for_a_stream_that_was_cut_names_nothing 101
e05-a-wake-ignores-places peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 101
e06-an-accept-ignores-places peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 101
e18-a-stream-holds-no-place peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 101
e01-a-pass-wakes-nobody
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..8d9618378 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -353,7 +353,6 @@ impl Node {
             *peers = peers.saturating_add(1);
         }
         live.retain(|id, stream| (connects && !stream.connecting) || stream.pass(*id, now, stack, events, &mut extended));
-        self.wake_owners(now);
     }
 
     /// The client lets go of the stream: nobody reads it, and what its pipe still holds is sent
e02-an-accept-ends-in-no-pass
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index b2e2e64ad..4f9914cc9 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -126,7 +126,6 @@ impl Node {
         listener.unspent = listener.unspent.saturating_sub(1);
         let (bound, options) = (listener.bound, listener.options);
         let answer = self.take(now, bound, options, pipes);
-        self.bridge(now);
         answer
     }
 
e03-an-owner-holds-one-wake-at-most
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..da391fd88 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -150,7 +150,7 @@ impl Node {
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
-            while listener.unspent < owed {
+            while listener.unspent < owed.min(1) {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
                 }
e04-only-an-accept-that-takes-spends-a-wake
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index b2e2e64ad..d9ea0f6b1 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -123,9 +123,11 @@ impl Node {
     /// and what it already received moves at once.
     pub fn accept(&mut self, now: Instant, id: ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return Err(AcceptRefused::NoListener) };
-        listener.unspent = listener.unspent.saturating_sub(1);
         let (bound, options) = (listener.bound, listener.options);
         let answer = self.take(now, bound, options, pipes);
+        if let (Ok(_), Some(listener)) = (&answer, self.listeners.live.get_mut(&id)) {
+            listener.unspent = listener.unspent.saturating_sub(1);
+        }
         self.bridge(now);
         answer
     }
e05-a-wake-ignores-places
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..efa6d38aa 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -149,7 +149,7 @@ impl Node {
         let room = self.room();
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
-            let owed = stack.tcp_ready(listener.bound).min(room);
+            let owed = stack.tcp_ready(listener.bound);
             while listener.unspent < owed {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
e06-an-accept-ignores-places
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index b2e2e64ad..e2cec69ba 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -132,9 +132,6 @@ impl Node {
 
     fn take(&mut self, now: Instant, bound: toyos_net_tcp::ListenerId, options: Options, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(pipes) = pipes else { return Err(AcceptRefused::NoPipes) };
-        if self.room() == 0 {
-            return Err(AcceptRefused::Full);
-        }
         let Some((conn, tuple)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
         let id = self.streams.accepted(now, &mut self.stack, conn, tuple.remote.addr, options, pipes);
e07-a-finishing-connection-holds-no-place
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..a9b8d34d1 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -28,7 +28,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners())
     }
 
     pub(crate) fn room(&self) -> usize {
e09-a-closed-connect-wakes-nobody
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..d7bd4849d 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -364,7 +364,6 @@ impl Node {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
             self.streams.events.push_back(StreamEvent::Closed { id });
-            self.wake_owners(now);
             return;
         }
         stream.to_client = None;
e10-a-stream-reset-for-its-pipe-wakes-nobody
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..0669d43a0 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -419,7 +419,6 @@ impl Node {
         if held {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
-            self.wake_owners(now);
         }
     }
 
e11-a-closed-listener-wakes-nobody
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..cf1255229 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -124,9 +124,6 @@ impl Node {
     /// says nobody holds the other end of the wake pipe. `false` is an id that names no listener.
     pub fn close_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let closed = self.end_listener(now, id);
-        if closed {
-            self.wake_owners(now);
-        }
         closed
     }
 
e12-more-places-wake-nobody
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..e66c87073 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -23,7 +23,6 @@ impl Node {
     /// How many places the node has from here on. Nothing held is let go for a smaller number.
     pub fn set_places(&mut self, now: Instant, places: usize) {
         self.places = places;
-        self.wake_owners(now);
     }
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
e13-a-connect-ignores-places
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 091d3fb23..4aec759a6 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -334,9 +334,6 @@ impl Node {
     /// An active open to `remote`, answered by a [`StreamEvent`] once the handshake ends or
     /// `timeout` passes. Refused, nothing was sent and the pipe ends are dropped.
     pub fn connect(&mut self, now: Instant, remote: Endpoint, timeout: Option<Duration>, pipes: Pipes) -> Result<StreamId, ConnectRefused> {
-        if self.room() == 0 {
-            return Err(ConnectRefused::Full);
-        }
         let conn = self.stack.tcp_connect(now, remote).map_err(ConnectRefused::Stack)?;
         let deadline = timeout.map(|within| now.after(within));
         // An active open has [tcp]'s defaults until `set_nodelay`.
e14-a-listen-ignores-places
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..b51dc693a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -85,9 +85,6 @@ impl Node {
     /// the interface holds or comes to hold. Answers the listener and its port. Refused, nothing
     /// was made and `owner` is dropped.
     pub fn listen(&mut self, port: Option<Port>, owner: Box<dyn Wake>, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> {
-        if self.room() == 0 {
-            return Err(ListenRefused::Full);
-        }
         let candidate = || {
             let [low, high, ..] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
e15-a-listener-holds-no-place
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..1f6665268 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -28,7 +28,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e16-a-closed-listener-stays-in-the-stack
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..341d5ee6a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -132,7 +132,6 @@ impl Node {
 
     fn end_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let Some(listener) = self.listeners.live.remove(&id) else { return false };
-        self.stack.tcp_close_listener(now, listener.bound);
         true
     }
 
e17-a-refused-wake-is-ignored
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..43e18cff3 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -151,8 +151,8 @@ impl Node {
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
             while listener.unspent < owed {
-                if let Err(refusal) = listener.owner.wake() {
-                    return Some((*id, refusal));
+                if listener.owner.wake().is_err() {
+                    break;
                 }
                 listener.unspent = listener.unspent.saturating_add(1);
             }
e18-a-stream-holds-no-place
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 33bae3276..5cd96add3 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -28,7 +28,7 @@ impl Node {
 
     /// The places taken: streams, listeners, and connections [tcp] is finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
+        self.listeners().saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e19-a-drawn-port-reads-the-draws-high-half
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..269f61d2b 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -89,7 +89,7 @@ impl Node {
             return Err(ListenRefused::Full);
         }
         let candidate = || {
-            let [low, high, ..] = draw().to_le_bytes();
+            let [.., low, high] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
         };
         let Some((bound, port)) = self.stack.tcp_listen(port, candidate) else { return Err(ListenRefused::InUse) };
e20-an-accept-writes-its-stream-no-option
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 091d3fb23..885586362 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -321,7 +321,6 @@ impl Streams {
     /// Holds a connection a listener's owner accepted, established, on the pipes its accept
     /// handed over, and writes it its listener's `options`.
     pub(crate) fn accepted(&mut self, now: Instant, stack: &mut Stack, conn: ConnId, remote: Ipv4Addr, options: Options, pipes: Pipes) -> StreamId {
-        stack.tcp_set_options(now, conn, options);
         self.hold(Stream::established(conn, remote, options, pipes))
     }
 
e21-an-accepted-stream-holds-no-option
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 091d3fb23..ee8071d91 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -322,7 +322,7 @@ impl Streams {
     /// handed over, and writes it its listener's `options`.
     pub(crate) fn accepted(&mut self, now: Instant, stack: &mut Stack, conn: ConnId, remote: Ipv4Addr, options: Options, pipes: Pipes) -> StreamId {
         stack.tcp_set_options(now, conn, options);
-        self.hold(Stream::established(conn, remote, options, pipes))
+        self.hold(Stream::established(conn, remote, Options::default(), pipes))
     }
 
     pub(crate) fn next_deadline(&self) -> Option<Instant> {
e22-an-accepted-stream-is-counted-by-the-nodes-address
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index b2e2e64ad..bd1df73cc 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -137,7 +137,7 @@ impl Node {
         }
         let Some((conn, tuple)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
-        let id = self.streams.accepted(now, &mut self.stack, conn, tuple.remote.addr, options, pipes);
+        let id = self.streams.accepted(now, &mut self.stack, conn, tuple.local.addr, options, pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })
     }
 
e23-an-id-is-used-twice
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 091d3fb23..dca817638 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -313,7 +313,6 @@ impl Streams {
     /// Holds `stream` under an id of its own.
     fn hold(&mut self, stream: Stream) -> StreamId {
         let id = StreamId(self.next);
-        self.next = self.next.saturating_add(1);
         self.live.insert(id, stream);
         id
     }
o1-a-listener-is-one-connection
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..d7fd6b6b5 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -749,7 +749,7 @@ impl Tcp {
             return;
         }
         let Some(listener) = value(&mut self.listeners, index) else { return };
-        if listener.pending.len() >= limits::LISTEN_PENDING || listener.ready.len() >= limits::LISTEN_READY {
+        if !listener.pending.is_empty() || !listener.ready.is_empty() {
             self.log.count(Counter::ListenOverflow);
             return;
         }
t1-an-orphan-that-ends-stays-counted
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..996fc0cd7 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -449,9 +449,6 @@ impl Tcp {
     fn free(&mut self, index: u32) {
         let Some(generation) = self.conns.get(usize::try_from(index).unwrap_or(usize::MAX)).map(|s| s.generation) else { return };
         let Some(conn) = release(&mut self.conns, &mut self.free_conns, index) else { return };
-        if conn.user == User::Orphan {
-            self.orphans = self.orphans.saturating_sub(1);
-        }
         let remote = conn.tuple.remote.addr;
         let parked = self.parked.get(&remote).is_some_and(|p| p.conns.contains(&index));
         // Its index may name another connection next.
t2-a-closed-connection-is-not-counted
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..9856e5858 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1085,7 +1085,6 @@ impl Tcp {
                 sync.shutdown_write(now);
                 sync.orphan(now);
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
         }
t3-a-close-in-syn-received-is-not-counted
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..65ba099a1 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1072,7 +1072,6 @@ impl Tcp {
             Tcb::SynRcvd(rcvd) => {
                 rcvd.shutdown_write();
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
             Tcb::Sync(sync) if sync.rx.unread() > 0 => {
t4-ready-counts-handshakes-in-progress
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..178b9d5a8 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -585,7 +585,7 @@ impl Tcp {
 
     /// How many children completed their handshake and wait for [`Self::accept`].
     pub fn ready(&mut self, id: ListenerId) -> Result<usize, Error> {
-        slot(&mut self.listeners, id.index, id.generation).map(|l| l.ready.len()).ok_or(Error::NoSuchSocket)
+        slot(&mut self.listeners, id.index, id.generation).map(|l| l.pending.len()).ok_or(Error::NoSuchSocket)
     }
 
     /// The oldest child that completed its handshake.

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review of wt/toyos-ownstack-e at 42a8fb25b, round 1: git diff origin/wt/toyos-ownstack-d...42a8fb25b (stage D at 47231f0d7) and the hunks of fa6f350ff written by hand. Read, not run.

Net lines of the stage: 13 files, +1,404, -37. Production +380, -30 (listeners.rs 198, places.rs 39, streams.rs +57 -24, lease/tcp.rs +36 -3, lib.rs +13 -3, toyos-net-tcp 18, the shard 19); tests +1,017, -3 (tests/listeners.rs 944, held.rs 66, common/mod.rs +5 -1, tests/streams.rs +2 -2); the track and the manifest +7, -4. The growth is accepted for the listeners and the bound; the listener's option is not (BLOCKER 3).

BLOCKER

  1. Evidence — cargo run -- --ci host has run at no head of this branch — the pull request is a draft and host, toolchain and guest conclude skipping; request 3 (orch/builds/ownstack-e-r3.log, HEAD=42a8fb25b…, BUILD REQUEST EXIT=0) is the three crates' tests, four gates and this machine's clippy, not the host job, and clippy on CI's toolchain has not run.
  2. userland/netstack/node/src/listeners.rs:95, userland/netstack/node/src/lease/tcp.rs:75 — Node::listen takes no address and the forward passes Ipv4Addr::UNSPECIFIED — the pipe ABI's request carries the address a program bound (TcpBindPipedRequest.addr, toyos/src/net.rs:158), [tcp] takes one and prefers it (Tcp::listen(addr, …), s_ls_009_the_specific_address_first), and Node::udp_bind(addr, …) on main (toyos-net-node: clients' datagram sockets and the machine's mDNS name on ToyOS's own UDP, host-tested and shipped in nothing #772) takes one and refuses one that is not the machine's; a program that binds one address is answered on every address the interface holds, which is today's netstack's silent widening carried into new code, recorded in no issue, and the body lists it under "Unchanged". Remove it: the address reaches [tcp], and the body says what a named address the interface does not hold gets (refused as udp_bind refuses it, or a listener that waits for it). The patch that must turn a new test red: lease/tcp.rs:75 with Ipv4Addr::UNSPECIFIED in place of the address, against a test in which a listener bound to an address other than 192.0.2.1 leaves a SYN to 192.0.2.1 at its port answered <SEQ=0><ACK=SEG.SEQ+1><CTL=RST,ACK> (RFC 9293 §3.10.7.1) with no wake.
  3. userland/netstack/node/src/listeners.rs:110-120, userland/netstack/node/src/streams.rs:323-326 — Node::set_listener_nodelay, listener_nodelay and the write at the accept are built on a claim the body marks "From memory, not measured", and the body names the measurement that settles it — a branch that built on a guess where one cheap measurement would have told it goes back to measure. Ruling, under "The listener's option" below: measure and keep, or delete all three and the test.
  4. userland/netstack/node/src/listeners.rs:165 — while self.wake_each(now) {} → self.wake_each(now); passes every test — the rule its comment states (a listener ended for a refused wake gives its place back, and every listener after it in the round is still owed its wakes in this pass) has no test: a_wake_the_owners_pipe_refuses_ends_the_listener has one listener. The test it must turn red: three places, listeners at 22 and 23 in that order, two handshakes at 23 (its owner holds one wake, for the one place), 22's owner's pipe set to refuse, then one handshake at 22 delivered by node.receive alone; before any transmit, 23's owner holds two wakes and drain_refused_listeners names 22. Asserted after pump it cannot fail: Node::transmit ends in a pass that writes the missing wake.

NOTE

  • toyos-net-shard/tcp/src/stack.rs:218-220 — orphans was inserted between eligible and its doc comment — orphans now reads "Connections that became eligible for the caller's round…" and eligible has none.
  • issues/toyos-has-its-own-network-stack.md:27 — "Exit: that issue's" — the exit of issues/netstack-lookup-slots-have-no-per-client-share.md is a test of lookups; nothing it names reads the places. The line names its own: one program at its share leaves another's connect, listen and accept answered.
  • userland/netstack/node/tests/listeners.rs:253-290 — the outside reading of an emitted segment (source MAC, IPv4 checksum and length, TCP checksum) is written a second time beside tests/streams.rs:277-323 — it is the oracle both files lean on; one declaration in tests/common/mod.rs is read by both.
  • If BLOCKER 3 ends in "keep": the track says Tcp::set_listener_options (LS-10) has no caller in anything that will ship, with an exit (the node calls it, or the specification and [tcp] drop it); and an issue records that std seeds an accepted TcpStream with nodelay: false and answers nodelay() from that (sdk/std/sys/net/connection.rs:146, :330), which a listener's option makes false.
  • The body, "What a peer can make the node hold" — it gives the per-listener numbers and not their product: 128 waiting connections of 65,535 bytes each is 8.4 MB a listener that no place counts, and a listener is one place.

Asked in the brief

What a peer can make the node hold. Checked against the code; the body's account stands.

  • Half-open: stack.rs:752 drops a SYN at 256 pending or 128 ready and counts tcp.listen-overflow; a pending child is a SynRcvd with no ring; it is given up at SYNACK_GIVE_UP (60 s) with nothing sent, and its late ACK meets LISTEN's <SEQ=SEG.ACK><CTL=RST> (§3.10.7.2, second check; asserted at tests/listeners.rs:505-508). Memory is 256 control blocks a listener and CPU 256 SYN-ACK timers; only the port is unavailable. RFC 9293 has no rule for a full backlog; a dropped SYN is right and a reset would not be, since it reads as a closed port. "256 SYNs a minute" is 256 over 60 s. Recorded at track :28 with an exit a test can fail. It is a weakness, known, tracked and still true; it is less than what ships, where one SYN shuts the port.
  • Waiting for an accept: 128 a listener; a final ACK past them leaves its child in SYN-RECEIVED (stack.rs:843-849, tcp.accept-queue-full). A ring takes its storage at the first write (ring.rs), so a waiting connection holds 65,535 bytes only once its peer sends text, and no send buffer. Nothing times a waiting connection: it waits until it is accepted, its peer resets it or its listener closes, as a host's accept queue does. The node keeps no queue of its own.
  • Places: held is streams, listeners and orphans. Both writes of User::Orphan count (stack.rs:1074-1075, :1087-1088), the one decrement is in free, and every orphan ends through end → free; a close with text unread resets and counts nothing. peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them holds what its name says, and e05, e06 and e18 are red on it at :877, :887 and :887 in the step logs.
  • Finishing alone: an orphan keeps its place for as long as its peer acknowledges within 60 s; track :27 and the line on a client gone with nothing left in its pipe record it, the second with an exit.
  • What the peer sees: a SYN past the queues, nothing; a handshake given up, nothing, then LISTEN's reset for its late ACK; a SYN for no listener, <SEQ=0><ACK=SEG.SEQ+1><CTL=RST,ACK> (§3.10.7.1); a listener closed or ended under it, <SEQ=SND.NXT><CTL=RST> (§3.10.5); a node with no place, an established connection nobody is woken for. I read each rule in RFC 9293 and each assertion matches it.
  • No panic from the wire that I can construct: the new unreachable!s (lease/tcp.rs:13-18, :77) are on ids the node holds. A listener's [tcp] id is released only by end_listener, after it left live; Tcp::accept returns only a child free has not taken out of ready, synchronized, and makes it Held before tuple and set_options ask; Tcp::listen refuses nothing but AddrInUse.

The wake accounting. unspent rises only in wake_each and only to min(ready, room); it falls only by an accept, whatever it answers. By my reading no wake is lost or written twice across a pass (the pass is idempotent on unspent), a place freeing (every site that gives one back ends in wake_owners: pass, a closed connect, pipe_broken, close_listener, set_places; e01, e09 to e12 are red on the assertion in the step logs), or a listener closing with connections queued (its count goes with it, [tcp] resets what waited, the owner reads the end). Two surpluses, both harmless: room is read once a round, so N listeners are each woken for the same place and all but one accept is answered Full once; and a wake for a connection its peer reset stands for the next. The one loss is the owner that reads a wake and sends no accept. It is no defect of the node's: the node is better than what ships (the next arrival raises ready above unspent and is announced, where today the listener is stranded for the boot), the cause is std's accept, and issues/an-accept-that-never-reaches-netstack-strands-its-listener.md has an owner and an exit a test fails. Track :29 says so truly.

The listener's option.

  • POSIX says nothing of an accepted socket taking its listener's TCP_NODELAY. That Linux and macOS take the option on a listening socket and copy it to the child when its handshake ends is my memory as it is the implementer's; nobody measured it.
  • A portable Rust program cannot ask: std's TcpListener has no set_nodelay, nor has tokio's. The askers are libc's setsockopt (userland/libc/src/socket.rs:553 sends a listener's id to netstack, which answers ERR_NOT_CONNECTED today) and crates that reach the fd through it. No program in the image does, so no guest test would show it.
  • It is not dead if the move maps TcpSetOption and TcpGetOption on a listener's id onto it, and dead otherwise.
  • Why not [tcp]'s inheritance: the implementer's reason holds. For nodelay the write at the accept is indistinguishable on the wire from a host's copy at the handshake's end, since no text leaves before the accept; using LS-10 needs a read-back call, a larger addition to [tcp] than the one write. The node's copy is the kind Stream::options already is.
  • The seed did not need it: the node gives [tcp]'s listener no option (set_listener_options has one caller, s_ls_010), so an accepted connection has Options::default() by construction, and stage D's review asked no more than that the seed be true.
  • Ruling: measure on macOS here and on Linux where one is at hand, quote command, exit and output in the body, and keep the calls with the two NOTEs above; or delete set_listener_nodelay, listener_nodelay, Listener::options, the options parameters and the write in Streams::accepted, the test and e20/e21, and file today's refusal as an issue whose exit is that measurement.

The oracle and the controls. etherparse reads every emitted segment and builds every received one; the RFC 9293 numbers asserted are the RFC's. The peers are the tests' own, recorded in the track with its exit. 31 runs of 27 patches at the head: I read where each of the 31 panicked in its step log, and every one is 101 with 0 passed; 1 failed on a line of its named test, none on a build; o1 against a_connect_between_two_accepts_is_queued_not_reset is red at the helper's expect (:333, the SYN-ACK that never came) and not at the test's no-reset assertion, since the patch drops the second SYN where the replaced stack reset it. Arm by arm in listeners.rs and places.rs the one rule with no red is BLOCKER 4; a_wake_left_by_a_connection_its_peer_reset_stands_for_the_next guards an absence and has no line to remove. There is no whole-change revert: the tests do not build without the source, which the body says. The keep-alive assumption holds: Sync::ack (conn.rs:603-609) accepts an acknowledgment up to SND.UNA plus the flight, hand_off never moves SND.NXT back, and Net::takes acknowledges no more than the peer has heard; a refused one would leave gave_up false, the sixteen cut with the seventeenth and the outbox at :899 unmoved, so the tests fail on the assumption being wrong.

The hand-resolved hunks. Node::connect (streams.rs:336-344): the place before tcp_connect, nothing held on either refusal, connecting and the deadline over Stream::established; stage D's 34 tests are green on it in request 3. Stream::established: no field of stage D's is missing and reader_left is gone. The track: stage D's "no bound on its streams" line is rightly dropped, its three rewritten lines are the ones kept, and each new line has an exit.

CI's clippy. No clone left unused (owner.clone(), refused.clone(), second.clone(), whole.clone() are each read after); no instant subtracted; no remainder, no bit trick; drain(..) is returned, not collected; Net::to is not a to_ method; no let returned directly (accept and close_listener have a statement between); no child process, no function cast. Streams::accepted has seven parameters, the most too_many_arguments allows. I expect none of the nine to red.

What the branch must show next

After the fix round for BLOCKERs 2 to 4, which is new code and comes back here, #775 merges with main and this branch merges that.

  • A hand resolution in any of these is new code and comes back for a round: userland/netstack/node/src/lib.rs (the module list, the re-exports, Node's fields and new, and above all where receive, transmit and fire end in a pass), src/streams.rs, src/listeners.rs, src/places.rs, src/lease.rs, src/lease/tcp.rs, tests/common/mod.rs, tests/listeners.rs, tests/streams.rs, toyos-net-shard/src/lib.rs, toyos-net-shard/tcp/src/stack.rs, toyos-net-shard/tcp/tests/held.rs. The manifest's description and the track will stop the merge; they are records, and the orchestrator reads them.
  • One thing the merge makes false without stopping: places.rs calls itself "the one bound on what clients make the node hold" and the track says "the bound on both", while main's datagram sockets hold no place. Either a datagram socket holds one, which is code and a round, or both sentences say what is not bounded.
  • The log: the host job of the head that enters the queue, concluded success and read whole, showing cargo run -- --ci host exit 0 and in it toyos-net-node's listeners target running 22 tests plus this round's new ones, each named test ok (the two BLOCKER tests, peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them, a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up, a_connect_between_two_accepts_is_queued_not_reset); streams 34, lease 18, slirp 3, and main's datagram and name targets at the counts main's own host log has; toyos-net-tcp's held 5 and take 5; every toyos-net-shard target that has tests above 0; the hostws::, userlandhost::, sourcegate:: and licence:: tests run and ok; clippy on the three crates under CI's toolchain with no warning. guest / suite concludes success and is not skipped; no guest test reaches the change.
  • On that log, with no hand resolution in the files above, the orchestrator may close BLOCKER 1 and land without another round. Not at 42a8fb25b.

Before the move may be dispatched

From this review:

  • This stage and the resolver's are on main; a listen carries its address; the listener's option is measured and mapped, or gone and filed.
  • The shell's half of each contract is in the move's brief: set_places before the first request, from a number that prices a listener at its 128 waiting receive buffers; close_listener when the kernel says the wake pipe's reader left, since the node no longer probes an owner by writing, and a guest test in which a dead owner's port is free with no peer connecting; drain_refused_listeners every turn; one byte a Wake::wake; ListenRefused, AcceptRefused and ConnectRefused::Full each mapped onto one of the pipe ABI's codes, said in the body; a request that names a stream after its Cut.
  • issues/netstack-datagram-sockets-and-listeners-have-no-bound.md: its listener half is met on the node; its datagram half is not, and the move must not carry it unrecorded.

From the track's "does not yet meet" lines, those whose exit names the move or stage 5:

  • "No second TCP": the move's guest run against the host kernel's TCP through slirp is that exit, and the two listener scenarios run there on both arms.
  • The pass over every stream: the T14 measurement at 1, 100 and 1,000 idle streams is owed at the move, and the change lands with it if the threshold is passed.
  • The departed client's tail: the move closes issues/netstack-cuts-a-departed-clients-unsent-tail-at-the-ceiling.md with its bench row.
  • A refusal [udp] logs against a client's call: its exit is "at the move and not after it".
  • Stage 3's: the scenario for the mapping of UDP's refusals and dhcp.renew-unroutable are owed by stage 5, and the captured exchanges from slirp and the T14 replay at stage 5.
  • Carried and still true after the move, each with its line: the SYN flood, the places as one number, the unspent wake, the orphan's idle bound, the rate floor, the link-down DHCP timers. None blocks the dispatch; each is said in the move's body as a present weakness.

SEND BACK

Japabu and others added 2 commits October 8, 2026 21:58
Stage D's head carries origin/main at 35d3585 (#772): the node's
datagram sockets and its mDNS name.

Resolved by hand:

- userland/netstack/node/src/lib.rs, the module list: both sides whole,
  in order: datagram, lease, listeners, name, places, streams. Every
  other hunk of the file merged by itself: receive and fire still end in
  bridge after settle, transmit in pass(now, true), and next_deadline is
  stage D's four terms.
- userland/netstack/node/Cargo.toml, description: names what both sides
  put in the package, and says the bound is on streams and listeners.
- the track, the stage 5 paragraph: in the tree are the node, its lease,
  the datagram sockets, the name, streams, listeners and the bound on
  streams and listeners; still to build are the resolver and the move.
- the track, "What the node does not yet meet": stage E's line on
  Tcp::ready and Tcp::orphans and stage D's six lines from main, all
  kept, none reworded.

The node's tests at this tree: exit 0 (datagram 5, lease 18, listeners
22, name 13, slirp 3, streams 34).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…tion is the host's measured rule, a datagram socket holds a place

BLOCKER 2. Node::listen takes the address the program bound and hands it
to [tcp]. One the interface does not hold usable is refused
(ListenRefused::NotLocal) in Shard::listen by [ip]'s is_assigned, the
predicate [udp]'s bind refuses by, so both socket kinds have one rule.

BLOCKER 3. Measured on macOS, by libc over loopback and by
tests/host.rs through socket2: TCP_NODELAY set on a listening socket
before a connection began is on the accepted socket; set after the
connection was established and before the accept, it is not. The node
gave it to both. It now does what the host does by [tcp]'s own
inheritance (LS-10): set_listener_nodelay writes [tcp]'s listener, a
connection takes the options at its SYN, and the accept seeds the stream
with what Tcp::options, a new read, says the connection has. The write
at the accept and its parameters are gone. Linux is not read: the host
check's first run of tests/host.rs is that reading, and the new issue
says so.

BLOCKER 4. The loop over wake_each has its test: a listener ended for a
refused wake gives its place to another in the same pass, asserted
before any transmit opportunity.

A datagram socket holds a place from its bind to its close, and a bind
with none left is refused ResourceExhausted with nothing made. A
connection that waits to be accepted holds none: places.rs and the
track say why, and what the shell must price a listener at.

NOTEs: the doc comment `orphans` took from `eligible` is back; the
places line of the track names its own exit; the outside reading of a
TCP segment is declared once, in tests/common, and read by both test
files; issues/netstack-datagram-sockets-and-listeners-have-no-bound.md
says what is left.

socket2 is a new dev-dependency of toyos-net-node, already in the lock
through the workspace's patch: it sets and reads a socket option std has
no call for on a listener, on every host, with no unsafe.

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

The 35 mutations of stage E, in 39 runs, at 643584d4b, and the host program of BLOCKER 3

These replace the patches of the comments above. Regenerated for lines that moved: e02, e04, e06, e07, e12, e14, e15, e18, e19, e21, e22. New: t5, e20 (rewritten for the rule as measured), e24 to e30. Run by me in the worktree (orch/ownstack/e/mutations4.sh, log logs4/mutations.log, script exit 0): git apply --check, git apply, cargo test --no-run (exit 0 each), the one test by --exact name (running 1 test), git apply -R, tree clean after each.

mutation test that turned red build test
t1-an-orphan-that-ends-stays-counted an_orphan_is_counted_until_both_fins 0 101
t2-a-closed-connection-is-not-counted an_orphan_is_counted_until_both_fins 0 101
t3-a-close-in-syn-received-is-not-counted a_close_before_the_handshake_ends_leaves_an_orphan_until_it_gives_up 0 101
t4-ready-counts-handshakes-in-progress ready_counts_the_connections_accept_has_yet_to_return 0 101
t5-options-are-the-defaults options_are_the_ones_the_connection_has 0 101
o1-a-listener-is-one-connection a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up 0 101
o1-a-listener-is-one-connection a_connect_between_two_accepts_is_queued_not_reset 0 101
e01-a-pass-wakes-nobody a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 0 101
e02-an-accept-ends-in-no-pass a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends 0 101
e03-an-owner-holds-one-wake-at-most a_connect_between_two_accepts_is_queued_not_reset 0 101
e04-only-an-accept-that-takes-spends-a-wake an_accept_spends_a_wake_whatever_it_answers 0 101
e19-a-drawn-port-reads-the-draws-high-half a_listen_on_a_taken_port_is_refused_and_a_drawn_port_listens 0 101
e05-a-wake-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 0 101
e06-an-accept-ignores-places a_wake_is_owed_only_for_a_connection_there_is_a_place_for 0 101
e07-a-finishing-connection-holds-no-place a_wake_is_owed_only_for_a_connection_there_is_a_place_for 0 101
e13-a-connect-ignores-places a_connect_past_the_places_is_refused_and_sends_nothing 0 101
e18-a-stream-holds-no-place a_connect_past_the_places_is_refused_and_sends_nothing 0 101
e14-a-listen-ignores-places a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 0 101
e15-a-listener-holds-no-place a_listener_holds_a_place_and_a_listen_without_one_makes_nothing 0 101
e09-a-closed-connect-wakes-nobody closing_a_connect_gives_its_place_to_a_connection_that_waits 0 101
e10-a-stream-reset-for-its-pipe-wakes-nobody a_stream_reset_for_its_pipe_gives_its_place_to_a_connection_that_waits 0 101
e11-a-closed-listener-wakes-nobody closing_a_listener_gives_its_place_to_a_connection_that_waits_at_another 0 101
e12-more-places-wake-nobody more_places_wake_the_owner_of_a_connection_that_waits 0 101
e16-a-closed-listener-stays-in-the-stack closing_a_listener_resets_what_waits_and_frees_its_port 0 101
e17-a-refused-wake-is-ignored a_wake_the_owners_pipe_refuses_ends_the_listener 0 101
e20-a-listeners-option-does-not-reach-tcp a_stream_starts_with_the_options_its_connection_took_from_its_listener 0 101
e21-an-accepted-stream-holds-no-option a_stream_starts_with_the_options_its_connection_took_from_its_listener 0 101
e22-an-accepted-stream-is-counted-by-the-nodes-address an_accepted_stream_is_one_of_its_peers_addresss_sixteen 0 101
e23-an-id-is-used-twice a_request_for_a_stream_that_was_cut_names_nothing 0 101
e05-a-wake-ignores-places peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 0 101
e06-an-accept-ignores-places peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 0 101
e18-a-stream-holds-no-place peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them 0 101
e24-a-bind-ignores-places a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101
e25-a-datagram-socket-holds-no-place a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101
e26-a-closed-socket-wakes-nobody a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101
e27-a-closed-socket-keeps-its-place a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing 0 101
e28-a-listen-is-at-every-address a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds 0 101
e30-a-listen-takes-an-address-nobody-holds a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds 0 101
e29-a-listener-ended-in-a-pass-ends-the-pass a_listener_ended_for_its_wake_gives_its_place_to_another_in_the_same_pass 0 101

The host program

Cargo.toml names libc = "0.2" and an empty [workspace]; cargo run --offline on macOS 27, arm64, exit 0.

src/main.rs
//! What this host's TCP gives an accepted socket of its listener's TCP_NODELAY: set on the
//! listening socket before the connection began, and after it was established but before the
//! accept. Loopback; libc's setsockopt and getsockopt on the raw descriptors.

use std::net::{Ipv4Addr, TcpListener, TcpStream};
use std::os::fd::AsRawFd;

fn set(fd: i32, on: bool) {
    let value: libc::c_int = on.into();
    let done = unsafe { libc::setsockopt(fd, libc::IPPROTO_TCP, libc::TCP_NODELAY, (&raw const value).cast(), size_of::<libc::c_int>() as libc::socklen_t) };
    assert_eq!(done, 0, "setsockopt: {}", std::io::Error::last_os_error());
}

fn get(fd: i32) -> bool {
    let mut value: libc::c_int = -1;
    let mut len = size_of::<libc::c_int>() as libc::socklen_t;
    let done = unsafe { libc::getsockopt(fd, libc::IPPROTO_TCP, libc::TCP_NODELAY, (&raw mut value).cast(), &raw mut len) };
    assert_eq!(done, 0, "getsockopt: {}", std::io::Error::last_os_error());
    value != 0
}

fn main() {
    println!("{} {}", std::env::consts::OS, std::env::consts::ARCH);
    for before in [true, false] {
        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
        println!("listener at first: {}", get(listener.as_raw_fd()));
        if before {
            set(listener.as_raw_fd(), true);
        }
        // A connect that returned is a handshake that ended: the connection waits in the queue.
        let _peer = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
        if !before {
            set(listener.as_raw_fd(), true);
        }
        let (accepted, _) = listener.accept().unwrap();
        let when = if before { "before the connection began" } else { "after it was established, before the accept" };
        println!("set on the listener {when}: listener {}, accepted {}", get(listener.as_raw_fd()), get(accepted.as_raw_fd()));
    }
}
output
     Locking 1 package to latest compatible version
   Compiling libc v0.2.190
   Compiling listener-nodelay v0.0.0 (the scratch directory)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.51s
     Running `target/debug/listener-nodelay`
macos aarch64
listener at first: false
set on the listener before the connection began: listener true, accepted true
listener at first: false
set on the listener after it was established, before the accept: listener true, accepted false

The patches

e01-a-pass-wakes-nobody
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..8d9618378 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -353,7 +353,6 @@ impl Node {
             *peers = peers.saturating_add(1);
         }
         live.retain(|id, stream| (connects && !stream.connecting) || stream.pass(*id, now, stack, events, &mut extended));
-        self.wake_owners(now);
     }
 
     /// The client lets go of the stream: nobody reads it, and what its pipe still holds is sent
e02-an-accept-ends-in-no-pass
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..cdfd85e39 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -138,7 +138,6 @@ impl Node {
         listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
-        self.bridge(now);
         answer
     }
 
e03-an-owner-holds-one-wake-at-most
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..da391fd88 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -150,7 +150,7 @@ impl Node {
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
-            while listener.unspent < owed {
+            while listener.unspent < owed.min(1) {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
                 }
e04-only-an-accept-that-takes-spends-a-wake
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..f0a53ca2b 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -135,9 +135,11 @@ impl Node {
     /// and what it already received moves at once.
     pub fn accept(&mut self, now: Instant, id: ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return Err(AcceptRefused::NoListener) };
-        listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
+        if let (Ok(_), Some(listener)) = (&answer, self.listeners.live.get_mut(&id)) {
+            listener.unspent = listener.unspent.saturating_sub(1);
+        }
         self.bridge(now);
         answer
     }
e05-a-wake-ignores-places
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..efa6d38aa 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -149,7 +149,7 @@ impl Node {
         let room = self.room();
         let stack = &mut self.stack;
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
-            let owed = stack.tcp_ready(listener.bound).min(room);
+            let owed = stack.tcp_ready(listener.bound);
             while listener.unspent < owed {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
e06-an-accept-ignores-places
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..a5f7a1aad 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -144,9 +144,6 @@ impl Node {
 
     fn take(&mut self, bound: toyos_net_tcp::ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(pipes) = pipes else { return Err(AcceptRefused::NoPipes) };
-        if self.room() == 0 {
-            return Err(AcceptRefused::Full);
-        }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
         let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
e07-a-finishing-connection-holds-no-place
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 1113a98ea..4cbd0412c 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -38,7 +38,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets)
     }
 
     pub(crate) fn room(&self) -> usize {
e09-a-closed-connect-wakes-nobody
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..d7bd4849d 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -364,7 +364,6 @@ impl Node {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
             self.streams.events.push_back(StreamEvent::Closed { id });
-            self.wake_owners(now);
             return;
         }
         stream.to_client = None;
e10-a-stream-reset-for-its-pipe-wakes-nobody
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 1346df63c..0669d43a0 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -419,7 +419,6 @@ impl Node {
         if held {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
-            self.wake_owners(now);
         }
     }
 
e11-a-closed-listener-wakes-nobody
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..cf1255229 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -124,9 +124,6 @@ impl Node {
     /// says nobody holds the other end of the wake pipe. `false` is an id that names no listener.
     pub fn close_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let closed = self.end_listener(now, id);
-        if closed {
-            self.wake_owners(now);
-        }
         closed
     }
 
e12-more-places-wake-nobody
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 1113a98ea..91efad0f2 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -32,7 +32,6 @@ impl Node {
     /// How many places the node has from here on. Nothing held is let go for a smaller number.
     pub fn set_places(&mut self, now: Instant, places: usize) {
         self.places = places;
-        self.wake_owners(now);
     }
 
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
e13-a-connect-ignores-places
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 091d3fb23..4aec759a6 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -334,9 +334,6 @@ impl Node {
     /// An active open to `remote`, answered by a [`StreamEvent`] once the handshake ends or
     /// `timeout` passes. Refused, nothing was sent and the pipe ends are dropped.
     pub fn connect(&mut self, now: Instant, remote: Endpoint, timeout: Option<Duration>, pipes: Pipes) -> Result<StreamId, ConnectRefused> {
-        if self.room() == 0 {
-            return Err(ConnectRefused::Full);
-        }
         let conn = self.stack.tcp_connect(now, remote).map_err(ConnectRefused::Stack)?;
         let deadline = timeout.map(|within| now.after(within));
         // An active open has [tcp]'s defaults until `set_nodelay`.
e14-a-listen-ignores-places
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..43605e498 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -98,9 +98,6 @@ impl Node {
     /// hold, on `port` or on a port chosen from `draw`'s candidates. Answers the listener and
     /// its port. Refused, nothing was made and `owner` is dropped.
     pub fn listen(&mut self, addr: Ipv4Addr, port: Option<Port>, owner: Box<dyn Wake>, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> {
-        if self.room() == 0 {
-            return Err(ListenRefused::Full);
-        }
         let candidate = || {
             let [low, high, ..] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
e15-a-listener-holds-no-place
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 1113a98ea..ac49d8fcc 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -38,7 +38,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e16-a-closed-listener-stays-in-the-stack
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..341d5ee6a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -132,7 +132,6 @@ impl Node {
 
     fn end_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let Some(listener) = self.listeners.live.remove(&id) else { return false };
-        self.stack.tcp_close_listener(now, listener.bound);
         true
     }
 
e17-a-refused-wake-is-ignored
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 0df6d315e..43e18cff3 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -151,8 +151,8 @@ impl Node {
         let refused = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
             while listener.unspent < owed {
-                if let Err(refusal) = listener.owner.wake() {
-                    return Some((*id, refusal));
+                if listener.owner.wake().is_err() {
+                    break;
                 }
                 listener.unspent = listener.unspent.saturating_add(1);
             }
e18-a-stream-holds-no-place
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 1113a98ea..b4f3ae458 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -38,7 +38,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.listeners().saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e19-a-drawn-port-reads-the-draws-high-half
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..b94db2aeb 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -102,7 +102,7 @@ impl Node {
             return Err(ListenRefused::Full);
         }
         let candidate = || {
-            let [low, high, ..] = draw().to_le_bytes();
+            let [.., low, high] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
         };
         let (bound, port) = match self.stack.tcp_listen(addr, port, candidate) {
e20-a-listeners-option-does-not-reach-tcp
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..e53562cad 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -123,7 +123,6 @@ impl Node {
     pub fn set_listener_nodelay(&mut self, id: ListenerId, nodelay: bool) -> bool {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return false };
         listener.options.nodelay = nodelay;
-        self.stack.tcp_set_listener_options(listener.bound, listener.options);
         true
     }
 
e21-an-accepted-stream-holds-no-option
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..952672c8e 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -149,7 +149,7 @@ impl Node {
         }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
-        let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
+        let id = self.streams.accepted(conn, tuple.remote.addr, Options::default(), pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })
     }
 
e22-an-accepted-stream-is-counted-by-the-nodes-address
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..1ab57411a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -149,7 +149,7 @@ impl Node {
         }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
-        let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
+        let id = self.streams.accepted(conn, tuple.local.addr, options, pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })
     }
 
e23-an-id-is-used-twice
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 091d3fb23..dca817638 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -313,7 +313,6 @@ impl Streams {
     /// Holds `stream` under an id of its own.
     fn hold(&mut self, stream: Stream) -> StreamId {
         let id = StreamId(self.next);
-        self.next = self.next.saturating_add(1);
         self.live.insert(id, stream);
         id
     }
e24-a-bind-ignores-places
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..2b19d06a1 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -103,9 +103,6 @@ impl Node {
     /// or, with none named, an ephemeral one, which spends the one draw. Returns the socket and
     /// the port it holds.
     pub fn udp_bind(&mut self, addr: Ipv4Addr, port: Option<Port>, draw: impl FnOnce() -> u32) -> Result<(DatagramId, Port), Refused> {
-        if self.room() == 0 {
-            return Err(Refused::ResourceExhausted);
-        }
         let (id, port) = self.stack.bind(addr, port, draw).map_err(refused)?;
         self.sockets = self.sockets.saturating_add(1);
         Ok((DatagramId(id), port))
e25-a-datagram-socket-holds-no-place
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index 1113a98ea..a82719c29 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -38,7 +38,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e26-a-closed-socket-wakes-nobody
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..bb81727af 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -128,7 +128,6 @@ impl Node {
     pub fn udp_close(&mut self, now: Instant, id: DatagramId) -> Result<(), Refused> {
         self.stack.close(now, id.0).map_err(refused)?;
         self.sockets = self.sockets.saturating_sub(1);
-        self.wake_owners(now);
         Ok(())
     }
 }
e27-a-closed-socket-keeps-its-place
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..4606c036b 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -127,7 +127,6 @@ impl Node {
     /// accepted still leaves, to [udp]'s bound. Its place is back.
     pub fn udp_close(&mut self, now: Instant, id: DatagramId) -> Result<(), Refused> {
         self.stack.close(now, id.0).map_err(refused)?;
-        self.sockets = self.sockets.saturating_sub(1);
         self.wake_owners(now);
         Ok(())
     }
e28-a-listen-is-at-every-address
diff --git a/userland/netstack/node/src/lease/tcp.rs b/userland/netstack/node/src/lease/tcp.rs
index bd9bcbd73..d6e04ad2b 100644
--- a/userland/netstack/node/src/lease/tcp.rs
+++ b/userland/netstack/node/src/lease/tcp.rs
@@ -72,7 +72,7 @@ impl Stack {
     /// A passive open at `addr`, 0.0.0.0 meaning every address the interface holds or comes to
     /// hold, and at `port` or at one of `random`'s candidates.
     pub(crate) fn tcp_listen(&mut self, addr: Ipv4Addr, port: Option<Port>, random: impl FnMut() -> u16) -> Result<(ListenerId, Port), ListenError> {
-        let id = self.shard.listen(addr, port, random)?;
+        let id = self.shard.listen(Ipv4Addr::UNSPECIFIED, port, random)?;
         Ok((id, held(self.shard.listener_port(id))))
     }
 
e29-a-listener-ended-in-a-pass-ends-the-pass
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 6d27272ac..0149aea23 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -174,7 +174,7 @@ impl Node {
     pub(crate) fn wake_owners(&mut self, now: Instant) {
         // A listener ended here gives its place back, which another's owner may be owed a wake
         // for.
-        while self.wake_each(now) {}
+        self.wake_each(now);
     }
 
     /// One round over the listeners, up to the first whose pipe refuses a wake; `true` ended it.
e30-a-listen-takes-an-address-nobody-holds
diff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index d7ee5fb50..fd8e2d469 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -479,9 +479,6 @@ impl Shard {
     /// `addr` is the local address to listen on, UNSPECIFIED for any, and otherwise one [ip]
     /// holds assigned or announcing, as a datagram socket's is; port 0 takes `random`'s draws.
     pub fn listen(&mut self, addr: Ipv4Addr, port: Option<Port>, random: impl FnMut() -> u16) -> Result<ListenerId, ListenError> {
-        if !addr.is_unspecified() && !self.ip.is_assigned(addr) {
-            return Err(ListenError::NotLocal);
-        }
         self.tcp.listen(addr, port, random).map_err(ListenError::Tcp)
     }
 
o1-a-listener-is-one-connection
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..d7fd6b6b5 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -749,7 +749,7 @@ impl Tcp {
             return;
         }
         let Some(listener) = value(&mut self.listeners, index) else { return };
-        if listener.pending.len() >= limits::LISTEN_PENDING || listener.ready.len() >= limits::LISTEN_READY {
+        if !listener.pending.is_empty() || !listener.ready.is_empty() {
             self.log.count(Counter::ListenOverflow);
             return;
         }
t1-an-orphan-that-ends-stays-counted
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..996fc0cd7 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -449,9 +449,6 @@ impl Tcp {
     fn free(&mut self, index: u32) {
         let Some(generation) = self.conns.get(usize::try_from(index).unwrap_or(usize::MAX)).map(|s| s.generation) else { return };
         let Some(conn) = release(&mut self.conns, &mut self.free_conns, index) else { return };
-        if conn.user == User::Orphan {
-            self.orphans = self.orphans.saturating_sub(1);
-        }
         let remote = conn.tuple.remote.addr;
         let parked = self.parked.get(&remote).is_some_and(|p| p.conns.contains(&index));
         // Its index may name another connection next.
t2-a-closed-connection-is-not-counted
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..9856e5858 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1085,7 +1085,6 @@ impl Tcp {
                 sync.shutdown_write(now);
                 sync.orphan(now);
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
         }
t3-a-close-in-syn-received-is-not-counted
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..65ba099a1 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1072,7 +1072,6 @@ impl Tcp {
             Tcb::SynRcvd(rcvd) => {
                 rcvd.shutdown_write();
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
             Tcb::Sync(sync) if sync.rx.unread() > 0 => {
t4-ready-counts-handshakes-in-progress
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index 4f71e2fb4..178b9d5a8 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -585,7 +585,7 @@ impl Tcp {
 
     /// How many children completed their handshake and wait for [`Self::accept`].
     pub fn ready(&mut self, id: ListenerId) -> Result<usize, Error> {
-        slot(&mut self.listeners, id.index, id.generation).map(|l| l.ready.len()).ok_or(Error::NoSuchSocket)
+        slot(&mut self.listeners, id.index, id.generation).map(|l| l.pending.len()).ok_or(Error::NoSuchSocket)
     }
 
     /// The oldest child that completed its handshake.
t5-options-are-the-defaults
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..ec389d86e 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -977,7 +977,7 @@ impl Tcp {
     /// The options the connection has: the ones its listener had when its SYN arrived, or the
     /// defaults of an active open, until [`Self::set_options`] writes others.
     pub fn options(&mut self, id: ConnId) -> Result<Options, Error> {
-        Ok(self.conn(id)?.options)
+        self.conn(id).map(|_| Options::default())
     }
 
     pub fn set_options(&mut self, now: Instant, id: ConnId, options: Options) -> Result<(), Error> {

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review of wt/toyos-ownstack-e at 643584d4b, round 2: git diff 42a8fb25b 643584d4b read as new code, the hand hunks of b230f88a5, and every file the round changed whole. Read, not run.

Net lines against origin/main (35d35859e): 22 files, +3,553, -15, stage D's included. Stage E alone (git diff da4a51968 643584d4b): 18 files, +1,714, -95. Production +457, -39 (listeners.rs 210, places.rs 47, streams.rs +56 -24, lease/tcp.rs +40 -4, lib.rs +19 -4, datagram.rs +15 -3, the shard +46 -4, [tcp] 24); tests +1,189, -52; the track, two issues, the manifest and the lock +68, -4. The round's growth is accepted: the address, the socket's place and one read of [tcp] replace a write at the accept and its parameters.

Round 1's BLOCKERs

  1. OPEN. Evidence. host, toolchain and guest conclude skipping at this head (run 37837232460); cargo run -- --ci host has run at no head of the branch. This round's gates are the implementer's own and the body carries each command and exit, but no log: the directory it names (orch/ownstack/e/logs4/) is in neither the pull request nor the worktree, and I could not find it on the machine. So I could not read where any of the 39 runs panicked, as I did in round 1. That is not what Evidence asks, and it does not close this.
  2. CLOSED as written, and its fix opens BLOCKER 5. Node::listen(addr, …) reaches Tcp::listen; Shard::listen refuses by Ip::is_assigned, the predicate Udp::bind reads (toyos-net-udp/src/lib.rs:315). e28 is my patch, generated against the head's blob (bd9bcbd73), and by reading it is red at tests/listeners.rs:745: the listen at 192.0.2.7 answers Ok and unwrap_err panics. e30 is red at the same line for the same reason. A third patch I tried by reading, the check kept and Ipv4Addr::UNSPECIFIED handed to [tcp] after it, is red at :754 (AddrInUse beside the listener at every address). Body table: build 0, test 101, both.
  3. CLOSED on macOS; the Linux reading rides on BLOCKER 1. The libc program, its command, exit 0 and output are posted: set before the connection began, the accepted socket has the option; set after it was established, it does not. The write at the accept is gone with its parameters, and the code is [tcp]'s own inheritance. Checked in the code: stack.rs:756 copies the listener's options into the child at its SYN and nowhere after, so a connection whose SYN came first keeps the old value and a later one takes the new; a SYN repeated for a child in SYN-RECEIVED goes to the child and re-reads nothing. Node::set_nodelay on an accepted stream writes the stream's whole set back with one field changed (streams.rs:389-392); the set was read from [tcp] at the accept and the node writes a listener nothing but nodelay, so nothing inherited is lost. e20 and e21 are red at :824 by reading, and so is the old rule (seed from the listener at the accept: (Some(true), Some(true))). t5 is red at the first assertion of options_are_the_ones_the_connection_has.
  4. CLOSED. a_listener_ended_for_its_wake_gives_its_place_to_another_in_the_same_pass is the test I named: three places, 22 then 23, two handshakes at 23 and one wake, 22's pipe refusing, 22's last ACK through node.receive alone, and both assertions before any transmit. By reading, e29 leaves 23's owner at one wake (room is 1 in the only round, and the round ends at 22's refusal) and is red at :778. Body table: build 0, test 101.

Round 1's NOTEs are each closed: eligible has its comment; the track's places line names its own exit; common::segment is the one outside reading and both copies are gone, with every assertion of the two kept; the product is in the body, places.rs and the track with an exit at the move; the issue for std's seeded nodelay is filed with an owner and an exit. A datagram socket holds a place, so places.rs, the manifest and the track are true of the merged tree.

BLOCKER

  1. toyos-net-shard/src/lib.rs:481-486, userland/netstack/node/tests/listeners.rs:752-757 — a program that names the machine's address takes the port of another program's listener at every address, and every connection after it — Tcp::listen refuses only the same (port, addr) pair (stack.rs:556) and listener_for prefers the pair that names the address (:545), so with a server listening at 0.0.0.0:22 any client that can reach netstack listens at 192.0.2.1:22 and is handed every SYN from then on. The test asserts it: (wakes(&named), wakes(&any)) == (1, 0). That is authority nobody moved into the second program, and the node has no word for the program behind a request, so it cannot allow the pair to one owner and refuse it to another. [udp] holds a port once whatever the address (toyos-net-udp/src/lib.rs:322), so "one rule for both socket kinds" is false of the head. From memory, not measured: Linux refuses the second bind EADDRINUSE while either socket listens, and the BSDs refuse it to another user. Remove it: a listen on a port any listener holds is ListenRefused::InUse, whichever address either named, until a listen carries its program; LS-09 stays [tcp]'s and is not reachable from the node. The test: a listener at every address on 22, then a listen at 192.0.2.1:22 is InUse with nothing made and the first listener's owner is the one woken; and the same in the other order. The patch that must turn it red: the new check removed.

NOTE

  • userland/netstack/node/tests/host.rs:31-34 — two assertions in a row, neither naming the host: a red on the first hides the second answer, and the log of a red does not say which system answered. One assert_eq!((accepted_with_nodelay(true), accepted_with_nodelay(false)), (true, false), "{}", std::env::consts::OS), so a red log is the reading.
  • userland/netstack/node/tests/host.rs:13-14 — "A connect that returned is a handshake that ended" is true of the connecting end only: connect returns when the SYN-ACK arrives, and the listener's end finishes on the ACK after it. On a host that copies the option when its end finishes, the second arm reads false only because loopback delivers that ACK before connect returns, which nothing in the test waits on. The comment says what the arm rests on, and the issue says what a red there is: (true, true) once and (true, false) otherwise is this, a flaky arm, deleted and not re-run.
  • issues/toyos-has-its-own-network-stack.md — a listener at an address the lease then loses or changes keeps its place and its port, is handed nothing and its owner is told nothing; the body has it as read and untested and the tree has it nowhere. A test beside lease's, or a track line with an exit.
  • The body, "Gates" — it cites logs that are not in the pull request; the three exits and the counts stand on the implementer's word until BLOCKER 1's log.

Asked in the brief

tests/host.rs as an oracle. It is the kind root CLAUDE.md names: a differential implementation, for the one rule here no specification holds. It is tied to the node only by the same pair of answers written in two files that name each other, which a reader checks. It asserts, and that is right: a test that records an answer and asserts none cannot fail, and a table of answers by host would make ToyOS claim nothing. With the first NOTE a host that differs is a red that carries both of its answers. A green on Linux prints one line, test a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok, in the node's host target, running 1 test; that line in the host job's log is the Linux reading. Windows is unread and no check runs there; the manifest's "on Linux, macOS and Windows alike" is true of socket2's calls and says nothing of the answers.

If Linux answers differently. POSIX is silent, so the rule is what a portable program can depend on. A program cannot know which connections already wait at its listener, so it cannot depend on either answer to the second question; the only use it can rely on is the option set before any connection begins. So: if Linux differs on the second arm, ToyOS keeps [tcp]'s rule at the SYN, which is one mechanism, already specified (LS-10) and what macOS measured; the arm's assertion goes, and the issue records each host's answer with its command. If Linux differs on the first arm, hosts do not agree that a listener's option reaches a connection at all; that is no implementer's and no reviewer's choice, the branch stops and the owner rules. My memory of Linux is that it answers as macOS did on both; nobody has measured it.

socket2. Acceptable: general, widely used, already in the lock (one line, no new package, no new fetch), and the body says why. libc is in the lock and does the job only with unsafe and only on Unix, which is the scratch program's shape and not a suite's. What resolves is the workspace's patched fork (Cargo.toml:242), whose host arms are upstream's; it compiled and ran on macOS.

NotLocal and the wildcard. Shard::listen skips the check for 0.0.0.0, and every test but one listens there. A named address is one [ip] holds announcing or assigned at the listen; afterwards nothing looks again (third NOTE).

A waiting connection holds no place. Sound against the peer: counted, a handshake alone would take a place, and any peer of any listener would refuse every client everything; uncounted, a peer fills the queue of the listener it reaches. What it costs is recorded with an exit at the move, and the move will find the exit hard: one number cannot price a stream at two buffers and a listener at 8.4 MB without being either very small or false as a memory bound. That is the move's to answer in code and its reviewer's to hold; it is a present weakness, known and tracked.

A datagram socket's place. sockets rises on a bind [udp] accepted and falls on a close [udp] accepted, and [udp] ends a socket nowhere else; the responder's socket is bound past udp_bind and holds none; a second close answers NotConnected and gives nothing back (asserted at :800-801). e24 to e27 are red at :792, :791, :799 and :799 by reading. A client that binds in a loop refuses every other client its connect, listen and bind: the track says so and names an exit a test can fail. It is a new way to do what a connect loop already did, not a new weakness.

Evidence. Not met for landing. For the round it is commands and exits with no log I can read; the patches I checked against the head's blobs and the assertions they must fail, for t5, e20, e21 and e24 to e30. e01, e03, e05, e09 to e11, e13, e16, e17, e23, t1 to t4 and o1 are posted against earlier blobs and apply by offset; their context is in the head unchanged.

CI's clippy. Nothing the round adds meets one of the ten: every clone is read after (refused, named, second); Streams::accepted is down to five parameters and Node::listen has five; no instant is subtracted, no remainder taken, no bit isolated; drain_refused_listeners() returns an iterator and is not a drain(..) collected in place; accept, close_listener and Shard::udp_close each have a statement between the let and the return; no child process, no function cast. I expect none to red.

What the branch must show next

BLOCKER 5 and the first three NOTEs are code or records and come back here for a round. After it, and after #774 is on main and merged through stage D:

  • A hand resolution in any of these is new code and comes back: userland/netstack/node/src/lib.rs (the module list, Node's fields and new, and where receive, transmit, fire, settle and next_deadline end), src/datagram.rs (udp_bind and udp_close, where the place is taken and given back), src/places.rs, src/listeners.rs, src/streams.rs, src/lease.rs, src/lease/tcp.rs, toyos-net-shard/src/lib.rs, toyos-net-shard/tcp/src/stack.rs, tests/common/mod.rs, tests/listeners.rs, tests/streams.rs, tests/host.rs. The manifest's description and dev-dependencies, the track and Cargo.lock are records the orchestrator reads.
  • One thing that merge can make false without stopping: whether the resolver's sockets hold a place. If they are bound through udp_bind, a client at the bound refuses every lookup, and places.rs and the track say so with an exit; if they are the node's own, as the responder's is, places.rs says that.
  • The log: the host job of the head that enters the queue, concluded success and read whole, showing cargo run -- --ci host exit 0 and in it, on ubuntu-24.04: toyos-net-node's host target running 1 test with the line above ok; listeners 25 plus the next round's, with a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds, a_listener_ended_for_its_wake_gives_its_place_to_another_in_the_same_pass, a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing, a_stream_starts_with_the_options_its_connection_took_from_its_listener and BLOCKER 5's test each ok; streams 34, lease 18, name 13, datagram 5, slirp 3, and the resolver's targets at the counts toyos-net-node: the resolver on ToyOS's own UDP, host-tested and shipped in nothing #774's own host log has; toyos-net-tcp's held 6 and take 5; every toyos-net-shard target that has tests above 0; hostws::, userlandhost::, sourcegate:: and licence:: run and ok; clippy on the three crates under CI's toolchain with no warning. guest / suite concludes success and is not skipped; no guest test reaches the change.
  • If the host target is red on Linux, nothing lands: the two answers in its log go into the issue, and the paragraph above says which way each goes.
  • On that log, with BLOCKER 5 closed by a round and no hand resolution in the files above, the orchestrator may close BLOCKER 1 and land without another round. Not at 643584d4b.

SEND BACK

…dress went is ended, the host oracle waits on its listener

A port is one listener's. [tcp] hands a SYN to the listener that named its
address before one at every address (LS-09), so with a server at 0.0.0.0:22 any
program that reached netstack listened at the machine's address on 22 and was
handed every connection from then on: authority nobody moved into it. A listen
carries no word for its program, so the node cannot allow the pair to one owner
and refuse it to another. Node::listen now refuses a named port any listener
holds, whichever address either named, as [udp] holds a port once; LS-09 stays
[tcp]'s and is not reachable from the node. The track has the line and its exit:
a listen that carries its program.

A listener whose address went is ended. One that named the machine's address
kept its place and its port when the lease lost the address, was handed nothing
and its owner was told nothing. The pass that writes the wakes now ends it when
the lease names another address or none: [tcp] resets what waits, the port and
the place are free, the owner reads the end of its wake pipe, and the shell
reads Node::drain_ended_listeners, which replaces drain_refused_listeners and
says why (ListenerEnd). Ended and not kept, because a listener's owner makes no
call a refusal could answer: it only waits for wakes, and none would come. A
renewal and a link that returns keep the address and the listener.

tests/host.rs waits on the event its second answer depends on. A connect returns
at the SYN-ACK and the listener's end is established by the ACK after it, so the
option set "after the connection was established" rested on loopback delivering
that ACK first. The test now sets it only once the listener is readable (mio, in
the lock already, default features off), bounded by a ceiling that fails, and
asserts the pair once with the host's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Japabu and others added 2 commits October 8, 2026 22:34
…ge E

By hand: the node's module list and its header's paragraph on who owns what
(both sides whole), the manifest's description (what both sides put in the
package), and the track's stage 5 paragraph (in the tree: the resolver and
listeners both; still to build: the move). Everything else merged by itself.

places.rs says what the merged tree does: the responder's socket and each
query's are bound past Node::udp_bind and hold no place, so clients at the
bound refuse no lookup. The resolver's test that binds every dynamic port but
one now gives its node that many places, since a client's socket holds one,
and asserts that with every place a client's socket the lookup still starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Shard::listens_at is the one rule for an address a listener may be at: a listen
is refused by it, and the pass that writes the wakes ends a listener it no
longer holds for. No listener is made that the next pass ends, and none stands
where a listen would be refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu Japabu changed the title toyos-net-node: listeners on the own stack's accept queue, and one bound on what clients make the node hold (stage E, stacks on #775) toyos-net-node: listeners on the own stack's accept queue, and one bound on the streams, listeners and datagram sockets clients make the node hold (stage E, stacks on #775) Oct 8, 2026
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches of round 5, all 41, each generated by git diff at c27cc36aa (so each index line names that head's blob) and applying with git apply --check exactly. The table of runs is in the body. This set replaces every earlier one.

d21-the-nodes-deadline-leaves-streams-out.patch
diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index f842572af..a405eb61c 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -172,7 +172,7 @@ impl Node {
 
     pub fn next_deadline(&self) -> Option<Instant> {
         let name = self.name.as_ref().and_then(name::Name::next_deadline);
-        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
+        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).min()
     }
 
     /// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`].
e01-a-pass-wakes-nobody.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..c225ede9b 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -357,7 +357,6 @@ impl Node {
             *peers = peers.saturating_add(1);
         }
         live.retain(|id, stream| (connects && !stream.connecting) || stream.pass(*id, now, stack, events, &mut extended));
-        self.wake_owners(now);
     }
 
     /// The client lets go of the stream: nobody reads it, and what its pipe still holds is sent
e02-an-accept-ends-in-no-pass.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..f05dbe131 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -163,7 +163,6 @@ impl Node {
         listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
-        self.bridge(now);
         answer
     }
 
e03-an-owner-holds-one-wake-at-most.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..1f2a6a295 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -212,7 +212,7 @@ impl Node {
                 return Some((*id, ListenerEnd::Address));
             }
             let owed = stack.tcp_ready(listener.bound).min(room);
-            while listener.unspent < owed {
+            while listener.unspent < owed.min(1) {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, ListenerEnd::Wake(refusal)));
                 }
e04-only-an-accept-that-takes-spends-a-wake.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..bcfd298ae 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -160,9 +160,11 @@ impl Node {
     /// and what it already received moves at once.
     pub fn accept(&mut self, now: Instant, id: ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return Err(AcceptRefused::NoListener) };
-        listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
+        if let (Ok(_), Some(listener)) = (&answer, self.listeners.live.get_mut(&id)) {
+            listener.unspent = listener.unspent.saturating_sub(1);
+        }
         self.bridge(now);
         answer
     }
e05-a-wake-ignores-places.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..d082e5e52 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -211,7 +211,7 @@ impl Node {
             if !stack.shard().listens_at(listener.at.addr) {
                 return Some((*id, ListenerEnd::Address));
             }
-            let owed = stack.tcp_ready(listener.bound).min(room);
+            let owed = stack.tcp_ready(listener.bound);
             while listener.unspent < owed {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, ListenerEnd::Wake(refusal)));
e06-an-accept-ignores-places.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..50b634416 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -169,9 +169,6 @@ impl Node {
 
     fn take(&mut self, bound: toyos_net_tcp::ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(pipes) = pipes else { return Err(AcceptRefused::NoPipes) };
-        if self.room() == 0 {
-            return Err(AcceptRefused::Full);
-        }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
         let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
e07-a-finishing-connection-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..b49e100c5 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets)
     }
 
     pub(crate) fn room(&self) -> usize {
e09-a-closed-connect-wakes-nobody.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..883e9f346 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -368,7 +368,6 @@ impl Node {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
             self.streams.events.push_back(StreamEvent::Closed { id });
-            self.wake_owners(now);
             return;
         }
         stream.to_client = None;
e10-a-stream-reset-for-its-pipe-wakes-nobody.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..f5931d971 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -419,7 +419,6 @@ impl Node {
         if held {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
-            self.wake_owners(now);
         }
     }
 
e11-a-closed-listener-wakes-nobody.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..81014d7de 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -182,9 +182,6 @@ impl Node {
     /// says nobody holds the other end of the wake pipe. `false` is an id that names no listener.
     pub fn close_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let closed = self.end_listener(now, id);
-        if closed {
-            self.wake_owners(now);
-        }
         closed
     }
 
e12-more-places-wake-nobody.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..3087ec3d2 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -39,7 +39,6 @@ impl Node {
     /// How many places the node has from here on. Nothing held is let go for a smaller number.
     pub fn set_places(&mut self, now: Instant, places: usize) {
         self.places = places;
-        self.wake_owners(now);
     }
 
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
e13-a-connect-ignores-places.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..6cb7ee30b 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -333,9 +333,6 @@ impl Node {
     /// An active open to `remote`, answered by a [`StreamEvent`] once the handshake ends or
     /// `timeout` passes. Refused, nothing was sent and the pipe ends are dropped.
     pub fn connect(&mut self, now: Instant, remote: Endpoint, timeout: Option<Duration>, pipes: Pipes) -> Result<StreamId, ConnectRefused> {
-        if self.room() == 0 {
-            return Err(ConnectRefused::Full);
-        }
         let conn = self.stack.tcp_connect(now, remote).map_err(ConnectRefused::Stack)?;
         let deadline = timeout.map(|within| now.after(within));
         // An active open has [tcp]'s defaults until `set_nodelay`.
e14-a-listen-ignores-places.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..67a0a126c 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -119,9 +119,6 @@ impl Node {
     /// hold, on `port` or on a port chosen from `draw`'s candidates. Answers the listener and
     /// its port. Refused, nothing was made and `owner` is dropped.
     pub fn listen(&mut self, addr: Ipv4Addr, port: Option<Port>, owner: Box<dyn Wake>, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> {
-        if self.room() == 0 {
-            return Err(ListenRefused::Full);
-        }
         // A drawn port is one [tcp] finds no listener on at any address.
         if port.is_some_and(|port| self.listeners.live.values().any(|listener| listener.at.port == port)) {
             return Err(ListenRefused::InUse);
e15-a-listener-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..afdf25522 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e16-a-closed-listener-stays-in-the-stack.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..c0881d1bb 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -190,7 +190,6 @@ impl Node {
 
     fn end_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let Some(listener) = self.listeners.live.remove(&id) else { return false };
-        self.stack.tcp_close_listener(now, listener.bound);
         true
     }
 
e17-a-refused-wake-is-ignored.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..d556e26f3 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -213,8 +213,8 @@ impl Node {
             }
             let owed = stack.tcp_ready(listener.bound).min(room);
             while listener.unspent < owed {
-                if let Err(refusal) = listener.owner.wake() {
-                    return Some((*id, ListenerEnd::Wake(refusal)));
+                if listener.owner.wake().is_err() {
+                    break;
                 }
                 listener.unspent = listener.unspent.saturating_add(1);
             }
e18-a-stream-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..ff4b7856d 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.listeners().saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e19-a-drawn-port-reads-the-draws-high-half.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..9fa63551b 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -127,7 +127,7 @@ impl Node {
             return Err(ListenRefused::InUse);
         }
         let candidate = || {
-            let [low, high, ..] = draw().to_le_bytes();
+            let [.., low, high] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
         };
         let (bound, port) = match self.stack.tcp_listen(addr, port, candidate) {
e20-a-listeners-option-does-not-reach-tcp.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..1f0acd342 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -148,7 +148,6 @@ impl Node {
     pub fn set_listener_nodelay(&mut self, id: ListenerId, nodelay: bool) -> bool {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return false };
         listener.options.nodelay = nodelay;
-        self.stack.tcp_set_listener_options(listener.bound, listener.options);
         true
     }
 
e21-an-accepted-stream-holds-no-option.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..58d4b8a1b 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -174,7 +174,7 @@ impl Node {
         }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
-        let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
+        let id = self.streams.accepted(conn, tuple.remote.addr, Options::default(), pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })
     }
 
e22-an-accepted-stream-is-counted-by-the-nodes-address.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..e7b3b57b0 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -174,7 +174,7 @@ impl Node {
         }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
-        let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
+        let id = self.streams.accepted(conn, tuple.local.addr, options, pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })
     }
 
e23-an-id-is-used-twice.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..958bf203d 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -313,7 +313,6 @@ impl Streams {
     /// Holds `stream` under an id of its own.
     fn hold(&mut self, stream: Stream) -> StreamId {
         let id = StreamId(self.next);
-        self.next = self.next.saturating_add(1);
         self.live.insert(id, stream);
         id
     }
e24-a-bind-ignores-places.patch
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..2b19d06a1 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -103,9 +103,6 @@ impl Node {
     /// or, with none named, an ephemeral one, which spends the one draw. Returns the socket and
     /// the port it holds.
     pub fn udp_bind(&mut self, addr: Ipv4Addr, port: Option<Port>, draw: impl FnOnce() -> u32) -> Result<(DatagramId, Port), Refused> {
-        if self.room() == 0 {
-            return Err(Refused::ResourceExhausted);
-        }
         let (id, port) = self.stack.bind(addr, port, draw).map_err(refused)?;
         self.sockets = self.sockets.saturating_add(1);
         Ok((DatagramId(id), port))
e25-a-datagram-socket-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..021315755 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e26-a-closed-socket-wakes-nobody.patch
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..bb81727af 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -128,7 +128,6 @@ impl Node {
     pub fn udp_close(&mut self, now: Instant, id: DatagramId) -> Result<(), Refused> {
         self.stack.close(now, id.0).map_err(refused)?;
         self.sockets = self.sockets.saturating_sub(1);
-        self.wake_owners(now);
         Ok(())
     }
 }
e27-a-closed-socket-keeps-its-place.patch
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..4606c036b 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -127,7 +127,6 @@ impl Node {
     /// accepted still leaves, to [udp]'s bound. Its place is back.
     pub fn udp_close(&mut self, now: Instant, id: DatagramId) -> Result<(), Refused> {
         self.stack.close(now, id.0).map_err(refused)?;
-        self.sockets = self.sockets.saturating_sub(1);
         self.wake_owners(now);
         Ok(())
     }
e28-a-listen-is-at-every-address.patch
diff --git a/userland/netstack/node/src/lease/tcp.rs b/userland/netstack/node/src/lease/tcp.rs
index bd9bcbd73..d6e04ad2b 100644
--- a/userland/netstack/node/src/lease/tcp.rs
+++ b/userland/netstack/node/src/lease/tcp.rs
@@ -72,7 +72,7 @@ impl Stack {
     /// A passive open at `addr`, 0.0.0.0 meaning every address the interface holds or comes to
     /// hold, and at `port` or at one of `random`'s candidates.
     pub(crate) fn tcp_listen(&mut self, addr: Ipv4Addr, port: Option<Port>, random: impl FnMut() -> u16) -> Result<(ListenerId, Port), ListenError> {
-        let id = self.shard.listen(addr, port, random)?;
+        let id = self.shard.listen(Ipv4Addr::UNSPECIFIED, port, random)?;
         Ok((id, held(self.shard.listener_port(id))))
     }
 
e29-a-listener-ended-in-a-pass-ends-the-pass.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..db8f8e793 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -200,7 +200,7 @@ impl Node {
     pub(crate) fn wake_owners(&mut self, now: Instant) {
         // A listener ended here gives its place back, which another's owner may be owed a wake
         // for.
-        while self.wake_each(now) {}
+        self.wake_each(now);
     }
 
     /// One round over the listeners, up to the first to end; `true` ended one.
e30-a-listen-takes-an-address-nobody-holds.patch
diff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index ed06f07bc..809b4df82 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -485,9 +485,6 @@ impl Shard {
     /// `addr` is the local address to listen on, one [`Self::listens_at`]; port 0 takes
     /// `random`'s draws.
     pub fn listen(&mut self, addr: Ipv4Addr, port: Option<Port>, random: impl FnMut() -> u16) -> Result<ListenerId, ListenError> {
-        if !self.listens_at(addr) {
-            return Err(ListenError::NotLocal);
-        }
         self.tcp.listen(addr, port, random).map_err(ListenError::Tcp)
     }
 
e31-a-port-is-held-at-one-address-only.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..1e6e5a38c 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -123,9 +123,6 @@ impl Node {
             return Err(ListenRefused::Full);
         }
         // A drawn port is one [tcp] finds no listener on at any address.
-        if port.is_some_and(|port| self.listeners.live.values().any(|listener| listener.at.port == port)) {
-            return Err(ListenRefused::InUse);
-        }
         let candidate = || {
             let [low, high, ..] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
e32-a-listener-outlives-its-address.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 327af671f..b3bcb9586 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -208,9 +208,6 @@ impl Node {
         let room = self.room();
         let stack = &mut self.stack;
         let ended = self.listeners.live.iter_mut().find_map(|(id, listener)| {
-            if !stack.shard().listens_at(listener.at.addr) {
-                return Some((*id, ListenerEnd::Address));
-            }
             let owed = stack.tcp_ready(listener.bound).min(room);
             while listener.unspent < owed {
                 if let Err(refusal) = listener.owner.wake() {
e33-an-address-in-use-is-any-address.patch
diff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index ed06f07bc..514da5ebe 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -479,7 +479,7 @@ impl Shard {
     /// Whether `addr` is one a listener may be at: UNSPECIFIED for any, and otherwise one [ip]
     /// holds assigned or announcing, as a datagram socket's is.
     pub fn listens_at(&self, addr: Ipv4Addr) -> bool {
-        addr.is_unspecified() || self.ip.is_assigned(addr)
+        addr.is_unspecified() || self.ip.is_assigned(addr) || true
     }
 
     /// `addr` is the local address to listen on, one [`Self::listens_at`]; port 0 takes
n2-deadline-without-the-name.patch
diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index f842572af..40ec34d60 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -172,7 +172,7 @@ impl Node {
 
     pub fn next_deadline(&self) -> Option<Instant> {
         let name = self.name.as_ref().and_then(name::Name::next_deadline);
-        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
+        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
     }
 
     /// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`].
o1-a-listener-is-one-connection.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..702a7df89 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -749,7 +749,7 @@ impl Tcp {
             return;
         }
         let Some(listener) = value(&mut self.listeners, index) else { return };
-        if listener.pending.len() >= limits::LISTEN_PENDING || listener.ready.len() >= limits::LISTEN_READY {
+        if !listener.pending.is_empty() || !listener.ready.is_empty() {
             self.log.count(Counter::ListenOverflow);
             return;
         }
r16-deadline-without-the-lookups.patch
diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index f842572af..64bcede45 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -172,7 +172,7 @@ impl Node {
 
     pub fn next_deadline(&self) -> Option<Instant> {
         let name = self.name.as_ref().and_then(name::Name::next_deadline);
-        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
+        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.streams.next_deadline()).min()
     }
 
     /// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`].
t1-an-orphan-that-ends-stays-counted.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..b173f3aeb 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -449,9 +449,6 @@ impl Tcp {
     fn free(&mut self, index: u32) {
         let Some(generation) = self.conns.get(usize::try_from(index).unwrap_or(usize::MAX)).map(|s| s.generation) else { return };
         let Some(conn) = release(&mut self.conns, &mut self.free_conns, index) else { return };
-        if conn.user == User::Orphan {
-            self.orphans = self.orphans.saturating_sub(1);
-        }
         let remote = conn.tuple.remote.addr;
         let parked = self.parked.get(&remote).is_some_and(|p| p.conns.contains(&index));
         // Its index may name another connection next.
t2-a-closed-connection-is-not-counted.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..82552c793 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1091,7 +1091,6 @@ impl Tcp {
                 sync.shutdown_write(now);
                 sync.orphan(now);
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
         }
t3-a-close-in-syn-received-is-not-counted.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..b2e18f953 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1078,7 +1078,6 @@ impl Tcp {
             Tcb::SynRcvd(rcvd) => {
                 rcvd.shutdown_write();
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
             Tcb::Sync(sync) if sync.rx.unread() > 0 => {
t4-ready-counts-handshakes-in-progress.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..823abd405 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -585,7 +585,7 @@ impl Tcp {
 
     /// How many children completed their handshake and wait for [`Self::accept`].
     pub fn ready(&mut self, id: ListenerId) -> Result<usize, Error> {
-        slot(&mut self.listeners, id.index, id.generation).map(|l| l.ready.len()).ok_or(Error::NoSuchSocket)
+        slot(&mut self.listeners, id.index, id.generation).map(|l| l.pending.len()).ok_or(Error::NoSuchSocket)
     }
 
     /// The oldest child that completed its handshake.
t5-options-are-the-defaults.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..ec389d86e 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -977,7 +977,7 @@ impl Tcp {
     /// The options the connection has: the ones its listener had when its SYN arrived, or the
     /// defaults of an active open, until [`Self::set_options`] writes others.
     pub fn options(&mut self, id: ConnId) -> Result<Options, Error> {
-        Ok(self.conn(id)?.options)
+        self.conn(id).map(|_| Options::default())
     }
 
     pub fn set_options(&mut self, now: Instant, id: ConnId, options: Options) -> Result<(), Error> {

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review of wt/toyos-ownstack-e at c27cc36aa, round 3: git diff 643584d4b c27cc36aa read as new code, git show --remerge-diff fdfcf6576, lib.rs, listeners.rs, places.rs, datagram.rs and tests/host.rs whole at the head, and the round's step logs (request 5, one log a step) read file by file. Read, not run.

Net lines against origin/main (a4416fe6c): 24 files, +3,749, -18, stage D's included. Stage E alone (git diff origin/wt/toyos-ownstack-d...c27cc36aa, D at 49e38ca4a): 20 files, +1,910, -98. Production +498, -39 (listeners.rs 238, places.rs 54, streams.rs +56 -24, the shard +52 -4, lease/tcp.rs +40 -4, [tcp] 24, lib.rs +19 -4, datagram.rs +15 -3); tests +1,330, -55; the track, two issues, the manifest and the lock +82, -4. The round's growth for BLOCKER 5 (four lines and a field) is accepted. The round's growth for the ended listener is not: BLOCKER 6 names the deletion.

Earlier BLOCKERs

  1. OPEN. Evidence. host, toolchain and guest conclude SKIPPED at c27cc36aa: a draft, and cargo run -- --ci host has run at no head of the branch. What exists for the round I could read this time: gate-node-tests exit 0 with datagram 5, host 1, lease 20, listeners 28, name 13, resolve 23, slirp 3, streams 34, every test listed ok; gate-tcp-tests 0 (held 6, take 5); gate-shard-tests 0 (2, 5, 6, 19, 4, 3, 5, 3); gate-root-lib 0 with 45; three clippies 0; gate-lock 0; the mutation summary at HEAD=c27cc36aa, clean=yes. That is the round's evidence and not the landing's.
  2. CLOSED in round 2.
  3. CLOSED on macOS in round 2; the Linux reading still rides on 1.
  4. CLOSED in round 2.
  5. CLOSED. listeners.rs:126 refuses a named port any live listener holds before [tcp] is asked; a drawn port is one port_listened finds at no address (stack.rs:548, :561). Red first at 643584d4b plus the three tests: both cross-address orders exit 101 at tests/listeners.rs:766:23, unwrap_err() on Ok((ListenerId(1), Port(22))); the same-address order exits 0, which [tcp] already refused. e31 at c27cc36aa: build 0, both cross-address tests 101 at the same line with the same value. The helper asserts the refusal, the dropped owner, one listener and one place, and that the next handshake wakes and is accepted by the first. The track has the line with an exit a test can fail.

Round 2's NOTEs: tests/host.rs is one assert_eq! over the pair with the host's name, closed; the second arm waits on the listener, closed (below); the logs are readable, closed. The third, a listener whose address goes, was answered by code, and that code is BLOCKER 6.

The merge fdfcf6576, against the list in #775's round 5

  • The header sentence (lib.rs:12-15): one sentence with datagram, name and resolve, and places said to count clients' sockets with the responder's and a lookup's outside. Met.
  • Modules datagram, lease, listeners, name, places, resolve, streams; the three pub use lines at :47, :48, :59. Met.
  • Node and Node::new: name, resolver, streams, listeners, sockets, places once each in both. Met.
  • next_deadline (:175): five terms, D's line; a listener keeps no deadline. d21, n2 and r16 after the merge: 101 at tests/streams.rs:566, tests/name.rs:170, tests/resolve.rs:615, build 0 each. Met.
  • settle ends in serve_name then resolver.pass, its one call site in the crate; receive and fire end in bridge after settle; transmit in pass(now, true); the listeners' pass hangs on Node::pass (streams.rs:360) and replaces nothing. Met.
  • lease.rs, src/resolve.rs, name.rs and toyos-dns at the head are D's byte for byte: git diff 49e38ca4a c27cc36aa names 20 files and none of them. Cargo.lock against D is two lines, mio and socket2 in the node's list; cargo metadata --locked 0. Met.
  • A lookup's sockets and the places: decided in words in places.rs, the issue and the track, and held by the changed test. Met.

The changed resolver test. Not weakened. Every assertion it had stands unchanged: the lookup takes the one port left, the next lookup is NotStarted::ResourceExhausted, the lookup ends NoPort at WAIT_MS. It gains one: with 16,383 places and 16,383 client sockets, a further client bind is ResourceExhausted and the lookup still starts, which is the merged tree's claim. The refused bind is answered before its draw and changes nothing. e25 there: 101 at tests/resolve.rs:575.

BLOCKER

  1. userland/netstack/node/src/listeners.rs:211-213, :68-75, toyos-net-shard/src/lib.rs:479-483, userland/netstack/node/tests/lease.rs:234-257 — the wire ends a program's listener, for good — a listener that named the machine's address is removed in the first pass after [ip] stops holding it usable, and nothing brings it back. What takes the address is not the program's doing and is not authenticated: two ARP frames ten seconds apart from any host on the link (a_second_conflict_takes_a_held_lease_and_declines_it, and the test's own conflict), a NAK to a REQUEST that was broadcast, or a server that is away for longer than the lease has left. The lease comes back seconds later, as a rule at the same address, and the listener does not: no unchanged program listens again because its address was away, since no host tells it. So a transient loss the wire could already cause becomes a permanent loss of the service, which is worse than the host the body cites from memory, where the socket stays bound and answers again when the address returns.
    The reason given, that the owner would otherwise hold a port and a place and learn nothing, is the state the branch keeps everywhere else: a datagram socket bound to the lost address stands with its port and its place (datagram.rs, a send is refused, a receive is silent); a stream established from it stands until [tcp] gives it up; a listener at every address stands with no address at all; and a listener at the held address stands through a link that is down, reachable by nobody and told nothing (the round's own second test). The port and the place are the owner's until it closes; with BLOCKER 5's rule nobody else is owed the port.
    Remove it: the check in wake_each, ListenerEnd (the drain is (ListenerId, WriteRefusal) again, under whichever of the two names), Shard::listens_at back into its one caller, Listener::at down to the port BLOCKER 5 reads, the header's paragraph, and the sentence in the track. Nothing replaces it in the node: [ip] takes no segment for an address it does not hold, and Tcp's bound still has the pair when the address is back. The test, beside the lease's, over the same three losses: the named listener and the one at every address both stand, two listeners and two places, nothing drained, neither owner's end dropped; a SYN to the lost address is answered by nothing and wakes nobody; a listen there on another port is NotLocal; and once the lease is held again at that address a handshake completes and wakes the named listener's owner once. It is red at c27cc36aa as it stands, which is its control. a_listener_at_the_held_address_outlives_a_renewal_and_a_link_that_returns folds into it. The track says the listener stands as a datagram socket does, that no host was read, and keeps the exit it has. The move's body loses the last clause of difference 9.
  2. Evidence, as above.

NOTE

  • userland/netstack/node/tests/host.rs:38 — poll.poll(...).expect(...) panics on ErrorKind::Interrupted, which mio hands back and does not retry — a red there is no host's answer; the loop already runs until an event or the ceiling, so an interrupted poll is one more turn of it.
  • The body, "What I am unsure of": "It is the brief's ruling" of the ended listener — the track and the source carry no ruling; with BLOCKER 6 the sentence goes.
  • The body, difference 9 and the paragraph "a listener whose address the machine loses" — rewritten with BLOCKER 6.

Asked in the brief

Ending against a renewal, a link flap, rebinding. As built, none of the three ends it: Stack::renew re-adds the held address and [ip] keeps it usable; a link down leaves the lease and the address; rebinding holds the address until expiry. A renewal the server answers with a NAK, a link that returns to a NAK, and expiry each end it. The owner learns only that its wake pipe ended; why is the shell's (ListenerEnd::Address), and no word on the pipe ABI carries it to the program.

The renewal and link-flap test without a mutation. It is not a test that cannot fail: Wire::link(false) pumps a transmit and so a pass with the link down, and renew goes through add_address. A predicate that also asked for the link, or a renewal that removed and re-added, turns it red at its one assertion. It holds [ip]'s and lease's behaviour through the node, and no line of this stage; under BLOCKER 6 it is part of the one test.

tests/host.rs, the second arm. "Listener readable" is the right event on both hosts: a listening socket is readable when its accept queue holds a connection, which is after the host made the child, whether at the SYN or at the last ACK. The poller is registered before the connect, so the edge is not missed; both arms take the same path. From memory, not measured: Linux clones the child from the listener at the last ACK, so with this wait it answers (true, false) as macOS did. The 60 s is a ceiling on a wait for an event and panics by name: not a flat wait. What the arm cannot tell apart is a rule at the SYN from a rule at the last ACK, and no portable program can either.

mio. Acceptable under Dependencies: general and widely used, already in the lock through the workspace's patch, one line in the node's list and no new package or fetch, default features off, a dev-dependency of one target; the body says why. The question needs readiness without an accept, which std and socket2 cannot give. What resolves is the fork, whose host arms are upstream's; Windows is unread.

What the host log must show, and who may land

After BLOCKER 6 is closed by a round, #775 is on main, and this branch has merged main:

  • The measure that replaces a hunk-by-hunk reading of that merge: git diff <the head round 4 judges> <the head that enters the queue> over userland/netstack/node, toyos-net-shard, toyos-dns, userland/netstack/src/resolve.rs, Cargo.lock's toyos-net-node entry and the track is empty, and git diff origin/main...<head> names stage E's files and no others. If it is empty, however the conflicts with D's squash were resolved, nothing is new. If it is not, a difference in any of these is new code and comes back: node/src/lib.rs, listeners.rs, places.rs, streams.rs, datagram.rs, lease.rs, lease/tcp.rs, resolve.rs; toyos-net-shard/src/lib.rs, tcp/src/stack.rs; tests/common/mod.rs, tests/listeners.rs, tests/lease.rs, tests/streams.rs, tests/resolve.rs, tests/host.rs, tcp/tests/held.rs. The manifest, the issues and the track are records the orchestrator reads.
  • The host job of that head, concluded success and not skipped, on ubuntu-24.04, read whole: cargo run -- --ci host exit 0, and in it toyos-net-node: tests/host.rs running 1 test and test a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok, which is the Linux reading; datagram 5, name 13, resolve 23, slirp 3, streams 34, and lease and listeners at the counts round 4 names (20 and 28 at this head), with by name the three port tests, round 4's test of the lost address, a_listener_ended_for_its_wake_gives_its_place_to_another_in_the_same_pass, a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing, a_stream_starts_with_the_options_its_connection_took_from_its_listener, a_query_with_no_port_to_leave_from_ends_its_lookup_by_name, and the four that hold the deadline line. toyos-net-tcp: held 6, take 5. toyos-net-shard: acquisition 5, drr 6, egress 19, icmp 4, log 3, net 5, udp 3, 2 unit tests. toyos-dns: 44. hostws::, userlandhost::, sourcegate::, licence:: run and ok. Clippy on [tcp], the shard and the node under CI's toolchain with no warning. guest / suite success and not skipped.
  • If the host target is red on Linux, nothing lands; the log line is the reading, linux: (…, …). (true, true): Linux gives a waiting connection its listener's later option; ToyOS keeps [tcp]'s rule at the SYN, the arm's assertion goes, the issue records both hosts with their commands, and that diff is a round. (false, _): hosts do not agree that a listener's option reaches a connection at all; the branch stops and the owner rules.
  • On that log and that empty diff the orchestrator closes BLOCKER 1 and lands without another round. Not at c27cc36aa.

Before the move may be dispatched: the plan's table

Confirmed against the track at this head: rows 2, 3, 6, 11 to 14 and 17 carry the track's own "before the move" and "at the move" exits as written. Corrections:

SEND BACK

Round 2 ended a listener in the first pass after the machine lost the address
its listen named, and nothing brought it back. What takes the address is the
wire's doing and is not authenticated: a NAK, a server away for longer than the
lease has left, two ARP frames inside the defend interval. The lease returns
seconds later, as a rule at the same address, and no unchanged program listens
again because its address was away. So a loss the wire could already cause for
seconds became the loss of a service for good.

Removed whole: the check in wake_each, ListenerEnd (the drain carries the
refusal of the wake again), Shard::listens_at back into Shard::listen, and
Listener::at down to the port the one-listener-a-port rule reads. Nothing
replaces it: [ip] takes no segment for an address it does not hold, and [tcp]
still has the listener when the address is back. A listener at a lost address
stands as a datagram socket bound to it does.

a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back, beside
the lease's tests, over a NAK, expiry and a conflict, and through a renewal and
a link that returns before them: both listeners stand with two places, nothing
is drained and neither owner's end is dropped; a SYN to the lost address is
answered by nothing and wakes nobody; a listen there on another port is
NotLocal; and once the lease is held again a handshake completes and wakes the
named listener's owner once. Red against the round 2 source. It replaces round
2's two tests. common::outside reads a TCP segment for it.

tests/host.rs: an interrupted poll is one more turn of the loop.

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

Mutation patches of round 6, all 39, each generated by git diff at e737052a5 (so each index line names that head's blob) and applying with git apply --check exactly. The table of runs is in the body. This set replaces every earlier one; e32 and e33 went with the code they mutated.

d21-the-nodes-deadline-leaves-streams-out.patch
diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 60b4e21ce..c96f197dc 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -172,7 +172,7 @@ impl Node {
 
     pub fn next_deadline(&self) -> Option<Instant> {
         let name = self.name.as_ref().and_then(name::Name::next_deadline);
-        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
+        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).min()
     }
 
     /// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`].
e01-a-pass-wakes-nobody.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..c225ede9b 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -357,7 +357,6 @@ impl Node {
             *peers = peers.saturating_add(1);
         }
         live.retain(|id, stream| (connects && !stream.connecting) || stream.pass(*id, now, stack, events, &mut extended));
-        self.wake_owners(now);
     }
 
     /// The client lets go of the stream: nobody reads it, and what its pipe still holds is sent
e02-an-accept-ends-in-no-pass.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..e61ad23ea 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -150,7 +150,6 @@ impl Node {
         listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
-        self.bridge(now);
         answer
     }
 
e03-an-owner-holds-one-wake-at-most.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..1614970b5 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -195,7 +195,7 @@ impl Node {
         let stack = &mut self.stack;
         let ended = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
-            while listener.unspent < owed {
+            while listener.unspent < owed.min(1) {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
                 }
e04-only-an-accept-that-takes-spends-a-wake.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..6b83d9bf2 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -147,9 +147,11 @@ impl Node {
     /// and what it already received moves at once.
     pub fn accept(&mut self, now: Instant, id: ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return Err(AcceptRefused::NoListener) };
-        listener.unspent = listener.unspent.saturating_sub(1);
         let bound = listener.bound;
         let answer = self.take(bound, pipes);
+        if let (Ok(_), Some(listener)) = (&answer, self.listeners.live.get_mut(&id)) {
+            listener.unspent = listener.unspent.saturating_sub(1);
+        }
         self.bridge(now);
         answer
     }
e05-a-wake-ignores-places.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..9b44fabc3 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -194,7 +194,7 @@ impl Node {
         let room = self.room();
         let stack = &mut self.stack;
         let ended = self.listeners.live.iter_mut().find_map(|(id, listener)| {
-            let owed = stack.tcp_ready(listener.bound).min(room);
+            let owed = stack.tcp_ready(listener.bound);
             while listener.unspent < owed {
                 if let Err(refusal) = listener.owner.wake() {
                     return Some((*id, refusal));
e06-an-accept-ignores-places.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..c9860dfb3 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -156,9 +156,6 @@ impl Node {
 
     fn take(&mut self, bound: toyos_net_tcp::ListenerId, pipes: Option<Pipes>) -> Result<Accepted, AcceptRefused> {
         let Some(pipes) = pipes else { return Err(AcceptRefused::NoPipes) };
-        if self.room() == 0 {
-            return Err(AcceptRefused::Full);
-        }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
         let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
e07-a-finishing-connection-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..b49e100c5 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets)
     }
 
     pub(crate) fn room(&self) -> usize {
e09-a-closed-connect-wakes-nobody.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..883e9f346 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -368,7 +368,6 @@ impl Node {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
             self.streams.events.push_back(StreamEvent::Closed { id });
-            self.wake_owners(now);
             return;
         }
         stream.to_client = None;
e10-a-stream-reset-for-its-pipe-wakes-nobody.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..f5931d971 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -419,7 +419,6 @@ impl Node {
         if held {
             self.stack.tcp_abort(now, stream.conn);
             self.streams.live.remove(&id);
-            self.wake_owners(now);
         }
     }
 
e11-a-closed-listener-wakes-nobody.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..95f854377 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -169,9 +169,6 @@ impl Node {
     /// says nobody holds the other end of the wake pipe. `false` is an id that names no listener.
     pub fn close_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let closed = self.end_listener(now, id);
-        if closed {
-            self.wake_owners(now);
-        }
         closed
     }
 
e12-more-places-wake-nobody.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..3087ec3d2 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -39,7 +39,6 @@ impl Node {
     /// How many places the node has from here on. Nothing held is let go for a smaller number.
     pub fn set_places(&mut self, now: Instant, places: usize) {
         self.places = places;
-        self.wake_owners(now);
     }
 
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
e13-a-connect-ignores-places.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..6cb7ee30b 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -333,9 +333,6 @@ impl Node {
     /// An active open to `remote`, answered by a [`StreamEvent`] once the handshake ends or
     /// `timeout` passes. Refused, nothing was sent and the pipe ends are dropped.
     pub fn connect(&mut self, now: Instant, remote: Endpoint, timeout: Option<Duration>, pipes: Pipes) -> Result<StreamId, ConnectRefused> {
-        if self.room() == 0 {
-            return Err(ConnectRefused::Full);
-        }
         let conn = self.stack.tcp_connect(now, remote).map_err(ConnectRefused::Stack)?;
         let deadline = timeout.map(|within| now.after(within));
         // An active open has [tcp]'s defaults until `set_nodelay`.
e14-a-listen-ignores-places.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..ec95e7606 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -106,9 +106,6 @@ impl Node {
     /// hold, on `port` or on a port chosen from `draw`'s candidates. Answers the listener and
     /// its port. Refused, nothing was made and `owner` is dropped.
     pub fn listen(&mut self, addr: Ipv4Addr, port: Option<Port>, owner: Box<dyn Wake>, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> {
-        if self.room() == 0 {
-            return Err(ListenRefused::Full);
-        }
         // A drawn port is one [tcp] finds no listener on at any address.
         if port.is_some_and(|port| self.listeners.live.values().any(|listener| listener.port == port)) {
             return Err(ListenRefused::InUse);
e15-a-listener-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..afdf25522 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e16-a-closed-listener-stays-in-the-stack.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..b383536f4 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -177,7 +177,6 @@ impl Node {
 
     fn end_listener(&mut self, now: Instant, id: ListenerId) -> bool {
         let Some(listener) = self.listeners.live.remove(&id) else { return false };
-        self.stack.tcp_close_listener(now, listener.bound);
         true
     }
 
e17-a-refused-wake-is-ignored.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..2dcfb4372 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -196,8 +196,8 @@ impl Node {
         let ended = self.listeners.live.iter_mut().find_map(|(id, listener)| {
             let owed = stack.tcp_ready(listener.bound).min(room);
             while listener.unspent < owed {
-                if let Err(refusal) = listener.owner.wake() {
-                    return Some((*id, refusal));
+                if listener.owner.wake().is_err() {
+                    break;
                 }
                 listener.unspent = listener.unspent.saturating_add(1);
             }
e18-a-stream-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..ff4b7856d 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.listeners().saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e19-a-drawn-port-reads-the-draws-high-half.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..4410eb12a 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -114,7 +114,7 @@ impl Node {
             return Err(ListenRefused::InUse);
         }
         let candidate = || {
-            let [low, high, ..] = draw().to_le_bytes();
+            let [.., low, high] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
         };
         let (bound, port) = match self.stack.tcp_listen(addr, port, candidate) {
e20-a-listeners-option-does-not-reach-tcp.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..a1dc0a2f8 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -135,7 +135,6 @@ impl Node {
     pub fn set_listener_nodelay(&mut self, id: ListenerId, nodelay: bool) -> bool {
         let Some(listener) = self.listeners.live.get_mut(&id) else { return false };
         listener.options.nodelay = nodelay;
-        self.stack.tcp_set_listener_options(listener.bound, listener.options);
         true
     }
 
e21-an-accepted-stream-holds-no-option.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..ae6f41f5b 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -161,7 +161,7 @@ impl Node {
         }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
-        let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
+        let id = self.streams.accepted(conn, tuple.remote.addr, Options::default(), pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })
     }
 
e22-an-accepted-stream-is-counted-by-the-nodes-address.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..587b48a64 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -161,7 +161,7 @@ impl Node {
         }
         let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) };
         // The peer's address is what `streams` counts a stream its client can see no more by.
-        let id = self.streams.accepted(conn, tuple.remote.addr, options, pipes);
+        let id = self.streams.accepted(conn, tuple.local.addr, options, pipes);
         Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port })
     }
 
e23-an-id-is-used-twice.patch
diff --git a/userland/netstack/node/src/streams.rs b/userland/netstack/node/src/streams.rs
index 41e86527a..958bf203d 100644
--- a/userland/netstack/node/src/streams.rs
+++ b/userland/netstack/node/src/streams.rs
@@ -313,7 +313,6 @@ impl Streams {
     /// Holds `stream` under an id of its own.
     fn hold(&mut self, stream: Stream) -> StreamId {
         let id = StreamId(self.next);
-        self.next = self.next.saturating_add(1);
         self.live.insert(id, stream);
         id
     }
e24-a-bind-ignores-places.patch
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..2b19d06a1 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -103,9 +103,6 @@ impl Node {
     /// or, with none named, an ephemeral one, which spends the one draw. Returns the socket and
     /// the port it holds.
     pub fn udp_bind(&mut self, addr: Ipv4Addr, port: Option<Port>, draw: impl FnOnce() -> u32) -> Result<(DatagramId, Port), Refused> {
-        if self.room() == 0 {
-            return Err(Refused::ResourceExhausted);
-        }
         let (id, port) = self.stack.bind(addr, port, draw).map_err(refused)?;
         self.sockets = self.sockets.saturating_add(1);
         Ok((DatagramId(id), port))
e25-a-datagram-socket-holds-no-place.patch
diff --git a/userland/netstack/node/src/places.rs b/userland/netstack/node/src/places.rs
index ce45b8c18..021315755 100644
--- a/userland/netstack/node/src/places.rs
+++ b/userland/netstack/node/src/places.rs
@@ -45,7 +45,7 @@ impl Node {
     /// The places taken: streams, listeners, datagram sockets, and connections [tcp] is
     /// finishing alone.
     pub fn held(&self) -> usize {
-        self.streams().saturating_add(self.listeners()).saturating_add(self.sockets).saturating_add(self.stack.tcp_orphans())
+        self.streams().saturating_add(self.listeners()).saturating_add(self.stack.tcp_orphans())
     }
 
     pub(crate) fn room(&self) -> usize {
e26-a-closed-socket-wakes-nobody.patch
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..bb81727af 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -128,7 +128,6 @@ impl Node {
     pub fn udp_close(&mut self, now: Instant, id: DatagramId) -> Result<(), Refused> {
         self.stack.close(now, id.0).map_err(refused)?;
         self.sockets = self.sockets.saturating_sub(1);
-        self.wake_owners(now);
         Ok(())
     }
 }
e27-a-closed-socket-keeps-its-place.patch
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index b24dead14..4606c036b 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -127,7 +127,6 @@ impl Node {
     /// accepted still leaves, to [udp]'s bound. Its place is back.
     pub fn udp_close(&mut self, now: Instant, id: DatagramId) -> Result<(), Refused> {
         self.stack.close(now, id.0).map_err(refused)?;
-        self.sockets = self.sockets.saturating_sub(1);
         self.wake_owners(now);
         Ok(())
     }
e28-a-listen-is-at-every-address.patch
diff --git a/userland/netstack/node/src/lease/tcp.rs b/userland/netstack/node/src/lease/tcp.rs
index bd9bcbd73..d6e04ad2b 100644
--- a/userland/netstack/node/src/lease/tcp.rs
+++ b/userland/netstack/node/src/lease/tcp.rs
@@ -72,7 +72,7 @@ impl Stack {
     /// A passive open at `addr`, 0.0.0.0 meaning every address the interface holds or comes to
     /// hold, and at `port` or at one of `random`'s candidates.
     pub(crate) fn tcp_listen(&mut self, addr: Ipv4Addr, port: Option<Port>, random: impl FnMut() -> u16) -> Result<(ListenerId, Port), ListenError> {
-        let id = self.shard.listen(addr, port, random)?;
+        let id = self.shard.listen(Ipv4Addr::UNSPECIFIED, port, random)?;
         Ok((id, held(self.shard.listener_port(id))))
     }
 
e29-a-listener-ended-in-a-pass-ends-the-pass.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..d9462f536 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -186,7 +186,7 @@ impl Node {
     pub(crate) fn wake_owners(&mut self, now: Instant) {
         // A listener ended here gives its place back, which another's owner may be owed a wake
         // for.
-        while self.wake_each(now) {}
+        self.wake_each(now);
     }
 
     /// One round over the listeners, up to the first whose pipe refuses a wake; `true` ended it.
e30-a-listen-takes-an-address-nobody-holds.patch
diff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index d7ee5fb50..fd8e2d469 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -479,9 +479,6 @@ impl Shard {
     /// `addr` is the local address to listen on, UNSPECIFIED for any, and otherwise one [ip]
     /// holds assigned or announcing, as a datagram socket's is; port 0 takes `random`'s draws.
     pub fn listen(&mut self, addr: Ipv4Addr, port: Option<Port>, random: impl FnMut() -> u16) -> Result<ListenerId, ListenError> {
-        if !addr.is_unspecified() && !self.ip.is_assigned(addr) {
-            return Err(ListenError::NotLocal);
-        }
         self.tcp.listen(addr, port, random).map_err(ListenError::Tcp)
     }
 
e31-a-port-is-held-at-one-address-only.patch
diff --git a/userland/netstack/node/src/listeners.rs b/userland/netstack/node/src/listeners.rs
index 552c962a6..e2373720f 100644
--- a/userland/netstack/node/src/listeners.rs
+++ b/userland/netstack/node/src/listeners.rs
@@ -110,9 +110,6 @@ impl Node {
             return Err(ListenRefused::Full);
         }
         // A drawn port is one [tcp] finds no listener on at any address.
-        if port.is_some_and(|port| self.listeners.live.values().any(|listener| listener.port == port)) {
-            return Err(ListenRefused::InUse);
-        }
         let candidate = || {
             let [low, high, ..] = draw().to_le_bytes();
             u16::from_le_bytes([low, high])
n2-deadline-without-the-name.patch
diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 60b4e21ce..75820dbbd 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -172,7 +172,7 @@ impl Node {
 
     pub fn next_deadline(&self) -> Option<Instant> {
         let name = self.name.as_ref().and_then(name::Name::next_deadline);
-        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
+        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
     }
 
     /// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`].
o1-a-listener-is-one-connection.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..702a7df89 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -749,7 +749,7 @@ impl Tcp {
             return;
         }
         let Some(listener) = value(&mut self.listeners, index) else { return };
-        if listener.pending.len() >= limits::LISTEN_PENDING || listener.ready.len() >= limits::LISTEN_READY {
+        if !listener.pending.is_empty() || !listener.ready.is_empty() {
             self.log.count(Counter::ListenOverflow);
             return;
         }
r16-deadline-without-the-lookups.patch
diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 60b4e21ce..ff58dcf6b 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -172,7 +172,7 @@ impl Node {
 
     pub fn next_deadline(&self) -> Option<Instant> {
         let name = self.name.as_ref().and_then(name::Name::next_deadline);
-        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).chain(self.streams.next_deadline()).min()
+        self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.streams.next_deadline()).min()
     }
 
     /// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`].
t1-an-orphan-that-ends-stays-counted.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..b173f3aeb 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -449,9 +449,6 @@ impl Tcp {
     fn free(&mut self, index: u32) {
         let Some(generation) = self.conns.get(usize::try_from(index).unwrap_or(usize::MAX)).map(|s| s.generation) else { return };
         let Some(conn) = release(&mut self.conns, &mut self.free_conns, index) else { return };
-        if conn.user == User::Orphan {
-            self.orphans = self.orphans.saturating_sub(1);
-        }
         let remote = conn.tuple.remote.addr;
         let parked = self.parked.get(&remote).is_some_and(|p| p.conns.contains(&index));
         // Its index may name another connection next.
t2-a-closed-connection-is-not-counted.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..82552c793 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1091,7 +1091,6 @@ impl Tcp {
                 sync.shutdown_write(now);
                 sync.orphan(now);
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
         }
t3-a-close-in-syn-received-is-not-counted.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..b2e18f953 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -1078,7 +1078,6 @@ impl Tcp {
             Tcb::SynRcvd(rcvd) => {
                 rcvd.shutdown_write();
                 conn.user = User::Orphan;
-                self.orphans = self.orphans.saturating_add(1);
                 self.settle(id.index, now);
             }
             Tcb::Sync(sync) if sync.rx.unread() > 0 => {
t4-ready-counts-handshakes-in-progress.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..823abd405 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -585,7 +585,7 @@ impl Tcp {
 
     /// How many children completed their handshake and wait for [`Self::accept`].
     pub fn ready(&mut self, id: ListenerId) -> Result<usize, Error> {
-        slot(&mut self.listeners, id.index, id.generation).map(|l| l.ready.len()).ok_or(Error::NoSuchSocket)
+        slot(&mut self.listeners, id.index, id.generation).map(|l| l.pending.len()).ok_or(Error::NoSuchSocket)
     }
 
     /// The oldest child that completed its handshake.
t5-options-are-the-defaults.patch
diff --git a/toyos-net-shard/tcp/src/stack.rs b/toyos-net-shard/tcp/src/stack.rs
index b0c7a8391..ec389d86e 100644
--- a/toyos-net-shard/tcp/src/stack.rs
+++ b/toyos-net-shard/tcp/src/stack.rs
@@ -977,7 +977,7 @@ impl Tcp {
     /// The options the connection has: the ones its listener had when its SYN arrived, or the
     /// defaults of an active open, until [`Self::set_options`] writes others.
     pub fn options(&mut self, id: ConnId) -> Result<Options, Error> {
-        Ok(self.conn(id)?.options)
+        self.conn(id).map(|_| Options::default())
     }
 
     pub fn set_options(&mut self, now: Instant, id: ConnId, options: Options) -> Result<(), Error> {

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review of wt/toyos-ownstack-e at e737052a5, round 4: git diff c27cc36aa e737052a5 (8 files, +129, -100; one commit), listeners.rs, tests/lease.rs and tests/common/mod.rs whole at the head, the body, and the round's step logs (round 6, one log a step) read file by file. Read, not run.

Net lines against origin/main (a4416fe6c): 24 files, +3,782, -22, stage D's included. Stage E alone (git diff origin/wt/toyos-ownstack-d...e737052a5, D at 49e38ca4a): 20 files, +1,943, -102. Production +476, -39 (listeners.rs 222, places.rs 54, streams.rs +56 -24, the shard +46 -4, lease/tcp.rs +40 -4, [tcp] 24, lib.rs +19 -4, datagram.rs +15 -3); tests +1,385, -59; the track, two issues, the manifest and the lock +82, -4. The round itself takes production down by 22 lines (+22, -44 over the shard and the node's src). Nothing further to delete.

Earlier BLOCKERs

  1. OPEN. Evidence. host, toolchain and guest conclude SKIPPED at e737052a5: a draft, and cargo run -- --ci host has run at no head of the branch. What exists for the round, read: gate-node-tests exit 0 with datagram 5, host 1, lease 19, listeners 28, name 13, resolve 23, slirp 3, streams 34, every test listed ok; gate-tcp-tests 0 (held 6, take 5); gate-shard-tests 0 (2, 5, 6, 19, 4, 3, 5, 3); gate-root-lib 0 with 45; three clippies 0; gate-lock 0; the mutation summary at HEAD=e737052a5, clean=yes, 45 runs of 39 patches, build 0 and test 101 each, and the node's tests and held 0 again after them. The round's evidence, not the landing's.
  2. CLOSED in round 2.
  3. CLOSED on macOS in round 2; the Linux reading still rides on 1.
  4. CLOSED in round 2.
  5. CLOSED in round 3. e31 at this head: build 0, both cross-address tests 101 at tests/listeners.rs:766.
  6. CLOSED. As listed, item by item:
    • the check in wake_each: gone; wake_each is 643584d4b's but for the binding's name.
    • ListenerEnd: gone from listeners.rs, the pub use and both test files; no name of it is left in the tree. The drain is (ListenerId, WriteRefusal).
    • Shard::listens_at: gone; toyos-net-shard/src/lib.rs against 643584d4b is the empty diff.
    • Listener::at: now port, read by the one-listener-a-port rule at listeners.rs:113 and by nothing else. Endpoint stays imported for Accepted::remote.
    • the header paragraph and the track's sentence: rewritten as asked; the track's exit is 643584d4b's word for word.
    • nothing replaces it in the node: listeners.rs against 643584d4b is BLOCKER 5's rule, its field, the header's two paragraphs and the drain's name.
    • The measurement: a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back built against c27cc36aa's source exits 101 at tests/lease.rs:282:13, a NAK, the address lost, (1, 1, 1) against (2, 2, 0), 18 filtered out; at e737052a5 it is ok among lease's 19.

The test, against what round 3 named

Every assertion is there, and inside the loop over the three losses, so each loss gets all of them:

  • both stand, two places, nothing drained: :282, (listeners(), held(), drain_ended_listeners().count()) == (2, 2, 0).
  • neither owner's end dropped, and no wake: :283.
  • both at five points: the link down, renewed with the link back, the address lost, after the SYN to the lost address, the address back.
  • the SYN to the lost address answered by nothing: :299, the node's frames for it are []; waking nobody: the stand after it.
  • a listen there on another port NotLocal: :301.
  • the lease held again at that address, a handshake completes, the named owner woken once: :303-:314. The address is the same one by two readings: message_of offers A, and common::segment asserts the SYN-ACK's source is A. The SYN-ACK is etherparse's reading, from port 22 acknowledging ISS + 1; (named.wakes, any.wakes, listeners(), held()) == (1, 0, 2, 2).
  • the renewal and link-flap test is folded in ahead of the loss, with the lease's address asserted Some(A).

BLOCKER

  1. Evidence, as above. Nothing else.

NOTE

None.

Asked in the brief

"Has no mutation at this head." Right. The test guards an absence, and its control is the removed rule put back whole: the same test text against c27cc36aa's source, red at the first loss point. The red run stops at the NAK, the first of the three, and so shows nothing of expiry or the conflict by itself; that each of the three tripped the rule is round 5's green of the old test at c27cc36aa, which asserted the end for each. Together they are sufficient. At this head no line of the stage exists for this test alone: the one line it would turn red is Shard::listen's !self.ip.is_assigned(addr) (:301), and e28 and e30 already hold that at tests/listeners.rs:744. [tcp]'s bound keeping the pair is no line either: nothing on the path of a lost address calls into [tcp]. That a SYN to an address not held goes unanswered is [ip]'s, held in its crate. A defect here would be a rule added, and the red-first run is that defect.

The name drain_ended_listeners. It stands. What it drains is listeners the node ended; the item's type and the doc line say the one reason there is. A listener its owner closed is not in it, and was in it under neither name. Row 25 of the move's plan takes this name.

Seen::Tcp in the shared helper. Every other reader reads what it read. common::outside has one caller, Wire::pump; its ARP, DHCP and echo arms are untouched, and a TCP frame through it was a panic before this round, so no green test saw one. Seen::dhcp answers None for the new variant. Seen::source answers Some(A), which is what segment asserts of the frame; its one caller (tests/lease.rs:98) is in a test that emits no segment. Segment gains PartialEq, Eq and nothing else. tests/lan/mod.rs has its own outside and Seen and is not in the diff. The log has datagram 5, name 13, resolve 23, slirp 3, streams 34, listeners 28 green with it.

tests/host.rs. An interrupted poll falls through to the loop's condition, the ceiling is recomputed from start, and every other error still panics by name. Round 3's NOTE is closed.

The body. "It is the brief's ruling" is gone; difference 9 ends "stands, and answers again when the address is back". Its numbers for this head (20 files, +1,943, -102; +476, -39; +1,385, -59; +82, -4) are --numstat's, and its two claims against 643584d4b hold. Both body NOTEs of round 3 are closed.

Anything dead or new. Nothing dead: no caller of a removed name is left, and Stack::shard is stage D's. New in the round: Seen::Tcp and its two arms, from_peer and syn in tests/lease.rs, Told, and four lines in tests/host.rs; all test code, each used.

What closes BLOCKER 1, and who may land

Unchanged from round 3 but for the head and the counts. After #775 is on main and this branch has merged main:

  • git diff e737052a5 <the head that enters the queue> over userland/netstack/node, toyos-net-shard, toyos-dns, userland/netstack/src/resolve.rs, Cargo.lock's toyos-net-node entry and the track is empty, and git diff origin/main...<head> names stage E's 20 files and no others. Empty, however the conflicts with D's squash were resolved, nothing is new. Not empty, a difference in any of round 3's listed files is new code and comes back for a round.
  • The host job of that head, concluded success and not skipped, on ubuntu-24.04, read whole: cargo run -- --ci host exit 0, and in it toyos-net-node: tests/host.rs running 1 test and test a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok, which is the Linux reading; datagram 5, name 13, resolve 23, slirp 3, streams 34, lease 19, listeners 28; by name a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back, a_listen_at_the_machines_address_takes_no_port_held_at_every_address, a_listen_at_every_address_takes_no_port_held_at_the_machines_address, a_second_listen_at_the_machines_address_takes_no_port_held_there, a_listener_ended_for_its_wake_gives_its_place_to_another_in_the_same_pass, a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing, a_stream_starts_with_the_options_its_connection_took_from_its_listener, a_query_with_no_port_to_leave_from_ends_its_lookup_by_name, and the deadline line's a_connect_past_its_deadline_is_timed_out_at_the_deadline, a_held_lease_is_announced_at_once_and_a_second_later, a_resolver_that_never_answers_is_asked_at_each_waits_end and next_deadline_is_the_earliest_of_the_clients_and_the_stacks. toyos-net-tcp: held 6, take 5. toyos-net-shard: acquisition 5, drr 6, egress 19, icmp 4, log 3, net 5, udp 3, 2 unit tests. toyos-dns: 44. hostws::, userlandhost::, sourcegate::, licence:: run and ok. Clippy on [tcp], the shard and the node under CI's toolchain with no warning. guest / suite success and not skipped.
  • If the host target is red on Linux, nothing lands; the log line is the reading, linux: (…, …). (true, true): Linux gives a waiting connection its listener's later option; ToyOS keeps [tcp]'s rule at the SYN, the arm's assertion goes, the issue records both hosts with their commands, and that diff is a round. (false, _): hosts do not agree that a listener's option reaches a connection at all; the branch stops and the owner rules.
  • On that log and that empty diff the orchestrator closes BLOCKER 1 and lands without another round. Not at e737052a5: the code is sound and no code finding is open, and the only thing between this head and main is the measurement.

SEND BACK

Japabu added a commit that referenced this pull request Oct 8, 2026
…d only when no query is still read

Round 1 of the review of #781.

A report from the wire is anyone's word: an off-path sender who guesses a
query's source port can quote its addresses and ports, and needs no id.
toyos-net-udp's SocketError::Unreachable was both an ICMP prohibition and
[ip]'s own report that a next hop failed; it is two words now, Prohibited
and NextHopFailed. The node's resolver lets a query go only on what it
knows: [udp]'s refusal in the call, and NextHopFailed. An ICMP error
(Refused, Prohibited) goes to the new toyos_dns::Lookup::on_report, which
asks the next server at once where one is left to ask, lets no query go
and ends nothing: the query's socket stays open and its answer is read.

Lookup counts the queries sent in a row with no wait between them, and
sends at once only while that run is shorter than the servers, so no
server is asked twice inside WAIT_MS whoever reports what. When nobody is
left to ask at once it ends Unreachable only if no query is still read,
and otherwise waits the newest query's wait out: before, an unreached
newest query ended the lookup under older queries whose answers were a
millisecond away, and an unreached last query ended it TimedOut early.

an_alias_answered_late_restarts_the_lookup_and_lets_the_old_names_queries_go
is its base's again, two resolvers and 10.5 s: on 20b2143 that body was
red (TimedOut at the silent resolver's last query, reported unreached
while three queries to the answering resolver were read), which was the
defect above and not a property of the test.

The track's two residuals name their owner, the move's node stage after
#777, the design it takes for [ip]'s queue, and what it deletes with it;
the departure line names NUD-04's limit and why NUD-05 still passes.

Readers of the old word: toyos-net-udp's US-049 and US-027, the shard's
ICD-012 test, and the node's resolver. The node's datagram.rs matches
Error::Failed(_) and is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…, the drivers' room and wake (#782) and the SMI_CMD call (#780), into the listeners stage

No file stopped the merge. One file is both sides': the track, where
main's two removed bullets and two added ones (#779) sit in the node's
and stage 3's lists and this stage's lines in stage 5's, merged by git
and read against both parents.

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

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Merge round: origin/main at 5f2657703 (#775 as d5de6fabe, #779, #782, #780) merged into e737052a5; head f0c483ea2, still a draft. No file stopped the merge; nothing was resolved by hand.

The diff proof. git diff e737052a5 f0c483ea2 --stat -- userland/netstack/node toyos-net-shard toyos-dns userland/netstack/src/resolve.rs issues/toyos-has-its-own-network-stack.md Cargo.lock userland/Cargo.lock:

 issues/toyos-has-its-own-network-stack.md |  4 ++--
 userland/netstack/node/tests/name.rs      | 27 +++++++++++++++++----------
 2 files changed, 19 insertions(+), 12 deletions(-)

Not empty, and both differences are main's, from #779 (47d8a6baf):

The node's src, its other tests, the shard, [tcp], toyos-dns, resolve.rs and the lock are e737052a5's byte for byte. git diff --shortstat origin/main...f0c483ea2: 20 files changed, 1943 insertions(+), 102 deletions(-), the stage's 20 and no others.

#779 and #782 under this stage. Nothing it rests on moved: #779's rules read 169.254/16 alone and no source or test of the stage names that prefix; its silent-next-hop rule is a test of [ip]'s with no change of [ip]'s source; #782 is the shipped netstack's drivers, which nothing here depends on.

Exits at f0c483ea2 (macOS, loaded: 1-minute load average 35 to 107 as sampled), tree clean before and after:

step exit
cargo run -- --build-only 0
cargo metadata --locked 0
cargo test --locked -p toyos-net-tcp (held 6, take 5) 0
cargo test --locked -p toyos-net-shard (2 unit, acquisition 5, drr 6, egress 19, icmp 4, log 3, net 5, udp 3) 0
cargo test --locked --manifest-path userland/netstack/node/Cargo.toml (datagram 5, host 1, lease 19, listeners 28, name 13, resolve 23, slirp 3, streams 34) 0
cargo test --locked -p toyos-dns (44) 0
cargo test --locked -p toyos-net-ip 0
clippy, adopted lints, -D warnings: [tcp], the shard, the node, toyos-dns 0, 0, 0, 0
cargo run -- --ci host (Host: 78 step(s), all green) 0

tests/host.rs here: a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok, so macos: (true, false). The Linux reading is still the host job's.

Mutations not run again: the merge touches none of the eight files the 39 patches name, and all 39 pass git apply --check at f0c483ea2. No guest test run: against main the branch is the stage's 20 files, and no shipped package depends on the node, the shard or [tcp].

The title still says "stacks on #775"; the brief fenced this round to the body.

@Japabu Japabu changed the title toyos-net-node: listeners on the own stack's accept queue, and one bound on the streams, listeners and datagram sockets clients make the node hold (stage E, stacks on #775) toyos-net-node: listeners on the own stack's accept queue, and one bound on the streams, listeners and datagram sockets clients make the node hold (stage E) Oct 9, 2026
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

The orchestrator's call on the merge at f0c483ea2: the review's condition was that the merge leave the reviewed stage unchanged. The restricted diff shows two files, the track (+2 -2, both stages' lines standing) and the node's tests/name.rs (+17 -10), each byte-identical to main's and brought by #779; neither is this stage's work. The condition is met. Marked ready; it lands on host and guest / suite read at this head.

@Japabu
Japabu marked this pull request as ready for review October 9, 2026 01:53
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI at f0c483ea2, read from each job's own log (the orchestrator). host (Linux): [ci] Host: 79 step(s), all green; the node's lease 19, listeners 28, host 1 and the stream crate's held 6, each 0 failed. The Linux reading of tests/host.rs: a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok, and the test asserts (true, false) on whatever host runs it, so Linux answers as the Mac did: a connection has the option its listener had when the connection began, and one set once it waited to be accepted does not reach it. That is the rule the stage implements, so no round is owed and nothing goes to the owner. guest / suite: test result: ok. 36 passed, 36 total, no FAIL line. Queued.

@Japabu
Japabu added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 3fd6a2a Oct 9, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-ownstack-e branch October 9, 2026 02:33
Japabu added a commit that referenced this pull request Oct 9, 2026
… pipe ABI's broadcast permission

One conflict, in issues/toyos-has-its-own-network-stack.md. The stage line takes main's list of what is in the tree, listeners and the places, and keeps this branch's broadcast permission as what is still to build before the move. Among the node's open lines main's five new ones and its rewritten 100 s line stand, and the line on a reset stream's options keeps this branch's wording, since libc's getsockopt of TCP_NODELAY answers from its socket's entry here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Japabu added a commit that referenced this pull request Oct 9, 2026
…ers (#777), the drivers' room and wake (#782), the SMI_CMD call (#780) and the guest waits (#786), into the resolver rules

One conflict, in issues/toyos-has-its-own-network-stack.md, resolved by hand
with every line of both sides accounted for:

- The stage 5 paragraph: main's "In the tree", which now names streams,
  listeners and the one bound, and this branch's "Still to build on it",
  less the streams and listeners that landed: datagram senders that wait on
  the hop, then the move.
- "What the node does not yet meet": this branch's two lines stand in place
  of the two lines they rewrote, which main had left as they were; the line
  on an answer to a 169.254/16 asker, which #779 deleted with the defect, is
  gone, as is the line on a silent next hop, which #779 deleted with its
  test (no conflict).
- "What stage 3 departs from its specifications": both sides added a line at
  the list's end; both stand, this branch's on NUD-04 and #779's on the
  scenarios the readers' specification lacks.

Every other file merged without a conflict. toyos-dns/src/lib.rs and
userland/netstack/node/src/resolve.rs are byte-identical to b2d9037.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Japabu added a commit that referenced this pull request Oct 9, 2026
One conflict, the track's stage-5 paragraph: this branch named datagram
senders that wait on the hop as still to build, #783 named the datagram
sockets' broadcast permission. Both stand, in that order, before the
move. The clause that ordered the first after #777 is met and is
restated as where the work lies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Japabu added a commit that referenced this pull request Oct 9, 2026
…kes them and its accept answers its stream's

The round-1 review of the listener's-option stage found three defects and
this answers them.

A bind's request ends in the TcpOptions an accept's answer ends in, the
shell's Listening is made with them and opens its first socket with them
before the socket is in the set, and libc's bind names its socket's
TCP_NODELAY there: the second request libc sent after the bind's answer,
and its close-and-fail unwind, are gone, and with them the frame in which
a SYN could begin a connection without an option set before bind.
toyos::net::tcp_bind keeps its signature, which the socket2 and mio forks
call, and stands on tcp_bind_with; the renamed issue's exit asks for one
call once the forks pass their own.

The node's listen takes the listener's nodelay, written to [tcp] in the
call that made the passive open, and Accepted carries the option its
stream was handed over with, which the node had in hand in take and
dropped. Three tests of the node: a listener holds what its listen named
from its first connection; a handshake reset before it ends leaves the
next connection the option its listener holds when its own SYN arrives,
both ways round; and an accept whose own pass lets its stream go still
answers the stream's option, where Node::nodelay of its id answers None.
The track's node line is closed by them.

The three tests added on a smoltcp socket are deleted: no new test is
built on the stack being replaced. The shell's rule differs from the
host's for the connection after a handshake its peer reset, since smoltcp
puts a socket reset in SynReceived straight back to Listen with the Nagle
switch it had; that is filed, read from the two sources and not run, with
the move as its exit, and the shell's module header says it.

run_test_hooked lost its last caller with a hook and is deleted;
nodelay_accepted's arm for other hosts, which nothing in the tree built,
is deleted; libc's accept writing its caller's address before
alloc_socket can fail is filed; the renamed issue's Linux paragraph is
struck, the host check's log of #777 on ubuntu-24.04 being that reading.

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