Repository navigation
netstack: a listener and a datagram socket are their holders; no request names a socket by a number - #1052
Conversation
…request names a socket by a number netstack named listeners and datagram sockets by u32 ids that any client holding the `netstack` connector could close, accept on or receive on (issues/netstack-socket-ids-are-ambient.md). Each is now what a stream already is: an object named only by what its holders hold. - A listen or a bind is a request on a new connection, which becomes the socket's first holder and is answered with a bell netstack made. Every later request is a frame on a holder's own connection, acting on its own socket: ACCEPT (with the stream's region, answered with the stream's connection and bells), SEND and RECEIVE (one datagram per frame each way), SET_OPTION, OPTION and EXPORT. No frame carries a socket id; a holder frame on a new connection is no request there, and a frame a holder's kind does not send is its leaving. - The node keeps each socket's holders (holders.rs): every holder is rung while a connection or a datagram waits and it has not asked since it was last rung, so all holders take from one queue, the loser of a race is answered ERR_WOULD_BLOCK, and no ring is lost. A holder past the first takes a place; the socket ends with its last holder. The per-connection wake count, the wake-pipe refusals that ended a listener, the client-made notify and data pipes and the waiting receive are gone. - An export makes another holder through netstack's private port, its first frame socket::ADOPT. An exported end now says what it holds, so a descriptor's kind is one `fd:<n>:exported` for streams and sockets alike (net::adopt_exported); std's spawn and fd::copy export listeners and datagram sockets as they did streams. - toyos::net::socket keeps the process's ends in the table stream.rs now shares (net/ends.rs). A descriptor's handle is a copy of the bell, which pollers watch (mio unchanged) and no file call reaches. A listener holder's connection keeps TRANSFER: the kernel carries an accept's region only on a connection with it, and no descriptor names that handle. - std's TcpListener and UdpSocket and libc's sockets sit on it: UDP gets real nonblocking and read timeouts, broadcast reads netstack's value, and libc's bound socket is netstack's object, so posix_spawn hands it on. Issues: netstack-socket-ids-are-ambient, an-accept-that-never-reaches-netstack-strands- its-listener (a ring is read only just before a request netstack then answers) and netstack-holds-a-pending-request-for-a-client-that-left (no receive waits in netstack) are closed; only-a-connected-stream-is-shared-with-a-child is renamed to what is left. Tests: node holder laws (one queue, no lost ring, last holder, places), the holder decoder's boundary, datagram framing; netstack_holders_exported_to_children runs the stream export beside a new job in which a child accepts on and exchanges datagrams over sockets handed to it, then its parent, through a forwarded QEMU port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1: PR #1052 at f12211eArchitectureThe security fix holds. No request names a listener or a datagram socket any more. A socket is now the set of connections that hold it, and each frame acts only on its own holder's socket.
The protocol lives in Boundary checks I traced:
The three flagged decisions:
The closed records are correct. Size. Overall +732 lines (2605/1873). Outside the test directories it is +540, including in-file unit tests. Test directories add +281 and issue records −89. FindingsBLOCKER 1:
BLOCKER 2:
BLOCKER 3: mio fork
BLOCKER 4 (PR description; the orchestrator's to correct): "Net −150 lines" is false. The branch is +732 overall and +540 outside the test directories. Evidence gap (not a code defect). The untested cases
Suggestions
SEND BACK |
…eer is netstack's, and mio's comments say so - An accept or a receive asks netstack only once its holder's bell rang, or once it read a ring and its request did not go (a flag per end, under the end's turn): an empty bell waits or answers WouldBlock with no request and no region. An export rings its new holder at once of what waits, so no pass need come before its first look. - A datagram socket's peer is [udp]'s connected socket: a holder's CONNECT, PEER and SEND_PEER frames, so every holder shares the peer and its filter, and a connected socket's pending ICMP error rings and is answered once (`Refused::ConnectionRefused`, `Refused::Unreachable`). The process-local peer goes from toyos::net::socket and libc; libc's connect of an unbound datagram socket binds it, as a first send does. - The mio fork's listener comments describe the bell (5d013b8); the lock names it. - socket_export races the parent and a second child, both blocked in an accept and then a receive: each takes one connection and one datagram, and the harness checks the job's claim lines. - The review's suggestions on the issue records, the forward's race, and a name in serve.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2: PR #1052 at 2222561 (follow-up to r1)ArchitectureRound 1's design points are resolved in the right places:
Size. This round is +430/−153. FindingsBLOCKER 1:
BLOCKER 2:
Checked and fine
Suggestions
SEND BACK |
…eive's error rings again - A look reads its bell's readiness without reading it, makes the stream's slot and region only when it rang, and reads the bell empty just before the request goes. A look that fails before its request leaves the ring where mio and libc's poll see it; the in-process kept-ring flag goes. - A datagram receive tells the holders whatever it answers: a pending ICMP error answered once no longer strands the datagram behind it. `a_pending_error_is_answered_once_and_the_datagram_behind_it_rings_again` holds it and was red before the fix; the ICMP builder moves to the tests' `lan` module for it. - The review's two wording suggestions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r3: PR #1052 at 032114b (follow-up to r2)ArchitectureThis round fixes both blockers and leaves the design unchanged. The client-side state is now just the bell. Blocker 1 (kept ring lost to pollers): resolved
Blocker 2 (ICMP error stranding a datagram): resolved
Evidence
FindingsNo blockers. SUGGESTION:
SUGGESTION:
LAND |
A look made and closed a kernel poller each time, so under memory exhaustion a nonblocking receive or accept panicked in `Poller::new` instead of answering. Each end now makes its poller with itself, held in its turn, and a look that does not wait asks the kernel for nothing it can refuse; making an end refuses `ResourceExhausted` where the kernel makes no poller. A blocking wait, which runs outside the turn the end's other calls take meanwhile, makes its own with `Poller::try_new` and answers `ResourceExhausted` where it cannot. The module's no-lost-ring sentence now names what it covers: a call that fails before its request goes. `issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md` records round 2's recurrence of that harness wedge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Landing at e5f90fd. Review r3 returned LAND, and I required one last change: each socket end makes its poller with the end, so a non-blocking call can no longer panic under memory exhaustion (a blocking wait uses |
The T14's ToyOS osbench boot at f644cef killed `osbench lan` at the list bound, 321 s into a group whose discovery is bounded at 60 s: with no peer on the link its `recv_from`, under a 1 s read timeout, never returned (issues/std-udp-socket-ignores-its-timeouts.md). That was netstack parking a datagram receive until a datagram came. #1052 has since made a datagram socket its holder's own and std's receive wait on it with the read timeout, which is that issue's fix; its exit asked for a guest test with no answering peer, and this is it. `udp_receive_times_out`, on netcase's real netstack: two receives under a 300 ms timeout with nothing sent each end `WouldBlock`, std's Unix word for a receive timeout, after at least 300 ms and inside a 30 s hang ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
netstack named listeners and datagram sockets by
u32ids. Any process holding thenetstackconnector could close, accept on or receive on another process's socket by naming its number (issues/netstack-socket-ids-are-ambient.md; a service swap once closed the wrong socket and crashed netd).Resulting behaviour
A listener and a datagram socket are now what a stream already is: objects named only by what their holders hold.
ACCEPT(with the stream's region; answered with the stream's connection and bells),SEND/RECEIVE(one datagram per frame each way),SET_OPTION,OPTION,EXPORT. No frame carries a socket id. A holder frame on a new connection is no request there (ERR_INVALID_INPUT); a frame a holder of that kind does not send drops the holder.WouldBlock, with no request and no region. A rung look makes its slot and region and reads the bell empty just before the request goes, so a look that fails first leaves the ring where pollers see it. An export rings its new holder at once of what waits.system/netstack/node/src/holders.rs). A holder is rung while a connection or datagram waits and it has not asked since it was last rung. All holders take from one queue; the loser of a race getsERR_WOULD_BLOCK(NetError::WouldBlock). The client reads its bell only just before a request netstack then answers. A holder past the first takes a place; the socket ends with its last holder.exportmakes another holder through netstack's private port; its first frame issocket::ADOPT. An exported end now says what it holds, so one descriptor kind,fd:<n>:exported, covers streams and sockets (net::adopt_exported). std's spawn andfd::copyexport listeners and datagram sockets as they did streams, so a later libc fork can use the same path.toyos::net::socketkeeps the process's ends in the tablestream.rsnow shares (net/ends.rs). std'sTcpListener/UdpSocketand libc's sockets sit on it. UDP gains real nonblocking and read timeouts;broadcast()/getsockoptread netstack's value; std gainsFromRawFd/From<OwnedFd>for both types. A libc socket that binds becomes netstack's object, soposix_spawnhands it on. mio's code needs no change, since a socket descriptor's handle is its bell; its listener comments are corrected in fork commit 5d013b8, which the lock names.Deleted: the id table and its random start, the client-made notify/data pipes, the per-connection wake count, wake-pipe refusals ending a listener, and netstack's waiting UDP receive. The branch adds about 730 lines overall, 540 of them outside the test directories.
Decisions worth review
TRANSFER. The kernel carries handles only on a connection withTRANSFER, and an accept carries its region. The stream rule (holder handles carry noTRANSFER) still holds for the bell, which is a socket descriptor's handle, and for a datagram socket's connection. No descriptor names the listener's connection handle. The first guest run went red on exactly this: the child's accept failedPermissionDenied.netstack-moves-every-byte-through-two-kernel-pipes' stage 4 planned a region per datagram socket. I rewrote stage 4 to say what is left: the region, since a datagram is still copied through the connection's pipes.CONNECTconnects [udp]'s socket, so the peer and its receive filter are the socket's and every holder's;PEERreads it andSEND_PEERsends to it. A connected socket's pending ICMP error rings its holders and is answered once. libc's connect of an unbound datagram socket binds it, as a first send does.netstack-socket-ids-are-ambient;an-accept-that-never-reaches-netstack-strands-its-listener, whose mechanism (a spent wake netstack still counted) is gone; andnetstack-holds-a-pending-request-for-a-client-that-left, since no receive waits in netstack.only-a-connected-stream-is-shared-with-a-childis renameda-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child, with what remains. Other records naming the old protocol are updated.Checks (revision f12211e, macOS host)
cargo run -- --ci hostcargo run -- --build-onlycargo test --test toyos-build -- netstack_holders_exported_to_children stream_holders libc_children iommu_virtio_platformcargo test --test toyos-build -- netstack_holders_exported_to_childrenafter the TRANSFER fixa_holder_that_never_looks…,every_holder_takes_from_one_queue…, datagram queue test…), restoreda_holder_asks_only_its_own_kind_of_socketred, restoredLogs: https://github.com/ToyOSOrg/evidence/tree/c6ba67bde5f02cb735d9150995095804562df7e5/f12211e8f (
ci-host-1,build-2,guest-2the red run,guest-3the green one,m1,m2,metal-request.txt).The first holders-row run is also its negative control: a child whose accept failed turned the row red.
New host tests: node holder laws (one queue across holders, a never-looking holder costs the other nothing, last-holder close, places), datagram socket holders (each datagram once and whole), the serve boundary (
a_new_connection_names_no_socket,a_holder_asks_only_its_own_kind_of_socket), and datagram framing round trips.New guest job
socket_export, in the renamed rownetstack_holders_exported_to_children. It shares one netcase boot withstream_export. A child accepts on a listener and exchanges datagrams over a socket handed to it; then the parent does the same through its own holders. The row needs a guest because export, endowment, adoption and real netstack cross real processes. The harness is the peer through a newBootOptions::forward(a QEMU user-nethostfwdof one loopback port; refused on a profile with no NIC). It runs on QEMU x86-64 only (SUITE_ARCH) and touches no public network.The swap case is not rebuilt:
swap_crash_rolls_backand netd are gone, and no request can carry a stale id any more.Metal
None of the claims need metal: they are about order, contents and counts, with no timing or device behaviour.
netstack_gone_mid_bind, which I edited (its two arms now use a connect's and a listen's shapes), runs only in the T14's shared boot. It is staged with--metal-readback.Review r1 (head 2222561)
All three blockers fixed as above. Checks at 2222561:
cargo run -- --ci hostexit 0, 103 steps;cargo run -- --build-onlyexit 0;netstack_holders_exported_to_children,stream_holders,libc_childrenpass.socket_exportnow also races the parent and a second child, both blocked in accept and then recv: the harness connects twice and sends two datagrams, each process takes one of each, and the harness checks the job's claim lines. New host tests: connected peer shared by holders, a new holder rung at once (mutation dropping that ring:a_holder_made_while_a_connection_waits_is_rung_at_oncered, restored). Logs: https://github.com/ToyOSOrg/evidence/tree/f0d7342f5e2ef6b6f36192d22f3e8206277678ef/22225610b. Metal request restaged at this head.Review r2 (head 032114b)
Both blockers fixed as above; a datagram receive also rings the holders whatever it answers, so a pending ICMP error no longer strands the datagram behind it.
a_pending_error_is_answered_once_and_the_datagram_behind_it_rings_againwas red before that fix (r2-before-fix.log: the asker's ring count stayed 1). Checks:cargo run -- --ci host102 of 103 green, the red one clippy's unusedsumimport innode/tests/resolve.rs, fixed and rechecked withcargo clippy -p toyos-net-node --all-targets -- -D warnings(exit 0);netstack_holders_exported_to_children,libc_children,iommu_virtio_platformpass;stream_holderspassed on its own rerun after the combined run's worker sat 20 minutes on a sysroot lock its own process held, unrelated to this change. Logs: https://github.com/ToyOSOrg/evidence/tree/7ed116016badcda0e4821ae4e5f99921fb0feb22/032114b1d. Not restaged for metal: neither fix touchesnetstack_gone_mid_bind's path.Review r3 (head e5f90fd)
Each socket end makes its own poller when the end is made, and keeps it in the end's turn. A look that does not wait asks the kernel for nothing it can refuse, and making an end answers
ResourceExhaustedif the kernel makes no poller. A blocking wait runs outside the turn, which the end's other calls take meanwhile, so it makes a poller of its own with the newPoller::try_newand answersResourceExhaustedwhere it cannot. The module's no-lost-ring sentence now covers only a call that fails before its request goes. The round 2 sysroot-lock wedge is recorded in the existingissues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md, not as a new issue.Checks at e5f90fd:
cargo run -- --ci hostexit 0, all 103 steps green;netstack_holders_exported_to_childrenandiommu_virtio_platformpass. Logs: https://github.com/ToyOSOrg/evidence/tree/611bab2b37de1355b663add6094bcde434ff19d0/e5f90fd5f.Risks
MAX_DATAGRAM(1472) is declared intoyos::net::socketbeside udp'sMAX_PAYLOAD, with a compile-time check that the second fits the first.🤖 Generated with Claude Code