Repository navigation
Conversation
An empty commit to open the draft pull request on. Its body is stage 3's design and its roast, against issues/netstack-moves-every-byte-through-two-kernel-pipes.md and #863's rulings. It asks three rulings before anything is built: whether accepted streams move onto the region in this stage, that mio and socket2 reach a stream through std because a Rust program links two instances of `toyos`, and that toyos-transport and toyos-untrusted join std's dependency graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Collaborator
Author
|
Rulings on R1-R3, and acceptance.
The track's text moves with these in the build's diff. Accepted: build stage 3. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this round is
The design round of stage 3 of
issues/netstack-moves-every-byte-through-two-kernel-pipes.md(the track #863 landed). One empty commit; nothing is built until the orchestrator accepts this design in a comment here. The track's design, its amendments A to E and its rulings 1 to 14 (#863's body and thread) are taken as accepted; this body is how stage 3 lands them in the code, the three places where landing them reaches past what the track says, and the roast of that.Stages 1 (#867,
867ec23ed:fstat'sDeviceMemory,SharedMemory::lent) and 2 (#878,4ca63a698:toyos-transport'sstream.rs) are onmainat52e32385d, the base of this branch. Nothing stage 3 needs is unbuilt.No kernel addition. The data path stays userland's, the owner's constraint on every stage: the kernel makes and maps the region and carries doorbells and hang-ups on pipes, as it does today. Under ruling R1 below, the kernel loses
SYS_CONNECTION_JOINhere rather than in stage 4.Three rulings this design asks for
R1. Accepted streams move onto the region in stage 3 (recommended), or stay piped until stage 4. The track keeps [tcp]'s heap storage "for stage 4's passive children" and puts the accept's copy into the region, and its two listener cases, in stage 4's exit. Read literally, stage 3 makes only a connected stream a region, so every client layer carries two kinds of stream for one stage: std's
TcpStream, libc's socket entry, mio's and socket2's arms and netstack'sserve.rskeep the piped stream beside the region one;TcpShutdown,TcpSetOption,TcpClosefor streams andmake_socket_fdstay for accepted streams alone; and stage 3's handle rules cover only half the streams.TcpStream::from_raw_fdof an accepted stream's handle would panic with "no end in this process", sincetoyos::netenters only region ends.MsgType::TcpAccept { listener }carries a fresh region, netstack copies what the passive child holds in [tcp]'s heap storage (in-order text, out-of-order ranges, the FIN) into the region's receive ring at its offsets before it answers, as amendment C designed, and answersTcpAcceptResponse { remote_addr, remote_port, local_port, nodelay }with the bell (ruling 6). The accept still takes the listener's wake first, as std, libc and mio do today, so no region is made before a child is there (ruling 5 holds as it holds today). Stage 4 keeps the listener's connection in place of its wake pipe, the datagram socket and the*Pipedbind messages. Stage 3's exit then takes stage 4's two listener cases (a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_endsover the region, and ruling 1's text-past-a-hole-and-FIN case with its three controls).make_socket_fdloses its last caller, soSYS_CONNECTION_JOIN,ConnectionEnd::joinedand their abuse tests go here, as dead code goes.The rest of this body assumes B. Under A, everything below applies to connected streams only, and the piped path stays as it is.
R2. mio and socket2 reach a stream through std, not through their own
toyos::net. The track says std, libc, mio and socket2 "read and write the rings throughtoyos::net", and amendment E keeps one table of the process's ends there. But a Rust program links two instances oftoyos: std's, built withrustc-dep-of-std, and the one a crate that namestoyoslinks (issues/a-rust-program-holds-two-copies-of-its-stream-state.md). mio and socket2 nametoyosdirectly. A stream mio connects would be entered in mio's instance, and std'sfrom_raw_fd,OwnedFd's drop,File::from_raw_fdand the spawn checks, all in std's instance, would not see it. So two instances would hold two tables, and the refusals would answer wrongly in both directions. The design: the one end table is std's instance's, whichtoyos::netholds. mio's and socket2's ToyOS arms hold astd::net::TcpStreamand read, write, connect and shut down through it, as their unix arms do.std::os::toyos::net(insdk/std/os/) gives them what std has no word for: a stream's two doorbell handles to watch (the connection forREADABLE, the bell forWRITABLE) andregister(interest), which runs amendment D's arm and ring. libc is one instance in a C program and usestoyos::netdirectly. A Rust program that callstoyos::net's stream API itself reaches its own instance's table; the module header says so, and std is the only Rust caller in the tree.R3.
toyos-transportandtoyos-untrustedjoin std's dependency graph. #863's unsure item. The client end istoyos-transport's code, which std reaches throughtoyos. Both crates become published SDK crates: they joinbuild/sdkversion.rs'sPUBLISHEDbeforetoyos, gain therustc-dep-of-stdfeaturetoyosandtoyos-abihave, and joinbuild/sysroot.rs'sstd_patch,SYSROOT_SOURCESandSYSROOT_MANIFESTS. Therust/fork'slibrary/Cargo.lockgains both. The sysroot's key then moves when either changes, which is right, since std links them. The alternative, a second copy of the ring's decisions insidetoyos, would be a sibling ofstream.rs, and is refused.The design, layer by layer
toyos-transport(lib/blockring/transport): the session's decisions, generic and loom-modelledstream.rsgains amendment B's ring-back, as netring: a session region replaces the pipes between netstack and its clients (design, and the track that stages it) #863 says it would:Consumer::ring_back(page, ended) -> boolandProducer::ring_back(page, ended) -> boolexchange the peer's want for 0 and answer whether to ring, but only when the direction is ready: bytes past the head, orended, for a consumer's peer; room, or a fatalended, for a producer's peer.A new
session.rs: a duplex byte stream of two rings and twoendwords, overNwords and two doorbells. Nothing in it is network-specific. Anendword is 0 while open;ORDERLYis the producer's last byte published; any other value is a failure that also makes the other direction ready to every look (ruling 2). The protocol says what the values mean.Word, aBellstrait (drain this direction's doorbell, wait on it until a deadline, ring the server) and aLocktrait. The sequences are amendment B's: a direction's end is held to drain, arm, look and copy, and never across a wait. A waiter counts itself in before it drains and out when it returns. A thread that took the doorbell, by a wait or a drain, and leaves the end with another waiter counted arms again, looks, and rings the server if the direction is ready.WouldBlockis answered only after the drain, the arm and the second look.writeloads the receiveend(Acquire) before it copies, and a failure is its answer with nothing copied (ruling 3).registerarms each named want, looks, and rings the server if a named direction is ready (amendment D); it drains nothing and counts nothing.rungexchanges each client want whose direction is ready for 0 and answers which doorbells to ring (amendment B);fail(code)stores the receiveend(Release) after the last tail, then exchanges both client wants for 0 and answers each that was set (amendment A, ruling 2); publishing, releasing and the orderlyendanswer their ring asstream.rsdoes.Spans it is handed, and blocks only inside itsBells::wait. The crate still maps, copies and blocks nothing itself.The three exit models are
tests/loom.rscases that drive the sequences above. Their bells are loom models of a pipe (a byte count under a loom mutex, read level-triggered, a wait that takes a byte), and their lock is loom's mutex. The kernel'sdeliveranswers a watch by the pipe's level at its look and arms it again if empty; the poll's watch models that.rx_dataand a writer asleep ontx_room, while the server releases room and publishes data in either order, end with neither waiting. Control: theone-doorbellfeature rings both wants on the connection's doorbell.pollof one direction, the blocking call leaving bytes, the receiveendor room behind, end with the poll answered. Control:no-pass-on.end, and with a fatalendwhile the send ring is full, each made ready before the registration or racing it, the source gets its first event (eight cases). Controls:register-no-arm,register-no-ring, both together, stage 2'swant-relaxed, andhead-only, under which a look reads only the send ring's head and not the fatalend.Each control is a declared feature that no shipping build enables. Each joins
build/ci.rs'sCONTROLSunder its case's name, andbuild/clippy.rsgets a shape for each. Bound: the five preemptions netring: a session region replaces the pipes between netstack and its clients (design, and the track that stages it) #863's models used. The unbounded run of each green case is posted as evidence, with its verdict or its time-out.toyos::net(sdk/toyos/src/net.rs): the protocol, the region's layout and the process's endsSYS_SHM_CREATE. The header is the first 4 KiB: 1,024 words, each one the protocol uses on a 64-byte line of its own. The receive ring is the next 4 MiB (C= 4,194,304), and the send ring the last 4 MiB less 4 KiB (C= 4,190,208). The header's words are the two rings' heads and tails, the four wants, the receiveend(netstack's:ORDERLYis the peer's FIN, thenRESET,TIMED_OUT,UNREACHABLE), the sendend(the client's: shut down for writing), andnodelay(the client's). They are twostream::Places and three words.TcpConnect { addr, port, timeout_ms, nodelay }carries one handle, the region. Its answer isTcpConnectResponse { local_port }with one handle, the bell's read end, or an error.TcpAccept { listener }with a region is answeredTcpAcceptResponsewith the bell (R1). Both go on a connection that is then the stream's.TcpConnectPiped,TcpAcceptPiped,TcpShutdownandTcpSetOptiongo, andTcpClosestays only for a listener. The client writes the region'snodelaybefore it sends the connect, so libc's "nodelay on from the first segment" needs no second request.SYS_FUTEX_WAITandSYS_FUTEX_WAKE, the published design of Drepper's "Futexes Are Tricky" (2011, §"Mutex, Take 2").toyoshas no lock and allocates nothing, and keeps it that way: the table is two static arrays,MAX_STREAMS= 128 ends and 256 handles. That is an estimate, the per-process bound libc's socket table already has (MAX_SOCKETS). Past it a connect or an accept is refusedResourceExhaustedbefore anything is made.Stream::connect,accept,read,write,shutdown_write,set_nodelay,nodelay,register,look,adopt(handle)(a handle the process holds an end for, sharing it; otherwiseRefused::NotThisProcess, named, with nothing written on the connection),duplicate,let_go(handle), andis_stream(handle).adoptandis_streamare lookups and write nothing.ConnectionAborted.[tcp] (
system/netstack/shard/tcp): its buffers are the region's ringsRingkeeps its heap storage and gains a lent one. It holds aStorage, the heapVecit has today or aBox<dyn Lent>, which copies bytes in and out at ring offsets and never hands out a slice of memory a client can write. [tcp] staysforbid(unsafe_code): the copy through the mapping is netstack's adapter's.placewrites an out-of-order segment's payload at its offset in the lent ring, andcommitadvances its tail. The window is the ring's room as [tcp] counts it, so the client's head is [tcp]'sconsume. netstack reads the head and hands [tcp]after_read(n), as the user's read does today, which keeps the capacity rule ofrx's module: it grows by what the client reads, fromRECEIVE_BUFFER_INITIALup to the configured buffer, never past the 4 MiB ring.queued(n)counts them in with no copy, and an acknowledgment'sconsumeis netstack's head store.Exit::sendgets a payload it copies out of the ring into the segment once, before the checksum. A client that rewrites a byte after publishing it corrupts only its own stream (netring: a session region replaces the pipes between netstack and its clients (design, and the track that stages it) #863's hostile-client section).Tcp::lend(conn, rx, tx)moves a passive child onto lent storage, copying what its heap holds to the same offsets, the out-of-order ranges included, and the FIN stays [tcp]'s state. The heap storage is then dropped.The node (
system/netstack/node) and netstack (system/netstack/src)A stream is a [tcp] connection, its lent rings and its session server end. A pass moves what the frames and the client's doorbells changed:
rx_data;after_readand release room, ringingtx_room;nodelayand the sendend, which is the FIN once [tcp] has segmented to that tail;fail(code).The node never blocks on a client. Everything it reads from the region is a word it judges, as netring: a session region replaces the pipes between netstack and its clients (design, and the track that stages it) #863's hostile-client section says: a cursor past its bound is a
Violationthat resets that stream alone.netstack's own wants.
tx_datais armed when netstack has taken every published byte and [tcp] has room.rx_roomis armed only while the window last offered is below [tcp]'s update threshold (RFC 9293 §3.8.6.2.2), the one state in which a read owes the peer a segment. Otherwise netstack reads the head on its next pass.The client leaving is the connection's hang-up (
OTHER_END_GONE). The node closes the connection to [tcp] with what the client published, which [tcp] sends before its FIN (RFC 9293 §3.10.4), or resets it when unread text is left (§3.6.1), asclosedoes today. [tcp] owns the orphan from then (ORPHAN_IDLE).OWNERLESS_LIFE,OWNERLESS_PER_PEERand the cut's clock go: they exist because a pipe cannot say its reader left.netstack's shell:
serve.rstakes a region throughSharedMemory::lent, refusing by name, before anything is mapped, a handle that is no region, one that is not memory the kernel allocated, and one that is not exactly 8 MiB. It makes the bell and keeps the stream on its connection, and watches each stream once, the connection forREADABLEandOTHER_END_GONE, where today it watches two pipes. It drains doorbells non-blocking.WATCHES_PER_PLACEfalls from 2 to 1.PLACE_BYTESbecomes the region and its three doorbell pipes' pages, 14 MiB, untilissues/a-pipe-is-a-two-mebibyte-page-for-a-mapping-nothing-makes.mdcloses. The region is the client's allocation, and netstack's mapping and handle keep it alive, so it is counted as a place, as the pipes are counted today.std, the
rust/fork, libc, mio and socket2TcpStream(sdk/std/sys/net/connection.rs) is a handle entered intoyos::net, with the timeouts and the nonblocking flag it holds today.read,write,shutdown,set_nodelay,nodelayandduplicatecalltoyos::net.take_erroranswers the receiveend's failure.make_socket_fd,ended,RESETandstream_refusedgo, and so dotoyos::net'sread_end,write_refusedandStreamRefusal.sdk/std/os/io.rs, ToyOS's own file, whereAsRawFd for TcpStreamalready is):FromRawFd,IntoRawFd,From<OwnedFd>andFrom<TcpStream> for OwnedFdforTcpStream.from_raw_fdpanics withadopt's refusal.into_raw_fdleaves the entry for the number it gives out.File::from_raw_fdasksis_streamand panics with the refusal if the handle is a stream's.is_streamof every handle it would put in a child's slot byStdio::inherit(), aFileorinherit_handleinto any slot, and of every handleendoworprovidewould move. It answers the refusal before anything moves, with no child started.OwnedHandle::from_raw's# Safetyexcludes a handletoyos::netanswers for, sosend_handlesrefuses one by its type (ruling 12).rust/fork: a commit on its branch changes the bodies of the two existing ToyOS arms inlibrary/std/src/os/fd/owned.rs.OwnedFd's drop callstoyos::net::let_go, andBorrowedFd::try_clone_to_ownedcallstoyos::net::duplicate. Each is a lookup and then the raw syscall for a handle that is no stream's. The commit addstoyos-transportandtoyos-untrustedtolibrary/Cargo.lock(R3). The gitlink moves in this pull request.sdk/libc/src/socket.rs): a stream's entry holds its handle in place ofrx_fdandtx_fd.connect,accept,send,recv,shutdown,setsockopt(TCP_NODELAY)andclosecalltoyos::net.recvanswers eachendin its POSIX word:ECONNRESET,ETIMEDOUT,EHOSTUNREACH. libc'spollof a socket asks the rings first, then counts in, drains, arms and looks again under each direction's end (amendment B), and watches the connection forPOLLINand the bell forPOLLOUT.toyosbranch, and a lockfile bump. mio'sTcpStreamwrapsstd::net::TcpStream; it registers the connection forREADABLEand the bell forWRITABLE, and runsregisteron eachregisterandreregister. Its listener's accept takes the wake and calls std's accept. socket2's ToyOSSocketholds a stdTcpStreamonce connected, or accepted, in place of its own table's joined handle.What it deletes
netstack's
src/pipes.rsToClient/FromClienthalf, the node'sToClient,FromClient,ReadRefusal,Pipes,PipeEnd,pipe_gone,pipe_broken, the end ordering, theCHUNKread,drained, and the ownerless clock.serve.rslosesdata_pipes, two watches a stream,TOKEN_FROM_CLIENTandTOKEN_TO_CLIENT.toyos::netlosesTcpConnectPiped,TcpAcceptPiped,TcpShutdown,TcpSetOption,read_end,write_refusedandStreamRefusal. std losesmake_socket_fdand the empty-write probe. Under R1-B the kernel losesSYS_CONNECTION_JOINandConnectionEnd::joined. The listener's wake pipe and every datagram pipe stay for stage 4.Issues this reaches:
netstack-spends-two-poller-slots-per-piped-connection.mdandnetstack-watches-both-pipes-of-every-connection-on-every-pass.md(a stream is one watch, and a datagram socket is still a pipe until stage 4), anda-streams-failure-reaches-its-client-as-a-reset-whatever-it-was.md(its exit names a pipe's word, which this does not build). Each is touched only if its own exit is met. The track records stage 3 done.Checks: each exit item, its tier and its control
toyos-transporttests/loom.rsone-doorbellend, roomno-pass-onregister-no-arm,register-no-ring, both,want-relaxed,head-onlywritewith room after a reset:ConnectionReset, nothing copiedtoyos-transportunit test of the client endendinto_raw_fd,From<TcpStream> for OwnedFd,try_clone_to_ownedtaken back share the one end and its cursorstests/guest/rust-teststoyosand stand on the kernel's handle table; a fake netstack, a port of the test's own served to a child asnetstack, asnetstack_gone_mid_bind.rsdoes, plays netstack's side withtoyos-transport's server end, so no NIC or peer is needed, and reads its end of the connection to see nothing writtenFile::from_raw_fdof a stream's handle panics with the refusalfrom_raw_fdtaking it silentlyinherit_handlepast the stdio slots,endow,provide: refusal, no child, the stream reads and writes through the handle afterendowandprovide, the handle moving and its entry stayinga_listener_answers_a_syn_...over the region; text in order, past a hole and a FIN before the accept, the hole filled aftersystem/netstack/node/tests/listeners.rspipe_copy_bytes< 0.01 per byte downloaded, C0, hold and spin per MB beside the scout's rowsinternet_downloadwith the scout's instrument (git diff 777c96b2b 2f9e090b8) carried onto stage 3's head, less the netstack stamps on the pipe writes stage 3 deletes; never landedThe node's stream tests (
node/tests/streams.rs, 1,482 lines bywc -l, most of them asserting pipe behaviour) are rewritten over a heap-backed region with the realtoyos-transportclient end as the client. A pipe-behaviour case with no region meaning is cut, and the body names each one. Stage 3's negative control for the whole change is the guest suite and the host suite run onmainwith the new tests applied, where the new tests must red. The independent oracles are RFC 9293, POSIX.1-2024 (send,recv,select,accept) and virtio 1.2 §2.9, the track's, and the host's TCP in the node's existing differential tests.Gates the build round runs:
cargo run -- --ci host;cargo run -- --build-only;cargo testfor the guest suite (every boot that starts netstack reaches this: sshserver's tokio listener, the desktop's,netcase); each control byCONTROLS; the T14 image by--metal-readback.The roast
ring.rs,rx.rsandtx.rsat their edges; the node'sstreams.rsandlisteners.rs, with their tests;serve.rs;toyos::net; std'sconnection.rsandos/io.rs; libc'ssocket.rsandpoll; two forks andrust/; the build's publish and sysroot lists. New:session.rsand its models, the end table and its futex lock. Production should come out near even. The piped stream code it deletes is about what the session's client end adds, and the tests grow by the exit's cases.main's release then publishes. Their API becomes a published API at a version that moves with every change, under the SDK's0.N.0+keyrule.toyosan allocator, which no part of the SDK has. A process past the bound is refusedResourceExhaustedby name. The bound is an estimate.OwnedFddrop andtry_clone_to_ownedin every ToyOS Rust program takes the table's lock for a lookup, and so does eachFile::from_raw_fdand each handle a spawn moves (netring: a session region replaces the pipes between netstack and its clients (design, and the track that stages it) #863's roast). Not measured.PIPESlock. On a download, a client read armsrx_dataat most once per empty ring, and netstack ringsrx_roomonly below the window threshold. The T14 boot reads what is left of the lock.toyos::net. A Rust program that drivestoyos::net's streams itself has a second table that std cannot see. Nothing in the tree does that, and the module header says so.What I am unsure of
ConnectionAbortedfor a stream whose netstack went away: it is neither the peer's reset nor its FIN, and std has no closer word.MAX_STREAMS= 128 is the right fixed bound, or whethertoyosshould takeallocfor this one table.rx_roomarmed only below the update threshold costs the receive autotuning anything on the T14. It reads the client's reads at the next pass, not at the read. The boot shows the throughput beside the scout's 785.95 Mb/s.🤖 Generated with Claude Code
https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy