diff --git a/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md b/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md index fd1f1df5948..d5dda726075 100644 --- a/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md +++ b/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md @@ -10,9 +10,8 @@ netstack lets a piped connection's socket and id go once the wire is finished and it has closed both of its pipe ends (`bridge_piped`, `userland/netstack/src/main.rs`). A peer's reset does both at once, so a client that still holds the stream names an id netstack no longer has, and -`handle_tcp_set_option` and `handle_tcp_get_option` answer it -`ERR_NOT_CONNECTED`. Before the socket left with its bridge both answered from -the kept socket. +`handle_tcp_set_option` answers it `ERR_NOT_CONNECTED`. Before the socket left +with its bridge it answered from the kept socket. What a host answers `std::net::TcpStream` on a stream whose peer reset it, after the read that reported the reset: @@ -21,10 +20,12 @@ after the read that reported the reset: |---|---|---| | macOS (Darwin 27.0.0), measured | `Err(InvalidInput)`, `EINVAL` | `Ok(false)` | | Linux, by the review of the change that made this, not measured | `Ok(())` | `Ok` | -| ToyOS, read from the code | `Err(NotConnected)` | `Err(NotConnected)` | +| ToyOS, read from the code | `Err(NotConnected)` | `Ok`, the value last set | -So the write is refused as macOS refuses it, under another kind, and the read -is refused where both hosts answer. Keeping the id until the client's close +So the write is refused as macOS refuses it, under another kind. The read asks +netstack nothing: std answers from the value its `TcpStream` holds +(`sdk/std/sys/net/connection.rs`) and libc's `getsockopt` from its socket's +entry (`userland/libc/src/socket.rs`). Keeping the id until the client's close request is not the fix: a client that dies sends none, and netstack, having closed its pipe ends, has nothing left that says the client is gone. diff --git a/issues/libc-drops-a-tcp-nodelay-set-before-connect.md b/issues/libc-drops-a-tcp-nodelay-set-before-connect.md new file mode 100644 index 00000000000..cc081fbfd7b --- /dev/null +++ b/issues/libc-drops-a-tcp-nodelay-set-before-connect.md @@ -0,0 +1,23 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# libc drops a TCP_NODELAY set before connect + +Read from the code, not run. `setsockopt` (`userland/libc/src/socket.rs`) +sends `TCP_NODELAY` to netstack only for a socket netstack holds, and a stream +socket is one only from `connect` or `accept`. Set on a socket not yet +connected, the option answers 0, is stored nowhere and is not sent when the +connection is made; `getsockopt` then answers 0 for it, which is true of what +netstack holds and not of what the program was told. Linux keeps the option +across `connect`. `SO_BROADCAST` on a datagram socket not yet bound is kept in +the socket's entry and sent at `bind`; this is the same case with no such +path. + +**Exit**: a `TCP_NODELAY` set before `connect` reaches netstack with the +connection or is refused by name, and a guest C case reads it back after +`connect`. + +**Owner**: libc, in a libc stage of its own under `issues/toyos-has-its-own-network-stack.md`: it touches only `userland/libc` and guest C cases, and lands before the guest tests that follow the move of netd onto the node. diff --git a/issues/libc-reads-an-ipv4-address-in-host-order.md b/issues/libc-reads-an-ipv4-address-in-host-order.md new file mode 100644 index 00000000000..92e8912ff0e --- /dev/null +++ b/issues/libc-reads-an-ipv4-address-in-host-order.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# libc reads an IPv4 address in host order + +`sockaddr_in`'s `s_addr` is in network byte order: its four bytes in memory +are the address's four octets. libc's `parse_sockaddr` +(`userland/libc/src/socket.rs`) takes them with `s_addr.to_be_bytes()`, which +on a little-endian machine reverses them, and `fill_sockaddr`, `getaddrinfo` +and `inet_addr` write `u32::from_be_bytes(ip)`, reversed the same way. So an +address libc made itself comes back right, and one a program made as POSIX +says does not: `inet_pton` copies the octets in order and `htonl` swaps, and +`connect` reads either backwards. + +Measured in a guest on `tests/netcase`, x86-64, with a host server listening: +`connect` to `{htonl(0x0a000202)}` answered -1, and to +`{inet_addr("10.0.2.2")}` answered 0. + +**Exit**: every address libc reads or writes is its octets in memory order, +and a guest C case connects to an address built with `htonl` and to one built +with `inet_pton`. + +**Owner**: libc, in a libc stage of its own under `issues/toyos-has-its-own-network-stack.md`: it touches only `userland/libc` and guest C cases, and lands before the guest tests that follow the move of netd onto the node. diff --git a/issues/libc-sends-from-a-datagram-socket-it-never-bound.md b/issues/libc-sends-from-a-datagram-socket-it-never-bound.md new file mode 100644 index 00000000000..602498724f8 --- /dev/null +++ b/issues/libc-sends-from-a-datagram-socket-it-never-bound.md @@ -0,0 +1,32 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# libc sends from a datagram socket it never bound + +netstack holds a datagram socket from `bind` +(`toyos::net::udp_bind`), which is also where libc's socket entry gets its two +pipe ends. `sendto` (`userland/libc/src/socket.rs`) does not bind a socket +that was never bound: it writes the datagram to the entry's `tx_fd`, which is +0 and names whatever handle 0 is in the process, and only then would ask +netstack to send from socket id 0. Measured in a guest on `tests/netcase`, +x86-64, from a job of test-runner's: `sendto` answers -1 with `errno` 5 +(`EIO`), and the job exits 0. Which step refused is not shown: libc answers +`EIO` for a refused write and for several of `udp_send_to`'s refusals alike. +What a process whose handle 0 takes a write sees is unmeasured. POSIX binds an unbound datagram socket to an +ephemeral port at its first send, and a C program that broadcasts, `socket`, +`setsockopt(SO_BROADCAST)`, `sendto`, does exactly this. std has no such +socket: `UdpSocket::bind` is its only constructor. + +That program does not compile either: `userland/libc/include/netinet/in.h` +defines no `INADDR_BROADCAST`. + +**Exit**: the first `sendto` on an unbound datagram socket binds it to a port +netstack chooses, handing over a `SO_BROADCAST` already set as `bind` does, +`netinet/in.h` defines `INADDR_BROADCAST`, and a guest C case sends from a +socket it never bound. + +**Owner**: libc, in a libc stage of its own under `issues/toyos-has-its-own-network-stack.md`: it touches only `userland/libc` and guest C cases, and lands before the guest tests that follow the move of netd onto the node. The C case the track's broadcast line names for the sequence without +`bind` waits on this. diff --git a/issues/std-holds-a-streams-nodelay-per-duplicate.md b/issues/std-holds-a-streams-nodelay-per-duplicate.md new file mode 100644 index 00000000000..f5ddbd738b2 --- /dev/null +++ b/issues/std-holds-a-streams-nodelay-per-duplicate.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# std holds a stream's nodelay per duplicate + +Read from the code, not run. `TcpStream::try_clone` +(`sdk/std/sys/net/connection.rs`, `duplicate`) shares the one netstack socket +between the stream and its duplicate and gives the duplicate its own copy of +`nodelay`. `set_nodelay` on either changes the socket both name and stores the +value only in the one it was called on, so `nodelay()` on the other answers +what was true before. A host's getter reads the kernel's value and cannot +differ. Two threads setting opposite values on one stream can also reach +netstack in one order and the store in the other. `UdpSocket`'s `broadcast` +is one value shared by every duplicate and locked across its request. + +**Exit**: `nodelay` is one value shared by every duplicate of a stream and +locked across the request that changes it, and a guest test sets it through a +duplicate and reads it from the original. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`, with the +reset-stream option test of +`issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md`, +which reads the same value. diff --git a/issues/the-socket2-forks-so-broadcast-setter-does-nothing.md b/issues/the-socket2-forks-so-broadcast-setter-does-nothing.md new file mode 100644 index 00000000000..579c5a70e3c --- /dev/null +++ b/issues/the-socket2-forks-so-broadcast-setter-does-nothing.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# The socket2 fork's SO_BROADCAST setter does nothing + +Read from the fork at the commit the lock names (`1d90250`), not run. On +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 +datagram socket at all, so a program that reaches a datagram socket through +socket2 has none on ToyOS. + +**Exit**: the fork's ToyOS backend either carries a datagram socket whose +`SO_BROADCAST` reaches netstack and reads back, or refuses the option by name +on a socket it cannot send a datagram from; the lock names the fork commit +that does. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`; the +change is a commit on the socket2 fork's `toyos` branch. diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index be7074354fb..8a5b0ede3b8 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -17,7 +17,7 @@ netd runs smoltcp. Its replacement is ToyOS's own stack, built clean-room: reade - Then multi-core, netring (blocked on the owner's ABI ruling), TCP and IP hardening, IPv6, offloads, soak. - This track owns the T14's outbound rows, the machine reaching its router and the internet on the I219, judged from the stick: they arrive on this stack and not on smoltcp (owner: "no smoltcp."), and until they do no T14 row reads the wired card (`issues/the-host-cannot-reach-the-t14-while-it-runs-toyos.md`). -Stage 5 lands in slices, the orchestrator's cut under the owner's words "i want smoltcp out as fast as possible": netd's decisions go into `toyos-net-node` (`userland/netstack/node`), pure and host-tested and shipped in nothing, and then one change moves netd onto it and deletes smoltcp. In the tree: the node, its DHCP lease, its clients' datagram sockets, its mDNS name, its resolver, streams, listeners and the one bound on its clients' streams, listeners and datagram sockets (`userland/netstack/node/src/places.rs`); a lookup's sockets stand outside that bound, under `toyos_dns::MAX_LOOKUPS` and its rounds. Still to build on it: the move. +Stage 5 lands in slices, the orchestrator's cut under the owner's words "i want smoltcp out as fast as possible": netd's decisions go into `toyos-net-node` (`userland/netstack/node`), pure and host-tested and shipped in nothing, and then one change moves netd onto it and deletes smoltcp. In the tree: the node, its DHCP lease, its clients' datagram sockets, its mDNS name, its resolver, streams, listeners and the one bound on its clients' streams, listeners and datagram sockets (`userland/netstack/node/src/places.rs`); a lookup's sockets stand outside that bound, under `toyos_dns::MAX_LOOKUPS` and its rounds. Still to build on it: the datagram sockets' broadcast permission; then the move. What the node does not yet meet: @@ -31,12 +31,12 @@ What the node does not yet meet: - 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 `toyos-net-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. - A stream its client can see no more, its reader gone or the peer's FIN read through and its client writing no more, is reset once its pipe has given up no byte for 100 s, and a peer address restarts that clock for at most 16 such streams at once (`OWNERLESS_PER_PEER`, `userland/netstack/node/src/streams.rs`); one past them has 100 s from the pass that found it so, until one of the 16 is done and it takes its place. What holds in this stage, from the code: per address, 16, an estimate no measurement set. There is no floor on the rate: each of the 16 is kept for as long as its pipe gives up one byte per 100 s, which for a full 2 MiB pipe is 2,097,152 x 100 s, over six years, and then for its tail in [tcp] (the line below on a client gone with nothing left in its pipe). The 16 is no bound across addresses: addresses are not counted, and where a connect's address is its client's choice, an accepted stream's is the peer's, which on the link answers for as many addresses as it likes. The bound across them is the node's places (`userland/netstack/node/src/places.rs`), of which every stream holds one until the node lets it go, and there is no second one: in `peers_at_more_addresses_than_there_are_places_hold_no_stream_past_them` (`userland/netstack/node/tests/listeners.rs`) peers at four addresses bring sixteen connections each to a node with three places, which holds two such streams at a time, each kept alive past its 100 s, and refuses every accept and connect past them. `issues/netstack-cuts-a-departed-clients-unsent-tail-at-the-ceiling.md` stays open until netd runs on the node: on a host its exit's second clause is met (`a_departed_clients_tail_arrives_whole_while_its_peer_takes_it`) and its first for 16 streams an address. Exit: the T14 or a guest shows a peer holding one at a byte per 100 s, and the cut takes a floor on the rate, or the owner rules it needs none; and the move closes that issue with its bench row. -- A stream its peer reset is gone from the node at once, so for a client that still holds it `Node::set_nodelay` answers `false` and `Node::nodelay` `None`: `issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md`, carried by the node as built. std's `nodelay()` asks netd nothing: it answers from the value its `TcpStream` holds (`sdk/std/sys/net/connection.rs`). What the node carries is `set_nodelay` on a reset stream, refused where macOS refuses it under another kind and Linux answers, and libc's `getsockopt` of `TCP_NODELAY`, which asks through `toyos::net::tcp_get_option` (`userland/libc/src/socket.rs`) and is refused where both hosts answer. The node cannot hold the stream for the client: its two pipe ends are all it has of the client, letting both go is how the client learns of the reset, and with both gone nothing tells it when the client leaves, so a kept stream would be kept for ever. Exit, outside the node: that issue's guest test is green, and a `getsockopt` of `TCP_NODELAY` on a stream its peer reset is answered, by libc from a value its socket holds as std's does or by a pipe ABI that gives netd a handle whose other end's leaving the kernel reports. +- A stream its peer reset is gone from the node at once, so for a client that still holds it `Node::set_nodelay` answers `false` and `Node::nodelay` `None`: `issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md`, carried by the node as built. std's `nodelay()` asks netd nothing: it answers from the value its `TcpStream` holds (`sdk/std/sys/net/connection.rs`), and libc's `getsockopt` of `TCP_NODELAY` from its socket's entry (`userland/libc/src/socket.rs`). What the node carries is `set_nodelay` on a reset stream, refused where macOS refuses it under another kind and Linux answers. The node cannot hold the stream for the client: its two pipe ends are all it has of the client, letting both go is how the client learns of the reset, and with both gone nothing tells it when the client leaves, so a kept stream would be kept for ever. Exit, outside the node: that issue's guest test is green, and a `getsockopt` of `TCP_NODELAY` on a stream its peer reset is answered, by libc from a value its socket holds as std's does or by a pipe ABI that gives netd a handle whose other end's leaving the kernel reports. - Every frame 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 frame, as netd's `bridge_piped` is today. For an idle stream a pass is one `recv_with`, two `status` and one read of its client's pipe, which in netd is a system call: 1,000 system calls a frame with 1,000 idle streams. A pass's `recv_with` also re-files its connection's deadline and offers it to the transmit round, so one frame makes every idle stream eligible and the next opportunity serves each to learn it has nothing. [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, the one a pipe's wake names, and those due. Exit: at the move, netd's CPU time per frame on the T14 with 1, 100 and 1,000 idle streams beside one bulk stream is measured, and the change lands if 1,000 idle streams cost more than 12.3 µs a frame over what one costs. The 12.3 µs is the time a full frame takes on a wire of 1 Gbit/s, 1,538 bytes of 8 ns with its preamble and gap: past it the passes alone take longer than the bulk stream's frames take to arrive. It is a threshold by arithmetic and no measurement; by estimate, not measured, 1,000 system calls exceed it. - 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 (`toyos-net-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`, `userland/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 `userland/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 is refused `udp.broadcast-not-permitted` and answered invalid input, where smoltcp sends it: the pipe ABI has no request that permits it, and std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) answers `Ok` to a permission nothing grants. No program in the tree sends to a broadcast address or sets the option; the sources of the third-party C programs an image builds from a recipe have not been searched. 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 move of netd onto the node does not land with this open. Exit, both before the move, as a stage of their own: that search, for `SO_BROADCAST` and a `sendto` to a broadcast address, is posted with its command and output; and the pipe ABI carries the permission from std's and libc's setters to `Udp::set_broadcast`, a send refused for want of it answered in a word that says permission and not input. +- A client's datagram to a broadcast address is refused `udp.broadcast-not-permitted` and answered invalid input, where smoltcp sends it. 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` (`toyos/src/net.rs`), sent by std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) and by libc's `setsockopt` of `SO_BROADCAST` (`userland/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. Nothing enforces it yet: netd on smoltcp answers the request done and sends to a broadcast address for every socket, and the node has no call that takes the permission and answers the refusal invalid input. The search is run. `grep -rnI -E 'SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255|sendto|setsockopt|sys/socket\.h|socket\(' userland/doom/doomgeneric`, the one third-party C program an image builds, at the commit `userland/doom/build.rs` pins, exits 1 with no line; the same without `sys/socket\.h` over `tests/testcases` exits 1 with no line; `SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255` over the trees of `src/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, before the move, and the move does not land without it: a slice of its own on the node's datagram sockets gives the node a call that hands the permission to `Udp::set_broadcast` and gives `Refused` (`userland/netstack/node/src/datagram.rs`) the word for `ERR_PERMISSION_DENIED`, and in `each_refusal_is_answered_in_the_pipes_word_for_it` (`userland/netstack/node/tests/datagram.rs`) the two `BroadcastNotPermitted` sends turn from invalid input to that word and the same two sends leave once the socket holds the permission. Exit, after the move, which keeps this line open until both are green on netd on the node, since no test can read the refusal on smoltcp: 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 sequence without `bind` waits on `issues/libc-sends-from-a-datagram-socket-it-never-bound.md`. - A refusal [udp] logs against a client's call waits in the stack until the node's next `receive`, `fire` or `link`, because only those pass over the stack's reports. Exit, at the move and not after it: the datagram calls end in that pass, or netd drains it once per turn of its loop. - 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. Exit: one type carries the label to both; due at the move if handing `answer_as` its `&'static str` takes a leak. - The node's name is not probed for or defended (RFC 6762 §8.1, §9), as `toyos-mdns` records of itself. It is not announced again when the link comes back, which §8.3 asks and netd does today, where link-up resets its DHCP client and the address is new again: here the lease outlives the link. Exit: `toyos-mdns` does both, on the node's `link`. diff --git a/sdk/std/sys/net/connection.rs b/sdk/std/sys/net/connection.rs index 10f89ee4ec3..c593ca3d3d6 100644 --- a/sdk/std/sys/net/connection.rs +++ b/sdk/std/sys/net/connection.rs @@ -24,7 +24,8 @@ fn net_err_to_io(e: NetError) -> io::Error { NetError::NotConnected => io::ErrorKind::NotConnected, NetError::InvalidInput => io::ErrorKind::InvalidInput, NetError::NetstackNotFound => io::ErrorKind::NotConnected, - _ => io::ErrorKind::Other, + NetError::PermissionDenied => io::ErrorKind::PermissionDenied, + NetError::ResourceExhausted | NetError::Protocol(_) | NetError::Io => io::ErrorKind::Other, }; io::Error::new(kind, "netstack error") } @@ -490,6 +491,9 @@ pub struct UdpSocket { 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>, read_timeout_ms: AtomicU32, write_timeout_ms: AtomicU32, } @@ -515,6 +519,7 @@ impl UdpSocket { rx_fd: unsafe { OwnedFd::from_raw_fd(bound.rx.into_raw().0 as i32) }, 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), }) @@ -574,6 +579,7 @@ impl UdpSocket { rx_fd: unsafe { OwnedFd::from_raw_fd(new_rx_fd.0 as i32) }, local: self.local, peer: crate::sync::Mutex::new(*self.peer.lock().unwrap()), + broadcast: Arc::clone(&self.broadcast), read_timeout_ms: AtomicU32::new(self.read_timeout_ms.load(Relaxed)), write_timeout_ms: AtomicU32::new(self.write_timeout_ms.load(Relaxed)), }) @@ -599,12 +605,16 @@ impl UdpSocket { Ok(if ms == 0 { None } else { Some(Duration::from_millis(ms as u64)) }) } - pub fn set_broadcast(&self, _broadcast: bool) -> io::Result<()> { + 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(()) } pub fn broadcast(&self) -> io::Result { - Ok(false) + Ok(*self.broadcast.lock().unwrap()) } pub fn set_multicast_loop_v4(&self, _: bool) -> io::Result<()> { diff --git a/tests/libc-arch/src/lib.rs b/tests/libc-arch/src/lib.rs index c1a68464238..b3c608dd3a1 100644 --- a/tests/libc-arch/src/lib.rs +++ b/tests/libc-arch/src/lib.rs @@ -6,8 +6,9 @@ //! architecture checks its own module. The UTF-8 reader against //! `core::str::from_utf8`, the number reader against the host C library's and //! IEEE 754's rounding of hexadecimal input computed exactly, AArch64's -//! `long double` widening against compiler-builtins', and the errno codes -//! against `include/errno.h`. +//! `long double` widening against compiler-builtins', the errno codes against +//! `include/errno.h`, and the socket option rule against the host's +//! `setsockopt` and `getsockopt`. #[cfg(test)] extern crate alloc; @@ -40,6 +41,9 @@ mod pollreq; #[path = "../../../userland/libc/src/sigmask.rs"] mod sigmask; #[cfg(test)] +#[path = "../../../userland/libc/src/sockopt.rs"] +mod sockopt; +#[cfg(test)] #[path = "../../../userland/libc/src/strtonum.rs"] mod strtonum; #[cfg(test)] @@ -76,6 +80,8 @@ mod prototypes; #[cfg(test)] mod signal_masks; #[cfg(test)] +mod socket_options; +#[cfg(test)] mod strtonum_differential; #[cfg(test)] mod text_differential; diff --git a/tests/libc-arch/src/socket_options.rs b/tests/libc-arch/src/socket_options.rs new file mode 100644 index 00000000000..2ac86db4206 --- /dev/null +++ b/tests/libc-arch/src/socket_options.rs @@ -0,0 +1,184 @@ +//! libc's socket option rule against the host's own `setsockopt` and +//! `getsockopt`, one table through both: `SO_BROADCAST` on a datagram socket +//! and `TCP_NODELAY` on a stream socket, a value and a buffer of 8, 4, 2, 1 +//! and 0 bytes and a null one of each length, and a null length. Compared are +//! the return and its `errno`, the length written back, which bytes of the +//! buffer were written, and whether what they hold is non-zero: a host reads a +//! set option back as any non-zero `int`, Darwin as the option's flag bit. +//! +//! libc's numbers and answers are Linux's, so on Linux every row agrees. Darwin +//! answers the rows [`darwin`] names differently, and is held to those. + +use std::ffi::c_int; +use std::net::{TcpListener, UdpSocket}; +use std::os::fd::AsRawFd; +use std::ptr; + +use crate::header; +use crate::sockopt::{self, Kept, Refusal}; + +const SOCKET_H: &str = include_str!("../../../userland/libc/include/sys/socket.h"); +const IN_H: &str = include_str!("../../../userland/libc/include/netinet/in.h"); + +unsafe extern "C" { + fn setsockopt(fd: c_int, level: c_int, name: c_int, value: *const u8, len: u32) -> c_int; + fn getsockopt(fd: c_int, level: c_int, name: c_int, value: *mut u8, len: *mut u32) -> c_int; + #[cfg_attr(target_os = "macos", link_name = "__error")] + #[cfg_attr(target_os = "linux", link_name = "__errno_location")] + fn errno_location() -> *mut c_int; +} + +/// The host's `EFAULT` and `EINVAL`, which macOS and Linux number alike. +const EFAULT: c_int = 14; +const EINVAL: c_int = 22; + +const LENGTHS: [u32; 5] = [8, 4, 2, 1, 0]; +/// What a buffer holds before a read, which no answer's byte is. +const UNWRITTEN: u8 = 0xAA; + +/// The option as a C program built against libc's headers names it. +fn libc_numbers(option: Kept) -> (c_int, c_int) { + match option { + Kept::Broadcast => (header::int(SOCKET_H, "SOL_SOCKET"), header::int(SOCKET_H, "SO_BROADCAST")), + Kept::NoDelay => (header::int(SOCKET_H, "IPPROTO_TCP"), header::int(IN_H, "TCP_NODELAY")), + } +} + +/// The option as the host numbers it: on Linux, as libc does. +fn host_numbers(option: Kept) -> (c_int, c_int) { + if cfg!(target_os = "macos") { + match option { + Kept::Broadcast => (0xFFFF, 0x20), + Kept::NoDelay => (6, 1), + } + } else { + libc_numbers(option) + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum Answer { + /// -1, and this `errno`. A refused read's length is not compared: POSIX + /// leaves it unspecified. + Refused(c_int), + /// A setter's 0. + Set, + /// A getter's 0: the length written back, which bytes were written, and + /// whether they hold a non-zero value. + Read { len: u32, written: [bool; 8], nonzero: bool }, +} + +fn refused(refusal: Refusal) -> Answer { + Answer::Refused(match refusal { + Refusal::Short => EINVAL, + Refusal::Fault => EFAULT, + }) +} + +fn host_refusal() -> Answer { + // SAFETY: the host's errno slot is this thread's. + Answer::Refused(unsafe { *errno_location() }) +} + +fn read(buf: &[u8; 8], len: u32) -> Answer { + let written = buf.map(|byte| byte != UNWRITTEN); + Answer::Read { len, written, nonzero: buf.iter().any(|&byte| byte != UNWRITTEN && byte != 0) } +} + +/// One socket's option as libc keeps it and as the host does. +struct Pair { + option: Kept, + fd: c_int, + kept: bool, +} + +impl Pair { + /// A setter handed `len` bytes at `value`, by libc's rule and by the host. + fn set(&mut self, value: *const u8, len: u32) -> (Answer, Answer) { + let (level, name) = libc_numbers(self.option); + assert_eq!(sockopt::kept(level, name), Some(self.option)); + // SAFETY: `value` is null or eight readable bytes, and `len` at most 8. + let ours = match unsafe { sockopt::switch(value, len) } { + Ok(on) => { + self.kept = on; + Answer::Set + } + Err(refusal) => refused(refusal), + }; + let (level, name) = host_numbers(self.option); + // SAFETY: as above. + let host = match unsafe { setsockopt(self.fd, level, name, value, len) } { + 0 => Answer::Set, + _ => host_refusal(), + }; + (ours, host) + } + + /// A getter handed `len` bytes at `value`, null when `null`, by libc's rule + /// and by the host. + fn get(&self, null: bool, len: u32) -> (Answer, Answer) { + let call = |get: &dyn Fn(*mut u8, *mut u32) -> Option| { + let (mut buf, mut len) = ([UNWRITTEN; 8], len); + let value = if null { ptr::null_mut() } else { buf.as_mut_ptr() }; + get(value, &mut len).unwrap_or_else(|| read(&buf, len)) + }; + // SAFETY: the buffer is null or eight writable bytes, and `len` at most 8. + let ours = call(&|value, len| unsafe { sockopt::answer(self.kept as i32, value, len) }.err().map(refused)); + let (level, name) = host_numbers(self.option); + // SAFETY: as above. + let host = call(&|value, len| (unsafe { getsockopt(self.fd, level, name, value, len) } != 0).then(host_refusal)); + (ours, host) + } +} + +/// What Darwin answers where it is not what Linux and libc do. It judges a +/// setter's pointer before its length unless the length is 0, and a getter's +/// null buffer holds no byte, which it reports as a read of length 0. +fn darwin(setter: bool, null: bool, len: u32) -> Option { + match (setter, null, len) { + (true, true, 1 | 2) => Some(Answer::Refused(EFAULT)), + (false, true, 1..) => Some(Answer::Read { len: 0, written: [false; 8], nonzero: false }), + _ => None, + } +} + +fn hold((ours, host): (Answer, Answer), setter: bool, null: bool, len: u32, row: &str) { + let expected = match darwin(setter, null, len) { + Some(theirs) if cfg!(target_os = "macos") => theirs, + _ => ours, + }; + assert_eq!(host, expected, "{row}: libc answers {ours:?}"); +} + +#[test] +fn an_options_value_crosses_as_the_hosts_does() { + let datagram = UdpSocket::bind("127.0.0.1:0").unwrap(); + let stream = TcpListener::bind("127.0.0.1:0").unwrap(); + for (option, fd) in [(Kept::Broadcast, datagram.as_raw_fd()), (Kept::NoDelay, stream.as_raw_fd())] { + let mut pair = Pair { option, fd, kept: false }; + for on in [1u8, 0, 1] { + let value = [on, 0, 0, 0, 0, 0, 0, 0]; + for len in LENGTHS { + for null in [false, true] { + let row = format!("{option:?} {on}, {len} bytes, null {null}"); + let at = if null { ptr::null() } else { value.as_ptr() }; + hold(pair.set(at, len), true, null, len, &format!("set {row}")); + hold(pair.get(null, len), false, null, len, &format!("get {row}")); + } + } + assert_eq!(pair.kept, on != 0, "{option:?}: the value of four bytes or more was kept"); + let (ours, host) = pair.get(false, 4); + assert_eq!(ours, Answer::Read { len: 4, written: [true, true, true, true, false, false, false, false], nonzero: on != 0 }); + assert_eq!(host, ours); + } + + let (level, name) = host_numbers(option); + let mut buf = [UNWRITTEN; 8]; + // SAFETY: a null length is the refusal asked for; the buffer is eight bytes. + let host = unsafe { getsockopt(fd, level, name, buf.as_mut_ptr(), ptr::null_mut()) }; + assert_eq!((host, host_refusal()), (-1, Answer::Refused(EFAULT)), "{option:?}: a null length"); + // SAFETY: as above. + let ours = unsafe { sockopt::answer(1, buf.as_mut_ptr(), ptr::null_mut()) }; + assert_eq!((ours, buf), (Err(Refusal::Fault), [UNWRITTEN; 8])); + } +} diff --git a/toyos/src/net.rs b/toyos/src/net.rs index 84b4d4e0a67..b5d991d1533 100644 --- a/toyos/src/net.rs +++ b/toyos/src/net.rs @@ -19,7 +19,7 @@ pub enum MsgType { UdpClose = 11, DnsLookup = 12, TcpSetOption = 13, - TcpGetOption = 14, + UdpSetOption = 15, TcpConnectPiped = 20, TcpBindPiped = 21, TcpAcceptPiped = 22, @@ -36,7 +36,7 @@ impl MsgType { 11 => Some(Self::UdpClose), 12 => Some(Self::DnsLookup), 13 => Some(Self::TcpSetOption), - 14 => Some(Self::TcpGetOption), + 15 => Some(Self::UdpSetOption), 20 => Some(Self::TcpConnectPiped), 21 => Some(Self::TcpBindPiped), 22 => Some(Self::TcpAcceptPiped), @@ -74,9 +74,16 @@ pub const ERR_INVALID_INPUT: u32 = 6; /// so a capacity refusal on that code is indistinguishable from an ordinary /// failed connection — including to a test trying to find where the cap is. pub const ERR_RESOURCE_EXHAUSTED: u32 = 7; +/// The socket was not given the permission the request needs: a datagram to a +/// 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; pub const ERR_OTHER: u32 = 255; +/// A stream's Nagle algorithm off: [`tcp_set_option`]. pub const OPT_NODELAY: u32 = 1; +/// A datagram socket may send to a broadcast address: [`udp_set_option`]. +pub const OPT_BROADCAST: u32 = 2; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NetError { @@ -90,6 +97,8 @@ pub enum NetError { /// netstack is at its own limit. Retryable against the same peer, unlike /// [`NetError::ConnectionRefused`] — see [`ERR_RESOURCE_EXHAUSTED`]. ResourceExhausted, + /// See [`ERR_PERMISSION_DENIED`]. + PermissionDenied, Protocol(u32), Io, } @@ -104,6 +113,7 @@ impl NetError { ERR_NOT_CONNECTED => NetError::NotConnected, ERR_INVALID_INPUT => NetError::InvalidInput, ERR_RESOURCE_EXHAUSTED => NetError::ResourceExhausted, + ERR_PERMISSION_DENIED => NetError::PermissionDenied, 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. @@ -212,16 +222,14 @@ ipc_payload! { pub len: u16, } + /// [`MsgType::TcpSetOption`] 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 SocketOptionResponse { - pub value: u32, - } - pub struct ErrorResponse { pub code: u32, } @@ -488,13 +496,6 @@ pub fn tcp_set_option(socket_id: TcpSocketId, option: u32, value: u32) -> Result .status() } -pub fn tcp_get_option(socket_id: TcpSocketId, option: u32) -> Result { - let resp: SocketOptionResponse = NetstackConn::connect()? - .request(MsgType::TcpGetOption, &SocketOptionRequest { socket_id: socket_id.0, option, value: 0 })? - .response()?; - Ok(resp.value) -} - // UDP client functions pub fn udp_bind(addr: [u8; 4], port: u16) -> Result { @@ -535,6 +536,12 @@ pub fn udp_close(socket_id: UdpSocketId) -> Result<(), NetError> { .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 { let mut buf = [0u8; 256]; let n = NetstackConn::connect()? @@ -628,7 +635,24 @@ mod tests { NetError::from_error_code(ERR_CONNECTION_REFUSED), NetError::ConnectionRefused ); + 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_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) + )); + } } diff --git a/userland/libc/include/sys/socket.h b/userland/libc/include/sys/socket.h index dfda2863f05..a25ada6026f 100644 --- a/userland/libc/include/sys/socket.h +++ b/userland/libc/include/sys/socket.h @@ -23,6 +23,7 @@ typedef unsigned int socklen_t; #define SOL_SOCKET 1 #define SO_REUSEADDR 2 #define SO_ERROR 4 +#define SO_BROADCAST 6 #define SO_KEEPALIVE 9 #define SO_RCVTIMEO 20 #define SO_SNDTIMEO 21 diff --git a/userland/libc/src/errno.rs b/userland/libc/src/errno.rs index ce9cd41f9ce..e07148fd199 100644 --- a/userland/libc/src/errno.rs +++ b/userland/libc/src/errno.rs @@ -26,6 +26,7 @@ pub(crate) const ECHILD: i32 = 10; pub(crate) const EAGAIN: i32 = 11; pub(crate) const ENOMEM: i32 = 12; pub(crate) const EACCES: i32 = 13; +pub(crate) const EFAULT: i32 = 14; pub(crate) const EBUSY: i32 = 16; pub(crate) const EEXIST: i32 = 17; pub(crate) const ENODEV: i32 = 19; diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 8ea47af3be4..594b72fc83b 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -25,6 +25,7 @@ mod pthread; mod refused; mod sigmask; mod socket; +mod sockopt; mod stdio; mod string; mod strtonum; diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs index bdda0d2576b..2cdc871b38e 100644 --- a/userland/libc/src/socket.rs +++ b/userland/libc/src/socket.rs @@ -5,9 +5,10 @@ use alloc::vec::Vec; use core::ptr; use toyos_abi::RawHandle; use toyos_abi::syscall; -use toyos::net::{NetError, TcpSocketId, UdpSocketId, OPT_NODELAY}; +use toyos::net::{NetError, TcpSocketId, UdpSocketId, OPT_BROADCAST, OPT_NODELAY}; -use crate::errno::{EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EINVAL, EIO, ENOMEM, ENOTCONN, ETIMEDOUT}; +use crate::errno::{EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EFAULT, EINVAL, EIO, ENOMEM, ENOTCONN, ETIMEDOUT}; +use crate::sockopt::{self, Kept}; // C types matching POSIX @@ -49,11 +50,6 @@ const AF_INET: i32 = 2; const AF_UNSPEC: i32 = 0; const SOCK_STREAM: i32 = 1; const SOCK_DGRAM: i32 = 2; -const IPPROTO_TCP: i32 = 6; - -const SOL_SOCKET: i32 = 1; -const SO_ERROR: i32 = 4; -const TCP_NODELAY: i32 = 1; // Internal socket table @@ -76,6 +72,11 @@ struct SocketEntry { 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. A + // datagram socket netstack does not hold yet keeps `broadcast` for `bind` + // to hand over. + nodelay: bool, + broadcast: bool, } const MAX_SOCKETS: usize = 128; @@ -116,7 +117,8 @@ fn net_err_to_errno(e: NetError) -> i32 { NetError::AddrInUse => EADDRINUSE, NetError::NotConnected => ENOTCONN, NetError::InvalidInput => EINVAL, - _ => EIO, + NetError::PermissionDenied => EACCES, + NetError::NetstackNotFound | NetError::ResourceExhausted | NetError::Protocol(_) | NetError::Io => EIO, } } @@ -174,6 +176,8 @@ pub unsafe extern "C" fn socket(domain: i32, sock_type: i32, _protocol: i32) -> rx_fd: 0, tx_fd: 0, notify_fd: 0, + nodelay: false, + broadcast: false, }; let fd = alloc_socket(entry); if fd < 0 { @@ -253,6 +257,14 @@ pub unsafe extern "C" fn bind(fd: i32, addr: *const Sockaddr, addrlen: SocklenT) Ok(b) => b, Err(e) => { set_errno(net_err_to_errno(e)); return -1; } }; + 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); + 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; @@ -310,6 +322,8 @@ pub unsafe extern "C" fn accept( rx_fd: accepted.rx.into_raw().0 as i32, tx_fd: accepted.tx.into_raw().0 as i32, notify_fd: 0, + nodelay: false, + broadcast: false, }; let new_fd = alloc_socket(new_entry); if new_fd < 0 { @@ -532,32 +546,57 @@ pub unsafe extern "C" fn close_socket(fd: i32) -> bool { // setsockopt / getsockopt +fn option_errno(refusal: sockopt::Refusal) -> i32 { + match refusal { + sockopt::Refusal::Short => EINVAL, + sockopt::Refusal::Fault => EFAULT, + } +} + #[no_mangle] pub unsafe extern "C" fn setsockopt( fd: i32, level: i32, optname: i32, optval: *const u8, - _optlen: SocklenT, + optlen: SocklenT, ) -> i32 { let slot = match sock_from_fd(fd) { Some(s) => s, None => { set_errno(EBADF); return -1; } }; - let entry = match slot.as_ref() { + let entry = match slot.as_mut() { Some(e) => e, None => { set_errno(EBADF); return -1; } }; - // TCP_NODELAY is the only option we actually send to netstack - if level == IPPROTO_TCP && optname == TCP_NODELAY && entry.netstack_id != 0 { - let val = if optval.is_null() { 0u32 } else { *(optval as *const i32) as u32 }; - if let Err(e) = toyos::net::tcp_set_option(TcpSocketId(entry.netstack_id), OPT_NODELAY, val) { - set_errno(net_err_to_errno(e)); - return -1; + // All other options silently succeed (SO_REUSEADDR, SO_KEEPALIVE, etc.) + let Some(option) = sockopt::kept(level, optname) else { return 0 }; + let on = match sockopt::switch(optval, optlen) { + Ok(on) => on, + Err(refusal) => { set_errno(option_errno(refusal)); return -1; } + }; + match option { + Kept::NoDelay if entry.netstack_id != 0 => { + if let Err(e) = toyos::net::tcp_set_option(TcpSocketId(entry.netstack_id), OPT_NODELAY, on as u32) { + set_errno(net_err_to_errno(e)); + return -1; + } + entry.nodelay = on; + } + Kept::NoDelay => {} + 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; } } - // All other options silently succeed (SO_REUSEADDR, SO_KEEPALIVE, etc.) 0 } @@ -578,37 +617,16 @@ pub unsafe extern "C" fn getsockopt( None => { set_errno(EBADF); return -1; } }; - if optval.is_null() || optlen.is_null() { - set_errno(EINVAL); - return -1; - } - - if level == IPPROTO_TCP && optname == TCP_NODELAY && entry.netstack_id != 0 { - match toyos::net::tcp_get_option(TcpSocketId(entry.netstack_id), OPT_NODELAY) { - Ok(val) => { - *(optval as *mut i32) = val as i32; - *optlen = 4; - return 0; - } - Err(e) => { - set_errno(net_err_to_errno(e)); - return -1; - } - } - } - - if level == SOL_SOCKET && optname == SO_ERROR { - *(optval as *mut i32) = 0; - *optlen = 4; - return 0; - } - - // Default: return 0 for unknown options - if *optlen >= 4 { - *(optval as *mut i32) = 0; - *optlen = 4; + let value = match sockopt::kept(level, optname) { + Some(Kept::NoDelay) => entry.nodelay as i32, + Some(Kept::Broadcast) => entry.broadcast as i32, + // Every other option reads 0, `SO_ERROR` among them. + None => 0, + }; + match sockopt::answer(value, optval, optlen) { + Ok(()) => 0, + Err(refusal) => { set_errno(option_errno(refusal)); -1 } } - 0 } // getpeername / getsockname diff --git a/userland/libc/src/sockopt.rs b/userland/libc/src/sockopt.rs new file mode 100644 index 00000000000..0c67a100869 --- /dev/null +++ b/userland/libc/src/sockopt.rs @@ -0,0 +1,73 @@ +//! The socket options libc keeps a value for, and how an option's value +//! crosses `setsockopt` and `getsockopt`: each an `int`, refused and truncated +//! as Linux does, whose option numbers these are. It reads nothing but what it +//! is handed, so the host tests it (`toyos-libc-copies`) against the host's +//! own two calls. + +use core::mem::size_of; + +pub(crate) const IPPROTO_TCP: i32 = 6; +pub(crate) const SOL_SOCKET: i32 = 1; +pub(crate) const SO_BROADCAST: i32 = 6; +pub(crate) const TCP_NODELAY: i32 = 1; + +/// An option a socket keeps a value for. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum Kept { + NoDelay, + Broadcast, +} + +/// The kept option `optname` names at `level`. +pub(crate) fn kept(level: i32, optname: i32) -> Option { + match (level, optname) { + (IPPROTO_TCP, TCP_NODELAY) => Some(Kept::NoDelay), + (SOL_SOCKET, SO_BROADCAST) => Some(Kept::Broadcast), + _ => None, + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum Refusal { + /// A value shorter than an `int`: `EINVAL`. + Short, + /// A null pointer where bytes are read or written: `EFAULT`. + Fault, +} + +/// What a setter is told: whether the `int` at `optval` is non-zero. The +/// length is judged before the pointer is. +/// +/// # Safety +/// `optval` is null or `optlen` readable bytes. +pub(crate) unsafe fn switch(optval: *const u8, optlen: u32) -> Result { + if (optlen as usize) < size_of::() { + return Err(Refusal::Short); + } + if optval.is_null() { + return Err(Refusal::Fault); + } + Ok(optval.cast::().read_unaligned() != 0) +} + +/// Answers a getter `value`: as many of its bytes as `*optlen` holds, at most +/// an `int`'s, and that count written back. A buffer no byte goes into may be +/// null. +/// +/// # Safety +/// `optlen` is null or a writable length, and `optval` null or `*optlen` +/// writable bytes. +pub(crate) unsafe fn answer(value: i32, optval: *mut u8, optlen: *mut u32) -> Result<(), Refusal> { + if optlen.is_null() { + return Err(Refusal::Fault); + } + let len = (*optlen as usize).min(size_of::()); + if len != 0 { + if optval.is_null() { + return Err(Refusal::Fault); + } + core::ptr::copy_nonoverlapping(value.to_ne_bytes().as_ptr(), optval, len); + } + *optlen = len as u32; + Ok(()) +} diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs index 76e8c7fcc90..b1a522882aa 100644 --- a/userland/netstack/src/main.rs +++ b/userland/netstack/src/main.rs @@ -582,7 +582,7 @@ impl Netstack { Some(MsgType::UdpClose) => self.handle_udp_close(&req, socket_set), Some(MsgType::DnsLookup) => self.handle_dns_lookup(req, socket_set), Some(MsgType::TcpSetOption) => self.handle_tcp_set_option(&req, socket_set), - Some(MsgType::TcpGetOption) => self.handle_tcp_get_option(&req, socket_set), + Some(MsgType::UdpSetOption) => self.handle_udp_set_option(&req), Some(MsgType::TcpConnectPiped) => self.handle_tcp_connect_piped(req, socket_set, iface), Some(MsgType::TcpBindPiped) => self.handle_tcp_bind_piped(&req, socket_set), Some(MsgType::TcpAcceptPiped) => self.handle_tcp_accept_piped(&req, socket_set), @@ -870,21 +870,19 @@ impl Netstack { } } - fn handle_tcp_get_option(&mut self, msg: &Request, socket_set: &mut SocketSet<'_>) { + /// smoltcp sends to a broadcast address for every socket, so the + /// permission has nothing here to switch. + fn handle_udp_set_option(&self, msg: &Request) { let Ok(req) = ipc::decode_payload::(msg.payload()) else { msg.client.error(ERR_INVALID_INPUT); return; }; - let Some(SocketKind::TcpStream(handle)) = self.sockets.get(&req.socket_id) else { + let Some(SocketKind::Udp(_)) = self.sockets.get(&req.socket_id) else { msg.client.error(ERR_NOT_CONNECTED); return; }; - let socket = socket_set.get_mut::(*handle); match req.option { - OPT_NODELAY => { - let val = if socket.nagle_enabled() { 0u32 } else { 1u32 }; - msg.client.result(&SocketOptionResponse { value: val }); - } + OPT_BROADCAST => msg.client.done(), _ => msg.client.error(ERR_INVALID_INPUT), } }