From f12211e8fef1c78621a545ba1ead9a391882e141 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 23 Oct 2026 21:33:39 +0200 Subject: [PATCH 1/4] netstack: a listener and a datagram socket are their holders, and no 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::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 --- ...c-program-cannot-order-the-waiting-case.md | 14 +- ...s-not-bound-is-not-shared-with-a-child.md} | 40 +- ...r-reaches-netstack-strands-its-listener.md | 30 - ...rs-an-address-nobody-asked-netstack-for.md | 6 +- ...-pending-request-for-a-client-that-left.md | 23 - ...ves-every-byte-through-two-kernel-pipes.md | 10 +- issues/netstack-socket-ids-are-ambient.md | 37 - ...-forks-so-broadcast-setter-does-nothing.md | 2 +- issues/toyos-builds-itself.md | 2 +- issues/toyos-has-its-own-network-stack.md | 5 +- sdk/libc/src/posix_io.rs | 4 +- sdk/libc/src/socket.rs | 556 ++++++------ sdk/std/os/io.rs | 51 ++ sdk/std/os/process.rs | 13 +- sdk/std/sys/net/connection.rs | 221 ++--- sdk/std/sys/process.rs | 23 +- sdk/toyos/src/fd.rs | 58 +- sdk/toyos/src/ipc.rs | 8 +- sdk/toyos/src/launch.rs | 8 +- sdk/toyos/src/net.rs | 347 ++------ sdk/toyos/src/net/ends.rs | 173 ++++ sdk/toyos/src/net/socket.rs | 495 +++++++++++ sdk/toyos/src/net/stream.rs | 271 ++---- system/netstack/node/src/datagram.rs | 89 +- system/netstack/node/src/holders.rs | 127 +++ system/netstack/node/src/lease.rs | 13 + system/netstack/node/src/lib.rs | 10 +- system/netstack/node/src/listeners.rs | 189 ++-- system/netstack/node/src/places.rs | 27 +- system/netstack/node/src/streams.rs | 10 +- system/netstack/node/tests/common/mod.rs | 26 +- system/netstack/node/tests/datagram.rs | 113 ++- system/netstack/node/tests/lease.rs | 13 +- system/netstack/node/tests/listeners.rs | 237 +++-- system/netstack/node/tests/name.rs | 4 +- system/netstack/node/tests/resolve.rs | 13 +- system/netstack/node/tests/streams.rs | 2 +- system/netstack/shard/src/lib.rs | 10 + system/netstack/src/client.rs | 10 +- system/netstack/src/main.rs | 43 +- system/netstack/src/pipes.rs | 60 +- system/netstack/src/serve.rs | 814 +++++++++--------- system/netstack/udp/src/lib.rs | 10 + system/supervisor/src/main.rs | 2 +- tests/common/qemu.rs | 16 +- .../src/bin/netstack_gone_mid_bind.rs | 56 +- .../guest/rust-tests/src/bin/socket_export.rs | 79 ++ tests/images/netcase.toml | 4 +- tests/toyos.rs | 104 ++- 49 files changed, 2605 insertions(+), 1873 deletions(-) rename issues/{only-a-connected-stream-is-shared-with-a-child.md => a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md} (59%) delete mode 100644 issues/an-accept-that-never-reaches-netstack-strands-its-listener.md delete mode 100644 issues/netstack-holds-a-pending-request-for-a-client-that-left.md delete mode 100644 issues/netstack-socket-ids-are-ambient.md create mode 100644 sdk/toyos/src/net/ends.rs create mode 100644 sdk/toyos/src/net/socket.rs create mode 100644 system/netstack/node/src/holders.rs create mode 100644 tests/guest/rust-tests/src/bin/socket_export.rs diff --git a/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md b/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md index ede3987cd72..cb301559dbb 100644 --- a/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md +++ b/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md @@ -21,9 +21,9 @@ loopback delivers in. A red there prints the host's name and both answers. `ubuntu-24.04`, whose log names `a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok`. -The pipe ABI carries both halves (`sdk/toyos/src/net.rs`): a bind's request -carries its listener's options and `MsgType::TcpListenerSetOption` sets one -afterwards, and an accept's answer says what its connection holds +The protocol carries both halves (`sdk/toyos/src/net/socket.rs`): a listen's +request carries its listener's options and its holder's `SET_OPTION` sets +one afterwards, and an accept's answer says what its connection holds (`TcpOptions`). netstack answers the rule for both by the node's calls (`system/netstack/src/serve.rs`, `system/netstack/node/src/listeners.rs`), and the node takes a listener's option in its `listen` and in @@ -33,7 +33,7 @@ connection took from its listener. The `libc_sockets` guest test that read it `setsockopt` and `getsockopt`) was cut in `ff1a11dbd`. What no program reads: - Whether a C program can set the option while a connection waits and know - that one does. libc's `poll` now watches a listener by its wake pipe + that one does. libc's `poll` now watches a listener by its holder's bell (`sdk/libc/src/posix_io.rs`); no test reads that it answers a waiting connection. - No Rust program can set the option on a listener. `std::net::TcpListener` @@ -47,8 +47,7 @@ connection took from its listener. The `libc_sockets` guest test that read it (`836d14f`) delegates a stream's option to std's. std's `TcpListener::accept` (`sdk/std/sys/net/connection.rs`) takes the accepted stream's option from the answer, which no test can turn red while no std - listener can hold one; `toyos::net` still has two bind calls, `tcp_bind` - and `tcp_bind_with`. + listener can hold one. **Exit condition**: on ToyOS a program that sets `TCP_NODELAY` on a listening socket and accepts one connection begun before and one begun after @@ -56,8 +55,7 @@ reads the option on each as that test has a host answer, by libc's `getsockopt`, with libc's `poll` answering for the waiting listener, and by std's `nodelay()`, which needs a listener a Rust program can set the option on: the socket2 fork's, with the lock naming the commit that hands its -listener's option to netstack in its bind; and `toyos::net` has one bind -call, which takes its options. +listener's option to netstack in its listen. **Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`; the fork commit is on socket2's `toyos` branch. diff --git a/issues/only-a-connected-stream-is-shared-with-a-child.md b/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md similarity index 59% rename from issues/only-a-connected-stream-is-shared-with-a-child.md rename to issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md index f72da0c0b47..cc694356817 100644 --- a/issues/only-a-connected-stream-is-shared-with-a-child.md +++ b/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md @@ -4,22 +4,23 @@ kind: defect opened: 2026-10-12 --- -# Only a connected stream is shared with a child - -A TCP stream is shared by its holders (`sdk/toyos/src/net/stream.rs`): the -process that connected or accepted it, and every process a spawn gave a -descriptor of it, which exports a holder (`fd::stream`, -`sdk/toyos/src/fd.rs`) that the child takes up. Each holder keeps working when -another exits or is killed. A stream's own handle stays in its process: -`endow` and `provide` of one are refused with `Misuse::Moved`. - -Every other socket is not shared: -- Listeners and datagram sockets are named by ambient ids - (`issues/netstack-socket-ids-are-ambient.md`). A spawn passes a std - listener's descriptor as its wake pipe's bare handle, which names nothing the - child can use. -- A C socket that has not connected is libc's entry alone, an object of - libc's own, which a spawn refuses (`NotSupported`). +# A socket libc or socket2 has not bound is not shared with a child + +A TCP stream, a listener and a datagram socket are shared by their holders +(`sdk/toyos/src/net/stream.rs`, `sdk/toyos/src/net/socket.rs`): the process +that connected, accepted, listened or bound, and every process a spawn gave a +descriptor of it, which exports a holder (`fd::exported`, +`sdk/toyos/src/fd.rs`) that the child takes up. A stream's holder keeps +working when another exits or is killed. A stream's own handle stays in its +process: `endow` and `provide` of one are refused with `Misuse::Moved`. + +What is not shared: +- A listener's and a datagram socket's holders are read one after the other by + `netstack_holders_exported_to_children` (`tests/toyos.rs`): the child + accepts and exchanges datagrams, and the parent after it. No test reads + both at once, nor a survivor after the other holder is killed. +- A C socket that has neither bound nor connected is libc's entry alone, an + object of libc's own, which a spawn refuses (`NotSupported`). - A socket2 socket's descriptor is an object of socket2's own, so a spawn refuses it (`NotSupported`), and `Socket::from_raw_fd` of a stream's descriptor answers every operation "invalid socket fd". @@ -44,10 +45,9 @@ so listeners and datagram sockets are shared as streams are. Planning's reading of the evidence (2026-10-21): data a dead holder already consumed need not be recovered. -Order: listeners and datagram sockets as holder sessions, after the pipe page; -then C sockets shared before they connect; the spawn from the inheritable flag -last, since every interim rule for an inheritable socket is rejected or -unapproved. +Order: C sockets shared before they bind or connect; the spawn from the +inheritable flag last, since every interim rule for an inheritable socket is +rejected or unapproved. Owner: the orchestrator, who stages `issues/netstack-moves-every-byte-through-two-kernel-pipes.md`. diff --git a/issues/an-accept-that-never-reaches-netstack-strands-its-listener.md b/issues/an-accept-that-never-reaches-netstack-strands-its-listener.md deleted file mode 100644 index d7bd5d40937..00000000000 --- a/issues/an-accept-that-never-reaches-netstack-strands-its-listener.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# An accept that never reaches netstack strands its listener's owner - -std's `TcpListener::accept` (`sdk/std/sys/net/connection.rs`) reads -netstack's wake byte first, and only then calls `toyos::net::stream::accept`, -whose `open` (`sdk/toyos/src/net/stream.rs`) reserves a slot in the process's -stream table, makes the stream's region (`SharedMemory::create`) and reaches -netstack (`NetstackConn::connect`) before it sends the request. If any of -those fails, or the send does, the accept returns an error with the wake spent -and no request made. netstack spends a wake only on an accept it answers -(`Node::accept`, `system/netstack/node/src/listeners.rs`), so it still -counts the owner as woken, writes no second wake, and the owner's next -`accept` blocks for the rest of the boot while the connection holds the port. - -Read from the code and not reproduced. The table refuses past `MAX_STREAMS` -with `ResourceExhausted`, as the region does when no 8 MiB of 2 MiB pages is -free (`issues/a-stream-commits-eight-mebibytes-and-a-process-holds-128.md`), -and `NetstackConn::connect` fails with `ResourceExhausted` when the kernel's -port queue refuses it. - -**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. - -**Exit**: no failure between the wake and netstack's answer leaves the owner -holding a spent wake netstack still counts, and a test in which that step fails and -the next `accept` still returns the waiting connection. diff --git a/issues/getsockname-answers-an-address-nobody-asked-netstack-for.md b/issues/getsockname-answers-an-address-nobody-asked-netstack-for.md index 8652fd61e24..00418b3a989 100644 --- a/issues/getsockname-answers-an-address-nobody-asked-netstack-for.md +++ b/issues/getsockname-answers-an-address-nobody-asked-netstack-for.md @@ -9,8 +9,10 @@ opened: 2026-09-08 `sdk/libc/src/socket.rs`'s `getsockname()` fills the caller's `sockaddr` with `[10, 0, 2, 15]` and the socket's local port. The address is a literal in that function; netd is not asked, and the SDK has nothing to ask it with — -`toyos::net` carries `tcp_connect`, `tcp_bind`, `tcp_accept`, the UDP calls and -`dns_lookup`, and no call that answers "what is this machine's address". +`toyos::net` carries a stream's connect, a listen, an accept, the datagram +calls and `dns_lookup`, and no call that answers "what is this machine's +address": a socket's `toyos::net::socket::local` is the address its bind +named. It was true by coincidence until now: netd carried the same literal, so the shim and the stack agreed. netd takes its address from DHCP as of the change diff --git a/issues/netstack-holds-a-pending-request-for-a-client-that-left.md b/issues/netstack-holds-a-pending-request-for-a-client-that-left.md deleted file mode 100644 index 6b6a409c087..00000000000 --- a/issues/netstack-holds-a-pending-request-for-a-client-that-left.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-26 ---- - -# netstack holds a pending receive for a client that left - -A UDP receive with no datagram yet keeps its client's connection in -`Sockets::receiving` (`system/netstack/src/serve.rs`) until a datagram arrives. -Nothing watches that connection (`Sockets::watch` watches each stream's -connection, each listener's wake pipe, each datagram socket's pipe and each -waiting lookup's connection), so a client that hangs up is noticed only when -netstack writes its answer: a receive on a socket nothing sends to holds its -entry for the life of the socket. - -A lookup's client is watched and let go at once (`TOKEN_LOOKUP`), and so is a -stream's from its connect: a stream is the connection it was asked on, watched -for its hang-up. - -Exit condition: a waiting receive's connection is watched the same way, and a -test of `serve.rs` that hangs up a pending receive sees its entry freed without -any traffic. diff --git a/issues/netstack-moves-every-byte-through-two-kernel-pipes.md b/issues/netstack-moves-every-byte-through-two-kernel-pipes.md index b76206c0644..1d7c3f93d83 100644 --- a/issues/netstack-moves-every-byte-through-two-kernel-pipes.md +++ b/issues/netstack-moves-every-byte-through-two-kernel-pipes.md @@ -59,7 +59,7 @@ The design and its roast are PR #863's body. on, which then carries only doorbells and the hang-up; std, libc, mio and socket2 reach it through `toyos::net::stream`, and `SYS_CONNECTION_JOIN` is gone. A stream is shared with a child as a holder of its own; other - sockets are not yet (`issues/only-a-connected-stream-is-shared-with-a-child.md`). A sent byte is copied + sockets are not yet (`issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md`). A sent byte is copied twice by netstack, out of the lent send ring into a scratch buffer and from it into the frame, since the frame builder takes one slice; a builder that copies each span of the ring into the frame itself would remove the first. @@ -69,9 +69,11 @@ The design and its roast are PR #863's body. 0.00015) against 2.0029, the pipe lock held 0.179 ms against 0.587 and spun 0.092 against 0.108, C0 7.735 ms against 7.770, 812.7 Mb/s against 824.1 (https://github.com/ToyOSOrg/evidence/tree/7cc1bda0dcb038084de60d65c64f30a3956a13a3/round3). -- Stage 4: a listener's connection carries its wakes in place of its pipe, a - datagram socket is a region as a stream is, and the pipe ABI's messages and - `toyos::net`'s pipe handles go. netstack copies each datagram record's +- Stage 4: a datagram socket is a region as a stream is. A listener and a + datagram socket are already their holders' connections and bells + (`toyos::net::socket`), each datagram one frame on its holder's + connection, and the pipe ABI's messages and `toyos::net`'s pipe handles are + gone; a datagram is still copied through the connection's pipes. netstack copies each datagram record's `len`, `port` and `addr` out of the region once and judges the copy: a `len` past the bytes the record holds ends that socket alone. `toyos::net`'s `send_to` refuses what a sender can judge and writes no record: a payload past diff --git a/issues/netstack-socket-ids-are-ambient.md b/issues/netstack-socket-ids-are-ambient.md deleted file mode 100644 index 2b4566b1187..00000000000 --- a/issues/netstack-socket-ids-are-ambient.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-23 ---- - -# netstack's socket ids are numbers any client can name - -A stream is its connection and is named by no number, but a listener and a -datagram socket are each named by a `u32` id netstack hands out, and every -request that acts on one (close, accept, send, receive) names it by that id -alone (`Sockets::listener` and `Sockets::datagram`, -`system/netstack/src/serve.rs`). netstack checks that the id is in its table and -nothing else: **it does not ask whether the client naming it is the one it was -issued to.** Clients open a fresh connection per request -(`toyos::net::NetstackConn::connect`), so there is no client identity for the -check to be made against. Any process holding the `netstack` connector can -close or accept on any other process's listener, or receive on its datagram -socket, by guessing its number. - -**How it was found.** A service swap (`toyos-swap`) replaced netd, netstack's -predecessor, while its clients still held ids from the netd before. sshd, on -noticing, dropped its listener, which closed the listener's old id, a number -that in the new netd was `logd`'s stream connect still waiting for its SYN-ACK, -and netd's next pass panicked in smoltcp (`tests/swapcase`, -`swap_crash_rolls_back`, one run of four). A client's words crashed netd. - -**What bounds it now.** A stream, a pending connect among them, is not an id -any more, and the table starts at a random id, so two netstacks' ranges overlap -only by chance. None of that is the fix: an id is -still ambient authority over somebody else's socket. - -**Exit condition.** A listener and a datagram socket are each named by -something only their owner holds (a per-socket handle, or the connection that -made it, as a stream already is), and a request naming a socket its connection -does not hold is refused. That is an ABI change to `toyos::net` and netstack's -wire protocol, and lands on its own. diff --git a/issues/the-socket2-forks-so-broadcast-setter-does-nothing.md b/issues/the-socket2-forks-so-broadcast-setter-does-nothing.md index f50b9b26491..a0783d753f1 100644 --- a/issues/the-socket2-forks-so-broadcast-setter-does-nothing.md +++ b/issues/the-socket2-forks-so-broadcast-setter-does-nothing.md @@ -11,7 +11,7 @@ ToyOS, socket2's `setsockopt` (`src/sys/toyos.rs`) matches `(SOL_SOCKET, SO_BROADCAST)` to an empty arm and answers `Ok`, and its `getsockopt` answers 0 for it: a setter that says yes to a permission it hands to nobody, where std's `UdpSocket::set_broadcast` and libc's `setsockopt` send -`toyos::net::udp_set_option`. No send follows it today: the backend holds no +`toyos::net::socket::set_option`. No send follows it today: the backend holds no datagram socket at all, so a program that reaches a datagram socket through socket2 has none on ToyOS. diff --git a/issues/toyos-builds-itself.md b/issues/toyos-builds-itself.md index 08c2b040366..999a747c446 100644 --- a/issues/toyos-builds-itself.md +++ b/issues/toyos-builds-itself.md @@ -90,7 +90,7 @@ An owner is an issue under `issues/`, or this track. | 18 | libc gains `select`, `setvbuf` and `socketpair`. A descriptor is a small table number since #978, so `FD_SETSIZE` holds | not started | this track | — | | 19 | The `libc` crate gains a ToyOS module, checked by `ctest` against `sdk/libc`'s headers | not started | this track | 18 | | 20 | Children: `SIGCHLD`, sessions, a quit notice and Ctrl+C, the process track's stages 4–7 | in progress | `a-childs-end-is-an-event-and-a-parent-takes-its-children-down` | — | -| 21 | Sockets shared by their holders and inherited by a child, which curl, libssh2 and every inheriting server need | direction set | `only-a-connected-stream-is-shared-with-a-child`, `libc-close-of-a-socket-is-unproven-on-a-connected-socket` | engineering: streams shared; listeners, datagram sockets and inheritance next | +| 21 | Sockets shared by their holders and inherited by a child, which curl, libssh2 and every inheriting server need | direction set | `a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child`, `libc-close-of-a-socket-is-unproven-on-a-connected-socket` | engineering: streams, listeners and datagram sockets shared; unbound C sockets and inheritance next | | 22 | cargo's eight C libraries cross-built: curl, libgit2, libssh2, OpenSSL, SQLite, nghttp2, zlib, blake3; five stopped on headers, types and functions libc lacked at `15625e0cb` | not started | this track | 18, 19 | | 23 | cargo's TLS: OpenSSL through its Perl `Configure`, or curl's rustls backend on `ToyOSOrg/ring`. A build from vendored sources needs neither, and neither needs the `toyos-crypto` track | not started, choice open | this track, `the-internet-clients-work-unchanged` | 22; HTTPS on `the-lan-is-not-yet-production-grade` | | 24 | cargo at run time: its SQLite and its locks, sockets and children | not started | this track, `sqlite-and-postgresql-run-on-toyos` | 14–17, 20–22 | diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 67a85773592..58f9af86a01 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -28,13 +28,12 @@ What the node does not yet meet: - The node's places are one number for every client: a program that connects, listens or binds in a loop takes them all, and so does a peer that keeps the connections [tcp] is finishing alive, each of which holds its stream's place (the line below on a client gone with nothing left in its pipe). The node has no word for the program behind a request, as `issues/netstack-lookup-slots-have-no-per-client-share.md` records of netd. Exit: a test of the node in which one program at its share of the places leaves another's connect, listen, accept and bind answered. - A port is one listener's, whatever address either listen named: a listen on a port a listener holds is refused `ListenRefused::InUse` (`system/netstack/node/src/listeners.rs`), so [tcp]'s rule that a SYN goes to the listener that named its address before one at every address (LS-09) is not reachable from the node, and one program's two listeners on one port at two addresses, which a host allows one user, are refused too. The node has no word for the program behind a listen: with LS-09 reachable, any program that reaches netstack would take every later connection of another program's listener at every address by naming the machine's address on its port. Exit: a listen carries its program, a second listener on a held port is allowed only to the program that holds every listener on it, and a test of the node finds one program's pair standing, a SYN handed to the one that named its address, and another program's listen on that port refused. - A listener's handshakes in progress are [tcp]'s `limits::LISTEN_PENDING`, each held until `limits::SYNACK_GIVE_UP`, and a SYN past them is dropped: 256 SYNs a minute from addresses that never answer shut a port to every other peer for as long as they keep coming. [tcp] has no SYN cookie and lets no older handshake go for a newer one. Exit: a test in `system/netstack/shard/tcp/tests/` in which a peer's handshake completes while LS-12's flood runs. -- The node counts a wake unspent until an accept arrives, and cannot tell an accept on its way from one that will never be sent: an owner that reads a wake and sends no accept is owed one wake fewer from then on, and the last connection waiting is not announced. That is the node's half of `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`. Exit: that issue's, by a client that cannot spend a wake without its accept arriving, or by an accept that waits in netd and needs no wake. - How a stream ended reaches its client by the receive ring's `end` word, which names each failure; only a reset is tested: `issues/a-streams-failure-reaches-its-client-as-a-reset-whatever-it-was.md`. Exit: that issue's. - Every batch of received frames and every deadline ends in a pass over every stream, every transmit opportunity walks every stream twice (once for the count each peer address keeps alive, once to pass those whose connect is not answered), and `Node::next_deadline` reads every stream: linear in streams per batch, as netstack's bridge on smoltcp was per frame. For an idle stream a pass is a `status` and loads of its region's words, and no system call. [tcp] cannot say which connections a segment, a timer, an ICMP error or a failed next hop moved: it reports `drain_eligible` and `drain_gone`, which are the round's. The change: [tcp] marks a held connection in `for_conn`, `tick`, `icmp` and `end` and hands the marks out once (`drain_moved`); the node maps a connection to its stream, keeps its deadlines ordered, and passes only the streams [tcp] moved, those a client's ring names, and those due. A client's hang-up (`Node::left`) runs a pass of its own as well, beside the one `Sockets::bridge` runs for a wake's answers, so k clients leaving in one wake cost k + 1 passes. Not measured at the move. The count is what a guest can read and a time is not: netstack's places end at what one poller watches (`MAX_PLACES`, `system/netstack/src/main.rs`), so 1,000 idle streams cannot exist, the T14's bench opens no peer that holds streams, and QEMU gives no verdict on time. Exit: a guest test counts the streams a pass moves per batch received beside one bulk stream, with 1 idle stream and with 100, and the line closes when the two counts are equal. - A client that is gone with nothing left in its pipe is finished by [tcp]'s rules for a user who let go: a reset once it has been idle 60 s, where netd on smoltcp reset it 100 s after the client left whatever the peer said. A peer that acknowledges a byte a minute holds such a connection for as long as it has bytes to acknowledge. Exit: the T14 or a guest shows such a peer holding one, and [tcp]'s rule takes a bound from the client's leaving; or the owner rules the idle bound is the one. - `Tcp::ready`, `Tcp::orphans` and `Tcp::options` are additions to [tcp] the specifications do not hold, made for the node's wakes, its places and the copy a stream keeps of its connection's options; their tests (`system/netstack/shard/tcp/tests/held.rs`) name no scenario id. `Shard::listen` refuses an address [ip] does not hold usable, by the rule [udp]'s bind refuses by (`udp.bind-address-not-local`), and counts nothing for it; the node's test of it (`a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds`) names no scenario either. A listener whose address the machine loses stands as a datagram socket bound to it does, with its port and its place and its owner told nothing, and answers again once the address is back (`a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back`, `system/netstack/node/tests/lease.rs`); no host was read for it. Exit: the specifications hold the three calls and the refusal, and the tests name their scenarios. - The word each of [udp]'s refusals is answered in on the pipe ABI is `Refused` in `system/netstack/node/src/datagram.rs`, tested by `each_refusal_is_answered_in_the_pipes_word_for_it` against no reader's scenario; `udp.no-ephemeral-port` has a word and no test that reaches it. Exit: the scenario owed below exists, and the test names its id. -- A client's datagram to a broadcast address needs its socket's permission, where smoltcp sends it for every socket. The owner's ruling: "Of course carry the permission why would you even ask. The premise is and always has been to do things properly". The pipe ABI carries the permission as far as netd: `MsgType::UdpSetOption` with `OPT_BROADCAST` (`sdk/toyos/src/net.rs`), sent by std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) and by libc's `setsockopt` of `SO_BROADCAST` (`sdk/libc/src/socket.rs`), which keeps it for a socket not yet bound and sends it at `bind`; the word for a send refused for want of it is `ERR_PERMISSION_DENIED`, `ErrorKind::PermissionDenied` in std and `EACCES` in libc. The node enforces it, and netstack hands it the request (`Sockets::udp_set_option`, `system/netstack/src/serve.rs`): `Node::udp_set_broadcast` hands the permission to `Udp::set_broadcast`, and a send without it is refused `udp.broadcast-not-permitted` and answered `Refused::PermissionDenied` (`system/netstack/node/src/datagram.rs`), which `a_broadcast_needs_its_permission` (US-21, `system/netstack/node/tests/datagram.rs`) holds, with both broadcast addresses read off the wire in link-broadcast frames once the socket holds it and refused again once it is taken back, and `each_refusal_is_answered_in_the_pipes_word_for_it` holds the word. The permission of the call goes with the datagram to the route it leaves by: [udp] queues it with what its socket held, a closed socket's included, and `Ip::send_udp` refuses a link broadcast to one without it, `ip.broadcast-not-permitted`, counted and logged, so a prefix a DHCP server renews under a held address between the call and the frame makes no broadcast of a datagram accepted for a host (`s_udp_us_021_shard_a_datagram_accepted_for_a_host_is_no_broadcast_after_a_renewal_narrows_the_prefix` and its two neighbours in `system/netstack/shard/tests/udp.rs`, and from a server's ACK `a_datagram_accepted_for_a_host_is_no_broadcast_after_the_server_renews_a_narrower_prefix` in the node's tests). The search is run. `grep -rnI -E 'SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255|sendto|setsockopt|sys/socket\.h|socket\(' apps/doom/doomgeneric`, the one third-party C program an image builds, at the commit `apps/doom/build.rs` pins, exits 1 with no line; the same without `sys/socket\.h` over `tests/corpus/tinycc` exits 1 with no line; `SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255` over the trees of `build/libcxx.rs`'s `SOURCES` finds LLVM libc's own macro and its documentation and no caller; and in ToyOS's tree outside the stack's crates the option is named only by the two setters. The socket2 fork's setter reaches nobody: `issues/the-socket2-forks-so-broadcast-setter-does-nothing.md`. Exit: a guest test, `udp_broadcast_needs_its_permission`, reads `broadcast()` false, then true after `set_broadcast(true)`, on the socket and on a `try_clone` of it, which `broadcast()` answering a constant or a duplicate holding a value of its own turns red, and sends to the limited broadcast address before and after the set and is refused `PermissionDenied` only before, which std's setter reverted to `Ok(())` turns red; and a guest C case reads libc's setter, `socket`, `setsockopt(SO_BROADCAST)`, `bind`, `sendto` to the limited broadcast address, refused `EACCES` without the option and sent with it, and with the option set before `bind`, which `bind` no longer handing the kept option over turns red. The case runs the sequence without `bind` too, where the first `sendto` binds the socket and hands the kept option over; `tests/guest/c/sendto_unbound.c` holds that send for a socket permitted to make it. +- A client's datagram to a broadcast address needs its socket's permission, where smoltcp sends it for every socket. The owner's ruling: "Of course carry the permission why would you even ask. The premise is and always has been to do things properly". The protocol carries the permission as far as netstack: a holder's `SET_OPTION` with `OPT_BROADCAST` (`sdk/toyos/src/net/socket.rs`), sent by std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) and by libc's `setsockopt` of `SO_BROADCAST` (`sdk/libc/src/socket.rs`), which keeps it for a socket not yet bound and sends it at `bind`; the word for a send refused for want of it is `ERR_PERMISSION_DENIED`, `ErrorKind::PermissionDenied` in std and `EACCES` in libc. The node enforces it, and netstack hands it the request (`answer_socket`, `system/netstack/src/serve.rs`): `Node::udp_set_broadcast` hands the permission to `Udp::set_broadcast`, and a send without it is refused `udp.broadcast-not-permitted` and answered `Refused::PermissionDenied` (`system/netstack/node/src/datagram.rs`), which `a_broadcast_needs_its_permission` (US-21, `system/netstack/node/tests/datagram.rs`) holds, with both broadcast addresses read off the wire in link-broadcast frames once the socket holds it and refused again once it is taken back, and `each_refusal_is_answered_in_the_pipes_word_for_it` holds the word. The permission of the call goes with the datagram to the route it leaves by: [udp] queues it with what its socket held, a closed socket's included, and `Ip::send_udp` refuses a link broadcast to one without it, `ip.broadcast-not-permitted`, counted and logged, so a prefix a DHCP server renews under a held address between the call and the frame makes no broadcast of a datagram accepted for a host (`s_udp_us_021_shard_a_datagram_accepted_for_a_host_is_no_broadcast_after_a_renewal_narrows_the_prefix` and its two neighbours in `system/netstack/shard/tests/udp.rs`, and from a server's ACK `a_datagram_accepted_for_a_host_is_no_broadcast_after_the_server_renews_a_narrower_prefix` in the node's tests). The search is run. `grep -rnI -E 'SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255|sendto|setsockopt|sys/socket\.h|socket\(' apps/doom/doomgeneric`, the one third-party C program an image builds, at the commit `apps/doom/build.rs` pins, exits 1 with no line; the same without `sys/socket\.h` over `tests/corpus/tinycc` exits 1 with no line; `SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255` over the trees of `build/libcxx.rs`'s `SOURCES` finds LLVM libc's own macro and its documentation and no caller; and in ToyOS's tree outside the stack's crates the option is named only by the two setters. The socket2 fork's setter reaches nobody: `issues/the-socket2-forks-so-broadcast-setter-does-nothing.md`. Exit: a guest test, `udp_broadcast_needs_its_permission`, reads `broadcast()` false, then true after `set_broadcast(true)`, on the socket and on a `try_clone` of it, which `broadcast()` answering a constant or a duplicate holding a value of its own turns red, and sends to the limited broadcast address before and after the set and is refused `PermissionDenied` only before, which std's setter reverted to `Ok(())` turns red; and a guest C case reads libc's setter, `socket`, `setsockopt(SO_BROADCAST)`, `bind`, `sendto` to the limited broadcast address, refused `EACCES` without the option and sent with it, and with the option set before `bind`, which `bind` no longer handing the kept option over turns red. The case runs the sequence without `bind` too, where the first `sendto` binds the socket and hands the kept option over; `tests/guest/c/sendto_unbound.c` holds that send for a socket permitted to make it. - The machine's name is told to the node twice, as `toyos_dhcp::HostName` in `Node::new` and as `toyos_mdns::Host` in `Node::answer_as`: the first owns its bytes and the second borrows them. netstack's is a constant, so neither takes a leak. Exit: one type carries the label to both. - The machine's name is probed for and defended (RFC 6762 §8, §9) by `toyos-mdns` (`system/netstack/mdns`), on the node, which netstack runs. What that does not yet meet. Every machine is named `toyos-t14` (`HOSTNAME`, `system/netstack/src/main.rs`) and a responder that loses its name holds none, so of two on one network the second answers to no name: asked "When two machines on one network have the same ToyOS host name, what should the second one do?", the owner answered "Hold no name, say so (Recommended)". Asked on 2026-10-09 "A ToyOS machine that loses its .local name to a conflict stays nameless until its network link next returns, even after the other machine has left. Should it try for its name again by itself?", he answered "Retry on a slow interval (Recommended)". Built: a lost name is probed for again a minute after each loss (`RETRY_MS`, `system/netstack/mdns/src/lib.rs`) and at once on a link after none, and is claimed by the first probing no host answers; RFC 6762 permits probing again at §8.1's rate ("wait five seconds after any failed probe attempt before trying again") and does not require it. Two packets from any host on the link still take a held name, a response to take it back to probing and a response under the probe, and one more a minute keeps it: no path back exists that a peer cannot hold shut, since a host that answers every probe is what a holder of the name is. The caller does not say how a message was addressed, so two rules that turn on it are not applied: a response is read however it came, where §6 reads a unicast one only within two seconds of a question that asked for one; and a message from a source outside the subnet is ignored even when it was sent to the group, where §11 deems that on the link "regardless of source IP address", so a host of the same name on another subnet of the link is never a conflict. Exit: each machine's name is its own, and a test with two guests on one network finds each by its name; and the caller says how a message was addressed, with a test in which a unicast response nobody asked for takes no name and one in which a response sent to the group from another subnet does. Not measured, each with its exit: the shipped responder's claim on the T14's wired card, which no `system.toml` in the tree gives netstack (when the outbound rows land, their boot shows the lease line, then the claim line, and no loss line); and the link's return on metal (the orchestrator's attended session with the owner, never automated). A link that goes and returns inside one pass of the Intel driver reaches the name as no change: `issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md`. - A socket's datagrams that wait for a next hop wait in the one queue it has, `toyos_net_udp::limits::TX_DATAGRAMS` of them: with all 16 waiting for one next hop, the socket's sends to every other destination are refused `udp.tx-queue-full`, `Refused::ResourceExhausted` to a client, until that hop answers or [ip] gives it up, 3 s after its first request left (`BROADCAST_SOLICIT` requests a `RETRANS` apart). It is no regression by this track's earlier record of what ships, that smoltcp kept each datagram in its own socket until the neighbour answered. What a closed socket had accepted waits in the closed sender's `CLOSED_DATAGRAMS` the same way, so 16 datagrams of closed sockets that wait for one silent host have the next closes' datagrams discarded, `udp.tx-discarded-on-close`, for those 3 s: the line on US-53 among stage 3's departures. `s_udp_us_025_what_waits_for_a_next_hop_is_bounded_by_its_sockets_queue` holds the first as it stands. The move's guest runs showed no program refused for it: none of their jobs sends sixteen datagrams past a hop that does not answer. Owner: whoever reads the T14's first outbound run on this stack. Exit: a run on the T14 or in a guest shows a program refused for it, and a socket's bound is counted per next hop; or the owner rules the one queue is the bound. @@ -42,7 +41,7 @@ What the node does not yet meet: - `toyos_dns::Lookup::on_datagram` still takes a reply's source address and port, which the node's connected sockets have already matched and the node hands it from its own record of the query: the resolver that read them left with smoltcp, and the node hands both from its own record. Not removed at the move, which left `toyos-dns` as it was. Exit: the two parameters and the comparison inside `on_datagram` go. - `node.address-refused` has no test: `toyos-dhcp` accepts no address or prefix [ip] refuses, by the same `toyos-net-wire` checks in both, so the refusal cannot be reached from the wire. Exit: the client hands [ip] a type that carries the check, and the counter goes. -The listener defects are this track's: `issues/a-connect-between-two-accepts-is-reset.md`, whose listener queues now and whose `lan_swap` is not restored, and `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`, in std's accept; and `issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md`: the pipe ABI gives a listener its option in its bind's request and by `MsgType::TcpListenerSetOption` and an accept's answer its connection's (`TcpOptions`, `sdk/toyos/src/net.rs`), netstack maps the bind to `Node::listen`, whose `nodelay` the request's is, the set to `Node::set_listener_nodelay` and the answer's options to `Accepted`'s (`system/netstack/src/serve.rs`); what is left of its exit is libc's `poll` and the socket2 and mio forks. +The listener defects are this track's: `issues/a-connect-between-two-accepts-is-reset.md`, whose listener queues now and whose `lan_swap` is not restored; and `issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md`: the protocol gives a listener its option in its listen's request and by its holder's `SET_OPTION` and an accept's answer its connection's (`TcpOptions`, `sdk/toyos/src/net.rs`, `sdk/toyos/src/net/socket.rs`), netstack maps the listen to `Node::listen`, whose `nodelay` the request's is, the set to `Node::set_listener_nodelay` and the answer's options to `Accepted`'s (`system/netstack/src/serve.rs`); what is left of its exit is libc's `poll` and the socket2 and mio forks. Owed from the stage 3 specifications: since stage 5, the scenario for netd's mapping of UDP's refusals onto the pipe ABI, and `dhcp.renew-unroutable`, which netd counts where `toyos-net-udp` refuses the renewal `udp.no-route`; by IP hardening, the scenarios for fragment reassembly and path MTU discovery. diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index 968454c46aa..e56027ffd0d 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -1015,8 +1015,8 @@ const MAX_POLLED: usize = toyos::poller::Poller::MAX_HANDLES as usize - 1; /// one that is ready answers at once; a direction that is not is counted in, /// drained, armed and looked at again (`toyos::net::stream::watch`), and its /// doorbell watched for reading: the connection for `POLLIN`, the bell for -/// `POLLOUT`. Every other descriptor, a listener's wake pipe and a datagram -/// socket's receive pipe for a socket's, is watched as it is. +/// `POLLOUT`. Every other descriptor is watched as it is, a listener's and a +/// datagram socket's by its holder's bell. /// /// # Safety /// `fds` points at `n` entries. diff --git a/sdk/libc/src/socket.rs b/sdk/libc/src/socket.rs index 55ef0eca58e..fddcd87a90e 100644 --- a/sdk/libc/src/socket.rs +++ b/sdk/libc/src/socket.rs @@ -1,21 +1,20 @@ // BSD sockets over netstack: a TCP stream is `toyos::net::stream`'s region -// and connection, and a listener's wakes and a datagram socket's bytes travel -// in pipes. A socket that connects, and one an accept makes, is its stream: -// its descriptor names the stream's handle, as std's does, so a spawn passes -// it as a stream (`toyos::fd::copy`) and every other socket is an entry of -// this library's. +// and connection, and a listener and a datagram socket `toyos::net::socket`'s +// holder. A socket is an entry of this library's until it binds or connects; +// from then its descriptor names netstack's object as std's does, so a spawn +// passes it as a holder of its own (`toyos::fd::copy`). use alloc::alloc::{alloc as heap_alloc, dealloc as heap_dealloc}; use alloc::vec::Vec; use core::ptr; use toyos_abi::RawHandle; -use toyos_abi::syscall; +use toyos::net::socket::{self, Kind}; use toyos::net::stream::{self, Wait}; -use toyos::net::{NetError, TcpOptions, TcpSocketId, UdpSocketId, OPT_BROADCAST}; +use toyos::net::{NetError, TcpOptions, OPT_BROADCAST, OPT_NODELAY}; use crate::errcode::{ - EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EFAULT, EINVAL, EIO, EISCONN, ENOMEM, ENOPROTOOPT, - ENOSPC, ENOTCONN, ETIMEDOUT, + EACCES, EADDRINUSE, EAFNOSUPPORT, EAGAIN, EBADF, ECONNREFUSED, ECONNRESET, EFAULT, EINVAL, EIO, EISCONN, ENOMEM, + ENOPROTOOPT, ENOSPC, ENOTCONN, ETIMEDOUT, }; use crate::inaddr::{self, SockaddrIn, AF_INET}; use crate::dgramreq; @@ -55,28 +54,17 @@ enum SocketKind { Udp, } +/// A socket netstack does not hold yet: what its bind or connect makes it +/// with. A listener's `nodelay` goes in its bind's request, a datagram +/// socket's `broadcast` and peer are handed over after its bind, and a +/// stream's `nodelay` into its region before its connect. #[derive(Clone, Copy)] struct SocketEntry { kind: SocketKind, - netstack_id: u32, // netstack socket_id (0 = not yet connected/bound) - /// A datagram socket's peer was named. - connected: bool, - bound: bool, - local_port: u16, - remote_addr: [u8; 4], - remote_port: u16, - // A datagram socket's pipe fds (0 = not set) - rx_fd: i32, // read end of rx pipe (netstack→client) - tx_fd: i32, // write end of tx pipe (client→netstack) - notify_fd: i32, // read end of listener notify pipe - // What netstack holds for the socket, which `getsockopt` answers from: set - // by a request it answered, or read from the accept's answer. A socket - // netstack does not hold yet keeps each for the call that makes it there: - // a listener's `nodelay` goes in its `bind`'s request, `broadcast` is - // handed over after a datagram socket's bind, and a stream's `nodelay` - // into its region before its connect. nodelay: bool, broadcast: bool, + /// Where a datagram socket's `send` goes. + peer: Option<([u8; 4], u16)>, } const MAX_SOCKETS: usize = 128; @@ -94,21 +82,33 @@ static mut SOCKETS: [Option; MAX_SOCKETS] = [None; MAX_SOCKETS]; fn sock_from_fd(fd: i32) -> Option { let (hold, object) = toyos::fd::hold(fd).ok()?; if object.tag == toyos::fd::STREAM { - return Some(Sock::Stream(Stream { _hold: hold, handle: object.handle })); + return Some(Sock::Stream(Held { _hold: hold, handle: object.handle })); + } + if object.tag == toyos::fd::SOCKET { + return match socket::kind(object.handle) { + Some(Kind::Listener) => Some(Sock::Listener(Held { _hold: hold, handle: object.handle })), + Some(Kind::Datagram) => Some(Sock::Datagram(Held { _hold: hold, handle: object.handle })), + // Another copy of `toyos`'s: this library holds no end of it. + None => None, + }; } Some(Sock::Entry(Socket { _hold: hold, idx: index_of(&object)? })) } /// What a socket descriptor names, held. enum Sock { - /// A socket that is no stream: this library's entry. + /// A socket netstack does not hold yet: this library's entry. Entry(Socket), /// A stream, whose descriptor names its handle. - Stream(Stream), + Stream(Held), + /// A listener, whose descriptor names its holder's. + Listener(Held), + /// A datagram socket, likewise. + Datagram(Held), } -/// A stream's descriptor, held for the call. -struct Stream { +/// A descriptor of netstack's object, held for the call. +struct Held { _hold: toyos::fd::Hold, handle: RawHandle, } @@ -142,10 +142,9 @@ impl Socket { } /// `entry` as a socket descriptor, the lowest free and inherited, or `-1` -/// with `errno` set and everything it held let go. +/// with `errno` set. fn alloc_socket(entry: SocketEntry) -> i32 { let Some(idx) = (0..MAX_SOCKETS).find(|&i| unsafe { SOCKETS[i].is_none() }) else { - let_go(entry); set_errno(ENOMEM); return -1; }; @@ -160,26 +159,11 @@ fn alloc_socket(entry: SocketEntry) -> i32 { } } -/// A socket no descriptor names any more: its pipes, its stream and -/// netstack's socket go. +/// A socket no descriptor names any more: its entry goes. extern "C" fn release_socket(word: u64) { let idx = (word & 0xffff_ffff) as usize; if idx < MAX_SOCKETS { - if let Some(entry) = unsafe { SOCKETS[idx].take() } { - let_go(entry); - } - } -} - -fn let_go(entry: SocketEntry) { - if entry.rx_fd != 0 { syscall::close(RawHandle(entry.rx_fd as u32)); } - if entry.tx_fd != 0 { syscall::close(RawHandle(entry.tx_fd as u32)); } - if entry.notify_fd != 0 { syscall::close(RawHandle(entry.notify_fd as u32)); } - if entry.netstack_id != 0 { - match entry.kind { - SocketKind::Tcp => { let _ = toyos::net::tcp_close(TcpSocketId(entry.netstack_id)); } - SocketKind::Udp => { let _ = toyos::net::udp_close(UdpSocketId(entry.netstack_id)); } - } + unsafe { SOCKETS[idx] = None }; } } @@ -196,6 +180,8 @@ fn net_err_to_errno(e: NetError) -> i32 { NetError::NotConnected => ENOTCONN, NetError::InvalidInput => EINVAL, NetError::PermissionDenied => EACCES, + NetError::WouldBlock => EAGAIN, + NetError::NotASocket => EBADF, NetError::NetstackNotFound | NetError::ResourceExhausted | NetError::Protocol(_) | NetError::Io => EIO, } } @@ -216,39 +202,54 @@ unsafe fn fill_sockaddr(addr: *mut Sockaddr, addrlen: *mut SocklenT, ip: [u8; 4] SockaddrIn::new(ip, port).answer(addr as *mut u8, addrlen); } -/// Bind a datagram socket netstack does not hold yet, handing over a -/// `SO_BROADCAST` set before it existed there. -fn bind_datagram(entry: &mut SocketEntry, ip: [u8; 4], port: u16) -> Result<(), NetError> { - let bound = toyos::net::udp_bind(ip, port)?; - if entry.broadcast { - if let Err(e) = toyos::net::udp_set_option(bound.socket_id, OPT_BROADCAST, 1) { - // `bound`'s pipe ends close where it drops. - let _ = toyos::net::udp_close(bound.socket_id); - return Err(e); +/// Binds the datagram socket descriptor `fd` names, whose entry is `entry`, +/// handing over what was set before netstack held it: from here `fd` names +/// netstack's socket, and the entry goes once the caller's hold does. +fn bind_datagram(fd: i32, entry: &SocketEntry, ip: [u8; 4], port: u16) -> Result { + let bound = socket::bind(ip, port).map_err(net_err_to_errno)?; + let handed = (|| { + if entry.broadcast { + socket::set_option(bound.handle, OPT_BROADCAST, 1)?; } + socket::set_peer(bound.handle, entry.peer) + })(); + if let Err(e) = handed { + socket::let_go(bound.handle); + return Err(net_err_to_errno(e)); } - entry.netstack_id = bound.socket_id.0; - entry.local_port = bound.bound_port; - entry.bound = true; - entry.tx_fd = bound.tx.into_raw().0 as i32; - entry.rx_fd = bound.rx.into_raw().0 as i32; - Ok(()) + toyos::fd::replace(fd, socket::descriptor(bound.handle)).map_err(|_| EBADF)?; + Ok(bound.handle) } -/// A datagram socket's calls of netstack, answered in `errno`s: its bytes go -/// down its pipe, then the request that names where they go. -impl dgramreq::Datagrams for SocketEntry { +/// A datagram socket's sends: one netstack holds, or one it does not yet, +/// which its first send binds. +struct Sending<'a> { + fd: i32, + entry: Option<&'a SocketEntry>, + bound: Option, +} + +impl dgramreq::Datagrams for Sending<'_> { type Error = i32; fn held(&self) -> bool { - self.netstack_id != 0 + self.bound.is_some() } fn bind(&mut self, ip: [u8; 4], port: u16) -> Result<(), i32> { - bind_datagram(self, ip, port).map_err(net_err_to_errno) + let entry = self.entry.ok_or(EBADF)?; + self.bound = Some(bind_datagram(self.fd, entry, ip, port)?); + Ok(()) } fn send(&mut self, data: &[u8], ip: [u8; 4], port: u16) -> Result { - syscall::write(RawHandle(self.tx_fd as u32), data).map_err(|_| EIO)?; - let sent = toyos::net::udp_send_to(UdpSocketId(self.netstack_id), ip, port, data.len() as u16); - sent.map(|sent| sent as usize).map_err(net_err_to_errno) + let handle = self.bound.ok_or(EBADF)?; + socket::send_to(handle, ip, port, data).map_err(net_err_to_errno) + } +} + +/// `data` to `ip`:`port` from the datagram socket `sending` is. +fn send_datagram(mut sending: Sending<'_>, data: &[u8], ip: [u8; 4], port: u16) -> isize { + match dgramreq::send_to(&mut sending, data, ip, port) { + Ok(sent) => sent as isize, + Err(errno) => { set_errno(errno); -1 } } } @@ -268,28 +269,24 @@ pub unsafe extern "C" fn socket(domain: i32, sock_type: i32, _protocol: i32) -> return -1; } }; - let entry = SocketEntry { - kind, - netstack_id: 0, - connected: false, - bound: false, - local_port: 0, - remote_addr: [0; 4], - remote_port: 0, - rx_fd: 0, - tx_fd: 0, - notify_fd: 0, - nodelay: false, - broadcast: false, - }; - alloc_socket(entry) + alloc_socket(SocketEntry { kind, nodelay: false, broadcast: false, peer: None }) } #[no_mangle] pub unsafe extern "C" fn connect(fd: i32, addr: *const Sockaddr, addrlen: SocklenT) -> i32 { + let (ip, port) = match parse_sockaddr(addr, addrlen) { + Some(v) => v, + None => { set_errno(EINVAL); return -1; } + }; let mut socket = match sock_from_fd(fd) { Some(Sock::Entry(socket)) => socket, - Some(Sock::Stream(_)) => { set_errno(EISCONN); return -1; } + Some(Sock::Datagram(held)) => { + return match socket::set_peer(held.handle, Some((ip, port))) { + Ok(()) => 0, + Err(e) => { set_errno(net_err_to_errno(e)); -1 } + }; + } + Some(Sock::Stream(_) | Sock::Listener(_)) => { set_errno(EISCONN); return -1; } None => { set_errno(EBADF); return -1; } }; let slot = socket.slot(); @@ -297,10 +294,6 @@ pub unsafe extern "C" fn connect(fd: i32, addr: *const Sockaddr, addrlen: Sockle Some(e) => e, None => { set_errno(EBADF); return -1; } }; - let (ip, port) = match parse_sockaddr(addr, addrlen) { - Some(v) => v, - None => { set_errno(EINVAL); return -1; } - }; match entry.kind { SocketKind::Tcp => { @@ -317,9 +310,7 @@ pub unsafe extern "C" fn connect(fd: i32, addr: *const Sockaddr, addrlen: Sockle } } SocketKind::Udp => { - entry.remote_addr = ip; - entry.remote_port = port; - entry.connected = true; + entry.peer = Some((ip, port)); 0 } } @@ -330,37 +321,24 @@ pub unsafe extern "C" fn bind(fd: i32, addr: *const Sockaddr, addrlen: SocklenT) let mut socket = match sock_from_fd(fd) { Some(Sock::Entry(socket)) => socket, Some(Sock::Stream(_)) => { set_errno(EISCONN); return -1; } + Some(Sock::Listener(_) | Sock::Datagram(_)) => { set_errno(EINVAL); return -1; } None => { set_errno(EBADF); return -1; } }; - let slot = socket.slot(); - let entry = match slot.as_mut() { - Some(e) => e, - None => { set_errno(EBADF); return -1; } - }; + let Some(entry) = *socket.slot() else { set_errno(EBADF); return -1; }; let (ip, port) = match parse_sockaddr(addr, addrlen) { Some(v) => v, None => { set_errno(EINVAL); return -1; } }; - match entry.kind { - SocketKind::Tcp => { - let bound = match toyos::net::tcp_bind_with(ip, port, TcpOptions::new(entry.nodelay)) { - Ok(b) => b, - Err(e) => { set_errno(net_err_to_errno(e)); return -1; } - }; - entry.netstack_id = bound.socket_id.0; - entry.local_port = bound.bound_port; - entry.bound = true; - entry.notify_fd = bound.notify.into_raw().0 as i32; - 0 - } - SocketKind::Udp => { - if let Err(e) = bind_datagram(entry, ip, port) { - set_errno(net_err_to_errno(e)); - return -1; - } - 0 - } + let bound = match entry.kind { + SocketKind::Tcp => socket::listen(ip, port, TcpOptions::new(entry.nodelay)) + .map_err(net_err_to_errno) + .and_then(|bound| toyos::fd::replace(fd, socket::descriptor(bound.handle)).map_err(|_| EBADF)), + SocketKind::Udp => bind_datagram(fd, &entry, ip, port).map(drop), + }; + match bound { + Ok(()) => 0, + Err(errno) => { set_errno(errno); -1 } } } @@ -376,49 +354,32 @@ pub unsafe extern "C" fn accept( addr: *mut Sockaddr, addrlen: *mut SocklenT, ) -> i32 { - let mut socket = match sock_from_fd(fd) { - Some(Sock::Entry(socket)) => socket, - Some(Sock::Stream(_)) => { set_errno(EINVAL); return -1; } + let listener = match sock_from_fd(fd) { + Some(Sock::Listener(held)) => held, + Some(_) => { set_errno(EINVAL); return -1; } None => { set_errno(EBADF); return -1; } }; - let slot = socket.slot(); - let entry = match slot.as_ref() { - Some(e) => e, - None => { set_errno(EBADF); return -1; } - }; - let listener_id = TcpSocketId(entry.netstack_id); - let notify_fd = entry.notify_fd; + let handle = listener.handle; - // A connection's arrival is a byte on the notify pipe. - let notify = RawHandle(notify_fd as u32); + // The descriptor's handle is the holder's bell: a connection that waits rings it. let mut poller = None; - let arrived = interruptibly( - |block| match block { - true => { - let _ = syscall::read(notify, &mut [0u8; 1]); - Some(()) - } - false => match syscall::read_nonblock(notify, &mut [0u8; 1]) { - Err(syscall::SyscallError::WouldBlock) => None, - _ => Some(()), - }, + let accepted = interruptibly( + |block| match socket::accept(handle, if block { Wait::Blocking(None) } else { Wait::Nonblocking }) { + Err(NetError::WouldBlock) if !block => None, + answer => Some(answer), }, |wake| { let poller = poller.get_or_insert_with(|| toyos::poller::Poller::new(2)); - poller.watch_raw(notify, toyos::poller::READABLE, 0); + poller.watch_raw(handle, toyos::poller::READABLE, 0); poller.watch_raw(wake, toyos::poller::READABLE, 1); poller.wait(1, u64::MAX, |_| {}); Ok(()) }, ); - if let Err(errno) = arrived { - set_errno(errno); - return -1; - } - - let accepted = match stream::accept(listener_id) { - Ok(a) => a, - Err(e) => { set_errno(net_err_to_errno(e)); return -1; } + let accepted = match accepted { + Ok(Ok(accepted)) => accepted, + Ok(Err(e)) => { set_errno(net_err_to_errno(e)); return -1; } + Err(errno) => { set_errno(errno); return -1; } }; if !addr.is_null() && !addrlen.is_null() { @@ -438,28 +399,23 @@ pub unsafe extern "C" fn send(fd: i32, buf: *const u8, len: usize, _flags: i32) if buf.is_null() || len == 0 { return 0; } - let mut socket = match sock_from_fd(fd) { - Some(Sock::Stream(stream)) => return write_stream(stream.handle, core::slice::from_raw_parts(buf, len)), - Some(Sock::Entry(socket)) => socket, - None => { set_errno(EBADF); return -1; } - }; - let slot = socket.slot(); - let entry = match slot.as_ref() { - Some(e) => e, - None => { set_errno(EBADF); return -1; } - }; - - match entry.kind { - SocketKind::Tcp => { set_errno(ENOTCONN); -1 } - SocketKind::Udp => { - if !entry.connected { - set_errno(ENOTCONN); - return -1; + let data = core::slice::from_raw_parts(buf, len); + match sock_from_fd(fd) { + Some(Sock::Stream(held)) => write_stream(held.handle, data), + Some(Sock::Datagram(held)) => match socket::peer(held.handle) { + Ok(Some((ip, port))) => send_datagram(Sending { fd, entry: None, bound: Some(held.handle) }, data, ip, port), + Ok(None) => { set_errno(ENOTCONN); -1 } + Err(e) => { set_errno(net_err_to_errno(e)); -1 } + }, + Some(Sock::Entry(mut socket)) => match *socket.slot() { + Some(entry @ SocketEntry { kind: SocketKind::Udp, peer: Some((ip, port)), .. }) => { + send_datagram(Sending { fd, entry: Some(&entry), bound: None }, data, ip, port) } - sendto(fd, buf, len, _flags, - &SockaddrIn::new(entry.remote_addr, entry.remote_port) as *const SockaddrIn as *const Sockaddr, - core::mem::size_of::() as SocklenT) - } + Some(_) => { set_errno(ENOTCONN); -1 } + None => { set_errno(EBADF); -1 } + }, + Some(Sock::Listener(_)) => { set_errno(ENOTCONN); -1 } + None => { set_errno(EBADF); -1 } } } @@ -495,29 +451,30 @@ fn read_stream(handle: RawHandle, data: &mut [u8]) -> isize { } } +/// A datagram into `data` from the datagram socket `handle` reaches, and where +/// it came from: a handler interrupts the wait for it as [`interruptibly`] +/// says. +fn read_datagram(handle: RawHandle, data: &mut [u8]) -> Result { + let mut poller = None; + let received = interruptibly( + |block| match socket::recv_from(handle, data, if block { Wait::Blocking(None) } else { Wait::Nonblocking }) { + Err(NetError::WouldBlock) if !block => None, + answer => Some(answer), + }, + |wake| { + let poller = poller.get_or_insert_with(|| toyos::poller::Poller::new(2)); + poller.watch_raw(handle, toyos::poller::READABLE, 0); + poller.watch_raw(wake, toyos::poller::READABLE, 1); + poller.wait(1, u64::MAX, |_| {}); + Ok(()) + }, + ); + received?.map_err(net_err_to_errno) +} #[no_mangle] pub unsafe extern "C" fn recv(fd: i32, buf: *mut u8, len: usize, _flags: i32) -> isize { - if buf.is_null() || len == 0 { - return 0; - } - let mut socket = match sock_from_fd(fd) { - Some(Sock::Stream(stream)) => return read_stream(stream.handle, core::slice::from_raw_parts_mut(buf, len)), - Some(Sock::Entry(socket)) => socket, - None => { set_errno(EBADF); return -1; } - }; - let slot = socket.slot(); - let entry = match slot.as_ref() { - Some(e) => e, - None => { set_errno(EBADF); return -1; } - }; - - match entry.kind { - SocketKind::Tcp => { set_errno(ENOTCONN); -1 } - SocketKind::Udp => { - recvfrom(fd, buf, len, _flags, ptr::null_mut(), ptr::null_mut()) - } - } + recvfrom(fd, buf, len, _flags, ptr::null_mut(), ptr::null_mut()) } #[no_mangle] @@ -532,31 +489,23 @@ pub unsafe extern "C" fn sendto( if buf.is_null() || len == 0 { return 0; } - let mut socket = match sock_from_fd(fd) { - // A stream's `sendto` ignores the address, as `send`. - Some(Sock::Stream(stream)) => return write_stream(stream.handle, core::slice::from_raw_parts(buf, len)), - Some(Sock::Entry(socket)) => socket, - None => { set_errno(EBADF); return -1; } - }; - let slot = socket.slot(); - let entry = match slot.as_mut() { - Some(e) => e, - None => { set_errno(EBADF); return -1; } - }; - - match entry.kind { - SocketKind::Tcp => { set_errno(ENOTCONN); -1 } - SocketKind::Udp => { - let (ip, port) = match parse_sockaddr(dest_addr, addrlen) { - Some(v) => v, - None => { set_errno(EINVAL); return -1; } - }; - let data = core::slice::from_raw_parts(buf, len); - match dgramreq::send_to(entry, data, ip, port) { - Ok(sent) => sent as isize, - Err(errno) => { set_errno(errno); -1 } - } - } + let data = core::slice::from_raw_parts(buf, len); + let sock = sock_from_fd(fd); + // A stream's `sendto` ignores the address, as `send`. + if let Some(Sock::Stream(held)) = &sock { + return write_stream(held.handle, data); + } + let Some((ip, port)) = parse_sockaddr(dest_addr, addrlen) else { set_errno(EINVAL); return -1; }; + match sock { + Some(Sock::Datagram(held)) => send_datagram(Sending { fd, entry: None, bound: Some(held.handle) }, data, ip, port), + Some(Sock::Entry(mut socket)) => match *socket.slot() { + Some(entry @ SocketEntry { kind: SocketKind::Udp, .. }) => send_datagram(Sending { fd, entry: Some(&entry), bound: None }, data, ip, port), + Some(_) => { set_errno(ENOTCONN); -1 } + None => { set_errno(EBADF); -1 } + }, + Some(Sock::Listener(_)) => { set_errno(ENOTCONN); -1 } + Some(Sock::Stream(_)) => unreachable!("a stream wrote above"), + None => { set_errno(EBADF); -1 } } } @@ -572,50 +521,30 @@ pub unsafe extern "C" fn recvfrom( if buf.is_null() || len == 0 { return 0; } - let mut socket = match sock_from_fd(fd) { + let data = core::slice::from_raw_parts_mut(buf, len); + match sock_from_fd(fd) { // A stream's `recvfrom` names no address, as `recv`. - Some(Sock::Stream(stream)) => return read_stream(stream.handle, core::slice::from_raw_parts_mut(buf, len)), - Some(Sock::Entry(socket)) => socket, - None => { set_errno(EBADF); return -1; } - }; - let slot = socket.slot(); - let entry = match slot.as_ref() { - Some(e) => e, - None => { set_errno(EBADF); return -1; } - }; - - match entry.kind { - SocketKind::Tcp => { set_errno(ENOTCONN); -1 } - SocketKind::Udp => { - // Send control request and get metadata response - let recv_resp = match toyos::net::udp_recv_from(UdpSocketId(entry.netstack_id), len as u32) { - Ok(r) => r, - Err(e) => { set_errno(net_err_to_errno(e)); return -1; } - }; - - if !src_addr.is_null() && !addrlen.is_null() { - fill_sockaddr(src_addr, addrlen, recv_resp.addr, recv_resp.port); - } - - let n = (recv_resp.len as usize).min(len); - if n > 0 { - // Read data from rx pipe - let data = core::slice::from_raw_parts_mut(buf, n); - match syscall::read(RawHandle(entry.rx_fd as u32), data) { - Ok(bytes_read) => bytes_read as isize, - Err(_) => { set_errno(EIO); -1 } + Some(Sock::Stream(held)) => read_stream(held.handle, data), + Some(Sock::Datagram(held)) => match read_datagram(held.handle, data) { + Ok(received) => { + if !src_addr.is_null() && !addrlen.is_null() { + fill_sockaddr(src_addr, addrlen, received.addr, received.port); } - } else { - 0 + received.len as isize } - } + Err(errno) => { set_errno(errno); -1 } + }, + Some(Sock::Entry(_) | Sock::Listener(_)) => { set_errno(ENOTCONN); -1 } + None => { set_errno(EBADF); -1 } } } #[no_mangle] pub unsafe extern "C" fn shutdown(fd: i32, how: i32) -> i32 { let handle = match sock_from_fd(fd) { - Some(Sock::Stream(stream)) => stream.handle, + Some(Sock::Stream(held)) => held.handle, + Some(Sock::Listener(_)) => { set_errno(ENOTCONN); return -1; } + Some(Sock::Datagram(_)) => return 0, Some(Sock::Entry(mut socket)) => { let tcp = socket.slot().as_ref().is_some_and(|entry| matches!(entry.kind, SocketKind::Tcp)); if tcp { @@ -676,7 +605,7 @@ fn interruptibly( pub(crate) fn read_if_stream(fd: i32, data: &mut [u8]) -> Option { match sock_from_fd(fd)? { Sock::Stream(held) if stream::is_stream(held.handle) => Some(read_stream(held.handle, data)), - Sock::Stream(_) | Sock::Entry(_) => None, + _ => None, } } @@ -684,7 +613,7 @@ pub(crate) fn read_if_stream(fd: i32, data: &mut [u8]) -> Option { pub(crate) fn write_if_stream(fd: i32, data: &[u8]) -> Option { match sock_from_fd(fd)? { Sock::Stream(held) if stream::is_stream(held.handle) => Some(write_stream(held.handle, data)), - Sock::Stream(_) | Sock::Entry(_) => None, + _ => None, } } @@ -694,20 +623,10 @@ pub(crate) fn stream_of(object: &toyos::fd::Object) -> Option { (object.tag == toyos::fd::STREAM).then_some(object.handle) } -/// The kernel handle a socket that is no stream is watched by, for `poll`: a -/// listener's wake pipe, a datagram socket's receive pipe. +/// The kernel handle a socket that is no stream is watched by, for `poll`: +/// its holder's bell, readable while a connection or a datagram waits. pub(crate) fn watched_of(object: &toyos::fd::Object) -> Option { - let entry = entry_of(object)?; - let handle = if entry.notify_fd != 0 { entry.notify_fd } else { entry.rx_fd }; - (handle != 0).then_some(RawHandle(handle as u32)) -} - -/// A copy of the entry of the socket a held descriptor's `object` is. -fn entry_of(object: &toyos::fd::Object) -> Option { - let idx = index_of(object)?; - // SAFETY: the caller's hold keeps `release_socket` off the entry; the - // read is unserialized, as `Socket::slot`'s is. - unsafe { *core::ptr::addr_of!(SOCKETS[idx]) } + (object.tag == toyos::fd::SOCKET).then_some(object.handle) } @@ -721,6 +640,18 @@ fn option_errno(refusal: sockopt::Refusal) -> i32 { } } +/// The option netstack holds for `kept` on a listener or a datagram socket: +/// a listener's Nagle switch and a datagram socket's broadcast permission. A +/// listener sends to no broadcast address, and a datagram socket has no Nagle +/// switch. +fn held_option(kind: Kind, kept: Kept) -> Option { + match (kind, kept) { + (Kind::Listener, Kept::NoDelay) => Some(OPT_NODELAY), + (Kind::Datagram, Kept::Broadcast) => Some(OPT_BROADCAST), + (Kind::Listener, Kept::Broadcast) | (Kind::Datagram, Kept::NoDelay) => None, + } +} + #[no_mangle] pub unsafe extern "C" fn setsockopt( fd: i32, @@ -730,7 +661,9 @@ pub unsafe extern "C" fn setsockopt( optlen: SocklenT, ) -> i32 { let mut socket = match sock_from_fd(fd) { - Some(Sock::Stream(stream)) => return set_stream_option(stream.handle, level, optname, optval, optlen), + Some(Sock::Stream(held)) => return set_stream_option(held.handle, level, optname, optval, optlen), + Some(Sock::Listener(held)) => return set_held_option(held.handle, Kind::Listener, level, optname, optval, optlen), + Some(Sock::Datagram(held)) => return set_held_option(held.handle, Kind::Datagram, level, optname, optval, optlen), Some(Sock::Entry(socket)) => socket, None => { set_errno(EBADF); return -1; } }; @@ -750,34 +683,32 @@ pub unsafe extern "C" fn setsockopt( Ok(on) => on, Err(refusal) => { set_errno(option_errno(refusal)); return -1; } }; + // Kept for the bind or the connect that hands it over. match option { - Kept::NoDelay => { - // A listener's is a request; a socket netstack does not hold yet - // keeps it for its `bind` or its `connect`. - if entry.bound { - let id = TcpSocketId(entry.netstack_id); - if let Err(e) = toyos::net::tcp_listener_set_option(id, toyos::net::OPT_NODELAY, on as u32) { - set_errno(net_err_to_errno(e)); - return -1; - } - } - entry.nodelay = on; - } - Kept::Broadcast => { - // Only a datagram is ever sent to a broadcast address, and netstack - // holds a datagram socket from its `bind`. - if let (SocketKind::Udp, true) = (entry.kind, entry.netstack_id != 0) { - if let Err(e) = toyos::net::udp_set_option(UdpSocketId(entry.netstack_id), OPT_BROADCAST, on as u32) { - set_errno(net_err_to_errno(e)); - return -1; - } - } - entry.broadcast = on; - } + Kept::NoDelay => entry.nodelay = on, + Kept::Broadcast => entry.broadcast = on, } 0 } +/// `setsockopt` on a listener or a datagram socket: a request of netstack's. +unsafe fn set_held_option(handle: RawHandle, kind: Kind, level: i32, optname: i32, optval: *const u8, optlen: SocklenT) -> i32 { + let option = match sockopt::kept(level, optname, kind == Kind::Datagram) { + Ok(Some(option)) => option, + Ok(None) => return 0, + Err(refusal) => { set_errno(option_errno(refusal)); return -1; } + }; + let on = match sockopt::switch(optval, optlen) { + Ok(on) => on, + Err(refusal) => { set_errno(option_errno(refusal)); return -1; } + }; + let Some(option) = held_option(kind, option) else { return 0 }; + match socket::set_option(handle, option, on as u32) { + Ok(()) => 0, + Err(e) => { set_errno(net_err_to_errno(e)); -1 } + } +} + /// `setsockopt` on a stream: its Nagle switch is a word of its region. unsafe fn set_stream_option(handle: RawHandle, level: i32, optname: i32, optval: *const u8, optlen: SocklenT) -> i32 { let option = match sockopt::kept(level, optname, false) { @@ -808,14 +739,30 @@ pub unsafe extern "C" fn getsockopt( optlen: *mut SocklenT, ) -> i32 { let (option, entry_value) = match sock_from_fd(fd) { - Some(Sock::Stream(stream)) => match sockopt::kept(level, optname, false) { - Ok(Some(Kept::NoDelay)) => match stream::nodelay(stream.handle) { + Some(Sock::Stream(held)) => match sockopt::kept(level, optname, false) { + Ok(Some(Kept::NoDelay)) => match stream::nodelay(held.handle) { Ok(on) => (Some(Kept::NoDelay), on as i32), Err(refused) => { set_errno(crate::errcode::of_stream(refused)); return -1; } }, Ok(option) => (option, 0), Err(refusal) => { set_errno(option_errno(refusal)); return -1; } }, + Some(sock @ (Sock::Listener(_) | Sock::Datagram(_))) => { + let (kind, held) = match sock { + Sock::Listener(held) => (Kind::Listener, held), + Sock::Datagram(held) => (Kind::Datagram, held), + Sock::Entry(_) | Sock::Stream(_) => unreachable!("matched as netstack's socket"), + }; + match sockopt::kept(level, optname, kind == Kind::Datagram) { + Ok(Some(kept)) => match held_option(kind, kept).map(|option| socket::option(held.handle, option)) { + Some(Ok(value)) => (Some(kept), (value != 0) as i32), + Some(Err(e)) => { set_errno(net_err_to_errno(e)); return -1; } + None => (Some(kept), 0), + }, + Ok(None) => (None, 0), + Err(refusal) => { set_errno(option_errno(refusal)); return -1; } + } + } Some(Sock::Entry(mut socket)) => { let Some(entry) = *socket.slot() else { set_errno(EBADF); return -1; }; match sockopt::kept(level, optname, matches!(entry.kind, SocketKind::Udp)) { @@ -843,18 +790,23 @@ pub unsafe extern "C" fn getpeername( addr: *mut Sockaddr, addrlen: *mut SocklenT, ) -> i32 { - let (ip, port) = match sock_from_fd(fd) { - Some(Sock::Stream(stream)) => match stream::endpoints(stream.handle) { - Ok((peer, _)) => peer, + let peer = match sock_from_fd(fd) { + Some(Sock::Stream(held)) => match stream::endpoints(held.handle) { + Ok((peer, _)) => Some(peer), Err(refused) => { set_errno(crate::errcode::of_stream(refused)); return -1; } }, + Some(Sock::Datagram(held)) => match socket::peer(held.handle) { + Ok(peer) => peer, + Err(e) => { set_errno(net_err_to_errno(e)); return -1; } + }, + Some(Sock::Listener(_)) => None, Some(Sock::Entry(mut socket)) => match *socket.slot() { - Some(entry) if entry.connected => (entry.remote_addr, entry.remote_port), - Some(_) => { set_errno(ENOTCONN); return -1; } + Some(entry) => entry.peer, None => { set_errno(EBADF); return -1; } }, None => { set_errno(EBADF); return -1; } }; + let Some((ip, port)) = peer else { set_errno(ENOTCONN); return -1; }; fill_sockaddr(addr, addrlen, ip, port); 0 } @@ -866,12 +818,16 @@ pub unsafe extern "C" fn getsockname( addrlen: *mut SocklenT, ) -> i32 { let local_port = match sock_from_fd(fd) { - Some(Sock::Stream(stream)) => match stream::endpoints(stream.handle) { + Some(Sock::Stream(held)) => match stream::endpoints(held.handle) { Ok((_, local_port)) => local_port, Err(refused) => { set_errno(crate::errcode::of_stream(refused)); return -1; } }, + Some(Sock::Listener(held) | Sock::Datagram(held)) => match socket::local(held.handle) { + Ok((_, port)) => port, + Err(e) => { set_errno(net_err_to_errno(e)); return -1; } + }, Some(Sock::Entry(mut socket)) => match *socket.slot() { - Some(entry) => entry.local_port, + Some(_) => 0, None => { set_errno(EBADF); return -1; } }, None => { set_errno(EBADF); return -1; } diff --git a/sdk/std/os/io.rs b/sdk/std/os/io.rs index 51fbf81c4e3..a6a634c31ca 100644 --- a/sdk/std/os/io.rs +++ b/sdk/std/os/io.rs @@ -68,6 +68,7 @@ as_raw_fd! { crate::process::ChildStderr => |p| p.as_inner().raw_fd(), borrowed; crate::net::TcpStream => |s| s.as_inner().as_raw_fd(); crate::net::TcpListener => |l| l.as_inner().as_raw_fd(); + crate::net::UdpSocket => |u| u.as_inner().as_raw_fd(), borrowed; } /// A stream's descriptor is read and written through its region, never as a @@ -154,6 +155,56 @@ impl From for fd::OwnedFd { } } +/// A listener's and a datagram socket's descriptor, given out and taken back +/// as a stream's is: taken back, one whose socket this process holds an end of +/// of that kind shares it, and any other panics. A process holds an end by its +/// listen or bind, or a spawn that gave it a holder. +macro_rules! socket_fd { + ($($ty:ident, $kind:ident, $what:literal);+ $(;)?) => {$( + #[stable(feature = "toyos_ext", since = "1.0.0")] + impl FromRawFd for crate::net::$ty { + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> crate::net::$ty { + // SAFETY: the caller's, as `FromRawFd` states it. + crate::net::$ty::from(unsafe { fd::OwnedFd::from_raw_fd(fd) }) + } + } + + #[stable(feature = "toyos_ext", since = "1.0.0")] + impl IntoRawFd for crate::net::$ty { + #[inline] + fn into_raw_fd(self) -> RawFd { + self.into_inner().into_fd().into_raw_fd() + } + } + + #[stable(feature = "toyos_ext", since = "1.0.0")] + impl From for crate::net::$ty { + #[inline] + fn from(owned: fd::OwnedFd) -> Self { + let handle = owned.as_raw_handle(); + if toyos::net::socket::kind(toyos_abi::RawHandle(handle)) != Some(toyos::net::socket::Kind::$kind) { + panic!(concat!("toyos::net: handle {} is no ", $what, " of this process's: one reaches a process by its own bind or a spawn that gave it a holder of one"), handle); + } + crate::net::$ty::from_inner(crate::sys::net::$ty::from_fd(owned)) + } + } + + #[stable(feature = "toyos_ext", since = "1.0.0")] + impl From for fd::OwnedFd { + #[inline] + fn from(socket: crate::net::$ty) -> Self { + socket.into_inner().into_fd() + } + } + )+}; +} + +socket_fd! { + TcpListener, Listener, "listener"; + UdpSocket, Datagram, "datagram socket"; +} + /// Switch stdin between canonical and raw mode. #[stable(feature = "toyos_ext", since = "1.0.0")] pub fn set_stdin_raw(raw: bool) { diff --git a/sdk/std/os/process.rs b/sdk/std/os/process.rs index 24a64bb64d6..90ff09c4d56 100644 --- a/sdk/std/os/process.rs +++ b/sdk/std/os/process.rs @@ -14,15 +14,16 @@ pub fn tty_piped() -> crate::process::Stdio { pub trait CommandExt { /// Give the child descriptor `child_fd`, naming the kernel handle /// `parent_handle`, which this process keeps: the spawn duplicates it, or, - /// for a stream's, gives the child a holder of the stream of its own. One - /// of 0 to 2 replaces what the command's stdio would have given. + /// for a stream's, a listener's or a datagram socket's, gives the child a + /// holder of its own. One of 0 to 2 replaces what the command's stdio + /// would have given. #[stable(feature = "toyos_ext", since = "1.0.0")] fn inherit_handle(&mut self, child_fd: u32, parent_handle: u32) -> &mut Self; - /// Give the child descriptor `child_fd` as the stream end `end`, which - /// `toyos::net::stream::export` made and which this process keeps: the - /// spawn duplicates it, and the child takes it up as a holder of the - /// stream. + /// Give the child descriptor `child_fd` as the exported end `end`, which + /// `toyos::net`'s `export` made and which this process keeps: the spawn + /// duplicates it, and the child takes it up as a holder of the stream or + /// the socket. #[stable(feature = "toyos_ext", since = "1.0.0")] fn inherit_exported(&mut self, child_fd: u32, end: u32) -> &mut Self; diff --git a/sdk/std/sys/net/connection.rs b/sdk/std/sys/net/connection.rs index 528406193d6..8f40c276309 100644 --- a/sdk/std/sys/net/connection.rs +++ b/sdk/std/sys/net/connection.rs @@ -1,13 +1,12 @@ +use toyos::net::socket; use toyos::net::stream::{self, Refused, Wait}; -use toyos::net::{NetError, TcpSocketId, UdpSocketId}; +use toyos::net::NetError; use toyos_abi::RawHandle; -use toyos_abi::syscall::{self, SyscallError}; use crate::fmt; use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut}; use crate::net::{Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, SocketAddrV4, ToSocketAddrs}; use crate::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; -use crate::sync::Arc; use crate::sync::atomic::Ordering::Relaxed; use crate::sync::atomic::{AtomicBool, AtomicU32}; use crate::time::Duration; @@ -21,10 +20,11 @@ fn stream_fd(handle: RawHandle) -> io::Result { Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } -/// A kernel handle this process just got, as an owned descriptor. -fn owned(handle: RawHandle) -> io::Result { - let fd = crate::sys::new_fd(handle)?; - // SAFETY: a descriptor just installed for this handle alone. +/// A listener's or a datagram socket's handle this process just got, as an +/// owned descriptor. +fn socket_fd(handle: RawHandle) -> io::Result { + let fd = toyos::fd::install(socket::descriptor(handle), false, 0).map_err(crate::sys::fd_error)?; + // SAFETY: a descriptor just installed for this socket alone. Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } @@ -44,6 +44,8 @@ fn net_err_to_io(e: NetError) -> io::Error { NetError::InvalidInput => io::ErrorKind::InvalidInput, NetError::NetstackNotFound => io::ErrorKind::NotConnected, NetError::PermissionDenied => io::ErrorKind::PermissionDenied, + NetError::WouldBlock => return io::ErrorKind::WouldBlock.into(), + NetError::NotASocket => io::ErrorKind::InvalidInput, NetError::ResourceExhausted | NetError::Protocol(_) | NetError::Io => io::ErrorKind::Other, }; io::Error::new(kind, "netstack error") @@ -63,13 +65,6 @@ fn duration_to_ms(d: Option) -> u32 { } } -fn syscall_err(e: SyscallError) -> io::Error { - match e { - SyscallError::WouldBlock => io::ErrorKind::WouldBlock.into(), - _ => io::Error::new(io::ErrorKind::Other, "syscall error"), - } -} - /// How long `TcpStream::connect` waits for each address to answer. const CONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -108,22 +103,6 @@ fn wait(nonblocking: bool, timeout_ms: u32) -> Wait { } } -// --- Shared socket ownership (prevents double-close on duplicate) --- - -enum NetstackSocket { - Tcp(TcpSocketId), - Udp(UdpSocketId), -} - -impl Drop for NetstackSocket { - fn drop(&mut self) { - let _ = match self { - NetstackSocket::Tcp(id) => toyos::net::tcp_close(*id), - NetstackSocket::Udp(id) => toyos::net::udp_close(*id), - }; - } -} - // --- TcpStream --- /// A stream's handle, which `toyos::net::stream` holds its one end by: every @@ -332,19 +311,26 @@ impl fmt::Debug for TcpStream { // --- TcpListener --- +/// A listener's handle, which `toyos::net::socket` holds this process's holder +/// of the listener by: every descriptor of it shares the holder. The handle's +/// drop lets the holder go, and the listener goes with its last holder. pub struct TcpListener { - notify_fd: OwnedFd, - socket: Arc, - local: SocketAddr, + fd: OwnedFd, nonblocking: AtomicBool, } impl TcpListener { - fn socket_id(&self) -> TcpSocketId { - match *self.socket { - NetstackSocket::Tcp(id) => id, - _ => unreachable!(), - } + /// A listener on `fd`, a handle `toyos::net::socket` holds a listener's end for. + pub(crate) fn from_fd(fd: OwnedFd) -> TcpListener { + TcpListener { fd, nonblocking: AtomicBool::new(false) } + } + + pub(crate) fn into_fd(self) -> OwnedFd { + self.fd + } + + fn handle(&self) -> RawHandle { + kernel(&self.fd) } pub fn bind(addr: A) -> io::Result { @@ -353,29 +339,18 @@ impl TcpListener { .next() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no addresses found"))?; let (ip, port) = addr_to_v4(&addr)?; - let bound = toyos::net::tcp_bind(ip, port).map_err(net_err_to_io)?; - Ok(TcpListener { - notify_fd: owned(bound.notify.into_raw())?, - socket: Arc::new(NetstackSocket::Tcp(bound.socket_id)), - local: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), bound.bound_port)), - nonblocking: AtomicBool::new(false), - }) + let bound = socket::listen(ip, port, toyos::net::TcpOptions::new(false)).map_err(net_err_to_io)?; + Ok(TcpListener::from_fd(socket_fd(bound.handle)?)) } pub fn socket_addr(&self) -> io::Result { - Ok(self.local) + let (addr, port) = socket::local(self.handle()).map_err(net_err_to_io)?; + Ok(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(addr), port))) } pub fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> { - let mut byte = [0u8; 1]; - let notify_fd = kernel(&self.notify_fd); - if self.nonblocking.load(Relaxed) { - syscall::read_nonblock(notify_fd, &mut byte).map_err(syscall_err)?; - } else { - syscall::read(notify_fd, &mut byte).map_err(syscall_err)?; - } - - let accepted = stream::accept(self.socket_id()).map_err(net_err_to_io)?; + let wait = if self.nonblocking.load(Relaxed) { Wait::Nonblocking } else { Wait::Blocking(None) }; + let accepted = socket::accept(self.handle(), wait).map_err(net_err_to_io)?; let peer = SocketAddr::V4(SocketAddrV4::new( Ipv4Addr::from(accepted.remote_addr), accepted.remote_port, @@ -384,12 +359,7 @@ impl TcpListener { } pub fn duplicate(&self) -> io::Result { - Ok(TcpListener { - notify_fd: self.notify_fd.try_clone()?, - socket: Arc::clone(&self.socket), - local: self.local, - nonblocking: AtomicBool::new(self.nonblocking.load(Relaxed)), - }) + Ok(TcpListener { fd: self.fd.try_clone()?, nonblocking: AtomicBool::new(self.nonblocking.load(Relaxed)) }) } pub fn set_ttl(&self, _ttl: u32) -> io::Result<()> { @@ -418,43 +388,46 @@ impl TcpListener { } pub fn as_fd(&self) -> BorrowedFd<'_> { - self.notify_fd.as_fd() + self.fd.as_fd() } pub fn as_raw_fd(&self) -> i32 { - self.notify_fd.as_raw_fd() + self.fd.as_raw_fd() } } -// No Drop impl — Arc handles close on last drop. - impl fmt::Debug for TcpListener { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "TcpListener(fd={}, local={})", self.notify_fd.as_raw_fd(), self.local) + write!(f, "TcpListener(fd={})", self.fd.as_raw_fd()) } } // --- UdpSocket --- +/// A datagram socket's handle, as [`TcpListener`]'s is a listener's. Where a +/// send that names no address goes is this process's (`socket::set_peer`), +/// shared by every descriptor of the socket here. pub struct UdpSocket { - socket: Arc, - tx_fd: OwnedFd, - rx_fd: OwnedFd, - local: SocketAddr, - peer: crate::sync::Mutex>, - /// What netstack holds for the socket every duplicate names, locked across - /// the request that changes it so the two cannot come to differ. - broadcast: Arc>, + fd: OwnedFd, read_timeout_ms: AtomicU32, + /// Kept for its getter: a send never waits, since netstack answers it at once. write_timeout_ms: AtomicU32, + nonblocking: AtomicBool, } impl UdpSocket { - fn socket_id(&self) -> UdpSocketId { - match *self.socket { - NetstackSocket::Udp(id) => id, - _ => unreachable!(), - } + /// A datagram socket on `fd`, a handle `toyos::net::socket` holds a + /// datagram socket's end for. + pub(crate) fn from_fd(fd: OwnedFd) -> UdpSocket { + UdpSocket { fd, read_timeout_ms: AtomicU32::new(0), write_timeout_ms: AtomicU32::new(0), nonblocking: AtomicBool::new(false) } + } + + pub(crate) fn into_fd(self) -> OwnedFd { + self.fd + } + + fn handle(&self) -> RawHandle { + kernel(&self.fd) } pub fn bind(addr: A) -> io::Result { @@ -463,42 +436,29 @@ impl UdpSocket { .next() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no addresses found"))?; let (ip, port) = addr_to_v4(&addr)?; - let bound = toyos::net::udp_bind(ip, port).map_err(net_err_to_io)?; - Ok(UdpSocket { - socket: Arc::new(NetstackSocket::Udp(bound.socket_id)), - tx_fd: owned(bound.tx.into_raw())?, - rx_fd: owned(bound.rx.into_raw())?, - local: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), bound.bound_port)), - peer: crate::sync::Mutex::new(None), - broadcast: Arc::new(crate::sync::Mutex::new(false)), - read_timeout_ms: AtomicU32::new(0), - write_timeout_ms: AtomicU32::new(0), - }) + let bound = socket::bind(ip, port).map_err(net_err_to_io)?; + Ok(UdpSocket::from_fd(socket_fd(bound.handle)?)) } pub fn peer_addr(&self) -> io::Result { - self.peer - .lock() - .unwrap() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "not connected")) + match socket::peer(self.handle()).map_err(net_err_to_io)? { + Some((addr, port)) => Ok(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(addr), port))), + None => Err(io::Error::new(io::ErrorKind::NotConnected, "not connected")), + } } pub fn socket_addr(&self) -> io::Result { - Ok(self.local) + let (addr, port) = socket::local(self.handle()).map_err(net_err_to_io)?; + Ok(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(addr), port))) } pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { - let resp = - toyos::net::udp_recv_from(self.socket_id(), buf.len() as u32).map_err(net_err_to_io)?; - - let n = (resp.len as usize).min(buf.len()); - if n > 0 { - let rx_fd = kernel(&self.rx_fd); - syscall::read(rx_fd, &mut buf[..n]).map_err(syscall_err)?; - } - - let addr = Ipv4Addr::new(resp.addr[0], resp.addr[1], resp.addr[2], resp.addr[3]); - Ok((n, SocketAddr::V4(SocketAddrV4::new(addr, resp.port)))) + let wait = wait(self.nonblocking.load(Relaxed), self.read_timeout_ms.load(Relaxed)); + let received = socket::recv_from(self.handle(), buf, wait).map_err(|e| match e { + NetError::TimedOut if wait != Wait::Nonblocking => io::ErrorKind::WouldBlock.into(), + e => net_err_to_io(e), + })?; + Ok((received.len, SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(received.addr), received.port)))) } pub fn peek_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { @@ -507,28 +467,15 @@ impl UdpSocket { pub fn send_to(&self, buf: &[u8], addr: &SocketAddr) -> io::Result { let (ip, port) = addr_to_v4(addr)?; - - // Write data to TX pipe first, then send control message - let tx_fd = kernel(&self.tx_fd); - if !buf.is_empty() { - syscall::write(tx_fd, buf).map_err(syscall_err)?; - } - - let sent = toyos::net::udp_send_to(self.socket_id(), ip, port, buf.len() as u16) - .map_err(net_err_to_io)?; - Ok(sent as usize) + socket::send_to(self.handle(), ip, port, buf).map_err(net_err_to_io) } pub fn duplicate(&self) -> io::Result { Ok(UdpSocket { - socket: Arc::clone(&self.socket), - tx_fd: self.tx_fd.try_clone()?, - rx_fd: self.rx_fd.try_clone()?, - local: self.local, - peer: crate::sync::Mutex::new(*self.peer.lock().unwrap()), - broadcast: Arc::clone(&self.broadcast), + fd: self.fd.try_clone()?, read_timeout_ms: AtomicU32::new(self.read_timeout_ms.load(Relaxed)), write_timeout_ms: AtomicU32::new(self.write_timeout_ms.load(Relaxed)), + nonblocking: AtomicBool::new(self.nonblocking.load(Relaxed)), }) } @@ -553,15 +500,11 @@ impl UdpSocket { } pub fn set_broadcast(&self, broadcast: bool) -> io::Result<()> { - let mut held = self.broadcast.lock().unwrap(); - toyos::net::udp_set_option(self.socket_id(), toyos::net::OPT_BROADCAST, broadcast as u32) - .map_err(net_err_to_io)?; - *held = broadcast; - Ok(()) + socket::set_option(self.handle(), toyos::net::OPT_BROADCAST, broadcast as u32).map_err(net_err_to_io) } pub fn broadcast(&self) -> io::Result { - Ok(*self.broadcast.lock().unwrap()) + socket::option(self.handle(), toyos::net::OPT_BROADCAST).map(|on| on != 0).map_err(net_err_to_io) } pub fn set_multicast_loop_v4(&self, _: bool) -> io::Result<()> { @@ -616,7 +559,8 @@ impl UdpSocket { Ok(None) } - pub fn set_nonblocking(&self, _: bool) -> io::Result<()> { + pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { + self.nonblocking.store(nonblocking, Relaxed); Ok(()) } @@ -630,11 +574,7 @@ impl UdpSocket { } pub fn send(&self, buf: &[u8]) -> io::Result { - let peer = self - .peer - .lock() - .unwrap() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "not connected"))?; + let peer = self.peer_addr()?; self.send_to(buf, &peer) } @@ -643,17 +583,18 @@ impl UdpSocket { .to_socket_addrs()? .next() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no addresses found"))?; - *self.peer.lock().unwrap() = Some(addr); - Ok(()) + let peer = addr_to_v4(&addr)?; + socket::set_peer(self.handle(), Some(peer)).map_err(net_err_to_io) } -} -// No Drop impl — Arc handles close on last drop. -// OwnedFd drops close the pipe fds. + pub fn as_raw_fd(&self) -> i32 { + self.fd.as_raw_fd() + } +} impl fmt::Debug for UdpSocket { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "UdpSocket(local={})", self.local) + write!(f, "UdpSocket(fd={})", self.fd.as_raw_fd()) } } diff --git a/sdk/std/sys/process.rs b/sdk/std/sys/process.rs index 305ca23e88c..d8211ddad77 100644 --- a/sdk/std/sys/process.rs +++ b/sdk/std/sys/process.rs @@ -196,16 +196,16 @@ impl Command { /// Give the child descriptor `child_fd`, naming this process's /// `parent_handle`, which this process keeps: the spawn duplicates it, or, - /// for a stream's, gives the child a holder of the stream of its own. + /// for a stream's or a socket's, gives the child a holder of its own. pub fn inherit_handle(&mut self, child_fd: u32, parent_handle: u32) { self.inherit(child_fd, parent_handle, toyos::fd::Kind::Handle); } - /// Give the child descriptor `child_fd` as the stream end `end`, which - /// `toyos::net::stream::export` made: the spawn duplicates it, and the - /// child takes it up as a holder of the stream. + /// Give the child descriptor `child_fd` as the exported end `end`, which + /// `toyos::net`'s `export` made: the spawn duplicates it, and the child + /// takes it up as a holder of the stream or the socket. pub fn inherit_exported(&mut self, child_fd: u32, end: u32) { - self.inherit(child_fd, end, toyos::fd::Kind::Stream); + self.inherit(child_fd, end, toyos::fd::Kind::Exported); } fn inherit(&mut self, child_fd: u32, handle: u32, kind: toyos::fd::Kind) { @@ -331,11 +331,16 @@ impl Command { let mut fds: Vec<(u32, toyos_abi::RawHandle, toyos::fd::Kind)> = Vec::new(); for &(fd, handle, kind) in &self.extra_fds { let handle = toyos_abi::RawHandle(handle); - if toyos::net::stream::is_stream(handle) { - let end = toyos::net::stream::export(handle).map_err(|e| refused(toyos::spawn::Refused::Export(e)))?; - held.push((fd, end, toyos::fd::Kind::Stream)); + let exported = if toyos::net::stream::is_stream(handle) { + Some(toyos::net::stream::export(handle)) + } else if toyos::net::socket::is_socket(handle) { + Some(toyos::net::socket::export(handle)) } else { - fds.push((fd, handle, kind)); + None + }; + match exported { + Some(end) => held.push((fd, end.map_err(|e| refused(toyos::spawn::Refused::Export(e)))?, toyos::fd::Kind::Exported)), + None => fds.push((fd, handle, kind)), } } fds.extend(held.iter().map(|(fd, h, kind)| (*fd, toyos::AsHandle::as_handle(h), *kind))); diff --git a/sdk/toyos/src/fd.rs b/sdk/toyos/src/fd.rs index 850ad2218df..494a0398e2c 100644 --- a/sdk/toyos/src/fd.rs +++ b/sdk/toyos/src/fd.rs @@ -22,9 +22,9 @@ //! //! **A process's descriptors arrive as endowments**, `fd:` (`FD_PREFIX`) //! or `fd::` ([`Kind`]), each installed at its number at the table's -//! first use; its stdio is descriptors 0 to 2 among them. A stream's arrives -//! as an exported end, which the table takes up as this process's holder -//! (`net::stream::adopt_exported`). +//! first use; its stdio is descriptors 0 to 2 among them. A stream's, a +//! listener's and a datagram socket's arrive as an exported end, which the +//! table takes up as this process's holder (`net::adopt_exported`). use toyos_abi::RawHandle; @@ -58,6 +58,11 @@ pub const STREAM: u32 = 2; /// The tag of a directory the kernel serves, which has no handle /// (`crate::files`). pub const KERNEL_DIR: u32 = 3; +/// The tag of a listener's or a datagram socket's descriptor +/// (`crate::net::socket`): a copy of its bell, which a poller watches and no +/// file call reaches, let go by the copy of this crate that holds the +/// socket's end. +pub const SOCKET: u32 = 4; /// What a descriptor names. #[repr(C)] @@ -87,6 +92,12 @@ impl Object { Self { handle, tag: STREAM, word: handle.0 as u64, release: Some(release) } } + /// A listener's or a datagram socket's bell, which `release`, of the copy + /// of this crate holding the socket's end, lets go of as [`Self::stream`]. + pub const fn socket(handle: RawHandle, release: extern "C" fn(u64)) -> Self { + Self { handle, tag: SOCKET, word: handle.0 as u64, release: Some(release) } + } + /// The kernel handle a kernel call on the descriptor is made on: `None` /// for an object a library keeps that is no handle. pub fn kernel_handle(&self) -> Option { @@ -212,21 +223,24 @@ pub fn replace(fd: Fd, object: Object) -> Result<(), Error> { } /// What a child is given for `fd`, and its kind: a copy of the kernel handle -/// it names, or for a stream an end of a new holder of it -/// (`net::stream::export`). `None` for a descriptor that is not open, as a -/// child of POSIX's holds no closed one. Refused for an object that is no -/// kernel handle, for one the kernel will not duplicate, and for a stream -/// netstack makes no holder of. +/// it names, or for a stream, a listener or a datagram socket an end of a new +/// holder of it (`net::stream::export`, `net::socket::export`). `None` for a +/// descriptor that is not open, as a child of POSIX's holds no closed one. +/// Refused for an object that is no kernel handle, for one the kernel will +/// not duplicate, and for a socket netstack makes no holder of. pub fn copy(fd: Fd) -> Result, crate::spawn::Refused> { use crate::spawn::Refused; let copied = with(fd, |o| match o.kernel_handle() { + _ if o.tag == SOCKET && crate::net::socket::is_socket(o.handle) => { + crate::net::socket::export(o.handle).map(|end| (end, Kind::Exported)).map_err(Refused::Export) + } Some(handle) if crate::net::stream::is_stream(handle) => { - crate::net::stream::export(handle).map(|end| (end, Kind::Stream)).map_err(Refused::Export) + crate::net::stream::export(handle).map(|end| (end, Kind::Exported)).map_err(Refused::Export) } Some(handle) if o.tag != STREAM => { toyos_abi::syscall::dup(handle).map(|copy| (crate::OwnedHandle(copy), Kind::Handle)).map_err(Refused::Syscall) } - // A stream another copy of this crate holds the end of, or no handle. + // A socket another copy of this crate holds the end of, or no handle. _ => Err(Refused::Syscall(toyos_abi::syscall::SyscallError::NotSupported)), }); copied.map_or(Ok(None), |copied| copied.map(Some)) @@ -237,9 +251,9 @@ pub fn copy(fd: Fd) -> Result, crate::spawn:: pub enum Kind { /// The kernel handle the descriptor names: `fd:`. Handle, - /// A stream's exported end, which the child takes up as its holder: - /// `fd::stream`. - Stream, + /// A stream's, a listener's or a datagram socket's exported end, which + /// the child takes up as its holder: `fd::exported`. + Exported, } impl Kind { @@ -247,7 +261,7 @@ impl Kind { pub const fn suffix(self) -> &'static str { match self { Self::Handle => "", - Self::Stream => ":stream", + Self::Exported => ":exported", } } } @@ -269,7 +283,7 @@ pub fn endowed_fd(label: &str) -> Option<(Fd, Kind)> { let rest = label.strip_prefix(toyos_abi::syscall::FD_PREFIX)?; let (digits, kind) = match rest.split_once(':') { None => (rest, Kind::Handle), - Some((digits, "stream")) => (digits, Kind::Stream), + Some((digits, "exported")) => (digits, Kind::Exported), Some(_) => return None, }; let canonical = digits.bytes().all(|b| b.is_ascii_digit()) && (digits == "0" || !digits.starts_with('0')); @@ -317,9 +331,9 @@ mod owner { }) } - /// Install each `fd:` endowment at its number, a stream's taken up as + /// Install each `fd:` endowment at its number, a socket's taken up as /// this process's holder first. One whose number another already took, - /// or whose stream this process holds no room for, is refused, its handle + /// or whose socket this process holds no room for, is refused, its handle /// closed: how many were. fn endow(table: &mut Table, endowed: &Endowments) -> Result { let mut refused = 0; @@ -328,8 +342,8 @@ mod owner { let Some(handle) = endowed.take::(label) else { continue }; let object = match kind { Kind::Handle => Object::handle(handle.into_raw()), - Kind::Stream => match crate::net::stream::adopt_exported(handle) { - Some(descriptor) => crate::net::stream::descriptor(descriptor), + Kind::Exported => match crate::net::adopt_exported(handle) { + Some(object) => object, None => { refused += 1; continue; @@ -441,13 +455,13 @@ mod tests { fn an_fd_label_names_its_number_and_kind_and_no_other_label_names_one() { assert_eq!(endowed_fd("fd:0"), Some((0, Kind::Handle))); assert_eq!(endowed_fd("fd:4095"), Some((4095, Kind::Handle))); - assert_eq!(endowed_fd("fd:3:stream"), Some((3, Kind::Stream))); - for kind in [Kind::Handle, Kind::Stream] { + assert_eq!(endowed_fd("fd:3:exported"), Some((3, Kind::Exported))); + for kind in [Kind::Handle, Kind::Exported] { assert_eq!(endowed_fd(&format!("fd:7{}", kind.suffix())), Some((7, kind)), "a label of its own"); } for other in [ "fd:4096", "fd:-1", "fd:", "fd:1x", "svc", "fd1", "dev:fd:1", "fd:+3", "fd:03", "fd:00", "fd: 3", - "fd:3:", "fd:3:file", "fd:3:stream:", "fd::stream", "fd:4096:stream", + "fd:3:", "fd:3:file", "fd:3:stream", "fd:3:exported:", "fd::exported", "fd:4096:exported", ] { assert_eq!(endowed_fd(other), None, "{other}"); } diff --git a/sdk/toyos/src/ipc.rs b/sdk/toyos/src/ipc.rs index 3c901891489..d094c00c98e 100644 --- a/sdk/toyos/src/ipc.rs +++ b/sdk/toyos/src/ipc.rs @@ -363,6 +363,11 @@ pub fn send_bytes(handle: RawHandle, msg_type: u32, data: &[u8]) -> Result<(), I message(handle, msg_type, data, none(), false).map_err(blocking) } +/// [`Connection::signal_with`] on a raw handle. +pub(crate) fn signal_with(handle: RawHandle, msg_type: u32, handles: impl IntoIterator) -> Result<(), IpcError> { + message(handle, msg_type, &[], handles, false).map_err(blocking) +} + /// Send a framed payload without ever blocking, or refuse the whole frame. /// /// A server writing to a client it does not trust to read cannot use [`send`]: @@ -764,7 +769,8 @@ fn fill(conn: &Connection, buf: &mut [u8], len: &mut usize) -> Fill { Fill::Filled } -fn as_bytes(val: &T) -> &[u8] { +/// A payload's bytes, as a frame carries them. +pub(crate) fn as_bytes(val: &T) -> &[u8] { unsafe { core::slice::from_raw_parts(val as *const T as *const u8, core::mem::size_of::()) } } diff --git a/sdk/toyos/src/launch.rs b/sdk/toyos/src/launch.rs index f57b7c07ee3..46c37339d0c 100644 --- a/sdk/toyos/src/launch.rs +++ b/sdk/toyos/src/launch.rs @@ -82,7 +82,7 @@ const FD_BYTES: usize = 8; const fn kind_word(kind: Kind) -> u32 { match kind { Kind::Handle => 0, - Kind::Stream => 1, + Kind::Exported => 1, } } @@ -90,7 +90,7 @@ const fn kind_word(kind: Kind) -> u32 { const fn kind_of(word: u32) -> Option { match word { 0 => Some(Kind::Handle), - 1 => Some(Kind::Stream), + 1 => Some(Kind::Exported), _ => None, } } @@ -503,7 +503,7 @@ mod tests { #[test] fn a_descriptor_crosses_with_its_kind() { let mut buf = [0u8; 256]; - let fds = [(0, RawHandle(5), Kind::Handle), (1, RawHandle(6), Kind::Stream)]; + let fds = [(0, RawHandle(5), Kind::Handle), (1, RawHandle(6), Kind::Exported)]; let request = Launch { program: "/system/bin/cat", argv: b"", @@ -516,7 +516,7 @@ mod tests { }; let len = request.encode(&mut buf).expect("encode a launch"); let decoded = Request::decode(&buf[..len]).expect("decode it"); - assert_eq!(decoded.fds().collect::>(), [(0, Some(Kind::Handle)), (1, Some(Kind::Stream))]); + assert_eq!(decoded.fds().collect::>(), [(0, Some(Kind::Handle)), (1, Some(Kind::Exported))]); assert_eq!(decoded.handle_count(), 3, "two descriptors and the job"); let second_kind = HEADER + FD_BYTES + 4; buf[second_kind..second_kind + 4].copy_from_slice(&7u32.to_le_bytes()); diff --git a/sdk/toyos/src/net.rs b/sdk/toyos/src/net.rs index 6281b60b1af..753721cfe54 100644 --- a/sdk/toyos/src/net.rs +++ b/sdk/toyos/src/net.rs @@ -2,49 +2,43 @@ //! //! Owns the netstack IPC protocol and provides client functions for TCP, UDP, and DNS. //! All networking in ToyOS goes through the `netstack` daemon: a TCP stream is -//! a region and a connection ([`stream`]), and a listener and a datagram socket -//! carry their bytes and wakes in kernel pipes. +//! a region and a connection ([`stream`]), and a listener and a datagram +//! socket a connection and a bell ([`socket`]). **No request names a socket +//! by a number**: each is named by the connection only its holders hold. +mod ends; +pub mod socket; pub mod stream; use crate::ipc::{IpcError, IpcHeader, IpcPayload}; use crate::ipc_payload; -use crate::{Connection, OwnedHandle, Pipe, RawHandle}; +use crate::{Connection, OwnedHandle, RawHandle}; +/// The requests a new connection to netstack carries. Each that makes a +/// socket makes the connection its first holder's; a request on a socket is +/// that holder's own ([`socket`], [`stream`]). #[repr(u32)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MsgType { - TcpClose = 4, + /// [`UdpBindRequest`], answered [`BoundResponse`] with the holder's bell + /// ([`socket::bind`]). UdpBind = 8, - UdpSendTo = 9, - UdpRecvFrom = 10, - UdpClose = 11, DnsLookup = 12, - TcpListenerSetOption = 14, - UdpSetOption = 15, - TcpBindPiped = 21, /// A stream's region with [`TcpConnectRequest`], answered with its two /// bells ([`stream::connect`]). TcpConnect = 23, - /// A stream's region with [`TcpAcceptRequest`], answered with its two - /// bells ([`stream::accept`]). - TcpAccept = 24, + /// [`TcpListenRequest`], answered [`BoundResponse`] with the holder's + /// bell ([`socket::listen`]). + TcpListen = 25, } impl MsgType { pub fn from_u32(v: u32) -> Option { match v { - 4 => Some(Self::TcpClose), 8 => Some(Self::UdpBind), - 9 => Some(Self::UdpSendTo), - 10 => Some(Self::UdpRecvFrom), - 11 => Some(Self::UdpClose), 12 => Some(Self::DnsLookup), - 14 => Some(Self::TcpListenerSetOption), - 15 => Some(Self::UdpSetOption), - 21 => Some(Self::TcpBindPiped), 23 => Some(Self::TcpConnect), - 24 => Some(Self::TcpAccept), + 25 => Some(Self::TcpListen), _ => None, } } @@ -83,15 +77,18 @@ pub const ERR_RESOURCE_EXHAUSTED: u32 = 7; /// broadcast address from a socket whose [`OPT_BROADCAST`] is off. The request /// was well formed, which is what keeps this apart from [`ERR_INVALID_INPUT`]. pub const ERR_PERMISSION_DENIED: u32 = 8; +/// A holder's accept or receive found nothing to take: its bell rings once +/// there is something ([`socket`]). +pub const ERR_WOULD_BLOCK: u32 = 9; pub const ERR_OTHER: u32 = 255; -/// Nagle's algorithm off, on a listener: its bind's [`TcpOptions`] and then -/// [`tcp_listener_set_option`], for every connection whose SYN arrives from -/// then on, and one that began before keeps what it has. A connection says -/// what it began with in its accept's answer; a stream's own is a word of its -/// region ([`stream::NODELAY`]). +/// Nagle's algorithm off, on a listener: its listen's [`TcpOptions`] and then +/// [`socket::set_option`], for every connection whose SYN arrives from then +/// on, and one that began before keeps what it has. A connection says what it +/// began with in its accept's answer; a stream's own is a word of its region +/// ([`stream::NODELAY`]). pub const OPT_NODELAY: u32 = 1; -/// A datagram socket may send to a broadcast address: [`udp_set_option`]. +/// A datagram socket may send to a broadcast address: [`socket::set_option`]. pub const OPT_BROADCAST: u32 = 2; /// The widest request payload netstack reads: a [`dns_lookup`]'s hostname, @@ -112,6 +109,10 @@ pub enum NetError { ResourceExhausted, /// See [`ERR_PERMISSION_DENIED`]. PermissionDenied, + /// See [`ERR_WOULD_BLOCK`], and a holder's call that does not wait. + WouldBlock, + /// A handle that reaches none of this process's sockets. + NotASocket, Protocol(u32), Io, } @@ -127,6 +128,7 @@ impl NetError { ERR_INVALID_INPUT => NetError::InvalidInput, ERR_RESOURCE_EXHAUSTED => NetError::ResourceExhausted, ERR_PERMISSION_DENIED => NetError::PermissionDenied, + ERR_WOULD_BLOCK => NetError::WouldBlock, ERR_OTHER => NetError::Io, // An older client meets a newer netstack here rather than at a panic: // an unknown code is still an error, and still says which one. @@ -135,30 +137,6 @@ impl NetError { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct TcpSocketId(pub u32); - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct UdpSocketId(pub u32); - -// Protocol request/response structs - -/// A duplex data path is **two pipe ends, sent with the request that opens -/// it**, in this order. The client makes both pipes and keeps the ends facing -/// itself. -/// -/// Stated once because the two sides of the swap are in different programs: a -/// reversed pair is two working pipes carrying each other's bytes, which no -/// type here can catch. -pub const DATA_HANDLES: usize = 2; -/// The end netstack writes into and the client reads from. -pub const DATA_TO_CLIENT: usize = 0; -/// The end the client writes into and netstack reads from. -pub const DATA_FROM_CLIENT: usize = 1; - -/// A bind sends one end: the one netstack writes an accept notification into. -pub const NOTIFY_HANDLES: usize = 1; - ipc_payload! { /// [`MsgType::TcpConnect`], with the stream's region. pub struct TcpConnectRequest { @@ -175,11 +153,7 @@ ipc_payload! { pub _pad: u16, } - pub struct SocketCloseRequest { - pub socket_id: u32, - } - - pub struct TcpBindPipedRequest { + pub struct TcpListenRequest { pub addr: [u8; 4], pub port: u16, pub _pad: u16, @@ -187,17 +161,13 @@ ipc_payload! { pub options: TcpOptions, } - pub struct TcpBindResponse { - pub socket_id: u32, - pub bound_port: u16, + /// The answer to [`MsgType::TcpListen`] and [`MsgType::UdpBind`], with the + /// first holder's bell. + pub struct BoundResponse { + pub port: u16, pub _pad: u16, } - /// [`MsgType::TcpAccept`], with the stream's region. - pub struct TcpAcceptRequest { - pub listener: u32, - } - /// The options a listener holds, a word each: zero is off and anything /// else on. #[derive(Debug, PartialEq, Eq)] @@ -205,9 +175,10 @@ ipc_payload! { nodelay: u32, } - /// The answer to [`MsgType::TcpAccept`], with the stream's two bells: the peer, - /// and whether Nagle's algorithm is off for the stream as its listener - /// had it, which netstack has written into the region's header too. + /// The answer to a listener holder's [`socket::ACCEPT`], with the stream's + /// connection and its two bells: the peer, and whether Nagle's algorithm + /// is off for the stream as its listener had it, which netstack has + /// written into the region's header too. pub struct TcpAcceptResponse { pub remote_addr: [u8; 4], pub remote_port: u16, @@ -221,45 +192,9 @@ ipc_payload! { pub _pad: u16, } - pub struct UdpBindResponse { - pub socket_id: u32, - pub bound_port: u16, - pub _pad: u16, - } - - pub struct UdpSendToRequest { - pub socket_id: u32, - pub addr: [u8; 4], - pub port: u16, - pub len: u16, - } - - pub struct UdpRecvFromRequest { - pub socket_id: u32, - pub max_len: u32, - } - - pub struct UdpRecvResponse { - pub addr: [u8; 4], - pub port: u16, - pub len: u16, - } - - /// [`MsgType::TcpListenerSetOption`] and [`MsgType::UdpSetOption`]: the - /// request's type says which kind of socket `socket_id` names. - pub struct SocketOptionRequest { - pub socket_id: u32, - pub option: u32, - pub value: u32, - } - pub struct ErrorResponse { pub code: u32, } - - struct SentBytes { - value: u32, - } } impl TcpOptions { @@ -273,21 +208,6 @@ impl TcpOptions { } } -// Return types - -pub struct TcpBound { - pub notify: Pipe, - pub socket_id: TcpSocketId, - pub bound_port: u16, -} - -pub struct UdpBound { - pub socket_id: UdpSocketId, - pub bound_port: u16, - pub tx: Pipe, - pub rx: Pipe, -} - // NetstackConn — per-operation IPC connection (typestate protocol) pub struct NetstackConn(Connection); @@ -344,6 +264,37 @@ impl NetstackConn { } } +/// The widest first frame an exported end carries. +const MAX_ADOPT: usize = { + let (stream, socket) = (core::mem::size_of::(), core::mem::size_of::()); + if stream > socket { stream } else { socket } +}; + +/// An exported end taken up as this process's holder of what its first frame +/// says it holds, a socket ([`socket::ADOPT`]) or else a stream +/// (`stream::adopted`'s rules), as its descriptor's object. The frame +/// is read without waiting: netstack queued it before it answered the export, +/// so it is there or never sent. `None`, with everything closed, where the +/// process takes up no holder of it. +pub fn adopt_exported(end: OwnedHandle) -> Option { + let connection = Connection::received(end.into_raw()); + let mut frames: crate::ipc::FrameRx = crate::ipc::FrameRx::new(); + let first = match frames.pump(&connection) { + crate::ipc::RxStep::Frame { msg_type, payload_len, .. } => { + let handles = frames.take_batch(); + Some((msg_type, frames.payload(payload_len), handles)) + } + _ => None, + }; + match first { + Some((socket::ADOPT, payload, handles)) => socket::adopted(connection, payload, handles).map(socket::descriptor), + first => { + let first = first.filter(|&(msg_type, ..)| msg_type == stream::ADOPT).map(|(_, payload, handles)| (payload, handles)); + stream::adopted(connection, first).map(stream::descriptor) + } + } +} + /// A netstack that hung up mid-exchange is a netstack that is not there. /// /// **[`NetstackConn::connect`] already says so and the exchange did not, which is @@ -379,21 +330,28 @@ pub(crate) fn hangup(e: IpcError) -> NetError { } } +/// The header of netstack's answer on `connection`: a [`RespType::Result`]'s, +/// whose payload and handles are the caller's to read, or the error a +/// [`RespType::Error`] carries. +pub(crate) fn answer(connection: RawHandle) -> Result { + let header = crate::ipc::recv_header(connection).map_err(hangup)?; + if header.msg_type == RespType::Error as u32 { + let err: ErrorResponse = crate::ipc::recv_payload(connection, &header).map_err(hangup)?; + return Err(NetError::from_error_code(err.code)); + } + if header.msg_type != RespType::Result as u32 { + return Err(NetError::Protocol(header.msg_type)); + } + Ok(header) +} + pub struct PendingResponse(NetstackConn); impl PendingResponse { fn conn(&self) -> &Connection { &(self.0).0 } fn recv_checked_header(&self) -> Result { - let header = self.conn().recv_header().map_err(hangup)?; - if header.msg_type == RespType::Error as u32 { - let err: ErrorResponse = self.conn().recv_payload(&header).map_err(hangup)?; - return Err(NetError::from_error_code(err.code)); - } - if header.msg_type != RespType::Result as u32 { - return Err(NetError::Protocol(header.msg_type)); - } - Ok(header) + answer(crate::AsHandle::as_handle(self.conn())) } pub fn response(self) -> Result { @@ -425,111 +383,6 @@ impl PendingResponse { } } -/// The two pipes behind a duplex data path, split into what the caller keeps -/// and what netstack is given. -/// -/// The `to_netstack` ends are owned here only until the send; a caller that errors -/// out before then drops this and both pipes go with it. -struct DataPath { - rx: Pipe, - tx: Pipe, - to_netstack: [Pipe; DATA_HANDLES], -} - -impl DataPath { - fn create() -> Result { - let (rx, netstack_tx) = crate::pipe_pair().map_err(|_| NetError::Io)?; - let (netstack_rx, tx) = crate::pipe_pair().map_err(|_| NetError::Io)?; - Ok(Self { rx, tx, to_netstack: [netstack_tx, netstack_rx] }) - } - - fn split(self) -> (Pipe, Pipe, [OwnedHandle; DATA_HANDLES]) { - (self.rx, self.tx, self.to_netstack.map(OwnedHandle::from)) - } -} - -// TCP client functions: a listener's. A stream's are [`stream`]'s. - -/// A listener that holds no option. -pub fn tcp_bind(addr: [u8; 4], port: u16) -> Result { - tcp_bind_with(addr, port, TcpOptions::new(false)) -} - -/// A listener that holds `options` before any SYN can reach its port. -pub fn tcp_bind_with(addr: [u8; 4], port: u16, options: TcpOptions) -> Result { - let netstack = NetstackConn::connect()?; - let (notify, netstack_notify) = crate::pipe_pair().map_err(|_| NetError::Io)?; - - let resp: TcpBindResponse = netstack - .request_with_handles( - [netstack_notify.into()], - MsgType::TcpBindPiped, - &TcpBindPipedRequest { addr, port, _pad: 0, options }, - )? - .response()?; - - Ok(TcpBound { notify, socket_id: TcpSocketId(resp.socket_id), bound_port: resp.bound_port }) -} - -/// The listener's owner lets go of it. -pub fn tcp_close(socket_id: TcpSocketId) -> Result<(), NetError> { - NetstackConn::connect()? - .request(MsgType::TcpClose, &SocketCloseRequest { socket_id: socket_id.0 })? - .status() -} - -pub fn tcp_listener_set_option(socket_id: TcpSocketId, option: u32, value: u32) -> Result<(), NetError> { - NetstackConn::connect()? - .request(MsgType::TcpListenerSetOption, &SocketOptionRequest { socket_id: socket_id.0, option, value })? - .status() -} - -// UDP client functions - -pub fn udp_bind(addr: [u8; 4], port: u16) -> Result { - let netstack = NetstackConn::connect()?; - let (rx, tx, handles) = DataPath::create()?.split(); - - let resp: UdpBindResponse = netstack - .request_with_handles(handles, MsgType::UdpBind, &UdpBindRequest { addr, port, _pad: 0 })? - .response()?; - - Ok(UdpBound { socket_id: UdpSocketId(resp.socket_id), bound_port: resp.bound_port, tx, rx }) -} - -pub fn udp_send_to(socket_id: UdpSocketId, addr: [u8; 4], port: u16, len: u16) -> Result { - let resp: SentBytes = NetstackConn::connect()? - .request(MsgType::UdpSendTo, &UdpSendToRequest { - socket_id: socket_id.0, - addr, - port, - len, - })? - .response()?; - Ok(resp.value) -} - -pub fn udp_recv_from(socket_id: UdpSocketId, max_len: u32) -> Result { - NetstackConn::connect()? - .request(MsgType::UdpRecvFrom, &UdpRecvFromRequest { - socket_id: socket_id.0, - max_len, - })? - .response() -} - -pub fn udp_close(socket_id: UdpSocketId) -> Result<(), NetError> { - NetstackConn::connect()? - .request(MsgType::UdpClose, &SocketCloseRequest { socket_id: socket_id.0 })? - .status() -} - -pub fn udp_set_option(socket_id: UdpSocketId, option: u32, value: u32) -> Result<(), NetError> { - NetstackConn::connect()? - .request(MsgType::UdpSetOption, &SocketOptionRequest { socket_id: socket_id.0, option, value })? - .status() -} - pub fn dns_lookup(hostname: &str, results: &mut [[u8; 4]]) -> Result { if hostname.len() > MAX_REQUEST { return Err(NetError::InvalidInput); @@ -574,7 +427,7 @@ mod tests { use toyos_abi::syscall::SyscallError; /// `SYS_MESSAGE_SEND` into a connection whose server end has gone: the - /// first refusal any request can meet, a `tcp_bind`'s included. + /// first refusal any request can meet, a listen's included. #[test] fn a_gone_handle_transfer_is_a_netstack_that_is_not_there() { assert_eq!(hangup(IpcError::Syscall(SyscallError::Gone)), NetError::NetstackNotFound); @@ -626,41 +479,27 @@ mod tests { ); assert_eq!(NetError::from_error_code(ERR_PERMISSION_DENIED), NetError::PermissionDenied); assert_eq!(NetError::from_error_code(ERR_INVALID_INPUT), NetError::InvalidInput); + assert_eq!(NetError::from_error_code(ERR_WOULD_BLOCK), NetError::WouldBlock); assert_eq!(NetError::from_error_code(ERR_OTHER), NetError::Io); assert_eq!(NetError::from_error_code(4242), NetError::Protocol(4242)); } - /// The option request as netstack decodes it: three words in the order - /// socket, option, value, and nothing shorter. - #[test] - fn an_option_request_is_three_words_on_the_wire() { - let wire = [0x44, 0x33, 0x22, 0x11, 2, 0, 0, 0, 1, 0, 0, 0]; - let request: SocketOptionRequest = crate::ipc::decode_payload(&wire).unwrap(); - assert_eq!(request.socket_id, 0x1122_3344); - assert_eq!(request.option, OPT_BROADCAST); - assert_eq!(request.value, 1); - assert!(matches!( - crate::ipc::decode_payload::(&wire[..11]), - Err(IpcError::Malformed) - )); - } - - /// The bind's request as netstack decodes it: the options last, after + /// The listen's request as netstack decodes it: the options last, after /// the address and the port, any non-zero word an option that is on, and /// a request without them refused. #[test] - fn a_binds_request_ends_in_its_listeners_options() { + fn a_listens_request_ends_in_its_listeners_options() { let mut wire = [192, 0, 2, 7, 0x16, 0, 0, 0, 0, 0, 0, 0]; - let request: TcpBindPipedRequest = crate::ipc::decode_payload(&wire).unwrap(); + let request: TcpListenRequest = crate::ipc::decode_payload(&wire).unwrap(); assert_eq!((request.addr, request.port, request.options), ([192, 0, 2, 7], 22, TcpOptions::new(false))); wire[8] = 1; - let request: TcpBindPipedRequest = crate::ipc::decode_payload(&wire).unwrap(); + let request: TcpListenRequest = crate::ipc::decode_payload(&wire).unwrap(); assert_eq!(request.options, TcpOptions::new(true)); wire[8..].copy_from_slice(&[0, 0, 0, 0x80]); - let request: TcpBindPipedRequest = crate::ipc::decode_payload(&wire).unwrap(); + let request: TcpListenRequest = crate::ipc::decode_payload(&wire).unwrap(); assert!(request.options.nodelay()); assert!(matches!( - crate::ipc::decode_payload::(&wire[..8]), + crate::ipc::decode_payload::(&wire[..8]), Err(IpcError::Malformed) )); } diff --git a/sdk/toyos/src/net/ends.rs b/sdk/toyos/src/net/ends.rs new file mode 100644 index 00000000000..eb6a5ef5b18 --- /dev/null +++ b/sdk/toyos/src/net/ends.rs @@ -0,0 +1,173 @@ +//! A process's ends of netstack's objects, each reached by one handle: the table a stream's +//! module and a socket's keep, and the rights a holder keeps of what it holds. +//! +//! **An end goes with its handle and its last call.** A call counts itself in through the +//! handle; a handle closed under it reaches the end no more, and the end is handed back for its +//! module to let go once the call has returned. Its slot stays reserved until then, so no new end +//! takes the slot while the module clears what it keeps beside it. + +use toyos_abi::handle::Rights; +use toyos_abi::syscall; +use toyos_abi::RawHandle; + +use super::NetError; + +/// What a holder keeps of its connection: it reads, writes and waits on it, and duplicates it +/// for an end's own handle and a descriptor's. No `TRANSFER`, so no spawn or message can move +/// one. +pub(super) const HELD_CONNECTION: Rights = Rights::DUP.union(Rights::WAIT).union(Rights::READ).union(Rights::WRITE); +/// What a holder keeps of a bell netstack rings: it reads and waits on it, and duplicates it. +pub(super) const HELD_BELL: Rights = Rights::DUP.union(Rights::WAIT).union(Rights::READ); + +/// `handle` held with `rights` alone, the original closed: refused, the original is closed all +/// the same. +pub(super) fn narrowed(handle: RawHandle, rights: Rights) -> Result { + let narrowed = syscall::dup_narrowed(handle, rights); + syscall::close(handle); + narrowed.map_err(|_| NetError::Io) +} + +/// A slot of the table. `repr(u8)` puts `Free` at zero, so a table costs a program that makes no +/// end no initialised data. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub(super) enum Slot { + Free, + /// Reserved: by a call making an end that has not answered, or by an end its module is + /// letting go of. + Making, + /// An end, the handle that reaches it until that is closed, and the calls counted in. + Live { end: E, handle: Option, calls: u32 }, +} + +pub(super) struct Ends { + pub(super) slots: [Slot; N], +} + +impl Ends { + pub(super) const fn new() -> Self { + Self { slots: [const { Slot::Free }; N] } + } + + /// A free slot made [`Slot::Making`]. + pub(super) fn reserve(&mut self) -> Option { + let slot = self.slots.iter().position(|slot| matches!(slot, Slot::Free))?; + self.slots[slot] = Slot::Making; + Some(slot) + } + + pub(super) fn unreserve(&mut self, slot: usize) { + self.slots[slot] = Slot::Free; + } + + /// The made end, reached by `handle`. + pub(super) fn enter(&mut self, slot: usize, end: E, handle: RawHandle) { + self.slots[slot] = Slot::Live { end, handle: Some(handle), calls: 0 }; + } + + pub(super) fn find(&self, handle: RawHandle) -> Option { + self.slots.iter().position(|slot| matches!(slot, Slot::Live { handle: Some(held), .. } if *held == handle)) + } + + /// The end `handle` reaches. + pub(super) fn get(&self, handle: RawHandle) -> Option { + match self.slots[self.find(handle)?] { + Slot::Live { end, .. } => Some(end), + Slot::Free | Slot::Making => None, + } + } + + /// The end `handle` reaches, changed by `f`. + pub(super) fn update(&mut self, handle: RawHandle, f: impl FnOnce(&mut E)) -> Option<()> { + let slot = self.find(handle)?; + let Slot::Live { end, .. } = &mut self.slots[slot] else { return None }; + f(end); + Some(()) + } + + /// A call through `handle` begins: its end, counted. + pub(super) fn call(&mut self, handle: RawHandle) -> Option<(usize, E)> { + let slot = self.find(handle)?; + let Slot::Live { end, calls, .. } = &mut self.slots[slot] else { return None }; + *calls += 1; + Some((slot, *end)) + } + + /// A call ends: the end, where it was its last use. + pub(super) fn returned(&mut self, slot: usize) -> Option { + let Slot::Live { calls, .. } = &mut self.slots[slot] else { unreachable!("a call counted in an end that is not live") }; + *calls -= 1; + self.free_unused(slot) + } + + /// `handle` reaches its end no more: `None` for a handle that reached none, else its end's + /// slot and the end where it was its last use. + pub(super) fn forget(&mut self, handle: RawHandle) -> Option> { + let slot = self.find(handle)?; + if let Slot::Live { handle, .. } = &mut self.slots[slot] { + *handle = None; + } + Some(self.free_unused(slot).map(|end| (slot, end))) + } + + /// The end, where nothing reaches it any more: its slot [`Slot::Making`] until its module + /// has let go of what it keeps beside it. + fn free_unused(&mut self, slot: usize) -> Option { + match self.slots[slot] { + Slot::Live { end, handle: None, calls: 0 } => { + self.slots[slot] = Slot::Making; + Some(end) + } + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn h(n: u32) -> RawHandle { + RawHandle(n) + } + + /// The end goes only with its handle and its last call: a handle closed under a call lets + /// the end go only once the call returns, and its slot is not taken again before its module + /// gives it back. + #[test] + fn an_end_goes_with_its_handle_and_its_last_call() { + let mut ends: Ends = Ends::new(); + let slot = ends.reserve().unwrap(); + ends.enter(slot, 7, h(10)); + assert_eq!((ends.find(h(10)), ends.get(h(10))), (Some(slot), Some(7)), "the handle reaches the end"); + let (called, during) = ends.call(h(10)).unwrap(); + assert_eq!(during, 7); + assert_eq!(ends.forget(h(10)), Some(None), "a call is left"); + assert_eq!(ends.find(h(10)), None, "the handle reaches nothing"); + assert_eq!(ends.returned(called), Some(7), "the last call frees it"); + assert_eq!(ends.reserve(), Some(slot + 1), "a freed slot is not taken before its module gives it back"); + ends.unreserve(slot); + assert_eq!(ends.slots[slot], Slot::Free); + } + + /// A handle no end was made for reaches nothing, and forgetting it frees nothing. + #[test] + fn a_handle_no_end_was_made_for_reaches_nothing() { + let mut ends: Ends = Ends::new(); + let slot = ends.reserve().unwrap(); + ends.enter(slot, 7, h(10)); + assert_eq!((ends.find(h(12)), ends.call(h(12)), ends.forget(h(12)), ends.update(h(12), |_| {})), (None, None, None, None)); + assert_eq!(ends.update(h(10), |end| *end = 8), Some(())); + assert_eq!(ends.get(h(10)), Some(8)); + } + + /// Past the table's bound no slot is reserved, and a reservation given back is a slot again. + #[test] + fn past_the_bound_no_slot_is_reserved() { + let mut ends: Ends = Ends::new(); + let slots: Vec = (0..4).map(|_| ends.reserve().unwrap()).collect(); + assert_eq!(ends.reserve(), None); + ends.unreserve(slots[2]); + assert_eq!(ends.reserve(), Some(slots[2])); + } +} diff --git a/sdk/toyos/src/net/socket.rs b/sdk/toyos/src/net/socket.rs new file mode 100644 index 00000000000..a7559fa276c --- /dev/null +++ b/sdk/toyos/src/net/socket.rs @@ -0,0 +1,495 @@ +//! A TCP listener and a datagram socket: objects of netstack's, each named by what only its +//! holders hold. +//! +//! **A socket has holders, each a process's connection to netstack** and a bell netstack rings +//! it on. The process that listened or bound is the first; [`export`] asks netstack for another, +//! whose connection end a spawn endows and the child takes up (`super::adopt_exported`). A +//! request on the connection acts on its own socket and nothing else: no frame names a socket, +//! so a holder has nothing to name another's by. The socket ends with its last holder's hang-up. +//! +//! **A holder's connection carries frames**, each answered before the next is sent, under the +//! end's turn: [`ACCEPT`], with a stream's region, answered with the stream's connection and +//! bells ([`super::TcpAcceptResponse`]); [`SEND`], one datagram; [`RECEIVE`], answered with one +//! datagram; [`SET_OPTION`] and [`OPTION`]; and [`EXPORT`], answered [`EXPORTED`] or +//! [`REFUSED`]. An exported end's first frame is [`ADOPT`], carrying [`Adopt`] and the new +//! holder's bell. **A datagram is one frame** each way, [`Datagram`] then its bytes, so two +//! holders' datagrams never interleave. +//! +//! **The bell says "look"**: netstack rings a holder while a connection or a datagram waits and +//! the holder has not asked since it was last rung. So an accept and a receive read the bell +//! empty just before they ask, and one that finds nothing waits for the bell: what another +//! holder took first is answered [`NetError::WouldBlock`], and no ring is lost, since a ring is +//! read only by a call whose request netstack then answers. Nothing else reads the bell. +//! +//! **A handle with no end here is refused** [`NetError::NotASocket`]. A descriptor's handle is +//! a copy of its end's bell, so a poller watches it for reading; the end keeps its own +//! connection and bell, which only the end's last handle and call let go. Like a stream's, an +//! end stays in its process: its bell carries no `TRANSFER`, nor a datagram socket's +//! connection. A listener's connection keeps it, since the kernel carries an accept's region +//! only on a connection that has it; no descriptor names that handle, so nothing hands it on. + +use toyos_abi::handle::Rights; +use toyos_abi::syscall; +use toyos_abi::RawHandle; + +use crate::ipc::{self, Connection}; +use crate::sync::Lock; +use crate::OwnedHandle; + +use super::ends::{narrowed, Ends, HELD_BELL, HELD_CONNECTION}; +use super::stream::{self, Wait}; +use super::{answer, hangup, BoundResponse, MsgType, NetError, NetstackConn, TcpListenRequest, TcpOptions, UdpBindRequest}; + +pub use super::stream::{EXPORT, EXPORTED, REFUSED}; + +/// The first frame on a socket's exported end, carrying [`Adopt`] and the new +/// holder's bell: unlike a stream's (`stream::ADOPT`), so an exported end says +/// what it holds. +pub const ADOPT: u32 = 0x84; + +/// A listener holder's accept: a bare frame carrying the stream's region. +pub const ACCEPT: u32 = 3; +/// [`OptionRequest`]: [`super::OPT_NODELAY`] on a listener, [`super::OPT_BROADCAST`] on a +/// datagram socket. Answered bare. +pub const SET_OPTION: u32 = 4; +/// A `u32`, the option, answered with a `u32`, its value. +pub const OPTION: u32 = 5; +/// A datagram holder's send: [`Datagram`], its destination, then its bytes. Answered bare. +pub const SEND: u32 = 6; +/// A datagram holder's receive, bare: answered [`Datagram`], its source, then its bytes, or +/// [`super::ERR_WOULD_BLOCK`]. +pub const RECEIVE: u32 = 7; + +/// The most bytes one datagram carries: an Ethernet frame's 1500, past the IPv4 header and the +/// UDP header (RFC 894, RFC 791, RFC 768). +pub const MAX_DATAGRAM: usize = 1500 - 20 - 8; +/// The widest frame a holder sends or is answered: a datagram's. +pub const MAX_FRAME: usize = core::mem::size_of::() + MAX_DATAGRAM; + +crate::ipc_payload! { + /// A datagram's far end: where a [`SEND`] goes, and where a [`RECEIVE`]'s came from. + #[derive(Debug, PartialEq, Eq)] + pub struct Datagram { + pub addr: [u8; 4], + pub port: u16, + pub _pad: u16, + } + + pub struct OptionRequest { + pub option: u32, + pub value: u32, + } + + /// An exported end's [`ADOPT`]: what kind of socket it holds, and the + /// address and port it is at. + pub struct Adopt { + pub kind: u32, + pub addr: [u8; 4], + pub port: u16, + pub _pad: u16, + } +} + +/// `data` to `addr`:`port` as one frame's payload, in `out`: `None` for more than a datagram +/// carries. +pub fn datagram<'a>(addr: [u8; 4], port: u16, data: &[u8], out: &'a mut [u8; MAX_FRAME]) -> Option<&'a [u8]> { + if data.len() > MAX_DATAGRAM { + return None; + } + let at = core::mem::size_of::(); + out[..at].copy_from_slice(ipc::as_bytes(&Datagram { addr, port, _pad: 0 })); + out[at..at + data.len()].copy_from_slice(data); + Some(&out[..at + data.len()]) +} + +/// A frame's payload as one datagram: its far end and its bytes, or `None` for a payload +/// shorter than a [`Datagram`] or longer than [`MAX_FRAME`]. +pub fn datagram_of(payload: &[u8]) -> Option<(Datagram, &[u8])> { + let at = core::mem::size_of::(); + if payload.len() < at || payload.len() > MAX_FRAME { + return None; + } + let header = ipc::decode_payload(&payload[..at]).ok()?; + Some((header, &payload[at..])) +} + +/// What a socket is. +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Kind { + Listener = 1, + Datagram = 2, +} + +impl Kind { + pub fn from_u32(word: u32) -> Option { + match word { + 1 => Some(Self::Listener), + 2 => Some(Self::Datagram), + _ => None, + } + } +} + +// ---- the process's ends ---- + +/// The most sockets one process holds: the per-process bound libc's socket table has, as +/// [`stream::MAX_STREAMS`]. An estimate: no measurement sets it. +pub const MAX_SOCKETS: usize = 128; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct End { + kind: Kind, + /// The end's own handle of its connection, which every request is made on and only [`free`] + /// closes. + connection: RawHandle, + /// The end's own handle of its bell. + bell: RawHandle, + /// The address its listen or bind named, and the port it holds. + addr: [u8; 4], + port: u16, + /// Where a datagram socket's send goes when it names nowhere: this process's word, which + /// netstack does not hold. + peer: Option<([u8; 4], u16)>, +} + +static TABLE: Lock> = Lock::new(Ends::new()); + +/// Each end's turn on its connection: one request and its answer at a time. +static TURNS: [Lock<()>; MAX_SOCKETS] = [const { Lock::new(()) }; MAX_SOCKETS]; + +/// Lets go of an end no handle or call reaches: its connection, which is its holder's leaving, +/// and its bell. +fn free(slot: usize, end: End) { + syscall::close(end.connection); + syscall::close(end.bell); + TABLE.lock().unreserve(slot); +} + +/// Runs `f` as a call through `handle`, its end kept until it returns. +fn call(handle: RawHandle, f: impl FnOnce(usize, End) -> Result) -> Result { + let (slot, end) = TABLE.lock().call(handle).ok_or(NetError::NotASocket)?; + let answer = f(slot, end); + if let Some(end) = TABLE.lock().returned(slot) { + free(slot, end); + } + answer +} + +/// A call on a socket of `kind` only. +fn call_on(handle: RawHandle, kind: Kind, f: impl FnOnce(usize, End) -> Result) -> Result { + call(handle, |slot, end| if end.kind == kind { f(slot, end) } else { Err(NetError::InvalidInput) }) +} + +/// The rings `bell` holds, read: a look follows. +pub(super) fn drain(bell: RawHandle) { + let mut bytes = [0u8; 64]; + while let Ok(1..) = syscall::read_nonblock(bell, &mut bytes) {} +} + +/// `attempt` until it takes something, waiting for `bell` between as `wait` says. +fn waited(bell: RawHandle, wait: Wait, mut attempt: impl FnMut() -> Result) -> Result { + loop { + match attempt() { + Err(NetError::WouldBlock) => {} + answer => return answer, + } + let until = match wait { + Wait::Nonblocking => return Err(NetError::WouldBlock), + Wait::Blocking(until) => until, + }; + let left = until.map_or(u64::MAX, |until| until.saturating_sub(toyos_abi::clock::nanos_since_boot())); + if left == 0 { + return Err(NetError::TimedOut); + } + // Netstack gone hangs the bell up, which is readable, and the next attempt says so. + let poller = crate::poller::Poller::new(1); + poller.watch_raw(bell, crate::poller::READABLE, 0); + poller.wait(1, left, |_| {}); + } +} + +// ---- listen and bind ---- + +/// A socket this process made, its first holder: the descriptor's handle, and its port. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Bound { + pub handle: RawHandle, + pub port: u16, +} + +/// A listener at `addr`:`port`, port 0 asking netstack to choose one, holding `options` before +/// any SYN can reach it. +pub fn listen(addr: [u8; 4], port: u16, options: TcpOptions) -> Result { + open(Kind::Listener, addr, |netstack| { + netstack.request(MsgType::TcpListen, &TcpListenRequest { addr, port, _pad: 0, options })?.answer_with_handles::() + }) +} + +/// A datagram socket at `addr`:`port`, port 0 asking netstack to choose one. +pub fn bind(addr: [u8; 4], port: u16) -> Result { + open(Kind::Datagram, addr, |netstack| { + netstack.request(MsgType::UdpBind, &UdpBindRequest { addr, port, _pad: 0 })?.answer_with_handles::() + }) +} + +/// Makes a socket: a slot, then `ask`'s request on a new connection to netstack, whose answer +/// hands over the bell and makes the connection this process's holder. Refused +/// [`NetError::ResourceExhausted`] before anything is made where the process holds +/// [`MAX_SOCKETS`]. +fn open(kind: Kind, addr: [u8; 4], ask: impl FnOnce(NetstackConn) -> Result<(BoundResponse, [RawHandle; 1], Connection), NetError>) -> Result { + let slot = TABLE.lock().reserve().ok_or(NetError::ResourceExhausted)?; + let made = NetstackConn::connect().and_then(ask).and_then(|(bound, [bell], connection)| { + let (descriptor, connection, bell) = take_up(kind, connection.into_owned(), bell)?; + Ok((descriptor, End { kind, connection, bell, addr, port: bound.port, peer: None })) + }); + let mut table = TABLE.lock(); + match made { + Ok((handle, end)) => { + table.enter(slot, end, handle); + Ok(Bound { handle, port: end.port }) + } + Err(refused) => { + table.unreserve(slot); + Err(refused) + } + } +} + +/// A holder's connection and bell, narrowed to what a `kind` socket's holder uses, and a copy of +/// the bell for the descriptor: the descriptor's, the connection, the bell. Refused, every handle +/// is closed. +fn take_up(kind: Kind, connection: OwnedHandle, bell: RawHandle) -> Result<(RawHandle, RawHandle, RawHandle), NetError> { + let rights = match kind { + Kind::Listener => HELD_CONNECTION.union(Rights::TRANSFER), + Kind::Datagram => HELD_CONNECTION, + }; + let connection = narrowed(connection.into_raw(), rights); + let bell = narrowed(bell, HELD_BELL); + match (connection, bell) { + (Ok(connection), Ok(bell)) => match syscall::dup(bell) { + Ok(descriptor) => Ok((descriptor, connection, bell)), + Err(_) => { + [connection, bell].into_iter().for_each(syscall::close); + Err(NetError::ResourceExhausted) + } + }, + (connection, bell) => { + connection.into_iter().chain(bell).for_each(syscall::close); + Err(NetError::Io) + } + } +} + +// ---- handles ---- + +/// The kind of socket `handle` reaches, if it reaches one of this process's. A lookup, and +/// nothing is written. +pub fn kind(handle: RawHandle) -> Option { + TABLE.lock().get(handle).map(|end| end.kind) +} + +pub fn is_socket(handle: RawHandle) -> bool { + kind(handle).is_some() +} + +/// The address the socket `handle` reaches is at, as its listen or bind named +/// it, and the port it holds. +pub fn local(handle: RawHandle) -> Result<([u8; 4], u16), NetError> { + TABLE.lock().get(handle).map(|end| (end.addr, end.port)).ok_or(NetError::NotASocket) +} + +/// `handle`, which reaches one of this process's sockets, as a descriptor's object: let go by +/// this copy of the crate, which holds the end (`crate::fd::SOCKET`). +pub fn descriptor(handle: RawHandle) -> crate::fd::Object { + crate::fd::Object::socket(handle, released) +} + +extern "C" fn released(word: u64) { + if let Ok(handle) = u32::try_from(word) { + let_go(RawHandle(handle)); + } +} + +/// Closes `handle`; the end goes with its last handle and call. +pub fn let_go(handle: RawHandle) { + let forgotten = TABLE.lock().forget(handle); + syscall::close(handle); + if let Some(Some((slot, end))) = forgotten { + free(slot, end); + } +} + +// ---- a listener's ---- + +/// The oldest connection waiting at the listener `handle` reaches, as a stream: waiting for one +/// as `wait` says, [`NetError::WouldBlock`] or [`NetError::TimedOut`] where none came. +pub fn accept(handle: RawHandle, wait: Wait) -> Result { + call_on(handle, Kind::Listener, |slot, end| { + waited(end.bell, wait, || { + let _turn = TURNS[slot].lock(); + stream::accept(end.connection, end.bell) + }) + }) +} + +// ---- a datagram socket's ---- + +/// A datagram [`recv_from`] took: the bytes it wrote, and where it came from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Received { + /// The datagram's bytes, cut to the buffer: the rest is gone. + pub len: usize, + pub addr: [u8; 4], + pub port: u16, +} + +/// `data` as one datagram to `addr`:`port`: the bytes it took, all of them. +pub fn send_to(handle: RawHandle, addr: [u8; 4], port: u16, data: &[u8]) -> Result { + let mut frame = [0u8; MAX_FRAME]; + let frame = datagram(addr, port, data, &mut frame).ok_or(NetError::InvalidInput)?; + call_on(handle, Kind::Datagram, |slot, end| { + let _turn = TURNS[slot].lock(); + ipc::send_bytes(end.connection, SEND, frame).map_err(hangup)?; + bare(end.connection)?; + Ok(data.len()) + }) +} + +/// The oldest datagram waiting at the socket `handle` reaches, into `buf`: waiting for one as +/// `wait` says, [`NetError::WouldBlock`] or [`NetError::TimedOut`] where none came. +pub fn recv_from(handle: RawHandle, buf: &mut [u8], wait: Wait) -> Result { + call_on(handle, Kind::Datagram, |slot, end| { + waited(end.bell, wait, || { + let _turn = TURNS[slot].lock(); + drain(end.bell); + ipc::signal(end.connection, RECEIVE).map_err(hangup)?; + let header = answer(end.connection)?; + let mut frame = [0u8; MAX_FRAME]; + let n = ipc::recv_bytes(end.connection, &header, &mut frame).map_err(hangup)?; + let (from, data) = datagram_of(&frame[..n]).filter(|_| n == header.len() as usize).ok_or(NetError::Protocol(RECEIVE))?; + let len = data.len().min(buf.len()); + buf[..len].copy_from_slice(&data[..len]); + Ok(Received { len, addr: from.addr, port: from.port }) + }) + }) +} + +/// Where a send that names nowhere goes, from here on: this process's word for the socket. +pub fn set_peer(handle: RawHandle, peer: Option<([u8; 4], u16)>) -> Result<(), NetError> { + TABLE.lock().update(handle, |end| end.peer = peer).ok_or(NetError::NotASocket) +} + +pub fn peer(handle: RawHandle) -> Result, NetError> { + TABLE.lock().get(handle).map(|end| end.peer).ok_or(NetError::NotASocket) +} + +// ---- either's ---- + +pub fn set_option(handle: RawHandle, option: u32, value: u32) -> Result<(), NetError> { + call(handle, |slot, end| { + let _turn = TURNS[slot].lock(); + ipc::send(end.connection, SET_OPTION, &OptionRequest { option, value }).map_err(hangup)?; + bare(end.connection) + }) +} + +/// The value netstack holds for `option`, which any holder may have set. +pub fn option(handle: RawHandle, option: u32) -> Result { + call(handle, |slot, end| { + let _turn = TURNS[slot].lock(); + ipc::send(end.connection, OPTION, &option).map_err(hangup)?; + let header = answer(end.connection)?; + ipc::recv_payload(end.connection, &header).map_err(|_| NetError::Protocol(OPTION)) + }) +} + +/// An answer that carries nothing. +fn bare(connection: RawHandle) -> Result<(), NetError> { + let header = answer(connection)?; + match header.is_empty() && header.handles() == 0 { + true => Ok(()), + false => Err(NetError::Protocol(header.msg_type)), + } +} + +// ---- export and adoption ---- + +/// A new holder of the socket `handle` reaches, for another process: the end of its connection, +/// which carries `TRANSFER` so a spawn can endow it, and which `super::adopt_exported` takes up +/// there. +/// netstack out of places for it is [`NetError::ResourceExhausted`]. +pub fn export(handle: RawHandle) -> Result { + call(handle, |slot, end| { + let _turn = TURNS[slot].lock(); + stream::ask_export(end.connection) + }) +} + +/// An exported end taken up as this process's holder of its socket, `payload` and `handles` the +/// [`ADOPT`] netstack queued on it (`super::adopt_exported`): the end and its bell narrowed to +/// keep no `TRANSFER`. The descriptor's handle; `None`, with everything closed, where the frame +/// is malformed, the process holds [`MAX_SOCKETS`] or the kernel will not narrow them. +pub(super) fn adopted(connection: Connection, payload: &[u8], handles: ipc::Batch) -> Option { + let adopt: Adopt = ipc::decode_payload(payload).ok()?; + let [bell] = handles.exact::<1>()?; + let Some(kind) = Kind::from_u32(adopt.kind) else { + syscall::close(bell); + return None; + }; + let slot = TABLE.lock().reserve(); + let Some(slot) = slot else { + syscall::close(bell); + return None; + }; + let made = take_up(kind, connection.into_owned(), bell); + let mut table = TABLE.lock(); + match made { + Ok((descriptor, connection, bell)) => { + table.enter(slot, End { kind, connection, bell, addr: adopt.addr, port: adopt.port, peer: None }, descriptor); + Some(descriptor) + } + Err(_) => { + table.unreserve(slot); + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A datagram is one frame's payload: its far end, then exactly its bytes, read back as it + /// was written, whatever bytes it carries, an empty one too. + #[test] + fn a_datagram_is_one_frame_read_back_whole() { + let mut out = [0u8; MAX_FRAME]; + for data in [&b""[..], b"one", &[0xff; MAX_DATAGRAM]] { + let frame = datagram([192, 0, 2, 7], 5_353, data, &mut out).expect("a datagram that fits").to_vec(); + assert_eq!(frame.len(), core::mem::size_of::() + data.len()); + let (from, read) = datagram_of(&frame).expect("a frame of one datagram"); + assert_eq!((from, read), (Datagram { addr: [192, 0, 2, 7], port: 5_353, _pad: 0 }, data)); + } + } + + /// One byte past a datagram is refused at the sender, before any frame; a payload shorter + /// than the far end, or past the widest frame, is no datagram. + #[test] + fn what_no_datagram_carries_is_refused_both_ways() { + let mut out = [0u8; MAX_FRAME]; + assert!(datagram([0; 4], 1, &[0; MAX_DATAGRAM + 1], &mut out).is_none()); + for len in 0..core::mem::size_of::() { + assert!(datagram_of(&[0; 8][..len]).is_none(), "{len} bytes"); + } + assert!(datagram_of(&[0; MAX_FRAME + 1]).is_none()); + } + + #[test] + fn a_kind_is_its_word_and_no_other_word_is_one() { + for kind in [Kind::Listener, Kind::Datagram] { + assert_eq!(Kind::from_u32(kind as u32), Some(kind)); + } + assert_eq!((Kind::from_u32(0), Kind::from_u32(3)), (None, None)); + } +} diff --git a/sdk/toyos/src/net/stream.rs b/sdk/toyos/src/net/stream.rs index 8d6e2c2d28f..835963bd35a 100644 --- a/sdk/toyos/src/net/stream.rs +++ b/sdk/toyos/src/net/stream.rs @@ -14,7 +14,9 @@ //! neither takes the other's wake. A doorbell says "look", never "there are //! bytes" (virtio 1.2 §2.9). The process that connected or accepted is the //! first holder; [`export`] asks netstack for another, whose connection end a -//! spawn endows and the child takes up ([`adopt_exported`]). Every holder +//! spawn endows and the child takes up (`super::adopt_exported`). An accept is a +//! listener holder's request (`super::socket`), answered with the stream's +//! connection. Every holder //! shares the one region, and the session's gates make their calls take turns //! (`crate::transport::session`). //! @@ -44,7 +46,9 @@ use toyos_abi::handle::Rights; use toyos_abi::syscall::{self, SyscallError}; use toyos_abi::RawHandle; -use crate::ipc::{self, Connection, FrameRx, RxStep, TrySendError}; +use super::ends::{narrowed, Ends, HELD_BELL, HELD_CONNECTION}; + +use crate::ipc::{self, Connection, TrySendError}; use crate::shm::SharedMemory; use crate::sync::Lock; use crate::transport::session::{self, Bells, Doorbell, Holder, HungUp, Layout, Mode, Polled, Receiving, Refused as Answer, Sending, Took, Waited}; @@ -52,7 +56,7 @@ use crate::transport::stream::Place; use crate::transport::Span; use crate::OwnedHandle; -use super::{hangup, MsgType, NetError, NetstackConn, TcpAcceptRequest, TcpAcceptResponse, TcpConnectRequest, TcpConnectResponse, TcpSocketId}; +use super::{answer, hangup, MsgType, NetError, NetstackConn, TcpAcceptResponse, TcpConnectRequest, TcpConnectResponse}; pub use crate::transport::session::Way; @@ -144,29 +148,15 @@ crate::ipc_payload! { } } -/// What a holder keeps of each handle it holds: what it uses, and no -/// `TRANSFER`. It reads, writes and waits on its connection, reads and waits -/// on its bells, maps its region, and duplicates each for the end's own -/// handle and for a descriptor's. -const HELD_CONNECTION: Rights = Rights::DUP.union(Rights::WAIT).union(Rights::READ).union(Rights::WRITE); -const HELD_BELL: Rights = Rights::DUP.union(Rights::WAIT).union(Rights::READ); +/// What a holder keeps of its region besides its connection and bells +/// (`super::ends`): it maps it, and duplicates it. const HELD_REGION: Rights = Rights::DUP.union(Rights::MAP); -/// `handle` held with `rights` alone, the original closed: refused, the -/// original is closed all the same. -fn narrowed(handle: RawHandle, rights: Rights) -> Result { - let narrowed = syscall::dup_narrowed(handle, rights); - syscall::close(handle); - narrowed.map_err(|_| NetError::Io) -} - // ---- the process's ends ---- /// The most streams one process holds: the per-process bound libc's socket /// table has. An estimate: no measurement sets it. pub const MAX_STREAMS: usize = 128; -/// The most handles of streams' connections one process holds. -const MAX_HANDLES: usize = 2 * MAX_STREAMS; type Rx = Receiving; type Tx = Sending; @@ -179,9 +169,9 @@ struct Halves { control: Lock<()>, } -/// A half, or none. `repr(u8)` puts `Empty` at zero, as it does -/// [`Slot::Free`], so the tables cost a program that makes no stream no -/// initialised data. +/// A half, or none. `repr(u8)` puts `Empty` at zero, as the table's own +/// slots have their free one, so the tables cost a program that makes no +/// stream no initialised data. #[repr(u8)] enum Kept { Empty, @@ -191,7 +181,7 @@ enum Kept { static HALVES: [Halves; MAX_STREAMS] = [const { Halves { rx: Lock::new(Kept::Empty), tx: Lock::new(Kept::Empty), control: Lock::new(()) } }; MAX_STREAMS]; -static TABLE: Lock = Lock::new(Table::new()); +static TABLE: Lock> = Lock::new(Ends::new()); /// What an end holds besides its halves. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -207,95 +197,6 @@ struct End { send_bell: RawHandle, peer: ([u8; 4], u16), local_port: u16, - handles: u32, - calls: u32, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[repr(u8)] -enum Slot { - Free, - /// Reserved: by a connect or an accept that has not answered, or by an end - /// [`free`] is letting go of. - Making, - Live(End), -} - -/// The process's ends, and the handles each is reached by. -struct Table { - slots: [Slot; MAX_STREAMS], - handles: [Option<(RawHandle, usize)>; MAX_HANDLES], -} - -impl Table { - const fn new() -> Self { - Self { slots: [Slot::Free; MAX_STREAMS], handles: [None; MAX_HANDLES] } - } - - /// A free slot made [`Slot::Making`], where a handle has room beside it. - fn reserve(&mut self) -> Option { - self.handles.iter().position(Option::is_none)?; - let slot = self.slots.iter().position(|slot| *slot == Slot::Free)?; - self.slots[slot] = Slot::Making; - Some(slot) - } - - fn unreserve(&mut self, slot: usize) { - self.slots[slot] = Slot::Free; - } - - /// The made end, reached by `handle`. - fn enter(&mut self, slot: usize, end: End, handle: RawHandle) { - let room = self.handles.iter().position(Option::is_none).expect("a reservation keeps a handle's room"); - self.handles[room] = Some((handle, slot)); - self.slots[slot] = Slot::Live(End { handles: 1, calls: 0, ..end }); - } - - fn find(&self, handle: RawHandle) -> Option { - self.handles.iter().flatten().find(|(held, _)| *held == handle).map(|&(_, slot)| slot) - } - - fn end(&mut self, slot: usize) -> &mut End { - match &mut self.slots[slot] { - Slot::Live(end) => end, - Slot::Free | Slot::Making => unreachable!("a handle names an end that is not live"), - } - } - - /// A call through `handle` begins: its end, counted. - fn call(&mut self, handle: RawHandle) -> Option<(usize, End)> { - let slot = self.find(handle)?; - let end = self.end(slot); - end.calls += 1; - Some((slot, *end)) - } - - /// A call ends: the end, where it was its last use. - fn returned(&mut self, slot: usize) -> Option { - let end = self.end(slot); - end.calls -= 1; - self.free_unused(slot) - } - - /// The end, where nothing reaches it any more. Its slot stays reserved - /// until [`free`] has cleared its halves, so no new stream takes the slot - /// and has its halves cleared under it. - fn free_unused(&mut self, slot: usize) -> Option { - let end = *self.end(slot); - (end.handles == 0 && end.calls == 0).then(|| { - self.slots[slot] = Slot::Making; - end - }) - } - - /// `handle` reaches its end no more: `None` for a handle that reached - /// none, else its end's slot and the end where it was its last use. - fn forget(&mut self, handle: RawHandle) -> Option> { - let at = self.handles.iter().position(|entry| entry.is_some_and(|(held, _)| held == handle))?; - let (_, slot) = self.handles[at].take().expect("found"); - self.end(slot).handles -= 1; - Some(self.free_unused(slot).map(|end| (slot, end))) - } } /// Lets go of an end no handle or call reaches: its halves, its connection, @@ -665,9 +566,7 @@ pub fn failure(handle: RawHandle) -> Result, Refused> { /// The peer's address and port, and this end's port. pub fn endpoints(handle: RawHandle) -> Result<(([u8; 4], u16), u16), Refused> { - let mut table = TABLE.lock(); - let slot = table.find(handle).ok_or(Refused::NotThisProcess(handle))?; - let end = table.end(slot); + let end = TABLE.lock().get(handle).ok_or(Refused::NotThisProcess(handle))?; Ok((end.peer, end.local_port)) } @@ -826,8 +725,8 @@ pub struct Connected { /// before anything is made where the process holds [`MAX_STREAMS`]. pub fn connect(addr: [u8; 4], port: u16, timeout_ms: u32, nodelay: bool) -> Result { let request = TcpConnectRequest { addr, port, _pad: 0, timeout_ms }; - let opened = open(nodelay, (addr, port), |netstack, region| { - netstack.request_with_handles([region], MsgType::TcpConnect, &request)?.answer_with_handles::() + let opened = open(nodelay, (addr, port), |region| { + NetstackConn::connect()?.request_with_handles([region], MsgType::TcpConnect, &request)?.answer_with_handles::() })?; Ok(Connected { handle: opened.handle, local_port: opened.answer.local_port }) } @@ -843,12 +742,26 @@ pub struct Accepted { pub nodelay: bool, } -/// The oldest connection waiting at `listener` as a stream. The caller took -/// one of the listener's wakes first. Refused as [`connect`] is. -pub fn accept(listener: TcpSocketId) -> Result { - let request = TcpAcceptRequest { listener: listener.0 }; - let opened = open(false, ([0; 4], 0), |netstack, region| { - netstack.request_with_handles([region], MsgType::TcpAccept, &request)?.answer_with_handles::() +/// The oldest connection waiting at the listener whose holder's connection is +/// `listener`, as a stream: its region lent with a `super::socket::ACCEPT` and +/// its connection and bells in the answer. The holder's `bell` is read empty +/// just before the request goes, and not where nothing reached netstack. +/// Refused as [`connect`] is, and [`NetError::WouldBlock`] where none waits. +pub(super) fn accept(listener: RawHandle, bell: RawHandle) -> Result { + let opened = open(false, ([0; 4], 0), |region| { + super::socket::drain(bell); + ipc::signal_with(listener, super::socket::ACCEPT, [region]).map_err(hangup)?; + let header = answer(listener)?; + let accepted: TcpAcceptResponse = ipc::recv_payload(listener, &header).map_err(hangup)?; + let mut handles = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES]; + match ipc::recv_handles(listener, &header, &mut handles) { + Ok(3) => Ok((accepted, [handles[1], handles[2]], Connection::received(handles[0]))), + Ok(n) => { + handles[..n].iter().for_each(|&h| syscall::close(h)); + Err(NetError::Protocol(header.msg_type)) + } + Err(e) => Err(hangup(e)), + } })?; let answer = opened.answer; Ok(Accepted { @@ -891,13 +804,13 @@ impl Answered for TcpAcceptResponse { } } -/// Makes a stream: a slot, a region, then `ask`'s request on a new connection -/// to netstack carrying it, whose answer hands over the bells. The process is -/// the stream's first holder. Nothing made outlives a refusal. +/// Makes a stream: a slot, a region, then `ask`'s request carrying it, whose +/// answer hands over the bells and the stream's connection. The process is the +/// stream's first holder. Nothing made outlives a refusal. fn open( nodelay: bool, peer: ([u8; 4], u16), - ask: impl FnOnce(NetstackConn, OwnedHandle) -> Result<(A, [RawHandle; BELLS], Connection), NetError>, + ask: impl FnOnce(OwnedHandle) -> Result<(A, [RawHandle; BELLS], Connection), NetError>, ) -> Result, NetError> { let slot = TABLE.lock().reserve().ok_or(NetError::ResourceExhausted)?; let made = (|| { @@ -911,7 +824,7 @@ fn open( words[NODELAY].store(u32::from(nodelay), Ordering::Relaxed); let (rx, tx) = session::client(words, LAYOUT); let lent = region.share().map_err(|_| NetError::Io)?; - let (answer, bells, connection) = ask(NetstackConn::connect()?, lent)?; + let (answer, bells, connection) = ask(lent)?; let (region, base) = region.into_raw(); let held = take_up(connection.into_owned(), bells, region, base)?; *HALVES[slot].rx.lock() = Kept::Held(rx); @@ -946,13 +859,13 @@ impl Held { /// The end, its region mapped at `base`. fn end(&self, base: usize, peer: ([u8; 4], u16), local_port: u16) -> End { let (connection, receive_bell, send_bell) = (self.connection, self.receive_bell, self.send_bell); - End { region: Some((self.region, base)), connection, receive_bell, send_bell, peer, local_port, handles: 0, calls: 0 } + End { region: Some((self.region, base)), connection, receive_bell, send_bell, peer, local_port } } } /// The descriptor's handle and the end's own: `connection` narrowed, then /// duplicated. Refused, it is closed. -fn hold_connection(connection: OwnedHandle) -> Result<(RawHandle, RawHandle), NetError> { +pub(super) fn hold_connection(connection: OwnedHandle) -> Result<(RawHandle, RawHandle), NetError> { let descriptor = narrowed(connection.into_raw(), HELD_CONNECTION)?; syscall::dup(descriptor).map(|own| (descriptor, own)).map_err(|_| { syscall::close(descriptor); @@ -985,7 +898,7 @@ fn take_up(connection: OwnedHandle, bells: [RawHandle; BELLS], region: RawHandle /// A new holder of the stream `handle` reaches, for another process: the end /// of its connection, which carries `TRANSFER` so a spawn can endow it, and -/// which [`adopt_exported`] takes up there. netstack gone is +/// which `super::adopt_exported` takes up there. netstack gone is /// [`NetError::NetstackNotFound`]; netstack out of places for it is /// [`NetError::ResourceExhausted`]. pub fn export(handle: RawHandle) -> Result { @@ -1000,8 +913,9 @@ pub fn export(handle: RawHandle) -> Result { } } -/// [`EXPORT`] on a holder's connection, and netstack's answer to it. -fn ask_export(connection: RawHandle) -> Result { +/// [`EXPORT`] on a holder's connection, a stream's or a socket's, and +/// netstack's answer to it. +pub(super) fn ask_export(connection: RawHandle) -> Result { ipc::signal(connection, EXPORT).map_err(hangup)?; let header = ipc::recv_header(connection).map_err(hangup)?; match header.msg_type { @@ -1024,17 +938,16 @@ fn ask_export(connection: RawHandle) -> Result { } } -/// An exported end taken up as this process's holder of its stream: the -/// [`ADOPT`] frame netstack queued on it read, its region judged and mapped, -/// and the end, the region and the bells narrowed to keep no `TRANSFER`. The -/// descriptor's handle. Where the frame is missing or malformed, or the region -/// refused, the end is a stream still, every call on which answers `Aborted`. -/// `None`, the end closed, where the process holds [`MAX_STREAMS`] or the -/// kernel will not narrow it. -pub fn adopt_exported(end: OwnedHandle) -> Option { +/// An exported end taken up as this process's holder of its stream, `first` +/// the [`ADOPT`] netstack queued on it (`super::adopt_exported`): its region +/// judged and mapped, and the end, the region and the bells narrowed to keep +/// no `TRANSFER`. The descriptor's handle. Where the frame is missing or +/// malformed, or the region refused, the end is a stream still, every call on +/// which answers `Aborted`. `None`, the end closed, where the process holds +/// [`MAX_STREAMS`] or the kernel will not narrow it. +pub(super) fn adopted(connection: Connection, first: Option<(&[u8], ipc::Batch)>) -> Option { let slot = TABLE.lock().reserve()?; - let connection = Connection::received(end.into_raw()); - let taken = adoption(&connection).and_then(|(holder, adopt, [region, receive, send])| { + let taken = first.and_then(adoption).and_then(|(holder, adopt, [region, receive, send])| { let Ok(region) = SharedMemory::lent(region, REGION_BYTES) else { [receive, send].into_iter().for_each(syscall::close); return None; @@ -1051,7 +964,7 @@ pub fn adopt_exported(end: OwnedHandle) -> Option { }), None => hold_connection(connection.into_owned()).map(|(descriptor, connection)| { let invalid = toyos_abi::HANDLE_INVALID; - let end = End { region: None, connection, receive_bell: invalid, send_bell: invalid, peer: ([0; 4], 0), local_port: 0, handles: 0, calls: 0 }; + let end = End { region: None, connection, receive_bell: invalid, send_bell: invalid, peer: ([0; 4], 0), local_port: 0 }; (descriptor, end) }), }; @@ -1068,13 +981,10 @@ pub fn adopt_exported(end: OwnedHandle) -> Option { } } -/// The [`ADOPT`] frame on an exported end, read without waiting: netstack -/// queued it before it answered the export, so it is there or never sent. -fn adoption(connection: &Connection) -> Option<(Holder, Adopt, [RawHandle; BELLS + 1])> { - let mut frames: FrameRx<{ core::mem::size_of::() }> = FrameRx::new(); - let RxStep::Frame { msg_type: ADOPT, payload_len, .. } = frames.pump(connection) else { return None }; - let adopt: Adopt = ipc::decode_payload(frames.payload(payload_len)).ok()?; - let handles = frames.take_handles::<{ BELLS + 1 }>()?; +/// An [`ADOPT`] frame's holder, payload and handles. +fn adoption((payload, handles): (&[u8], ipc::Batch)) -> Option<(Holder, Adopt, [RawHandle; BELLS + 1])> { + let adopt: Adopt = ipc::decode_payload(payload).ok()?; + let handles = handles.exact::<{ BELLS + 1 }>()?; match Holder::new(adopt.holder) { Some(holder) => Some((holder, adopt, handles)), None => { @@ -1088,43 +998,6 @@ fn adoption(connection: &Connection) -> Option<(Holder, Adopt, [RawHandle; BELLS mod tests { use super::*; - fn h(n: u32) -> RawHandle { - RawHandle(n) - } - - fn end() -> End { - End { - region: Some((h(1), 0x20_0000)), - connection: h(3), - receive_bell: h(2), - send_bell: h(4), - peer: ([192, 0, 2, 2], 80), - local_port: 49_152, - handles: 0, - calls: 0, - } - } - - /// The end goes only with its handle and its last call, and so does its - /// own handle of the connection, which [`free`] closes: the last - /// descriptor closed under a call hangs the session up only once the call - /// returns. - #[test] - fn an_end_goes_with_its_handle_and_its_last_call() { - let mut table = Table::new(); - let slot = table.reserve().unwrap(); - table.enter(slot, end(), h(10)); - assert_eq!(table.find(h(10)), Some(slot), "the handle reaches the end"); - let (called, during) = table.call(h(10)).unwrap(); - assert_eq!(during.connection, h(3), "the call rings and waits through the end's own handle"); - assert_eq!(table.forget(h(10)), Some(None), "a call is left"); - assert_eq!(table.find(h(10)), None, "the handle reaches nothing"); - assert_eq!(table.returned(called), Some(End { handles: 0, calls: 0, ..end() }), "the last call frees it"); - assert_eq!(table.reserve(), Some(slot + 1), "a freed slot is not taken before its halves are cleared"); - table.unreserve(slot); - assert_eq!(table.slots[slot], Slot::Free); - } - /// A wait for a gate that lasts is reported each stall interval and goes /// on until rung; one with a deadline ends at the deadline, reported only /// where it reached a stall interval first. @@ -1158,26 +1031,4 @@ mod tests { }, |_| panic!("a wait shorter than a stall interval was reported")); assert_eq!(early, Waited::TimedOut); } - - /// A handle no end was made for is no stream, and forgetting it frees - /// nothing. - #[test] - fn a_handle_no_end_was_made_for_is_no_stream() { - let mut table = Table::new(); - let slot = table.reserve().unwrap(); - table.enter(slot, end(), h(10)); - assert_eq!((table.find(h(12)), table.call(h(12)), table.forget(h(12))), (None, None, None)); - assert_eq!(table.find(h(10)), Some(slot)); - } - - /// Past [`MAX_STREAMS`] a stream is refused before anything is made, and - /// a reservation given back is a slot again. - #[test] - fn past_the_bound_no_slot_is_reserved() { - let mut table = Table::new(); - let slots: Vec = (0..MAX_STREAMS).map(|_| table.reserve().unwrap()).collect(); - assert_eq!(table.reserve(), None); - table.unreserve(slots[7]); - assert_eq!(table.reserve(), Some(slots[7])); - } } diff --git a/system/netstack/node/src/datagram.rs b/system/netstack/node/src/datagram.rs index 09ae8438977..5326f46134b 100644 --- a/system/netstack/node/src/datagram.rs +++ b/system/netstack/node/src/datagram.rs @@ -7,9 +7,14 @@ //! nothing. A destination, a port or a length is the client's number and is refused, never //! trusted; a datagram off the wire reaches a socket only through [udp]'s demultiplexer. //! -//! **A socket holds a place** (`places`) from its bind to its close: what it makes the node hold -//! is its two queues, `limits::RX_DATAGRAMS` received and `limits::TX_DATAGRAMS` accepted. A bind -//! with no place left is answered [`Refused::ResourceExhausted`] with nothing made. +//! **A socket has holders** (`holders`), each told on its own bell that a datagram waits, all +//! receiving from the one queue: a datagram goes to the one holder whose receive takes it. The +//! socket lives until its last holder leaves ([`Node::datagram_departed`]). +//! +//! **A socket holds a place** (`places`) from its bind until then, and each holder past its +//! first one more: what it makes the node hold is its two queues, `limits::RX_DATAGRAMS` +//! received and `limits::TX_DATAGRAMS` accepted. A bind with no place left is answered +//! [`Refused::ResourceExhausted`] with nothing made. //! //! **A send to a broadcast address needs the socket's permission** ([`Node::udp_set_broadcast`], //! POSIX's `SO_BROADCAST`), which a socket is bound without: one sent without it is answered @@ -30,9 +35,13 @@ use core::net::Ipv4Addr; +use alloc::boxed::Box; + use toyos_net_udp::{Counter, Error, SocketId}; use toyos_net_wire::{Instant, Port}; +use crate::holders::{Bell, HolderId, Holders}; +use crate::streams::ExportRefused; use crate::Node; /// One client socket. An id outlives its socket only as a refusal: [udp] never hands a closed @@ -45,7 +54,8 @@ pub struct DatagramId(SocketId); pub enum Refused { /// The port is another socket's, or no ephemeral port is free. AddrInUse, - /// The id names no socket, or the machine has no address or route to send from. + /// The id names no socket, or the holder none of its, or the machine has no address or route + /// to send from. NotConnected, /// The call names what no datagram of this socket may carry: an address that is not this /// machine's to bind, a destination nothing is sent to, port 0, more than one frame holds. @@ -119,22 +129,37 @@ fn refused(error: Error) -> Refused { impl Node { /// Binds a socket to `addr`, 0.0.0.0 meaning every address the machine holds, and to `port` /// 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, draw: impl FnOnce() -> u32) -> Result<(DatagramId, Port), Refused> { + /// the port it holds; `bell` is its first holder's, [`HolderId::FIRST`]. + pub fn udp_bind(&mut self, addr: Ipv4Addr, port: Option, bell: Box, 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); + self.datagrams.insert(DatagramId(id), Holders::new(bell)); Ok((DatagramId(id), port)) } + /// A holder more of socket `id`, told on `bell`, which takes a place: the socket's port. + pub fn export_datagram(&mut self, id: DatagramId, bell: Box) -> Result<(HolderId, Port), ExportRefused> { + if self.room() == 0 { + return Err(ExportRefused::Full); + } + let holders = self.datagrams.get_mut(&id).ok_or(ExportRefused::Gone)?; + let Ok(port) = self.stack.port(id.0) else { unreachable!("[udp] lost a socket the node holds") }; + Ok((holders.add(bell), port)) + } + /// Whether the socket's datagrams may go to a broadcast address, the limited one or the /// directed one of a prefix the machine holds, from here on. pub fn udp_set_broadcast(&mut self, id: DatagramId, permitted: bool) -> Result<(), Refused> { self.stack.set_broadcast(id.0, permitted).map_err(refused) } + /// Whether [`Self::udp_set_broadcast`] last permitted it. + pub fn udp_broadcast(&mut self, id: DatagramId) -> Result { + self.stack.broadcast(id.0).map_err(refused) + } + /// Queues `payload` for `destination:port`; it leaves in a later [`Node::transmit`]. pub fn udp_send_to(&mut self, now: Instant, id: DatagramId, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), Refused> { let sent = self.stack.send_to(now, id.0, destination, port, payload).map_err(refused); @@ -142,19 +167,49 @@ impl Node { sent } - /// The oldest datagram that reached the socket, in `out`, or `None` when none waits. One - /// longer than `out` is cut to it and the rest is gone, as [udp] counts. - pub fn udp_recv_from(&mut self, id: DatagramId, out: &mut [u8]) -> Result, Refused> { + /// `holder`'s receive: the oldest datagram that reached the socket, in `out`, or `None` when + /// none waits. One longer than `out` is cut to it and the rest is gone, as [udp] counts. + pub fn udp_recv_from(&mut self, id: DatagramId, holder: HolderId, out: &mut [u8]) -> Result, Refused> { + if !self.datagrams.get_mut(&id).is_some_and(|holders| holders.asked(holder)) { + return Err(Refused::NotConnected); + } let received = self.stack.recv_from(id.0, out).map_err(refused)?; + self.tell_datagram_holders(); Ok(received.map(|received| Datagram { len: received.len, source: received.source, source_port: received.source_port })) } - /// Closes the socket: its port is free at once, what it received is gone, and what it had - /// 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(()) + /// `holder` of socket `id` left. The last holder's leaving closes the socket: its port is + /// free at once, what it received is gone, and what it had accepted still leaves, to [udp]'s + /// bound. Either gives a place back. + pub fn datagram_departed(&mut self, now: Instant, id: DatagramId, holder: HolderId) { + let Some(holders) = self.datagrams.get_mut(&id) else { return }; + if !holders.remove(holder) { + return; + } + if holders.is_empty() { + self.datagrams.remove(&id); + if let Err(error) = self.stack.close(now, id.0) { + unreachable!("[udp] refused the close of a socket the node holds: {error:?}"); + } + } + self.tell_holders(); + } + + /// The clients' datagram sockets the node holds. + pub fn datagrams(&self) -> usize { + self.datagrams.len() + } + + /// The clients' datagram sockets the node holds, and their holders past each first. + pub(crate) fn datagram_places(&self) -> usize { + self.datagrams.values().map(Holders::more).fold(self.datagrams.len(), usize::saturating_add) + } + + /// Tells each holder of a socket a datagram waits for. + pub(crate) fn tell_datagram_holders(&mut self) { + for (id, holders) in &mut self.datagrams { + let Ok(waiting) = self.stack.waiting(id.0) else { unreachable!("[udp] lost a socket the node holds") }; + holders.tell(waiting > 0); + } } } diff --git a/system/netstack/node/src/holders.rs b/system/netstack/node/src/holders.rs new file mode 100644 index 00000000000..4447c232d1a --- /dev/null +++ b/system/netstack/node/src/holders.rs @@ -0,0 +1,127 @@ +//! The holders of a listener or a datagram socket: each one process's connection to netstack, +//! told on a bell of its own that the socket has something to take, an accept or a datagram. +//! +//! **A holder is told once a look.** The bell is rung while the socket has something to take and +//! the holder has not been told since it last asked for it, so a holder that asks again after its +//! ring is rung again while something is left, and one that does not ask is rung no more. A +//! holder reads its bell, then asks; so no thing to take waits on a holder that is not looking, +//! and no bell takes more than one ring a request. + +use alloc::boxed::Box; +use alloc::collections::BTreeMap; + +/// A holder's bell. It is netstack's own end of a pipe whose other end only that holder holds. +pub trait Bell { + /// Rings, never waiting: a bell already rung and not yet read is rung still. + fn ring(&mut self); +} + +/// One holder of one socket, as the node numbers them: the socket's first is +/// [`HolderId::FIRST`], and no number is used twice for one socket. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HolderId(u32); + +impl HolderId { + /// The holder whose listen or bind made the socket. + pub const FIRST: Self = Self(0); +} + +struct Holding { + bell: Box, + /// Rung since its last ask. + told: bool, +} + +pub(crate) struct Holders { + by_id: BTreeMap, + next: u32, +} + +impl Holders { + /// A socket's holders: `first` alone. + pub(crate) fn new(first: Box) -> Self { + Self { by_id: BTreeMap::from([(HolderId::FIRST, Holding { bell: first, told: false })]), next: 1 } + } + + pub(crate) fn add(&mut self, bell: Box) -> HolderId { + let id = HolderId(self.next); + self.next = self.next.saturating_add(1); + self.by_id.insert(id, Holding { bell, told: false }); + id + } + + /// Whether `holder` was one. + pub(crate) fn remove(&mut self, holder: HolderId) -> bool { + self.by_id.remove(&holder).is_some() + } + + pub(crate) fn is_empty(&self) -> bool { + self.by_id.is_empty() + } + + /// The holders past the first, each of which takes a place. + pub(crate) fn more(&self) -> usize { + self.by_id.len().saturating_sub(1) + } + + pub(crate) fn holds(&self, holder: HolderId) -> bool { + self.by_id.contains_key(&holder) + } + + /// `holder` asks: from here it is owed a ring for what it finds left. `false` is no holder. + pub(crate) fn asked(&mut self, holder: HolderId) -> bool { + self.by_id.get_mut(&holder).map(|holding| holding.told = false).is_some() + } + + /// Rings every holder not told since its last ask, where the socket has something to take. + pub(crate) fn tell(&mut self, ready: bool) { + if !ready { + return; + } + for holding in self.by_id.values_mut().filter(|holding| !holding.told) { + holding.bell.ring(); + holding.told = true; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::rc::Rc; + use core::cell::Cell; + + struct Counted(Rc>); + + impl Bell for Counted { + fn ring(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + fn bell() -> (Box, Rc>) { + let rings = Rc::new(Cell::new(0)); + (Box::new(Counted(rings.clone())), rings) + } + + /// Every holder is told when there is something, once until it asks again, and one that + /// asks while something is left is told again. + #[test] + fn a_holder_is_told_once_a_look() { + let (first, a) = bell(); + let mut holders = Holders::new(first); + let (second, b) = bell(); + let other = holders.add(second); + holders.tell(false); + assert_eq!((a.get(), b.get()), (0, 0), "nothing to take rings nobody"); + holders.tell(true); + holders.tell(true); + assert_eq!((a.get(), b.get()), (1, 1), "each told once"); + assert!(holders.asked(other)); + holders.tell(true); + assert_eq!((a.get(), b.get()), (1, 2), "the one that asked is told what is left"); + assert!(holders.remove(other)); + assert!(!holders.asked(other), "a holder that left asks nothing"); + assert_eq!(holders.more(), 0); + } +} diff --git a/system/netstack/node/src/lease.rs b/system/netstack/node/src/lease.rs index 130160f564f..08b6b2b3fc3 100644 --- a/system/netstack/node/src/lease.rs +++ b/system/netstack/node/src/lease.rs @@ -209,6 +209,19 @@ impl Stack { self.shard.recv_from(id, out) } + /// The port a socket holds. + pub(crate) fn port(&mut self, id: SocketId) -> Result { + self.shard.udp_port(id) + } + + pub(crate) fn waiting(&mut self, id: SocketId) -> Result { + self.shard.udp_waiting(id) + } + + pub(crate) fn broadcast(&mut self, id: SocketId) -> Result { + self.shard.udp_broadcast(id) + } + pub(crate) fn close(&mut self, now: Instant, id: SocketId) -> Result<(), toyos_net_udp::Error> { self.shard.udp_close(now, id) } diff --git a/system/netstack/node/src/lib.rs b/system/netstack/node/src/lib.rs index bf7e5be8042..417d1e39e0b 100644 --- a/system/netstack/node/src/lib.rs +++ b/system/netstack/node/src/lib.rs @@ -39,6 +39,7 @@ extern crate alloc; mod datagram; +mod holders; mod lease; mod listeners; mod name; @@ -46,7 +47,8 @@ mod places; mod resolve; mod streams; -pub use listeners::{AcceptRefused, Accepted, ListenRefused, ListenerId, Wake, WriteRefusal}; +pub use holders::{Bell, HolderId}; +pub use listeners::{AcceptRefused, Accepted, ListenRefused, ListenerId}; pub use streams::{ConnectRefused, ExportRefused, Session, StreamEvent, StreamId}; use alloc::vec; @@ -97,8 +99,8 @@ pub struct Node { datagram: Vec, streams: streams::Streams, listeners: listeners::Listeners, - /// The clients' datagram sockets bound and not closed. - sockets: usize, + /// The clients' datagram sockets bound and not closed, and their holders. + datagrams: alloc::collections::BTreeMap, /// How many streams, listeners, datagram sockets and connections [tcp] finishes alone the /// node holds at most. places: usize, @@ -124,7 +126,7 @@ impl Node { datagram: vec![0; usize::from(toyos_dhcp::limits::MAX_MESSAGE)], streams: streams::Streams::default(), listeners: listeners::Listeners::default(), - sockets: 0, + datagrams: alloc::collections::BTreeMap::new(), places: 0, }) } diff --git a/system/netstack/node/src/listeners.rs b/system/netstack/node/src/listeners.rs index 0a4039093a5..71d542bcdc4 100644 --- a/system/netstack/node/src/listeners.rs +++ b/system/netstack/node/src/listeners.rs @@ -1,28 +1,26 @@ -//! Listeners: a passive open of [tcp]'s and the pipe its owner reads its wakes from. The queues -//! are [tcp]'s: handshakes in progress, and connections that finished theirs and wait, oldest -//! first. A connection that waits is nobody's stream yet, its text held in [tcp]'s own storage; -//! an accept makes the oldest one a stream on the region the accept lends, and what it held moves -//! there at its offsets, text past a hole included, the FIN staying [tcp]'s state. +//! Listeners: a passive open of [tcp]'s and its holders (`holders`), each told on its own bell +//! that a connection waits. The queues are [tcp]'s: handshakes in progress, and connections that +//! finished theirs and wait, oldest first. A connection that waits is nobody's stream yet, its +//! text held in [tcp]'s own storage; an accept makes the oldest one a stream on the region the +//! accept lends, and what it held moves there at its offsets, text past a hole included, the FIN +//! staying [tcp]'s state. //! -//! **The owner holds one unspent wake for each connection that waits and that there is a place -//! for, and an accept spends one whatever it answers.** The owner reads a wake and then accepts, -//! once; the node writes what is missing whenever a pass ends ([`Node::bridge`]), from what -//! [tcp] holds and what `places` has left at that moment, so no wake depends on the pass that -//! saw a connection arrive. A wake left over by a connection its peer reset before the accept -//! stands for the next one, and until then its accept is answered [`AcceptRefused::Nothing`]. -//! The node cannot tell an accept that is on its way from one that will never be sent: an owner -//! that reads a wake and sends no accept is owed one wake fewer from then on. +//! **Every holder takes from the one queue.** A holder is told while a connection waits that +//! there is a place for, once until it asks again ([`Node::accept`]); the node tells whenever a +//! pass ends ([`Node::bridge`]), from what [tcp] holds and what `places` has left at that moment, +//! so no ring depends on the pass that saw a connection arrive. Two holders told of one +//! connection both ask; one takes it and the other is answered [`AcceptRefused::Nothing`]. //! //! **A port is one listener's**, whatever address either listen named: [tcp] hands a SYN to //! the listener that named its address before one at every address, so a second listen on a //! held port would take the first's connections, and a listen carries no word for its program -//! by which the node could allow that to the first's owner alone. +//! by which the node could allow that to the first's holders alone. //! -//! **A listener lives until its owner lets go**: [`Node::close_listener`], or a wake its pipe -//! refuses, for whatever reason. [tcp] then resets every connection that still waits, so each -//! peer learns at once, and the port is free. Nothing the wire does ends one: a listener -//! whose address the machine loses stands, with its port and its place, and is handed the -//! next SYN once the address is the machine's again. +//! **A listener lives until its last holder leaves** ([`Node::listener_departed`]); each holder +//! past the first takes a place ([`Node::export_listener`]). [tcp] then resets every connection +//! that still waits, so each peer learns at once, and the port is free. Nothing the wire does +//! ends one: a listener whose address the machine loses stands, with its port and its place, and +//! is handed the next SYN once the address is the machine's again. //! //! **A stream starts with the options its connection has**, and those are its listener's as //! they were when its SYN arrived: [tcp] hands them over then (LS-10), the node reads them @@ -33,33 +31,17 @@ //! (`tests/host.rs` asks the host the tests run on). use alloc::boxed::Box; -use alloc::collections::{BTreeMap, VecDeque}; +use alloc::collections::BTreeMap; use core::net::Ipv4Addr; use toyos_net_shard::ListenError; use toyos_net_tcp::{Endpoint, Error, Options}; use toyos_net_wire::{Instant, Port}; -use crate::streams::{Session, StreamId}; +use crate::holders::{Bell, HolderId, Holders}; +use crate::streams::{ExportRefused, Session, StreamId}; use crate::Node; -/// Why the write end of a listener's wake pipe took nothing. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum WriteRefusal { - Full, - /// Nobody holds the read end. - Gone, - /// Anything else: the handle is not the pipe end its owner said it was. - Broken, -} - -/// The write end of the pipe a listener's owner reads its wakes from. Dropped, the owner reads -/// the end. -pub trait Wake { - /// Writes one wake. - fn wake(&mut self) -> Result<(), WriteRefusal>; -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ListenerId(u64); @@ -87,12 +69,12 @@ pub struct Accepted { pub nodelay: bool, } -/// Why an accept took no connection. Whatever waits still waits, and the session is dropped. +/// Why an accept took no connection. Whatever waits still waits, and no session was made. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AcceptRefused { - /// The id names no listener. + /// The id names no listener, or the holder none of its. NoListener, - /// The request carried no region. + /// The connection that waits found no region to become a stream on. NoRegion, /// The node holds all it has places for (`places`), whatever waits. Full, @@ -103,9 +85,7 @@ pub enum AcceptRefused { struct Listener { bound: toyos_net_tcp::ListenerId, port: Port, - owner: Box, - /// Wakes written that no accept has spent. - unspent: usize, + holders: Holders, /// The options last written to [tcp]. options: Options, } @@ -115,16 +95,22 @@ pub(crate) struct Listeners { live: BTreeMap, /// The next listener's id: none is used twice. next: u64, - ended: VecDeque<(ListenerId, WriteRefusal)>, +} + +impl Listeners { + /// The holders past each listener's first, which take places of their own. + pub(crate) fn more_holders(&self) -> usize { + self.live.values().map(|listener| listener.holders.more()).fold(0, usize::saturating_add) + } } impl Node { /// A passive open at `addr`, 0.0.0.0 meaning every address the interface holds or comes to /// hold, on `port` or on a port chosen from `draw`'s candidates, with `nodelay` as /// [`Node::set_listener_nodelay`] would set it, for every connection of the listener's - /// from its first. Answers the listener and its port. Refused, nothing was made and - /// `owner` is dropped. - pub fn listen(&mut self, addr: Ipv4Addr, port: Option, nodelay: bool, owner: Box, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> { + /// from its first. Answers the listener and its port; `bell` is its first holder's, + /// [`HolderId::FIRST`]. Refused, nothing was made and `bell` is dropped. + pub fn listen(&mut self, addr: Ipv4Addr, port: Option, nodelay: bool, bell: Box, mut draw: impl FnMut() -> u32) -> Result<(ListenerId, Port), ListenRefused> { if self.room() == 0 { return Err(ListenRefused::Full); } @@ -146,10 +132,32 @@ impl Node { self.listeners.next = self.listeners.next.saturating_add(1); let options = Options { nodelay, ..Options::default() }; self.stack.tcp_set_listener_options(bound, options); - self.listeners.live.insert(id, Listener { bound, port, owner, unspent: 0, options }); + self.listeners.live.insert(id, Listener { bound, port, holders: Holders::new(bell), options }); Ok((id, port)) } + /// A holder more of listener `id`, told on `bell`, which takes a place: the listener's port. + pub fn export_listener(&mut self, id: ListenerId, bell: Box) -> Result<(HolderId, Port), ExportRefused> { + if self.room() == 0 { + return Err(ExportRefused::Full); + } + let listener = self.listeners.live.get_mut(&id).ok_or(ExportRefused::Gone)?; + Ok((listener.holders.add(bell), listener.port)) + } + + /// `holder` of listener `id` left; the last holder's leaving closes it. Either gives a place + /// back. + pub fn listener_departed(&mut self, now: Instant, id: ListenerId, holder: HolderId) { + let Some(listener) = self.listeners.live.get_mut(&id) else { return }; + if !listener.holders.remove(holder) { + return; + } + if listener.holders.is_empty() { + self.end_listener(now, id); + } + self.tell_holders(); + } + /// Nagle's algorithm off or on (RFC 9293 §3.7.4) for every connection whose SYN arrives at /// `id` from here on; one that began before keeps what it has. `false` is an id that names /// no listener. @@ -160,79 +168,58 @@ impl Node { true } - /// The owner's accept: the oldest connection waiting at `id` becomes a stream on `session`'s - /// region, and what it already received moves there at once. - pub fn accept(&mut self, now: Instant, id: ListenerId, session: Option>) -> Result { - let Some(listener) = self.listeners.live.get_mut(&id) else { return Err(AcceptRefused::NoListener) }; - listener.unspent = listener.unspent.saturating_sub(1); + /// Whether Nagle's algorithm is off for the connections that begin at `id` from here on: + /// `None` is an id that names no listener. + pub fn listener_nodelay(&self, id: ListenerId) -> Option { + self.listeners.live.get(&id).map(|listener| listener.options.nodelay) + } + + /// `holder`'s accept: the oldest connection waiting at `id` becomes a stream on the region of + /// the session `session` makes, which is asked for only once there is a connection and a + /// place for it, and what the connection already received moves there at once. + pub fn accept(&mut self, now: Instant, id: ListenerId, holder: HolderId, session: impl FnOnce() -> Option>) -> Result { + let Some(listener) = self.listeners.live.get_mut(&id).filter(|listener| listener.holders.holds(holder)) else { + return Err(AcceptRefused::NoListener); + }; + listener.holders.asked(holder); let bound = listener.bound; let answer = self.take(bound, session); self.pass(now, false); answer } - fn take(&mut self, bound: toyos_net_tcp::ListenerId, session: Option>) -> Result { - let Some(session) = session else { return Err(AcceptRefused::NoRegion) }; + fn take(&mut self, bound: toyos_net_tcp::ListenerId, session: impl FnOnce() -> Option>) -> Result { if self.room() == 0 { return Err(AcceptRefused::Full); } - let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { return Err(AcceptRefused::Nothing) }; + if self.stack.tcp_ready(bound) == 0 { + return Err(AcceptRefused::Nothing); + } + let session = session().ok_or(AcceptRefused::NoRegion)?; + let Some((conn, tuple, options)) = self.stack.tcp_accept(bound) else { unreachable!("[tcp] counted a connection it would not hand over") }; let id = self.streams.accepted(&mut self.stack, conn, options, session); Ok(Accepted { id, remote: tuple.remote, local: tuple.local.port, nodelay: options.nodelay }) } - /// The owner lets go of the listener: netstack calls it for a close, and when the kernel - /// 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); + fn end_listener(&mut self, now: Instant, id: ListenerId) { + if let Some(listener) = self.listeners.live.remove(&id) { + self.stack.tcp_close_listener(now, listener.bound); } - closed } - 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 - } - - /// Writes every owner the wakes it is owed, and ends a listener whose pipe refuses one. One - /// lookup in [tcp] a listener: nothing of [tcp]'s is moved or offered for sending. - 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) {} - } - - /// One round over the listeners, up to the first whose pipe refuses a wake; `true` ended it. - fn wake_each(&mut self, now: Instant) -> bool { + /// Tells every holder of a listener or a datagram socket what it is owed: a listener's, while + /// a connection waits that there is a place for; a datagram socket's, while a datagram + /// waits. One lookup in [tcp] or [udp] a socket: nothing is moved or offered for sending. + pub(crate) fn tell_holders(&mut self) { 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); - while listener.unspent < owed { - if let Err(refusal) = listener.owner.wake() { - return Some((*id, refusal)); - } - listener.unspent = listener.unspent.saturating_add(1); - } - None - }); - let Some((id, refusal)) = ended else { return false }; - self.end_listener(now, id); - self.listeners.ended.push_back((id, refusal)); - true + for listener in self.listeners.live.values_mut() { + listener.holders.tell(room > 0 && self.stack.tcp_ready(listener.bound) > 0); + } + self.tell_datagram_holders(); } /// The listeners the node holds. pub fn listeners(&self) -> usize { self.listeners.live.len() } - - /// The listeners ended since the last call because their owner's pipe refused a wake, and - /// what it answered. - pub fn drain_ended_listeners(&mut self) -> impl Iterator + '_ { - self.listeners.ended.drain(..) - } } diff --git a/system/netstack/node/src/places.rs b/system/netstack/node/src/places.rs index cf5b6032d24..fa608aa869e 100644 --- a/system/netstack/node/src/places.rs +++ b/system/netstack/node/src/places.rs @@ -1,10 +1,11 @@ //! Places: the one bound on the streams, listeners and datagram sockets clients make the node //! hold. A stream holds a place from its connect or its accept until the node lets it go, a -//! listener from its listen until it is closed, a datagram socket from its bind until it is -//! closed, and a connection the node closed holds the place its stream had until [tcp] has -//! finished it, because its buffers are still the region its client lent and its peer decides -//! for how long, within [tcp]'s bounds on a connection nobody holds. With no place left a -//! connect, a listen, an accept and a bind are refused with nothing made. +//! listener from its listen and a datagram socket from its bind until its last holder leaves, +//! each holder past a socket's first a place of its own, and a connection the node closed holds +//! the place its stream had until [tcp] has finished it, because its buffers are still the region +//! its client lent and its peer decides for how long, within [tcp]'s bounds on a connection +//! nobody holds. With no place left a connect, a listen, an accept, a bind and an export are +//! refused with nothing made. //! //! **The node's own sockets stand outside the places.** The responder's socket for the //! machine's name and the socket of each query `resolve` has out are bound past @@ -14,7 +15,8 @@ //! //! The number is the shell's: each stream is its client's region and its first holder's //! connection and two bells kept alive and its connection watched, each holder past the first -//! its own connection and bells, priced as a place and one watch, each listener its wake pipe. +//! its own connection and bells, and each holder of a listener or a datagram socket its +//! connection and one bell, each priced as a place and one watch. //! A new node has none. //! //! **A place is not one size.** A stream or a connection [tcp] finishes is its client's region, @@ -28,24 +30,23 @@ //! alone, so any peer of any listener could then refuse every client its connect, listen and //! bind, where now it can fill only the queue of the listener it reaches. -use toyos_net_wire::Instant; - use crate::Node; 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) { + pub fn set_places(&mut self, places: usize) { self.places = places; - self.wake_owners(now); + self.tell_holders(); } - /// The places taken: streams and their holders past the first, listeners, datagram sockets, - /// and connections [tcp] is finishing alone. + /// The places taken: streams, listeners and datagram sockets and their holders past each + /// first, and connections [tcp] is finishing alone. pub fn held(&self) -> usize { self.streams() .saturating_add(self.streams.more_holders()) .saturating_add(self.listeners()) - .saturating_add(self.sockets) + .saturating_add(self.listeners.more_holders()) + .saturating_add(self.datagram_places()) .saturating_add(self.stack.tcp_orphans()) } diff --git a/system/netstack/node/src/streams.rs b/system/netstack/node/src/streams.rs index 68a8092d8d2..067ec99ac0c 100644 --- a/system/netstack/node/src/streams.rs +++ b/system/netstack/node/src/streams.rs @@ -63,10 +63,10 @@ pub enum ConnectRefused { /// Why an export made no holder. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ExportRefused { - /// The node holds all it has places for, and a holder past a stream's first takes one. + /// The node holds all it has places for, and a holder past a socket's first takes one. Full, - /// The id names no stream the node holds established. - NoStream, + /// The id names no established stream, listener or datagram socket the node holds. + Gone, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -320,7 +320,7 @@ impl Node { let Streams { live, events, .. } = &mut self.streams; let stack = &mut self.stack; live.retain(|id, stream| (connects && stream.server.is_some()) || stream.pass(*id, now, stack, events)); - self.wake_owners(now); + self.tell_holders(); } /// A holder more for established stream `id`, which takes a place: the stream's peer and @@ -329,7 +329,7 @@ impl Node { if self.room() == 0 { return Err(ExportRefused::Full); } - let stream = self.streams.live.get_mut(&id).filter(|stream| stream.server.is_some()).ok_or(ExportRefused::NoStream)?; + let stream = self.streams.live.get_mut(&id).filter(|stream| stream.server.is_some()).ok_or(ExportRefused::Gone)?; stream.more_holders = stream.more_holders.saturating_add(1); Ok(self.stack.tcp_endpoints(stream.conn)) } diff --git a/system/netstack/node/tests/common/mod.rs b/system/netstack/node/tests/common/mod.rs index f516505b957..862d1bbd0d5 100644 --- a/system/netstack/node/tests/common/mod.rs +++ b/system/netstack/node/tests/common/mod.rs @@ -11,12 +11,14 @@ pub mod client; +use std::cell::Cell; use std::net::Ipv4Addr; +use std::rc::Rc; use std::time::Duration; use etherparse::{ArpOperation, Icmpv4Type, LinkSlice, NetSlice, SlicedPacket, TransportSlice}; use toyos_dhcp::HostName; -use toyos_net_node::Node; +use toyos_net_node::{Bell, Node}; use toyos_net_shard::{Config, Secrets}; use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; use toyos_net_wire::Instant; @@ -31,6 +33,26 @@ pub const R: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 254); pub const DNS: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 53); pub const MASK: Ipv4Addr = Ipv4Addr::new(255, 255, 255, 0); +/// A holder's bell: the rings it took, which the test reads by the count it holds. +pub struct Rings(pub Rc>); + +impl Bell for Rings { + fn ring(&mut self) { + self.0.set(self.0.get() + 1); + } +} + +/// A bell, and its count of rings. +pub fn bell() -> (Box, Rc>) { + let rings = Rc::new(Cell::new(0)); + (Box::new(Rings(rings.clone())), rings) +} + +/// A bell whose rings nobody reads. +pub fn unread() -> Box { + bell().0 +} + /// The places `Wire`'s node has for streams and listeners. pub const PLACES: usize = 32; @@ -339,7 +361,7 @@ impl Wire { let config = Config { mac: IndividualMac::new(MacAddr(mac)).unwrap(), receive_buffer: 65_535, send_buffer: 65_535, secrets }; let mut draws = 0x5a00_0000; let mut node = Node::new(now, config, HostName::new("toyos"), draw(&mut draws)).unwrap(); - node.set_places(now, PLACES); + node.set_places(PLACES); Self { node, mac, now, sent: Vec::new(), draws } } diff --git a/system/netstack/node/tests/datagram.rs b/system/netstack/node/tests/datagram.rs index 827fe7ea2be..3ee07186187 100644 --- a/system/netstack/node/tests/datagram.rs +++ b/system/netstack/node/tests/datagram.rs @@ -13,7 +13,7 @@ use std::time::Duration; use common::{from_server, message_of, terms, xid, A, ACK, BROADCAST, DNS, MAC, MAC_B, MAC_R, R}; use lan::{udp, Lan, Seen, Udp, B, OFF_LINK}; -use toyos_net_node::{Datagram, Event, Refused}; +use toyos_net_node::{Datagram, Event, ExportRefused, HolderId, Refused}; use toyos_net_shard::Refusal; use toyos_net_udp::Counter; use toyos_net_wire::Port; @@ -37,13 +37,13 @@ fn undrawn() -> u32 { fn a_socket_holds_the_port_it_named_or_the_one_its_draw_picked() { let mut lan = Lan::new(); lan.lease(3_600); - let (named, held) = lan.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (named, held) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); assert_eq!(held.get(), 4_000); - let (first, drawn) = lan.node.udp_bind(ANY, None, || 5).unwrap(); + let (first, drawn) = lan.node.udp_bind(ANY, None, common::unread(), || 5).unwrap(); assert_eq!(drawn.get(), 49_152 + 5); - let (second, next) = lan.node.udp_bind(ANY, None, || 5).unwrap(); + let (second, next) = lan.node.udp_bind(ANY, None, common::unread(), || 5).unwrap(); assert_eq!(next.get(), 49_152 + 6, "the port after a taken one"); - let (_, wrapped) = lan.node.udp_bind(ANY, None, || 16_384 + 7).unwrap(); + let (_, wrapped) = lan.node.udp_bind(ANY, None, common::unread(), || 16_384 + 7).unwrap(); assert_eq!(wrapped.get(), 49_152 + 7, "a draw past the range is an offset into it"); for id in [named, first, second] { @@ -61,7 +61,7 @@ fn a_socket_holds_the_port_it_named_or_the_one_its_draw_picked() { fn a_datagram_leaves_as_the_call_named_it() { let mut lan = Lan::new(); lan.lease(3_600); - let (id, _) = lan.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); lan.node.udp_send_to(lan.now, id, B, 7, b"to the link").unwrap(); lan.node.udp_send_to(lan.now, id, OFF_LINK, 9, b"past it").unwrap(); assert!(lan.datagrams().is_empty(), "accepted is queued, not sent"); @@ -79,19 +79,19 @@ fn a_datagram_leaves_as_the_call_named_it() { fn a_datagram_for_a_bound_port_is_handed_over_with_where_it_came_from() { let mut lan = Lan::new(); lan.lease(3_600); - let (id, _) = lan.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); let mut out = [0u8; 64]; - assert_eq!(lan.node.udp_recv_from(id, &mut out), Ok(None)); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Ok(None)); lan.deliver(&udp(MAC, MAC_B, (B, 9_999), (A, 4_000), b"ping")); lan.deliver(&udp(MAC, MAC_R, (OFF_LINK, 0), (A, 4_000), b"unanswerable")); lan.deliver(&udp(MAC, MAC_B, (B, 9_999), (A, 4_001), b"nobody's")); - assert_eq!(lan.node.udp_recv_from(id, &mut out), Ok(Some(Datagram { len: 4, source: B, source_port: port(9_999) }))); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Ok(Some(Datagram { len: 4, source: B, source_port: port(9_999) }))); assert_eq!(&out[..4], b"ping"); - assert_eq!(lan.node.udp_recv_from(id, &mut out), Ok(Some(Datagram { len: 12, source: OFF_LINK, source_port: None }))); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Ok(Some(Datagram { len: 12, source: OFF_LINK, source_port: None }))); assert_eq!(&out[..12], b"unanswerable"); - assert_eq!(lan.node.udp_recv_from(id, &mut out), Ok(None), "port 4001's datagram is nobody's"); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Ok(None), "port 4001's datagram is nobody's"); let unreachable: Vec<&Seen> = lan.sent.iter().map(|(_, seen)| seen).filter(|seen| matches!(seen, Seen::PortUnreachable { .. })).collect(); assert_eq!(unreachable, [&Seen::PortUnreachable { to: B }]); assert_eq!(lan.counted(Counter::RxNoSocket), 1); @@ -109,16 +109,16 @@ fn refused_send(lan: &mut Lan, id: toyos_net_node::DatagramId, to: Ipv4Addr, por #[test] fn each_refusal_is_answered_in_the_pipes_word_for_it() { let mut lan = Lan::new(); - let (any, _) = lan.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (any, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); refused_send(&mut lan, any, B, 7, b"early", Refused::NotConnected, Counter::NoSourceAddress); - assert_eq!(lan.node.udp_bind(A, port(4_001), undrawn), Err(Refused::InvalidInput), "an address not yet held"); + assert_eq!(lan.node.udp_bind(A, port(4_001), common::unread(), undrawn), Err(Refused::InvalidInput), "an address not yet held"); assert_eq!(lan.counted(Counter::BindAddressNotLocal), 1); lan.lease(600); - assert_eq!(lan.node.udp_bind(ANY, port(4_000), undrawn), Err(Refused::AddrInUse)); - assert_eq!(lan.node.udp_bind(ANY, port(68), undrawn), Err(Refused::AddrInUse), "the DHCP client's port"); + assert_eq!(lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn), Err(Refused::AddrInUse)); + assert_eq!(lan.node.udp_bind(ANY, port(68), common::unread(), undrawn), Err(Refused::AddrInUse), "the DHCP client's port"); assert_eq!(lan.counted(Counter::PortInUse), 2); - let (held, _) = lan.node.udp_bind(A, port(4_001), undrawn).unwrap(); + let (held, _) = lan.node.udp_bind(A, port(4_001), common::unread(), undrawn).unwrap(); let invalid: [(Ipv4Addr, u16, &[u8], Counter); 6] = [ (B, 0, b"x", Counter::SendPortZero), @@ -150,7 +150,7 @@ fn each_refusal_is_answered_in_the_pipes_word_for_it() { let mut routerless = Lan::new(); routerless.lease_on(&terms(3_600, None)); - let (id, _) = routerless.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (id, _) = routerless.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); refused_send(&mut routerless, id, OFF_LINK, 7, b"x", Refused::NotConnected, Counter::NoRoute); } @@ -163,8 +163,8 @@ fn each_refusal_is_answered_in_the_pipes_word_for_it() { fn a_broadcast_needs_its_permission() { let mut lan = Lan::new(); lan.lease(3_600); - let (id, _) = lan.node.udp_bind(ANY, port(5_000), undrawn).unwrap(); - let (other, _) = lan.node.udp_bind(ANY, port(5_002), undrawn).unwrap(); + let (id, _) = lan.node.udp_bind(ANY, port(5_000), common::unread(), undrawn).unwrap(); + let (other, _) = lan.node.udp_bind(ANY, port(5_002), common::unread(), undrawn).unwrap(); let frames = lan.sent.len(); for to in [Ipv4Addr::BROADCAST, SUBNET_BROADCAST] { refused_send(&mut lan, id, to, 5_001, b"hi", Refused::PermissionDenied, Counter::BroadcastNotPermitted); @@ -191,7 +191,8 @@ fn a_broadcast_needs_its_permission() { let destinations: Vec = lan.datagrams().iter().map(|(_, udp)| udp.destination).collect(); assert_eq!(destinations, [Ipv4Addr::BROADCAST, SUBNET_BROADCAST, B], "and no broadcast since the permission went"); - lan.node.udp_close(lan.now, id).unwrap(); + assert_eq!(lan.node.udp_broadcast(id), Ok(false), "as it was taken back"); + lan.node.datagram_departed(lan.now, id, HolderId::FIRST); assert_eq!(lan.node.udp_set_broadcast(id, true), Err(Refused::NotConnected), "a closed socket is given nothing"); } @@ -206,7 +207,7 @@ fn a_datagram_accepted_for_a_host_is_no_broadcast_after_the_server_renews_a_narr let edge = Ipv4Addr::new(192, 0, 2, 127); let mut lan = Lan::new(); lan.lease(600); - let (id, _) = lan.node.udp_bind(ANY, port(5_000), undrawn).unwrap(); + let (id, _) = lan.node.udp_bind(ANY, port(5_000), common::unread(), undrawn).unwrap(); let requests = |lan: &Lan| lan.udp().into_iter().filter(|udp| udp.port == 67).count(); let sent = requests(&lan); assert!(lan.run_until(Duration::from_secs(400), |lan| requests(lan) > sent), "the renewal leaves"); @@ -241,7 +242,7 @@ fn line(rule: Counter, peer: (Ipv4Addr, u16), suppressed: u64) -> Event { fn a_refusal_is_logged_by_the_call_that_met_it() { let mut lan = Lan::new(); lan.lease(3_600); - let (id, _) = lan.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); assert_eq!(lan.node.drain_events().count(), 0, "nothing is owed the log before the send"); assert_eq!(lan.node.udp_send_to(lan.now, id, Ipv4Addr::BROADCAST, 7, b"x"), Err(Refused::PermissionDenied)); @@ -263,20 +264,70 @@ fn a_refusal_is_logged_by_the_call_that_met_it() { assert_eq!(logged, [line(Counter::BroadcastNotPermitted, (Ipv4Addr::BROADCAST, 9), 1)]); } +// A socket's holders take from one queue: each datagram is handed whole to the one holder whose +// receive takes it, each holder is rung once a look while one waits, and a holder that left +// receives nothing. The socket and its port stand until the last holder leaves. +#[test] +fn every_holder_receives_from_one_queue_each_datagram_once_and_whole() { + let mut lan = Lan::new(); + lan.lease(3_600); + let (first_bell, first_rings) = common::bell(); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), first_bell, undrawn).unwrap(); + let (second_bell, second_rings) = common::bell(); + let (second, at) = lan.node.export_datagram(id, second_bell).expect("a place for a holder"); + assert_eq!(at.get(), 4_000, "a holder is told the socket's port"); + let rings = || (first_rings.get(), second_rings.get()); + assert_eq!(rings(), (0, 0), "nothing waits"); + + lan.deliver(&udp(MAC, MAC_B, (B, 9_999), (A, 4_000), b"one")); + lan.deliver(&udp(MAC, MAC_B, (B, 9_999), (A, 4_000), b"two, longer")); + assert_eq!(rings(), (1, 1), "each holder is told once while datagrams wait"); + + let mut out = [0u8; 64]; + let got = lan.node.udp_recv_from(id, second, &mut out).unwrap().unwrap(); + assert_eq!(&out[..got.len], b"one"); + assert_eq!(rings(), (1, 2), "the holder that looked is told what is left"); + let got = lan.node.udp_recv_from(id, HolderId::FIRST, &mut out).unwrap().unwrap(); + assert_eq!(&out[..got.len], b"two, longer", "the next datagram, whole, to the other holder"); + assert_eq!(lan.node.udp_recv_from(id, second, &mut out), Ok(None), "each datagram went once"); + assert_eq!(rings(), (1, 2), "nothing left, no ring"); + + lan.node.datagram_departed(lan.now, id, second); + assert_eq!(lan.node.udp_recv_from(id, second, &mut out), Err(Refused::NotConnected), "a holder that left takes nothing"); + assert_eq!(lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn), Err(Refused::AddrInUse), "the first holder still holds the port"); + lan.node.datagram_departed(lan.now, id, HolderId::FIRST); + assert_eq!(lan.node.export_datagram(id, common::unread()).err(), Some(ExportRefused::Gone)); + lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).expect("the last holder's leaving freed the port"); +} + +// A holder past a socket's first takes a place, and its leaving gives it back. +#[test] +fn a_datagram_sockets_holder_past_its_first_takes_a_place() { + let mut lan = Lan::new(); + lan.lease(3_600); + lan.node.set_places(2); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); + let (second, _) = lan.node.export_datagram(id, common::unread()).expect("a second place"); + assert_eq!(lan.node.held(), 2); + assert_eq!(lan.node.export_datagram(id, common::unread()).err(), Some(ExportRefused::Full)); + lan.node.datagram_departed(lan.now, id, second); + assert_eq!(lan.node.held(), 1); +} + // A closed socket's port is free at once, and what it had accepted still leaves: in the first // opportunity after the close, not one later. #[test] fn a_closed_socket_frees_its_port_and_what_it_accepted_still_leaves() { let mut lan = Lan::new(); lan.lease(3_600); - let (id, _) = lan.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); lan.node.udp_send_to(lan.now, id, B, 7, b"first").unwrap(); lan.pump(); assert_eq!(lan.datagrams().len(), 1, "B's link address is known from here"); lan.node.udp_send_to(lan.now, id, B, 7, b"second").unwrap(); lan.node.udp_send_to(lan.now, id, B, 7, b"third").unwrap(); - assert_eq!(lan.node.udp_close(lan.now, id), Ok(())); + lan.node.datagram_departed(lan.now, id, HolderId::FIRST); lan.opportunity(); let payloads: Vec<&[u8]> = lan.datagrams().iter().map(|(_, udp)| udp.payload.as_slice()).collect(); assert_eq!(payloads, [&b"first"[..], &b"second"[..], &b"third"[..]]); @@ -284,14 +335,14 @@ fn a_closed_socket_frees_its_port_and_what_it_accepted_still_leaves() { let mut out = [0u8; 8]; assert_eq!(lan.node.udp_send_to(lan.now, id, B, 7, b"after"), Err(Refused::NotConnected)); - assert_eq!(lan.node.udp_recv_from(id, &mut out), Err(Refused::NotConnected)); - assert_eq!(lan.node.udp_close(lan.now, id), Err(Refused::NotConnected)); - let (again, held) = lan.node.udp_bind(ANY, port(4_000), undrawn).expect("the port is free"); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Err(Refused::NotConnected)); + lan.node.datagram_departed(lan.now, id, HolderId::FIRST); + let (again, held) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).expect("the port is free"); assert_eq!(held.get(), 4_000); assert_ne!(again, id, "a closed socket's id names no later socket"); lan.deliver(&udp(MAC, MAC_B, (B, 9_999), (A, 4_000), b"ping")); - assert_eq!(lan.node.udp_recv_from(id, &mut out), Err(Refused::NotConnected)); - assert_eq!(lan.node.udp_recv_from(again, &mut out).map(|got| got.map(|datagram| datagram.len)), Ok(Some(4))); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Err(Refused::NotConnected)); + assert_eq!(lan.node.udp_recv_from(again, HolderId::FIRST, &mut out).map(|got| got.map(|datagram| datagram.len)), Ok(Some(4))); } // RFC 1122 §2.3.2.1 and RFC 4861 §7.2.2 have address resolution give up after its requests. A @@ -304,7 +355,7 @@ fn a_clients_datagram_for_a_host_nobody_answers_for_is_dropped_and_the_client_is const NOBODY: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 9); let mut lan = Lan::new(); lan.lease(3_600); - let (id, _) = lan.node.udp_bind(ANY, port(4_000), undrawn).unwrap(); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); lan.node.udp_send_to(lan.now, id, NOBODY, 7, b"anyone").unwrap(); lan.node.udp_send_to(lan.now, id, B, 7, b"b").unwrap(); lan.pump(); @@ -315,6 +366,6 @@ fn a_clients_datagram_for_a_host_nobody_answers_for_is_dropped_and_the_client_is assert!(lan.run_until(Duration::from_secs(10), |lan| lan.counted(Counter::TxUnreachable) == 1), "the datagram is dropped"); assert_eq!(lan.now, sent.after(Duration::from_secs(3)), "when [ip] gave the host up"); assert_eq!(lan.datagrams().len(), 1); - assert_eq!(lan.node.udp_recv_from(id, &mut [0u8; 8]), Ok(None)); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut [0u8; 8]), Ok(None)); assert_eq!(lan.node.udp_send_to(lan.now, id, B, 7, b"again"), Ok(())); } diff --git a/system/netstack/node/tests/lease.rs b/system/netstack/node/tests/lease.rs index a6ee8cd6d13..460a578bb3f 100644 --- a/system/netstack/node/tests/lease.rs +++ b/system/netstack/node/tests/lease.rs @@ -14,7 +14,7 @@ use std::time::Duration; use common::*; use etherparse::{PacketBuilder, TcpOptionElement}; use toyos_net_ip::AddrState; -use toyos_net_node::{Counter, Event, ListenRefused, Wake, WriteRefusal}; +use toyos_net_node::{Bell, Counter, Event, ListenRefused}; use toyos_net_shard::Refusal; use toyos_net_wire::{Instant, Port}; @@ -218,8 +218,8 @@ fn a_second_conflict_takes_a_held_lease_and_declines_it() { assert!(!wire.answers_arp_for(A)); } -/// What a listener's owner saw of its wake pipe: the wakes written, and whether the node dropped -/// its end, at which the owner reads the end. +/// What a listener's holder saw of its bell: the rings, and whether the node dropped it, which +/// is the listener's end. #[derive(Default)] struct Told { wakes: Cell, @@ -228,10 +228,9 @@ struct Told { struct Owner(Rc); -impl Wake for Owner { - fn wake(&mut self) -> Result<(), WriteRefusal> { +impl Bell for Owner { + fn ring(&mut self) { self.0.wakes.set(self.0.wakes.get() + 1); - Ok(()) } } @@ -310,7 +309,7 @@ fn a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back() { let named = listen(&mut wire, A, 22).expect("the held address"); let any = listen(&mut wire, Ipv4Addr::UNSPECIFIED, 23).expect("every address"); let stand = |wire: &mut Wire, when: &str| { - assert_eq!((wire.node.listeners(), wire.node.held(), wire.node.drain_ended_listeners().count()), (2, 2, 0), "{how}, {when}"); + assert_eq!((wire.node.listeners(), wire.node.held()), (2, 2), "{how}, {when}"); assert_eq!((named.dropped.get(), any.dropped.get(), named.wakes.get(), any.wakes.get()), (false, false, 0, 0), "{how}, {when}"); }; diff --git a/system/netstack/node/tests/listeners.rs b/system/netstack/node/tests/listeners.rs index d5c640a37df..b239666d62b 100644 --- a/system/netstack/node/tests/listeners.rs +++ b/system/netstack/node/tests/listeners.rs @@ -2,7 +2,7 @@ //! puts one elsewhere, and clients that are `toyos::transport::session`'s own client end over a //! region on the heap (`common::client`). No scenario ids: the specifications' listening //! scenarios are `toyos-net-tcp`'s; these are what the node does between [tcp]'s queues, an -//! owner's wakes and its accepts. +//! holders' rings and their accepts. //! //! What is not ours: every segment the node emits is read by `etherparse` as it leaves //! ([`Net::hears`]), its IPv4 and TCP checksums and lengths that crate's sums, and every segment @@ -23,7 +23,7 @@ use std::time::Duration; use common::client::Client; use common::{arp, batch, terms, Segment, Wire, A, BUDGET, ELSEWHERE, MAC, MAC_R, R}; use etherparse::{ArpOperation, LinkSlice, NetSlice, PacketBuilder, SlicedPacket, TcpOptionElement, TransportSlice}; -use toyos_net_node::{AcceptRefused, Accepted, ConnectRefused, ListenRefused, ListenerId, Node, Refused, StreamEvent, StreamId, Wake, WriteRefusal}; +use toyos_net_node::{AcceptRefused, Accepted, Bell, ConnectRefused, ExportRefused, HolderId, ListenRefused, ListenerId, Node, Refused, StreamEvent, StreamId}; use toyos::transport::session::Refused as Read; use toyos::transport::Violation; use toyos_net_tcp::{limits, Counter, Endpoint}; @@ -71,14 +71,12 @@ const SYN: u8 = 1; const FIN: u8 = 2; const RST: u8 = 4; -// ---- an owner's wake pipe ---- +// ---- a holder's bell ---- #[derive(Debug, Default)] struct Notified { - /// The wakes written and not refused. + /// The rings. wakes: usize, - /// What the pipe answers a wake instead of taking it. - refusal: Option, dropped: bool, } @@ -86,14 +84,9 @@ type Owner = Rc>; struct WakeEnd(Owner); -impl Wake for WakeEnd { - fn wake(&mut self) -> Result<(), WriteRefusal> { - let mut notified = self.0.borrow_mut(); - if let Some(refusal) = notified.refusal { - return Err(refusal); - } - notified.wakes += 1; - Ok(()) +impl Bell for WakeEnd { + fn ring(&mut self) { + self.0.borrow_mut().wakes += 1; } } @@ -279,12 +272,6 @@ impl Net { } /// The peer's handshake with the node's port `to`. - fn handshake_to(&mut self, peer: Peer, to: u16) { - self.syn_to(peer, to); - let ack = self.iss(peer).wrapping_add(1); - self.deliver(&frame(peer, to, ISS + 1, Some(ack), 0, &[])); - } - fn handshake(&mut self, peer: Peer) { self.syn(peer); self.finish(peer); @@ -331,17 +318,41 @@ impl Net { (id, owner) } - /// The owner's accept, on a new client's region. + /// The first holder's accept, on a new client's region. fn accept(&mut self, id: ListenerId) -> Result<(Accepted, Rc), AcceptRefused> { + self.accept_as(id, HolderId::FIRST) + } + + /// `holder`'s accept, on a new client's region. + fn accept_as(&mut self, id: ListenerId, holder: HolderId) -> Result<(Accepted, Rc), AcceptRefused> { let (client, session) = Client::new(); - let answer = self.node.accept(self.now, id, Some(session)); + let answer = self.node.accept(self.now, id, holder, || Some(session)); self.pump(); answer.map(|accepted| (accepted, client)) } + /// A holder more of listener `id`, and its bell. + fn export(&mut self, id: ListenerId) -> (HolderId, Owner) { + let owner = Owner::default(); + let (holder, port) = self.node.export_listener(id, Box::new(WakeEnd(owner.clone()))).expect("a place for a holder"); + assert_eq!(port.get(), SSH, "a holder is told the listener's port"); + (holder, owner) + } + + /// The listener's one holder leaves, and the pass after. + fn close(&mut self, id: ListenerId) { + self.node.listener_departed(self.now, id, HolderId::FIRST); + self.pump(); + } + /// An accept that takes `peer`'s connection. fn accepts(&mut self, id: ListenerId, peer: Peer) -> (StreamId, Rc) { - let (accepted, client) = self.accept(id).expect("a connection and a place"); + self.accepts_as(id, HolderId::FIRST, peer) + } + + /// `holder`'s accept that takes `peer`'s connection. + fn accepts_as(&mut self, id: ListenerId, holder: HolderId, peer: Peer) -> (StreamId, Rc) { + let (accepted, client) = self.accept_as(id, holder).expect("a connection and a place"); assert_eq!((accepted.remote, accepted.local), (peer.endpoint(), Port::new(SSH).unwrap())); (accepted.id, client) } @@ -378,7 +389,7 @@ impl Net { /// A node with `places` places. fn with_places(places: usize) -> Net { let mut net = Net::new(); - net.node.set_places(net.now, places); + net.node.set_places(places); net } @@ -405,7 +416,7 @@ fn a_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_ends() { assert_eq!((net.node.streams(), net.node.listeners(), net.node.held()), (0, 1, 1), "a connection that waits is nobody's stream"); let (client, session) = Client::new(); - let accepted = net.node.accept(net.now, id, Some(session)).unwrap(); + let accepted = net.node.accept(net.now, id, HolderId::FIRST, || Some(session)).unwrap(); assert_eq!((accepted.remote, accepted.local), (P1.endpoint(), Port::new(SSH).unwrap())); assert_eq!(inbox(&client), b"hello", "what arrived before the accept moves in it"); assert_eq!((net.node.streams(), net.node.listeners(), net.node.held()), (1, 1, 2)); @@ -469,17 +480,18 @@ fn a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up() { // The recorded failure of `issues/a-connect-between-two-accepts-is-reset.md`: a connection waits // for its accept, and the stack this one replaces answered every other peer's SYN with a reset // until the owner had taken it. Here each is answered, queued behind the one that waits, and -// accepted oldest first, with one wake a connection. +// accepted oldest first, the holder rung after each accept that leaves one waiting. #[test] fn a_connect_between_two_accepts_is_queued_not_reset() { let mut net = Net::new(); let (id, owner) = net.listen(SSH); net.handshake(P1); net.handshake(P2); - assert_eq!(wakes(&owner), 2, "one wake a connection"); + assert_eq!(wakes(&owner), 1, "one ring until the holder looks"); net.accepts(id, P1); + assert_eq!(wakes(&owner), 2, "a ring for the one left"); net.handshake(P3); - assert_eq!(wakes(&owner), 3); + assert_eq!(wakes(&owner), 2, "told already"); net.accepts(id, P2); net.accepts(id, P3); assert!(net.heard.iter().all(|segment| !segment.rst), "{:?}", net.heard); @@ -487,12 +499,13 @@ fn a_connect_between_two_accepts_is_queued_not_reset() { assert_eq!((wakes(&owner), net.node.streams()), (3, 3)); } -// ---- wakes ---- +// ---- rings ---- // RFC 9293 §3.10.7.4, first check: a reset at RCV.NXT ends the connection. Nobody had taken it, -// so [tcp] returns it to no accept, and the wake written for it finds nothing. +// so [tcp] returns it to no accept, and the ring rung for it finds nothing. Whatever an accept +// answers, the holder is told what it leaves. #[test] -fn an_accept_spends_a_wake_whatever_it_answers() { +fn an_accept_whatever_it_answers_is_a_look() { let mut net = Net::new(); let (id, owner) = net.listen(SSH); net.handshake(P1); @@ -501,25 +514,99 @@ fn an_accept_spends_a_wake_whatever_it_answers() { assert_eq!(wakes(&owner), 1); net.handshake(P2); - assert_eq!(wakes(&owner), 2, "the wake that accept spent is not counted against the next connection"); - assert_eq!(net.node.accept(net.now, id, None), Err(AcceptRefused::NoRegion)); + assert_eq!(wakes(&owner), 2, "the look that found nothing is owed the next connection"); + let mut made = false; + assert_eq!(net.node.accept(net.now, id, HolderId::FIRST, || { made = true; None }), Err(AcceptRefused::NoRegion)); + assert!(made, "a region is asked for once a connection waits"); assert_eq!(wakes(&owner), 3, "the connection an accept without a region left is announced again"); net.accepts(id, P2); assert_eq!(wakes(&owner), 3); + assert_eq!(net.node.accept(net.now, id, HolderId::FIRST, || panic!("no region is made for nothing")), Err(AcceptRefused::Nothing)); } #[test] -fn a_wake_left_by_a_connection_its_peer_reset_stands_for_the_next() { +fn a_ring_left_by_a_connection_its_peer_reset_stands_for_the_next() { let mut net = Net::new(); let (id, owner) = net.listen(SSH); net.handshake(P1); net.rst(P1); net.handshake(P2); - assert_eq!(wakes(&owner), 1, "the owner holds one wake and one connection waits"); + assert_eq!(wakes(&owner), 1, "the holder was told and has not looked: it looks and finds the next"); net.accepts(id, P2); assert_eq!(wakes(&owner), 1); } +// ---- holders ---- + +// Two holders, one queue: both are told of the first connection, each accept takes a different +// one, and the one that finds none is answered `Nothing`. Each is told again after a look that +// leaves one waiting. +#[test] +fn every_holder_takes_from_one_queue_and_the_loser_finds_nothing() { + let mut net = Net::new(); + let (id, first) = net.listen(SSH); + let (second, other) = net.export(id); + net.handshake(P1); + net.handshake(P2); + assert_eq!((wakes(&first), wakes(&other)), (1, 1), "each holder told once"); + net.accepts_as(id, second, P1); + assert_eq!((wakes(&first), wakes(&other)), (1, 2), "the holder that took one is told of the next"); + net.accepts(id, P2); + assert_eq!(net.accept_as(id, second).unwrap_err(), AcceptRefused::Nothing, "the queue is one: the second holder's look finds it empty"); + assert_eq!(net.node.streams(), 2); + net.handshake(P3); + assert_eq!((wakes(&first), wakes(&other)), (2, 3), "both looked since, and both are told"); +} + +// A holder that never looks takes nothing from one that does: the one that looks is rung for +// every connection it leaves waiting, and accepts each. +#[test] +fn a_holder_that_never_looks_loses_no_connection_for_one_that_does() { + let mut net = Net::new(); + let (id, idle) = net.listen(SSH); + let (busy, owner) = net.export(id); + for (nth, peer) in [P1, P2, P3].into_iter().enumerate() { + net.handshake(peer); + assert_eq!(wakes(&owner), nth + 1, "the looking holder is rung for {peer:?}"); + net.accepts_as(id, busy, peer); + } + assert_eq!((wakes(&idle), net.node.streams()), (1, 3), "the idle holder was told once and never looked"); +} + +// A listener stands while any holder holds it: the first holder's leaving leaves the port +// listening and its connections to the other, whose leaving ends it as a close does. +#[test] +fn a_listener_ends_with_its_last_holder() { + let mut net = Net::new(); + let (id, first) = net.listen(SSH); + let (second, other) = net.export(id); + assert_eq!(net.node.held(), 2, "a holder past the first takes a place"); + net.handshake(P1); + net.node.listener_departed(net.now, id, HolderId::FIRST); + assert!(first.borrow().dropped, "the holder's bell goes with it"); + assert_eq!(net.accept(id).unwrap_err(), AcceptRefused::NoListener, "a holder that left accepts nothing"); + assert_eq!((net.node.listeners(), net.node.held(), wakes(&other)), (1, 1, 1)); + net.accepts_as(id, second, P1); + net.handshake(P2); + net.node.listener_departed(net.now, id, second); + net.pump(); + assert!(net.last(P2).rst, "the last holder's leaving resets what waits"); + assert!(other.borrow().dropped); + assert_eq!((net.node.listeners(), net.node.held()), (0, 1), "the stream holds its place"); + assert_eq!(net.node.export_listener(id, Box::new(WakeEnd(Owner::default()))).err(), Some(ExportRefused::Gone)); + net.listen(SSH); +} + +#[test] +fn an_export_past_the_places_makes_no_holder() { + let mut net = with_places(1); + let (id, _owner) = net.listen(SSH); + let refused = Owner::default(); + assert_eq!(net.node.export_listener(id, Box::new(WakeEnd(refused.clone()))).err(), Some(ExportRefused::Full)); + assert!(refused.borrow().dropped); + assert_eq!(net.accept(id).unwrap_err(), AcceptRefused::Full, "the one holder there is holds the one place"); +} + // ---- places ---- // The close is RFC 9293 §3.6, case 1: the node's FIN, then the peer's FIN acknowledging it. Until @@ -595,7 +682,7 @@ fn closing_a_listener_gives_its_place_to_a_connection_that_waits_at_another() { let (other, _) = net.listen(TELNET); net.handshake(P1); assert_eq!(wakes(&owner), 0); - assert!(net.node.close_listener(net.now, other)); + net.close(other); assert_eq!(wakes(&owner), 1); } @@ -605,7 +692,7 @@ fn more_places_wake_the_owner_of_a_connection_that_waits() { let (_, owner) = net.listen(SSH); net.handshake(P1); assert_eq!(wakes(&owner), 0); - net.node.set_places(net.now, 2); + net.node.set_places(2); assert_eq!(wakes(&owner), 1); } @@ -643,7 +730,7 @@ fn a_listener_holds_a_place_and_a_listen_without_one_makes_nothing() { let (connect, _client) = net.connect(); assert_eq!(connect, Err(ConnectRefused::Full), "the listener's place is a stream's"); - assert!(net.node.close_listener(net.now, id)); + net.close(id); assert_eq!(net.node.held(), 0); net.listen(TELNET); } @@ -659,8 +746,7 @@ fn closing_a_listener_resets_what_waits_and_frees_its_port() { let (id, owner) = net.listen(SSH); net.syn(P1); net.handshake(P2); - assert!(net.node.close_listener(net.now, id)); - net.pump(); + net.close(id); for peer in [P1, P2] { let reset = net.last(peer); assert!(reset.rst && reset.seq == net.iss(peer).wrapping_add(1), "{reset:?}"); @@ -668,28 +754,14 @@ fn closing_a_listener_resets_what_waits_and_frees_its_port() { assert!(owner.borrow().dropped, "the owner reads the end"); assert_eq!((net.node.listeners(), net.node.held()), (0, 0)); assert_eq!(net.accept(id).unwrap_err(), AcceptRefused::NoListener); - assert!(!net.node.close_listener(net.now, id)); + net.close(id); + assert_eq!((net.node.listeners(), net.node.held()), (0, 0), "a holder that left leaves no more"); let (_, again) = net.listen(SSH); net.handshake(P3); assert_eq!((wakes(&owner), wakes(&again)), (1, 1)); } -#[test] -fn a_wake_the_owners_pipe_refuses_ends_the_listener() { - for refusal in [WriteRefusal::Gone, WriteRefusal::Full, WriteRefusal::Broken] { - let mut net = Net::new(); - let (id, owner) = net.listen(SSH); - owner.borrow_mut().refusal = Some(refusal); - net.handshake(P1); - let ended: Vec<(ListenerId, WriteRefusal)> = net.node.drain_ended_listeners().collect(); - assert_eq!(ended, [(id, refusal)]); - assert!(owner.borrow().dropped && net.last(P1).rst, "{refusal:?}: {:?}", net.heard); - assert_eq!((net.node.listeners(), net.node.held()), (0, 0), "{refusal:?}"); - net.listen(SSH); - } -} - // The drawn port is in the dynamic range, 49152 to 65535 (RFC 6335 §6), at the offset the draw's // low sixteen bits name. #[test] @@ -770,47 +842,27 @@ fn a_second_listen_at_the_machines_address_takes_no_port_held_there() { a_port_is_one_listeners(A, A); } -// A listener ended for a wake its owner's pipe refused gives its place back in the pass that -// ended it: the listener after it is owed a wake for that place before the node is asked -// anything more. The last handshake's ACK is delivered with no transmit opportunity after it, -// since an opportunity ends in a pass of its own. -#[test] -fn a_listener_ended_for_its_wake_gives_its_place_to_another_in_the_same_pass() { - let mut net = with_places(3); - let (first, refusing) = net.listen(SSH); - let (_, owner) = net.listen(TELNET); - net.handshake_to(P1, TELNET); - net.handshake_to(P2, TELNET); - assert_eq!(wakes(&owner), 1, "two wait, and there is a place for one"); - refusing.borrow_mut().refusal = Some(WriteRefusal::Gone); - net.syn(P3); - let ack = net.iss(P3).wrapping_add(1); - net.node.receive(net.now, BUDGET, batch(&[&frame(P3, SSH, ISS + 1, Some(ack), 0, &[])]), draw(&mut net.draws)); - assert_eq!(wakes(&owner), 2); - let ended: Vec<(ListenerId, WriteRefusal)> = net.node.drain_ended_listeners().collect(); - assert_eq!((ended, net.node.listeners(), net.node.held()), (vec![(first, WriteRefusal::Gone)], 1, 1)); -} - // A datagram socket is something a client makes the node hold, its two queues of sixteen -// datagrams, from its bind to its close. +// datagrams, from its bind until its last holder leaves. #[test] fn a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing() { let undrawn = || -> u32 { panic!("a named port draws nothing") }; + let bell = || Box::new(WakeEnd(Owner::default())); let mut net = with_places(2); let (_, owner) = net.listen(SSH); - let (socket, _) = net.node.udp_bind(ANY, Port::new(4_000), undrawn).expect("a free port and a place"); + let (socket, _) = net.node.udp_bind(ANY, Port::new(4_000), bell(), undrawn).expect("a free port and a place"); assert_eq!(net.node.held(), 2); - assert_eq!(net.node.udp_bind(ANY, Port::new(4_001), undrawn), Err(Refused::ResourceExhausted)); + assert_eq!(net.node.udp_bind(ANY, Port::new(4_001), bell(), undrawn), Err(Refused::ResourceExhausted)); let (connect, _client) = net.connect(); assert_eq!(connect, Err(ConnectRefused::Full), "the socket's place is a stream's"); net.handshake(P1); assert_eq!(wakes(&owner), 0); - assert_eq!(net.node.udp_close(net.now, socket), Ok(())); + net.node.datagram_departed(net.now, socket, HolderId::FIRST); assert_eq!((net.node.held(), wakes(&owner)), (1, 1), "its place is back, and the connection that waited is announced"); - assert_eq!(net.node.udp_close(net.now, socket), Err(Refused::NotConnected)); - assert_eq!(net.node.held(), 1, "a close that names no socket gives no place back"); - net.node.udp_bind(ANY, Port::new(4_001), undrawn).expect("the refused bind left its port free"); + net.node.datagram_departed(net.now, socket, HolderId::FIRST); + assert_eq!(net.node.held(), 1, "a holder that left gives no place back twice"); + net.node.udp_bind(ANY, Port::new(4_001), bell(), undrawn).expect("the refused bind left its port free"); assert_eq!(net.node.held(), 2); } @@ -826,7 +878,9 @@ fn a_stream_starts_with_the_options_its_connection_took_from_its_listener() { let (id, _owner) = net.listen(SSH); // One connection begins before the option is set and one after; both are accepted after. net.handshake(P1); + assert_eq!(net.node.listener_nodelay(id), Some(false)); assert!(net.node.set_listener_nodelay(id, true)); + assert_eq!(net.node.listener_nodelay(id), Some(true), "what any holder reads back"); net.handshake(P2); let (first, a) = net.accept(id).unwrap(); let (second, b) = net.accept(id).unwrap(); @@ -857,8 +911,9 @@ fn a_stream_starts_with_the_options_its_connection_took_from_its_listener() { net.says(third.id, &c, b"b"); assert_eq!(net.texts(P3), [b"a".to_vec()]); - assert!(net.node.close_listener(net.now, id)); + net.close(id); assert!(!net.node.set_listener_nodelay(id, true), "a closed listener's id names nothing"); + assert_eq!(net.node.listener_nodelay(id), None); } // A listen names its listener's option, so no SYN reaches the port between the passive open and @@ -950,7 +1005,7 @@ fn peers_at_more_addresses_than_there_are_places_hold_no_connection_past_them() net.handshake(Peer { addr, port: 42_000 + nth }); } } - assert_eq!((wakes(&owner), net.node.streams(), net.node.held()), (2, 0, 1), "sixty-four wait, and there are places for two"); + assert_eq!((wakes(&owner), net.node.streams(), net.node.held()), (1, 0, 1), "sixty-four wait: one ring until the holder looks"); for (round, pair) in [[B, C], [D, E]].into_iter().enumerate() { let peers = pair.map(|addr| Peer { addr, port: 42_000 }); @@ -961,7 +1016,7 @@ fn peers_at_more_addresses_than_there_are_places_hold_no_connection_past_them() let full = |net: &mut Net| { assert_eq!(net.accept(id).unwrap_err(), AcceptRefused::Full, "round {round}"); assert_eq!(net.connect().0, Err(ConnectRefused::Full), "round {round}"); - assert_eq!((net.node.streams(), net.node.held(), wakes(&owner)), (0, 3, 2 + 2 * round)); + assert_eq!((net.node.streams(), net.node.held(), wakes(&owner)), (0, 3, 2 + 2 * round), "no place, no ring"); }; full(&mut net); for step in 1..=5 { @@ -972,11 +1027,11 @@ fn peers_at_more_addresses_than_there_are_places_hold_no_connection_past_them() full(&mut net); } - // The peers take no more: [tcp] gives both connections up, and two of the connections - // that wait are announced. + // The peers take no more: [tcp] gives both connections up, and the holder is told the + // connections that wait. net.run(limits::ORPHAN_IDLE + Duration::from_secs(1)); assert_eq!(net.events(), vec![], "round {round}"); - assert_eq!((net.node.streams(), net.node.held(), wakes(&owner)), (0, 1, 4 + 2 * round)); + assert_eq!((net.node.streams(), net.node.held(), wakes(&owner)), (0, 1, 3 + 2 * round)); } } @@ -997,7 +1052,7 @@ fn no_cut_of_a_syn_is_a_syn() { } #[test] -fn no_flipped_bit_of_a_syn_wakes_an_owner_or_ends_a_listener() { +fn no_flipped_bit_of_a_syn_rings_a_holder_or_ends_a_listener() { let whole = frame(P1, SSH, ISS, None, SYN, &[]); for index in 0..whole.len() { for bit in 0..8 { diff --git a/system/netstack/node/tests/name.rs b/system/netstack/node/tests/name.rs index a6d2d1e7adf..888ac14cdbd 100644 --- a/system/netstack/node/tests/name.rs +++ b/system/netstack/node/tests/name.rs @@ -671,12 +671,12 @@ fn no_cut_of_a_query_is_answered() { fn the_names_port_is_held_by_one_socket() { let mdns = Port::new(MDNS); let mut lan = named(); - assert_eq!(lan.node.udp_bind(Ipv4Addr::UNSPECIFIED, mdns, || 0).map(|(_, port)| port), Err(Refused::AddrInUse)); + assert_eq!(lan.node.udp_bind(Ipv4Addr::UNSPECIFIED, mdns, common::unread(), || 0).map(|(_, port)| port), Err(Refused::AddrInUse)); let again = lan.node.answer_as(lan.now, Host::new("toyos").unwrap(), undrawn); assert_eq!(again, Err(toyos_net_udp::Error::Refused(toyos_net_udp::Counter::PortInUse))); let mut taken = Lan::new(); - taken.node.udp_bind(Ipv4Addr::UNSPECIFIED, mdns, || 0).unwrap(); + taken.node.udp_bind(Ipv4Addr::UNSPECIFIED, mdns, common::unread(), || 0).unwrap(); let refused = taken.node.answer_as(taken.now, Host::new("toyos").unwrap(), undrawn); assert_eq!(refused, Err(toyos_net_udp::Error::Refused(toyos_net_udp::Counter::PortInUse))); taken.lease(3_600); diff --git a/system/netstack/node/tests/resolve.rs b/system/netstack/node/tests/resolve.rs index 32030c0d82e..ebadb851de7 100644 --- a/system/netstack/node/tests/resolve.rs +++ b/system/netstack/node/tests/resolve.rs @@ -16,6 +16,7 @@ use std::time::Duration; use common::{arp, from_server, message, message_of, option, sum, terms, xid, A, ACK, BROADCAST, DISCOVER, DNS, MAC, MAC_B, MAC_R, NAK, OFFER, R, REQUEST}; use lan::{udp, Lan, Seen, Udp, B}; use toyos_dns::{Failure, Name, MAX_LOOKUPS, ROUNDS, WAIT_MS}; +use toyos_net_node::HolderId; use toyos_net_node::{Counter, Ended, LookupId, NotStarted, Refused, Resolved, Resolving}; use toyos_net_udp::limits::{EPHEMERAL_COUNT, EPHEMERAL_FIRST, TX_DATAGRAMS}; use toyos_net_udp::Counter as Rule; @@ -298,9 +299,9 @@ impl Net { /// Whether a socket holds `port`, by binding it: a port is one socket's. fn port_held(&mut self, port: u16) -> bool { - match self.lan.node.udp_bind(ANY, Port::new(port), undrawn) { + match self.lan.node.udp_bind(ANY, Port::new(port), common::unread(), undrawn) { Ok((id, _)) => { - self.lan.node.udp_close(self.lan.now, id).unwrap(); + self.lan.node.datagram_departed(self.lan.now, id, HolderId::FIRST); false } Err(Refused::AddrInUse) => true, @@ -839,11 +840,11 @@ fn a_query_with_no_port_to_leave_from_ends_its_lookup_by_name() { let mut net = Net::leased(&[ANSWERS], Some(R)); let last = u32::from(EPHEMERAL_COUNT) - 1; // A client's socket holds a place; a query's holds none. - net.lan.node.set_places(net.lan.now, usize::from(EPHEMERAL_COUNT) - 1); + net.lan.node.set_places(usize::from(EPHEMERAL_COUNT) - 1); for offset in 0..last { - net.lan.node.udp_bind(ANY, None, || offset).unwrap(); + net.lan.node.udp_bind(ANY, None, common::unread(), || offset).unwrap(); } - assert_eq!(net.lan.node.udp_bind(ANY, None, || last), Err(toyos_net_node::Refused::ResourceExhausted), "every place is a client's socket"); + assert_eq!(net.lan.node.udp_bind(ANY, None, common::unread(), || last), Err(toyos_net_node::Refused::ResourceExhausted), "every place is a client's socket"); let id = asking(net.lan.node.resolve(net.lan.now, ONE, name("www.example"), sequence([0x8001, last])).expect("the one port left, and a query needs no place")); assert_eq!(net.lan.node.resolve(net.lan.now, ONE, name("other.example"), sequence([0x8002, 0])), Err(NotStarted::ResourceExhausted)); assert_eq!(net.run(id, 10 * WAIT_MS), Some(Err(Ended::NoPort))); @@ -892,7 +893,7 @@ fn a_clients_datagrams_behind_the_burst_wait_in_their_own_socket_and_take_no_que for asked in &names { net.resolve(asked).unwrap(); } - let (client, _) = net.lan.node.udp_bind(ANY, None, || 0x3000).unwrap(); + let (client, _) = net.lan.node.udp_bind(ANY, None, common::unread(), || 0x3000).unwrap(); let behind: Vec> = (0..TX_DATAGRAMS).map(|n| format!("behind the burst {n}").into_bytes()).collect(); for datagram in &behind { net.lan.node.udp_send_to(net.lan.now, client, ANSWERS, 9, datagram).unwrap(); diff --git a/system/netstack/node/tests/streams.rs b/system/netstack/node/tests/streams.rs index 12fc6c0bb3c..5a6d2c1d97b 100644 --- a/system/netstack/node/tests/streams.rs +++ b/system/netstack/node/tests/streams.rs @@ -1053,7 +1053,7 @@ fn second() -> Holder { #[test] fn a_holder_past_the_first_takes_a_place_and_none_is_made_past_the_places() { let (mut net, id, _client) = established(); - net.node.set_places(net.now, 2); + net.node.set_places(2); assert_eq!(net.node.held(), 1); let (remote, _) = net.node.export(id).expect("a place for the second holder"); assert_eq!(remote, peer(), "the holder is told the stream's peer"); diff --git a/system/netstack/shard/src/lib.rs b/system/netstack/shard/src/lib.rs index 23e82fcb1e5..5bd3a8fa89a 100644 --- a/system/netstack/shard/src/lib.rs +++ b/system/netstack/shard/src/lib.rs @@ -623,6 +623,16 @@ impl Shard { self.udp.recv(id, out) } + /// [`Udp::waiting`](toyos_net_udp::Udp::waiting). + pub fn udp_waiting(&mut self, id: SocketId) -> Result { + self.udp.waiting(id) + } + + /// [`Udp::broadcast`](toyos_net_udp::Udp::broadcast). + pub fn udp_broadcast(&mut self, id: SocketId) -> Result { + self.udp.broadcast(id) + } + /// The port a socket holds. pub fn udp_port(&mut self, id: SocketId) -> Result { self.udp.binding(id).map(|(_, port)| port) diff --git a/system/netstack/src/client.rs b/system/netstack/src/client.rs index 9d6f87cd4f3..6e8b9ddfa39 100644 --- a/system/netstack/src/client.rs +++ b/system/netstack/src/client.rs @@ -1,7 +1,7 @@ use std::time::{Duration, Instant}; use toyos::AsHandle; -use toyos::ipc::{self, Connection, IpcPayload}; +use toyos::ipc::{self, Connection}; use toyos::net::{ErrorResponse, MAX_REQUEST, RespType}; use toyos::say; @@ -21,18 +21,10 @@ pub struct Client { } impl Client { - pub fn result(&self, payload: &T) { - self.answered(self.conn.try_send(RESP_RESULT, payload)); - } - pub fn result_bytes(&self, data: &[u8]) { self.answered(self.conn.try_send_bytes(RESP_RESULT, data)); } - pub fn done(&self) { - self.answered(self.conn.try_signal(RESP_RESULT)); - } - pub fn error(&self, code: u32) { self.answered(self.conn.try_send(RESP_ERROR, &ErrorResponse { code })); } diff --git a/system/netstack/src/main.rs b/system/netstack/src/main.rs index 8538ac131a3..66caebde191 100644 --- a/system/netstack/src/main.rs +++ b/system/netstack/src/main.rs @@ -6,9 +6,9 @@ //! wire goes to the node as the card handed it over, and every decision about //! an address, a socket or a segment is the node's. What is here is what the //! node cannot do and stay pure: the card (`card`), the clock, the kernel's -//! random source, the regions clients lend (`lent`), the kernel's pipes -//! (`pipes`), the clients' connections (`client`) and their requests -//! (`serve`). +//! random source, the regions clients lend (`lent`), the bells a socket's +//! holders are rung on (`pipes`), the clients' connections (`client`) and +//! their requests (`serve`). //! //! **One pass**: the card's link and its received frames, as one batch, go to the node, then //! every deadline that is due, then the node is offered the card's transmit @@ -22,7 +22,7 @@ //! the node builds none without it, and a card with none wakes the pass that //! has (`Card::wake_on_room`). //! -//! **Every draw is the kernel's**, the stack's secrets and each id, port and +//! **Every draw is the kernel's**, the stack's secrets and each port and //! transaction id after them, and a kernel that refuses one ends netstack by //! name: a value anyone can predict is a forged answer's way in. @@ -104,17 +104,22 @@ const PIPE_BYTES: u64 = 2 * 1024 * 1024; /// (`issues/a-pipe-is-a-two-mebibyte-page-for-a-mapping-nothing-makes.md`). const HOLDER_BYTES: u64 = 4 * PIPE_BYTES; -/// What one place can make this machine hold, at the largest of the four -/// things a place is (`toyos-net-node`'s `places`): a stream, its region and -/// its first holder; a holder past a stream's first; a listener, whose peers -/// fill its queue of `LISTEN_READY` finished connections with an initial -/// receive buffer of text each, nobody reading them to grow one, and its wake -/// pipe; a datagram socket, its two pipes and two queues. +/// What one holder of a listener or a datagram socket holds: the two pipes of +/// its connection and its bell. +const SOCKET_HOLDER_BYTES: u64 = 3 * PIPE_BYTES; + +/// What one place can make this machine hold, at the largest of the things a +/// place is (`toyos-net-node`'s `places`): a stream, its region and its first +/// holder; a holder past a stream's first; a listener, whose peers fill its +/// queue of `LISTEN_READY` finished connections with an initial receive buffer +/// of text each, nobody reading them to grow one, and its first holder; a +/// datagram socket, its two queues and its first holder; a holder past a +/// socket's first. const PLACE_BYTES: u64 = { let stream = toyos::net::stream::REGION_BYTES as u64 + HOLDER_BYTES; - let listener = toyos_net_tcp::limits::LISTEN_READY as u64 * toyos_net_tcp::limits::RECEIVE_BUFFER_INITIAL as u64 + PIPE_BYTES; - let datagram = 2 * PIPE_BYTES + (toyos_net_udp::limits::RX_BYTES + toyos_net_udp::limits::TX_BYTES) as u64; - assert!(stream >= listener && stream >= datagram && stream >= HOLDER_BYTES); + let listener = toyos_net_tcp::limits::LISTEN_READY as u64 * toyos_net_tcp::limits::RECEIVE_BUFFER_INITIAL as u64 + SOCKET_HOLDER_BYTES; + let datagram = SOCKET_HOLDER_BYTES + (toyos_net_udp::limits::RX_BYTES + toyos_net_udp::limits::TX_BYTES) as u64; + assert!(stream >= listener && stream >= datagram && stream >= HOLDER_BYTES && stream >= SOCKET_HOLDER_BYTES); stream }; @@ -131,9 +136,9 @@ const PLACE_BUDGET_SHARE: u64 = 8; const FIXED_WATCHES: u32 = 2; /// The most places one poller can watch: each place is one watched handle, a -/// stream holder's connection, a listener's wake pipe or a datagram socket's -/// receive pipe, asked about in the same batch as the fixed watches, the connections -/// that have not said what they want and the lookups' clients. +/// holder's connection of a stream, a listener or a datagram socket, asked +/// about in the same batch as the fixed watches, the connections that have not +/// said what they want and the lookups' clients. const MAX_PLACES: u32 = Poller::MAX_HANDLES - FIXED_WATCHES - MAX_PENDING_CONNS - serve::LOOKUP_WATCHES; /// How many places the node is given, of `total_mem` bytes of physical @@ -355,8 +360,8 @@ fn main() { let total_mem = total_memory(); let places = places_for(total_mem); - node.set_places(clock(), places); - let mut sockets = serve::Sockets::new(draw(), places); + node.set_places(places); + let mut sockets = serve::Sockets::new(places); let mut leases = Leases { began, said: None, settled: false }; // The link as the card came up with it, which the first change a pass @@ -411,7 +416,7 @@ fn main() { } card.report(); - sockets.settle(&mut node, now); + sockets.settle(&mut node); // Before anything is served: the lease is what gives this machine an // address, a route and its resolvers. if leases.pass(node.lease()) { diff --git a/system/netstack/src/pipes.rs b/system/netstack/src/pipes.rs index 187017385d9..59ff6f9d212 100644 --- a/system/netstack/src/pipes.rs +++ b/system/netstack/src/pipes.rs @@ -1,57 +1,23 @@ -//! The kernel's pipe end a listener's owner reads its wakes from, as the -//! node's [`Wake`]: one non-blocking write, and the kernel's refusal in the -//! node's word for it. +//! A holder's bell of a listener or a datagram socket, as the node's [`Bell`]: netstack's write +//! end of a pipe it made, whose read end only that holder is handed. //! -//! **The node owns the end and netstack only watches it.** [`hold`] splits a -//! pipe end into the [`Held`] the node is handed and the [`Watched`] netstack -//! keeps: the handle closes when the node drops its half, which is how an -//! owner reads the end of its listener, and from then the watched half names -//! nothing. -//! -//! **Untrusted input.** The end is whatever handle its owner moved, and -//! nothing checks its kind at intake: one that is no pipe end, or the wrong -//! end, answers a refusal here that is neither a full pipe nor a vanished -//! reader, and the node ends that listener for it. - -use std::rc::{Rc, Weak}; +//! **A ring is one non-blocking write that is never retried.** A full pipe is rung already, and +//! one whose reader is gone has a holder whose connection's hang-up reports its leaving. use toyos::Pipe; use toyos_abi::syscall::SyscallError; -use toyos_net_node::{Wake, WriteRefusal}; - -/// The half the node holds: the handle lives as long as this does. -pub struct Held(Rc); +use toyos_net_node::Bell; -/// The half netstack watches by. -pub struct Watched(Weak); - -pub fn hold(pipe: Pipe) -> (Held, Watched) { - let pipe = Rc::new(pipe); - let watched = Watched(Rc::downgrade(&pipe)); - (Held(pipe), watched) -} +pub struct Rung(Pipe); -impl Watched { - /// The end, while the node holds it. - pub fn held(&self) -> Option> { - self.0.upgrade() +impl Bell for Rung { + fn ring(&mut self) { + let _ = self.0.write_nonblock(&[1]); } } -fn write_refused(why: SyscallError) -> WriteRefusal { - match why { - SyscallError::WouldBlock => WriteRefusal::Full, - SyscallError::Gone => WriteRefusal::Gone, - _ => WriteRefusal::Broken, - } -} - -impl Wake for Held { - fn wake(&mut self) -> Result<(), WriteRefusal> { - match self.0.write_nonblock(&[1]) { - Ok(1) => Ok(()), - Ok(_) => Err(WriteRefusal::Full), - Err(why) => Err(write_refused(why)), - } - } +/// A new bell: the read end to hand its holder, and the end the node rings. +pub fn bell() -> Result<(Pipe, Rung), SyscallError> { + let (read, write) = toyos::pipe_pair()?; + Ok((read, Rung(write))) } diff --git a/system/netstack/src/serve.rs b/system/netstack/src/serve.rs index d7494e987ac..1f31824bc7d 100644 --- a/system/netstack/src/serve.rs +++ b/system/netstack/src/serve.rs @@ -2,11 +2,20 @@ //! onto `toyos::net`'s protocol. //! //! **One request is one call of the node's**, and every decision about a -//! socket is the node's: what is here is the number a client names a listener -//! or a datagram socket by, the clients that wait for an answer, the handles -//! netstack watches, and the word each of the node's refusals is written in. A -//! datagram's bytes travel in the socket's own pipes, which are netstack's and -//! not the node's, and reach no call. +//! socket is the node's: what is here is which socket and holder each +//! connection is, the clients that wait for an answer, the handles netstack +//! watches, and the word each of the node's refusals is written in. +//! +//! **A listener and a datagram socket are their holders** (`toyos::net::socket`): +//! the connection its listen or bind came on, which is handed its bell with the +//! answer, and one more for each export. netstack watches each holder's +//! connection for its frames, every one a request on that holder's own socket +//! answered at once, and its hang-up, which is that holder's leaving and, for +//! the last, the socket's close. **No frame names a socket**, so a connection +//! reaches only what it holds: a holder's frame on a new connection is a +//! message type that does not exist there, and one a holder's protocol does +//! not have drops that holder. An accept's stream gets a connection of the +//! private port's, handed over in the accept's answer. //! //! **A stream is its region and its holders** (`toyos::net::stream`): the //! connection its connect or accept came on, and one more for each export. @@ -18,14 +27,12 @@ //! queued on it before the asker is answered. A client names no stream by a //! number, and a frame no holder of the protocol sends drops that holder. //! -//! **Untrusted input.** A request's every field is its client's number: an id -//! that names nothing, or a socket of another kind, is refused -//! `ERR_NOT_CONNECTED`, a payload that is not the request's struct, a port of -//! zero and a name that is none `ERR_INVALID_INPUT`, and a length is a bound -//! on a read and never an allocation's size past what one frame carries. A -//! request that waits holds its client's connection, so what waits is -//! bounded: one receive a socket, a second refused `ERR_RESOURCE_EXHAUSTED`. -//! Nothing a client sends panics netstack. +//! **Untrusted input.** A request's every field is its client's number: a +//! payload that is not the request's struct, an option no socket of its kind +//! has, a port of zero and a name that is none are refused `ERR_INVALID_INPUT`, +//! and a length is a bound on a read and never an allocation's size past what +//! one frame carries. Only a connect and a lookup wait, each holding its own +//! connection. Nothing a client sends panics netstack. //! //! **A lookup's view is the badge on its connection**, which the supervisor //! minted with the row and session of the program it endowed the connector, @@ -33,11 +40,6 @@ //! no other. A lookup on a connection with no badge is refused //! `ERR_PERMISSION_DENIED`. Neither the name a lookup asks nor anything kept is //! said in the log or to `inspect`, which every holder of `netstack` reads. -//! -//! **An id is a number any client can name** -//! (`issues/netstack-socket-ids-are-ambient.md`): the table starts at a -//! random one so that an id a client kept across a replaced netstack names -//! nothing in the next. use std::collections::{BTreeMap, HashMap}; use std::net::Ipv4Addr; @@ -46,17 +48,18 @@ use std::time::Duration; use std::rc::{Rc, Weak}; use toyos::net::*; +use toyos::net::socket::{self, Kind, ACCEPT, OPTION, RECEIVE, SEND, SET_OPTION}; use toyos::net::stream::{self, Adopt, ADOPT, BELLS, EXPORT, EXPORTED, REFUSED, RING}; use toyos::transport::session::Holder; -use toyos::poller::{OTHER_END_GONE, READABLE}; +use toyos::poller::READABLE; use toyos::port::Acceptor; use toyos::ipc::RxStep; use toyos::{ipc, say, AsHandle, Connection, OwnedHandle, Pipe}; use toyos_abi::syscall::{SyscallError, MAX_BADGE}; use toyos_inspect::Snapshot; use toyos_net_node::{ - AcceptRefused, ConnectRefused, DatagramId, Ended, Event, ExportRefused, ListenRefused, ListenerId, LookupId, Node, - NotStarted, Refused, Resolving, StreamEvent, StreamId, WriteRefusal, + AcceptRefused, ConnectRefused, DatagramId, Ended, Event, ExportRefused, HolderId, ListenRefused, ListenerId, LookupId, + Node, NotStarted, Refused, Resolving, StreamEvent, StreamId, }; use toyos_net_shard::ConnectError; use toyos_net_tcp::{Endpoint, Failure}; @@ -64,16 +67,15 @@ use toyos_net_wire::{Instant, Port}; use crate::client::{Client, Request}; use crate::lent::{Holders, Holding, Region}; -use crate::pipes::{self, Watched}; +use crate::pipes; use crate::watches::Watches; use crate::HOSTNAME; -/// A watch's token names the socket it was asked of by its id, in the low -/// word, and never a place in a list: an answer can arrive after its socket +/// A watch's token names the holder it was asked of by its key, in the low +/// word, and never a place in a list: an answer can arrive after its holder /// is gone, and must then name nothing. const TOKEN_STREAM: u64 = 1 << 32; -const TOKEN_LISTENER: u64 = 3 << 32; -const TOKEN_DATAGRAM: u64 = 4 << 32; +const TOKEN_SOCKET: u64 = 3 << 32; /// A client that waits for its lookup's answer, by its connection's handle. /// Clear of the loop's own tokens and of a pending connection's. const TOKEN_LOOKUP: u64 = 5 << 32; @@ -87,18 +89,23 @@ pub const LOOKUP_WATCHES: u32 = toyos_dns::MAX_LOOKUPS as u32; /// bytes an address. const MAX_ANSWERED: usize = (256 - 1) / 5; -/// What a client's id names. -enum Socket { - Listener { id: ListenerId, wakes: Watched }, - Datagram(Datagram), +/// The socket a holder holds. +#[derive(Clone, Copy, Debug)] +enum Held { + Listener(ListenerId), + Datagram(DatagramId), } -/// A datagram socket's two pipes, which carry its payloads between its client -/// and netstack: the node holds a datagram only between two calls. -struct Datagram { - id: DatagramId, - to_client: Pipe, - from_client: Pipe, +/// One holder of a listener or a datagram socket, as netstack watches it. +struct SocketHolder { + socket: Held, + holder: HolderId, + /// The address the socket's listen or bind named, for each holder's + /// adoption. + addr: [u8; 4], + /// What it asks on, and whose hang-up is its leaving. + connection: Connection, + frames: ipc::FrameRx<{ socket::MAX_FRAME }>, } /// A stream's holders as netstack watches them, for as long as the node holds @@ -122,10 +129,10 @@ const FRAMES_A_WAKE: usize = 64; /// namespace. const EXPORT_PORT: &str = "export"; -/// The private port every holder netstack makes past a stream's first is a -/// connection of: its acceptor, and the namespace holding netstack's one -/// connector to it. It dies with netstack, so nothing pending reaches a -/// restarted one. +/// The private port every holder netstack makes is a connection of, but the +/// first of a connect, a listen and a bind, which is the request's own: its +/// acceptor, and the namespace holding netstack's one connector to it. It +/// dies with netstack, so nothing pending reaches a restarted one. struct Exports { acceptor: Acceptor, names: toyos::namespace::Namespace, @@ -150,14 +157,10 @@ impl Exports { } } -/// A client waiting for a datagram to reach its socket. -struct Receiving { - client: Client, - max_len: u32, -} - pub struct Sockets { - ids: HashMap, + /// Each holder of a listener or a datagram socket, by the key its watch's + /// token carries. + sockets: HashMap, next_id: u32, /// Each stream by its key. streams: HashMap, @@ -168,9 +171,6 @@ pub struct Sockets { holders: HashMap, exports: Exports, lookups: Vec<(LookupId, Client)>, - /// At most one receive waits on a socket, so a client's requests alone - /// hold no more of netstack's handles than its sockets do. - receiving: BTreeMap, /// The places the node was given. places: usize, /// A stream's connection was answered in this wake: one pass over the @@ -191,20 +191,20 @@ fn bells() -> Result<([Pipe; BELLS], [Pipe; BELLS]), SyscallError> { /// What a lent region makes: the session, the bells' read ends and the holders. type Lent = (Region, [Pipe; BELLS], Rc); -/// A stream's region off a request's connection, judged, and its first -/// holder's bells made: the session the node is lent, the bells' read ends the -/// answer hands over, and the holders. Refused by name, nothing is mapped. -fn lend(client: Client, handles: ipc::Batch) -> Result { - let Some([region]) = handles.exact::<1>() else { return Err((client, ERR_INVALID_INPUT)) }; +/// A stream's region, judged, and its first holder's bells made, the holder's +/// connection being `connection`: the session the node is lent, the bells' read +/// ends the answer hands over, and the holders. Refused by name, nothing is +/// mapped, and the connection is handed back with the refusal's word. +fn lend(connection: Connection, region: toyos::RawHandle) -> Result { let mapping = match Region::judged(region) { Ok(mapping) => mapping, Err(why) => { say!("netstack: refusing a stream's region — {why}"); - return Err((client, ERR_INVALID_INPUT)); + return Err((connection, ERR_INVALID_INPUT)); } }; - let Ok((theirs, [receive, send])) = bells() else { return Err((client, ERR_RESOURCE_EXHAUSTED)) }; - let first = Holding { connection: client.conn, frames: ipc::FrameRx::new(), receive, send }; + let Ok((theirs, [receive, send])) = bells() else { return Err((connection, ERR_RESOURCE_EXHAUSTED)) }; + let first = Holding { connection, frames: ipc::FrameRx::new(), receive, send }; match Region::new(mapping, first) { Ok((session, holders)) => Ok((session, theirs, holders)), Err(why) => panic!("netstack: the kernel would not duplicate a region it mapped: {why:?}"), @@ -278,12 +278,20 @@ fn listen_refused(refusal: ListenRefused) -> u32 { fn accept_refused(refusal: AcceptRefused) -> u32 { match refusal { - AcceptRefused::NoListener | AcceptRefused::Nothing => ERR_NOT_CONNECTED, + AcceptRefused::NoListener => ERR_NOT_CONNECTED, + AcceptRefused::Nothing => ERR_WOULD_BLOCK, AcceptRefused::NoRegion => ERR_INVALID_INPUT, AcceptRefused::Full => ERR_RESOURCE_EXHAUSTED, } } +fn export_refused(refusal: ExportRefused) -> u32 { + match refusal { + ExportRefused::Full => ERR_RESOURCE_EXHAUSTED, + ExportRefused::Gone => ERR_NOT_CONNECTED, + } +} + /// A lookup's answer: a count, then each address behind the family tag 4. A /// resolver may answer with a subset of a name's addresses, and these are the /// ones the server put first. @@ -321,23 +329,22 @@ fn answer_resolved(client: &Client, result: Result, Ended>) { } impl Sockets { - pub fn new(first_id: u32, places: usize) -> Self { + pub fn new(places: usize) -> Self { Self { - ids: HashMap::new(), - next_id: first_id.max(1), + sockets: HashMap::new(), + next_id: 1, streams: HashMap::new(), by_stream: BTreeMap::new(), holders: HashMap::new(), exports: Exports::new(), lookups: Vec::new(), - receiving: BTreeMap::new(), places, bridge: false, rung: Vec::new(), } } - /// An id no socket and no stream is kept under, and never 0. + /// A key no holder and no stream is kept under, and never 0. fn alloc_id(&mut self) -> u32 { loop { let id = self.next_id; @@ -345,7 +352,7 @@ impl Sockets { 0 => 1, next => next, }; - if !self.ids.contains_key(&id) && !self.streams.contains_key(&id) && !self.holders.contains_key(&id) { + if !self.sockets.contains_key(&id) && !self.streams.contains_key(&id) && !self.holders.contains_key(&id) { return id; } } @@ -360,36 +367,22 @@ impl Sockets { self.holders.insert(watch, (key, Holder::FIRST)); } - fn listener(&self, socket_id: u32) -> Option { - match self.ids.get(&socket_id) { - Some(Socket::Listener { id, .. }) => Some(*id), - Some(Socket::Datagram(_)) | None => None, - } - } - - fn datagram(&self, socket_id: u32) -> Option<&Datagram> { - match self.ids.get(&socket_id) { - Some(Socket::Datagram(socket)) => Some(socket), - Some(Socket::Listener { .. }) | None => None, - } + /// A holder of `socket` on `connection`, watched from here. + fn hold_socket(&mut self, socket: Held, holder: HolderId, addr: [u8; 4], connection: Connection) { + let key = self.alloc_id(); + self.sockets.insert(key, SocketHolder { socket, holder, addr, connection, frames: ipc::FrameRx::new() }); } - /// One whole request. A call that answers at once drops its client where - /// it answers; a connect, a lookup and a receive with nothing to hand - /// over keep theirs until the node has the answer ([`Self::settle`]). + /// One whole request on a new connection. A call that answers at once + /// drops its client where it answers; a connect and a lookup keep theirs + /// until the node has the answer ([`Self::settle`]); a listen and a bind + /// make theirs the socket's first holder. pub fn request(&mut self, node: &mut Node, now: Instant, req: Request, acceptor: &Acceptor, draw: fn() -> u32) { match MsgType::from_u32(req.msg_type) { - Some(MsgType::TcpClose) => self.close(node, now, &req), - Some(MsgType::UdpBind) => self.udp_bind(node, req, draw), - Some(MsgType::UdpSendTo) => self.udp_send_to(node, now, &req), - Some(MsgType::UdpRecvFrom) => self.udp_recv_from(node, now, req), - Some(MsgType::UdpClose) => self.udp_close(node, now, &req), + Some(MsgType::UdpBind) => self.udp_bind(node, now, req, draw), Some(MsgType::DnsLookup) => self.lookup(node, now, req, acceptor, draw), - Some(MsgType::TcpListenerSetOption) => self.listener_set_option(node, &req), - Some(MsgType::UdpSetOption) => self.udp_set_option(node, &req), Some(MsgType::TcpConnect) => self.connect(node, now, req), - Some(MsgType::TcpBindPiped) => self.listen(node, req, draw), - Some(MsgType::TcpAccept) => self.accept(node, now, req), + Some(MsgType::TcpListen) => self.listen(node, now, req, draw), None => { say!("netstack: unknown message type {}", req.msg_type); req.client.error(ERR_INVALID_INPUT); @@ -397,57 +390,6 @@ impl Sockets { } } - /// The client lets go of the listener or datagram socket its id names; an - /// id that names nothing is let go already. - fn close(&mut self, node: &mut Node, now: Instant, msg: &Request) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - match self.ids.remove(&req.socket_id) { - Some(Socket::Listener { id, .. }) => { - node.close_listener(now, id); - } - Some(Socket::Datagram(socket)) => self.end_datagram(node, now, socket), - None => {} - } - msg.client.done(); - } - - fn listener_set_option(&mut self, node: &mut Node, msg: &Request) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - let Some(id) = self.listener(req.socket_id) else { - msg.client.error(ERR_NOT_CONNECTED); - return; - }; - match req.option { - OPT_NODELAY if node.set_listener_nodelay(id, req.value != 0) => msg.client.done(), - OPT_NODELAY => msg.client.error(ERR_NOT_CONNECTED), - _ => msg.client.error(ERR_INVALID_INPUT), - } - } - - fn udp_set_option(&mut self, node: &mut Node, msg: &Request) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - let Some(socket) = self.datagram(req.socket_id) else { - msg.client.error(ERR_NOT_CONNECTED); - return; - }; - match req.option { - OPT_BROADCAST => match node.udp_set_broadcast(socket.id, req.value != 0) { - Ok(()) => msg.client.done(), - Err(refusal) => msg.client.error(refused(refusal)), - }, - _ => msg.client.error(ERR_INVALID_INPUT), - } - } - /// A stream's connect: its region lent, its connection the stream's from /// here, and its answer once the node has one ([`Self::settle`]). fn connect(&mut self, node: &mut Node, now: Instant, msg: Request) { @@ -459,9 +401,10 @@ impl Sockets { msg.client.error(ERR_INVALID_INPUT); return; }; - let (session, bells, holders) = match lend(msg.client, msg.handles) { + let Some([region]) = msg.handles.exact::<1>() else { return msg.client.error(ERR_INVALID_INPUT) }; + let (session, bells, holders) = match lend(msg.client.conn, region) { Ok(lent) => lent, - Err((client, code)) => return client.error(code), + Err((conn, code)) => return Client { conn }.error(code), }; let remote = Endpoint { addr: Ipv4Addr::from(req.addr), port }; let timeout = (req.timeout_ms > 0).then(|| Duration::from_millis(u64::from(req.timeout_ms))); @@ -476,201 +419,42 @@ impl Sockets { } } - fn listen(&mut self, node: &mut Node, msg: Request, draw: fn() -> u32) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - let Some([wakes]) = msg.handles.exact::<{ NOTIFY_HANDLES }>() else { + /// A listen: its connection the listener's first holder, answered with its + /// bell. + fn listen(&mut self, node: &mut Node, now: Instant, msg: Request, draw: fn() -> u32) { + let Ok(req) = ipc::decode_payload::(msg.payload()) else { msg.client.error(ERR_INVALID_INPUT); return; }; - // SAFETY: the kernel moved the handle into this process with the frame - // just read, and nothing else answers for it. - let (held, wakes) = pipes::hold(unsafe { Pipe::from_raw(wakes) }); - match node.listen(Ipv4Addr::from(req.addr), Port::new(req.port), req.options.nodelay(), Box::new(held), draw) { - Ok((listener, port)) => { - let socket_id = self.alloc_id(); - self.ids.insert(socket_id, Socket::Listener { id: listener, wakes }); - msg.client.result(&TcpBindResponse { socket_id, bound_port: port.get(), _pad: 0 }); - } + let Ok((theirs, bell)) = pipes::bell() else { return msg.client.error(ERR_RESOURCE_EXHAUSTED) }; + match node.listen(Ipv4Addr::from(req.addr), Port::new(req.port), req.options.nodelay(), Box::new(bell), draw) { + Ok((id, port)) => self.bound(node, now, Held::Listener(id), req.addr, msg.client, port, theirs), Err(refusal) => msg.client.error(listen_refused(refusal)), } } - /// A stream's accept: its region lent, and the oldest connection waiting - /// at the listener the stream on it, answered at once. - fn accept(&mut self, node: &mut Node, now: Instant, msg: Request) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - let Some(listener) = self.listener(req.listener) else { - msg.client.error(accept_refused(AcceptRefused::NoListener)); - return; - }; - let (session, bells, holders) = match lend(msg.client, msg.handles) { - Ok(lent) => lent, - Err((client, code)) => { - // Refused, the accept still spends its wake. - let _ = node.accept(now, listener, None); - return client.error(code); - } - }; - match node.accept(now, listener, Some(Box::new(session))) { - Ok(accepted) => { - hand_over(&holders, bells, &TcpAcceptResponse { - remote_addr: accepted.remote.addr.octets(), - remote_port: accepted.remote.port.get(), - local_port: accepted.local.get(), - nodelay: u32::from(accepted.nodelay), - }); - self.hold_stream(accepted.id, &holders, None); - } - Err(refusal) => { - if refusal == AcceptRefused::Full { - say!("netstack: refusing accept, {} of {} places held", node.held(), self.places); - } - answer_refused(&holders, accept_refused(refusal)); - } - } - } - - fn udp_bind(&mut self, node: &mut Node, msg: Request, draw: fn() -> u32) { + /// A datagram socket's bind: its connection the socket's first holder, + /// answered with its bell. + fn udp_bind(&mut self, node: &mut Node, now: Instant, msg: Request, draw: fn() -> u32) { let Ok(req) = ipc::decode_payload::(msg.payload()) else { msg.client.error(ERR_INVALID_INPUT); return; }; - let Some([to_client, from_client]) = msg.handles.exact::<{ DATA_HANDLES }>() else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - // SAFETY: the kernel moved both handles into this process with the - // frame just read, and nothing else answers for either. - let (to_client, from_client) = unsafe { (Pipe::from_raw(to_client), Pipe::from_raw(from_client)) }; - match node.udp_bind(Ipv4Addr::from(req.addr), Port::new(req.port), draw) { - Ok((id, port)) => { - let socket_id = self.alloc_id(); - self.ids.insert(socket_id, Socket::Datagram(Datagram { id, to_client, from_client })); - msg.client.result(&UdpBindResponse { socket_id, bound_port: port.get(), _pad: 0 }); - } - Err(refusal) => msg.client.error(refused(refusal)), - } - } - - /// The client wrote the datagram into its socket's pipe and then sent this - /// request, so the bytes the request names are there: a pipe that holds - /// fewer is a client naming bytes it never wrote, and a read that would - /// wait for them would wait on that client. - fn udp_send_to(&mut self, node: &mut Node, now: Instant, msg: &Request) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - let Some(socket) = self.datagram(req.socket_id) else { - msg.client.error(ERR_NOT_CONNECTED); - return; - }; - let mut payload = vec![0u8; usize::from(req.len)]; - // A datagram of no bytes put none in the pipe. - let read = if payload.is_empty() { Ok(0) } else { socket.from_client.read_nonblock(&mut payload) }; - match read { - Ok(read) if read == payload.len() => {} - Ok(_) | Err(SyscallError::WouldBlock) => { - msg.client.error(ERR_INVALID_INPUT); - return; - } - Err(_) => { - msg.client.error(ERR_OTHER); - return; - } - } - match node.udp_send_to(now, socket.id, Ipv4Addr::from(req.addr), req.port, &payload) { - Ok(()) => msg.client.result(&u32::from(req.len)), + let Ok((theirs, bell)) = pipes::bell() else { return msg.client.error(ERR_RESOURCE_EXHAUSTED) }; + match node.udp_bind(Ipv4Addr::from(req.addr), Port::new(req.port), Box::new(bell), draw) { + Ok((id, port)) => self.bound(node, now, Held::Datagram(id), req.addr, msg.client, port, theirs), Err(refusal) => msg.client.error(refused(refusal)), } } - fn udp_recv_from(&mut self, node: &mut Node, now: Instant, msg: Request) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - // A waiting client that hung up holds the socket's one wait for nobody. - if let Some(waiting) = self.receiving.remove(&req.socket_id) { - if !waiting.client.gone() { - self.receiving.insert(req.socket_id, waiting); - msg.client.error(ERR_RESOURCE_EXHAUSTED); - return; - } - } - let waiting = Receiving { client: msg.client, max_len: req.max_len }; - if let Some(waiting) = self.deliver(node, now, req.socket_id, waiting) { - self.receiving.insert(req.socket_id, waiting); - } - } - - /// Hands `waiting` its socket's oldest datagram, or hands `waiting` back - /// when none has arrived. - /// - /// **A datagram goes into the client's pipe whole, or its socket ends.** - /// The answer names a length, and a write takes what the pipe has room - /// for and cannot be taken back: a client reading that length out of a - /// pipe holding part of this datagram would splice the next one onto it. - fn deliver(&mut self, node: &mut Node, now: Instant, socket_id: u32, waiting: Receiving) -> Option { - let Some(socket) = self.datagram(socket_id) else { - waiting.client.error(ERR_NOT_CONNECTED); - return None; - }; - // The client's number bounds what it is handed and never what is - // allocated: no datagram is longer than [udp] delivers. - let room = usize::try_from(waiting.max_len).unwrap_or(usize::MAX).min(toyos_net_udp::limits::MAX_PAYLOAD); - let mut payload = vec![0u8; room]; - let datagram = match node.udp_recv_from(socket.id, &mut payload) { - Ok(Some(datagram)) => datagram, - Ok(None) => return Some(waiting), - Err(refusal) => { - waiting.client.error(refused(refusal)); - return None; - } - }; - let wrote = socket.to_client.write_nonblock(&payload[..datagram.len]); - if wrote == Ok(datagram.len) { - waiting.client.result(&UdpRecvResponse { - addr: datagram.source.octets(), - port: datagram.source_port.map_or(0, Port::get), - len: datagram.len as u16, - }); - return None; - } - say!( - "netstack: ending UDP socket {socket_id} — its receive pipe answered {wrote:?} to a {}-byte datagram", - datagram.len - ); - if let Some(Socket::Datagram(socket)) = self.ids.remove(&socket_id) { - self.end_datagram(node, now, socket); - } - waiting.client.error(ERR_CONNECTION_RESET); - None - } - - fn end_datagram(&mut self, node: &mut Node, now: Instant, socket: Datagram) { - if let Err(refusal) = node.udp_close(now, socket.id) { - unreachable!("the node refused the close of a datagram socket its table held: {refusal:?}"); - } - } - - fn udp_close(&mut self, node: &mut Node, now: Instant, msg: &Request) { - let Ok(req) = ipc::decode_payload::(msg.payload()) else { - msg.client.error(ERR_INVALID_INPUT); - return; - }; - if self.datagram(req.socket_id).is_some() { - if let Some(Socket::Datagram(socket)) = self.ids.remove(&req.socket_id) { - self.end_datagram(node, now, socket); - } + /// A socket the node made, answered to its first holder with its bell. + fn bound(&mut self, node: &mut Node, now: Instant, socket: Held, addr: [u8; 4], client: Client, port: Port, bell: Pipe) { + let answer = BoundResponse { port: port.get(), _pad: 0 }; + if let Err(why) = client.conn.try_send_with(RespType::Result as u32, &answer, [OwnedHandle::from(bell)]) { + say!("netstack: a socket's client left before its answer: {why:?}"); + return departed(node, now, socket, HolderId::FIRST); } - msg.client.done(); + self.hold_socket(socket, HolderId::FIRST, addr, client.conn); } /// Starts resolving the name `msg` carries for the view its connection's @@ -709,13 +493,7 @@ impl Sockets { /// Everything the node has to say since the last call, to the log and to /// the clients that waited for it, and the table entries of what the node /// let go. - pub fn settle(&mut self, node: &mut Node, now: Instant) { - for (socket_id, waiting) in std::mem::take(&mut self.receiving) { - if let Some(waiting) = self.deliver(node, now, socket_id, waiting) { - self.receiving.insert(socket_id, waiting); - } - } - + pub fn settle(&mut self, node: &mut Node) { let events: Vec = node.drain_stream_events().collect(); for event in events { let (id, answer) = match event { @@ -745,21 +523,6 @@ impl Sockets { answer_resolved(&client, resolved.result); } - let ended: Vec<(ListenerId, WriteRefusal)> = node.drain_ended_listeners().collect(); - for (ended, refusal) in ended { - let found = self.ids.iter().find_map(|(socket_id, socket)| match socket { - Socket::Listener { id, .. } if *id == ended => Some(*socket_id), - Socket::Listener { .. } | Socket::Datagram(_) => None, - }); - // None where its owner's close arrived in the pass that ended it. - let Some(socket_id) = found else { continue }; - self.ids.remove(&socket_id); - // Its owner gone is the ordinary end of a listener. - if refusal != WriteRefusal::Gone { - say!("netstack: closing listener {socket_id} — its notify pipe refused a wake: {refusal:?}"); - } - } - let events: Vec = node.drain_events().collect(); for event in events { match event { @@ -792,10 +555,9 @@ impl Sockets { } /// Asks the kernel about everything a pass could be owed for: each - /// stream's connection, for its client's rings and its hang-up, the wake - /// pipe of each listener and one pipe of each datagram socket for its - /// owner's leaving, and the connection of each client that waits for a - /// lookup, which hangs up by closing it. + /// holder's connection, a stream's for its client's rings and a socket's + /// for its requests, and either's hang-up, and the connection of each + /// client that waits for a lookup, which hangs up by closing it. pub fn watch(&self, poller: &mut Watches) { for (key, (stream, number)) in &self.holders { let Some(holders) = self.streams.get(stream).and_then(|stream| stream.holders.upgrade()) else { continue }; @@ -803,15 +565,8 @@ impl Sockets { poller.watch(&holding.connection, stream::SERVER_WATCH, TOKEN_STREAM | u64::from(*key)); } } - for (socket_id, socket) in &self.ids { - match socket { - Socket::Listener { wakes, .. } => { - if let Some(pipe) = wakes.held() { - poller.watch(&*pipe, OTHER_END_GONE, TOKEN_LISTENER | u64::from(*socket_id)); - } - } - Socket::Datagram(socket) => poller.watch(&socket.to_client, OTHER_END_GONE, TOKEN_DATAGRAM | u64::from(*socket_id)), - } + for (key, holder) in &self.sockets { + poller.watch(&holder.connection, READABLE, TOKEN_SOCKET | u64::from(*key)); } for (_, client) in &self.lookups { poller.watch(&client.conn, READABLE, TOKEN_LOOKUP | u64::from(client.conn.as_handle().0)); @@ -825,27 +580,7 @@ impl Sockets { let socket_id = token as u32; match token & TOKEN_KIND { TOKEN_STREAM => self.holder_answered(node, now, socket_id, answer), - TOKEN_LISTENER => { - let gone = answer.map_or(true, |met| met & OTHER_END_GONE != 0); - if let (true, Some(id)) = (gone, self.listener(socket_id)) { - if let Err(why) = answer { - say!("netstack: closing listener {socket_id} — the kernel refused the watch of its notify pipe: {why:?}"); - } - self.ids.remove(&socket_id); - node.close_listener(now, id); - } - } - TOKEN_DATAGRAM => { - let gone = answer.map_or(true, |met| met & OTHER_END_GONE != 0); - if gone && self.datagram(socket_id).is_some() { - if let Err(why) = answer { - say!("netstack: ending UDP socket {socket_id} — the kernel refused the watch of its receive pipe: {why:?}"); - } - if let Some(Socket::Datagram(socket)) = self.ids.remove(&socket_id) { - self.end_datagram(node, now, socket); - } - } - } + TOKEN_SOCKET => self.socket_answered(node, now, socket_id, answer), TOKEN_LOOKUP => { // A lookup whose client has left ends now: its sockets and // its place are another client's. @@ -926,7 +661,7 @@ impl Sockets { say!("netstack: refusing an export, {} of {} places held", node.held(), self.places); return refuse(ERR_RESOURCE_EXHAUSTED); } - Err(ExportRefused::NoStream) => return refuse(ERR_NOT_CONNECTED), + Err(refusal @ ExportRefused::Gone) => return refuse(export_refused(refusal)), }; let made = (|| { let (theirs, ours) = self.exports.connect()?; @@ -964,6 +699,175 @@ impl Sockets { } } + /// What a socket holder's connection had, frame by frame: each request + /// answered, and its hang-up, a frame no holder sends or an answer it + /// would not take, its leaving. + fn socket_answered(&mut self, node: &mut Node, now: Instant, key: u32, answer: Result) { + let Some(holder) = self.sockets.get(&key) else { return }; + let (socket, number) = (holder.socket, holder.holder); + if let Err(why) = answer { + say!("netstack: dropping a socket's holder — the kernel refused the watch of its connection: {why:?}"); + return self.socket_left(node, now, key); + } + for _ in 0..FRAMES_A_WAKE { + let Some(holder) = self.sockets.get_mut(&key) else { return }; + let step = holder.frames.pump(&holder.connection); + let asked = match step { + RxStep::Idle => return, + RxStep::Frame { msg_type, payload_len, handles } => asked(kind_of(socket), msg_type, holder.frames.payload(payload_len), handles), + RxStep::Eof | RxStep::Malformed => None, + }; + let Some(asked) = asked else { + if !matches!(step, RxStep::Eof) { + say!("netstack: dropping a socket's holder — it sent {step:?}, which no holder sends"); + } + return self.socket_left(node, now, key); + }; + let answered = match asked { + Asked::Accept => { + let region = holder.frames.take_batch(); + self.accept(node, now, key, region) + } + Asked::Export => self.export_socket(node, now, key), + asked => { + let reply = answer_socket(node, now, socket, number, asked); + let Some(holder) = self.sockets.get(&key) else { return }; + let sent = match reply { + Ok(Reply::Bare) => holder.connection.try_signal(RespType::Result as u32), + Ok(Reply::Word(word)) => holder.connection.try_send(RespType::Result as u32, &word), + Ok(Reply::Bytes(bytes)) => holder.connection.try_send_bytes(RespType::Result as u32, &bytes), + Err(code) => holder.connection.try_send(RespType::Error as u32, &ErrorResponse { code }), + }; + sent.is_ok() + } + }; + if !answered { + say!("netstack: dropping a socket's holder — it would not take an answer"); + return self.socket_left(node, now, key); + } + } + } + + fn socket_left(&mut self, node: &mut Node, now: Instant, key: u32) { + if let Some(holder) = self.sockets.remove(&key) { + departed(node, now, holder.socket, holder.holder); + } + } + + /// A listener holder's accept: the oldest connection waiting at its + /// listener the stream on `region`, its first holder a connection of the + /// private port's, handed to the asker with the stream's bells. Whether the + /// asker took its answer. + fn accept(&mut self, node: &mut Node, now: Instant, key: u32, region: ipc::Batch) -> bool { + let Some(&SocketHolder { socket: Held::Listener(id), holder, .. }) = self.sockets.get(&key) else { + unreachable!("an accept is a listener holder's") + }; + // Closed unread where the node asks for no session. + let mut region = Some(region); + let mut made = None; + let mut code = ERR_INVALID_INPUT; + let exports = &self.exports; + let accepted = node.accept(now, id, holder, || { + let [region] = region.take()?.exact::<1>()?; + let (theirs, ours) = match exports.connect() { + Ok(pair) => pair, + Err(_) => { + toyos_abi::syscall::close(region); + code = ERR_RESOURCE_EXHAUSTED; + return None; + } + }; + match lend(ours, region) { + Ok((session, bells, holders)) => { + made = Some((theirs, bells, holders)); + Some(Box::new(session) as Box) + } + Err((_, refused)) => { + code = refused; + None + } + } + }); + drop(region); + let (answer, handles) = match (accepted, made) { + (Ok(accepted), Some((theirs, [receive, send], holders))) => { + let answer = TcpAcceptResponse { + remote_addr: accepted.remote.addr.octets(), + remote_port: accepted.remote.port.get(), + local_port: accepted.local.get(), + nodelay: u32::from(accepted.nodelay), + }; + self.hold_stream(accepted.id, &holders, None); + (Ok(answer), Some([theirs.into_owned(), OwnedHandle::from(receive), OwnedHandle::from(send)])) + } + (Ok(_), None) => unreachable!("the node took a stream it was given no session for"), + (Err(AcceptRefused::NoRegion), _) => (Err(code), None), + (Err(refusal), _) => { + if refusal == AcceptRefused::Full { + say!("netstack: refusing accept, {} of {} places held", node.held(), self.places); + } + (Err(accept_refused(refusal)), None) + } + }; + let Some(asker) = self.sockets.get(&key) else { return true }; + match (answer, handles) { + (Ok(answer), Some(handles)) => asker.connection.try_send_with(RespType::Result as u32, &answer, handles).is_ok(), + (Err(code), _) => asker.connection.try_send(RespType::Error as u32, &ErrorResponse { code }).is_ok(), + (Ok(_), None) => unreachable!("an accepted stream's answer carries its handles"), + } + } + + /// A holder more of the socket the holder `key` names holds, answered + /// [`EXPORTED`] with the new holder's end, its [`socket::ADOPT`] queued on + /// it first, or [`REFUSED`]. Whether the asker took its answer. + fn export_socket(&mut self, node: &mut Node, now: Instant, key: u32) -> bool { + let Some(&SocketHolder { socket, addr, .. }) = self.sockets.get(&key) else { return true }; + let made = (|| { + let (theirs, ours) = self.exports.connect()?; + let (handed, bell) = pipes::bell()?; + Ok::<_, SyscallError>((theirs, ours, handed, bell)) + })(); + let reply = match made { + Err(why) => { + say!("netstack: refusing an export — the kernel made no connection or bell for it: {why:?}"); + Err(ERR_RESOURCE_EXHAUSTED) + } + Ok((theirs, ours, handed, bell)) => { + let exported = match socket { + Held::Listener(id) => node.export_listener(id, Box::new(bell)), + Held::Datagram(id) => node.export_datagram(id, Box::new(bell)), + }; + match exported { + Err(refusal) => { + if refusal == ExportRefused::Full { + say!("netstack: refusing an export, {} of {} places held", node.held(), self.places); + } + Err(export_refused(refusal)) + } + Ok((holder, port)) => { + let adopt = socket::Adopt { kind: kind_of(socket) as u32, addr, port: port.get(), _pad: 0 }; + match ours.try_send_with(socket::ADOPT, &adopt, [OwnedHandle::from(handed)]) { + Ok(()) => { + self.hold_socket(socket, holder, addr, ours); + Ok(theirs) + } + Err(why) => { + say!("netstack: refusing an export — its ADOPT would not queue: {why:?}"); + departed(node, now, socket, holder); + Err(ERR_RESOURCE_EXHAUSTED) + } + } + } + } + } + }; + let Some(asker) = self.sockets.get(&key) else { return true }; + match reply { + Ok(theirs) => asker.connection.try_signal_with(EXPORTED, [theirs.into_owned()]).is_ok(), + Err(code) => asker.connection.try_send(REFUSED, &code).is_ok(), + } + } + /// The pass over the streams the last wake's answers asked for, once. pub fn bridge(&mut self, node: &mut Node, now: Instant) { if std::mem::take(&mut self.bridge) { @@ -974,17 +878,11 @@ impl Sockets { /// The socket table as `inspect` reads it: counts, and no endpoint, /// because every client holding `netstack` can ask. pub fn inspect(&self, node: &Node, snap: &mut Snapshot) { - let (mut listeners, mut udp) = (0u32, 0u32); - for socket in self.ids.values() { - match socket { - Socket::Listener { .. } => listeners += 1, - Socket::Datagram(_) => udp += 1, - } - } snap.put("sockets.tcp", self.streams.len() as u32); snap.put("streams.holders", self.holders.len() as u32); - snap.put("sockets.listeners", listeners); - snap.put("sockets.udp", udp); + snap.put("sockets.listeners", node.listeners() as u32); + snap.put("sockets.udp", node.datagrams() as u32); + snap.put("sockets.holders", self.sockets.len() as u32); snap.put("streams.live", node.streams()); snap.put("places.held", node.held()); snap.put("places.max", self.places); @@ -1013,6 +911,99 @@ fn heard(step: RxStep) -> Heard { } } +fn kind_of(socket: Held) -> Kind { + match socket { + Held::Listener(_) => Kind::Listener, + Held::Datagram(_) => Kind::Datagram, + } +} + +/// `holder` of `socket` left. +fn departed(node: &mut Node, now: Instant, socket: Held, holder: HolderId) { + match socket { + Held::Listener(id) => node.listener_departed(now, id, holder), + Held::Datagram(id) => node.datagram_departed(now, id, holder), + } +} + +/// A socket holder's request, as its frame said it: only what its kind of +/// socket is asked, in exactly its shape. +#[derive(Debug, PartialEq, Eq)] +enum Asked { + /// With one handle, the stream's region. + Accept, + Export, + SetOption(u32, u32), + Option(u32), + Send { to: [u8; 4], port: u16, data: Vec }, + Receive, +} + +/// The request a holder of a `kind` socket sent, or `None` for a frame no +/// such holder sends, which is its leaving. +fn asked(kind: Kind, msg_type: u32, payload: &[u8], handles: usize) -> Option { + match (kind, msg_type, handles) { + (Kind::Listener, ACCEPT, 1) if payload.is_empty() => Some(Asked::Accept), + (_, EXPORT, 0) if payload.is_empty() => Some(Asked::Export), + (_, SET_OPTION, 0) => { + let option: socket::OptionRequest = ipc::decode_payload(payload).ok()?; + Some(Asked::SetOption(option.option, option.value)) + } + (_, OPTION, 0) => ipc::decode_payload::(payload).ok().map(Asked::Option), + (Kind::Datagram, SEND, 0) => { + let (to, data) = socket::datagram_of(payload)?; + Some(Asked::Send { to: to.addr, port: to.port, data: data.to_vec() }) + } + (Kind::Datagram, RECEIVE, 0) if payload.is_empty() => Some(Asked::Receive), + _ => None, + } +} + +/// What a socket request is answered with. +enum Reply { + Bare, + Word(u32), + Bytes(Vec), +} + +/// A request that is one call of the node's, on `holder`'s own `socket`. +fn answer_socket(node: &mut Node, now: Instant, socket: Held, holder: HolderId, asked: Asked) -> Result { + match (socket, asked) { + (Held::Listener(id), Asked::SetOption(OPT_NODELAY, value)) => { + node.set_listener_nodelay(id, value != 0).then_some(Reply::Bare).ok_or(ERR_NOT_CONNECTED) + } + (Held::Listener(id), Asked::Option(OPT_NODELAY)) => node.listener_nodelay(id).map(|on| Reply::Word(u32::from(on))).ok_or(ERR_NOT_CONNECTED), + (Held::Datagram(id), Asked::SetOption(OPT_BROADCAST, value)) => { + node.udp_set_broadcast(id, value != 0).map(|()| Reply::Bare).map_err(refused) + } + (Held::Datagram(id), Asked::Option(OPT_BROADCAST)) => node.udp_broadcast(id).map(|on| Reply::Word(u32::from(on))).map_err(refused), + (_, Asked::SetOption(..) | Asked::Option(_)) => Err(ERR_INVALID_INPUT), + (Held::Datagram(id), Asked::Send { to, port, data }) => { + node.udp_send_to(now, id, Ipv4Addr::from(to), port, &data).map(|()| Reply::Bare).map_err(refused) + } + (Held::Datagram(id), Asked::Receive) => { + let mut payload = [0u8; toyos_net_udp::limits::MAX_PAYLOAD]; + match node.udp_recv_from(id, holder, &mut payload) { + Ok(Some(datagram)) => { + let mut frame = [0u8; socket::MAX_FRAME]; + let source = datagram.source_port.map_or(0, Port::get); + let Some(frame) = socket::datagram(datagram.source.octets(), source, &payload[..datagram.len], &mut frame) else { + unreachable!("[udp] delivered a datagram longer than one frame carries") + }; + Ok(Reply::Bytes(frame.to_vec())) + } + Ok(None) => Err(ERR_WOULD_BLOCK), + Err(refusal) => Err(refused(refusal)), + } + } + (_, asked @ (Asked::Accept | Asked::Export | Asked::Send { .. } | Asked::Receive)) => { + unreachable!("{asked:?} is answered by its own call, or was refused as no holder's of its kind") + } + } +} + +const _: () = assert!(toyos_net_udp::limits::MAX_PAYLOAD <= socket::MAX_DATAGRAM, "a received datagram is one frame"); + /// A stream's request refused, on its first holder's connection. fn answer_refused(holders: &Holders, code: u32) { if let Some(first) = holders.by_number.borrow().get(&Holder::FIRST) @@ -1026,6 +1017,55 @@ fn answer_refused(holders: &Holders, code: u32) { mod tests { use super::*; + /// A new connection's requests make a socket or ask a name and act on + /// none: every request a holder makes of its own socket is no request + /// there, so a connection that holds nothing reaches nothing. + #[test] + fn a_new_connection_names_no_socket() { + for holders in [ACCEPT, OPTION, SET_OPTION, SEND, RECEIVE, EXPORT, EXPORTED, REFUSED, RING, ADOPT, socket::ADOPT] { + assert_eq!(MsgType::from_u32(holders), None, "{holders}"); + } + } + + /// A holder asks only what its kind of socket is asked, in exactly its + /// shape: anything else is its leaving, and none panics. + #[test] + fn a_holder_asks_only_its_own_kind_of_socket() { + let option = [1, 0, 0, 0, 1, 0, 0, 0]; + let mut frame = [0u8; socket::MAX_FRAME]; + let datagram = socket::datagram([192, 0, 2, 7], 53, b"hi", &mut frame).unwrap().to_vec(); + assert_eq!(asked(Kind::Listener, ACCEPT, &[], 1), Some(Asked::Accept)); + for kind in [Kind::Listener, Kind::Datagram] { + assert_eq!(asked(kind, EXPORT, &[], 0), Some(Asked::Export)); + assert_eq!(asked(kind, SET_OPTION, &option, 0), Some(Asked::SetOption(1, 1))); + assert_eq!(asked(kind, OPTION, &[2, 0, 0, 0], 0), Some(Asked::Option(2))); + } + assert_eq!(asked(Kind::Datagram, SEND, &datagram, 0), Some(Asked::Send { to: [192, 0, 2, 7], port: 53, data: b"hi".to_vec() })); + assert_eq!(asked(Kind::Datagram, RECEIVE, &[], 0), Some(Asked::Receive)); + + for (kind, msg_type, payload, handles) in [ + (Kind::Datagram, ACCEPT, &[][..], 1), + (Kind::Listener, SEND, &datagram[..], 0), + (Kind::Listener, RECEIVE, &[], 0), + (Kind::Listener, ACCEPT, &[], 0), + (Kind::Listener, ACCEPT, &[], 2), + (Kind::Listener, ACCEPT, &[0], 1), + (Kind::Datagram, RECEIVE, &[], 1), + (Kind::Datagram, RECEIVE, &[0], 0), + (Kind::Datagram, SEND, &datagram[..7], 0), + (Kind::Datagram, SEND, &datagram[..], 1), + (Kind::Listener, EXPORT, &[0], 0), + (Kind::Listener, EXPORT, &[], 1), + (Kind::Datagram, SET_OPTION, &option[..7], 0), + (Kind::Datagram, OPTION, &option[..], 0), + (Kind::Listener, RING, &[], 0), + (Kind::Datagram, socket::ADOPT, &[0; 8], 1), + (Kind::Listener, u32::MAX, &[], 0), + ] { + assert_eq!(asked(kind, msg_type, payload, handles), None, "{kind:?} {msg_type} {payload:?} {handles}"); + } + } + /// Only a bare ring and a bare export are a holder's: any payload, any /// handle, any other type and broken framing are its leaving, and none /// panics. diff --git a/system/netstack/udp/src/lib.rs b/system/netstack/udp/src/lib.rs index 8a3901b85c6..162cd885376 100644 --- a/system/netstack/udp/src/lib.rs +++ b/system/netstack/udp/src/lib.rs @@ -428,6 +428,16 @@ impl Udp { self.socket(id).map(|s| s.broadcast = permitted) } + /// Whether [`Self::set_broadcast`] last permitted it. + pub fn broadcast(&mut self, id: SocketId) -> Result { + self.socket(id).map(|s| s.broadcast) + } + + /// The datagrams waiting for a [`Self::recv`]. + pub fn waiting(&mut self, id: SocketId) -> Result { + self.socket(id).map(|s| s.rx.len()) + } + pub fn set_ttl(&mut self, id: SocketId, unicast: Ttl, multicast: Ttl) -> Result<(), Error> { self.socket(id).map(|s| { s.ttl = unicast; diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index fa95ffb1cb6..ca180751a91 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -3035,7 +3035,7 @@ fn start<'a>( toyos::fd::Kind::Handle => { command.inherit_handle(fd, handle.0); } - toyos::fd::Kind::Stream => { + toyos::fd::Kind::Exported => { command.inherit_exported(fd, handle.0); } } diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index b9ee178275d..1d868d7d83a 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -1205,6 +1205,10 @@ pub struct BootOptions { /// A slot file (`toyos_update::slots::*_FILE`) bent in the marked slot, /// with the other slot carrying the image whole ([`super::slots`]). pub bent: Option<&'static str>, + /// A TCP port of the host's loopback, `.0`, that QEMU's user network + /// forwards to the guest's port `.1`: what a peer on the host connects to a + /// guest's listener through. Only a profile with a NIC may set it. + pub forward: Option<(u16, u16)>, } impl BootOptions { @@ -1246,6 +1250,7 @@ impl Default for BootOptions { psci_trace: None, rtc_base: None, bent: None, + forward: None, } } } @@ -2482,19 +2487,24 @@ fn qemu_command( // The NIC before the virtio block, so a profile that has one and not the // other still creates it after the unit and before everything else. // `iommu_platform` is virtio's own way of asking to be decoded. + assert!(options.forward.is_none() || shape.nic != Nic::Absent, "a forwarded port needs a profile with a NIC"); + let netdev = match options.forward { + Some((host, guest)) => format!("user,id=net0,hostfwd=tcp:127.0.0.1:{host}-:{guest}"), + None => "user,id=net0".to_string(), + }; match shape.nic { Nic::Absent => {} Nic::Virtio => { - qemu.arg("-netdev").arg("user,id=net0").arg("-device").arg(format!( + qemu.arg("-netdev").arg(&netdev).arg("-device").arg(format!( "virtio-net-pci-non-transitional,netdev=net0{platform}" )); } Nic::Untranslated => { - qemu.arg("-netdev").arg("user,id=net0").arg("-device").arg("virtio-net-pci-non-transitional,netdev=net0"); + qemu.arg("-netdev").arg(&netdev).arg("-device").arg("virtio-net-pci-non-transitional,netdev=net0"); } Nic::BehindPort => { qemu.arg("-device").arg("pcie-root-port,id=nicport,chassis=9"); - qemu.arg("-netdev").arg("user,id=net0").arg("-device").arg(format!( + qemu.arg("-netdev").arg(&netdev).arg("-device").arg(format!( "virtio-net-pci-non-transitional,netdev=net0,bus=nicport{platform}" )); } diff --git a/tests/guest/rust-tests/src/bin/netstack_gone_mid_bind.rs b/tests/guest/rust-tests/src/bin/netstack_gone_mid_bind.rs index 8cc8de5d0ae..69e06a3e83d 100644 --- a/tests/guest/rust-tests/src/bin/netstack_gone_mid_bind.rs +++ b/tests/guest/rust-tests/src/bin/netstack_gone_mid_bind.rs @@ -24,13 +24,14 @@ //! well as through the SDK, because `ErrorKind::NotConnected` is the literal //! thing `/system/bin/sshserver` matches on. //! 2. **Gone between the connect and the request.** The client is connected -//! and queued when the port closes. `tcp_bind` hands netstack a pipe end in -//! its request's frame, and `SYS_MESSAGE_SEND` refuses the whole message with -//! `SyscallError::Gone`. The refused send consumed the pipe end and closed -//! it, so the pipe's read end, still the child's, reads end of file: the -//! kernel left the end at its own number and the SDK closed it. -//! 3. **The same, for a request that carries no handles**, which the same send -//! refuses with the same `SyscallError::Gone`. +//! and queued when the port closes. A stream's connect hands netstack a +//! handle in its request's frame, here a pipe end in its region's place, and +//! `SYS_MESSAGE_SEND` refuses the whole message with `SyscallError::Gone`. +//! The refused send consumed the pipe end and closed it, so the pipe's read +//! end, still the child's, reads end of file: the kernel left the end at its +//! own number and the SDK closed it. +//! 3. **The same, for a request that carries no handles**, a listen, which the +//! same send refuses with the same `SyscallError::Gone`. //! //! Arms 2 and 3 are the ones that produced the panic. They are red on a tree //! where `toyos::net::hangup` maps anything but `IpcError::Disconnected` to @@ -50,9 +51,7 @@ use std::os::toyos::process::CommandExt; use std::process::{Child, ChildStdout, Command, Stdio}; use toyos::endow::EndowError; -use toyos::net::{ - MsgType, NetError, NetstackConn, SocketCloseRequest, TcpBindPipedRequest, TcpBindResponse, TcpOptions, -}; +use toyos::net::{BoundResponse, MsgType, NetError, NetstackConn, TcpConnectRequest, TcpListenRequest, TcpOptions}; use toyos::{namespace, port, AsHandle}; use toyos_abi::syscall::{self, SVC_LABEL}; @@ -68,8 +67,8 @@ const SSH_PORT: u16 = 22; fn main() { match std::env::args().nth(1).as_deref() { Some("already-gone") => already_gone(), - Some("bind") => mid_flight(Request::Bind), - Some("close") => mid_flight(Request::Close), + Some("handle") => mid_flight(Request::Handle), + Some("bare") => mid_flight(Request::Bare), Some(other) => panic!("unknown role {other:?}"), None => test(), } @@ -77,8 +76,8 @@ fn main() { fn test() { arm_already_gone(); - arm_mid_flight("bind", "the handle transfer"); - arm_mid_flight("close", "the frame write"); + arm_mid_flight("handle", "the handle transfer"); + arm_mid_flight("bare", "the frame write"); println!("a netstack that leaves mid-bind is a netstack that is not there, at all three syscalls"); } @@ -174,10 +173,11 @@ fn already_gone() -> ! { /// Which syscall the child's request is refused at. enum Request { - /// Carries a pipe end, which a refused send closes — `tcp_bind`'s own shape. - Bind, - /// Carries none. - Close, + /// Carries a pipe end, which a refused send closes — a connect's own + /// shape, whose handle is its region. + Handle, + /// Carries none — a listen's. + Bare, } /// The child of arms 2 and 3. @@ -194,16 +194,16 @@ fn mid_flight(request: Request) -> ! { assert_eq!(n, 0, "the watch connection carried {n} byte(s); nobody ever wrote to it"); let err = match request { - Request::Bind => { - let (notify, netstack_notify) = toyos::pipe_pair().expect("the notify pipe"); + Request::Handle => { + let (notify, netstack_notify) = toyos::pipe_pair().expect("the pipe"); let err = netstack .request_with_handles( [netstack_notify.into()], - MsgType::TcpBindPiped, - &TcpBindPipedRequest { addr: [0, 0, 0, 0], port: SSH_PORT, _pad: 0, options: TcpOptions::new(false) }, + MsgType::TcpConnect, + &TcpConnectRequest { addr: [192, 0, 2, 2], port: SSH_PORT, _pad: 0, timeout_ms: 0 }, ) - .and_then(|pending| pending.response::().map(|_| ())) - .expect_err("a bind into a port whose acceptor is gone was answered"); + .and_then(|pending| pending.status()) + .expect_err("a request into a port whose acceptor is gone was answered"); // The refused send's write end was the pipe's only writer; a // writer still open refuses this read at once rather than blocking. assert_eq!( @@ -213,10 +213,10 @@ fn mid_flight(request: Request) -> ! { ); err } - Request::Close => netstack - .request(MsgType::TcpClose, &SocketCloseRequest { socket_id: 0 }) - .and_then(|pending| pending.status()) - .expect_err("a request into a port whose acceptor is gone was answered"), + Request::Bare => netstack + .request(MsgType::TcpListen, &TcpListenRequest { addr: [0, 0, 0, 0], port: SSH_PORT, _pad: 0, options: TcpOptions::new(false) }) + .and_then(|pending| pending.response::().map(|_| ())) + .expect_err("a listen into a port whose acceptor is gone was answered"), }; assert_eq!( diff --git a/tests/guest/rust-tests/src/bin/socket_export.rs b/tests/guest/rust-tests/src/bin/socket_export.rs new file mode 100644 index 00000000000..07263d05d94 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/socket_export.rs @@ -0,0 +1,79 @@ +//! A listener and a datagram socket a real netstack holds, each exported to a +//! child through std's `Command` and adopted there: the path from a bind to a +//! second process's accept and datagrams that only a guest with netstack shows. +//! +//! The harness is the peer: it listens for datagrams on its host's loopback, +//! which QEMU's user network reaches from the guest as `10.0.2.2`, names that +//! port as this job's argument, and connects to [`LISTENING`] through the port +//! QEMU forwards to it. The parent binds both sockets and spawns a child given +//! them as descriptors 3 and 4 (`CommandExt::inherit_handle`, which exports +//! each); the child says it is ready in a datagram, is told to go in one, +//! accepts the harness's connection and writes a line on it. Once the child +//! has ended, the parent does the same through its own holders, which still +//! hold both sockets. Every step waits on the peer's next datagram or +//! connection, under the harness's ceiling. + +use std::io::Write; +use std::net::{Ipv4Addr, SocketAddr, TcpListener, UdpSocket}; +use std::os::fd::FromRawFd; +use std::os::toyos::io::AsRawHandle; +use std::os::toyos::process::CommandExt; +use std::process::{Command, Stdio}; + +const SELF_PATH: &str = "/system/bin/test_rs_socket_export"; + +/// QEMU's user network's address of its host. +const HOST: Ipv4Addr = Ipv4Addr::new(10, 0, 2, 2); + +/// The port the guest listens on, which the harness's boot forwards to +/// (`tests/toyos.rs`'s `SOCKET_EXPORT_PORT`). +const LISTENING: u16 = 7070; + +/// The descriptors the child is given its holders under. +const LISTENER_FD: i32 = 3; +const DATAGRAM_FD: i32 = 4; + +fn main() { + let mut args = std::env::args().skip(1); + let port: u16 = match args.next().as_deref() { + Some("child") => { + let port = args.next().expect("the harness's port"); + return child(port.parse().unwrap_or_else(|e| panic!("a port, not {port:?}: {e}"))); + } + Some(port) => port.parse().unwrap_or_else(|e| panic!("a port, not {port:?}: {e}")), + None => panic!("the harness's datagram port is this job's argument"), + }; + let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, LISTENING)).expect("a listener through netstack"); + let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).expect("a datagram socket through netstack"); + let status = Command::new(SELF_PATH) + .arg("child") + .arg(port.to_string()) + .inherit_handle(LISTENER_FD as u32, listener.as_raw_handle()) + .inherit_handle(DATAGRAM_FD as u32, socket.as_raw_handle()) + .stdout(Stdio::null()) + .status() + .expect("spawn the child with a holder of each socket"); + assert!(status.success(), "the child ended {status:?}"); + serve("parent", &listener, &socket, port); + println!("a listener and a datagram socket exported to a child: it accepted and spoke on them, and so did the parent after it"); +} + +fn child(port: u16) { + // SAFETY: the descriptors its parent gave it, a holder of each socket. + let (listener, socket) = unsafe { (TcpListener::from_raw_fd(LISTENER_FD), UdpSocket::from_raw_fd(DATAGRAM_FD)) }; + serve("child", &listener, &socket, port); +} + +/// `who`'s turn: ready said and go heard in datagrams, then one connection +/// accepted and a line written on it. +fn serve(who: &str, listener: &TcpListener, socket: &UdpSocket, port: u16) { + let peer = SocketAddr::from((HOST, port)); + let ready = format!("{who} ready"); + let sent = socket.send_to(ready.as_bytes(), peer).unwrap_or_else(|e| panic!("{who}'s datagram: {e}")); + assert_eq!(sent, ready.len()); + let mut heard = [0u8; 64]; + let (n, from) = socket.recv_from(&mut heard).unwrap_or_else(|e| panic!("{who}'s receive: {e}")); + assert_eq!((&heard[..n], from), (&b"go"[..], peer), "{who} heard another datagram"); + let (mut stream, _) = listener.accept().unwrap_or_else(|e| panic!("{who}'s accept: {e}")); + writeln!(stream, "accepted by the {who}").unwrap_or_else(|e| panic!("{who}'s line: {e}")); +} diff --git a/tests/images/netcase.toml b/tests/images/netcase.toml index 17333da4a6f..2b1a38d2d1c 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -33,8 +33,8 @@ serves = ["soundserver", "volume", "sound-output"] devices = ["pci:04:01:00/1af4:1059"] syscap = ["rt"] -# A job reaches netstack through test-runner's namespace: `stream_exported_to_a_child` -# connects out through it. +# A job reaches netstack through test-runner's namespace: +# `netstack_holders_exported_to_children`'s jobs reach the harness through it. [programs.test-runner] receives = ["netstack"] diff --git a/tests/toyos.rs b/tests/toyos.rs index 639cdf5797a..e074cf712ba 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -230,9 +230,11 @@ const RUST_SKIP: &[&str] = &[ // A shell's Ctrl+C, the probe being its surface: `libc_children` runs it // on tests/images/libccase.toml, whose shell and `cat` it drives. "tty_interrupt", - // A stream exported to a child through real netstack: it connects out to - // the harness, which only `stream_exported_to_a_child`'s boot sets up. + // A stream, a listener and a datagram socket exported to a child through + // real netstack: they reach the harness, which only + // `netstack_holders_exported_to_children`'s boot sets up. "stream_export", + "socket_export", // A stream's holders across processes, as their own netstack: it spawns a // child that faults, which a shared boot's other members must not watch, // and `stream_holders` runs it. @@ -587,13 +589,16 @@ const MACHINE_TESTS: &[&str] = &[ // build has; no hardware is in it. x86-64 alone, the architecture the // suite builds guest binaries for (`qemu::SUITE_ARCH`). "stream_holders", - // One stream exported through std's `Command` and adopted in the child, - // through the real netstack of `tests/images/netcase.toml`: the export, - // the endowment and the adoption cross real processes and the real - // netstack, which no host test and `stream_holders`' stand-in show. The - // harness is the peer, on the host's loopback that QEMU's user network - // reaches, so no public network. x86-64 alone (`qemu::SUITE_ARCH`). - "stream_exported_to_a_child", + // A stream, a listener and a datagram socket exported through std's + // `Command` and adopted in a child, through the real netstack of + // `tests/images/netcase.toml`: the export, the endowment, the adoption and + // a second process's accept and datagrams on one queue cross real + // processes and the real netstack, which no host test and + // `stream_holders`' stand-in show. The harness is the peer, on the host's + // loopback that QEMU's user network reaches and forwards one port of, so + // no public network. Both jobs in one boot. x86-64 alone + // (`qemu::SUITE_ARCH`). + "netstack_holders_exported_to_children", // Login sessions: their owner parents, signs out and ends with each, and // the machine's reboot waits for them until forced, which only processes // in one boot show, as order, completion and counts. The reboot ends the @@ -4334,7 +4339,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "hosted_clang_hello" => common::hostedclang::hosted_clang_hello(), "libc_children" => common::libcchildren::libc_children(), "stream_holders" => stream_holders(test_config), - "stream_exported_to_a_child" => stream_exported_to_a_child(), + "netstack_holders_exported_to_children" => netstack_holders_exported_to_children(), "session_lifecycle" => common::sessionlifecycle::session_lifecycle(), "a_launch_gives_each_unsent_stdio_slot_a_null" => common::launchstdio::a_launch_gives_each_unsent_stdio_slot_a_null(), "a_row_reaches_its_own_state_and_no_other" => common::stateview::a_row_reaches_its_own_state_and_no_other(), @@ -5145,24 +5150,44 @@ fn crypto_instrument_prints_its_table(test_config: &Path) -> Result<(), String> /// else. const CRYPTO_JOB_CEILING_SECS: u64 = 300; -/// `test_rs_stream_export` on `tests/images/netcase.toml`'s boot, the -/// harness its stream's peer: the child's line, then the parent's, then the -/// FIN once both holders are gone. -fn stream_exported_to_a_child() -> Result<(), String> { - use std::io::Read; - const JOB: &str = "stream_export"; +/// The guest port `test_rs_socket_export` listens on, which the boot forwards +/// a port of the host's loopback to. +const SOCKET_EXPORT_PORT: u16 = 7070; + +/// `test_rs_stream_export` and `test_rs_socket_export` on one boot of +/// `tests/images/netcase.toml`, the harness every socket's peer: a stream's +/// child's line, then its parent's, then the FIN once both holders are gone; +/// then a listener's and a datagram socket's child's datagram, its accept and +/// its line, and the parent's after it. +fn netstack_holders_exported_to_children() -> Result<(), String> { /// netstack's line once its lease landed: before it, a connect is refused. const LEASED: &str = "netstack: ready, at most"; - let peer = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("no loopback listener: {e}"))?; - let port = peer.local_addr().map_err(|e| format!("the listener's port: {e}"))?.port(); let tests = compile::repo_root().join("tests/guest/rust-tests"); - let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &tests, JOB); + let bins: Vec<(String, Vec)> = + ["stream_export", "socket_export"].iter().map(|job| (job.to_string(), qemu::build_toyos_bin(qemu::SUITE_ARCH, &tests, job))).collect(); + // The host's end of the forward: a port found free and given up, which + // QEMU binds as it starts and refuses to start without. + let forwarded = std::net::TcpListener::bind("127.0.0.1:0") + .and_then(|probe| probe.local_addr()) + .map_err(|e| format!("no loopback port to forward: {e}"))? + .port(); let config = compile::repo_root().join("tests/images/netcase.toml"); - let mut qemu = QemuInstance::boot_with_options(&config, &[], &[(JOB.to_string(), bin)], BootOptions::default()); + let options = BootOptions { forward: Some((forwarded, SOCKET_EXPORT_PORT)), ..BootOptions::default() }; + let mut qemu = QemuInstance::boot_with_options(&config, &[], &bins, options); serial::Serial::boot(&qemu).must_be_clean()?; if !qemu.boot_log().contains(LEASED) && !qemu.drain_until(Duration::from_secs(60), |l| l.contains(LEASED)).contains(LEASED) { return Err("netstack never said its lease landed".into()); } + stream_exported_to_a_child(&mut qemu)?; + sockets_exported_to_a_child(&mut qemu, forwarded) +} + +/// `test_rs_stream_export`: the child's line, then the parent's, then the FIN +/// once both holders are gone. +fn stream_exported_to_a_child(qemu: &mut QemuInstance) -> Result<(), String> { + use std::io::Read; + let peer = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("no loopback listener: {e}"))?; + let port = peer.local_addr().map_err(|e| format!("the listener's port: {e}"))?.port(); // The peer's side on a thread of its own: the job's lines arrive while it runs. let heard = std::thread::spawn(move || -> Result, String> { let (mut conn, _) = peer.accept().map_err(|e| format!("the guest's connect: {e}"))?; @@ -5186,6 +5211,45 @@ fn stream_exported_to_a_child() -> Result<(), String> { Ok(()) } +/// `test_rs_socket_export`: for the child and then the parent, a datagram +/// saying it is ready, answered by one saying go, and a connection through the +/// forwarded port that it accepts and writes its line on. +fn sockets_exported_to_a_child(qemu: &mut QemuInstance, forwarded: u16) -> Result<(), String> { + use std::io::Read; + /// A liveness ceiling on each of the peer's waits, never a timing. + const CEILING: Duration = Duration::from_secs(120); + let datagrams = std::net::UdpSocket::bind("127.0.0.1:0").map_err(|e| format!("no loopback datagram socket: {e}"))?; + datagrams.set_read_timeout(Some(CEILING)).map_err(|e| e.to_string())?; + let port = datagrams.local_addr().map_err(|e| format!("the datagram socket's port: {e}"))?.port(); + let heard = std::thread::spawn(move || -> Result<(), String> { + for who in ["child", "parent"] { + let mut ready = [0u8; 64]; + let (n, from) = datagrams.recv_from(&mut ready).map_err(|e| format!("the {who}'s datagram: {e}"))?; + if &ready[..n] != format!("{who} ready").as_bytes() { + return Err(format!("the peer heard {:?} for the {who}'s datagram", String::from_utf8_lossy(&ready[..n]))); + } + datagrams.send_to(b"go", from).map_err(|e| format!("go to the {who}: {e}"))?; + let mut conn = std::net::TcpStream::connect(("127.0.0.1", forwarded)).map_err(|e| format!("the connect for the {who}: {e}"))?; + conn.set_read_timeout(Some(CEILING)).map_err(|e| e.to_string())?; + let mut line = Vec::new(); + conn.read_to_end(&mut line).map_err(|e| format!("the {who}'s line, until its FIN: {e} after {line:?}"))?; + let owed = format!("accepted by the {who}\n"); + if line != owed.as_bytes() { + return Err(format!("the peer heard {:?}, not {owed:?}", String::from_utf8_lossy(&line))); + } + } + Ok(()) + }); + let result = qemu.run_test(&format!("test_rs_socket_export {port}"), Duration::from_secs(300)); + if let Some(why) = &result.error { + return Err(format!("{why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + heard.join().map_err(|_| "the peer's thread panicked".to_string())? +} + /// `test_rs_stream_handles` on a boot of its own: a holder killed inside its /// gate leaves the other working, netstack's exit ends a blocked read in each, /// within a process a stream's handles are one end, one end reads and writes From 22225610bd417f1d405915acbeb0debeaa6fb132 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 23 Oct 2026 22:14:56 +0200 Subject: [PATCH 2/4] review r1: a look asks only once its bell rang, a datagram socket's peer 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 --- Cargo.lock | 2 +- ...as-not-bound-is-not-shared-with-a-child.md | 9 +- ...ves-every-byte-through-two-kernel-pipes.md | 2 +- sdk/libc/src/socket.rs | 56 ++++---- sdk/std/sys/net/connection.rs | 14 +- sdk/toyos/src/net/ends.rs | 13 +- sdk/toyos/src/net/socket.rs | 130 +++++++++++++----- sdk/toyos/src/net/stream.rs | 10 +- system/netstack/node/src/datagram.rs | 72 +++++++--- system/netstack/node/src/lease.rs | 13 +- system/netstack/node/src/listeners.rs | 8 +- system/netstack/node/tests/datagram.rs | 39 ++++++ system/netstack/node/tests/listeners.rs | 12 ++ system/netstack/shard/src/lib.rs | 21 ++- system/netstack/src/serve.rs | 55 +++++++- system/netstack/udp/src/lib.rs | 7 +- .../guest/rust-tests/src/bin/socket_export.rs | 72 +++++++--- tests/toyos.rs | 48 ++++++- 18 files changed, 430 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 298f95ffbb8..b532580cc85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1714,7 +1714,7 @@ dependencies = [ [[package]] name = "mio" version = "1.2.0" -source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#5c122cfa4f2f2c715133565552c98511ee60a2b8" +source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#5d013b872aae54a8bbf1ff84eb6c39ee145a0a1e" dependencies = [ "libc", "toyos-abi", diff --git a/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md b/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md index cc694356817..b24c13b8eb0 100644 --- a/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md +++ b/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md @@ -14,11 +14,12 @@ descriptor of it, which exports a holder (`fd::exported`, working when another exits or is killed. A stream's own handle stays in its process: `endow` and `provide` of one are refused with `Misuse::Moved`. +`netstack_holders_exported_to_children` (`tests/toyos.rs`) reads a listener's +and a datagram socket's holders in a parent and its child, one after the other +and both blocked at once; no guest test reads a survivor after the other holder +is killed. + What is not shared: -- A listener's and a datagram socket's holders are read one after the other by - `netstack_holders_exported_to_children` (`tests/toyos.rs`): the child - accepts and exchanges datagrams, and the parent after it. No test reads - both at once, nor a survivor after the other holder is killed. - A C socket that has neither bound nor connected is libc's entry alone, an object of libc's own, which a spawn refuses (`NotSupported`). - A socket2 socket's descriptor is an object of socket2's own, so a spawn diff --git a/issues/netstack-moves-every-byte-through-two-kernel-pipes.md b/issues/netstack-moves-every-byte-through-two-kernel-pipes.md index 1d7c3f93d83..11999833818 100644 --- a/issues/netstack-moves-every-byte-through-two-kernel-pipes.md +++ b/issues/netstack-moves-every-byte-through-two-kernel-pipes.md @@ -77,7 +77,7 @@ The design and its roast are PR #863's body. `len`, `port` and `addr` out of the region once and judges the copy: a `len` past the bytes the record holds ends that socket alone. `toyos::net`'s `send_to` refuses what a sender can judge and writes no record: a payload past - `toyos_abi::net::UDP_MAX_PAYLOAD`, as POSIX's `sendto` fails + `toyos::net::socket::MAX_DATAGRAM`, as POSIX's `sendto` fails `[EMSGSIZE]`; port 0; an unspecified address. That bound is the one declaration: [udp]'s `limits::MAX_PAYLOAD` goes, and [udp], the node's `name.rs` and `resolve.rs`, netstack's `serve.rs` and [udp]'s diff --git a/sdk/libc/src/socket.rs b/sdk/libc/src/socket.rs index fddcd87a90e..d882e6cf65f 100644 --- a/sdk/libc/src/socket.rs +++ b/sdk/libc/src/socket.rs @@ -56,15 +56,13 @@ enum SocketKind { /// A socket netstack does not hold yet: what its bind or connect makes it /// with. A listener's `nodelay` goes in its bind's request, a datagram -/// socket's `broadcast` and peer are handed over after its bind, and a -/// stream's `nodelay` into its region before its connect. +/// socket's `broadcast` is handed over after its bind, and a stream's +/// `nodelay` into its region before its connect. #[derive(Clone, Copy)] struct SocketEntry { kind: SocketKind, nodelay: bool, broadcast: bool, - /// Where a datagram socket's `send` goes. - peer: Option<([u8; 4], u16)>, } const MAX_SOCKETS: usize = 128; @@ -203,15 +201,19 @@ unsafe fn fill_sockaddr(addr: *mut Sockaddr, addrlen: *mut SocklenT, ip: [u8; 4] } /// Binds the datagram socket descriptor `fd` names, whose entry is `entry`, -/// handing over what was set before netstack held it: from here `fd` names -/// netstack's socket, and the entry goes once the caller's hold does. -fn bind_datagram(fd: i32, entry: &SocketEntry, ip: [u8; 4], port: u16) -> Result { +/// handing over what was set before netstack held it and connecting it to +/// `peer`: from here `fd` names netstack's socket, and the entry goes once the +/// caller's hold does. +fn bind_datagram(fd: i32, entry: &SocketEntry, ip: [u8; 4], port: u16, peer: Option<([u8; 4], u16)>) -> Result { let bound = socket::bind(ip, port).map_err(net_err_to_errno)?; let handed = (|| { if entry.broadcast { socket::set_option(bound.handle, OPT_BROADCAST, 1)?; } - socket::set_peer(bound.handle, entry.peer) + match peer { + Some((addr, port)) => socket::connect(bound.handle, addr, port), + None => Ok(()), + } })(); if let Err(e) = handed { socket::let_go(bound.handle); @@ -236,7 +238,7 @@ impl dgramreq::Datagrams for Sending<'_> { } fn bind(&mut self, ip: [u8; 4], port: u16) -> Result<(), i32> { let entry = self.entry.ok_or(EBADF)?; - self.bound = Some(bind_datagram(self.fd, entry, ip, port)?); + self.bound = Some(bind_datagram(self.fd, entry, ip, port, None)?); Ok(()) } fn send(&mut self, data: &[u8], ip: [u8; 4], port: u16) -> Result { @@ -269,7 +271,7 @@ pub unsafe extern "C" fn socket(domain: i32, sock_type: i32, _protocol: i32) -> return -1; } }; - alloc_socket(SocketEntry { kind, nodelay: false, broadcast: false, peer: None }) + alloc_socket(SocketEntry { kind, nodelay: false, broadcast: false }) } #[no_mangle] @@ -281,7 +283,7 @@ pub unsafe extern "C" fn connect(fd: i32, addr: *const Sockaddr, addrlen: Sockle let mut socket = match sock_from_fd(fd) { Some(Sock::Entry(socket)) => socket, Some(Sock::Datagram(held)) => { - return match socket::set_peer(held.handle, Some((ip, port))) { + return match socket::connect(held.handle, ip, port) { Ok(()) => 0, Err(e) => { set_errno(net_err_to_errno(e)); -1 } }; @@ -309,10 +311,12 @@ pub unsafe extern "C" fn connect(fd: i32, addr: *const Sockaddr, addrlen: Sockle Err(_) => { set_errno(EBADF); -1 } } } - SocketKind::Udp => { - entry.peer = Some((ip, port)); - 0 - } + // A connect binds a socket not yet bound, at any address to a port the + // stack chooses, as a first send does. + SocketKind::Udp => match bind_datagram(fd, entry, [0; 4], 0, Some((ip, port))) { + Ok(_) => 0, + Err(errno) => { set_errno(errno); -1 } + }, } } @@ -334,7 +338,7 @@ pub unsafe extern "C" fn bind(fd: i32, addr: *const Sockaddr, addrlen: SocklenT) SocketKind::Tcp => socket::listen(ip, port, TcpOptions::new(entry.nodelay)) .map_err(net_err_to_errno) .and_then(|bound| toyos::fd::replace(fd, socket::descriptor(bound.handle)).map_err(|_| EBADF)), - SocketKind::Udp => bind_datagram(fd, &entry, ip, port).map(drop), + SocketKind::Udp => bind_datagram(fd, &entry, ip, port, None).map(drop), }; match bound { Ok(()) => 0, @@ -402,18 +406,12 @@ pub unsafe extern "C" fn send(fd: i32, buf: *const u8, len: usize, _flags: i32) let data = core::slice::from_raw_parts(buf, len); match sock_from_fd(fd) { Some(Sock::Stream(held)) => write_stream(held.handle, data), - Some(Sock::Datagram(held)) => match socket::peer(held.handle) { - Ok(Some((ip, port))) => send_datagram(Sending { fd, entry: None, bound: Some(held.handle) }, data, ip, port), - Ok(None) => { set_errno(ENOTCONN); -1 } + Some(Sock::Datagram(held)) => match socket::send(held.handle, data) { + Ok(sent) => sent as isize, Err(e) => { set_errno(net_err_to_errno(e)); -1 } }, - Some(Sock::Entry(mut socket)) => match *socket.slot() { - Some(entry @ SocketEntry { kind: SocketKind::Udp, peer: Some((ip, port)), .. }) => { - send_datagram(Sending { fd, entry: Some(&entry), bound: None }, data, ip, port) - } - Some(_) => { set_errno(ENOTCONN); -1 } - None => { set_errno(EBADF); -1 } - }, + // A socket netstack does not hold was never connected. + Some(Sock::Entry(_)) => { set_errno(ENOTCONN); -1 } Some(Sock::Listener(_)) => { set_errno(ENOTCONN); -1 } None => { set_errno(EBADF); -1 } } @@ -799,11 +797,7 @@ pub unsafe extern "C" fn getpeername( Ok(peer) => peer, Err(e) => { set_errno(net_err_to_errno(e)); return -1; } }, - Some(Sock::Listener(_)) => None, - Some(Sock::Entry(mut socket)) => match *socket.slot() { - Some(entry) => entry.peer, - None => { set_errno(EBADF); return -1; } - }, + Some(Sock::Listener(_) | Sock::Entry(_)) => None, None => { set_errno(EBADF); return -1; } }; let Some((ip, port)) = peer else { set_errno(ENOTCONN); return -1; }; diff --git a/sdk/std/sys/net/connection.rs b/sdk/std/sys/net/connection.rs index 8f40c276309..936d27bb2a5 100644 --- a/sdk/std/sys/net/connection.rs +++ b/sdk/std/sys/net/connection.rs @@ -404,13 +404,12 @@ impl fmt::Debug for TcpListener { // --- UdpSocket --- -/// A datagram socket's handle, as [`TcpListener`]'s is a listener's. Where a -/// send that names no address goes is this process's (`socket::set_peer`), -/// shared by every descriptor of the socket here. +/// A datagram socket's handle, as [`TcpListener`]'s is a listener's. Its peer, +/// once connected, is netstack's, shared by every holder. pub struct UdpSocket { fd: OwnedFd, read_timeout_ms: AtomicU32, - /// Kept for its getter: a send never waits, since netstack answers it at once. + /// Kept for its getter: a send is not bounded by it (`SO_SNDTIMEO`). write_timeout_ms: AtomicU32, nonblocking: AtomicBool, } @@ -574,8 +573,7 @@ impl UdpSocket { } pub fn send(&self, buf: &[u8]) -> io::Result { - let peer = self.peer_addr()?; - self.send_to(buf, &peer) + socket::send(self.handle(), buf).map_err(net_err_to_io) } pub fn connect(&self, addr: A) -> io::Result<()> { @@ -583,8 +581,8 @@ impl UdpSocket { .to_socket_addrs()? .next() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no addresses found"))?; - let peer = addr_to_v4(&addr)?; - socket::set_peer(self.handle(), Some(peer)).map_err(net_err_to_io) + let (ip, port) = addr_to_v4(&addr)?; + socket::connect(self.handle(), ip, port).map_err(net_err_to_io) } pub fn as_raw_fd(&self) -> i32 { diff --git a/sdk/toyos/src/net/ends.rs b/sdk/toyos/src/net/ends.rs index eb6a5ef5b18..d0722122097 100644 --- a/sdk/toyos/src/net/ends.rs +++ b/sdk/toyos/src/net/ends.rs @@ -77,14 +77,6 @@ impl Ends { } } - /// The end `handle` reaches, changed by `f`. - pub(super) fn update(&mut self, handle: RawHandle, f: impl FnOnce(&mut E)) -> Option<()> { - let slot = self.find(handle)?; - let Slot::Live { end, .. } = &mut self.slots[slot] else { return None }; - f(end); - Some(()) - } - /// A call through `handle` begins: its end, counted. pub(super) fn call(&mut self, handle: RawHandle) -> Option<(usize, E)> { let slot = self.find(handle)?; @@ -156,9 +148,8 @@ mod tests { let mut ends: Ends = Ends::new(); let slot = ends.reserve().unwrap(); ends.enter(slot, 7, h(10)); - assert_eq!((ends.find(h(12)), ends.call(h(12)), ends.forget(h(12)), ends.update(h(12), |_| {})), (None, None, None, None)); - assert_eq!(ends.update(h(10), |end| *end = 8), Some(())); - assert_eq!(ends.get(h(10)), Some(8)); + assert_eq!((ends.find(h(12)), ends.call(h(12)), ends.forget(h(12))), (None, None, None)); + assert_eq!(ends.get(h(10)), Some(7)); } /// Past the table's bound no slot is reserved, and a reservation given back is a slot again. diff --git a/sdk/toyos/src/net/socket.rs b/sdk/toyos/src/net/socket.rs index a7559fa276c..4cb41b6c750 100644 --- a/sdk/toyos/src/net/socket.rs +++ b/sdk/toyos/src/net/socket.rs @@ -11,15 +11,18 @@ //! end's turn: [`ACCEPT`], with a stream's region, answered with the stream's connection and //! bells ([`super::TcpAcceptResponse`]); [`SEND`], one datagram; [`RECEIVE`], answered with one //! datagram; [`SET_OPTION`] and [`OPTION`]; and [`EXPORT`], answered [`EXPORTED`] or -//! [`REFUSED`]. An exported end's first frame is [`ADOPT`], carrying [`Adopt`] and the new -//! holder's bell. **A datagram is one frame** each way, [`Datagram`] then its bytes, so two -//! holders' datagrams never interleave. +//! [`REFUSED`]; a datagram socket's [`CONNECT`], [`PEER`] and [`SEND_PEER`], whose peer is the +//! socket's and so every holder's. An exported end's first frame is [`ADOPT`], carrying +//! [`Adopt`] and the new holder's bell. **A datagram is one frame** each way, [`Datagram`] then +//! its bytes, so two holders' datagrams never interleave. //! -//! **The bell says "look"**: netstack rings a holder while a connection or a datagram waits and -//! the holder has not asked since it was last rung. So an accept and a receive read the bell -//! empty just before they ask, and one that finds nothing waits for the bell: what another -//! holder took first is answered [`NetError::WouldBlock`], and no ring is lost, since a ring is -//! read only by a call whose request netstack then answers. Nothing else reads the bell. +//! **The bell says "look"**: netstack rings a holder while a connection, a datagram or a +//! datagram socket's pending error waits and the holder has not asked since it was last rung, +//! a new holder at once. So an accept and a receive ask only once the bell rang: one that finds +//! it empty waits, or answers [`NetError::WouldBlock`], with no request; one that read a ring +//! asks, and what another holder took first is answered `WouldBlock` too. A ring read by a call +//! that failed before its request went is kept for the next, so none is lost. Nothing else +//! reads the bell. //! //! **A handle with no end here is refused** [`NetError::NotASocket`]. A descriptor's handle is //! a copy of its end's bell, so a poller watches it for reading; the end keeps its own @@ -28,6 +31,8 @@ //! connection. A listener's connection keeps it, since the kernel carries an accept's region //! only on a connection that has it; no descriptor names that handle, so nothing hands it on. +use core::sync::atomic::{AtomicBool, Ordering}; + use toyos_abi::handle::Rights; use toyos_abi::syscall; use toyos_abi::RawHandle; @@ -59,6 +64,14 @@ pub const SEND: u32 = 6; /// A datagram holder's receive, bare: answered [`Datagram`], its source, then its bytes, or /// [`super::ERR_WOULD_BLOCK`]. pub const RECEIVE: u32 = 7; +/// A datagram holder's connect: [`Datagram`], the peer, its `_pad` and nothing more. Answered +/// bare. +pub const CONNECT: u32 = 13; +/// A datagram holder's ask for its socket's peer, bare: answered [`Datagram`], or +/// [`super::ERR_NOT_CONNECTED`]. +pub const PEER: u32 = 14; +/// A datagram holder's send to its socket's peer: the datagram's bytes alone. Answered bare. +pub const SEND_PEER: u32 = 15; /// The most bytes one datagram carries: an Ethernet frame's 1500, past the IPv4 header and the /// UDP header (RFC 894, RFC 791, RFC 768). @@ -148,9 +161,6 @@ struct End { /// The address its listen or bind named, and the port it holds. addr: [u8; 4], port: u16, - /// Where a datagram socket's send goes when it names nowhere: this process's word, which - /// netstack does not hold. - peer: Option<([u8; 4], u16)>, } static TABLE: Lock> = Lock::new(Ends::new()); @@ -158,6 +168,10 @@ static TABLE: Lock> = Lock::new(Ends::new()); /// Each end's turn on its connection: one request and its answer at a time. static TURNS: [Lock<()>; MAX_SOCKETS] = [const { Lock::new(()) }; MAX_SOCKETS]; +/// Each end read a ring and has not yet sent the request it owes: kept by a look that failed +/// before its request went, so the next look asks. Read and written under the end's turn. +static RUNG: [AtomicBool; MAX_SOCKETS] = [const { AtomicBool::new(false) }; MAX_SOCKETS]; + /// Lets go of an end no handle or call reaches: its connection, which is its holder's leaving, /// and its bell. fn free(slot: usize, end: End) { @@ -181,18 +195,35 @@ fn call_on(handle: RawHandle, kind: Kind, f: impl FnOnce(usize, End) -> Resul call(handle, |slot, end| if end.kind == kind { f(slot, end) } else { Err(NetError::InvalidInput) }) } -/// The rings `bell` holds, read: a look follows. -pub(super) fn drain(bell: RawHandle) { +/// The rings `bell` holds, read: whether it rang, or hung up with netstack. +fn drain(bell: RawHandle) -> bool { let mut bytes = [0u8; 64]; - while let Ok(1..) = syscall::read_nonblock(bell, &mut bytes) {} + let mut rang = false; + loop { + match syscall::read_nonblock(bell, &mut bytes) { + Ok(0) => return true, + Ok(_) => rang = true, + Err(_) => return rang, + } + } } -/// `attempt` until it takes something, waiting for `bell` between as `wait` says. -fn waited(bell: RawHandle, wait: Wait, mut attempt: impl FnMut() -> Result) -> Result { +/// `ask` once the end's bell rang, until it takes something, waiting for the bell between as +/// `wait` says. `ask` is handed what it calls once its request went. +fn look(slot: usize, end: End, wait: Wait, mut ask: impl FnMut(&dyn Fn()) -> Result) -> Result { + let rung = &RUNG[slot]; loop { - match attempt() { - Err(NetError::WouldBlock) => {} - answer => return answer, + { + let _turn = TURNS[slot].lock(); + if drain(end.bell) { + rung.store(true, Ordering::Relaxed); + } + if rung.load(Ordering::Relaxed) { + match ask(&|| rung.store(false, Ordering::Relaxed)) { + Err(NetError::WouldBlock) => {} + answer => return answer, + } + } } let until = match wait { Wait::Nonblocking => return Err(NetError::WouldBlock), @@ -202,9 +233,9 @@ fn waited(bell: RawHandle, wait: Wait, mut attempt: impl FnMut() -> Result Result<(Bou let slot = TABLE.lock().reserve().ok_or(NetError::ResourceExhausted)?; let made = NetstackConn::connect().and_then(ask).and_then(|(bound, [bell], connection)| { let (descriptor, connection, bell) = take_up(kind, connection.into_owned(), bell)?; - Ok((descriptor, End { kind, connection, bell, addr, port: bound.port, peer: None })) + Ok((descriptor, End { kind, connection, bell, addr, port: bound.port })) }); let mut table = TABLE.lock(); match made { Ok((handle, end)) => { + RUNG[slot].store(false, Ordering::Relaxed); table.enter(slot, end, handle); Ok(Bound { handle, port: end.port }) } @@ -325,12 +357,7 @@ pub fn let_go(handle: RawHandle) { /// The oldest connection waiting at the listener `handle` reaches, as a stream: waiting for one /// as `wait` says, [`NetError::WouldBlock`] or [`NetError::TimedOut`] where none came. pub fn accept(handle: RawHandle, wait: Wait) -> Result { - call_on(handle, Kind::Listener, |slot, end| { - waited(end.bell, wait, || { - let _turn = TURNS[slot].lock(); - stream::accept(end.connection, end.bell) - }) - }) + call_on(handle, Kind::Listener, |slot, end| look(slot, end, wait, |asked| stream::accept(end.connection, asked))) } // ---- a datagram socket's ---- @@ -360,10 +387,9 @@ pub fn send_to(handle: RawHandle, addr: [u8; 4], port: u16, data: &[u8]) -> Resu /// `wait` says, [`NetError::WouldBlock`] or [`NetError::TimedOut`] where none came. pub fn recv_from(handle: RawHandle, buf: &mut [u8], wait: Wait) -> Result { call_on(handle, Kind::Datagram, |slot, end| { - waited(end.bell, wait, || { - let _turn = TURNS[slot].lock(); - drain(end.bell); + look(slot, end, wait, |asked| { ipc::signal(end.connection, RECEIVE).map_err(hangup)?; + asked(); let header = answer(end.connection)?; let mut frame = [0u8; MAX_FRAME]; let n = ipc::recv_bytes(end.connection, &header, &mut frame).map_err(hangup)?; @@ -375,13 +401,44 @@ pub fn recv_from(handle: RawHandle, buf: &mut [u8], wait: Wait) -> Result) -> Result<(), NetError> { - TABLE.lock().update(handle, |end| end.peer = peer).ok_or(NetError::NotASocket) +/// The socket `handle` reaches connected to `addr`:`port` (POSIX's `connect`), for every holder: +/// from here it receives only the peer's datagrams, and [`send`] goes there. +pub fn connect(handle: RawHandle, addr: [u8; 4], port: u16) -> Result<(), NetError> { + call_on(handle, Kind::Datagram, |slot, end| { + let _turn = TURNS[slot].lock(); + ipc::send(end.connection, CONNECT, &Datagram { addr, port, _pad: 0 }).map_err(hangup)?; + bare(end.connection) + }) } +/// The peer the socket `handle` reaches is connected to, by any of its holders. pub fn peer(handle: RawHandle) -> Result, NetError> { - TABLE.lock().get(handle).map(|end| end.peer).ok_or(NetError::NotASocket) + call_on(handle, Kind::Datagram, |slot, end| { + let _turn = TURNS[slot].lock(); + ipc::signal(end.connection, PEER).map_err(hangup)?; + match answer(end.connection) { + Ok(header) => { + let peer: Datagram = ipc::recv_payload(end.connection, &header).map_err(|_| NetError::Protocol(PEER))?; + Ok(Some((peer.addr, peer.port))) + } + Err(NetError::NotConnected) => Ok(None), + Err(e) => Err(e), + } + }) +} + +/// `data` as one datagram to the connected socket's peer: the bytes it took, all of them. +/// [`NetError::NotConnected`] for a socket no holder connected. +pub fn send(handle: RawHandle, data: &[u8]) -> Result { + if data.len() > MAX_DATAGRAM { + return Err(NetError::InvalidInput); + } + call_on(handle, Kind::Datagram, |slot, end| { + let _turn = TURNS[slot].lock(); + ipc::send_bytes(end.connection, SEND_PEER, data).map_err(hangup)?; + bare(end.connection)?; + Ok(data.len()) + }) } // ---- either's ---- @@ -446,7 +503,8 @@ pub(super) fn adopted(connection: Connection, payload: &[u8], handles: ipc::Batc let mut table = TABLE.lock(); match made { Ok((descriptor, connection, bell)) => { - table.enter(slot, End { kind, connection, bell, addr: adopt.addr, port: adopt.port, peer: None }, descriptor); + RUNG[slot].store(false, Ordering::Relaxed); + table.enter(slot, End { kind, connection, bell, addr: adopt.addr, port: adopt.port }, descriptor); Some(descriptor) } Err(_) => { diff --git a/sdk/toyos/src/net/stream.rs b/sdk/toyos/src/net/stream.rs index 835963bd35a..daace7b1ea8 100644 --- a/sdk/toyos/src/net/stream.rs +++ b/sdk/toyos/src/net/stream.rs @@ -744,13 +744,13 @@ pub struct Accepted { /// The oldest connection waiting at the listener whose holder's connection is /// `listener`, as a stream: its region lent with a `super::socket::ACCEPT` and -/// its connection and bells in the answer. The holder's `bell` is read empty -/// just before the request goes, and not where nothing reached netstack. -/// Refused as [`connect`] is, and [`NetError::WouldBlock`] where none waits. -pub(super) fn accept(listener: RawHandle, bell: RawHandle) -> Result { +/// its connection and bells in the answer, `asked` called once the request +/// went. Refused as [`connect`] is, and [`NetError::WouldBlock`] where none +/// waits. +pub(super) fn accept(listener: RawHandle, asked: &dyn Fn()) -> Result { let opened = open(false, ([0; 4], 0), |region| { - super::socket::drain(bell); ipc::signal_with(listener, super::socket::ACCEPT, [region]).map_err(hangup)?; + asked(); let header = answer(listener)?; let accepted: TcpAcceptResponse = ipc::recv_payload(listener, &header).map_err(hangup)?; let mut handles = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES]; diff --git a/system/netstack/node/src/datagram.rs b/system/netstack/node/src/datagram.rs index 5326f46134b..682ad693837 100644 --- a/system/netstack/node/src/datagram.rs +++ b/system/netstack/node/src/datagram.rs @@ -26,8 +26,9 @@ //! has accepted: while [ip] asks for a link address the socket's datagrams to other next hops //! leave, and a send past the queue is answered [`Refused::ResourceExhausted`] whatever its //! destination. One whose next hop [ip] gives up, or that has no way out when its turn comes, is -//! dropped and counted `udp.tx-unreachable`, and the client is told nothing: its socket is not -//! connected. +//! dropped and counted `udp.tx-unreachable`. An unconnected socket's client is told nothing; a +//! connected one's next call answers [`Refused::Unreachable`] once, as it answers an ICMP error +//! from its peer ([`Node::udp_connect`]). //! //! A refusal [udp] logs is in [`Node::drain_events`] when the send that met it returns, and one //! [ip] logs as a datagram leaves when that [`Node::transmit`] returns; no other call of a @@ -37,7 +38,7 @@ use core::net::Ipv4Addr; use alloc::boxed::Box; -use toyos_net_udp::{Counter, Error, SocketId}; +use toyos_net_udp::{Counter, Error, SocketError, SocketId}; use toyos_net_wire::{Instant, Port}; use crate::holders::{Bell, HolderId, Holders}; @@ -66,6 +67,12 @@ pub enum Refused { /// The socket holds all it may until some of it has left, or the node holds all it has /// places for: the same call succeeds later. ResourceExhausted, + /// An ICMP error said a connected socket's peer refuses the port: the socket's pending + /// error, answered once. + ConnectionRefused, + /// A connected socket's datagram found no way to its peer: an ICMP error said the way is + /// prohibited, or [ip] gave the next hop up. The socket's pending error, answered once. + Unreachable, } /// A datagram [`Node::udp_recv_from`] handed over. @@ -78,33 +85,32 @@ pub struct Datagram { pub source_port: Option, } -/// [udp]'s refusal as the client hears it. A client's socket is bound and never connected, and -/// never names its source, so the rules of those calls, and the counters that are no refusal, -/// are not its to meet. +/// [udp]'s refusal as the client hears it. A client's socket never names its source, so that +/// call's rule, and the counters that are no refusal, are not its to meet. fn refused(error: Error) -> Refused { let rule = match error { Error::NoSuchSocket => return Refused::NotConnected, Error::Refused(rule) => rule, - Error::Failed(_) => unreachable!("[udp] reported a network error against a socket that is not connected"), + Error::Failed(SocketError::Refused) => return Refused::ConnectionRefused, + Error::Failed(SocketError::Prohibited | SocketError::NextHopFailed) => return Refused::Unreachable, }; match rule { Counter::PortInUse | Counter::NoEphemeralPort => Refused::AddrInUse, - Counter::NoSourceAddress | Counter::NoRoute | Counter::SourceAddressNotAssigned => Refused::NotConnected, + Counter::NoSourceAddress | Counter::NoRoute | Counter::SourceAddressNotAssigned | Counter::NotConnected => Refused::NotConnected, Counter::BindAddressNotLocal | Counter::SendLoopback | Counter::SendToSelf | Counter::SendInvalidDestination | Counter::SendPortZero | Counter::SendUnspecifiedDestination - | Counter::ExceedsMtu => Refused::InvalidInput, - Counter::BroadcastNotPermitted => Refused::PermissionDenied, - Counter::TxQueueFull => Refused::ResourceExhausted, - Counter::ConnectUnspecified + | Counter::ExceedsMtu + | Counter::ConnectUnspecified | Counter::ConnectGroup | Counter::ConnectPortZero - | Counter::ConnectedDestinationMismatch - | Counter::NotConnected - | Counter::SourceNotPermitted + | Counter::ConnectedDestinationMismatch => Refused::InvalidInput, + Counter::BroadcastNotPermitted => Refused::PermissionDenied, + Counter::TxQueueFull => Refused::ResourceExhausted, + Counter::SourceNotPermitted | Counter::Rx | Counter::RxDelivered | Counter::RxNoSocket @@ -139,14 +145,18 @@ impl Node { Ok((DatagramId(id), port)) } - /// A holder more of socket `id`, told on `bell`, which takes a place: the socket's port. + /// A holder more of socket `id`, told on `bell` at once of a datagram that waits, which takes + /// a place: the socket's port. pub fn export_datagram(&mut self, id: DatagramId, bell: Box) -> Result<(HolderId, Port), ExportRefused> { if self.room() == 0 { return Err(ExportRefused::Full); } let holders = self.datagrams.get_mut(&id).ok_or(ExportRefused::Gone)?; + let holder = holders.add(bell); let Ok(port) = self.stack.port(id.0) else { unreachable!("[udp] lost a socket the node holds") }; - Ok((holders.add(bell), port)) + // Told now of what waits: no pass need come before its first look. + self.tell_holders(); + Ok((holder, port)) } /// Whether the socket's datagrams may go to a broadcast address, the limited one or the @@ -160,6 +170,28 @@ impl Node { self.stack.broadcast(id.0).map_err(refused) } + /// Connects the socket to `peer:port` (POSIX's `connect`): from here its datagrams come only + /// from there, and [`Self::udp_send`] goes there, for every holder alike. Datagrams already + /// received from anyone else are dropped. + pub fn udp_connect(&mut self, now: Instant, id: DatagramId, peer: Ipv4Addr, port: u16) -> Result<(), Refused> { + let connected = self.stack.connect(now, id.0, peer, port).map_err(refused); + self.log(now); + connected + } + + /// The peer [`Self::udp_connect`] named, if any. + pub fn udp_peer(&mut self, id: DatagramId) -> Result, Refused> { + self.stack.peer(id.0).map_err(refused) + } + + /// Queues `payload` for the connected socket's peer; one not connected is refused + /// [`Refused::NotConnected`]. + pub fn udp_send(&mut self, now: Instant, id: DatagramId, payload: &[u8]) -> Result<(), Refused> { + let sent = self.stack.send_connected(now, id.0, payload).map_err(refused); + self.log(now); + sent + } + /// Queues `payload` for `destination:port`; it leaves in a later [`Node::transmit`]. pub fn udp_send_to(&mut self, now: Instant, id: DatagramId, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), Refused> { let sent = self.stack.send_to(now, id.0, destination, port, payload).map_err(refused); @@ -205,11 +237,11 @@ impl Node { self.datagrams.values().map(Holders::more).fold(self.datagrams.len(), usize::saturating_add) } - /// Tells each holder of a socket a datagram waits for. + /// Tells each holder of a socket a datagram, or its pending error, waits for. pub(crate) fn tell_datagram_holders(&mut self) { for (id, holders) in &mut self.datagrams { - let Ok(waiting) = self.stack.waiting(id.0) else { unreachable!("[udp] lost a socket the node holds") }; - holders.tell(waiting > 0); + let Ok(readable) = self.stack.readable(id.0) else { unreachable!("[udp] lost a socket the node holds") }; + holders.tell(readable); } } } diff --git a/system/netstack/node/src/lease.rs b/system/netstack/node/src/lease.rs index 08b6b2b3fc3..f063d6a9315 100644 --- a/system/netstack/node/src/lease.rs +++ b/system/netstack/node/src/lease.rs @@ -214,8 +214,17 @@ impl Stack { self.shard.udp_port(id) } - pub(crate) fn waiting(&mut self, id: SocketId) -> Result { - self.shard.udp_waiting(id) + pub(crate) fn readable(&mut self, id: SocketId) -> Result { + self.shard.udp_readable(id) + } + + pub(crate) fn peer(&mut self, id: SocketId) -> Result, toyos_net_udp::Error> { + self.shard.udp_peer(id) + } + + /// Queues a datagram for a connected socket's peer. + pub(crate) fn send_connected(&mut self, now: Instant, id: SocketId, payload: &[u8]) -> Result<(), toyos_net_udp::Error> { + self.shard.udp_send(now, id, payload) } pub(crate) fn broadcast(&mut self, id: SocketId) -> Result { diff --git a/system/netstack/node/src/listeners.rs b/system/netstack/node/src/listeners.rs index 71d542bcdc4..1a4be4c7f78 100644 --- a/system/netstack/node/src/listeners.rs +++ b/system/netstack/node/src/listeners.rs @@ -136,13 +136,17 @@ impl Node { Ok((id, port)) } - /// A holder more of listener `id`, told on `bell`, which takes a place: the listener's port. + /// A holder more of listener `id`, told on `bell` at once of a connection that waits, which + /// takes a place: the listener's port. pub fn export_listener(&mut self, id: ListenerId, bell: Box) -> Result<(HolderId, Port), ExportRefused> { if self.room() == 0 { return Err(ExportRefused::Full); } let listener = self.listeners.live.get_mut(&id).ok_or(ExportRefused::Gone)?; - Ok((listener.holders.add(bell), listener.port)) + let made = (listener.holders.add(bell), listener.port); + // Told now of what waits: no pass need come before its first look. + self.tell_holders(); + Ok(made) } /// `holder` of listener `id` left; the last holder's leaving closes it. Either gives a place diff --git a/system/netstack/node/tests/datagram.rs b/system/netstack/node/tests/datagram.rs index 3ee07186187..e20f5b4c95d 100644 --- a/system/netstack/node/tests/datagram.rs +++ b/system/netstack/node/tests/datagram.rs @@ -264,6 +264,45 @@ fn a_refusal_is_logged_by_the_call_that_met_it() { assert_eq!(logged, [line(Counter::BroadcastNotPermitted, (Ipv4Addr::BROADCAST, 9), 1)]); } +// RFC 768 has no connection; POSIX's `connect` on a datagram socket names the one peer it sends +// to and receives from. The peer is the socket's, so every holder's: a holder that did not +// connect sends there and reads it back, and a datagram from anyone else is no holder's. +#[test] +fn a_connected_sockets_peer_is_every_holders() { + let mut lan = Lan::new(); + lan.lease(3_600); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); + let (second, _) = lan.node.export_datagram(id, common::unread()).unwrap(); + assert_eq!((lan.node.udp_peer(id), lan.node.udp_send(lan.now, id, b"x")), (Ok(None), Err(Refused::NotConnected))); + assert_eq!(lan.node.udp_connect(lan.now, id, B, 0), Err(Refused::InvalidInput), "port 0"); + lan.node.udp_connect(lan.now, id, B, 7).expect("one host of the link"); + assert_eq!(lan.node.udp_peer(id), Ok(Some((B, Port::new(7).unwrap())))); + lan.node.udp_send(lan.now, id, b"to the peer").expect("a connected socket's send"); + lan.pump(); + let last = lan.datagrams().last().map(|(_, udp)| (udp.destination, udp.port, udp.payload.clone())); + assert_eq!(last, Some((B, 7, b"to the peer".to_vec()))); + + lan.deliver(&udp(MAC, MAC_R, (OFF_LINK, 7), (A, 4_000), b"a stranger")); + lan.deliver(&udp(MAC, MAC_B, (B, 7), (A, 4_000), b"the peer")); + let mut out = [0u8; 64]; + let got = lan.node.udp_recv_from(id, second, &mut out).unwrap().unwrap(); + assert_eq!((&out[..got.len], got.source), (&b"the peer"[..], B), "the holder that did not connect reads the peer"); + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Ok(None), "the stranger's is nobody's"); +} + +// A holder made while a datagram waits is rung at once: nothing promises a pass before its +// first look. +#[test] +fn a_holder_made_while_a_datagram_waits_is_rung_at_once() { + let mut lan = Lan::new(); + lan.lease(3_600); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), common::unread(), undrawn).unwrap(); + lan.deliver(&udp(MAC, MAC_B, (B, 9_999), (A, 4_000), b"waiting")); + let (bell, rings) = common::bell(); + lan.node.export_datagram(id, bell).unwrap(); + assert_eq!(rings.get(), 1); +} + // A socket's holders take from one queue: each datagram is handed whole to the one holder whose // receive takes it, each holder is rung once a look while one waits, and a holder that left // receives nothing. The socket and its port stand until the last holder leaves. diff --git a/system/netstack/node/tests/listeners.rs b/system/netstack/node/tests/listeners.rs index b239666d62b..6b9b180bdcf 100644 --- a/system/netstack/node/tests/listeners.rs +++ b/system/netstack/node/tests/listeners.rs @@ -597,6 +597,18 @@ fn a_listener_ends_with_its_last_holder() { net.listen(SSH); } +// A holder made while a connection waits is rung at once: nothing promises a pass before its +// first look. +#[test] +fn a_holder_made_while_a_connection_waits_is_rung_at_once() { + let mut net = Net::new(); + let (id, _first) = net.listen(SSH); + net.handshake(P1); + let (second, owner) = net.export(id); + assert_eq!(wakes(&owner), 1); + net.accepts_as(id, second, P1); +} + #[test] fn an_export_past_the_places_makes_no_holder() { let mut net = with_places(1); diff --git a/system/netstack/shard/src/lib.rs b/system/netstack/shard/src/lib.rs index 5bd3a8fa89a..a5df372ac4d 100644 --- a/system/netstack/shard/src/lib.rs +++ b/system/netstack/shard/src/lib.rs @@ -623,9 +623,24 @@ impl Shard { self.udp.recv(id, out) } - /// [`Udp::waiting`](toyos_net_udp::Udp::waiting). - pub fn udp_waiting(&mut self, id: SocketId) -> Result { - self.udp.waiting(id) + /// [`Udp::readable`](toyos_net_udp::Udp::readable). + pub fn udp_readable(&mut self, id: SocketId) -> Result { + self.udp.readable(id) + } + + /// The peer a connected socket's datagrams go to and come from. + pub fn udp_peer(&mut self, id: SocketId) -> Result, toyos_net_udp::Error> { + self.udp.binding(id).map(|(binding, _)| match binding { + toyos_net_udp::Binding::Connected { peer, peer_port, .. } => Some((peer, peer_port)), + toyos_net_udp::Binding::Any | toyos_net_udp::Binding::Specific(_) => None, + }) + } + + /// Queues a datagram for a connected socket's peer; accepted means queued, not sent. + pub fn udp_send(&mut self, now: Instant, id: SocketId, payload: &[u8]) -> Result<(), toyos_net_udp::Error> { + let queued = self.udp.send(&mut self.ip, id, payload); + self.settle(now); + queued } /// [`Udp::broadcast`](toyos_net_udp::Udp::broadcast). diff --git a/system/netstack/src/serve.rs b/system/netstack/src/serve.rs index 1f31824bc7d..204628b9c03 100644 --- a/system/netstack/src/serve.rs +++ b/system/netstack/src/serve.rs @@ -48,7 +48,7 @@ use std::time::Duration; use std::rc::{Rc, Weak}; use toyos::net::*; -use toyos::net::socket::{self, Kind, ACCEPT, OPTION, RECEIVE, SEND, SET_OPTION}; +use toyos::net::socket::{self, Kind, ACCEPT, CONNECT, OPTION, PEER, RECEIVE, SEND, SEND_PEER, SET_OPTION}; use toyos::net::stream::{self, Adopt, ADOPT, BELLS, EXPORT, EXPORTED, REFUSED, RING}; use toyos::transport::session::Holder; use toyos::poller::READABLE; @@ -229,6 +229,9 @@ fn refused(refusal: Refused) -> u32 { Refused::InvalidInput => ERR_INVALID_INPUT, Refused::PermissionDenied => ERR_PERMISSION_DENIED, Refused::ResourceExhausted => ERR_RESOURCE_EXHAUSTED, + Refused::ConnectionRefused => ERR_CONNECTION_REFUSED, + // The pipe ABI's word for an ICMP error a connect ends in, as `connect_failed`'s. + Refused::Unreachable => ERR_OTHER, } } @@ -577,14 +580,14 @@ impl Sockets { /// for a token that is none of them. An answer about a socket or a stream /// that is gone since says nothing. pub fn answered(&mut self, node: &mut Node, now: Instant, token: u64, answer: Result) -> bool { - let socket_id = token as u32; + let key = token as u32; match token & TOKEN_KIND { - TOKEN_STREAM => self.holder_answered(node, now, socket_id, answer), - TOKEN_SOCKET => self.socket_answered(node, now, socket_id, answer), + TOKEN_STREAM => self.holder_answered(node, now, key, answer), + TOKEN_SOCKET => self.socket_answered(node, now, key, answer), TOKEN_LOOKUP => { // A lookup whose client has left ends now: its sockets and // its place are another client's. - let spoke = |client: &Client| u64::from(client.conn.as_handle().0) == u64::from(socket_id); + let spoke = |client: &Client| u64::from(client.conn.as_handle().0) == u64::from(key); self.lookups.retain(|(id, client)| { let left = spoke(client) && client.gone(); if left { @@ -735,6 +738,7 @@ impl Sockets { let sent = match reply { Ok(Reply::Bare) => holder.connection.try_signal(RespType::Result as u32), Ok(Reply::Word(word)) => holder.connection.try_send(RespType::Result as u32, &word), + Ok(Reply::Peer(peer)) => holder.connection.try_send(RespType::Result as u32, &peer), Ok(Reply::Bytes(bytes)) => holder.connection.try_send_bytes(RespType::Result as u32, &bytes), Err(code) => holder.connection.try_send(RespType::Error as u32, &ErrorResponse { code }), }; @@ -937,6 +941,9 @@ enum Asked { Option(u32), Send { to: [u8; 4], port: u16, data: Vec }, Receive, + Connect { to: [u8; 4], port: u16 }, + Peer, + SendPeer(Vec), } /// The request a holder of a `kind` socket sent, or `None` for a frame no @@ -955,6 +962,12 @@ fn asked(kind: Kind, msg_type: u32, payload: &[u8], handles: usize) -> Option Some(Asked::Receive), + (Kind::Datagram, CONNECT, 0) => { + let peer: socket::Datagram = ipc::decode_payload(payload).ok()?; + Some(Asked::Connect { to: peer.addr, port: peer.port }) + } + (Kind::Datagram, PEER, 0) if payload.is_empty() => Some(Asked::Peer), + (Kind::Datagram, SEND_PEER, 0) if payload.len() <= socket::MAX_DATAGRAM => Some(Asked::SendPeer(payload.to_vec())), _ => None, } } @@ -963,6 +976,7 @@ fn asked(kind: Kind, msg_type: u32, payload: &[u8], handles: usize) -> Option), } @@ -981,6 +995,14 @@ fn answer_socket(node: &mut Node, now: Instant, socket: Held, holder: HolderId, (Held::Datagram(id), Asked::Send { to, port, data }) => { node.udp_send_to(now, id, Ipv4Addr::from(to), port, &data).map(|()| Reply::Bare).map_err(refused) } + (Held::Datagram(id), Asked::Connect { to, port }) => { + node.udp_connect(now, id, Ipv4Addr::from(to), port).map(|()| Reply::Bare).map_err(refused) + } + (Held::Datagram(id), Asked::Peer) => match node.udp_peer(id).map_err(refused)? { + Some((addr, port)) => Ok(Reply::Peer(socket::Datagram { addr: addr.octets(), port: port.get(), _pad: 0 })), + None => Err(ERR_NOT_CONNECTED), + }, + (Held::Datagram(id), Asked::SendPeer(data)) => node.udp_send(now, id, &data).map(|()| Reply::Bare).map_err(refused), (Held::Datagram(id), Asked::Receive) => { let mut payload = [0u8; toyos_net_udp::limits::MAX_PAYLOAD]; match node.udp_recv_from(id, holder, &mut payload) { @@ -996,7 +1018,16 @@ fn answer_socket(node: &mut Node, now: Instant, socket: Held, holder: HolderId, Err(refusal) => Err(refused(refusal)), } } - (_, asked @ (Asked::Accept | Asked::Export | Asked::Send { .. } | Asked::Receive)) => { + ( + _, + asked @ (Asked::Accept + | Asked::Export + | Asked::Send { .. } + | Asked::Receive + | Asked::Connect { .. } + | Asked::Peer + | Asked::SendPeer(_)), + ) => { unreachable!("{asked:?} is answered by its own call, or was refused as no holder's of its kind") } } @@ -1022,7 +1053,7 @@ mod tests { /// there, so a connection that holds nothing reaches nothing. #[test] fn a_new_connection_names_no_socket() { - for holders in [ACCEPT, OPTION, SET_OPTION, SEND, RECEIVE, EXPORT, EXPORTED, REFUSED, RING, ADOPT, socket::ADOPT] { + for holders in [ACCEPT, OPTION, SET_OPTION, SEND, RECEIVE, CONNECT, PEER, SEND_PEER, EXPORT, EXPORTED, REFUSED, RING, ADOPT, socket::ADOPT] { assert_eq!(MsgType::from_u32(holders), None, "{holders}"); } } @@ -1042,6 +1073,9 @@ mod tests { } assert_eq!(asked(Kind::Datagram, SEND, &datagram, 0), Some(Asked::Send { to: [192, 0, 2, 7], port: 53, data: b"hi".to_vec() })); assert_eq!(asked(Kind::Datagram, RECEIVE, &[], 0), Some(Asked::Receive)); + assert_eq!(asked(Kind::Datagram, CONNECT, &datagram[..8], 0), Some(Asked::Connect { to: [192, 0, 2, 7], port: 53 })); + assert_eq!(asked(Kind::Datagram, PEER, &[], 0), Some(Asked::Peer)); + assert_eq!(asked(Kind::Datagram, SEND_PEER, b"hi", 0), Some(Asked::SendPeer(b"hi".to_vec()))); for (kind, msg_type, payload, handles) in [ (Kind::Datagram, ACCEPT, &[][..], 1), @@ -1059,6 +1093,13 @@ mod tests { (Kind::Datagram, SET_OPTION, &option[..7], 0), (Kind::Datagram, OPTION, &option[..], 0), (Kind::Listener, RING, &[], 0), + (Kind::Listener, CONNECT, &datagram[..8], 0), + (Kind::Listener, PEER, &[], 0), + (Kind::Listener, SEND_PEER, b"hi", 0), + (Kind::Datagram, CONNECT, &datagram[..], 0), + (Kind::Datagram, PEER, &[0], 0), + (Kind::Datagram, SEND_PEER, &[0; socket::MAX_DATAGRAM + 1], 0), + (Kind::Datagram, SEND_PEER, b"hi", 1), (Kind::Datagram, socket::ADOPT, &[0; 8], 1), (Kind::Listener, u32::MAX, &[], 0), ] { diff --git a/system/netstack/udp/src/lib.rs b/system/netstack/udp/src/lib.rs index 162cd885376..b948ef8619d 100644 --- a/system/netstack/udp/src/lib.rs +++ b/system/netstack/udp/src/lib.rs @@ -433,9 +433,10 @@ impl Udp { self.socket(id).map(|s| s.broadcast) } - /// The datagrams waiting for a [`Self::recv`]. - pub fn waiting(&mut self, id: SocketId) -> Result { - self.socket(id).map(|s| s.rx.len()) + /// Whether a [`Self::recv`] has something to answer: a datagram, or the socket's pending + /// error. + pub fn readable(&mut self, id: SocketId) -> Result { + self.socket(id).map(|s| !s.rx.is_empty() || s.pending.is_some()) } pub fn set_ttl(&mut self, id: SocketId, unicast: Ttl, multicast: Ttl) -> Result<(), Error> { diff --git a/tests/guest/rust-tests/src/bin/socket_export.rs b/tests/guest/rust-tests/src/bin/socket_export.rs index 07263d05d94..8c580c3fc3a 100644 --- a/tests/guest/rust-tests/src/bin/socket_export.rs +++ b/tests/guest/rust-tests/src/bin/socket_export.rs @@ -10,10 +10,15 @@ //! each); the child says it is ready in a datagram, is told to go in one, //! accepts the harness's connection and writes a line on it. Once the child //! has ended, the parent does the same through its own holders, which still -//! hold both sockets. Every step waits on the peer's next datagram or -//! connection, under the harness's ceiling. +//! hold both sockets. +//! +//! **Then the two race.** The parent spawns a second child, and both block in +//! an accept and then in a receive on the one queue each socket is: the harness +//! connects twice and sends two datagrams, and each process takes exactly one +//! of each, whichever loses a race waiting again on its bell. Every step waits +//! on the peer's next datagram or connection, under the harness's ceiling. -use std::io::Write; +use std::io::{BufRead, BufReader, Write}; use std::net::{Ipv4Addr, SocketAddr, TcpListener, UdpSocket}; use std::os::fd::FromRawFd; use std::os::toyos::io::AsRawHandle; @@ -36,32 +41,65 @@ const DATAGRAM_FD: i32 = 4; fn main() { let mut args = std::env::args().skip(1); let port: u16 = match args.next().as_deref() { - Some("child") => { + Some(role @ ("child" | "racer")) => { let port = args.next().expect("the harness's port"); - return child(port.parse().unwrap_or_else(|e| panic!("a port, not {port:?}: {e}"))); + let port = port.parse().unwrap_or_else(|e| panic!("a port, not {port:?}: {e}")); + // SAFETY: the descriptors its parent gave it, a holder of each socket. + let (listener, socket) = unsafe { (TcpListener::from_raw_fd(LISTENER_FD), UdpSocket::from_raw_fd(DATAGRAM_FD)) }; + match role { + "child" => serve("child", &listener, &socket, port), + _ => println!("{}", race("racer", &listener, &socket, port)), + } + return; } Some(port) => port.parse().unwrap_or_else(|e| panic!("a port, not {port:?}: {e}")), None => panic!("the harness's datagram port is this job's argument"), }; let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, LISTENING)).expect("a listener through netstack"); let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).expect("a datagram socket through netstack"); - let status = Command::new(SELF_PATH) - .arg("child") - .arg(port.to_string()) - .inherit_handle(LISTENER_FD as u32, listener.as_raw_handle()) - .inherit_handle(DATAGRAM_FD as u32, socket.as_raw_handle()) - .stdout(Stdio::null()) - .status() - .expect("spawn the child with a holder of each socket"); + let spawn = |role: &str| { + Command::new(SELF_PATH) + .arg(role) + .arg(port.to_string()) + .inherit_handle(LISTENER_FD as u32, listener.as_raw_handle()) + .inherit_handle(DATAGRAM_FD as u32, socket.as_raw_handle()) + .stdout(Stdio::piped()) + .spawn() + .unwrap_or_else(|e| panic!("spawn the {role} with a holder of each socket: {e}")) + }; + let status = spawn("child").wait().expect("wait for the child"); assert!(status.success(), "the child ended {status:?}"); serve("parent", &listener, &socket, port); println!("a listener and a datagram socket exported to a child: it accepted and spoke on them, and so did the parent after it"); + + let mut racer = spawn("racer"); + let mine = race("parent", &listener, &socket, port); + let mut theirs = String::new(); + BufReader::new(racer.stdout.take().expect("the racer's stdout")).read_line(&mut theirs).expect("the racer's datagram"); + assert!(racer.wait().expect("wait for the racer").success(), "the racer failed"); + let mut taken = [mine, theirs.trim().to_string()]; + taken.sort(); + assert_eq!(taken, ["one", "two"], "each process takes one datagram of the two"); + println!("two holders blocked on one queue: each took one connection and one datagram"); } -fn child(port: u16) { - // SAFETY: the descriptors its parent gave it, a holder of each socket. - let (listener, socket) = unsafe { (TcpListener::from_raw_fd(LISTENER_FD), UdpSocket::from_raw_fd(DATAGRAM_FD)) }; - serve("child", &listener, &socket, port); +/// `who`'s part of the race: say it is about to accept, accept one connection +/// and write a line on it, say it is about to receive, and receive one +/// datagram, whose bytes it answers. +fn race(who: &str, listener: &TcpListener, socket: &UdpSocket, port: u16) -> String { + let peer = SocketAddr::from((HOST, port)); + let say = |what: &str| { + let line = format!("{who} {what}"); + socket.send_to(line.as_bytes(), peer).unwrap_or_else(|e| panic!("{who}'s datagram: {e}")); + }; + say("racing"); + let (mut stream, _) = listener.accept().unwrap_or_else(|e| panic!("{who}'s accept in the race: {e}")); + writeln!(stream, "accepted by a racer").unwrap_or_else(|e| panic!("{who}'s line: {e}")); + drop(stream); + say("receiving"); + let mut heard = [0u8; 64]; + let (n, _) = socket.recv_from(&mut heard).unwrap_or_else(|e| panic!("{who}'s receive in the race: {e}")); + String::from_utf8_lossy(&heard[..n]).into_owned() } /// `who`'s turn: ready said and go heard in datagrams, then one connection diff --git a/tests/toyos.rs b/tests/toyos.rs index e074cf712ba..3d3aa5927c6 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5166,7 +5166,8 @@ fn netstack_holders_exported_to_children() -> Result<(), String> { let bins: Vec<(String, Vec)> = ["stream_export", "socket_export"].iter().map(|job| (job.to_string(), qemu::build_toyos_bin(qemu::SUITE_ARCH, &tests, job))).collect(); // The host's end of the forward: a port found free and given up, which - // QEMU binds as it starts and refuses to start without. + // QEMU binds as it starts and refuses to start without. Another process + // can take it between the two, which fails the boot by name. let forwarded = std::net::TcpListener::bind("127.0.0.1:0") .and_then(|probe| probe.local_addr()) .map_err(|e| format!("no loopback port to forward: {e}"))? @@ -5213,7 +5214,9 @@ fn stream_exported_to_a_child(qemu: &mut QemuInstance) -> Result<(), String> { /// `test_rs_socket_export`: for the child and then the parent, a datagram /// saying it is ready, answered by one saying go, and a connection through the -/// forwarded port that it accepts and writes its line on. +/// forwarded port that it accepts and writes its line on; then, once the parent +/// and a second child both say they are about to block, two connections and +/// two datagrams, each taken once. fn sockets_exported_to_a_child(qemu: &mut QemuInstance, forwarded: u16) -> Result<(), String> { use std::io::Read; /// A liveness ceiling on each of the peer's waits, never a timing. @@ -5238,6 +5241,39 @@ fn sockets_exported_to_a_child(qemu: &mut QemuInstance, forwarded: u16) -> Resul return Err(format!("the peer heard {:?}, not {owed:?}", String::from_utf8_lossy(&line))); } } + // The race: both processes say they are about to block, then two + // connections and two datagrams, each taken once. + let both = |what: &str| -> Result { + let mut said = Vec::new(); + let mut from = None; + for _ in 0..2 { + let mut line = [0u8; 64]; + let (n, at) = datagrams.recv_from(&mut line).map_err(|e| format!("a racer's {what}: {e}"))?; + said.push(String::from_utf8_lossy(&line[..n]).into_owned()); + from = Some(at); + } + said.sort(); + if said != [format!("parent {what}"), format!("racer {what}")] { + return Err(format!("the racers said {said:?}")); + } + from.ok_or_else(|| "no racer spoke".to_string()) + }; + both("racing")?; + let conns: Vec = (0..2) + .map(|_| std::net::TcpStream::connect(("127.0.0.1", forwarded)).map_err(|e| format!("a racer's connect: {e}"))) + .collect::>()?; + for mut conn in conns { + conn.set_read_timeout(Some(CEILING)).map_err(|e| e.to_string())?; + let mut line = Vec::new(); + conn.read_to_end(&mut line).map_err(|e| format!("a racer's line: {e} after {line:?}"))?; + if line != b"accepted by a racer\n" { + return Err(format!("a racer wrote {:?}", String::from_utf8_lossy(&line))); + } + } + let socket = both("receiving")?; + for datagram in [&b"one"[..], b"two"] { + datagrams.send_to(datagram, socket).map_err(|e| format!("a racer's datagram: {e}"))?; + } Ok(()) }); let result = qemu.run_test(&format!("test_rs_socket_export {port}"), Duration::from_secs(300)); @@ -5247,6 +5283,14 @@ fn sockets_exported_to_a_child(qemu: &mut QemuInstance, forwarded: u16) -> Resul if result.exit_code != Some(0) { return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); } + for claim in [ + "a listener and a datagram socket exported to a child: it accepted and spoke on them, and so did the parent after it", + "two holders blocked on one queue: each took one connection and one datagram", + ] { + if !result.stdout.contains(claim) { + return Err(format!("the job never said {claim:?}:\n{}", result.stdout)); + } + } heard.join().map_err(|_| "the peer's thread panicked".to_string())? } From 032114b1d83a95b77b486fe20d929009b92122c8 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 23 Oct 2026 23:26:21 +0200 Subject: [PATCH 3/4] review r2: a ring stays in the bell until the request goes, and a receive'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 --- ...as-not-bound-is-not-shared-with-a-child.md | 2 +- sdk/toyos/src/net/socket.rs | 65 +++++++++---------- sdk/toyos/src/net/stream.rs | 10 +-- system/netstack/node/src/datagram.rs | 5 +- system/netstack/node/tests/datagram.rs | 28 +++++++- system/netstack/node/tests/lan/mod.rs | 26 ++++++++ system/netstack/node/tests/resolve.rs | 30 +-------- system/netstack/src/serve.rs | 2 +- 8 files changed, 94 insertions(+), 74 deletions(-) diff --git a/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md b/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md index b24c13b8eb0..01020cd52ee 100644 --- a/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md +++ b/issues/a-socket-libc-or-socket2-has-not-bound-is-not-shared-with-a-child.md @@ -16,7 +16,7 @@ process: `endow` and `provide` of one are refused with `Misuse::Moved`. `netstack_holders_exported_to_children` (`tests/toyos.rs`) reads a listener's and a datagram socket's holders in a parent and its child, one after the other -and both blocked at once; no guest test reads a survivor after the other holder +and both waiting on one queue; no guest test reads a survivor after the other holder is killed. What is not shared: diff --git a/sdk/toyos/src/net/socket.rs b/sdk/toyos/src/net/socket.rs index 4cb41b6c750..7c52290774f 100644 --- a/sdk/toyos/src/net/socket.rs +++ b/sdk/toyos/src/net/socket.rs @@ -18,11 +18,12 @@ //! //! **The bell says "look"**: netstack rings a holder while a connection, a datagram or a //! datagram socket's pending error waits and the holder has not asked since it was last rung, -//! a new holder at once. So an accept and a receive ask only once the bell rang: one that finds -//! it empty waits, or answers [`NetError::WouldBlock`], with no request; one that read a ring -//! asks, and what another holder took first is answered `WouldBlock` too. A ring read by a call -//! that failed before its request went is kept for the next, so none is lost. Nothing else -//! reads the bell. +//! a new holder at once. So an accept and a receive ask only while the bell is readable: one that +//! finds it empty waits, or answers [`NetError::WouldBlock`], with no request; one that finds it +//! rung makes what its request needs and reads the bell empty just before the request goes, +//! and what another holder took first is answered `WouldBlock` too. A call that fails before +//! its request leaves the ring in the bell, where a poller still sees it, so none is lost. +//! Nothing else reads the bell. //! //! **A handle with no end here is refused** [`NetError::NotASocket`]. A descriptor's handle is //! a copy of its end's bell, so a poller watches it for reading; the end keeps its own @@ -31,8 +32,6 @@ //! connection. A listener's connection keeps it, since the kernel carries an accept's region //! only on a connection that has it; no descriptor names that handle, so nothing hands it on. -use core::sync::atomic::{AtomicBool, Ordering}; - use toyos_abi::handle::Rights; use toyos_abi::syscall; use toyos_abi::RawHandle; @@ -168,9 +167,6 @@ static TABLE: Lock> = Lock::new(Ends::new()); /// Each end's turn on its connection: one request and its answer at a time. static TURNS: [Lock<()>; MAX_SOCKETS] = [const { Lock::new(()) }; MAX_SOCKETS]; -/// Each end read a ring and has not yet sent the request it owes: kept by a look that failed -/// before its request went, so the next look asks. Read and written under the end's turn. -static RUNG: [AtomicBool; MAX_SOCKETS] = [const { AtomicBool::new(false) }; MAX_SOCKETS]; /// Lets go of an end no handle or call reaches: its connection, which is its holder's leaving, /// and its bell. @@ -195,31 +191,32 @@ fn call_on(handle: RawHandle, kind: Kind, f: impl FnOnce(usize, End) -> Resul call(handle, |slot, end| if end.kind == kind { f(slot, end) } else { Err(NetError::InvalidInput) }) } -/// The rings `bell` holds, read: whether it rang, or hung up with netstack. -fn drain(bell: RawHandle) -> bool { +/// The rings `bell` holds, read: a request follows at once. +fn drain(bell: RawHandle) { let mut bytes = [0u8; 64]; - let mut rang = false; - loop { - match syscall::read_nonblock(bell, &mut bytes) { - Ok(0) => return true, - Ok(_) => rang = true, - Err(_) => return rang, - } - } + while let Ok(1..) = syscall::read_nonblock(bell, &mut bytes) {} } -/// `ask` once the end's bell rang, until it takes something, waiting for the bell between as -/// `wait` says. `ask` is handed what it calls once its request went. +/// Whether `bell` is readable, waiting for it at most `nanos`: rung, or hung up with netstack. +/// Nothing is read. +fn rung(bell: RawHandle, nanos: u64) -> bool { + let poller = crate::poller::Poller::new(1); + poller.watch_raw(bell, crate::poller::READABLE, 0); + let mut answered = false; + poller.wait(1, nanos, |_| answered = true); + answered +} + +/// `ask` while the end's bell is rung, until it takes something, waiting for the bell between +/// as `wait` says. `ask` is handed [`drain`] for the bell, which it calls just before its +/// request goes. fn look(slot: usize, end: End, wait: Wait, mut ask: impl FnMut(&dyn Fn()) -> Result) -> Result { - let rung = &RUNG[slot]; + let drained = || drain(end.bell); loop { { let _turn = TURNS[slot].lock(); - if drain(end.bell) { - rung.store(true, Ordering::Relaxed); - } - if rung.load(Ordering::Relaxed) { - match ask(&|| rung.store(false, Ordering::Relaxed)) { + if rung(end.bell, 0) { + match ask(&drained) { Err(NetError::WouldBlock) => {} answer => return answer, } @@ -234,9 +231,7 @@ fn look(slot: usize, end: End, wait: Wait, mut ask: impl FnMut(&dyn Fn()) -> return Err(NetError::TimedOut); } // Netstack gone hangs the bell up, which is readable, and the next look says so. - let poller = crate::poller::Poller::new(1); - poller.watch_raw(end.bell, crate::poller::READABLE, 0); - poller.wait(1, left, |_| {}); + rung(end.bell, left); } } @@ -277,7 +272,6 @@ fn open(kind: Kind, addr: [u8; 4], ask: impl FnOnce(NetstackConn) -> Result<(Bou let mut table = TABLE.lock(); match made { Ok((handle, end)) => { - RUNG[slot].store(false, Ordering::Relaxed); table.enter(slot, end, handle); Ok(Bound { handle, port: end.port }) } @@ -357,7 +351,7 @@ pub fn let_go(handle: RawHandle) { /// The oldest connection waiting at the listener `handle` reaches, as a stream: waiting for one /// as `wait` says, [`NetError::WouldBlock`] or [`NetError::TimedOut`] where none came. pub fn accept(handle: RawHandle, wait: Wait) -> Result { - call_on(handle, Kind::Listener, |slot, end| look(slot, end, wait, |asked| stream::accept(end.connection, asked))) + call_on(handle, Kind::Listener, |slot, end| look(slot, end, wait, |drain| stream::accept(end.connection, drain))) } // ---- a datagram socket's ---- @@ -387,9 +381,9 @@ pub fn send_to(handle: RawHandle, addr: [u8; 4], port: u16, data: &[u8]) -> Resu /// `wait` says, [`NetError::WouldBlock`] or [`NetError::TimedOut`] where none came. pub fn recv_from(handle: RawHandle, buf: &mut [u8], wait: Wait) -> Result { call_on(handle, Kind::Datagram, |slot, end| { - look(slot, end, wait, |asked| { + look(slot, end, wait, |drain| { + drain(); ipc::signal(end.connection, RECEIVE).map_err(hangup)?; - asked(); let header = answer(end.connection)?; let mut frame = [0u8; MAX_FRAME]; let n = ipc::recv_bytes(end.connection, &header, &mut frame).map_err(hangup)?; @@ -503,7 +497,6 @@ pub(super) fn adopted(connection: Connection, payload: &[u8], handles: ipc::Batc let mut table = TABLE.lock(); match made { Ok((descriptor, connection, bell)) => { - RUNG[slot].store(false, Ordering::Relaxed); table.enter(slot, End { kind, connection, bell, addr: adopt.addr, port: adopt.port }, descriptor); Some(descriptor) } diff --git a/sdk/toyos/src/net/stream.rs b/sdk/toyos/src/net/stream.rs index daace7b1ea8..1c3f74fc1b5 100644 --- a/sdk/toyos/src/net/stream.rs +++ b/sdk/toyos/src/net/stream.rs @@ -744,13 +744,13 @@ pub struct Accepted { /// The oldest connection waiting at the listener whose holder's connection is /// `listener`, as a stream: its region lent with a `super::socket::ACCEPT` and -/// its connection and bells in the answer, `asked` called once the request -/// went. Refused as [`connect`] is, and [`NetError::WouldBlock`] where none -/// waits. -pub(super) fn accept(listener: RawHandle, asked: &dyn Fn()) -> Result { +/// its connection and bells in the answer, `drain` called just before the +/// request goes, once the slot and the region are made. Refused as [`connect`] +/// is, and [`NetError::WouldBlock`] where none waits. +pub(super) fn accept(listener: RawHandle, drain: &dyn Fn()) -> Result { let opened = open(false, ([0; 4], 0), |region| { + drain(); ipc::signal_with(listener, super::socket::ACCEPT, [region]).map_err(hangup)?; - asked(); let header = answer(listener)?; let accepted: TcpAcceptResponse = ipc::recv_payload(listener, &header).map_err(hangup)?; let mut handles = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES]; diff --git a/system/netstack/node/src/datagram.rs b/system/netstack/node/src/datagram.rs index 682ad693837..e6987a531d2 100644 --- a/system/netstack/node/src/datagram.rs +++ b/system/netstack/node/src/datagram.rs @@ -205,9 +205,10 @@ impl Node { if !self.datagrams.get_mut(&id).is_some_and(|holders| holders.asked(holder)) { return Err(Refused::NotConnected); } - let received = self.stack.recv_from(id.0, out).map_err(refused)?; + // Told whatever it answers: a pending error answered leaves the datagrams behind it. + let received = self.stack.recv_from(id.0, out); self.tell_datagram_holders(); - Ok(received.map(|received| Datagram { len: received.len, source: received.source, source_port: received.source_port })) + Ok(received.map_err(refused)?.map(|received| Datagram { len: received.len, source: received.source, source_port: received.source_port })) } /// `holder` of socket `id` left. The last holder's leaving closes the socket: its port is diff --git a/system/netstack/node/tests/datagram.rs b/system/netstack/node/tests/datagram.rs index e20f5b4c95d..a55603c654a 100644 --- a/system/netstack/node/tests/datagram.rs +++ b/system/netstack/node/tests/datagram.rs @@ -12,7 +12,7 @@ use std::net::Ipv4Addr; use std::time::Duration; use common::{from_server, message_of, terms, xid, A, ACK, BROADCAST, DNS, MAC, MAC_B, MAC_R, R}; -use lan::{udp, Lan, Seen, Udp, B, OFF_LINK}; +use lan::{udp, unreachable, Lan, Seen, Udp, B, OFF_LINK}; use toyos_net_node::{Datagram, Event, ExportRefused, HolderId, Refused}; use toyos_net_shard::Refusal; use toyos_net_udp::Counter; @@ -290,6 +290,32 @@ fn a_connected_sockets_peer_is_every_holders() { assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Ok(None), "the stranger's is nobody's"); } +// RFC 1122 §4.1.3.3: an ICMP port unreachable about a connected socket's datagram is reported +// to its user; POSIX's next call answers it once. A datagram from the peer that waits behind it +// is still the socket's, and the holder that took the error is rung again for it. +#[test] +fn a_pending_error_is_answered_once_and_the_datagram_behind_it_rings_again() { + const PORT_UNREACHABLE: u8 = 3; + let mut lan = Lan::new(); + lan.lease(3_600); + let (first, first_rings) = common::bell(); + let (id, _) = lan.node.udp_bind(ANY, port(4_000), first, undrawn).unwrap(); + let (second_bell, second_rings) = common::bell(); + let (second, _) = lan.node.export_datagram(id, second_bell).unwrap(); + lan.node.udp_connect(lan.now, id, B, 7).unwrap(); + lan.node.udp_send(lan.now, id, b"ping").unwrap(); + lan.pump(); + lan.deliver(&udp(MAC, MAC_B, (B, 7), (A, 4_000), b"pong")); + lan.deliver(&unreachable(PORT_UNREACHABLE, (B, MAC_B), (A, 4_000), (B, 7), 4)); + assert_eq!((first_rings.get(), second_rings.get()), (1, 1), "both told"); + + let mut out = [0u8; 64]; + assert_eq!(lan.node.udp_recv_from(id, HolderId::FIRST, &mut out), Err(Refused::ConnectionRefused)); + assert_eq!(first_rings.get(), 2, "the holder that took the error is rung for the datagram behind it"); + let got = lan.node.udp_recv_from(id, second, &mut out).unwrap().unwrap(); + assert_eq!(&out[..got.len], b"pong", "the error was answered once, and the datagram is still the socket's"); +} + // A holder made while a datagram waits is rung at once: nothing promises a pass before its // first look. #[test] diff --git a/system/netstack/node/tests/lan/mod.rs b/system/netstack/node/tests/lan/mod.rs index a1e5b3889a3..de4ec988d58 100644 --- a/system/netstack/node/tests/lan/mod.rs +++ b/system/netstack/node/tests/lan/mod.rs @@ -250,3 +250,29 @@ impl Lan { self.node.shard().udp_counters().get(counter) } } + +/// RFC 792's destination unreachable from `reporter` at `mac`: type 3 and `code`, the checksum, +/// four unused octets, then the internet header and the first 64 bits of the datagram it is +/// about. That datagram is spelled here as RFC 791 and RFC 768 lay out one from `source` to +/// `destination` carrying `length` octets: no options, DF, TTL 64, protocol 17. +pub fn unreachable(code: u8, (reporter, mac): (Ipv4Addr, [u8; 6]), source: (Ipv4Addr, u16), destination: (Ipv4Addr, u16), length: usize) -> Vec { + let datagram = u16::try_from(8 + length).unwrap().to_be_bytes(); + let total = u16::try_from(20 + 8 + length).unwrap().to_be_bytes(); + let mut quoted = vec![0x45, 0, total[0], total[1], 0, 0, 0x40, 0, 64, 17, 0, 0]; + quoted.extend_from_slice(&source.0.octets()); + quoted.extend_from_slice(&destination.0.octets()); + let check = sum("ed); + quoted[10..12].copy_from_slice(&check.to_be_bytes()); + quoted.extend_from_slice(&[&source.1.to_be_bytes()[..], &destination.1.to_be_bytes(), &datagram, &[0, 0]].concat()); + let mut icmp = vec![3, code, 0, 0, 0, 0, 0, 0]; + icmp.extend_from_slice("ed); + let check = sum(&icmp); + icmp[2..4].copy_from_slice(&check.to_be_bytes()); + let total = u16::try_from(20 + icmp.len()).unwrap().to_be_bytes(); + let mut ip = vec![0x45, 0, total[0], total[1], 0, 0, 0x40, 0, 64, 1, 0, 0]; + ip.extend_from_slice(&reporter.octets()); + ip.extend_from_slice(&source.0.octets()); + let check = sum(&ip); + ip[10..12].copy_from_slice(&check.to_be_bytes()); + [&MAC[..], &mac, &[0x08, 0x00], &ip, &icmp].concat() +} diff --git a/system/netstack/node/tests/resolve.rs b/system/netstack/node/tests/resolve.rs index ebadb851de7..2ccd9389f36 100644 --- a/system/netstack/node/tests/resolve.rs +++ b/system/netstack/node/tests/resolve.rs @@ -13,8 +13,8 @@ mod lan; use std::net::Ipv4Addr; use std::time::Duration; -use common::{arp, from_server, message, message_of, option, sum, terms, xid, A, ACK, BROADCAST, DISCOVER, DNS, MAC, MAC_B, MAC_R, NAK, OFFER, R, REQUEST}; -use lan::{udp, Lan, Seen, Udp, B}; +use common::{arp, from_server, message, message_of, option, terms, xid, A, ACK, BROADCAST, DISCOVER, DNS, MAC, MAC_B, MAC_R, NAK, OFFER, R, REQUEST}; +use lan::{udp, unreachable, Lan, Seen, Udp, B}; use toyos_dns::{Failure, Name, MAX_LOOKUPS, ROUNDS, WAIT_MS}; use toyos_net_node::HolderId; use toyos_net_node::{Counter, Ended, LookupId, NotStarted, Refused, Resolved, Resolving}; @@ -166,32 +166,6 @@ fn gives(query: &[u8], address: [u8; 4]) -> Vec { reply(query, RESPONSE, &[(TYPE_A, address.to_vec())]) } -/// RFC 792's destination unreachable from `reporter` at `mac`: type 3 and `code`, the checksum, -/// four unused octets, then the internet header and the first 64 bits of the datagram it is -/// about. That datagram is spelled here as RFC 791 and RFC 768 lay out one from `source` to -/// `destination` carrying `length` octets: no options, DF, TTL 64, protocol 17. -fn unreachable(code: u8, (reporter, mac): (Ipv4Addr, [u8; 6]), source: (Ipv4Addr, u16), destination: (Ipv4Addr, u16), length: usize) -> Vec { - let datagram = u16::try_from(8 + length).unwrap().to_be_bytes(); - let total = u16::try_from(20 + 8 + length).unwrap().to_be_bytes(); - let mut quoted = vec![0x45, 0, total[0], total[1], 0, 0, 0x40, 0, 64, 17, 0, 0]; - quoted.extend_from_slice(&source.0.octets()); - quoted.extend_from_slice(&destination.0.octets()); - let check = sum("ed); - quoted[10..12].copy_from_slice(&check.to_be_bytes()); - quoted.extend_from_slice(&[&source.1.to_be_bytes()[..], &destination.1.to_be_bytes(), &datagram, &[0, 0]].concat()); - let mut icmp = vec![3, code, 0, 0, 0, 0, 0, 0]; - icmp.extend_from_slice("ed); - let check = sum(&icmp); - icmp[2..4].copy_from_slice(&check.to_be_bytes()); - let total = u16::try_from(20 + icmp.len()).unwrap().to_be_bytes(); - let mut ip = vec![0x45, 0, total[0], total[1], 0, 0, 0x40, 0, 64, 1, 0, 0]; - ip.extend_from_slice(&reporter.octets()); - ip.extend_from_slice(&source.0.octets()); - let check = sum(&ip); - ip[10..12].copy_from_slice(&check.to_be_bytes()); - [&MAC[..], &mac, &[0x08, 0x00], &ip, &icmp].concat() -} - /// `code` from `reporter` about `query`, as the node sent it. fn reports(code: u8, reporter: (Ipv4Addr, [u8; 6]), query: &Udp) -> Vec { unreachable(code, reporter, (query.source, query.source_port), (query.destination, query.port), query.payload.len()) diff --git a/system/netstack/src/serve.rs b/system/netstack/src/serve.rs index 204628b9c03..607d80447e6 100644 --- a/system/netstack/src/serve.rs +++ b/system/netstack/src/serve.rs @@ -230,7 +230,7 @@ fn refused(refusal: Refused) -> u32 { Refused::PermissionDenied => ERR_PERMISSION_DENIED, Refused::ResourceExhausted => ERR_RESOURCE_EXHAUSTED, Refused::ConnectionRefused => ERR_CONNECTION_REFUSED, - // The pipe ABI's word for an ICMP error a connect ends in, as `connect_failed`'s. + // No finer word exists yet, as for a connect's (`connect_failed`). Refused::Unreachable => ERR_OTHER, } } From e5f90fd5f12387bf701241274e0f35063a7506c6 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 23 Oct 2026 23:50:50 +0200 Subject: [PATCH 4/4] review r3: a socket end looks at its bell with a poller made with it 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 --- ...-its-sysroot-can-wedge-on-its-own-locks.md | 11 ++++ sdk/toyos/src/net/socket.rs | 52 +++++++++++++------ sdk/toyos/src/poller.rs | 11 ++-- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md b/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md index 0bc7d7f9c71..f209e2a073a 100644 --- a/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md +++ b/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md @@ -35,6 +35,17 @@ fa7a35ac0091a05b — the holder left no readable note`, 930 s by 14:48:17, with `lsof` showing the key's lock file open twice in the harness and nowhere else. `--ci host` again ran beside it and had finished at 14:45:47. +It recurred on #1052's round 2, with nothing beside it: right after a `cargo +clean`, `cargo test --test toyos-build -- netstack_holders_exported_to_children +stream_holders libc_children iommu_virtio_platform` (harness pid 73228) took +the build lock exclusive to build `3c919ba2399d86e1` at 20:29:49 and finished +it at 20:31:15. Three of its tests ran and passed; `stream_holders`'s worker, +whose `stream_handles` build had begun at 20:29:45, waited on `using sysroot +3c919ba2399d86e1` from 20:29:49, first "held by pid 73228 (building sysroot +3c919ba2399d86e1)" and after the build "the holder left no readable note", for +1,290 s by 20:51:19, when the run was stopped. Alone, the same row passed in 8 s +(PR #1052's round 2 evidence, `guest-r2.log` and `guest-r2b.log`). + **Exit condition**: no path holds the worktree build lock while it waits on a key's lock, and a suite started on a head whose sysroot is not yet made finishes. diff --git a/sdk/toyos/src/net/socket.rs b/sdk/toyos/src/net/socket.rs index 7c52290774f..bd51e6a3bf0 100644 --- a/sdk/toyos/src/net/socket.rs +++ b/sdk/toyos/src/net/socket.rs @@ -22,8 +22,9 @@ //! finds it empty waits, or answers [`NetError::WouldBlock`], with no request; one that finds it //! rung makes what its request needs and reads the bell empty just before the request goes, //! and what another holder took first is answered `WouldBlock` too. A call that fails before -//! its request leaves the ring in the bell, where a poller still sees it, so none is lost. -//! Nothing else reads the bell. +//! its request goes leaves the ring in the bell, where a poller still sees it. Nothing else +//! reads the bell. The look is made with a poller of the end's own, made with the end, so a +//! call that does not wait asks the kernel for nothing it can refuse. //! //! **A handle with no end here is refused** [`NetError::NotASocket`]. A descriptor's handle is //! a copy of its end's bell, so a poller watches it for reading; the end keeps its own @@ -37,6 +38,7 @@ use toyos_abi::syscall; use toyos_abi::RawHandle; use crate::ipc::{self, Connection}; +use crate::poller::Poller; use crate::sync::Lock; use crate::OwnedHandle; @@ -164,8 +166,9 @@ struct End { static TABLE: Lock> = Lock::new(Ends::new()); -/// Each end's turn on its connection: one request and its answer at a time. -static TURNS: [Lock<()>; MAX_SOCKETS] = [const { Lock::new(()) }; MAX_SOCKETS]; +/// Each end's turn on its connection, one request and its answer at a time, and the poller its +/// look at its bell is made with: `Some` from the end's making until [`free`]. +static TURNS: [Lock>; MAX_SOCKETS] = [const { Lock::new(None) }; MAX_SOCKETS]; /// Lets go of an end no handle or call reaches: its connection, which is its holder's leaving, @@ -173,9 +176,16 @@ static TURNS: [Lock<()>; MAX_SOCKETS] = [const { Lock::new(()) }; MAX_SOCKETS]; fn free(slot: usize, end: End) { syscall::close(end.connection); syscall::close(end.bell); + *TURNS[slot].lock() = None; TABLE.lock().unreserve(slot); } +/// The poller a reserved slot's end looks at its bell with. Refused, nothing is made. +fn make_poller(slot: usize) -> Result<(), NetError> { + *TURNS[slot].lock() = Some(Poller::try_new(1).map_err(|_| NetError::ResourceExhausted)?); + Ok(()) +} + /// Runs `f` as a call through `handle`, its end kept until it returns. fn call(handle: RawHandle, f: impl FnOnce(usize, End) -> Result) -> Result { let (slot, end) = TABLE.lock().call(handle).ok_or(NetError::NotASocket)?; @@ -197,10 +207,9 @@ fn drain(bell: RawHandle) { while let Ok(1..) = syscall::read_nonblock(bell, &mut bytes) {} } -/// Whether `bell` is readable, waiting for it at most `nanos`: rung, or hung up with netstack. -/// Nothing is read. -fn rung(bell: RawHandle, nanos: u64) -> bool { - let poller = crate::poller::Poller::new(1); +/// Whether `bell` is readable, waiting for it at most `nanos` on `poller`: rung, or hung up +/// with netstack. Nothing is read. +fn rung(poller: &Poller, bell: RawHandle, nanos: u64) -> bool { poller.watch_raw(bell, crate::poller::READABLE, 0); let mut answered = false; poller.wait(1, nanos, |_| answered = true); @@ -214,8 +223,9 @@ fn look(slot: usize, end: End, wait: Wait, mut ask: impl FnMut(&dyn Fn()) -> let drained = || drain(end.bell); loop { { - let _turn = TURNS[slot].lock(); - if rung(end.bell, 0) { + let turn = TURNS[slot].lock(); + let Some(poller) = turn.as_ref() else { unreachable!("a live end has its poller") }; + if rung(poller, end.bell, 0) { match ask(&drained) { Err(NetError::WouldBlock) => {} answer => return answer, @@ -230,8 +240,11 @@ fn look(slot: usize, end: End, wait: Wait, mut ask: impl FnMut(&dyn Fn()) -> if left == 0 { return Err(NetError::TimedOut); } - // Netstack gone hangs the bell up, which is readable, and the next look says so. - rung(end.bell, left); + // Out of the turn, which the end's other calls take meanwhile, so on a poller of the + // wait's own. Netstack gone hangs the bell up, which is readable, and the next look says + // so. + let poller = Poller::try_new(1).map_err(|_| NetError::ResourceExhausted)?; + rung(&poller, end.bell, left); } } @@ -265,7 +278,7 @@ pub fn bind(addr: [u8; 4], port: u16) -> Result { /// [`MAX_SOCKETS`]. fn open(kind: Kind, addr: [u8; 4], ask: impl FnOnce(NetstackConn) -> Result<(BoundResponse, [RawHandle; 1], Connection), NetError>) -> Result { let slot = TABLE.lock().reserve().ok_or(NetError::ResourceExhausted)?; - let made = NetstackConn::connect().and_then(ask).and_then(|(bound, [bell], connection)| { + let made = make_poller(slot).and_then(|()| NetstackConn::connect()).and_then(ask).and_then(|(bound, [bell], connection)| { let (descriptor, connection, bell) = take_up(kind, connection.into_owned(), bell)?; Ok((descriptor, End { kind, connection, bell, addr, port: bound.port })) }); @@ -276,6 +289,7 @@ fn open(kind: Kind, addr: [u8; 4], ask: impl FnOnce(NetstackConn) -> Result<(Bou Ok(Bound { handle, port: end.port }) } Err(refused) => { + *TURNS[slot].lock() = None; table.unreserve(slot); Err(refused) } @@ -480,7 +494,8 @@ pub fn export(handle: RawHandle) -> Result { /// An exported end taken up as this process's holder of its socket, `payload` and `handles` the /// [`ADOPT`] netstack queued on it (`super::adopt_exported`): the end and its bell narrowed to /// keep no `TRANSFER`. The descriptor's handle; `None`, with everything closed, where the frame -/// is malformed, the process holds [`MAX_SOCKETS`] or the kernel will not narrow them. +/// is malformed, the process holds [`MAX_SOCKETS`] or the kernel makes no poller for the end or +/// will not narrow them. pub(super) fn adopted(connection: Connection, payload: &[u8], handles: ipc::Batch) -> Option { let adopt: Adopt = ipc::decode_payload(payload).ok()?; let [bell] = handles.exact::<1>()?; @@ -493,7 +508,13 @@ pub(super) fn adopted(connection: Connection, payload: &[u8], handles: ipc::Batc syscall::close(bell); return None; }; - let made = take_up(kind, connection.into_owned(), bell); + let made = match make_poller(slot) { + Ok(()) => take_up(kind, connection.into_owned(), bell), + Err(refused) => { + syscall::close(bell); + Err(refused) + } + }; let mut table = TABLE.lock(); match made { Ok((descriptor, connection, bell)) => { @@ -501,6 +522,7 @@ pub(super) fn adopted(connection: Connection, payload: &[u8], handles: ipc::Batc Some(descriptor) } Err(_) => { + *TURNS[slot].lock() = None; table.unreserve(slot); None } diff --git a/sdk/toyos/src/poller.rs b/sdk/toyos/src/poller.rs index a4edac9e3fb..5f76a596ebd 100644 --- a/sdk/toyos/src/poller.rs +++ b/sdk/toyos/src/poller.rs @@ -218,6 +218,12 @@ impl Poller { /// caller a ring smaller than the set it just declared, which makes the /// loss reachable while looking like a success. pub fn new(capacity: u32) -> Self { + Self::try_new(capacity).expect("Poller::new: inbox_setup failed") + } + + /// [`new`](Self::new), answering the kernel's refusal of the inbox, as a + /// memory-exhausted one, instead of panicking. + pub fn try_new(capacity: u32) -> Result { assert!( (1..=Self::MAX_HANDLES).contains(&capacity), "Poller::new: {capacity} handles is outside 1..={}; \ @@ -227,8 +233,7 @@ impl Poller { let entries = capacity.next_power_of_two(); // The inbox owns its page and the kernel maps it: one call, and no // second lifetime for a mapping that is only ever this inbox's. - let (inbox, base) = unsafe { syscall::inbox_setup(entries) } - .expect("Poller::new: inbox_setup failed"); + let (inbox, base) = unsafe { syscall::inbox_setup(entries) }?; // SAFETY: `base` is what `inbox_setup` just answered with — the // address of this process's own inbox page, laid out by the kernel // before it was mapped and unmapped only when the handle closes, which @@ -243,7 +248,7 @@ impl Poller { rings.submission_ring_size, rings.completion_ring_size, ); - Self { inbox, rings, capacity } + Ok(Self { inbox, rings, capacity }) } /// Watch the given handle for readiness.