From 19e2d33ea00d8437a491b3985b62d37e044a0864 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 23:56:32 +0200 Subject: [PATCH 1/8] libc binds a datagram socket at its first send, keeps a TCP_NODELAY for connect, and holds an address as its octets Three defects of libc's sockets, each filed by #783, each with a guest C case on tests/netcase that fails without its fix. A sendto on a datagram socket never bound wrote the datagram to handle 0 and asked netstack to send from socket 0. It now binds the socket to a port netstack chooses, through the one function bind uses, so a SO_BROADCAST set before the socket existed in netstack is handed over on either path. A TCP_NODELAY set on a stream socket before connect answered 0 and was stored nowhere. It is kept in the socket's entry and connect hands it to netstack once the connection exists; a refusal there closes the connection and fails the connect. On a datagram socket the option is EINVAL, as the host answers, where it used to reach netstack as a stream's request with a datagram socket's id. sockaddr_in's address was read with to_be_bytes and written with from_be_bytes, so an address libc made itself came back right and one built with htonl or inet_pton was reversed. inaddr.rs holds the struct with its port and address as bytes: no integer of either exists to be read in the machine's order, and every site that read or wrote one goes through it. The texts move there with it and are host-tested against the host C library's. inet_addr reads POSIX's four forms with ISO C's number forms, where it read only four decimal octets; inet_pton takes numbers of one to three digits, where it took any count; inet_ntop needs only the room its text takes, where it refused every buffer under 16 bytes; and each answers EAFNOSUPPORT or ENOSPC where POSIX names it. netinet/in.h gains in_addr_t, in_port_t and INADDR_BROADCAST, and TCP_NODELAY moves to netinet/tcp.h, where POSIX has it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...oes-not-carry-its-listeners-tcp-nodelay.md | 28 +++ ...-drops-a-tcp-nodelay-set-before-connect.md | 23 -- ...rinfo-ignores-its-service-and-its-hints.md | 27 ++ ...ibc-reads-an-ipv4-address-in-host-order.md | 26 -- ...s-from-a-datagram-socket-it-never-bound.md | 28 --- ...-a-whole-sockaddr-into-a-shorter-buffer.md | 22 ++ ...-a-streams-tcp-nodelay-reached-netstack.md | 28 +++ issues/toyos-has-its-own-network-stack.md | 2 +- tests/common/compile.rs | 25 +- tests/libc-arch/src/internet_addresses.rs | 162 ++++++++++++ tests/libc-arch/src/lib.rs | 10 +- tests/netcase/addr_order.c | 101 ++++++++ tests/netcase/nodelay_kept.c | 69 +++++ tests/netcase/sendto_unbound.c | 75 ++++++ tests/toyos.rs | 52 ++++ userland/libc/include/arpa/inet.h | 2 +- userland/libc/include/netinet/in.h | 21 +- userland/libc/include/netinet/tcp.h | 6 + userland/libc/src/errno.rs | 1 + userland/libc/src/inaddr.rs | 105 ++++++++ userland/libc/src/lib.rs | 1 + userland/libc/src/socket.rs | 235 ++++++------------ 22 files changed, 798 insertions(+), 251 deletions(-) create mode 100644 issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md delete mode 100644 issues/libc-drops-a-tcp-nodelay-set-before-connect.md create mode 100644 issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md delete mode 100644 issues/libc-reads-an-ipv4-address-in-host-order.md delete mode 100644 issues/libc-sends-from-a-datagram-socket-it-never-bound.md create mode 100644 issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md create mode 100644 issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md create mode 100644 tests/libc-arch/src/internet_addresses.rs create mode 100644 tests/netcase/addr_order.c create mode 100644 tests/netcase/nodelay_kept.c create mode 100644 tests/netcase/sendto_unbound.c create mode 100644 userland/libc/include/netinet/tcp.h create mode 100644 userland/libc/src/inaddr.rs diff --git a/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md b/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md new file mode 100644 index 00000000000..cfd3e5d6127 --- /dev/null +++ b/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md @@ -0,0 +1,28 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# A connection libc accepts does not carry its listener's TCP_NODELAY + +`setsockopt(IPPROTO_TCP, TCP_NODELAY)` on a stream socket that is not +connected is kept in libc's socket entry (`userland/libc/src/socket.rs`) and +handed to netstack by `connect`. A listener is never connected: its value is +kept, `getsockopt` answers it, and `accept` makes the new connection's entry +with the option clear and sends netstack nothing. POSIX does not say what an +accepted socket inherits. Measured on Darwin 27.0.0, one C program: the +option set on a listener before `bind` answers 0, set after `listen` answers +0, and `getsockopt` on the socket `accept` returns reads it set. Linux is +unread. + +No guest test can ask: nothing dials into a guest. The harness's network is +QEMU's user backend with no forwarded port (`tests/common/qemu.rs`), and +whether a guest reaches its own address through netstack is unmeasured. + +**Exit**: `accept` hands a listener's kept `TCP_NODELAY` to netstack for the +connection it answers, as `connect` does, and a guest C case accepts a +connection a host server's peer dials and reads the option set on it. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`: the +case needs a way into the guest, which is the harness's. diff --git a/issues/libc-drops-a-tcp-nodelay-set-before-connect.md b/issues/libc-drops-a-tcp-nodelay-set-before-connect.md deleted file mode 100644 index cc081fbfd7b..00000000000 --- a/issues/libc-drops-a-tcp-nodelay-set-before-connect.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -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-getaddrinfo-ignores-its-service-and-its-hints.md b/issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md new file mode 100644 index 00000000000..26a46d1485a --- /dev/null +++ b/issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# libc's getaddrinfo ignores its service and its hints + +Read from the code, not run. `getaddrinfo` (`userland/libc/src/socket.rs`) +names its second and third arguments `_service` and `_hints` and reads +neither. Every answer's port is 0, so the usual C client, +`getaddrinfo(host, "80", &hints, &res)` and `connect(fd, res->ai_addr, +res->ai_addrlen)`, dials port 0. Every answer's `ai_socktype` is +`SOCK_STREAM` and its `ai_protocol` 0 whatever the hints ask, `AI_PASSIVE` +with a null node answers failure where POSIX has the wildcard address, and +`AI_NUMERICHOST` still asks the resolver. Every failure answers -1, which is +none of the `EAI_` codes `include/netdb.h` defines, and `gai_strerror` has +one text. A numeric host is read as `inet_pton` reads one, four decimal +numbers, where POSIX has `inet_addr`'s forms. + +**Exit**: `getaddrinfo` answers the service's port, numeric or refused by +name, and the hints' socket type, protocol and flags, each failure is the +`EAI_` code POSIX names for it, and a guest C case connects to an address +and port `getaddrinfo` answered. + +**Owner**: libc, under `issues/toyos-has-its-own-network-stack.md`: it +touches only `userland/libc` and a guest C case on `tests/netcase`. diff --git a/issues/libc-reads-an-ipv4-address-in-host-order.md b/issues/libc-reads-an-ipv4-address-in-host-order.md deleted file mode 100644 index 92e8912ff0e..00000000000 --- a/issues/libc-reads-an-ipv4-address-in-host-order.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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 deleted file mode 100644 index 588bebb00c5..00000000000 --- a/issues/libc-sends-from-a-datagram-socket-it-never-bound.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# libc sends from a datagram socket it never bound - -Read from the code, not run. 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 then asks netstack to -send from socket id 0. 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/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md b/issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md new file mode 100644 index 00000000000..3697ccf9e19 --- /dev/null +++ b/issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md @@ -0,0 +1,22 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# libc writes a whole sockaddr into a shorter buffer + +Read from the code, not run. `fill_sockaddr` +(`userland/libc/src/socket.rs`), which `accept`, `recvfrom`, `getpeername` +and `getsockname` answer through, writes sixteen bytes at the caller's +address and sets `*addrlen` to 16 without reading what `*addrlen` held. A +caller whose buffer is shorter has the bytes past it overwritten. POSIX: "If +the actual length of the address is greater than the length of the supplied +sockaddr structure, the stored address shall be truncated", with `*addrlen` +set to the address's own length. + +**Exit**: each of the four calls writes at most `*addrlen` bytes and answers +the address's length, and a host test of the write holds a buffer shorter +than the address. + +**Owner**: libc, under `issues/toyos-has-its-own-network-stack.md`. diff --git a/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md b/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md new file mode 100644 index 00000000000..744c02df7f0 --- /dev/null +++ b/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md @@ -0,0 +1,28 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# Nothing reads whether a stream's TCP_NODELAY reached netstack + +netstack answers `MsgType::TcpSetOption` done or with an error, and nothing +answers what a stream holds: the pipe ABI has no option read +(`toyos/src/net.rs`), and std's `nodelay()` and libc's `getsockopt` answer the +value their own side stored. So a setter that stores the value and sends +nothing is seen by no test. Measured on libc's `connect`, which hands over a +`TCP_NODELAY` set before the connection existed: with the hand-over deleted, +the guest case that sets the option before `connect` and reads it after +(`tests/netcase/nodelay_kept.c`, the `libc_sockets` test) stays green. The +only effect of the option is when a small write leaves the machine, which a +QEMU test may not time. + +**Exit**: a test reads what the stack holds for a stream's Nagle switch, on +the node under a host test of its option call or through a guest's wire, and +turns red when libc's `connect` or std's `set_nodelay` stores the value and +sends no request. + +**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 asks the same request. diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index c42edd5ca9b..d0b43925acc 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -25,7 +25,7 @@ What the node does not yet meet: - It counts a DHCP message [udp] refused as `node.dhcp-unsent`, whatever the rule; the `dhcp.renew-unroutable` scenario owed above is not written. Exit: that scenario names the counter, or the node counts the renewal apart. - With the link down the DHCP client keeps its timers: every 4 to 64 s the node has a deadline, builds a DISCOVER that [udp] refuses, and counts it in `dhcp.tx.discover` and `node.dhcp-unsent`, from `Node::new` on. Exit: `toyos-dhcp`'s client is told the link went down and waits for it, and the node's first DISCOVER is the one that leaves. - 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 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: the node hands the permission to `Udp::set_broadcast` and answers a send refused for want of it `ERR_PERMISSION_DENIED`, held by a node test. 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`, sends to the limited broadcast address before and after `set_broadcast(true)` 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 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: the node hands the permission to `Udp::set_broadcast` and answers a send refused for want of it `ERR_PERMISSION_DENIED`, held by a node test. 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`, sends to the limited broadcast address before and after `set_broadcast(true)` 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/netcase/sendto_unbound.c` holds that send for a socket permitted to make it. - 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/tests/common/compile.rs b/tests/common/compile.rs index d18e0d5273a..e2e2d242ed6 100644 --- a/tests/common/compile.rs +++ b/tests/common/compile.rs @@ -49,25 +49,26 @@ fn first_error(stderr: &[u8]) -> String { .to_string() } -/// Compile one of the corpus's files to an object with the toolchain's clang, -/// from the corpus's own directory and by its bare name, as TinyCC's runner -/// does: `__FILE__` is part of what some cases print. -fn compile_one(c: &CSysroot, source: &str, object: &Path) { +/// Compile `source` in `dir` to an object with the toolchain's clang, from +/// that directory and by its bare name. +fn compile_one(c: &CSysroot, dir: &Path, flags: &[&str], source: &str, object: &Path) { let output = Command::new(&c.clang) .args(c.args()) - .args(CORPUS_FLAGS) + .args(flags) .arg("-c") .arg(source) .arg("-o") .arg(object) - .current_dir(testcases_dir()) + .current_dir(dir) .output() .unwrap_or_else(|e| panic!("run {}: {e}", c.clang.display())); assert!(output.status.success(), "{}", first_error(&output.stderr)); } /// Compile a corpus case, and its companion if it has one (`104+_inline.c` -/// for `104_inline`), and return the objects. +/// for `104_inline`), and return the objects. Each is compiled as TinyCC's +/// runner does, in the corpus's directory by its bare name: `__FILE__` is +/// part of what some cases print. pub fn compile_c(name: &str) -> Vec { let c = c_sysroot(); let mut sources = vec![format!("{name}.c")]; @@ -82,12 +83,20 @@ pub fn compile_c(name: &str) -> Vec { .enumerate() .map(|(i, source)| { let object = scratch(name, &format!("-{i}.o")); - compile_one(&c, source, &object); + compile_one(&c, &testcases_dir(), CORPUS_FLAGS, source, &object); object }) .collect() } +/// Compile `name.c` in `dir`, a C program of ToyOS's own outside the corpus, +/// with every warning an error, and return its object. +pub fn compile_own_c(dir: &Path, name: &str) -> Vec { + let object = scratch(name, ".o"); + compile_one(&c_sysroot(), dir, &["-Wall", "-Wextra", "-Werror"], &format!("{name}.c"), &object); + vec![object] +} + /// Link flags a case needs beyond the corpus's, by case name. /// /// An image whose lowest address is 0 hides a table reported from the load bias diff --git a/tests/libc-arch/src/internet_addresses.rs b/tests/libc-arch/src/internet_addresses.rs new file mode 100644 index 00000000000..5b34896d1a0 --- /dev/null +++ b/tests/libc-arch/src/internet_addresses.rs @@ -0,0 +1,162 @@ +//! libc's IPv4 addresses (`inaddr.rs`) against the host C library's: the +//! texts `inet_pton` and `inet_addr` read, the text `inet_ntop` writes, and a +//! `sockaddr_in`'s bytes against the host's `htonl` and `htons`. Where the +//! hosts' own readers part from POSIX's text or from each other, the text is +//! held alone, each case saying which. + +use std::ffi::{c_char, c_int, c_void, CStr, CString}; + +use crate::inaddr::{dotted_quad, dotted_text, numbers_and_dots, SockaddrIn, AF_INET}; + +unsafe extern "C" { + fn inet_pton(af: c_int, src: *const c_char, dst: *mut c_void) -> c_int; + fn inet_ntop(af: c_int, src: *const c_void, dst: *mut c_char, size: u32) -> *const c_char; + fn inet_addr(cp: *const c_char) -> u32; + fn htonl(hostlong: u32) -> u32; + fn htons(hostshort: u16) -> u16; +} + +/// Every text of `min..=max` of `parts` between dots, each followed by each +/// of `ends`. +fn texts(parts: &[&str], counts: std::ops::RangeInclusive, ends: &[&str], mut each: impl FnMut(&str)) { + for count in counts { + for pick in 0..parts.len().pow(count as u32) { + let chosen: Vec<&str> = (0..count).map(|i| parts[pick / parts.len().pow(i as u32) % parts.len()]).collect(); + for end in ends { + each(&format!("{}{end}", chosen.join("."))); + } + } + } +} + +/// The host's `inet_pton` of `text`. +fn host_pton(text: &str) -> Option<[u8; 4]> { + let c = CString::new(text).unwrap(); + let mut octets = [0u8; 4]; + // SAFETY: a NUL-terminated string, and four bytes to write. + let answer = unsafe { inet_pton(AF_INET, c.as_ptr(), octets.as_mut_ptr().cast()) }; + assert!(answer == 0 || answer == 1, "the host's inet_pton({text:?}) answered {answer}"); + (answer == 1).then_some(octets) +} + +/// The host's `inet_addr` of `text`: its answer's bytes in memory order. +fn host_addr(text: &str) -> [u8; 4] { + let c = CString::new(text).unwrap(); + // SAFETY: a NUL-terminated string. + unsafe { inet_addr(c.as_ptr()) }.to_ne_bytes() +} + +/// libc's `inet_addr`, which answers `INADDR_NONE` for no address. +fn ours_addr(text: &str) -> [u8; 4] { + numbers_and_dots(text.as_bytes()).unwrap_or([0xff; 4]) +} + +const OCTETS: [&str; 10] = ["0", "1", "9", "10", "99", "100", "199", "200", "249", "255"]; + +#[test] +fn a_dotted_quad_reads_as_the_host_reads_it() { + let mut parts = OCTETS.to_vec(); + parts.extend(["256", "300", "999", "1000", "", "a", " ", "1 ", " 1", "-1", "+1", "1x", "0x1"]); + let mut read = 0; + let mut judge = |text: &str| { + let ours = dotted_quad(text.as_bytes()); + assert_eq!(ours, host_pton(text), "inet_pton({text:?})"); + read += usize::from(ours.is_some()); + }; + texts(&parts, 1..=4, &[""], &mut judge); + texts(&parts[..12], 5..=5, &[""], &mut judge); + texts(&OCTETS, 4..=4, &[".", " ", "\n", "x"], &mut judge); + assert_eq!(read, OCTETS.len().pow(4), "the texts that are addresses"); +} + +/// POSIX: each number is "a one to three-digit decimal number between 0 and +/// 255". glibc refuses a leading zero and Darwin takes any count of digits, +/// so neither is asked. +#[test] +fn a_dotted_quad_s_number_is_one_to_three_decimal_digits() { + assert_eq!(dotted_quad(b"010.001.00.0"), Some([10, 1, 0, 0])); + assert_eq!(dotted_quad(b"0001.2.3.4"), None); + assert_eq!(dotted_quad(b"1.2.3.0255"), None); +} + +#[test] +fn numbers_and_dots_read_as_the_host_reads_them() { + let parts = [ + "0", "1", "9", "255", "256", "65535", "65536", "16777215", "16777216", "4294967295", "0x0", "0xff", "0x100", + "0xFFFF", "0X10000", "0xffffff", "0x1000000", "0xffffffff", "0xg", "010", "0377", "0400", "08", "00", "", "a", + "-1", "+1", "1x", + ]; + let mut read = 0; + let mut judge = |text: &str| { + assert_eq!(ours_addr(text), host_addr(text), "inet_addr({text:?})"); + read += usize::from(numbers_and_dots(text.as_bytes()).is_some()); + }; + texts(&parts, 1..=3, &["", " ", "\n", "\t9", " x", "x", "."], &mut judge); + texts(&parts, 4..=4, &["", " x"], &mut judge); + texts(&parts[..6], 5..=5, &[""], &mut judge); + judge(" 1.2.3.4"); + judge("1.2.3.4\x0b"); + judge("1.2.3.4\x0c\r"); + assert!(read > 10_000, "{read} texts were addresses"); +} + +/// Where BSD's reader parts from POSIX's text, "a 32-bit value" in ISO C's +/// integer forms: it wraps a number past 32 bits and takes `0x` with no +/// digit for zero. +#[test] +fn a_number_past_32_bits_or_without_digits_is_no_address() { + assert_eq!(numbers_and_dots(b"4294967296"), None); + assert_eq!(numbers_and_dots(b"0x100000000"), None); + assert_eq!(numbers_and_dots(b"0x.1"), None); + assert_eq!(numbers_and_dots(b"1.0x"), None); +} + +#[test] +fn an_address_is_written_as_the_host_writes_it() { + for position in 0..4 { + for octet in 0..=255u8 { + let mut ip = [7, 77, 177, 0]; + ip[position] = octet; + let mut ours = [0xaau8; 16]; + let len = dotted_text(ip, &mut ours); + let mut host = [0 as c_char; 16]; + // SAFETY: four bytes to read and sixteen to write. + let wrote = unsafe { inet_ntop(AF_INET, ip.as_ptr().cast(), host.as_mut_ptr(), 16) }; + assert!(!wrote.is_null(), "the host's inet_ntop({ip:?})"); + // SAFETY: the host's NUL-terminated answer. + let want = unsafe { CStr::from_ptr(wrote) }.to_bytes_with_nul(); + assert_eq!(&ours[..=len], want, "{ip:?}"); + } + } + let mut longest = [0xaau8; 16]; + assert_eq!(dotted_text([255; 4], &mut longest), 15); + assert_eq!(&longest, b"255.255.255.255\0"); +} + +/// `sockaddr_in` as Linux and `include/netinet/in.h` lay it out: the family, +/// then the port's two bytes and the address's four in network order. +#[test] +fn a_sockaddr_holds_its_port_and_address_in_network_order() { + assert_eq!(size_of::(), 16); + // SAFETY: sixteen bytes with no padding, read as bytes. + let bytes: [u8; 16] = unsafe { std::mem::transmute(SockaddrIn::new([10, 0, 2, 2], 0x1234)) }; + let family = (AF_INET as u16).to_ne_bytes(); + assert_eq!(bytes, [family[0], family[1], 0x12, 0x34, 10, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0]); + + // What a C program writes: `sin_port = htons(port)` and + // `sin_addr.s_addr = htonl(address)`, each stored as the machine stores it. + let mut written = [0u8; 16]; + written[..2].copy_from_slice(&family); + // SAFETY: the host's byte swaps. + let (port, address) = unsafe { (htons(0x1234), htonl(0x0a00_0202)) }; + written[2..4].copy_from_slice(&port.to_ne_bytes()); + written[4..8].copy_from_slice(&address.to_ne_bytes()); + // SAFETY: every sixteen bytes are a `SockaddrIn`. + let read: SockaddrIn = unsafe { std::mem::transmute(written) }; + assert_eq!(read.endpoint(), Some(([10, 0, 2, 2], 0x1234))); + + written[..2].copy_from_slice(&1u16.to_ne_bytes()); + // SAFETY: as above. + let unix: SockaddrIn = unsafe { std::mem::transmute(written) }; + assert_eq!(unix.endpoint(), None, "AF_UNIX's is no IPv4 address"); +} diff --git a/tests/libc-arch/src/lib.rs b/tests/libc-arch/src/lib.rs index c1a68464238..08d1bedb416 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 IPv4 addresses and their texts against the host C +//! library's. #[cfg(test)] extern crate alloc; @@ -25,6 +26,9 @@ mod fdreq; #[path = "../../../userland/libc/src/fparts.rs"] mod fparts; #[cfg(test)] +#[path = "../../../userland/libc/src/inaddr.rs"] +mod inaddr; +#[cfg(test)] #[path = "../../../userland/libc/src/linkreq.rs"] mod linkreq; #[cfg(test)] @@ -62,6 +66,8 @@ mod fparts_differential; #[cfg(test)] mod header; #[cfg(test)] +mod internet_addresses; +#[cfg(test)] mod link_requests; #[cfg(test)] mod listing_reader; diff --git a/tests/netcase/addr_order.c b/tests/netcase/addr_order.c new file mode 100644 index 00000000000..7276c94e843 --- /dev/null +++ b/tests/netcase/addr_order.c @@ -0,0 +1,101 @@ +/* An IPv4 address is in network byte order wherever a C program hands libc + one or is handed one: connect reaches the same host for an address built + with inet_pton, with htonl, with inet_addr and by getaddrinfo, and + getpeername answers it. argv: the address, as four decimal octets, and the + port of a host that accepts and holds each connection. */ +#include +#include +#include +#include +#include +#include +#include + +static int wrong; + +static void said(const char *what, long got, long want) { + printf("%s: %ld%s\n", what, got, got == want ? "" : " <-- WRONG"); + if (got != want) wrong++; +} + +/* connect a fresh stream socket to `addr` at `port`; the socket, or -1. */ +static int dial(struct in_addr addr, in_port_t port) { + struct sockaddr_in to; + memset(&to, 0, sizeof to); + to.sin_family = AF_INET; + to.sin_port = port; + to.sin_addr = addr; + int fd = socket(AF_INET, SOCK_STREAM, 0); + return connect(fd, (struct sockaddr *)&to, sizeof to) == 0 ? fd : -1; +} + +static void text_of(const char *what, in_addr_t host_order, const char *want) { + struct in_addr a; + char text[INET_ADDRSTRLEN]; + a.s_addr = htonl(host_order); + const char *got = inet_ntop(AF_INET, &a, text, sizeof text); + printf("%s: %s%s\n", what, got ? got : "(null)", got && strcmp(got, want) == 0 ? "" : " <-- WRONG"); + if (!got || strcmp(got, want) != 0) wrong++; +} + +int main(int argc, char **argv) { + if (argc != 3) return 2; + in_port_t port = htons((uint16_t)atoi(argv[2])); + unsigned char octets[4], in_memory[4]; + struct in_addr by_pton, by_htonl, by_inet_addr; + char text[INET_ADDRSTRLEN]; + + /* The octets, read from the text by nothing of the network's. */ + char *at = argv[1]; + for (int i = 0; i < 4; i++) { + octets[i] = (unsigned char)strtoul(at, &at, 10); + at++; + } + in_addr_t host_order = (in_addr_t)octets[0] << 24 | (in_addr_t)octets[1] << 16 | (in_addr_t)octets[2] << 8 | octets[3]; + + said("inet_pton reads the address", inet_pton(AF_INET, argv[1], &by_pton), 1); + memcpy(in_memory, &by_pton, 4); + said("into its octets in memory order", memcmp(in_memory, octets, 4) == 0, 1); + by_htonl.s_addr = htonl(host_order); + said("htonl builds the same four bytes", memcmp(&by_htonl, octets, 4) == 0, 1); + by_inet_addr.s_addr = inet_addr(argv[1]); + said("and so does inet_addr", memcmp(&by_inet_addr, octets, 4) == 0, 1); + said("ntohl gives the number back", ntohl(by_pton.s_addr) == host_order, 1); + + int a = dial(by_pton, port); + said("connect to inet_pton's address", a >= 0, 1); + said("connect to htonl's", dial(by_htonl, port) >= 0, 1); + said("connect to inet_addr's", dial(by_inet_addr, port) >= 0, 1); + + struct sockaddr_in name; + socklen_t len = sizeof name; + memset(&name, 0, sizeof name); + said("getpeername", getpeername(a, (struct sockaddr *)&name, &len), 0); + said("answers the address", memcmp(&name.sin_addr, octets, 4) == 0, 1); + said("and the port", name.sin_port == port, 1); + const char *shown = inet_ntop(AF_INET, &name.sin_addr, text, sizeof text); + said("which inet_ntop writes as it was given", shown && strcmp(shown, argv[1]) == 0, 1); + + struct addrinfo hints, *found = NULL; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + said("getaddrinfo of the address", getaddrinfo(argv[1], NULL, &hints, &found), 0); + if (found) { + struct in_addr by_getaddrinfo = ((struct sockaddr_in *)found->ai_addr)->sin_addr; + said("answers the same four bytes", memcmp(&by_getaddrinfo, octets, 4) == 0, 1); + said("connect to getaddrinfo's", dial(by_getaddrinfo, port) >= 0, 1); + freeaddrinfo(found); + } + + text_of("INADDR_LOOPBACK", INADDR_LOOPBACK, "127.0.0.1"); + text_of("INADDR_BROADCAST", INADDR_BROADCAST, "255.255.255.255"); + text_of("INADDR_ANY", INADDR_ANY, "0.0.0.0"); + + if (wrong) { + printf("addr_order: %d wrong\n", wrong); + return 1; + } + printf("addr_order: ok\n"); + return 0; +} diff --git a/tests/netcase/nodelay_kept.c b/tests/netcase/nodelay_kept.c new file mode 100644 index 00000000000..801db9eb002 --- /dev/null +++ b/tests/netcase/nodelay_kept.c @@ -0,0 +1,69 @@ +/* A TCP_NODELAY set on a stream socket before connect is kept and holds for + the connection, and one cleared stays cleared. argv: the address and the + port of a host that accepts and holds each connection. */ +#include +#include +#include +#include +#include +#include +#include +#include + +static int wrong; + +static void said(const char *what, long got, long want) { + printf("%s: %ld%s\n", what, got, got == want ? "" : " <-- WRONG"); + if (got != want) wrong++; +} + +/* Whether TCP_NODELAY is set on `fd`: 1 or 0, or -1 where the read fails. */ +static int nodelay(int fd) { + int value = -7; + socklen_t len = sizeof value; + if (getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &value, &len) != 0) return -1; + return value != 0; +} + +int main(int argc, char **argv) { + if (argc != 3) return 2; + struct sockaddr_in peer; + int on = 1, off = 0; + + memset(&peer, 0, sizeof peer); + peer.sin_family = AF_INET; + peer.sin_port = htons((uint16_t)atoi(argv[2])); + said("the peer's address is one", inet_pton(AF_INET, argv[1], &peer.sin_addr), 1); + + int t = socket(AF_INET, SOCK_STREAM, 0); + said("a fresh stream socket's TCP_NODELAY", nodelay(t), 0); + said("set before connect", setsockopt(t, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + said("read before connect", nodelay(t), 1); + said("connect", connect(t, (struct sockaddr *)&peer, sizeof peer), 0); + said("read after connect", nodelay(t), 1); + said("cleared on the connection", setsockopt(t, IPPROTO_TCP, TCP_NODELAY, &off, sizeof off), 0); + said("read after the clear", nodelay(t), 0); + said("set on the connection", setsockopt(t, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + said("read after the set", nodelay(t), 1); + + int u = socket(AF_INET, SOCK_STREAM, 0); + said("set on a second socket", setsockopt(u, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + said("and cleared before connect", setsockopt(u, IPPROTO_TCP, TCP_NODELAY, &off, sizeof off), 0); + said("connect", connect(u, (struct sockaddr *)&peer, sizeof peer), 0); + said("read after connect", nodelay(u), 0); + + int d = socket(AF_INET, SOCK_DGRAM, 0); + errno = 0; + said("set on a datagram socket", setsockopt(d, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), -1); + said("which is invalid at that socket", errno == EINVAL, 1); + errno = 0; + said("nor read from one", nodelay(d), -1); + said("for the same reason", errno == EINVAL, 1); + + if (wrong) { + printf("nodelay_kept: %d wrong\n", wrong); + return 1; + } + printf("nodelay_kept: ok\n"); + return 0; +} diff --git a/tests/netcase/sendto_unbound.c b/tests/netcase/sendto_unbound.c new file mode 100644 index 00000000000..9311a5f92cf --- /dev/null +++ b/tests/netcase/sendto_unbound.c @@ -0,0 +1,75 @@ +/* A datagram socket never bound is bound by its first send, to a port the + stack chooses, as POSIX has it: the usual C broadcast sequence, socket, + setsockopt, sendto, and a connected socket's send. argv: the address and + the port of a host that answers each datagram with itself. */ +#include +#include +#include +#include +#include +#include + +static int wrong; + +static void said(const char *what, long got, long want) { + printf("%s: %ld%s\n", what, got, got == want ? "" : " <-- WRONG"); + if (got != want) wrong++; +} + +/* The port `fd` is bound to, in host order. */ +static unsigned bound_port(int fd) { + struct sockaddr_in name; + socklen_t len = sizeof name; + memset(&name, 0, sizeof name); + if (getsockname(fd, (struct sockaddr *)&name, &len) != 0) return 0; + return ntohs(name.sin_port); +} + +int main(int argc, char **argv) { + if (argc != 3) return 2; + struct sockaddr_in peer, from, all; + socklen_t fromlen = sizeof from; + char buf[16]; + int on = 1, held = 0; + socklen_t heldlen = sizeof held; + + memset(&peer, 0, sizeof peer); + peer.sin_family = AF_INET; + peer.sin_port = htons((uint16_t)atoi(argv[2])); + said("the peer's address is one", inet_pton(AF_INET, argv[1], &peer.sin_addr), 1); + + int s = socket(AF_INET, SOCK_DGRAM, 0); + said("SO_BROADCAST set on a socket never bound", setsockopt(s, SOL_SOCKET, SO_BROADCAST, &on, sizeof on), 0); + said("sendto from it", sendto(s, "ping", 4, 0, (struct sockaddr *)&peer, sizeof peer), 4); + said("the send bound it to a port", bound_port(s) != 0, 1); + + memset(&from, 0, sizeof from); + memset(buf, 0, sizeof buf); + said("the answer came back to that port", recvfrom(s, buf, sizeof buf, 0, (struct sockaddr *)&from, &fromlen), 4); + said("and is what was sent", memcmp(buf, "ping", 4) == 0, 1); + said("from the peer's address", from.sin_addr.s_addr == peer.sin_addr.s_addr, 1); + said("and the peer's port", from.sin_port == peer.sin_port, 1); + + said("the option is read back", getsockopt(s, SOL_SOCKET, SO_BROADCAST, &held, &heldlen), 0); + said("and is still set", held != 0, 1); + memset(&all, 0, sizeof all); + all.sin_family = AF_INET; + all.sin_port = htons(9); + all.sin_addr.s_addr = htonl(INADDR_BROADCAST); + said("sendto the limited broadcast address", sendto(s, "x", 1, 0, (struct sockaddr *)&all, sizeof all), 1); + + int c = socket(AF_INET, SOCK_DGRAM, 0); + said("connect on a second socket never bound", connect(c, (struct sockaddr *)&peer, sizeof peer), 0); + said("send from it", send(c, "pong", 4, 0), 4); + memset(buf, 0, sizeof buf); + said("its answer", recv(c, buf, sizeof buf, 0), 4); + said("is what it sent", memcmp(buf, "pong", 4) == 0, 1); + said("the two sockets hold two ports", bound_port(c) != 0 && bound_port(c) != bound_port(s), 1); + + if (wrong) { + printf("sendto_unbound: %d wrong\n", wrong); + return 1; + } + printf("sendto_unbound: ok\n"); + return 0; +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 78bdb410732..4b1110e43db 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -260,6 +260,10 @@ const MACHINE_TESTS: &[&str] = &[ // connections: netstack is one binary that owns its NIC, with no host // build, and the T14's peer is the bench's network. "netstack_socket_churn", + // What libc's socket calls ask of netstack, read back from a peer that + // answers: the calls are libc's requests on netstack's port, netstack has + // no host build, and the T14's peer is the bench's network. + "libc_sockets", // The nested-NMI report is a raw write to the 16550, which the T14 does not // have. "nested_nmi_is_loud", @@ -2899,12 +2903,60 @@ fn netstack_socket_churn() -> Result<(), String> { Ok(()) } +/// libc's sockets as a C program uses them, on one boot of `tests/netcase`: +/// each of its C cases dials a host server that holds what it accepts, or +/// sends to one that answers each datagram with itself, at the address the +/// guest's network gives the host. A case's own comparisons are its verdict. +fn libc_sockets() -> Result<(), String> { + const LEASED: &str = "netstack: DHCP: lease "; + const HOST: &str = "10.0.2.2"; + let holder = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the holding server: {e}"))?; + let holding = holder.local_addr().map_err(|e| format!("the holding server's port: {e}"))?.port(); + // Each ends with the process: a guest that never dials leaves it waiting. + thread::spawn(move || holder.incoming().collect::>()); + let echo = std::net::UdpSocket::bind(("127.0.0.1", 0)).map_err(|e| format!("the answering server: {e}"))?; + let answering = echo.local_addr().map_err(|e| format!("the answering server's port: {e}"))?.port(); + thread::spawn(move || { + let mut datagram = [0u8; 64]; + while let Ok((len, from)) = echo.recv_from(&mut datagram) { + echo.send_to(&datagram[..len], from).expect("answer a datagram"); + } + }); + + let cases = [("sendto_unbound", answering), ("nodelay_kept", holding), ("addr_order", holding)]; + let case = compile::repo_root().join("tests/netcase"); + let bins: Vec<(String, Vec)> = cases + .iter() + .map(|(name, _)| (name.to_string(), compile::link_toyos(&compile::compile_own_c(&case, name), name))) + .collect(); + let mut qemu = QemuInstance::boot_with_options(&case, &bins, &[], BootOptions::default()); + let mut console = qemu.boot_log().to_string(); + await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + for (name, port) in cases { + let result = qemu.run_test(&format!("test_c_{name} {HOST} {port}"), Duration::from_secs(120)); + if qemu::VERBOSE.load(std::sync::atomic::Ordering::Relaxed) { + eprintln!(" [libc] {name} ended {:?} and said:\n{}", result.exit_code, result.stdout); + } + if let Some(why) = &result.error { + return Err(format!("{name}: {why}\nthe case said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("{name} ended {:?}:\n{}", result.exit_code, result.stdout)); + } + if !result.stdout.lines().any(|l| l.trim_end().ends_with(&format!("{name}: ok"))) { + return Err(format!("{name} never said it was done:\n{}", result.stdout)); + } + } + Ok(()) +} + /// Run the machine-shape test, which owns its QEMU: the machine shape *is* the /// test. fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { match name { "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config), "netstack_socket_churn" => netstack_socket_churn(), + "libc_sockets" => libc_sockets(), "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config), "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), diff --git a/userland/libc/include/arpa/inet.h b/userland/libc/include/arpa/inet.h index 145d3a58ab8..0376cca209a 100644 --- a/userland/libc/include/arpa/inet.h +++ b/userland/libc/include/arpa/inet.h @@ -9,7 +9,7 @@ extern "C" { int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); -uint32_t inet_addr(const char *cp); +in_addr_t inet_addr(const char *cp); #ifdef __cplusplus } diff --git a/userland/libc/include/netinet/in.h b/userland/libc/include/netinet/in.h index 66a6bf8dabb..4b7e83aaa89 100644 --- a/userland/libc/include/netinet/in.h +++ b/userland/libc/include/netinet/in.h @@ -8,21 +8,26 @@ extern "C" { #endif -#define INADDR_ANY ((uint32_t)0x00000000) -#define INADDR_LOOPBACK ((uint32_t)0x7f000001) -#define INADDR_NONE ((uint32_t)0xffffffff) +typedef uint16_t in_port_t; +typedef uint32_t in_addr_t; -#define INET_ADDRSTRLEN 16 +/* In host byte order: a program passes each through htonl. */ +#define INADDR_ANY ((in_addr_t)0x00000000) +#define INADDR_LOOPBACK ((in_addr_t)0x7f000001) +#define INADDR_BROADCAST ((in_addr_t)0xffffffff) +#define INADDR_NONE ((in_addr_t)0xffffffff) -#define TCP_NODELAY 1 +#define INET_ADDRSTRLEN 16 +/* Both in network byte order: the port's two bytes and the address's four + octets, in memory order. */ struct in_addr { - uint32_t s_addr; + in_addr_t s_addr; }; struct sockaddr_in { - unsigned short sin_family; - uint16_t sin_port; + sa_family_t sin_family; + in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; }; diff --git a/userland/libc/include/netinet/tcp.h b/userland/libc/include/netinet/tcp.h new file mode 100644 index 00000000000..c8a1a4bc495 --- /dev/null +++ b/userland/libc/include/netinet/tcp.h @@ -0,0 +1,6 @@ +#ifndef _NETINET_TCP_H +#define _NETINET_TCP_H + +#define TCP_NODELAY 1 + +#endif diff --git a/userland/libc/src/errno.rs b/userland/libc/src/errno.rs index ce9cd41f9ce..ed42de83200 100644 --- a/userland/libc/src/errno.rs +++ b/userland/libc/src/errno.rs @@ -30,6 +30,7 @@ pub(crate) const EBUSY: i32 = 16; pub(crate) const EEXIST: i32 = 17; pub(crate) const ENODEV: i32 = 19; pub(crate) const EINVAL: i32 = 22; +pub(crate) const ENOSPC: i32 = 28; pub(crate) const EPIPE: i32 = 32; pub(crate) const ERANGE: i32 = 34; pub(crate) const EDEADLK: i32 = 35; diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs new file mode 100644 index 00000000000..d54440019b3 --- /dev/null +++ b/userland/libc/src/inaddr.rs @@ -0,0 +1,105 @@ +//! IPv4 addresses and ports as a C program holds them, which is network byte +//! order: an address is its four octets in memory order and a port its two, +//! high first. Every address libc reads from or writes to a C program passes +//! through [`SockaddrIn`], which holds both as bytes, so no integer of either +//! exists to be read in the machine's order. The texts `inet_pton`, +//! `inet_addr` and `inet_ntop` read and write are here too. It reads and sets +//! nothing but what it is handed, so the host tests it (`toyos-libc-copies`). + +pub(crate) const AF_INET: i32 = 2; + +/// C's `struct sockaddr_in`, as `include/netinet/in.h` lays it out. +#[repr(C)] +pub(crate) struct SockaddrIn { + sin_family: u16, + sin_port: [u8; 2], + sin_addr: [u8; 4], + sin_zero: [u8; 8], +} + +impl SockaddrIn { + pub(crate) fn new(ip: [u8; 4], port: u16) -> Self { + Self { sin_family: AF_INET as u16, sin_port: port.to_be_bytes(), sin_addr: ip, sin_zero: [0; 8] } + } + + /// The address and port, or `None` for another family's. + pub(crate) fn endpoint(&self) -> Option<([u8; 4], u16)> { + (self.sin_family == AF_INET as u16).then(|| (self.sin_addr, u16::from_be_bytes(self.sin_port))) + } +} + +/// `inet_pton`'s text, POSIX's `ddd.ddd.ddd.ddd`: four decimal numbers of one +/// to three digits, each at most 255, and nothing else. +pub(crate) fn dotted_quad(text: &[u8]) -> Option<[u8; 4]> { + let mut octets = [0u8; 4]; + let mut parts = text.split(|&c| c == b'.'); + for octet in &mut octets { + let part = parts.next()?; + if !(1..=3).contains(&part.len()) || !part.iter().all(u8::is_ascii_digit) { + return None; + } + let value = part.iter().fold(0u16, |v, &c| v * 10 + u16::from(c - b'0')); + *octet = u8::try_from(value).ok()?; + } + parts.next().is_none().then_some(octets) +} + +/// One number of `inet_addr`'s text, as ISO C writes an integer constant: +/// `0x` and hexadecimal digits, `0` and octal digits, or decimal digits. +fn number(part: &[u8]) -> Option { + let (digits, radix) = match part { + [b'0', b'x' | b'X', rest @ ..] => (rest, 16), + [b'0', rest @ ..] if !rest.is_empty() => (rest, 8), + _ => (part, 10), + }; + if digits.is_empty() { + return None; + } + digits.iter().try_fold(0u32, |v, &c| v.checked_mul(radix)?.checked_add(char::from(c).to_digit(radix)?)) +} + +/// `inet_addr`'s text: one to four numbers between dots, each but the last +/// one octet and the last every octet that is left, so `127.1` is 127.0.0.1. +/// The address ends at the string's end or at white space, as BSD's reader +/// and glibc's have it; POSIX names no end. +pub(crate) fn numbers_and_dots(text: &[u8]) -> Option<[u8; 4]> { + let end = text.iter().position(|c| matches!(c, b' ' | b'\t'..=b'\r')).unwrap_or(text.len()); + let mut numbers = [0u32; 4]; + let mut count = 0; + for part in text[..end].split(|&c| c == b'.') { + *numbers.get_mut(count)? = number(part)?; + count += 1; + } + let (&last, leading) = numbers[..count].split_last()?; + let mut octets = [0u8; 4]; + for (octet, &n) in octets.iter_mut().zip(leading) { + *octet = u8::try_from(n).ok()?; + } + let left = 4 - leading.len(); + if u64::from(last) >> (8 * left) != 0 { + return None; + } + octets[leading.len()..].copy_from_slice(&last.to_be_bytes()[leading.len()..]); + Some(octets) +} + +/// `ip` as `inet_ntop` writes it into `buf`, NUL-terminated; the text's +/// length without the NUL. `buf` is `INET_ADDRSTRLEN` bytes, which holds the +/// longest. +pub(crate) fn dotted_text(ip: [u8; 4], buf: &mut [u8; 16]) -> usize { + let mut at = 0; + for (i, octet) in ip.into_iter().enumerate() { + if i > 0 { + buf[at] = b'.'; + at += 1; + } + let digits = [octet / 100, octet / 10 % 10, octet % 10]; + let skip = if octet >= 100 { 0 } else if octet >= 10 { 1 } else { 2 }; + for digit in &digits[skip..] { + buf[at] = b'0' + digit; + at += 1; + } + } + buf[at] = 0; + at +} diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 8ea47af3be4..f589ca7061d 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -10,6 +10,7 @@ mod elfsym; mod errno; mod fdreq; mod fparts; +mod inaddr; mod link; mod linkreq; mod listing; diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs index 8d3bfb44194..e60a7c7afd8 100644 --- a/userland/libc/src/socket.rs +++ b/userland/libc/src/socket.rs @@ -7,26 +7,13 @@ use toyos_abi::RawHandle; use toyos_abi::syscall; use toyos::net::{NetError, TcpSocketId, UdpSocketId, OPT_BROADCAST, OPT_NODELAY}; -use crate::errno::{EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EINVAL, EIO, ENOMEM, ENOTCONN, ETIMEDOUT}; +use crate::errno::{EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EINVAL, EIO, ENOMEM, ENOSPC, ENOTCONN, ETIMEDOUT}; +use crate::inaddr::{self, SockaddrIn, AF_INET}; // C types matching POSIX type SocklenT = u32; -#[repr(C)] -struct SockaddrIn { - sin_family: u16, - sin_port: u16, // network byte order (big-endian) - sin_addr: InAddr, - sin_zero: [u8; 8], -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct InAddr { - s_addr: u32, // network byte order -} - #[repr(C)] pub struct Sockaddr { sa_family: u16, @@ -45,7 +32,6 @@ pub struct Addrinfo { ai_next: *mut Addrinfo, } -const AF_INET: i32 = 2; const AF_UNSPEC: i32 = 0; const SOCK_STREAM: i32 = 1; const SOCK_DGRAM: i32 = 2; @@ -78,8 +64,9 @@ struct SocketEntry { 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. + // socket netstack does not hold yet keeps each for the call that makes it + // there to hand over: `broadcast` for a datagram socket's bind, `nodelay` + // for a stream's `connect`. nodelay: bool, broadcast: bool, } @@ -132,13 +119,7 @@ unsafe fn parse_sockaddr(addr: *const Sockaddr, len: SocklenT) -> Option<([u8; 4 if addr.is_null() || (len as usize) < core::mem::size_of::() { return None; } - let sin = &*(addr as *const SockaddrIn); - if sin.sin_family != AF_INET as u16 { - return None; - } - let port = u16::from_be(sin.sin_port); - let ip = sin.sin_addr.s_addr.to_be_bytes(); // s_addr is network order - Some((ip, port)) + (*(addr as *const SockaddrIn)).endpoint() } /// Fill a sockaddr_in from ip + port. @@ -146,14 +127,29 @@ unsafe fn fill_sockaddr(addr: *mut Sockaddr, addrlen: *mut SocklenT, ip: [u8; 4] if addr.is_null() || addrlen.is_null() { return; } - let sin = &mut *(addr as *mut SockaddrIn); - sin.sin_family = AF_INET as u16; - sin.sin_port = port.to_be(); - sin.sin_addr.s_addr = u32::from_be_bytes(ip); - sin.sin_zero = [0; 8]; + (addr as *mut SockaddrIn).write(SockaddrIn::new(ip, port)); *addrlen = core::mem::size_of::() as SocklenT; } +/// 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); + } + } + 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(()) +} + // BSD socket API #[no_mangle] @@ -212,6 +208,14 @@ pub unsafe extern "C" fn connect(fd: i32, addr: *const Sockaddr, addrlen: Sockle Ok(c) => c, Err(e) => { set_errno(net_err_to_errno(e)); return -1; } }; + if entry.nodelay { + if let Err(e) = toyos::net::tcp_set_option(conn.socket_id, OPT_NODELAY, 1) { + // `conn`'s pipe ends close where it drops. + let _ = toyos::net::tcp_close(conn.socket_id); + set_errno(net_err_to_errno(e)); + return -1; + } + } entry.netstack_id = conn.socket_id.0; entry.local_port = conn.local_port; entry.remote_addr = ip; @@ -258,23 +262,10 @@ pub unsafe extern "C" fn bind(fd: i32, addr: *const Sockaddr, addrlen: SocklenT) 0 } SocketKind::Udp => { - let bound = match toyos::net::udp_bind(ip, port) { - 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; - } + if let Err(e) = bind_datagram(entry, ip, port) { + 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.tx_fd = bound.tx.into_raw().0 as i32; - entry.rx_fd = bound.rx.into_raw().0 as i32; 0 } } @@ -368,7 +359,7 @@ pub unsafe extern "C" fn send(fd: i32, buf: *const u8, len: usize, _flags: i32) return -1; } sendto(fd, buf, len, _flags, - &make_sockaddr_in(entry.remote_addr, entry.remote_port) as *const SockaddrIn as *const Sockaddr, + &SockaddrIn::new(entry.remote_addr, entry.remote_port) as *const SockaddrIn as *const Sockaddr, core::mem::size_of::() as SocklenT) } } @@ -418,7 +409,7 @@ pub unsafe extern "C" fn sendto( 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; } }; @@ -433,6 +424,14 @@ pub unsafe extern "C" fn sendto( Some(v) => v, None => { set_errno(EINVAL); return -1; } }; + // POSIX: a socket not yet bound is bound at its first send, to a + // port the stack chooses. + if entry.netstack_id == 0 { + if let Err(e) = bind_datagram(entry, [0; 4], 0) { + set_errno(net_err_to_errno(e)); + return -1; + } + } // Write data to tx pipe let data = core::slice::from_raw_parts(buf, len); if let Err(_) = syscall::write(RawHandle(entry.tx_fd as u32), data) { @@ -551,6 +550,12 @@ pub unsafe extern "C" fn close_socket(fd: i32) -> bool { // setsockopt / getsockopt +/// POSIX's `EINVAL`, an option invalid at the socket's level: TCP's are no +/// datagram socket's. +fn tcp_option_of_a_datagram_socket(entry: &SocketEntry, level: i32, optname: i32) -> bool { + (level, optname) == (IPPROTO_TCP, TCP_NODELAY) && matches!(entry.kind, SocketKind::Udp) +} + #[no_mangle] pub unsafe extern "C" fn setsockopt( fd: i32, @@ -572,16 +577,20 @@ pub unsafe extern "C" fn setsockopt( if !matches!((level, optname), (IPPROTO_TCP, TCP_NODELAY) | (SOL_SOCKET, SO_BROADCAST)) { return 0; } - if optval.is_null() || (optlen as usize) < core::mem::size_of::() { + if optval.is_null() || (optlen as usize) < core::mem::size_of::() || tcp_option_of_a_datagram_socket(entry, level, optname) { set_errno(EINVAL); return -1; } let on = (optval as *const i32).read_unaligned() != 0; match (level, optname) { - (IPPROTO_TCP, TCP_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; + (IPPROTO_TCP, TCP_NODELAY) => { + // netstack holds a connection from `connect` or `accept`; a + // listener's id names none. + if entry.connected { + 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; } @@ -618,7 +627,7 @@ pub unsafe extern "C" fn getsockopt( None => { set_errno(EBADF); return -1; } }; - if optval.is_null() || optlen.is_null() { + if optval.is_null() || optlen.is_null() || tcp_option_of_a_datagram_socket(entry, level, optname) { set_errno(EINVAL); return -1; } @@ -705,7 +714,7 @@ pub unsafe extern "C" fn getaddrinfo( let name = core::slice::from_raw_parts(node, name_len); // Try parsing as IPv4 literal first - if let Some(ip) = parse_ipv4(name) { + if let Some(ip) = inaddr::dotted_quad(name) { return build_addrinfo_result(res, &[(ip, 0)]); } @@ -742,10 +751,7 @@ unsafe fn build_addrinfo_result(res: *mut *mut Addrinfo, addrs: &[([u8; 4], u16) return -1; } - (*sa).sin_family = AF_INET as u16; - (*sa).sin_port = port.to_be(); - (*sa).sin_addr.s_addr = u32::from_be_bytes(ip); - (*sa).sin_zero = [0; 8]; + sa.write(SockaddrIn::new(ip, port)); (*ai).ai_flags = 0; (*ai).ai_family = AF_INET; @@ -779,16 +785,16 @@ pub unsafe extern "C" fn gai_strerror(_errcode: i32) -> *const u8 { b"DNS lookup failed\0".as_ptr() } -// inet_pton / inet_ntop / htons / ntohs / htonl / ntohl +// inet_pton / inet_ntop / inet_addr / htons / ntohs / htonl / ntohl #[no_mangle] pub unsafe extern "C" fn inet_pton(af: i32, src: *const u8, dst: *mut u8) -> i32 { - if af != AF_INET || src.is_null() || dst.is_null() { - return 0; + if af != AF_INET { + set_errno(EAFNOSUPPORT); + return -1; } - let len = super::string::strlen(src); - let s = core::slice::from_raw_parts(src, len); - match parse_ipv4(s) { + let text = core::slice::from_raw_parts(src, super::string::strlen(src)); + match inaddr::dotted_quad(text) { Some(ip) => { ptr::copy_nonoverlapping(ip.as_ptr(), dst, 4); 1 @@ -804,51 +810,20 @@ pub unsafe extern "C" fn inet_ntop( dst: *mut u8, size: SocklenT, ) -> *const u8 { - if af != AF_INET || src.is_null() || dst.is_null() || size < 16 { + if af != AF_INET { + set_errno(EAFNOSUPPORT); return ptr::null(); } - let a = *src; - let b = *src.add(1); - let c = *src.add(2); - let d = *src.add(3); - let mut buf = [0u8; 16]; - let n = fmt_ip4(&mut buf, a, b, c, d); + let mut text = [0u8; 16]; + let n = inaddr::dotted_text((src as *const [u8; 4]).read(), &mut text); if n as u32 >= size { + set_errno(ENOSPC); return ptr::null(); } - ptr::copy_nonoverlapping(buf.as_ptr(), dst, n + 1); + ptr::copy_nonoverlapping(text.as_ptr(), dst, n + 1); dst as *const u8 } -fn fmt_ip4(buf: &mut [u8; 16], a: u8, b: u8, c: u8, d: u8) -> usize { - let mut pos = 0; - for (i, octet) in [a, b, c, d].iter().enumerate() { - if i > 0 { - buf[pos] = b'.'; - pos += 1; - } - pos += fmt_u8(&mut buf[pos..], *octet); - } - buf[pos] = 0; - pos -} - -fn fmt_u8(buf: &mut [u8], val: u8) -> usize { - if val >= 100 { - buf[0] = b'0' + val / 100; - buf[1] = b'0' + (val / 10) % 10; - buf[2] = b'0' + val % 10; - 3 - } else if val >= 10 { - buf[0] = b'0' + val / 10; - buf[1] = b'0' + val % 10; - 2 - } else { - buf[0] = b'0' + val; - 1 - } -} - #[no_mangle] pub unsafe extern "C" fn htons(hostshort: u16) -> u16 { hostshort.to_be() @@ -869,58 +844,10 @@ pub unsafe extern "C" fn ntohl(netlong: u32) -> u32 { u32::from_be(netlong) } +/// `INADDR_NONE` for a text that is no address, which is also what +/// 255.255.255.255 reads as. #[no_mangle] pub unsafe extern "C" fn inet_addr(cp: *const u8) -> u32 { - if cp.is_null() { - return u32::MAX; // INADDR_NONE - } - let len = super::string::strlen(cp); - let s = core::slice::from_raw_parts(cp, len); - match parse_ipv4(s) { - Some(ip) => u32::from_be_bytes(ip), - None => u32::MAX, - } -} - -// Helpers - -fn parse_ipv4(s: &[u8]) -> Option<[u8; 4]> { - let mut octets = [0u8; 4]; - let mut octet_idx = 0; - let mut val: u16 = 0; - let mut has_digit = false; - - for &c in s { - if c >= b'0' && c <= b'9' { - val = val * 10 + (c - b'0') as u16; - if val > 255 { - return None; - } - has_digit = true; - } else if c == b'.' { - if !has_digit || octet_idx >= 3 { - return None; - } - octets[octet_idx] = val as u8; - octet_idx += 1; - val = 0; - has_digit = false; - } else { - return None; - } - } - if !has_digit || octet_idx != 3 { - return None; - } - octets[3] = val as u8; - Some(octets) -} - -fn make_sockaddr_in(ip: [u8; 4], port: u16) -> SockaddrIn { - SockaddrIn { - sin_family: AF_INET as u16, - sin_port: port.to_be(), - sin_addr: InAddr { s_addr: u32::from_be_bytes(ip) }, - sin_zero: [0; 8], - } + let text = core::slice::from_raw_parts(cp, super::string::strlen(cp)); + inaddr::numbers_and_dots(text).map_or(u32::MAX, u32::from_ne_bytes) } From 1f8c4e5414514394e28979b50114f63d6ed3432b Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 01:10:06 +0200 Subject: [PATCH 2/8] libc_sockets runs the address case first Every case names its peer by an address, so with addresses read in host order the datagram case sent to another host and waited on its answer until the hang ceiling, and the red was not the address case's own. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- tests/toyos.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/toyos.rs b/tests/toyos.rs index 4b1110e43db..904f4830701 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2923,7 +2923,8 @@ fn libc_sockets() -> Result<(), String> { } }); - let cases = [("sendto_unbound", answering), ("nodelay_kept", holding), ("addr_order", holding)]; + // The address first: every case names its peer by one. + let cases = [("addr_order", holding), ("nodelay_kept", holding), ("sendto_unbound", answering)]; let case = compile::repo_root().join("tests/netcase"); let bins: Vec<(String, Vec)> = cases .iter() From 1eb7213e0ce906081876d9185d8c92cfde5d547e Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 02:35:04 +0200 Subject: [PATCH 3/8] The address tests name a documentation address The source gate refuses a public IPv4 address in a tracked file, and three texts of the inet_addr differential were one: `cargo run -- --ci host` was red on them, one step of 78. They are RFC 5737's now. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- tests/libc-arch/src/internet_addresses.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/libc-arch/src/internet_addresses.rs b/tests/libc-arch/src/internet_addresses.rs index 5b34896d1a0..a878701487d 100644 --- a/tests/libc-arch/src/internet_addresses.rs +++ b/tests/libc-arch/src/internet_addresses.rs @@ -94,9 +94,9 @@ fn numbers_and_dots_read_as_the_host_reads_them() { texts(&parts, 1..=3, &["", " ", "\n", "\t9", " x", "x", "."], &mut judge); texts(&parts, 4..=4, &["", " x"], &mut judge); texts(&parts[..6], 5..=5, &[""], &mut judge); - judge(" 1.2.3.4"); - judge("1.2.3.4\x0b"); - judge("1.2.3.4\x0c\r"); + judge(" 192.0.2.1"); + judge("192.0.2.1\x0b"); + judge("192.0.2.1\x0c\r"); assert!(read > 10_000, "{read} texts were addresses"); } From 68cf1f8708d90a473813bac6b8c7c1372aa9901e Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 03:38:46 +0200 Subject: [PATCH 4/8] The option test reads TCP_NODELAY from netinet/tcp.h #783's host test of the option rule read the number from netinet/in.h, which this stage moved to the header POSIX has it in: the merge compiled and the test was red, one step of 78 in `cargo run -- --ci host`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- tests/libc-arch/src/socket_options.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/libc-arch/src/socket_options.rs b/tests/libc-arch/src/socket_options.rs index 2ac86db4206..dafc9aadd85 100644 --- a/tests/libc-arch/src/socket_options.rs +++ b/tests/libc-arch/src/socket_options.rs @@ -18,7 +18,7 @@ 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"); +const TCP_H: &str = include_str!("../../../userland/libc/include/netinet/tcp.h"); unsafe extern "C" { fn setsockopt(fd: c_int, level: c_int, name: c_int, value: *const u8, len: u32) -> c_int; @@ -40,7 +40,7 @@ const UNWRITTEN: u8 = 0xAA; 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")), + Kept::NoDelay => (header::int(SOCKET_H, "IPPROTO_TCP"), header::int(TCP_H, "TCP_NODELAY")), } } From fe241fc3e4dd6df727e3e432ecd193b0ae491b57 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 04:31:27 +0200 Subject: [PATCH 5/8] The counters read's silence is recorded three more times, and virt_mask_windows's stalls are folded into it Two guests of one run on this branch at 68cf1f870 went silent at the same step in the same second, virt_el1_smp and virt_mask_windows, each straight after the spawn record of test_rs_counters_read. virt_mask_windows's wait is named for the boot's last word because that is what it waits on after unmap_touch; the silence is the read's. So its own file is folded in, with its first occurrence and the one on wt/toyos-move-abi at 94fd25e71, neither of which kept a console of the guest. Nothing cited the folded file. Each number was read off that run's log, none of which is in the tree. The issue gains the owner the tracker asks of it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...-under-host-load-can-go-silent-for-15-s.md | 95 ++++++++++++++++++- ...ask-windows-went-quiet-on-a-loaded-host.md | 29 ------ 2 files changed, 90 insertions(+), 34 deletions(-) delete mode 100644 issues/virt-mask-windows-went-quiet-on-a-loaded-host.md diff --git a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md index 04508750a14..12e4be8e032 100644 --- a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md +++ b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md @@ -8,8 +8,15 @@ opened: 2026-10-04 `test_rs_counters_read` in `tests/virtsmpcase` normally ends about 0.2 s of guest clock after `unmap_touch` does (median 0.17-0.20 s over 570 guests, max -1.46 s). Under host load it sometimes takes seconds, and twice it went silent -long enough for the harness's 15 s quiet bound to call the guest stalled. +1.46 s). Under host load it sometimes takes seconds, and it has gone silent +long enough for the harness's 15 s quiet bound to call the guest stalled: each +silent boot is below. + +`virt_mask_windows` boots the same case on a `mask-windows` kernel and waits +on two things, the job `unmap_touch` and then the boot's last word, so a +silence in the counters read reds it as `STALLED: waiting for the boot's last +word`: that wait spans the read, `test_rs_trace_read` and the shutdown. Its +stalls are recorded here. The first silent one: `virt_el1_smp`, 8 vCPUs under TCG, on a 14-core host at 1-minute load 38-53 while the primary checkout was building the stage-2 @@ -54,9 +61,84 @@ line of the wait and none the guest runs. - **What is not known.** Whether the guest was running, spinning or not being scheduled by the host, and whether it was the guest or its console that stopped. No register capture of it exists either, so it adds a count and a - second profile and brings the exit no closer. The fork compiler miscompiles, - for AArch64, an inclusive range that ends at its integer type's maximum; - whether that reaches this job's code has not been examined. + second profile and brings the exit no closer. + +The third and fourth silent ones, two guests of one run: `virt_el1_smp` and +`virt_mask_windows`, in the whole suite on `wt/toyos-libc-sockets` at +`68cf1f870`, whose diff touches no line of either wait and nothing these +guests start: `tests/virtsmpcase` runs no C program, opens no socket and +starts no netstack. + +- **The lines.** `FAIL virt_el1_smp: STALLED: waiting for the job + test_rs_counters_read to end — it went quiet` and `FAIL virt_mask_windows: + STALLED: waiting for the boot's last word — it went quiet`, both at the same + second of the host's clock; 35 of 37 passed. +- **The load.** `--jobs 4 --nocapture` on the 14-core host, 1-minute load + 75.99 when the run began and 63.37 when it ended, from other worktrees' + builds; liveness ceilings paid at 1.52x; workers 366 s building against + 808 s testing. +- **The waits.** `virt_el1_smp`: `judge_virt_job`'s `await_marker` on + `===TEST_END test_rs_counters_read `. `virt_mask_windows`: its second wait, + `await_marker` on `power::SHUTTING_DOWN`, after `judge_virt_job` had judged + `unmap_touch`. Each was ended by `GUEST_QUIET`. +- **The same profile as the second.** Each guest's last three lines are the + end of `unmap_touch`, the start of the read and the kernel's record of its + spawn, with no thread's exit after it. `virt_el1_smp`: `===TEST_END + unmap_touch exit=0===` at 13.577, `===TEST_START test_rs_counters_read===` + at 13.578, `spawn: /system/bin/test_rs_counters_read pid=13` at 13.590 on + cpu4. `virt_mask_windows`: the same three at 14.557, 14.558 and 14.585, on + cpu4. The two spawn records reached the harness within one second of each + other, and nothing followed from either for the 15 s. +- **What is known.** `virt_smp`, the same case launched one second before + `virt_el1_smp`, did the step in the same seconds and passed: the read began + at guest 13.936 and ended at 19.120, five seconds of the host's clock. + `virt_failed_ap_leaves_no_hole`, launched one second after, ends at + `unmap_touch` and does not wait on the read. Each red, run alone at the + same commit, passed in 4 s, at loads 32.47 and 37.04. +- **What is not known.** Whether the guests ran, spun or were not scheduled: + no register capture exists. Two guests going silent at the same step in the + same second is the first occurrence that bears on the host stopping the + guest, and it shows no more than that. The serial directory the harness + named for that run was not kept; the run's log, outside the tree, is the + only copy. + +Two earlier stalls of `virt_mask_windows` have no console of the guest, so +the step each stopped in is not read from it: + +- **`wt/toyos-move-abi` at `94fd25e71`.** `FAIL virt_mask_windows: STALLED: + waiting for the boot's last word — it went quiet`, with nothing in what the + guest said while it was waited on, `STALL virt_mask_windows (38s)`; the + whole suite at `--jobs 4`, 34 of 36, workers 1794 s building against 703 s + testing. Load 59.30 69.33 70.48 when the suite began and 85.73 69.02 67.23 + when it ended, 14 cores; fastest boot 2765 ms against the reference + 1424 ms, ceilings at 1.94x. The wait is the second one, ended by + `GUEST_QUIET`, which no host factor stretches. Known: `unmap_touch` had + ended with exit 0, its line reaching the harness 15 s before the verdict, + the guest named by elimination among the three of that case then running. + In the same run `virt_smp` said its `unmap_touch` line at guest 21.786 and + its `counters_read` line at 34.278, 13 s apart on the host's clock and 2 s + inside the bound; `virt_el1_smp`, earlier in that run, 13.481 and 15.702. + The branch changes nothing that guest's kernel or harness is built from. + At `c95d0ffc0` the test passed in the whole suite at load 12.82 rising to + 36.76 and alone at 36.76 to 42.53, neither at the red's load. Not known: + whether the guest was alive or what it did in those 15 s; the red run's + kept serial directory holds eleven UART logs, each an x86-64 boot, and no + PL011's. Whether the silence is `virt_smp`'s 13 s grown past 15 under the + hooks' cost, or a guest that stopped. It was not run again at that commit, + nor on `main` at that load. +- **`wt/toyos-acpi1` at `b9430ed61`.** The same line after 45 s, in the whole + suite; workers 19038 s building against 1375 s testing, load 82.65 83.62 + 77.89 moments after it, 14 cores. Nothing in that branch runs on that + boot. Alone at that load and commit it passed in 14 s. On `main` at + `d47b383cf`, `cargo test -- virt_mask_windows` passed three times: alone at + load 45 rising to 72 (10 s), and twice beside a whole suite at load 74 to + 77 (38 s, with ceilings paid at 8.00x and 31 s between its image and its + first guest line) and 75 to 67 (7 s). + +Not known of any of them: the fork compiler miscompiles, for AArch64, an +inclusive range that ends at its integer type's maximum (LLVM's +ScalarEvolution), and whether that reaches `test_rs_counters_read` is being +measured by a per-function census on another branch. The slow ones, with registers: a probe that captured `info registers -a` over QMP whenever the read had not ended 3 s after `unmap_touch` (`debug-slow.patch` @@ -84,3 +166,6 @@ whether its CPUs went idle by a `sched:` line's absence; its diary Exit: the cause of the silence is named from a capture of a silent guest (registers over QMP before anything else touches it), and fixed with the evidence, or shown to be the host stopping the guest. + +Owner: the kernel's AArch64 bring-up, `issues/toyos-runs-on-arm64.md`, held +by the orchestrator's next kernel worker. diff --git a/issues/virt-mask-windows-went-quiet-on-a-loaded-host.md b/issues/virt-mask-windows-went-quiet-on-a-loaded-host.md deleted file mode 100644 index f4184617a12..00000000000 --- a/issues/virt-mask-windows-went-quiet-on-a-loaded-host.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-04 ---- - -# `virt_mask_windows` went quiet inside a whole suite on a loaded host - -In the whole guest suite at `wt/toyos-acpi1`'s `b9430ed61`, `virt_mask_windows` -(AArch64, the `mask-windows` kernel, `tests/virtsmpcase`, eight vCPUs under -TCG) ended `STALLED: waiting for the boot's last word — it went quiet` after -45 s. That run's workers spent 19038 s building against 1375 s testing, and -the host's load average read 82.65 / 83.62 / 77.89 moments after it, on 14 -cores. The same test alone at that load, at the same commit, passed in 14 s. - -Nothing in that branch runs on that boot: its AArch64 changes are stubs no -path reaches, and `tests/virtsmpcase` starts no ACPI server. Owner: none yet. - -`main` at `d47b383cf`, `cargo test -- virt_mask_windows`, three runs, each -EXIT=0: alone at load 45 rising to 72 (PASS, 10 s); beside the branch's own -whole guest suite at load 74 to 77 (PASS, 38 s); and again beside it at load -75 to 67 (PASS, 7 s). The 38 s run printed nothing between its image built -and its first guest line for 31 s, and paid its liveness ceilings at 8.00x -(the others 3.99x and 2.62x). Not reproduced on `main`, and not ruled out -there. - -**Exit**: `virt_mask_windows` run at least as often as it took to see this, -alongside a load like that one, on `main`, reads no stall; or the stall's -cause, named and fixed. From 7d82642f31d5cc42ca08c94475ef96e6d1c3f93e Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 04:31:27 +0200 Subject: [PATCH 6/8] Round 1 of #787's review: the listener's refusal back, the datagram rule and the address calls' refusals on the host tier, a sockaddr truncated to its caller's buffer A listener's TCP_NODELAY is refused again, as it was before this stage: netstack holds a listener from bind and answers its id not connected, and nothing is kept for it. The stage had turned that into a kept value no accepted connection carried. nodelay_kept.c gains the listener's lines. The issue says what is left (a value set before bind) and names the ABI stage that gives a listener its option as its exit. A TCP option asked of a datagram socket is sockopt.rs's rule now, with Linux's two answers by reading (ENOPROTOOPT to a setter, EOPNOTSUPP to a getter, before a value or a length is read), a row of the host differential, and Darwin's measured answers in darwin(). The four guest lines that held Darwin's EINVAL are gone. inet_pton's and inet_ntop's family refusal and inet_ntop's fit are in inaddr.rs, against the host at every buffer size from 0 to 17 for a text of each length and at four families neither has. inet_pton refuses a number with a zero before another digit, as glibc does, so no text has two values; Darwin reads it, and the differential says so. SockaddrIn is aligned as C's struct is, and answers a caller's buffer no more bytes than it holds, with the address's own length written back, as POSIX has accept, recvfrom, getpeername and getsockname truncate one: against the host's getsockname at every length from 0 to 32. That closes issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md, which nothing cited. The two netcase tests boot through one helper and share one holding server. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...oes-not-carry-its-listeners-tcp-nodelay.md | 49 +++-- ...uses-the-option-requests-a-host-answers.md | 5 + ...-a-whole-sockaddr-into-a-shorter-buffer.md | 22 --- tests/libc-arch/src/internet_addresses.rs | 175 +++++++++++++++--- tests/libc-arch/src/socket_options.rs | 129 +++++++++---- tests/netcase/nodelay_kept.c | 21 ++- tests/toyos.rs | 44 +++-- userland/libc/src/errno.rs | 1 + userland/libc/src/inaddr.rs | 69 ++++++- userland/libc/src/socket.rs | 74 ++++---- userland/libc/src/sockopt.rs | 18 +- 11 files changed, 421 insertions(+), 186 deletions(-) delete mode 100644 issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md diff --git a/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md b/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md index cfd3e5d6127..06829cbd89d 100644 --- a/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md +++ b/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md @@ -6,23 +6,38 @@ opened: 2026-10-08 # A connection libc accepts does not carry its listener's TCP_NODELAY -`setsockopt(IPPROTO_TCP, TCP_NODELAY)` on a stream socket that is not -connected is kept in libc's socket entry (`userland/libc/src/socket.rs`) and -handed to netstack by `connect`. A listener is never connected: its value is -kept, `getsockopt` answers it, and `accept` makes the new connection's entry -with the option clear and sends netstack nothing. POSIX does not say what an -accepted socket inherits. Measured on Darwin 27.0.0, one C program: the -option set on a listener before `bind` answers 0, set after `listen` answers -0, and `getsockopt` on the socket `accept` returns reads it set. Linux is -unread. +A host's listener keeps `TCP_NODELAY` and the connections it accepts begin +with it. Measured on Darwin 27.0.0, one C program: the option set on a +listener before `bind` answers 0, set after `listen` answers 0, and +`getsockopt` on the socket `accept` returns reads it set. Linux is unread. -No guest test can ask: nothing dials into a guest. The harness's network is -QEMU's user backend with no forwarded port (`tests/common/qemu.rs`), and -whether a guest reaches its own address through netstack is unmeasured. +netstack has no option for a listener: `MsgType::TcpSetOption` +(`toyos/src/net.rs`) names a connection, and netstack answers a listener's id +`ERR_NOT_CONNECTED`. So libc (`userland/libc/src/socket.rs`): -**Exit**: `accept` hands a listener's kept `TCP_NODELAY` to netstack for the -connection it answers, as `connect` does, and a guest C case accepts a -connection a host server's peer dials and reads the option set on it. +- refuses `setsockopt(IPPROTO_TCP, TCP_NODELAY)` on a socket it has bound as + a listener with -1 and `ENOTCONN`, keeps nothing for it, and `getsockopt` + reads what the socket held before. `tests/netcase/nodelay_kept.c` holds the + refusal and the read. +- keeps a value set before `bind`, as it keeps one set before `connect`, and + the listener reads it back; `accept` makes the connection's entry with the + option clear and sends netstack nothing. Read from the code, not run. That + one is a program told yes and given no, and no guest test can read it: + nothing dials into a guest, whose network is QEMU's user backend with no + forwarded port (`tests/common/qemu.rs`). -**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`: the -case needs a way into the guest, which is the harness's. +Not carried by `accept` on smoltcp, by the orchestrator's decision: the stack +this track builds gives a connection the option its listener held when the +connection began (`Options`, `toyos-net-shard/tcp/src/lib.rs`, "inherited by a +listener's children", set by `Tcp::set_listener_options`), and a hand-over at +`accept` would carry one set after the connection was queued too, which is +another rule. + +**Exit**: the stage of `issues/toyos-has-its-own-network-stack.md` that gives +the pipe ABI a listener's option: libc hands a listener's `TCP_NODELAY` to the +stack, at `bind` for one set before it and at the set for one after, both +answer 0, a connection `accept` returns reads the option its listener held +when it began, and `nodelay_kept.c`'s listener lines turn to the host's +answers. Until then the refusal stands. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. 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 d5dda726075..78efd6233be 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 @@ -29,6 +29,11 @@ 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. +libc's `connect` asks the same request of a connection it has just been +answered, to hand over a `TCP_NODELAY` set before it: a peer that resets +between netstack's answer and that request makes `connect` fail `ENOTCONN`, +which no host's `connect` answers. + **Exit condition**: a guest test on `tests/netcase` whose peer resets a stream the client still holds, and `nodelay()` on it answers `Ok`, with `netstack_socket_churn` still green. diff --git a/issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md b/issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md deleted file mode 100644 index 3697ccf9e19..00000000000 --- a/issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# libc writes a whole sockaddr into a shorter buffer - -Read from the code, not run. `fill_sockaddr` -(`userland/libc/src/socket.rs`), which `accept`, `recvfrom`, `getpeername` -and `getsockname` answer through, writes sixteen bytes at the caller's -address and sets `*addrlen` to 16 without reading what `*addrlen` held. A -caller whose buffer is shorter has the bytes past it overwritten. POSIX: "If -the actual length of the address is greater than the length of the supplied -sockaddr structure, the stored address shall be truncated", with `*addrlen` -set to the address's own length. - -**Exit**: each of the four calls writes at most `*addrlen` bytes and answers -the address's length, and a host test of the write holds a buffer shorter -than the address. - -**Owner**: libc, under `issues/toyos-has-its-own-network-stack.md`. diff --git a/tests/libc-arch/src/internet_addresses.rs b/tests/libc-arch/src/internet_addresses.rs index a878701487d..15cc3988636 100644 --- a/tests/libc-arch/src/internet_addresses.rs +++ b/tests/libc-arch/src/internet_addresses.rs @@ -1,12 +1,18 @@ //! libc's IPv4 addresses (`inaddr.rs`) against the host C library's: the -//! texts `inet_pton` and `inet_addr` read, the text `inet_ntop` writes, and a -//! `sockaddr_in`'s bytes against the host's `htonl` and `htons`. Where the -//! hosts' own readers part from POSIX's text or from each other, the text is -//! held alone, each case saying which. +//! texts `inet_pton` and `inet_addr` read, the text `inet_ntop` writes into a +//! buffer of every size, both calls' refusal of another family, a +//! `sockaddr_in`'s bytes against the host's `htonl` and `htons`, and the +//! bytes a buffer of every length is answered against the host's +//! `getsockname`. Where the hosts' own readers part from POSIX's text or from +//! each other, each case says which. use std::ffi::{c_char, c_int, c_void, CStr, CString}; +use std::net::UdpSocket; +use std::os::fd::AsRawFd; +use std::ptr; -use crate::inaddr::{dotted_quad, dotted_text, numbers_and_dots, SockaddrIn, AF_INET}; +use crate::inaddr::{ntop, numbers_and_dots, pton, Refusal, SockaddrIn, AF_INET}; +use crate::socket_options::host_errno; unsafe extern "C" { fn inet_pton(af: c_int, src: *const c_char, dst: *mut c_void) -> c_int; @@ -14,8 +20,15 @@ unsafe extern "C" { fn inet_addr(cp: *const c_char) -> u32; fn htonl(hostlong: u32) -> u32; fn htons(hostshort: u16) -> u16; + fn getsockname(fd: c_int, addr: *mut u8, len: *mut u32) -> c_int; } +/// The host's `EAFNOSUPPORT`; `ENOSPC` macOS and Linux number alike. +const EAFNOSUPPORT: c_int = if cfg!(target_os = "macos") { 47 } else { 97 }; +const ENOSPC: c_int = 28; +/// What a buffer holds before a call writes it. +const UNWRITTEN: u8 = 0xAA; + /// Every text of `min..=max` of `parts` between dots, each followed by each /// of `ends`. fn texts(parts: &[&str], counts: std::ops::RangeInclusive, ends: &[&str], mut each: impl FnMut(&str)) { @@ -29,6 +42,11 @@ fn texts(parts: &[&str], counts: std::ops::RangeInclusive, ends: &[&str], } } +/// libc's `inet_pton` of an `AF_INET` text. +fn ours_pton(text: &str) -> Option<[u8; 4]> { + pton(AF_INET, text.as_bytes()).expect("AF_INET is libc's family") +} + /// The host's `inet_pton` of `text`. fn host_pton(text: &str) -> Option<[u8; 4]> { let c = CString::new(text).unwrap(); @@ -53,30 +71,72 @@ fn ours_addr(text: &str) -> [u8; 4] { const OCTETS: [&str; 10] = ["0", "1", "9", "10", "99", "100", "199", "200", "249", "255"]; +/// What Darwin's `inet_pton` reads: a number of any count of digits, zeros in +/// front among them, where libc and glibc refuse a zero before another digit +/// and a fourth digit. +fn darwin_reads(text: &str) -> Option<[u8; 4]> { + let parts: Vec<&str> = text + .split('.') + .map(|part| match part.trim_start_matches('0') { + _ if part.is_empty() || !part.bytes().all(|c| c.is_ascii_digit()) => part, + "" => "0", + rest => rest, + }) + .collect(); + ours_pton(&parts.join(".")) +} + #[test] fn a_dotted_quad_reads_as_the_host_reads_it() { let mut parts = OCTETS.to_vec(); parts.extend(["256", "300", "999", "1000", "", "a", " ", "1 ", " 1", "-1", "+1", "1x", "0x1"]); - let mut read = 0; + parts.extend(["00", "01", "010", "0255", "0001"]); + let (mut read, mut zero_led) = (0, 0); let mut judge = |text: &str| { - let ours = dotted_quad(text.as_bytes()); - assert_eq!(ours, host_pton(text), "inet_pton({text:?})"); + let ours = ours_pton(text); + let darwin = darwin_reads(text); + let want = if cfg!(target_os = "macos") { darwin } else { ours }; + assert_eq!(host_pton(text), want, "inet_pton({text:?}): libc reads {ours:?}"); read += usize::from(ours.is_some()); + zero_led += usize::from(ours != darwin); }; texts(&parts, 1..=4, &[""], &mut judge); texts(&parts[..12], 5..=5, &[""], &mut judge); texts(&OCTETS, 4..=4, &[".", " ", "\n", "x"], &mut judge); assert_eq!(read, OCTETS.len().pow(4), "the texts that are addresses"); + assert!(zero_led > 10_000, "{zero_led} texts were addresses but for a zero in front"); } -/// POSIX: each number is "a one to three-digit decimal number between 0 and -/// 255". glibc refuses a leading zero and Darwin takes any count of digits, -/// so neither is asked. +/// No text has two values: `inet_addr` reads `010` as octal, so `inet_pton` +/// refuses it. #[test] -fn a_dotted_quad_s_number_is_one_to_three_decimal_digits() { - assert_eq!(dotted_quad(b"010.001.00.0"), Some([10, 1, 0, 0])); - assert_eq!(dotted_quad(b"0001.2.3.4"), None); - assert_eq!(dotted_quad(b"1.2.3.0255"), None); +fn a_number_with_a_zero_before_another_digit_is_no_dotted_quad() { + assert_eq!(ours_pton("010.0.0.1"), None); + assert_eq!(numbers_and_dots(b"010.0.0.1"), Some([8, 0, 0, 1])); + assert_eq!(ours_pton("10.0.0.00"), None); + assert_eq!(ours_pton("10.0.0.0"), Some([10, 0, 0, 0])); +} + +/// A family neither libc nor the host has an address text for. The host's +/// own second family, IPv6, libc refuses the same way and is not asked. +#[test] +fn another_family_is_refused_as_the_host_refuses_it() { + for af in [0, 1, -1, 12345] { + assert_eq!(pton(af, b"192.0.2.1"), Err(Refusal::Family), "inet_pton, family {af}"); + let mut octets = [UNWRITTEN; 4]; + // SAFETY: a NUL-terminated string, and four bytes to write. + let host = unsafe { inet_pton(af, c"192.0.2.1".as_ptr(), octets.as_mut_ptr().cast()) }; + assert_eq!((host, host_errno(), octets), (-1, EAFNOSUPPORT, [UNWRITTEN; 4]), "the host's inet_pton, family {af}"); + + let (ip, mut ours, mut theirs) = ([192u8, 0, 2, 1], [UNWRITTEN; 16], [UNWRITTEN; 16]); + // SAFETY: four bytes to read and sixteen to write. + let answer = unsafe { ntop(af, ip.as_ptr(), ours.as_mut_ptr(), 16) }; + assert_eq!(answer, Err(Refusal::Family), "inet_ntop, family {af}"); + // SAFETY: as above. + let host = unsafe { inet_ntop(af, ip.as_ptr().cast(), theirs.as_mut_ptr().cast(), 16) }; + assert_eq!((host, host_errno()), (ptr::null(), EAFNOSUPPORT), "the host's inet_ntop, family {af}"); + assert_eq!(ours, theirs, "family {af}: nothing is written"); + } } #[test] @@ -111,33 +171,66 @@ fn a_number_past_32_bits_or_without_digits_is_no_address() { assert_eq!(numbers_and_dots(b"1.0x"), None); } +/// `inet_ntop` of `ip` into `size` bytes of a buffer, by libc and by the +/// host: libc's answer and its buffer, then the host's `errno` where it +/// answered none and its buffer. +fn written(ip: [u8; 4], size: u32) -> (Result<(), Refusal>, [u8; 32], Option, [u8; 32]) { + let (mut ours, mut theirs) = ([UNWRITTEN; 32], [UNWRITTEN; 32]); + // SAFETY: four bytes to read, and `size`, at most 32, to write. + let answer = unsafe { ntop(AF_INET, ip.as_ptr(), ours.as_mut_ptr(), size) }; + // SAFETY: as above. + let host = unsafe { inet_ntop(AF_INET, ip.as_ptr().cast(), theirs.as_mut_ptr().cast(), size) }; + assert!(host.is_null() || host == theirs.as_ptr().cast(), "the host answers its buffer"); + (answer, ours, host.is_null().then(host_errno), theirs) +} + #[test] fn an_address_is_written_as_the_host_writes_it() { for position in 0..4 { for octet in 0..=255u8 { let mut ip = [7, 77, 177, 0]; ip[position] = octet; - let mut ours = [0xaau8; 16]; - let len = dotted_text(ip, &mut ours); - let mut host = [0 as c_char; 16]; - // SAFETY: four bytes to read and sixteen to write. - let wrote = unsafe { inet_ntop(AF_INET, ip.as_ptr().cast(), host.as_mut_ptr(), 16) }; - assert!(!wrote.is_null(), "the host's inet_ntop({ip:?})"); - // SAFETY: the host's NUL-terminated answer. - let want = unsafe { CStr::from_ptr(wrote) }.to_bytes_with_nul(); - assert_eq!(&ours[..=len], want, "{ip:?}"); + let (answer, ours, refused, host) = written(ip, 16); + assert_eq!((answer, refused), (Ok(()), None), "{ip:?}"); + assert_eq!(ours, host, "{ip:?}"); + assert!(CStr::from_bytes_until_nul(&ours).is_ok(), "{ip:?}: the text ends in a NUL"); + } + } +} + +/// One address for each length a text has, 7 to 15, into a buffer of every +/// size from none to one past `INET_ADDRSTRLEN`: the text fits where its NUL +/// does, and a buffer it does not fit is left as it was. +#[test] +fn a_buffer_too_short_for_the_text_is_refused_as_the_host_refuses_it() { + let addresses: [[u8; 4]; 9] = [ + [0, 0, 0, 0], + [10, 0, 0, 0], + [10, 10, 0, 0], + [10, 10, 10, 0], + [10, 10, 10, 10], + [100, 10, 10, 10], + [100, 100, 10, 10], + [100, 100, 100, 10], + [255, 255, 255, 255], + ]; + for (ip, text_len) in addresses.into_iter().zip(7u32..) { + for size in 0..=17 { + let (answer, ours, refused, host) = written(ip, size); + let fits = size > text_len; + assert_eq!(answer, if fits { Ok(()) } else { Err(Refusal::Room) }, "{ip:?} into {size}"); + assert_eq!(refused, (!fits).then_some(ENOSPC), "the host's inet_ntop of {ip:?} into {size}"); + assert_eq!(ours, host, "{ip:?} into {size}"); + assert!(ours[size as usize..].iter().all(|&byte| byte == UNWRITTEN), "{ip:?}: a byte past {size}"); } } - let mut longest = [0xaau8; 16]; - assert_eq!(dotted_text([255; 4], &mut longest), 15); - assert_eq!(&longest, b"255.255.255.255\0"); } /// `sockaddr_in` as Linux and `include/netinet/in.h` lay it out: the family, /// then the port's two bytes and the address's four in network order. #[test] fn a_sockaddr_holds_its_port_and_address_in_network_order() { - assert_eq!(size_of::(), 16); + assert_eq!((size_of::(), align_of::()), (16, 4)); // SAFETY: sixteen bytes with no padding, read as bytes. let bytes: [u8; 16] = unsafe { std::mem::transmute(SockaddrIn::new([10, 0, 2, 2], 0x1234)) }; let family = (AF_INET as u16).to_ne_bytes(); @@ -160,3 +253,29 @@ fn a_sockaddr_holds_its_port_and_address_in_network_order() { let unix: SockaddrIn = unsafe { std::mem::transmute(written) }; assert_eq!(unix.endpoint(), None, "AF_UNIX's is no IPv4 address"); } + +/// A bound socket's address into a buffer of every length to twice its own, +/// by `SockaddrIn::answer` and by the host's `getsockname`: no byte past the +/// length handed in, and the address's own length written back. The first two +/// bytes are compared on Linux, whose layout libc's is; Darwin's are a length +/// and a one-byte family. +#[test] +fn an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it() { + let socket = UdpSocket::bind("127.0.0.1:0").unwrap(); + let address = SockaddrIn::new([127, 0, 0, 1], socket.local_addr().unwrap().port()); + for handed in 0..=32u32 { + let (mut ours, mut theirs) = ([UNWRITTEN; 32], [UNWRITTEN; 32]); + let (mut our_len, mut their_len) = (handed, handed); + // SAFETY: a length, and that many of the buffer's 32 bytes. + unsafe { address.answer(ours.as_mut_ptr(), &mut our_len) }; + // SAFETY: as above. + let host = unsafe { getsockname(socket.as_raw_fd(), theirs.as_mut_ptr(), &mut their_len) }; + assert_eq!((host, their_len), (0, our_len), "getsockname into {handed}"); + assert_eq!(our_len, 16, "into {handed}: the address's own length"); + let layout = if cfg!(target_os = "linux") { 0 } else { 2 }; + assert_eq!(ours[layout..], theirs[layout..], "into {handed}"); + let kept = (handed as usize).min(16); + assert!(ours[kept..].iter().all(|&byte| byte == UNWRITTEN), "into {handed}: a byte past the buffer"); + assert!(theirs[kept..].iter().all(|&byte| byte == UNWRITTEN), "the host, into {handed}"); + } +} diff --git a/tests/libc-arch/src/socket_options.rs b/tests/libc-arch/src/socket_options.rs index dafc9aadd85..04009e70767 100644 --- a/tests/libc-arch/src/socket_options.rs +++ b/tests/libc-arch/src/socket_options.rs @@ -1,7 +1,8 @@ //! 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 +//! `getsockopt`, one table through both: `SO_BROADCAST` on a datagram socket, +//! `TCP_NODELAY` on a stream socket and `TCP_NODELAY` on a datagram socket, +//! which has no such option, 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. @@ -31,6 +32,9 @@ unsafe extern "C" { /// The host's `EFAULT` and `EINVAL`, which macOS and Linux number alike. const EFAULT: c_int = 14; const EINVAL: c_int = 22; +/// Linux's `ENOPROTOOPT` and `EOPNOTSUPP`: Darwin answers neither here. +const ENOPROTOOPT: c_int = 92; +const EOPNOTSUPP: c_int = 95; const LENGTHS: [u32; 5] = [8, 4, 2, 1, 0]; /// What a buffer holds before a read, which no answer's byte is. @@ -72,12 +76,15 @@ fn refused(refusal: Refusal) -> Answer { Answer::Refused(match refusal { Refusal::Short => EINVAL, Refusal::Fault => EFAULT, + Refusal::NoSuchOption => ENOPROTOOPT, + Refusal::NotSupported => EOPNOTSUPP, }) } -fn host_refusal() -> Answer { +/// The host's `errno`, as its last call left it. +pub(crate) fn host_errno() -> c_int { // SAFETY: the host's errno slot is this thread's. - Answer::Refused(unsafe { *errno_location() }) + unsafe { *errno_location() } } fn read(buf: &[u8; 8], len: u32) -> Answer { @@ -89,61 +96,85 @@ fn read(buf: &[u8; 8], len: u32) -> Answer { struct Pair { option: Kept, fd: c_int, + datagram: bool, kept: bool, } impl Pair { + /// A TCP option asked of a datagram socket, which has none. + fn foreign(&self) -> bool { + self.datagram && self.option == Kept::NoDelay + } + + /// The option by libc's rule, asked of this socket by a setter or a getter. + fn named(&self, setter: bool) -> Result<(), Answer> { + let (level, name) = libc_numbers(self.option); + let named = sockopt::kept(level, name, self.datagram, setter).map_err(refused)?; + assert_eq!(named, Some(self.option)); + Ok(()) + } + /// 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) => { + let ours = match self.named(true).map(|()| unsafe { sockopt::switch(value, len) }) { + Ok(Ok(on)) => { self.kept = on; Answer::Set } - Err(refusal) => refused(refusal), + Ok(Err(refusal)) => refused(refusal), + Err(refused) => refused, }; 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(), + _ => Answer::Refused(host_errno()), }; (ours, host) } - /// A getter handed `len` bytes at `value`, null when `null`, by libc's rule - /// and by the host. + /// A getter handed `value` and `len`, by libc's rule and by the host. + fn answered(&self, value: *mut u8, len: *mut u32) -> (Option, Option) { + // SAFETY: the caller's. + let ours = self.named(false).and_then(|()| unsafe { sockopt::answer(self.kept as i32, value, len) }.map_err(refused)); + let (level, name) = host_numbers(self.option); + // SAFETY: the caller's. + let host = (unsafe { getsockopt(self.fd, level, name, value, len) } != 0).then(|| Answer::Refused(host_errno())); + (ours.err(), host) + } + + /// A getter handed `len` bytes at a buffer, 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 call = |ours: bool| { 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)) + // The buffer is null or eight writable bytes, and `len` at most 8. + let (our_refusal, host_refusal) = self.answered(value, &mut len); + (if ours { our_refusal } else { host_refusal }).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) + (call(true), call(false)) } } -/// 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 }), +/// What Darwin answers where it is not what Linux and libc do. A TCP option +/// asked of a datagram socket it calls invalid, once it has read a setter's +/// value. Of an option the socket has, 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(foreign: bool, setter: bool, null: bool, len: u32) -> Option { + match (foreign, setter, null, len) { + (true, true, true, 1..) => Some(Answer::Refused(EFAULT)), + (true, ..) => Some(Answer::Refused(EINVAL)), + (false, true, true, 1 | 2) => Some(Answer::Refused(EFAULT)), + (false, 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) { +fn hold((ours, host): (Answer, Answer), theirs: Option, row: &str) { + let expected = match theirs { Some(theirs) if cfg!(target_os = "macos") => theirs, _ => ours, }; @@ -154,31 +185,47 @@ fn hold((ours, host): (Answer, Answer), setter: bool, null: bool, len: u32, row: 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 }; + let rows = [ + (Kept::Broadcast, datagram.as_raw_fd(), true), + (Kept::NoDelay, stream.as_raw_fd(), false), + (Kept::NoDelay, datagram.as_raw_fd(), true), + ]; + for (option, fd, is_datagram) in rows { + let mut pair = Pair { option, fd, datagram: is_datagram, kept: false }; + let foreign = pair.foreign(); + let of = if is_datagram { "a datagram socket" } else { "a stream" }; 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 row = format!("{option:?} of {of} {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}")); + let set = pair.set(at, len); + hold(set, darwin(foreign, true, null, len), &format!("set {row}")); + let got = pair.get(null, len); + hold(got, darwin(foreign, false, null, len), &format!("get {row}")); + if foreign { + assert_eq!(set.0, refused(Refusal::NoSuchOption), "set {row}"); + assert_eq!(got.0, refused(Refusal::NotSupported), "get {row}"); + } } } + if foreign { + continue; + } 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])); + let (ours, host) = pair.answered(buf.as_mut_ptr(), ptr::null_mut()); + let want = if foreign { Refusal::NotSupported } else { Refusal::Fault }; + assert_eq!((ours, buf), (Some(refused(want)), [UNWRITTEN; 8]), "{option:?} of {of}: a null length"); + // Darwin reads the length first, whichever option is asked. + let theirs = foreign.then_some(Answer::Refused(EFAULT)); + hold((refused(want), host.expect("the host refuses a null length")), theirs, &format!("{option:?} of {of}: a null length")); } } diff --git a/tests/netcase/nodelay_kept.c b/tests/netcase/nodelay_kept.c index 801db9eb002..85c3a053cdb 100644 --- a/tests/netcase/nodelay_kept.c +++ b/tests/netcase/nodelay_kept.c @@ -1,6 +1,8 @@ /* A TCP_NODELAY set on a stream socket before connect is kept and holds for - the connection, and one cleared stays cleared. argv: the address and the - port of a host that accepts and holds each connection. */ + the connection, and one cleared stays cleared. A listener's set is refused: + netstack has no option for one to hand the connections it accepts, where a + host keeps it and they begin with it. argv: the address and the port of a + host that accepts and holds each connection. */ #include #include #include @@ -52,13 +54,16 @@ int main(int argc, char **argv) { said("connect", connect(u, (struct sockaddr *)&peer, sizeof peer), 0); said("read after connect", nodelay(u), 0); - int d = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in any; + memset(&any, 0, sizeof any); + any.sin_family = AF_INET; + int l = socket(AF_INET, SOCK_STREAM, 0); + said("bind a third socket", bind(l, (struct sockaddr *)&any, sizeof any), 0); + said("listen on it", listen(l, 1), 0); errno = 0; - said("set on a datagram socket", setsockopt(d, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), -1); - said("which is invalid at that socket", errno == EINVAL, 1); - errno = 0; - said("nor read from one", nodelay(d), -1); - said("for the same reason", errno == EINVAL, 1); + said("set on the listener", setsockopt(l, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), -1); + said("which is refused as not connected", errno == ENOTCONN, 1); + said("read from the listener", nodelay(l), 0); if (wrong) { printf("nodelay_kept: %d wrong\n", wrong); diff --git a/tests/toyos.rs b/tests/toyos.rs index b921e0a5b04..1cc8c0682d9 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2919,22 +2919,14 @@ fn scanout_wc(console: &str) -> Result<(), String> { /// are the verdict. fn netstack_socket_churn() -> Result<(), String> { const JOB: &str = "netstack_socket_churn"; - const LEASED: &str = "netstack: DHCP: lease "; let server = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the host server: {e}"))?; let port = server.local_addr().map_err(|e| format!("the host server's port: {e}"))?.port(); // Ends with the process: a guest that never dials leaves it in `accept`. thread::spawn(move || server.incoming().for_each(drop)); - // A second that holds what it accepts and reads none of it, for as long - // as the process lives. - let holder = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the holding server: {e}"))?; - let holding = holder.local_addr().map_err(|e| format!("the holding server's port: {e}"))?.port(); - thread::spawn(move || holder.incoming().collect::>()); + let holding = holding_server()?; let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); - let case = compile::repo_root().join("tests/netcase"); - let mut qemu = QemuInstance::boot_with_options(&case, &[], &[(JOB.to_string(), bin)], BootOptions::default()); - let mut console = qemu.boot_log().to_string(); - await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + let mut qemu = boot_netcase(&[], &[(JOB.to_string(), bin)])?; let result = qemu.run_test(&format!("test_rs_netstack_socket_churn {port} {holding}"), Duration::from_secs(120)); if let Some(why) = &result.error { @@ -2949,19 +2941,37 @@ fn netstack_socket_churn() -> Result<(), String> { Ok(()) } +/// A host server that holds each connection it accepts and reads none of it, +/// for as long as the process lives: its port. A guest that never dials +/// leaves it in `accept`. +fn holding_server() -> Result { + let holder = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the holding server: {e}"))?; + let port = holder.local_addr().map_err(|e| format!("the holding server's port: {e}"))?.port(); + thread::spawn(move || holder.incoming().collect::>()); + Ok(port) +} + +/// Boot `tests/netcase` with these binaries staged, to netstack's lease: its +/// jobs name their peer by an address. +fn boot_netcase(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result { + const LEASED: &str = "netstack: DHCP: lease "; + let case = compile::repo_root().join("tests/netcase"); + let mut qemu = QemuInstance::boot_with_options(&case, c_bins, rust_bins, BootOptions::default()); + let mut console = qemu.boot_log().to_string(); + await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + Ok(qemu) +} + /// libc's sockets as a C program uses them, on one boot of `tests/netcase`: /// each of its C cases dials a host server that holds what it accepts, or /// sends to one that answers each datagram with itself, at the address the /// guest's network gives the host. A case's own comparisons are its verdict. fn libc_sockets() -> Result<(), String> { - const LEASED: &str = "netstack: DHCP: lease "; const HOST: &str = "10.0.2.2"; - let holder = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the holding server: {e}"))?; - let holding = holder.local_addr().map_err(|e| format!("the holding server's port: {e}"))?.port(); - // Each ends with the process: a guest that never dials leaves it waiting. - thread::spawn(move || holder.incoming().collect::>()); + let holding = holding_server()?; let echo = std::net::UdpSocket::bind(("127.0.0.1", 0)).map_err(|e| format!("the answering server: {e}"))?; let answering = echo.local_addr().map_err(|e| format!("the answering server's port: {e}"))?.port(); + // Ends with the process, as the holding server does. thread::spawn(move || { let mut datagram = [0u8; 64]; while let Ok((len, from)) = echo.recv_from(&mut datagram) { @@ -2976,9 +2986,7 @@ fn libc_sockets() -> Result<(), String> { .iter() .map(|(name, _)| (name.to_string(), compile::link_toyos(&compile::compile_own_c(&case, name), name))) .collect(); - let mut qemu = QemuInstance::boot_with_options(&case, &bins, &[], BootOptions::default()); - let mut console = qemu.boot_log().to_string(); - await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + let mut qemu = boot_netcase(&bins, &[])?; for (name, port) in cases { let result = qemu.run_test(&format!("test_c_{name} {HOST} {port}"), Duration::from_secs(120)); if qemu::VERBOSE.load(std::sync::atomic::Ordering::Relaxed) { diff --git a/userland/libc/src/errno.rs b/userland/libc/src/errno.rs index a7d52c47a4e..6bd30ef43b2 100644 --- a/userland/libc/src/errno.rs +++ b/userland/libc/src/errno.rs @@ -38,6 +38,7 @@ pub(crate) const EDEADLK: i32 = 35; pub(crate) const ENOSYS: i32 = 38; pub(crate) const EOVERFLOW: i32 = 75; pub(crate) const EILSEQ: i32 = 84; +pub(crate) const ENOPROTOOPT: i32 = 92; pub(crate) const EOPNOTSUPP: i32 = 95; pub(crate) const EAFNOSUPPORT: i32 = 97; pub(crate) const EADDRINUSE: i32 = 98; diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs index d54440019b3..da8edcbc514 100644 --- a/userland/libc/src/inaddr.rs +++ b/userland/libc/src/inaddr.rs @@ -2,14 +2,18 @@ //! order: an address is its four octets in memory order and a port its two, //! high first. Every address libc reads from or writes to a C program passes //! through [`SockaddrIn`], which holds both as bytes, so no integer of either -//! exists to be read in the machine's order. The texts `inet_pton`, -//! `inet_addr` and `inet_ntop` read and write are here too. It reads and sets -//! nothing but what it is handed, so the host tests it (`toyos-libc-copies`). +//! exists to be read in the machine's order, and which answers a caller's +//! buffer no more bytes than it holds. The texts `inet_pton`, `inet_addr` and +//! `inet_ntop` read and write are here too, with those calls' refusals. It +//! reads and sets nothing but what it is handed, so the host tests it +//! (`toyos-libc-copies`) against the host C library's own calls. + +use core::mem::size_of; pub(crate) const AF_INET: i32 = 2; /// C's `struct sockaddr_in`, as `include/netinet/in.h` lays it out. -#[repr(C)] +#[repr(C, align(4))] pub(crate) struct SockaddrIn { sin_family: u16, sin_port: [u8; 2], @@ -26,16 +30,67 @@ impl SockaddrIn { pub(crate) fn endpoint(&self) -> Option<([u8; 4], u16)> { (self.sin_family == AF_INET as u16).then(|| (self.sin_addr, u16::from_be_bytes(self.sin_port))) } + + /// Answers a caller this address as POSIX has a call store one: as many + /// of its bytes as `*len` holds, and its own length written back. + /// + /// # Safety + /// `len` is a readable and writable length, and `addr` `*len` writable + /// bytes. + pub(crate) unsafe fn answer(&self, addr: *mut u8, len: *mut u32) { + let whole = size_of::(); + core::ptr::copy_nonoverlapping((self as *const Self).cast::(), addr, whole.min(*len as usize)); + *len = whole as u32; + } +} + +/// Why `inet_pton` or `inet_ntop` answers no address. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum Refusal { + /// A family that is not `AF_INET`: `EAFNOSUPPORT`. + Family, + /// `inet_ntop`'s buffer does not hold the text and its NUL: `ENOSPC`. + Room, +} + +/// `inet_pton`: `text`'s address, `None` for a text that is none. +pub(crate) fn pton(af: i32, text: &[u8]) -> Result, Refusal> { + if af != AF_INET { + return Err(Refusal::Family); + } + Ok(dotted_quad(text)) +} + +/// `inet_ntop`: the text of the address at `src` and its NUL into the `size` +/// bytes at `dst`, which a refusal leaves unwritten. +/// +/// # Safety +/// `src` is an `AF_INET` address's four bytes, read once the family is known +/// to be that, and `dst` is `size` writable bytes. +pub(crate) unsafe fn ntop(af: i32, src: *const u8, dst: *mut u8, size: u32) -> Result<(), Refusal> { + if af != AF_INET { + return Err(Refusal::Family); + } + let mut text = [0u8; 16]; + let len = dotted_text(src.cast::<[u8; 4]>().read(), &mut text); + if len as u32 >= size { + return Err(Refusal::Room); + } + core::ptr::copy_nonoverlapping(text.as_ptr(), dst, len + 1); + Ok(()) } /// `inet_pton`'s text, POSIX's `ddd.ddd.ddd.ddd`: four decimal numbers of one -/// to three digits, each at most 255, and nothing else. +/// to three digits, each at most 255, and nothing else. A number of more than +/// one digit that begins with `0` is refused, as glibc refuses it: `inet_addr` +/// reads such a number as octal, and no text has two values. pub(crate) fn dotted_quad(text: &[u8]) -> Option<[u8; 4]> { let mut octets = [0u8; 4]; let mut parts = text.split(|&c| c == b'.'); for octet in &mut octets { let part = parts.next()?; - if !(1..=3).contains(&part.len()) || !part.iter().all(u8::is_ascii_digit) { + let zero_led = part.len() > 1 && part[0] == b'0'; + if !(1..=3).contains(&part.len()) || zero_led || !part.iter().all(u8::is_ascii_digit) { return None; } let value = part.iter().fold(0u16, |v, &c| v * 10 + u16::from(c - b'0')); @@ -86,7 +141,7 @@ pub(crate) fn numbers_and_dots(text: &[u8]) -> Option<[u8; 4]> { /// `ip` as `inet_ntop` writes it into `buf`, NUL-terminated; the text's /// length without the NUL. `buf` is `INET_ADDRSTRLEN` bytes, which holds the /// longest. -pub(crate) fn dotted_text(ip: [u8; 4], buf: &mut [u8; 16]) -> usize { +fn dotted_text(ip: [u8; 4], buf: &mut [u8; 16]) -> usize { let mut at = 0; for (i, octet) in ip.into_iter().enumerate() { if i > 0 { diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs index 991ce26dcba..600916d99f9 100644 --- a/userland/libc/src/socket.rs +++ b/userland/libc/src/socket.rs @@ -7,7 +7,10 @@ use toyos_abi::RawHandle; use toyos_abi::syscall; use toyos::net::{NetError, TcpSocketId, UdpSocketId, OPT_BROADCAST, OPT_NODELAY}; -use crate::errno::{EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EFAULT, EINVAL, EIO, ENOMEM, ENOSPC, ENOTCONN, ETIMEDOUT}; +use crate::errno::{ + EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EFAULT, EINVAL, EIO, ENOMEM, ENOPROTOOPT, ENOSPC, + ENOTCONN, EOPNOTSUPP, ETIMEDOUT, +}; use crate::inaddr::{self, SockaddrIn, AF_INET}; use crate::sockopt::{self, Kept}; @@ -122,8 +125,7 @@ unsafe fn fill_sockaddr(addr: *mut Sockaddr, addrlen: *mut SocklenT, ip: [u8; 4] if addr.is_null() || addrlen.is_null() { return; } - (addr as *mut SockaddrIn).write(SockaddrIn::new(ip, port)); - *addrlen = core::mem::size_of::() as SocklenT; + SockaddrIn::new(ip, port).answer(addr as *mut u8, addrlen); } /// Bind a datagram socket netstack does not hold yet, handing over a @@ -549,15 +551,11 @@ fn option_errno(refusal: sockopt::Refusal) -> i32 { match refusal { sockopt::Refusal::Short => EINVAL, sockopt::Refusal::Fault => EFAULT, + sockopt::Refusal::NoSuchOption => ENOPROTOOPT, + sockopt::Refusal::NotSupported => EOPNOTSUPP, } } -/// POSIX's `EINVAL`, an option invalid at the socket's level: TCP's are no -/// datagram socket's. -fn tcp_option_of_a_datagram_socket(entry: &SocketEntry, option: Option) -> bool { - option == Some(Kept::NoDelay) && matches!(entry.kind, SocketKind::Udp) -} - #[no_mangle] pub unsafe extern "C" fn setsockopt( fd: i32, @@ -576,20 +574,21 @@ pub unsafe extern "C" fn setsockopt( }; // All other options silently succeed (SO_REUSEADDR, SO_KEEPALIVE, etc.) - let Some(option) = sockopt::kept(level, optname) else { return 0 }; - if tcp_option_of_a_datagram_socket(entry, Some(option)) { - set_errno(EINVAL); - return -1; - } + let option = match sockopt::kept(level, optname, matches!(entry.kind, SocketKind::Udp), true) { + 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; } }; match option { Kept::NoDelay => { - // netstack holds a connection from `connect` or `accept`; a - // listener's id names none. - if entry.connected { + // netstack holds a connection from `connect` or `accept`, and a + // listener from `bind`, whose id names no connection: it refuses + // a listener's set, and nothing is kept for one. + 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; @@ -629,11 +628,10 @@ pub unsafe extern "C" fn getsockopt( None => { set_errno(EBADF); return -1; } }; - let option = sockopt::kept(level, optname); - if tcp_option_of_a_datagram_socket(entry, option) { - set_errno(EINVAL); - return -1; - } + let option = match sockopt::kept(level, optname, matches!(entry.kind, SocketKind::Udp), false) { + Ok(option) => option, + Err(refusal) => { set_errno(option_errno(refusal)); return -1; } + }; let value = match option { Some(Kept::NoDelay) => entry.nodelay as i32, Some(Kept::Broadcast) => entry.broadcast as i32, @@ -781,17 +779,21 @@ pub unsafe extern "C" fn gai_strerror(_errcode: i32) -> *const u8 { #[no_mangle] pub unsafe extern "C" fn inet_pton(af: i32, src: *const u8, dst: *mut u8) -> i32 { - if af != AF_INET { - set_errno(EAFNOSUPPORT); - return -1; - } let text = core::slice::from_raw_parts(src, super::string::strlen(src)); - match inaddr::dotted_quad(text) { - Some(ip) => { + match inaddr::pton(af, text) { + Ok(Some(ip)) => { ptr::copy_nonoverlapping(ip.as_ptr(), dst, 4); 1 } - None => 0, + Ok(None) => 0, + Err(refusal) => { set_errno(address_errno(refusal)); -1 } + } +} + +fn address_errno(refusal: inaddr::Refusal) -> i32 { + match refusal { + inaddr::Refusal::Family => EAFNOSUPPORT, + inaddr::Refusal::Room => ENOSPC, } } @@ -802,18 +804,10 @@ pub unsafe extern "C" fn inet_ntop( dst: *mut u8, size: SocklenT, ) -> *const u8 { - if af != AF_INET { - set_errno(EAFNOSUPPORT); - return ptr::null(); - } - let mut text = [0u8; 16]; - let n = inaddr::dotted_text((src as *const [u8; 4]).read(), &mut text); - if n as u32 >= size { - set_errno(ENOSPC); - return ptr::null(); + match inaddr::ntop(af, src, dst, size) { + Ok(()) => dst as *const u8, + Err(refusal) => { set_errno(address_errno(refusal)); ptr::null() } } - ptr::copy_nonoverlapping(text.as_ptr(), dst, n + 1); - dst as *const u8 } #[no_mangle] diff --git a/userland/libc/src/sockopt.rs b/userland/libc/src/sockopt.rs index 0c67a100869..fc19f7da1ac 100644 --- a/userland/libc/src/sockopt.rs +++ b/userland/libc/src/sockopt.rs @@ -18,12 +18,16 @@ pub(crate) enum Kept { Broadcast, } -/// The kept option `optname` names at `level`. -pub(crate) fn kept(level: i32, optname: i32) -> Option { +/// The kept option `optname` names at `level`, asked of a datagram socket or a +/// stream. A datagram socket has no option at TCP's level, which Linux says +/// before it reads a value or a length, and in two words: to a setter +/// [`Refusal::NoSuchOption`], to a getter [`Refusal::NotSupported`]. +pub(crate) fn kept(level: i32, optname: i32, datagram: bool, setter: bool) -> Result, Refusal> { match (level, optname) { - (IPPROTO_TCP, TCP_NODELAY) => Some(Kept::NoDelay), - (SOL_SOCKET, SO_BROADCAST) => Some(Kept::Broadcast), - _ => None, + (IPPROTO_TCP, _) if datagram => Err(if setter { Refusal::NoSuchOption } else { Refusal::NotSupported }), + (IPPROTO_TCP, TCP_NODELAY) => Ok(Some(Kept::NoDelay)), + (SOL_SOCKET, SO_BROADCAST) => Ok(Some(Kept::Broadcast)), + _ => Ok(None), } } @@ -33,6 +37,10 @@ pub(crate) enum Refusal { Short, /// A null pointer where bytes are read or written: `EFAULT`. Fault, + /// A setter's level the socket's protocol has no option at: `ENOPROTOOPT`. + NoSuchOption, + /// A getter's: `EOPNOTSUPP`. + NotSupported, } /// What a setter is told: whether the `int` at `optval` is non-zero. The From 2c8a4ad581d752641bec547153cd362b49cb10cf Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 05:45:17 +0200 Subject: [PATCH 7/8] accept hands its listener's kept TCP_NODELAY to netstack, and a guest case reads it on a connection the host dials in Round 2 of #787's review, its five NOTEs. accept: with a listener's set after bind refused, the value a listener holds is frozen at bind and is the option it held when any connection to it began, which is the host's rule and the own stack's. A program that set it before bind was told yes, read 1 on the listener and got connections without it. accept now sends a kept one for the connection it was answered and stores it in the accepted socket's entry, as connect does; a refusal there closes the connection and fails the accept with the request's errno, recorded in the reset issue's sentence. The test: nodelay_kept.c gains two listeners, one that held the option at bind and one that did not, and reads the option on the socket accept returns from each. Nothing dialled into a guest before: the harness now asks QEMU's monitor to forward two loopback ports of its choosing (hostfwd_add, read back from info usernet) and dials both when the case says its listeners wait. The same source on the host, Darwin 27.0.0, against a loopback peer that dials in: the first 25 lines are the guest's, and the three on the refused set are the recorded difference. What the test reads is libc's entry, as getsockopt answers from it: accept storing the value and sending netstack nothing stays green, which nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack now says of accept beside connect. Issues. a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay is deleted: what remained of it, a set after bind refused until the pipe ABI gives a listener its option, is what a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess says and exits on; that file takes libc's half as it now stands and loses a message name the tree no longer has, and the track's listener-defects line names it. Filed: getsockname and getpeername answering a null buffer or a null length 0, with Darwin's answers measured. The counters record: the stall at 94fd25e71 is ten minutes after the two at 68cf1f870, not before them, and its compiler clause names the LLVM fault's file. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...oes-not-carry-its-listeners-tcp-nodelay.md | 43 ------------ ...-under-host-load-can-go-silent-for-15-s.md | 14 ++-- ...cepted-streams-is-answered-from-a-guess.md | 17 +++-- ...uses-the-option-requests-a-host-answers.md | 7 +- ...rname-answer-a-null-buffer-with-success.md | 41 +++++++++++ ...-a-streams-tcp-nodelay-reached-netstack.md | 9 ++- issues/toyos-has-its-own-network-stack.md | 2 +- tests/netcase/nodelay_kept.c | 34 ++++++++-- tests/toyos.rs | 68 ++++++++++++++++--- userland/libc/src/socket.rs | 15 +++- 10 files changed, 174 insertions(+), 76 deletions(-) delete mode 100644 issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md create mode 100644 issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md diff --git a/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md b/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md deleted file mode 100644 index 06829cbd89d..00000000000 --- a/issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# A connection libc accepts does not carry its listener's TCP_NODELAY - -A host's listener keeps `TCP_NODELAY` and the connections it accepts begin -with it. Measured on Darwin 27.0.0, one C program: the option set on a -listener before `bind` answers 0, set after `listen` answers 0, and -`getsockopt` on the socket `accept` returns reads it set. Linux is unread. - -netstack has no option for a listener: `MsgType::TcpSetOption` -(`toyos/src/net.rs`) names a connection, and netstack answers a listener's id -`ERR_NOT_CONNECTED`. So libc (`userland/libc/src/socket.rs`): - -- refuses `setsockopt(IPPROTO_TCP, TCP_NODELAY)` on a socket it has bound as - a listener with -1 and `ENOTCONN`, keeps nothing for it, and `getsockopt` - reads what the socket held before. `tests/netcase/nodelay_kept.c` holds the - refusal and the read. -- keeps a value set before `bind`, as it keeps one set before `connect`, and - the listener reads it back; `accept` makes the connection's entry with the - option clear and sends netstack nothing. Read from the code, not run. That - one is a program told yes and given no, and no guest test can read it: - nothing dials into a guest, whose network is QEMU's user backend with no - forwarded port (`tests/common/qemu.rs`). - -Not carried by `accept` on smoltcp, by the orchestrator's decision: the stack -this track builds gives a connection the option its listener held when the -connection began (`Options`, `toyos-net-shard/tcp/src/lib.rs`, "inherited by a -listener's children", set by `Tcp::set_listener_options`), and a hand-over at -`accept` would carry one set after the connection was queued too, which is -another rule. - -**Exit**: the stage of `issues/toyos-has-its-own-network-stack.md` that gives -the pipe ABI a listener's option: libc hands a listener's `TCP_NODELAY` to the -stack, at `bind` for one set before it and at the set for one after, both -answer 0, a connection `accept` returns reads the option its listener held -when it began, and `nodelay_kept.c`'s listener lines turn to the host's -answers. Until then the refusal stands. - -**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. diff --git a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md index 12e4be8e032..6de1f2db100 100644 --- a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md +++ b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md @@ -102,11 +102,12 @@ starts no netstack. named for that run was not kept; the run's log, outside the tree, is the only copy. -Two earlier stalls of `virt_mask_windows` have no console of the guest, so +Two other stalls of `virt_mask_windows` have no console of the guest, so the step each stopped in is not read from it: - **`wt/toyos-move-abi` at `94fd25e71`.** `FAIL virt_mask_windows: STALLED: - waiting for the boot's last word — it went quiet`, with nothing in what the + waiting for the boot's last word — it went quiet`, ten minutes after the + two at `68cf1f870` by the two runs' logs, with nothing in what the guest said while it was waited on, `STALL virt_mask_windows (38s)`; the whole suite at `--jobs 4`, 34 of 36, workers 1794 s building against 703 s testing. Load 59.30 69.33 70.48 when the suite began and 85.73 69.02 67.23 @@ -135,10 +136,11 @@ the step each stopped in is not read from it: 77 (38 s, with ceilings paid at 8.00x and 31 s between its image and its first guest line) and 75 to 67 (7 s). -Not known of any of them: the fork compiler miscompiles, for AArch64, an -inclusive range that ends at its integer type's maximum (LLVM's -ScalarEvolution), and whether that reaches `test_rs_counters_read` is being -measured by a per-function census on another branch. +Not known of any of them: whether the fork compiler's fault reaches +`test_rs_counters_read` or the kernel under it. +`issues/the-forks-llvm-deletes-a-loops-exit-on-a-no-wrap-flag-scalar-evolution-gives-the-wrong-value.md` +has LLVM's ScalarEvolution deleting a loop's exit on both architectures, and +records every function but the ones it names as not measured. The slow ones, with registers: a probe that captured `info registers -a` over QMP whenever the read had not ended 3 s after `unmap_touch` (`debug-slow.patch` diff --git a/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md b/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md index 4223534d926..ae9ae8036ff 100644 --- a/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md +++ b/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md @@ -17,17 +17,26 @@ connection waits to be accepted: a connect returns before the listener's end is established, so the answer rests on that event and not on the order loopback delivers in. A red there prints the host's name and both answers. -ToyOS as it ships does neither half: +ToyOS as it ships does neither half for an option set on a socket that +listens: -- netstack answers `TcpSetOption` and `TcpGetOption` `ERR_NOT_CONNECTED` for +- netstack answers `TcpSetOption` `ERR_NOT_CONNECTED` for any id that is not a stream (`handle_tcp_set_option`, `userland/netstack/src/main.rs`), and libc's `setsockopt` sends a listening socket's id there (`userland/libc/src/socket.rs`), so a C program that sets - the option on its listener is refused. + the option on its listener is refused, -1 and `ENOTCONN`, and the listener + keeps what it held. libc holds a listener from `bind`. One set before that + it keeps, and `accept` hands it to netstack for each connection, as + `connect` does: with every later set refused it is the option the listener + held when any connection began. `tests/netcase/nodelay_kept.c` holds the + refusal, and the accepted socket's read for a listener that held the option + at `bind` and for one that did not; its three lines on the refused set are + the ones this file's exit turns to the host's answers. - std builds every `TcpStream`, an accepted one included, with `nodelay: false` and answers `nodelay()` from that field without asking netstack (`sdk/std/sys/net/connection.rs`). Nothing can make that false - today, since no accepted connection has the option. Once a listener's + today: std has no setter for a listener, and what libc's `accept` hands + over is for a socket of libc's. Once a listener's option reaches the connections it accepts, it is false for each of them. The node (`userland/netstack/node/src/listeners.rs`) has the calls, 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 78efd6233be..5f3c2a03e32 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 @@ -30,9 +30,10 @@ 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. libc's `connect` asks the same request of a connection it has just been -answered, to hand over a `TCP_NODELAY` set before it: a peer that resets -between netstack's answer and that request makes `connect` fail `ENOTCONN`, -which no host's `connect` answers. +answered, to hand over a `TCP_NODELAY` set before it, and its `accept` of one +it has just been given, to hand over its listener's: a peer that resets +between netstack's answer and that request makes either fail `ENOTCONN`, with +the connection closed, which no host's `connect` or `accept` answers. **Exit condition**: a guest test on `tests/netcase` whose peer resets a stream the client still holds, and `nodelay()` on it answers `Ok`, with diff --git a/issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md b/issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md new file mode 100644 index 00000000000..d6403e041af --- /dev/null +++ b/issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md @@ -0,0 +1,41 @@ +--- +status: open +kind: defect +opened: 2026-10-09 +--- + +# libc's getsockname and getpeername answer a null buffer with success + +`getsockname` and `getpeername` (`userland/libc/src/socket.rs`) answer through +`fill_sockaddr`, which returns having written nothing where the address +buffer or the length pointer is null, and each call then answers 0. A program +that passed a null pointer is told its address was written. + +A host refuses it. Measured on Darwin 27.0.0, one C program, each call on a +connected stream: + +| | `getsockname` | `getpeername` | ToyOS, read from the code | +|---|---|---|---| +| null buffer, length 16 | -1 `EFAULT` | -1 `EFAULT` | 0, the length left at 16 | +| null length | -1 `EFAULT` | -1 `EFAULT` | 0 | +| null buffer, length 0 | 0, the length written 16 | 0, the length written 16 | 0, the length left at 0 | + +Linux is unread; by the review that found this, it reads the buffer only when +the length is not 0, which is Darwin's third row. + +`SockaddrIn::answer` (`userland/libc/src/inaddr.rs`), which the host test +`an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it` +(`tests/libc-arch/src/internet_addresses.rs`) holds against the host's +`getsockname` at every length, excludes both pointers by its contract, so no +test reaches the difference. + +`accept` and `recvfrom` take a null buffer as a caller that wants no address, +which POSIX gives them, and Darwin's `accept` answers a null pair with a +socket; what either answers a buffer with a null length is not read. + +**Exit**: the null cases are in the module a host builds, and a test in +`tests/libc-arch` compares the answer, `errno` and the length written back +with the host's own `getsockname` for the table's three rows, on each host +the `host` check runs on; `getpeername` answers through the same function. + +**Owner**: libc, `userland/libc`; whoever next changes its address calls. diff --git a/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md b/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md index 744c02df7f0..e203fc97284 100644 --- a/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md +++ b/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md @@ -13,14 +13,17 @@ value their own side stored. So a setter that stores the value and sends nothing is seen by no test. Measured on libc's `connect`, which hands over a `TCP_NODELAY` set before the connection existed: with the hand-over deleted, the guest case that sets the option before `connect` and reads it after -(`tests/netcase/nodelay_kept.c`, the `libc_sockets` test) stays green. The +(`tests/netcase/nodelay_kept.c`, the `libc_sockets` test) stays green. And on +its `accept`, which hands over the one its listener held at `bind`: with that +request deleted and the value still stored, the same case's read of the +accepted socket stays green. The only effect of the option is when a small write leaves the machine, which a QEMU test may not time. **Exit**: a test reads what the stack holds for a stream's Nagle switch, on the node under a host test of its option call or through a guest's wire, and -turns red when libc's `connect` or std's `set_nodelay` stores the value and -sends no request. +turns red when libc's `connect` or `accept` or std's `set_nodelay` stores the +value and sends no request. **Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`, with the reset-stream option test of diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 93e4c1a8a4a..9e27c442df5 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -45,7 +45,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 two parameters are read only by netd's resolver on smoltcp. Exit, at the move: they go with that resolver. - `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-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`, in std's accept. +The listener defects are this track's: `issues/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`, in std's accept; and `issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md`, whose exit waits on the pipe ABI giving a listener its option, which the node has (`Node::set_listener_nodelay`) and nothing ships. Owed from the stage 3 specifications: by 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/tests/netcase/nodelay_kept.c b/tests/netcase/nodelay_kept.c index 85c3a053cdb..da71e9f4090 100644 --- a/tests/netcase/nodelay_kept.c +++ b/tests/netcase/nodelay_kept.c @@ -1,8 +1,11 @@ /* A TCP_NODELAY set on a stream socket before connect is kept and holds for - the connection, and one cleared stays cleared. A listener's set is refused: - netstack has no option for one to hand the connections it accepts, where a - host keeps it and they begin with it. argv: the address and the port of a - host that accepts and holds each connection. */ + the connection, and one cleared stays cleared. One set before bind is the + listener's, and each connection it accepts begins with it. A listener's set + after bind is refused: netstack has no option for a listener, where a host + takes the set and gives it to the connections that begin afterwards. argv: + the address and the port of a host that accepts and holds each connection, + then the two ports to listen on, which that host dials once WAITING is + said. */ #include #include #include @@ -12,6 +15,8 @@ #include #include +#define WAITING "nodelay_kept: both listeners wait for a peer" + static int wrong; static void said(const char *what, long got, long want) { @@ -28,7 +33,7 @@ static int nodelay(int fd) { } int main(int argc, char **argv) { - if (argc != 3) return 2; + if (argc != 5) return 2; struct sockaddr_in peer; int on = 1, off = 0; @@ -57,9 +62,26 @@ int main(int argc, char **argv) { struct sockaddr_in any; memset(&any, 0, sizeof any); any.sin_family = AF_INET; + int held = socket(AF_INET, SOCK_STREAM, 0); + said("set on a third socket before bind", setsockopt(held, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + any.sin_port = htons((uint16_t)atoi(argv[3])); + said("bind it", bind(held, (struct sockaddr *)&any, sizeof any), 0); + said("listen on it", listen(held, 1), 0); + said("read from that listener", nodelay(held), 1); int l = socket(AF_INET, SOCK_STREAM, 0); - said("bind a third socket", bind(l, (struct sockaddr *)&any, sizeof any), 0); + any.sin_port = htons((uint16_t)atoi(argv[4])); + said("bind a fourth socket", bind(l, (struct sockaddr *)&any, sizeof any), 0); said("listen on it", listen(l, 1), 0); + + printf("%s\n", WAITING); + fflush(stdout); + int with = accept(held, NULL, NULL); + said("accept from the listener that held the option", with >= 0, 1); + said("read from its connection", nodelay(with), 1); + int without = accept(l, NULL, NULL); + said("accept from the listener that held none", without >= 0, 1); + said("read from its connection", nodelay(without), 0); + errno = 0; said("set on the listener", setsockopt(l, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), -1); said("which is refused as not connected", errno == ENOTCONN, 1); diff --git a/tests/toyos.rs b/tests/toyos.rs index 422ebffcf79..e67da326128 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2934,7 +2934,7 @@ fn netstack_socket_churn() -> Result<(), String> { let holding = holding_server()?; let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); - let mut qemu = boot_netcase(&[], &[(JOB.to_string(), bin)])?; + let mut qemu = boot_netcase(&[], &[(JOB.to_string(), bin)], BootOptions::default())?; let result = qemu.run_test(&format!("test_rs_netstack_socket_churn {port} {holding}"), Duration::from_secs(120)); if let Some(why) = &result.error { @@ -2961,21 +2961,59 @@ fn holding_server() -> Result { /// Boot `tests/netcase` with these binaries staged, to netstack's lease: its /// jobs name their peer by an address. -fn boot_netcase(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result { +fn boot_netcase( + c_bins: &[(String, Vec)], + rust_bins: &[(String, Vec)], + options: BootOptions, +) -> Result { const LEASED: &str = "netstack: DHCP: lease "; let case = compile::repo_root().join("tests/netcase"); - let mut qemu = QemuInstance::boot_with_options(&case, c_bins, rust_bins, BootOptions::default()); + let mut qemu = QemuInstance::boot_with_options(&case, c_bins, rust_bins, options); let mut console = qemu.boot_log().to_string(); await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; Ok(qemu) } +/// Have the guest's network carry what dials a loopback port of QEMU's +/// choosing to each of `guest_ports`: those host ports, in that order. +fn forwards_into(qemu: &QemuInstance, guest_ports: [u16; 2]) -> Result<[u16; 2], String> { + const FORWARD: &str = "TCP[HOST_FORWARD]"; + let mut monitor = qemu::QmpMonitor::open(qemu.qmp_socket()); + for port in guest_ports { + // `net0` is the NIC's backend in `common::qemu`'s argv. + let said = monitor.human(&format!("hostfwd_add net0 tcp:127.0.0.1:0-:{port}")); + if !said.trim().is_empty() { + return Err(format!("QEMU forwards nothing to the guest's port {port}: {said}")); + } + } + // A row is the protocol, a descriptor, the host's address and port, then + // the guest's. + let table = monitor.human("info usernet"); + let mut host_ports = [0u16; 2]; + for (host, guest) in host_ports.iter_mut().zip(guest_ports) { + *host = table + .lines() + .map(|row| row.split_whitespace().collect::>()) + .find(|row| row.first() == Some(&FORWARD) && row.get(5) == Some(&guest.to_string().as_str())) + .and_then(|row| row[3].parse().ok()) + .ok_or_else(|| format!("QEMU names no host port forwarded to the guest's {guest}:\n{table}"))?; + } + Ok(host_ports) +} + /// libc's sockets as a C program uses them, on one boot of `tests/netcase`: /// each of its C cases dials a host server that holds what it accepts, or /// sends to one that answers each datagram with itself, at the address the -/// guest's network gives the host. A case's own comparisons are its verdict. +/// guest's network gives the host; and once `nodelay_kept` says its two +/// listeners wait, the host dials each through a port QEMU forwards. A case's +/// own comparisons are its verdict. fn libc_sockets() -> Result<(), String> { const HOST: &str = "10.0.2.2"; + /// The ports `nodelay_kept` listens on in the guest, which nothing else + /// on its boot binds. + const LISTENERS: [u16; 2] = [7001, 7002]; + /// `nodelay_kept.c`'s `WAITING`. + const WAITING: &str = "nodelay_kept: both listeners wait for a peer"; let holding = holding_server()?; let echo = std::net::UdpSocket::bind(("127.0.0.1", 0)).map_err(|e| format!("the answering server: {e}"))?; let answering = echo.local_addr().map_err(|e| format!("the answering server's port: {e}"))?.port(); @@ -2988,15 +3026,29 @@ fn libc_sockets() -> Result<(), String> { }); // The address first: every case names its peer by one. - let cases = [("addr_order", holding), ("nodelay_kept", holding), ("sendto_unbound", answering)]; + let [held, clear] = LISTENERS; + let cases = [ + ("addr_order", holding.to_string()), + ("nodelay_kept", format!("{holding} {held} {clear}")), + ("sendto_unbound", answering.to_string()), + ]; let case = compile::repo_root().join("tests/netcase"); let bins: Vec<(String, Vec)> = cases .iter() .map(|(name, _)| (name.to_string(), compile::link_toyos(&compile::compile_own_c(&case, name), name))) .collect(); - let mut qemu = boot_netcase(&bins, &[])?; - for (name, port) in cases { - let result = qemu.run_test(&format!("test_c_{name} {HOST} {port}"), Duration::from_secs(120)); + let mut qemu = boot_netcase(&bins, &[], BootOptions { qmp: true, ..Default::default() })?; + let forwarded = forwards_into(&qemu, LISTENERS)?; + // Held to the test's end: a peer gone before the case's `accept` fails its hand-over. + let mut dialled = Vec::new(); + for (name, ports) in cases { + let result = + qemu.run_test_hooked(&format!("test_c_{name} {HOST} {ports}"), Duration::from_secs(120), WAITING, |_| { + dialled.extend(forwarded.map(|port| std::net::TcpStream::connect(("127.0.0.1", port)))); + }); + if let Some(Err(e)) = dialled.iter().find(|dial| dial.is_err()) { + return Err(format!("{name}: the host could not dial a port QEMU forwards to the guest: {e}")); + } if qemu::VERBOSE.load(std::sync::atomic::Ordering::Relaxed) { eprintln!(" [libc] {name} ended {:?} and said:\n{}", result.exit_code, result.stdout); } diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs index 600916d99f9..4cbc660eeec 100644 --- a/userland/libc/src/socket.rs +++ b/userland/libc/src/socket.rs @@ -64,7 +64,7 @@ struct SocketEntry { // What netstack holds for the socket, which `getsockopt` answers from. A // socket netstack does not hold yet keeps each for the call that makes it // there to hand over: `broadcast` for a datagram socket's bind, `nodelay` - // for a stream's `connect`. + // for a stream's `connect` and for each connection a listener accepts. nodelay: bool, broadcast: bool, } @@ -290,6 +290,9 @@ pub unsafe extern "C" fn accept( }; let listener_id = TcpSocketId(entry.netstack_id); let notify_fd = entry.notify_fd; + // A set after `bind` is refused, so this is the option the listener held + // when any connection to it began. + let nodelay = entry.nodelay; // Block until a connection arrives (read 1 byte from notify pipe) let mut notify_byte = [0u8; 1]; @@ -299,6 +302,14 @@ pub unsafe extern "C" fn accept( Ok(a) => a, Err(e) => { set_errno(net_err_to_errno(e)); return -1; } }; + if nodelay { + if let Err(e) = toyos::net::tcp_set_option(accepted.socket_id, OPT_NODELAY, 1) { + // `accepted`'s pipe ends close where it drops. + let _ = toyos::net::tcp_close(accepted.socket_id); + set_errno(net_err_to_errno(e)); + return -1; + } + } if !addr.is_null() && !addrlen.is_null() { fill_sockaddr(addr, addrlen, accepted.remote_addr, accepted.remote_port); @@ -315,7 +326,7 @@ 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, + nodelay, broadcast: false, }; let new_fd = alloc_socket(new_entry); From 5d9c996e20aaa35359bdcac2ff95d3295d7a915c Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 06:13:36 +0200 Subject: [PATCH 8/8] The reset-stream issue's exit ends the two libc compromises it records The file records that libc's connect and accept fail ENOTCONN, with the connection closed, when the peer resets before the TCP_NODELAY hand-over. Its exit was met by a guest test whose nodelay() answers Ok, which asks netstack nothing, so both failures would have stood with no record left. The exit now also asks that netstack answers the option request on a stream its peer reset, and that connect and accept answer as the host does, each by a test that can be red. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...-peer-reset-refuses-the-option-requests-a-host-answers.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 5f3c2a03e32..2bee6b0e5b1 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 @@ -37,6 +37,9 @@ the connection closed, which no host's `connect` or `accept` answers. **Exit condition**: a guest test on `tests/netcase` whose peer resets a stream the client still holds, and `nodelay()` on it answers `Ok`, with -`netstack_socket_churn` still green. +`netstack_socket_churn` still green; and the two compromises above ended: the +option request on a stream its peer reset is answered by netstack, and a libc +`connect` and a libc `accept` whose peer resets before the hand-over answer as +the host does, each held by a test that can be red. **Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`.