Skip to content

libc binds a datagram socket at its first send, keeps a TCP_NODELAY for connect and for the connections a listener accepts, holds an IPv4 address as its octets, and truncates one to its caller's buffer - #787

Merged
Japabu merged 12 commits into
mainfrom
wt/toyos-libc-sockets
Oct 9, 2026

Conversation

@Japabu

@Japabu Japabu commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Head 5d9c996e2: 2c8a4ad58, the head every measurement below was taken at, and one commit over it that changes one file of issues/ alone, the exit condition of a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md (git diff 2c8a4ad58 5d9c996e2 --stat: 1 file, +4 −1; cargo test --lib sourcegate at it, exit 0). At 2c8a4ad58: this stage on main at 0d6cd9a60, which carries #783, the change that filed the three defects this closes. The merge of main (f1b8b85e7) was git's own, no conflict: its tree is eae4f5926b06, the tree git merge-tree --write-tree 0d6cd9a60 7d82642f3 writes, and against main it was the stage's 26 files alone, +1200 −357 (round 7, merge). At 2c8a4ad58 the stage is 27 files, +1304 −363, and at 5d9c996e2 +1308 −364, issues/ +216 −122: userland/libc +301 −179, tests +791 −63, issues/ +212 −121. Round 7 over round 6 is one hunk of accept in userland/libc/src/socket.rs, tests/netcase/nodelay_kept.c, the case's side of tests/toyos.rs, and issues/.

Three defects of libc's sockets, each fixed at its owner in userland/libc, each with a guest C case that is red without its fix, and each issue deleted; and, by the round-1 review, a fourth: an address written past a caller's buffer.

What changed, per decision

  • An unbound datagram socket is bound at its first send. sendto on a socket netstack does not hold wrote the datagram to handle 0 and asked netstack to send from socket 0. It now binds to a port netstack chooses first, as POSIX has it. bind and that first send go through one function, bind_datagram, so a SO_BROADCAST set before the socket existed in netstack is handed over on either path, with The pipe ABI carries a datagram socket's broadcast permission from std and libc to netstack, and the option read is gone #783's refusal handling.
  • A TCP_NODELAY set before connect is kept and handed over. setsockopt stores it for a stream netstack does not hold yet; connect sends a kept one once tcp_connect has answered, and a refusal there closes the connection and fails the connect.
  • A listener's TCP_NODELAY set after bind stays refused, and one set before bind reaches the connections it accepts, the orchestrator's decisions on round 1's second BLOCKER and round 2's first NOTE. The guard in setsockopt is netstack_id != 0: netstack holds a listener from bind and answers its id ERR_NOT_CONNECTED, so the set is -1 ENOTCONN and nothing is kept for it. That freezes a listener's value at bind, so it is exactly the option the listener held when any connection to it began, which is the host's rule and the own stack's. accept hands a kept one to netstack for the connection it was answered and stores it in the accepted socket's entry, as connect does; before this a program that set it before bind was told yes, read 1 on the listener and got connections without it. A refusal of the hand-over, a peer that reset in between, closes the connection and fails the accept with the request's errno, as connect's path does: issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md says so. What is left, the set after bind, is issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md, whose exit is the pipe ABI giving a listener its option.
  • A TCP option asked of a datagram socket is refused in sockopt.rs, for every name at TCP's level, ahead of the value's length and pointers. The answer implemented is Linux's, by reading and not by measurement: ENOPROTOOPT to a setter (udp_setsockopt falls to ip_setsockopt, which refuses a level that is not SOL_IP) and EOPNOTSUPP to a getter (do_ip_getsockopt's refusal of the same). Darwin answers EINVAL, and EFAULT to a setter handed a null value of one byte or more (measured, round 6, host-dgram-nodelay); that is in darwin(). CI's Linux host job is the measurement, read by the orchestrator at the head that lands: a Linux red on the row NoDelay of a datagram socket in socket_options::an_options_value_crosses_as_the_hosts_does sends this back.
  • An address is its octets, by type. userland/libc/src/inaddr.rs holds SockaddrIn with sin_port: [u8; 2] and sin_addr: [u8; 4]: no integer of either exists to be read in the machine's order. It is #[repr(C, align(4))], C's alignment, asserted with its size.
  • An address is truncated to its caller's buffer. fill_sockaddr, which accept, recvfrom, getpeername and getsockname answer through, wrote sixteen bytes whatever *addrlen held. SockaddrIn::answer writes as many as *addrlen holds and writes the address's own length back, as POSIX has it and as the host's getsockname does at every length from 0 to 32. One function; issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md is deleted, and nothing cited it.
  • The texts and the two calls' refusals are in that module and host-tested. pton and ntop hold the family refusal and inet_ntop's fit; numbers_and_dots is inet_addr. inet_pton, and so getaddrinfo, refuses a number with a zero before another digit, as glibc does: inet_addr reads 010 as octal, and no text has two values. Darwin reads such a number as decimal with any count of digits, which the differential states (darwin_reads).
  • Headers. netinet/in.h gains INADDR_BROADCAST, in_addr_t and in_port_t; arpa/inet.h declares inet_addr as in_addr_t; TCP_NODELAY moves to a new netinet/tcp.h, where POSIX has it.
  • The harness. tests/toyos.rs's two netcase tests boot through one boot_netcase, which owns the lease line, and share one holding_server. Something dials into a guest, for nodelay_kept's two listeners: forwards_into asks QEMU's monitor for a forward from a loopback port of QEMU's choosing to each guest port (hostfwd_add, the host ports read back from info usernet), and libc_sockets dials both when the case says its listeners wait (run_test_hooked, on the guest's own line and no clock). Measured before it was built on: QEMU 11.1.1 with no guest takes hostfwd_add net0 tcp:127.0.0.1:0-:<port> and names the port it bound. All of it is in tests/toyos.rs; tests/common is untouched.

Declared functions that were wrong, fixed here

function was is by
inet_addr four decimal octets only POSIX's four forms, each number decimal, octal or hexadecimal as ISO C writes one; ends at the string's end or at white space POSIX's text; host differential
inet_pton any count of digits per number; 0 for an unknown family one to three decimal digits per number, none zero-led; -1 and EAFNOSUPPORT for another family POSIX's text and glibc's rule; host differential
inet_ntop null for every buffer under 16 bytes, with no errno null and ENOSPC only when the text and its NUL do not fit, the buffer left as it was; null and EAFNOSUPPORT for another family host differential at every size
accept, recvfrom, getpeername, getsockname sixteen bytes into any buffer at most *addrlen bytes, 16 written back host differential at every length

Tests, and why each is on its tier

Host, toyos-libc-copies. internet_addresses.rs, nine tests: the readers and the writer against the host's inet_pton, inet_addr and inet_ntop; inet_ntop into a buffer of every size from 0 to 17 for a text of each length from 7 to 15, comparing the answer, the host's errno and both buffers whole; four families neither libc nor the host has, through both calls; SockaddrIn's bytes, size and alignment; and SockaddrIn::answer against the host's getsockname at every length from 0 to 32. socket_options.rs: #783's table gains the row of TCP_NODELAY asked of a datagram socket, set and get at every length, null and not, and a null length.

Guest, one machine test libc_sockets on one boot of tests/netcase, three C programs against two servers the harness starts. Why QEMU: what is asserted is what libc's calls ask of netstack and what comes back, and netstack is one binary that owns its NIC with no host build; the T14's shared boots run no netstack with a peer the harness controls. The four datagram-socket lines round 1 had there are gone to the host tier. The listener's lines are in nodelay_kept.c because each is netstack's answer to libc's request: the refusal of a set with a listener's id, and the accepted socket's option, 1 from a listener that held it at bind and 0 from one that did not, which needs a connection netstack accepted from a peer. socket.rs has no host build, so no host test reaches either, and the boot is one the case already has. What the accepted socket's read sees is libc's entry, which getsockopt answers from: accept carrying nothing is red (h), and accept storing the value and sending netstack nothing is green (i), under issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md beside connect's.

Oracle: POSIX's text and one host

Each case is the same source on the host, built with cc -Wall -Wextra -Werror and run on the development machine, Darwin 27.0.0, against a loopback peer (round 6, host-cases; nodelay_kept again at this head's source with a peer that dials both listeners in, round 7, host-case-worktree). addr_order and sendto_unbound: the guest's output is byte for byte the host's (diff exit 0, 19 and 17 lines). nodelay_kept: its first 25 lines are the host's, the two accepted sockets' reads among them, and the three on a listener's set after bind are not, which is the recorded difference and no accident:

< set on the listener: -1
< which is refused as not connected: 1
< read from the listener: 0
> set on the listener: 0  <-- WRONG
> which is refused as not connected: 0  <-- WRONG
> read from the listener: 1  <-- WRONG

Linux is unread: no program here was run on it, and no Linux container runtime was running on the development machine. The differential tests run against whichever C library the host has, so CI's host job is their first reading against glibc and against Linux's setsockopt, getsockopt and getsockname. What rests on my reading until then: the datagram row's two words and that Linux says them before it reads a length; that glibc's inet_pton refuses exactly a zero before another digit; that getsockname copies min(len, 16) and writes 16 back. The tests to read by name in that job's log are the review's list, with internet_addresses::a_buffer_too_short_for_the_text_is_refused_as_the_host_refuses_it, internet_addresses::another_family_is_refused_as_the_host_refuses_it and internet_addresses::an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it.

Negative controls

Each a checked patch (git apply --check), applied, run, restored with git apply -R, tree clean after. m2, (f), (h) and (i) are at 2c8a4ad58, round 7, since socket.rs changed under them; the rest are at 7d82642f3, round 6, and nothing they mutate or run has changed since but socket.rs's accept. The patches are in comments below. Host is cargo test -p toyos-libc-copies; guest is cargo test --test toyos-build -- --jobs 1 --nocapture libc_sockets.

mutation exit what turned red
m1, sendto binds nothing guest 1 sendto_unbound, 11 lines: sendto from it: -1
m2, TCP_NODELAY dropped before connect (no hand-over, a set before netstack holds the socket keeps nothing) guest 1 nodelay_kept, 4 lines: read before connect: 0, read after connect: 0, read from that listener: 0, read from its connection: 0
m3, the address in host order host 101, guest 1 a_sockaddr_holds_its_port_and_address_in_network_order and the truncation test; addr_order, 8 lines
(a) inet_ntop's >= to >: the NUL one byte past a buffer sized exactly host 101 a_buffer_too_short_for_the_text_is_refused_as_the_host_refuses_it
(b) inet_ntop refuses every buffer under 16 host 101 the same test
(c) inet_pton answers 0 for another family host 101 another_family_is_refused_as_the_host_refuses_it
(d) the sockaddr written whole (the old fill_sockaddr) host 101 an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it
(e) a datagram socket takes a TCP option host 101 an_options_value_crosses_as_the_hosts_does: Set where Refused(92)
(f) a listener's set kept (round 1's guard, entry.connected) guest 1 nodelay_kept: the listener's three lines
(g) inet_pton reads a zero-led number host 101 a_number_with_a_zero_before_another_digit_is_no_dotted_quad and the differential's count
(h) accept carries nothing: the round's hunk reverted guest 1 nodelay_kept, 1 line: read from its connection: 0 on the socket accepted from the listener that held the option
(i) accept stores the listener's value and sends netstack nothing guest 0, green nothing: the read is of libc's entry

(a), (b) and (c) are the review's, moved with the code: the fit and the family refusal are inaddr.rs's now, so (b) and (c) mutate the refusal there, and the errno each maps to is one match in socket.rs a reader checks. On this host (e) is red by the test's own assertion of libc's two words and not by the host's answer, which is Darwin's; the host's side of that row is Linux's to give.

m4, (i) and m5 stay green, as the review decided of the first and the last: m4 (connect hands no kept option over) and (i), its like at accept, are issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md, whose exit names both calls, and m5 (bind_datagram hands no kept SO_BROADCAST over) is the track's broadcast line. m4 and m5 not run again: measured at 19e2d33ea, round 1.

Issues

Closed, each by its own exit: libc-sends-from-a-datagram-socket-it-never-bound (m1), libc-drops-a-tcp-nodelay-set-before-connect (m2), libc-reads-an-ipv4-address-in-host-order (m3), libc-writes-a-whole-sockaddr-into-a-shorter-buffer (d). git grep of each slug at the head finds nothing.

Filed: nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack, libc-getaddrinfo-ignores-its-service-and-its-hints, and libc-getsockname-and-getpeername-answer-a-null-buffer-with-success, round 2's third NOTE, not fixed here: its three rows of the host's answers are measured on Darwin 27.0.0 (round 7, host-null-sockname) and are its exit's oracle, libc its owner. a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers gains the sentence on connect's and accept's ENOTCONN when a peer resets between netstack's answer and the hand-over.

a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay, filed in round 6, is deleted, nothing citing it: with accept carrying the value set before bind, what remained of it is the set after bind, refused until the pipe ABI gives a listener its option, which a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess, on main since #777, already says and exits on. That file is made true of libc as it stands (the refusal's errno, the value kept before bind and handed over at accept, the test that holds both and the three lines its exit turns), loses TcpGetOption, a message #783 removed, and is named in the track's listener-defects line with what its exit waits on.

The stalls (fe241fc3e). issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md gains round 5's two silent guests of one run, with their six lines, the load and the waits, and the owner the tracker asks for. issues/virt-mask-windows-went-quiet-on-a-loaded-host.md is folded into it and deleted, nothing citing it (round 7 corrects one word of the fold, the stall at 94fd25e71 being ten minutes after the two at 68cf1f870 by the runs' logs and not before them, and names issues/the-forks-llvm-deletes-a-loops-exit-on-a-no-wrap-flag-scalar-evolution-gives-the-wrong-value.md where it spoke of the compiler): virt_mask_windows's second wait spans the counters read, and the one stall of it with a console ends on the read's spawn record. Its two other stalls, at b9430ed61 and on wt/toyos-move-abi at 94fd25e71, kept no console of the guest and are recorded as that: the step each stopped in is not read from it. Nothing in its recorded evidence shows a different defect.

Gates

Each is the command's own exit code, at 2c8a4ad58 with a clean tree; logs are named by round and step.

gate exit count
cargo run -- --build-only (round 7, build) 0
cargo run -- --ci host (round 7, ci-host) 0 "Host: 78 step(s), all green"; toyos-libc-copies 46 passed inside it
cargo run -- --clippy (round 7, clippy) 0
cargo test --test toyos-build -- --jobs 1 --nocapture libc_sockets (round 7, guest-libc-sockets) 0 1 passed, 1 total
the whole guest suite, cargo test --test toyos-build -- --jobs 4 --nocapture (round 7, guest-suite) 0 37 passed, 37 total

The suite ran once, on the 14-core host with other worktrees building: one-minute load 33.19 at its start and 35.33 at its end; liveness ceilings paid at 1.00x; workers 501 s building against 284 s testing. libc_sockets, netstack_socket_churn, virt_mask_windows, virt_el1_smp and virt_smp passed in 12, 9, 5, 11 and 10 s; no stall, so the counters record gains no occurrence, and one green run is no evidence about the read under load. CI's host (Linux) and guest / suite under KVM are read by the orchestrator at the head that lands.

What I am unsure of

  • Linux, as above: three readings no machine has confirmed.
  • Clippy does not read userland/libc/src/socket.rs: libc builds only for the guest and no shape lints it. inaddr.rs and sockopt.rs are linted through the host crate.
  • The hand-over at connect and at accept is unseen (m4, (i)): the tests read libc's entry, and nothing reads netstack's.
  • The forward's guest ports, 7001 and 7002, are constants of the test: nothing else on the case's boot listens, and a second listener there would have to pick others.
  • A peer that resets before accept's hand-over fails the accept ENOTCONN: read from the code and recorded, reached by no test.
  • netstack's answer to a listener's id is what the listener's lines rest on; the move off smoltcp carries them or the ABI stage turns them to the host's.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A

Japabu and others added 6 commits October 8, 2026 23:56
…or 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
setsockopt and getsockopt take #783's rule: the kept option by sockopt::kept,
the value through switch and answer, a null pointer EFAULT. This stage's two
changes sit on it: TCP_NODELAY on a datagram socket is EINVAL ahead of
either, and a stream's is kept unless the socket is a connection.

The never-bound issue stays deleted. Its modified side added the guest
measurement of the defect, sendto answering -1 with EIO, which the fix this
branch carries ends; the track's sentence that cited it is rewritten on
#783's version of the line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
#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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Evidence for the body at 3e91a7045: the mutation patches, and the host programs and logs the oracle rests on. None is in the tree.

Mutations, each git apply --check --recount, git apply --recount, cargo test --test toyos-build -- --jobs 1 --nocapture libc_sockets (m3 also cargo test -p toyos-libc-copies internet_addresses), git apply -R --recount, git status --porcelain --ignore-submodules=none empty after. m2 is the patch against the merged head; m1 and m3 to m5 apply to it unchanged.

m1-sendto-binds-nothing.patch
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -424,14 +424,6 @@ 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) {
m2-nodelay-dropped-before-connect.patch
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -208,14 +208,6 @@ 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;
@@ -586,16 +578,13 @@ pub unsafe extern "C" fn setsockopt(
         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 {
-                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;
-                }
+        Kept::NoDelay if entry.netstack_id != 0 => {
+            if let Err(e) = toyos::net::tcp_set_option(TcpSocketId(entry.netstack_id), OPT_NODELAY, on as u32) {
+                set_errno(net_err_to_errno(e));
+                return -1;
             }
             entry.nodelay = on;
         }
+        Kept::NoDelay => {}
         Kept::Broadcast => {
             // Only a datagram is ever sent to a broadcast address, and netstack
             // holds a datagram socket from its `bind`.
m3-address-in-host-order.patch
--- a/userland/libc/src/inaddr.rs
+++ b/userland/libc/src/inaddr.rs
@@ -20,12 +20,13 @@ pub(crate) struct SockaddrIn {
 
 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] }
+        Self { sin_family: AF_INET as u16, sin_port: port.to_be_bytes(), sin_addr: u32::from_be_bytes(ip).to_ne_bytes(), 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)))
+        (self.sin_family == AF_INET as u16)
+            .then(|| (u32::from_ne_bytes(self.sin_addr).to_be_bytes(), u16::from_be_bytes(self.sin_port)))
     }
 }
 
diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -849,5 +849,5 @@ pub unsafe extern "C" fn ntohl(netlong: u32) -> u32 {
 #[no_mangle]
 pub unsafe extern "C" fn inet_addr(cp: *const u8) -> u32 {
     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)
+    inaddr::numbers_and_dots(text).map_or(u32::MAX, u32::from_be_bytes)
 }
m4-connect-hands-nothing-over.patch
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -208,14 +208,6 @@ 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;
m5-bind-hands-no-broadcast-over.patch
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -135,13 +135,6 @@ unsafe fn fill_sockaddr(addr: *mut Sockaddr, addrlen: *mut SocklenT, ip: [u8; 4]
 /// `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;

The host peer (peer.c), which the three cases ran against on the host:

peer.c
/* The host run's peer, not in the tree: a TCP listener that holds what it
   accepts and a UDP socket that answers each datagram with itself, both on
   the loopback address. Prints "<tcp port> <udp port>" and serves until
   killed. */
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>

static unsigned bind_loopback(int fd) {
    struct sockaddr_in me;
    socklen_t len = sizeof me;
    memset(&me, 0, sizeof me);
    me.sin_family = AF_INET;
    me.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    if (bind(fd, (struct sockaddr *)&me, sizeof me) != 0) return 0;
    getsockname(fd, (struct sockaddr *)&me, &len);
    return ntohs(me.sin_port);
}

int main(void) {
    int l = socket(AF_INET, SOCK_STREAM, 0), u = socket(AF_INET, SOCK_DGRAM, 0);
    unsigned tcp = bind_loopback(l), udp = bind_loopback(u);
    listen(l, 16);
    printf("%u %u\n", tcp, udp);
    fflush(stdout);
    for (;;) {
        fd_set r;
        FD_ZERO(&r);
        FD_SET(l, &r);
        FD_SET(u, &r);
        if (select((l > u ? l : u) + 1, &r, 0, 0, 0) < 0) return 1;
        if (FD_ISSET(l, &r)) accept(l, 0, 0);
        if (FD_ISSET(u, &r)) {
            char buf[64];
            struct sockaddr_in from;
            socklen_t len = sizeof from;
            ssize_t n = recvfrom(u, buf, sizeof buf, 0, (struct sockaddr *)&from, &len);
            if (n >= 0) sendto(u, buf, (size_t)n, 0, (struct sockaddr *)&from, len);
        }
    }
}

The three cases on the host, round 1, step host-cases (the sources are tests/netcase/*.c):

log
$ cc -Wall -Wextra -Werror -o sendto_unbound tests/netcase/sendto_unbound.c
EXIT=0
$ cc -Wall -Wextra -Werror -o nodelay_kept tests/netcase/nodelay_kept.c
EXIT=0
$ cc -Wall -Wextra -Werror -o addr_order tests/netcase/addr_order.c
EXIT=0
$ ./sendto_unbound 127.0.0.1 <the peer's port>
the peer's address is one: 1
SO_BROADCAST set on a socket never bound: 0
sendto from it: 4
the send bound it to a port: 1
the answer came back to that port: 4
and is what was sent: 1
from the peer's address: 1
and the peer's port: 1
the option is read back: 0
and is still set: 1
sendto the limited broadcast address: 1
connect on a second socket never bound: 0
send from it: 4
its answer: 4
is what it sent: 1
the two sockets hold two ports: 1
sendto_unbound: ok
EXIT=0
$ ./nodelay_kept 127.0.0.1 <the peer's port>
the peer's address is one: 1
a fresh stream socket's TCP_NODELAY: 0
set before connect: 0
read before connect: 1
connect: 0
read after connect: 1
cleared on the connection: 0
read after the clear: 0
set on the connection: 0
read after the set: 1
set on a second socket: 0
and cleared before connect: 0
connect: 0
read after connect: 0
set on a datagram socket: -1
which is invalid at that socket: 1
nor read from one: -1
for the same reason: 1
nodelay_kept: ok
EXIT=0
$ ./addr_order 127.0.0.1 <the peer's port>
inet_pton reads the address: 1
into its octets in memory order: 1
htonl builds the same four bytes: 1
and so does inet_addr: 1
ntohl gives the number back: 1
connect to inet_pton's address: 1
connect to htonl's: 1
connect to inet_addr's: 1
getpeername: 0
answers the address: 1
and the port: 1
which inet_ntop writes as it was given: 1
getaddrinfo of the address: 0
answers the same four bytes: 1
connect to getaddrinfo's: 1
INADDR_LOOPBACK: 127.0.0.1
INADDR_BROADCAST: 255.255.255.255
INADDR_ANY: 0.0.0.0
addr_order: ok
EXIT=0
Darwin 27.0.0
sendto_unbound: host output against guest output, diff EXIT=0
nodelay_kept: host output against guest output, diff EXIT=0
addr_order: host output against guest output, diff EXIT=0

The edges on the host, round 3, step host-edges:

edges.c
/* What the host's C library answers at the edges of the calls libc declares. */
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>

static void pton(const char *s) {
    unsigned char o[4] = {0xaa, 0xaa, 0xaa, 0xaa};
    errno = 0;
    int r = inet_pton(AF_INET, s, o);
    printf("inet_pton(\"%s\") = %d errno=%d bytes=%02x %02x %02x %02x\n", s, r, errno, o[0], o[1], o[2], o[3]);
}
static void addr(const char *s) {
    in_addr_t a = inet_addr(s);
    unsigned char o[4];
    memcpy(o, &a, 4);
    printf("inet_addr(\"%s\") bytes=%02x %02x %02x %02x\n", s, o[0], o[1], o[2], o[3]);
}
static void ntop(socklen_t size) {
    unsigned char o[4] = {192, 0, 2, 1};
    char buf[32];
    memset(buf, '#', sizeof buf);
    buf[31] = 0;
    errno = 0;
    const char *r = inet_ntop(AF_INET, o, buf, size);
    printf("inet_ntop(192.0.2.1, size %u) = %s errno=%d (ENOSPC=%d)\n", (unsigned)size, r ? r : "NULL", errno, ENOSPC);
}

int main(void) {
    const char *p[] = {"10.0.2.2", "192.0.2.4", "255.255.255.255", "0.0.0.0", "0192.0.2.4", "192.0.2.04", "00192.0.2.4", "000192.0.2.4",
                       "192.0.2.00", "192.0.2.0", "256.1.1.1", "192.0.2", "192.0.2.4.5", "192.0.2.4.", ".192.0.2.4", "1..2.3", "", " 192.0.2.4",
                       "192.0.2.4 ", "192.0.2.4x", "0x192.0.2.4", "192.0.2.-4", "+192.0.2.4", "192.0.2.4\n", "127.1"};
    for (unsigned i = 0; i < sizeof p / sizeof *p; i++) pton(p[i]);
    const char *a[] = {"10.0.2.2", "192.0.2.4", "255.255.255.255", "0.0.0.0", "127.1", "127.0.1", "1.2.65535", "1.2.65536", "1.16777215",
                       "1.16777216", "4294967295", "4294967296", "16909060", "0x7f.1", "0x7f000001", "0X7F.0.0.1", "010.0.0.1", "08.0.0.1",
                       "09", "0", "00", "0x", "0x.1", "192.0.2.4 ", "192.0.2.4 x", "192.0.2.4\n", " 192.0.2.4", "192.0.2.4x", "256.0.0.1", "1.256.0.1",
                       "1.2.256.1", "192.0.2.256", "192.0.2.4.5", "192.0.2.4.", "1.", ".1", "1..2", "", "-1", "+1", "192.0.2.0x10", "0377.1",
                       "0400.1", "0xff.1", "0x100.1", "1.0x10000", "a", "0xg"};
    for (unsigned i = 0; i < sizeof a / sizeof *a; i++) addr(a[i]);
    for (socklen_t n = 8; n <= 11; n++) ntop(n);
    ntop(16);
    char buf[64];
    unsigned char big[4] = {255, 255, 255, 255};
    printf("inet_ntop(255.255.255.255, 16) = %s\n", inet_ntop(AF_INET, big, buf, 16));
    errno = 0;
    printf("inet_ntop(255.255.255.255, 15) = %p errno=%d\n", (void *)inet_ntop(AF_INET, big, buf, 15), errno);
    errno = 0;
    printf("inet_pton(af 99) = %d errno=%d (EAFNOSUPPORT=%d)\n", inet_pton(99, "192.0.2.4", buf), errno, EAFNOSUPPORT);
    errno = 0;
    printf("inet_ntop(af 99) = %p errno=%d\n", (void *)inet_ntop(99, big, buf, 64), errno);
    printf("INADDR_LOOPBACK=%08x INADDR_BROADCAST=%08x INADDR_ANY=%08x INADDR_NONE=%08x INET_ADDRSTRLEN=%d\n",
           (unsigned)INADDR_LOOPBACK, (unsigned)INADDR_BROADCAST, (unsigned)INADDR_ANY, (unsigned)INADDR_NONE, INET_ADDRSTRLEN);
    printf("sizeof in_addr_t=%zu in_port_t=%zu\n", sizeof(in_addr_t), sizeof(in_port_t));

    /* Does a connection a listener accepts carry the listener's TCP_NODELAY? */
    int l = socket(AF_INET, SOCK_STREAM, 0), on = 1;
    struct sockaddr_in me;
    memset(&me, 0, sizeof me);
    me.sin_family = AF_INET;
    me.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    socklen_t len = sizeof me;
    printf("listener set TCP_NODELAY before bind: %d\n", setsockopt(l, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on));
    bind(l, (struct sockaddr *)&me, sizeof me);
    listen(l, 1);
    getsockname(l, (struct sockaddr *)&me, &len);
    int c = socket(AF_INET, SOCK_STREAM, 0);
    printf("connect: %d\n", connect(c, (struct sockaddr *)&me, sizeof me));
    int acc = accept(l, 0, 0), v = -1;
    len = sizeof v;
    getsockopt(acc, IPPROTO_TCP, TCP_NODELAY, &v, &len);
    printf("accepted socket's TCP_NODELAY: %s\n", v ? "set" : "clear");
    int l2 = socket(AF_INET, SOCK_STREAM, 0);
    me.sin_port = 0;
    bind(l2, (struct sockaddr *)&me, sizeof me);
    listen(l2, 1);
    printf("listener set TCP_NODELAY after listen: %d errno=%d\n", setsockopt(l2, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), errno);
    /* recvfrom and getsockname on a datagram socket never bound */
    int d = socket(AF_INET, SOCK_DGRAM, 0);
    len = sizeof me;
    memset(&me, 0xaa, sizeof me);
    printf("getsockname of an unbound datagram socket: %d port=%u\n", getsockname(d, (struct sockaddr *)&me, &len), ntohs(me.sin_port));
    return 0;
}
log
$ cc -Wall -o edges edges.c
EXIT=0
$ ./edges
inet_pton("10.0.2.2") = 1 errno=0 bytes=0a 00 02 02
inet_pton("192.0.2.4") = 1 errno=0 bytes=c0 00 02 04
inet_pton("255.255.255.255") = 1 errno=0 bytes=ff ff ff ff
inet_pton("0.0.0.0") = 1 errno=0 bytes=00 00 00 00
inet_pton("0192.0.2.4") = 1 errno=0 bytes=c0 00 02 04
inet_pton("192.0.2.04") = 1 errno=0 bytes=c0 00 02 04
inet_pton("00192.0.2.4") = 1 errno=0 bytes=c0 00 02 04
inet_pton("000192.0.2.4") = 1 errno=0 bytes=c0 00 02 04
inet_pton("192.0.2.00") = 1 errno=0 bytes=c0 00 02 00
inet_pton("192.0.2.0") = 1 errno=0 bytes=c0 00 02 00
inet_pton("256.1.1.1") = 0 errno=0 bytes=aa aa aa aa
inet_pton("192.0.2") = 0 errno=0 bytes=aa aa aa aa
inet_pton("192.0.2.4.5") = 0 errno=0 bytes=aa aa aa aa
inet_pton("192.0.2.4.") = 0 errno=0 bytes=aa aa aa aa
inet_pton(".192.0.2.4") = 0 errno=0 bytes=aa aa aa aa
inet_pton("1..2.3") = 0 errno=0 bytes=aa aa aa aa
inet_pton("") = 0 errno=0 bytes=aa aa aa aa
inet_pton(" 192.0.2.4") = 0 errno=0 bytes=aa aa aa aa
inet_pton("192.0.2.4 ") = 0 errno=0 bytes=aa aa aa aa
inet_pton("192.0.2.4x") = 0 errno=0 bytes=aa aa aa aa
inet_pton("0x192.0.2.4") = 0 errno=0 bytes=aa aa aa aa
inet_pton("192.0.2.-4") = 0 errno=0 bytes=aa aa aa aa
inet_pton("+192.0.2.4") = 0 errno=0 bytes=aa aa aa aa
inet_pton("192.0.2.4
") = 0 errno=0 bytes=aa aa aa aa
inet_pton("127.1") = 0 errno=0 bytes=aa aa aa aa
inet_addr("10.0.2.2") bytes=0a 00 02 02
inet_addr("192.0.2.4") bytes=c0 00 02 04
inet_addr("255.255.255.255") bytes=ff ff ff ff
inet_addr("0.0.0.0") bytes=00 00 00 00
inet_addr("127.1") bytes=7f 00 00 01
inet_addr("127.0.1") bytes=7f 00 00 01
inet_addr("1.2.65535") bytes=01 02 ff ff
inet_addr("1.2.65536") bytes=ff ff ff ff
inet_addr("1.16777215") bytes=01 ff ff ff
inet_addr("1.16777216") bytes=ff ff ff ff
inet_addr("4294967295") bytes=ff ff ff ff
inet_addr("4294967296") bytes=00 00 00 00
inet_addr("16909060") bytes=01 02 03 04
inet_addr("0x7f.1") bytes=7f 00 00 01
inet_addr("0x7f000001") bytes=7f 00 00 01
inet_addr("0X7F.0.0.1") bytes=7f 00 00 01
inet_addr("010.0.0.1") bytes=08 00 00 01
inet_addr("08.0.0.1") bytes=ff ff ff ff
inet_addr("09") bytes=ff ff ff ff
inet_addr("0") bytes=00 00 00 00
inet_addr("00") bytes=00 00 00 00
inet_addr("0x") bytes=ff ff ff ff
inet_addr("0x.1") bytes=00 00 00 01
inet_addr("192.0.2.4 ") bytes=c0 00 02 04
inet_addr("192.0.2.4 x") bytes=c0 00 02 04
inet_addr("192.0.2.4
") bytes=c0 00 02 04
inet_addr(" 192.0.2.4") bytes=ff ff ff ff
inet_addr("192.0.2.4x") bytes=ff ff ff ff
inet_addr("256.0.0.1") bytes=ff ff ff ff
inet_addr("1.256.0.1") bytes=ff ff ff ff
inet_addr("1.2.256.1") bytes=ff ff ff ff
inet_addr("192.0.2.256") bytes=ff ff ff ff
inet_addr("192.0.2.4.5") bytes=ff ff ff ff
inet_addr("192.0.2.4.") bytes=ff ff ff ff
inet_addr("1.") bytes=ff ff ff ff
inet_addr(".1") bytes=ff ff ff ff
inet_addr("1..2") bytes=ff ff ff ff
inet_addr("") bytes=ff ff ff ff
inet_addr("-1") bytes=ff ff ff ff
inet_addr("+1") bytes=ff ff ff ff
inet_addr("192.0.2.0x10") bytes=c0 00 02 10
inet_addr("0377.1") bytes=ff 00 00 01
inet_addr("0400.1") bytes=ff ff ff ff
inet_addr("0xff.1") bytes=ff 00 00 01
inet_addr("0x100.1") bytes=ff ff ff ff
inet_addr("1.0x10000") bytes=01 01 00 00
inet_addr("a") bytes=ff ff ff ff
inet_addr("0xg") bytes=ff ff ff ff
inet_ntop(192.0.2.1, size 8) = NULL errno=28 (ENOSPC=28)
inet_ntop(192.0.2.1, size 9) = NULL errno=28 (ENOSPC=28)
inet_ntop(192.0.2.1, size 10) = 192.0.2.1 errno=0 (ENOSPC=28)
inet_ntop(192.0.2.1, size 11) = 192.0.2.1 errno=0 (ENOSPC=28)
inet_ntop(192.0.2.1, size 16) = 192.0.2.1 errno=0 (ENOSPC=28)
inet_ntop(255.255.255.255, 16) = 255.255.255.255
inet_ntop(255.255.255.255, 15) = 0x0 errno=28
inet_pton(af 99) = -1 errno=47 (EAFNOSUPPORT=47)
inet_ntop(af 99) = 0x0 errno=47
INADDR_LOOPBACK=7f000001 INADDR_BROADCAST=ffffffff INADDR_ANY=00000000 INADDR_NONE=ffffffff INET_ADDRSTRLEN=16
sizeof in_addr_t=4 in_port_t=2
listener set TCP_NODELAY before bind: 0
connect: 0
accepted socket's TCP_NODELAY: set
listener set TCP_NODELAY after listen: 0 errno=47
getsockname of an unbound datagram socket: 0 port=0
EXIT=0
Darwin 27.0.0

TCP_NODELAY on a datagram socket, round 0, step host-dgram-nodelay:

setsockopt(datagram, IPPROTO_TCP, TCP_NODELAY) = -1 errno=22 Invalid argument (ENOPROTOOPT=42 EINVAL=22 EOPNOTSUPP=102)
getsockopt = -1 errno=22 value=-7

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 of #787 at 68cf1f870, read as git diff 8bceea054 68cf1f870 (the stage alone, #783 at 8bceea054 taken as given). Read only: no build, no test run. Logs read are the stage's own, named by round and step.

Net lines, stage alone: 23 files, +809 −257. Production (userland/libc): +216 −163, net +53. Tests: +487 −12. issues/: +106 −82. The production growth is three behaviours added and inaddr.rs replacing hand conversion; accepted, less what the BLOCKERs below move or cut.

BLOCKER

  • Evidence — the whole guest suite is exit 1 at the reviewed head (round 5, guest-suite: 35 passed, 2 failed of 37), and the head is not the one that lands: origin/main (971eeaaf2) is 37 commits past 6776714db, the main this head carries, A guest test waits for a kernel record and for a program's line each, where it read one off a capture the other ended #786 among them, which rewrites waits in tests/toyos.rs, and The pipe ABI carries a datagram socket's broadcast permission from std and libc to netstack, and the option read is gone #783 is not on main. src/libc.rs:7 says libc is linked into std and so into every userland binary: this change rebuilds every guest's programs, so every guest test is one the change reaches, and "neither boot runs a C program" does not take the two reds out of the gate. I confirm from the log that neither stall is in code this stage touches (see the notes), which makes them main's defects; it does not make the gate green. Sent back for the suite at the final head, exit 0, with its log; the list at the end says what else is re-measured there.

  • userland/libc/src/socket.rs:589-598 with :305-318 — the stage turns a loud refusal into a silent wrong answer for a listener. At 8bceea054 setsockopt(TCP_NODELAY) on a bound listener went to netstack and came back -1 ENOTCONN; now it answers 0, getsockopt reads 1, and accept builds the connection's entry with nodelay: false and sends netstack nothing. The pull request measured that a host's accepted socket reads the option set, then filed the difference (issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md). A program told yes and given no is the defect this stage exists to remove, moved from connect to accept by this stage's own guard change; it is not found-on-the-way work. Either accept hands the listener's kept value to netstack and into the new entry, as connect does at :206-213, or a listener's set stays refused by name. Whichever: the listener's own answers (socket, bind, listen, set, read back) need no peer dialling in and have no line in tests/netcase/nodelay_kept.c; the claim "a listener's set is now kept and answers 0, as the host does" is made by the body and held by no test.

  • userland/libc/src/socket.rs:557-559, :580-583, :633-636; tests/netcase/nodelay_kept.c:55-61 — a rule of the option table sits outside sockopt.rs, is tested on the wrong tier, and its errno is a guess. (1) Tier: tcp_option_of_a_datagram_socket reads a level, a name and a socket kind and sends netstack nothing; The pipe ABI carries a datagram socket's broadcast permission from std and libc to netstack, and the option read is gone #783 made sockopt.rs for exactly such a rule and holds it against the host's own two calls. The body's reason for the guest tier ("what libc's calls ask of netstack and what comes back from a peer") is false of these four lines: a host test reaches them. (2) The answer: sockopt.rs:1-5 says libc refuses "as Linux does, whose option numbers these are", and socket_options.rs:9-10 that on Linux every row agrees. The stage answers EINVAL because Darwin does, and says of Linux "ENOPROTOOPT by my reading, not run". My reading agrees with that reading (udp_setsockopt falls to ip_setsockopt, which answers ENOPROTOOPT for a level that is not SOL_IP; the getter likewise), and it is equally not a measurement. One cheap measurement tells: move the rule into sockopt.rs, add the row (the TCP option on the datagram socket, set and get) to an_options_value_crosses_as_the_hosts_does, put the host that parts from libc's answer in darwin(), and read the Linux host job. The four guest lines go with it.

  • userland/libc/src/socket.rs:805-814, :784-787 — the boundary and both refusals of inet_ntop and inet_pton are behaviour the stage changed and claims in its table, and no test at any tier holds them; addr_order.c passes only INET_ADDRSTRLEN and AF_INET. Mutations that I expect to stay green, for the implementer to run: (a) socket.rs:811 n as u32 >= size to n as u32 > size, which writes the NUL one byte past a buffer the caller sized exactly; (b) socket.rs:805 restored to the old af != AF_INET || size < 16 with no errno; (c) socket.rs:784-786 to return 0 with no errno. The size is a value the caller chooses and the write is into the caller's buffer. The cheapest tier is the host: the fit belongs in inaddr.rs (dotted_text handed the caller's length and answering that the text does not fit), and an_address_is_written_as_the_host_writes_it must turn red on (a) and (b) by comparing with the host's inet_ntop at every size from 0 to 16, and a family row must turn red on (c).

NOTE

  • m4, decided: userland/libc/src/socket.rs:206-213 may land with issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md. Reasons. The keep (:598) and the hand-over are one change: leaving the hand-over out makes getsockopt answer 1 for a connection netstack holds with Nagle on, which is worse than the defect, and leaving both out leaves the defect. The blindness is not this change's: the pipe ABI has TcpSetOption and no read (toyos/src/net.rs), so the set on a live connection (:593, The pipe ABI carries a datagram socket's broadcast permission from std and libc to netstack, and the option read is gone #783's) and std's set_nodelay are equally unseen on main today, and the issue says so in general, with evidence (m4, round 1, exit 0), an owner and an exit a test can fail. No tier can read it now without an ABI addition on the stack that is being replaced, which is outside the fence. The deleted issue's exit asked for the option to reach netstack and for a guest case to read it back after connect; the second is m2's red and the first is eight lines a reader checks. So: not a blocker, and not half a change to cut. The same reasoning is why the accept hand-over above cannot be declined on "no test can turn it red".
  • m5, decided: userland/libc/src/socket.rs:133-139 are The pipe ABI carries a datagram socket's broadcast permission from std and libc to netstack, and the option read is gone #783's lines moved into the one function both paths bind through; cutting them from the sendto path would make a second bind path. Nothing on smoltcp can see them (userland/netstack/src/main.rs:1126-1141 answers the request done for every socket), and the track's broadcast line now names the case and the mutation that turns it red after the move. Lands as is.
  • issues/libc-writes-a-whole-sockaddr-into-a-shorter-buffer.md — leaving it filed is acceptable and I do not hold the branch for it: it is on main unchanged in reach, the overrun is of the calling process's own buffer by its own libc (no boundary is crossed), the brief was three defects, and the file has evidence, an owner and an exit. It is the first thing the next libc stage owes, because inaddr.rs is now the place the fix and its host test go; the owner may prefer to widen this stage instead, which is his call and not the implementer's.
  • issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md — true of socket.rs:695-729, owned, with an exit. Not this stage's; addr_order.c:83-87 supplies the port itself, so no line of the case depends on the defect.
  • The three deletions: each exit is met. libc-sends-from-a-datagram-socket-it-never-bound: m1 red (round 2, exit 1, sendto from it: -1), INADDR_BROADCAST at netinet/in.h:17. libc-reads-an-ipv4-address-in-host-order: m3 red on both tiers (round 2, host exit 101, guest exit 1); the search for a conversion left by hand finds only the four byte-swap functions, inet_addr's from_ne_bytes and sockopt.rs's int. libc-drops-a-tcp-nodelay-set-before-connect: m2 red (round 4, exit 1). No citation of any of the three slugs is left at this head; search again after the merge with main.
  • The two stalls, from round 5's guest-suite log. Both guests are tests/virtsmpcase on AArch64 under TCG, eight vCPUs; its system.toml starts logkeeper and test-runner and runs unmap_touch, test_rs_counters_read, test_rs_trace_read, shutdown: no C program, no socket, no netstack. The waits are judge_virt_job's and the power test's await_marker, neither in this diff. Both went silent at the same step, which the body does not say: virt_el1_smp's last lines are ===TEST_END unmap_touch exit=0=== at 13.577, ===TEST_START test_rs_counters_read=== at 13.578 and the kernel's spawn: /system/bin/test_rs_counters_read pid=13 at 13.590 on cpu4; virt_mask_windows's are the same three at 14.557, 14.558 and 14.585 on cpu4. They reached the harness within one second of each other and nothing followed for the 15 s quiet bound. virt_mask_windows is named for "the boot's last word" only because that is the one thing its test waits on; its silence is the counters read's.
    • issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md owes: two more silent boots in one run at 68cf1f870 (--jobs 4 --nocapture, one-minute load 75.99 at the start and 63.37 at the end, 14 cores), the six lines above, the wait that ended each, the same profile as the second recorded silence (straight after the spawn record, no thread's exit on the console); known: virt_smp and virt_failed_ap_leaves_no_hole, the same case launched within three seconds of them, each did the step and passed, and each red passed alone at the same head, at loads 32.47 and 37.04, virt_el1_smp with the read ending 0.13 s of guest clock after unmap_touch; not known: whether the guests ran, spun or were not scheduled, with no register capture, and two guests stopping in the same second is the first thing here that bears on "the host stopping the guest". The serial directory the harness named for this red run is no longer among the worktree's kept ones, so the round's log is the only copy.
    • issues/virt-mask-windows-went-quiet-on-a-loaded-host.md owes: this occurrence, and that the guest's last line was the counters read's spawn, so the file is either folded into the counters issue or says why it is a different defect.
    • Owed in this pull request, as a commit on the merged head. Root CLAUDE.md has a red under load "recorded with the host's load", the tracker is where the holder of an issue looks, and main already does exactly this at f9006e888, whose format (line, load, wait, what differs, what is known, what is not) is the one to follow. A merge commit's message is not the tracker.
  • tests/toyos.rs:2923-2950 — LEASED, the holding server and the boot-then-wait are copied from netstack_socket_churn, the function above it. One helper for the netcase boot and one for the holder: the move off smoltcp changes the lease line once, not twice.
  • The reorder (tests/toyos.rs:2943), judged: a fix of the report, not a mask. The stall it answered was itself red, no outcome changes colour, and a reversed address now reds by addr_order's own lines. What it does not change: sendto_unbound.c:48 and :65 wait in recvfrom with nothing but the harness's ceiling behind them, so any defect that loses a datagram, on this stack or the next, still ends at the ceiling and not on a line of the case. Known, and true of every blocking read a guest job makes.
  • The owner's ruling on smoltcp, judged: met. The cases ask libc for POSIX answers through the pipe ABI and compare with a peer; no line asserts something only smoltcp does. Two lines pass on smoltcp for a reason that is smoltcp's: sendto_unbound.c:59 (it sends to a broadcast address for every socket; the track's line records it) and the lease marker the harness waits on. The cases themselves survive the move unchanged; the marker is the move's to carry.
  • userland/libc/src/inaddr.rs:12-18 — SockaddrIn's alignment fell from 4 to 2 when its integers became byte arrays, and C's struct sockaddr_in is 4. getaddrinfo allocates one with Layout::new::<SockaddrIn>() and hands it to C. Not observable today (the allocator's floor is 16), and internet_addresses.rs:140 asserts the size only. #[repr(C, align(4))]: the type then says what the header says.
  • userland/libc/src/inaddr.rs:33-45 — inet_pton and getaddrinfo read 010.0.0.1 as 10.0.0.1 while inet_addr in the same module reads it as 8.0.0.1. POSIX's text allows the first; glibc refuses a leading zero so that no text has two values. Refusing is the stricter rule and makes the Linux differential total instead of excluded by a test of its own. A decision to state, not a defect.
  • userland/libc/src/socket.rs:206-213 — a peer that resets between tcp_connect's answer and the hand-over makes connect fail ENOTCONN, which no host's connect answers; the cause is issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md. One sentence there.
  • Body, prose: "Darwin ... reads the second as zero" is true of 0x.1 and not of 0x alone, which its own round-3 log shows as ff ff ff ff. "Neither boot runs a C program or opens a socket" is true and is not the reach of the change, which is every userland binary.

Re-measure on main with #783 landed

  1. The merge: issues/toyos-has-its-own-network-stack.md and tests/toyos.rs both moved on main; account for every hunk, and the diff against main is this stage alone.
  2. cargo run -- --ci host at the final head, exit 0, log.
  3. On CI's Linux host job, read each of these by name as ... ok in the job's log, not from the summary line, in toyos-libc-copies: internet_addresses::a_dotted_quad_reads_as_the_host_reads_it, internet_addresses::numbers_and_dots_read_as_the_host_reads_them, internet_addresses::an_address_is_written_as_the_host_writes_it, internet_addresses::a_sockaddr_holds_its_port_and_address_in_network_order, socket_options::an_options_value_crosses_as_the_hosts_does (with the datagram row, it is the measurement of Linux's errno), prototypes::every_prototype_is_a_definition_and_every_definition_is_declared, errno_codes::every_code_libc_answers_with_is_errno_h_s. I read the readers against glibc's inet_pton4 and inet_aton_end from memory and expect green; that is not a measurement either.
  4. The whole guest suite at the final head, exit 0, and guest / suite under KVM; libc_sockets alone with --nocapture, its output still byte for byte the host's.
  5. m1, m2 and m3 again on the final head, since socket.rs changes under them, and the mutations named in the BLOCKERs.
  6. cargo run -- --clippy and cargo run -- --build-only, exit 0.

SEND BACK

Japabu and others added 3 commits October 9, 2026 04:18
Both files the stage and the merge touch, tests/toyos.rs and
issues/toyos-has-its-own-network-stack.md, merged without a conflict; the
stage against the merged branch is the same 23 files and the same hunks
as against 8bceea0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…sk_windows's stalls are folded into it

Two guests of one run on this branch at 68cf1f8 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 94fd25e, 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…ule 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 6 evidence at 7d82642f3: the mutation patches, each applied with git apply --check and git apply, run, and restored with git apply -R, the tree clean after; and the host measurement the datagram row's darwin() rests on.

m1-sendto-binds-nothing.patch: host=- guest=1
diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 600916d99..2c1826acb 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -421,14 +421,6 @@ 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) {
m2-nodelay-dropped-before-connect.patch: host=- guest=1
diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 600916d99..5476ae08e 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -205,14 +205,6 @@ 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;
@@ -593,8 +585,8 @@ pub unsafe extern "C" fn setsockopt(
                     set_errno(net_err_to_errno(e));
                     return -1;
                 }
+                entry.nodelay = on;
             }
-            entry.nodelay = on;
         }
         Kept::Broadcast => {
             // Only a datagram is ever sent to a broadcast address, and netstack
m3-address-in-host-order.patch: host=101 guest=1
diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs
index da8edcbc5..b41b80121 100644
--- a/userland/libc/src/inaddr.rs
+++ b/userland/libc/src/inaddr.rs
@@ -23,12 +23,12 @@ pub(crate) struct SockaddrIn {
 
 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] }
+        Self { sin_family: AF_INET as u16, sin_port: port.to_be_bytes(), sin_addr: u32::from_be_bytes(ip).to_ne_bytes(), 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)))
+        (self.sin_family == AF_INET as u16).then(|| (u32::from_ne_bytes(self.sin_addr).to_be_bytes(), u16::from_be_bytes(self.sin_port)))
     }
 
     /// Answers a caller this address as POSIX has a call store one: as many
diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 600916d99..006f984c8 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -835,5 +835,5 @@ pub unsafe extern "C" fn ntohl(netlong: u32) -> u32 {
 #[no_mangle]
 pub unsafe extern "C" fn inet_addr(cp: *const u8) -> u32 {
     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)
+    inaddr::numbers_and_dots(text).map_or(u32::MAX, u32::from_be_bytes)
 }
a-ntop-writes-its-nul-one-past.patch: host=101 guest=-
diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs
index da8edcbc5..b46928180 100644
--- a/userland/libc/src/inaddr.rs
+++ b/userland/libc/src/inaddr.rs
@@ -73,7 +73,7 @@ pub(crate) unsafe fn ntop(af: i32, src: *const u8, dst: *mut u8, size: u32) -> R
     }
     let mut text = [0u8; 16];
     let len = dotted_text(src.cast::<[u8; 4]>().read(), &mut text);
-    if len as u32 >= size {
+    if len as u32 > size {
         return Err(Refusal::Room);
     }
     core::ptr::copy_nonoverlapping(text.as_ptr(), dst, len + 1);
b-ntop-refuses-any-buffer-under-16.patch: host=101 guest=-
diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs
index da8edcbc5..de80c3e1c 100644
--- a/userland/libc/src/inaddr.rs
+++ b/userland/libc/src/inaddr.rs
@@ -68,7 +68,7 @@ pub(crate) fn pton(af: i32, text: &[u8]) -> Result<Option<[u8; 4]>, Refusal> {
 /// `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 {
+    if af != AF_INET || size < 16 {
         return Err(Refusal::Family);
     }
     let mut text = [0u8; 16];
c-pton-answers-0-for-another-family.patch: host=101 guest=-
diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs
index da8edcbc5..63848f764 100644
--- a/userland/libc/src/inaddr.rs
+++ b/userland/libc/src/inaddr.rs
@@ -56,7 +56,7 @@ pub(crate) enum Refusal {
 /// `inet_pton`: `text`'s address, `None` for a text that is none.
 pub(crate) fn pton(af: i32, text: &[u8]) -> Result<Option<[u8; 4]>, Refusal> {
     if af != AF_INET {
-        return Err(Refusal::Family);
+        return Ok(None);
     }
     Ok(dotted_quad(text))
 }
d-sockaddr-written-whole.patch: host=101 guest=-
diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs
index da8edcbc5..9c8211c4f 100644
--- a/userland/libc/src/inaddr.rs
+++ b/userland/libc/src/inaddr.rs
@@ -39,7 +39,7 @@ impl SockaddrIn {
     /// bytes.
     pub(crate) unsafe fn answer(&self, addr: *mut u8, len: *mut u32) {
         let whole = size_of::<Self>();
-        core::ptr::copy_nonoverlapping((self as *const Self).cast::<u8>(), addr, whole.min(*len as usize));
+        core::ptr::copy_nonoverlapping((self as *const Self).cast::<u8>(), addr, whole);
         *len = whole as u32;
     }
 }
e-datagram-socket-takes-a-tcp-option.patch: host=101 guest=-
diff --git a/userland/libc/src/sockopt.rs b/userland/libc/src/sockopt.rs
index fc19f7da1..c387572f7 100644
--- a/userland/libc/src/sockopt.rs
+++ b/userland/libc/src/sockopt.rs
@@ -24,7 +24,6 @@ pub(crate) enum Kept {
 /// [`Refusal::NoSuchOption`], to a getter [`Refusal::NotSupported`].
 pub(crate) fn kept(level: i32, optname: i32, datagram: bool, setter: bool) -> Result<Option<Kept>, Refusal> {
     match (level, optname) {
-        (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),
f-listeners-set-kept.patch: host=- guest=1
diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 600916d99..d9e9242b9 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -588,7 +588,7 @@ pub unsafe extern "C" fn setsockopt(
             // 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 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;
g-pton-reads-a-zero-led-number.patch: host=101 guest=-
diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs
index da8edcbc5..6fea3d294 100644
--- a/userland/libc/src/inaddr.rs
+++ b/userland/libc/src/inaddr.rs
@@ -90,7 +90,7 @@ pub(crate) fn dotted_quad(text: &[u8]) -> Option<[u8; 4]> {
     for octet in &mut octets {
         let part = parts.next()?;
         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) {
+        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'));
A TCP option asked of a datagram socket, on the development machine (dg.c, not in the tree)
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
    int d = socket(AF_INET, SOCK_DGRAM, 0);
    unsigned lens[] = {8, 4, 2, 1, 0};
    for (int i = 0; i < 5; i++) for (int null = 0; null < 2; null++) {
        long long v = 1; errno = 0;
        int r = setsockopt(d, IPPROTO_TCP, TCP_NODELAY, null ? NULL : (void *)&v, lens[i]);
        printf("set len %u null %d: %d errno %d\n", lens[i], null, r, errno);
        unsigned char b[8]; memset(b, 0xaa, 8); socklen_t l = lens[i]; errno = 0;
        r = getsockopt(d, IPPROTO_TCP, TCP_NODELAY, null ? NULL : (void *)b, &l);
        printf("get len %u null %d: %d errno %d len back %u b0 %02x\n", lens[i], null, r, errno, l, b[0]);
    }
    unsigned char b[8]; errno = 0;
    int r = getsockopt(d, IPPROTO_TCP, TCP_NODELAY, b, NULL);
    printf("get null length: %d errno %d\n", r, errno);
    return 0;
}
$ cc -Wall -o dg dg.c && ./dg  (a TCP option asked of a datagram socket, on this host)
set len 8 null 0: -1 errno 22
get len 8 null 0: -1 errno 22 len back 8 b0 aa
set len 8 null 1: -1 errno 14
get len 8 null 1: -1 errno 22 len back 8 b0 aa
set len 4 null 0: -1 errno 22
get len 4 null 0: -1 errno 22 len back 4 b0 aa
set len 4 null 1: -1 errno 14
get len 4 null 1: -1 errno 22 len back 4 b0 aa
set len 2 null 0: -1 errno 22
get len 2 null 0: -1 errno 22 len back 2 b0 aa
set len 2 null 1: -1 errno 14
get len 2 null 1: -1 errno 22 len back 2 b0 aa
set len 1 null 0: -1 errno 22
get len 1 null 0: -1 errno 22 len back 1 b0 aa
set len 1 null 1: -1 errno 14
get len 1 null 1: -1 errno 22 len back 1 b0 aa
set len 0 null 0: -1 errno 22
get len 0 null 0: -1 errno 22 len back 0 b0 aa
set len 0 null 1: -1 errno 22
get len 0 null 1: -1 errno 22 len back 0 b0 aa
get null length: -1 errno 14
EXIT=0
Darwin 27.0.0

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Answer to the round-1 review (#787 (comment)), at 7d82642f3. The body is corrected to this head; the patches and the Darwin measurement are in #787 (comment). Logs are named by round and step.

The base. #783 has not landed. This head carries #783 at c95d0ffc0, which carries main at 971eeaaf2 (merge 1ca0dcff9, git's own, no conflict; the stage's hunks in tests/toyos.rs and the track are the ones it had against 8bceea054, diff exit 0). While the gates ran, main moved to cfbe514f4 and #783 to be7a52dc4; this head does not carry that, git merge-tree of the two is clean, and it is taken with every gate again when #783 lands or with the next round.

BLOCKER

  1. Evidence, the suite red and the head. Whole guest suite at 7d82642f3: exit 0, 37 passed, 37 total (round 6, guest-suite), started at a one-minute load of 19.77 and ended at 59.93, ceilings at 1.00x. --ci host exit 0, 78 steps all green; clippy 0; --build-only 0. Not measured: guest / suite under KVM and CI's Linux host, which a draft does not run.
  2. A listener's TCP_NODELAY. Decided by the orchestrator: refused by name, as at 8bceea054. The guard is netstack_id != 0 again, netstack answers a listener's id not connected, the set is -1 ENOTCONN and nothing is kept. nodelay_kept.c has the listener's lines (socket, bind, listen, set, read back), green in the guest; mutation (f), round 1's guard, reds those three lines (guest exit 1). 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. The tier: the refusal is netstack's answer, so the guest; the three lines are where the guest's output parts from the host's, shown in the body.
  3. The datagram-socket rule. In sockopt.rs (kept takes the socket's kind and refuses TCP's level on a datagram socket, any name), a row of an_options_value_crosses_as_the_hosts_does, the four guest lines gone. libc answers Linux's two words by reading, ENOPROTOOPT to a setter and EOPNOTSUPP to a getter, before a length is read; Darwin's measured EINVAL, and EFAULT for a setter's null value, are in darwin(). Mutation (e), the rule removed: host exit 101. The Linux job is the measurement and has not run; the body says a Linux red on that row sends it back.
  4. inet_ntop and inet_pton. The fit and both family refusals are inaddr.rs's (ntop, pton). a_buffer_too_short_for_the_text_is_refused_as_the_host_refuses_it compares with the host at every size from 0 to 17 for a text of each length 7 to 15, the buffers whole; another_family_is_refused_as_the_host_refuses_it is the family row, both calls, four families. (a) >= to >: host 101. (b) every buffer under 16 refused: host 101. (c) inet_pton answers 0 for another family: host 101. (b) and (c) mutate the refusal where it now lives; the errno each maps to is one match in socket.rs.

NOTE

  • m4, m5: land as decided; not run again.
  • The short sockaddr buffer: fixed here by the orchestrator's decision. SockaddrIn::answer truncates to *addrlen and writes 16 back; against the host's getsockname at every length from 0 to 32; mutation (d), the whole write, host 101; the issue is deleted and nothing cited it. One function's work.
  • getaddrinfo's issue: unchanged.
  • The three deletions: m1 guest 1, m2 guest 1, m3 host 101 and guest 1, again at this head. git grep of each slug, and of the two deleted this round, finds nothing.
  • The two stalls: fe241fc3e. issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md has round 5's two guests with the six lines, the load, the waits, what is known and not, and an owner. One correction to the review's reading: virt_failed_ap_leaves_no_hole ends at unmap_touch and does not wait on the read, so only virt_smp did the step beside them (guest 13.936 to 19.120); and the alone runs' logs carry no guest line, so "0.13 s" is not recorded. issues/virt-mask-windows-went-quiet-on-a-loaded-host.md is folded in and deleted: its recorded evidence shows no different defect, and its two stalls without a console (b9430ed61, and 94fd25e71 on wt/toyos-move-abi, each number read off the m1 logs) are recorded as stalls whose step is not read from the guest. The ScalarEvolution clause is under not known.
  • The duplicated boot helper: boot_netcase and holding_server, one each.
  • The reorder, the ruling on smoltcp: nothing to do. The listener's lines are a third that pass for smoltcp-era netstack's reason, and the body says so.
  • SockaddrIn's alignment: #[repr(C, align(4))], asserted with the size.
  • A leading zero: refused in inet_pton and so in getaddrinfo, as glibc; Darwin's reading is stated in the differential (darwin_reads); mutation (g) host 101.
  • connect's ENOTCONN on a reset: the sentence is in the reset issue.
  • Body, prose: rewritten; neither sentence is there.

The re-measure list

  1. The merge: above. 2. --ci host: 0. 3. CI's Linux host: not run, a draft; the names to read are in the body, with the three new tests. 4. Suite 0, 37 of 37; libc_sockets alone 0, addr_order and sendto_unbound byte for byte the host's, nodelay_kept but for the listener's three lines. 5. m1, m2, m3, (a), (b), (c) and (d) to (g): all red, table in the body. 6. clippy 0, --build-only 0.

One red on the way: round 6's first clippy at dba2ea6e2 (clippy::unit_arg in the option test), fixed by amending before any push.

@Japabu Japabu changed the title libc binds a datagram socket at its first send, keeps a TCP_NODELAY for connect, and holds an IPv4 address as its octets libc binds a datagram socket at its first send, keeps a TCP_NODELAY for connect, holds an IPv4 address as its octets, and truncates one to its caller's buffer Oct 9, 2026
@Japabu
Japabu marked this pull request as ready for review October 9, 2026 03:26
@Japabu
Japabu marked this pull request as draft October 9, 2026 03:26
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2 of #787 at 7d82642f3, against .claude/agents/reviewer.md. Read as the stage alone, git diff c95d0ffc0 7d82642f3, and within it the round's changes since 68cf1f870. Read only: nothing built or run. Logs read are the stage's own, named by round and step, and the m1/m2 step logs of #783's merge.

Net lines, stage alone: 26 files, +1200 −357. Production (userland/libc, headers included): +289 −178, net +111 (round 1: +53). Tests: +717 −63. issues/: +194 −116. The round's production growth is SockaddrIn::answer, the datagram rule and its two refusals, and ntop/pton moved in with theirs; each is a behaviour a host test now holds. Accepted.

While this was read #783 landed: main is 0d6cd9a60. git merge-tree --write-tree 0d6cd9a60 7d82642f3 exits 0 with tree eae4f5926b06, no conflict, and that tree against main is the same 26 files, +1200 −357; git grep of the five deleted slugs in it finds nothing.

Round 1's BLOCKERs

  1. Evidence, the suite and the head — CLOSED at this head. Round 6, guest-suite: head 7d82642f3, clean tree, exit 0, "37 passed, 37 total", load 19.77 at its start and 59.93 at its end, ceilings at 1.00x. Round 6, ci-host: exit 0, "Host: 78 step(s), all green". clippy 0, --build-only 0, libc_sockets alone exit 0. It is not the head that lands; what that head owes is at the end.
  2. A listener's TCP_NODELAY — CLOSED. userland/libc/src/socket.rs:591 is netstack_id != 0 again; netstack's handler answers an id that is no stream ERR_NOT_CONNECTED (userland/netstack/src/main.rs:859-861), so the set is -1 ENOTCONN and line 597 is not reached. Mutation (f), round 1's guard: guest exit 1, nodelay_kept's three listener lines <-- WRONG. The tier is right: the refusal is netstack's answer to libc's request, socket.rs has no host build, and the three lines ride a boot the case already has. What it leaves is under NOTE.
  3. The datagram rule — CLOSED for its home and its tier; its two words are still unmeasured. The rule is sockopt.rs:27, the row is in an_options_value_crosses_as_the_hosts_does, the four guest lines are gone. Mutation (e): host exit 101, left: Set, right: Refused(92) at socket_options.rs:208, which on Darwin is the test's own assertion of libc's words and not the host's answer, as the body says. I read Linux again and agree with the implementer: udp_setsockopt falls to ip_setsockopt, whose first test answers ENOPROTOOPT for a level that is not SOL_IP; udp_getsockopt falls to do_ip_getsockopt, which answers EOPNOTSUPP for the same before it copies the length in; so a null value, a null length and every length answer those two. Two readings are no measurement. It does not hold the verdict because the measurement is the required host check itself: a Linux that says another word turns that check red and the queue refuses the head. What the orchestrator reads there, by name, is at the end.
  4. inet_ntop and inet_pton — CLOSED. The fit and the family refusal are inaddr.rs:57-81. (a) >= to >: host 101, left: Ok(()), right: Err(Room) at internet_addresses.rs:221. (b) every buffer under 16 refused: host 101, same line. (c) another family answered 0: host 101 at :125. The test compares the answer, the host's errno and both 32-byte buffers whole at every size 0 to 17 for a text of each length 7 to 15.

The round's other changes

  • The short sockaddr. fill_sockaddr (socket.rs:124-129) is the only writer of a sockaddr into a caller's buffer in userland/libc/src, and accept, recvfrom, getpeername and getsockname all answer through it; getaddrinfo writes its own allocation. SockaddrIn::answer copies min(*len, 16) and writes 16 back. Mutation (d), the whole write: host 101 at internet_addresses.rs:276. The deleted issue's exit (each of the four calls writes at most *addrlen bytes and answers the address's length, and a host test holds a buffer shorter than the address) is met; nothing cites the file.
  • The leading zero. inaddr.rs:92-93. Mutation (g): host 101, a_number_with_a_zero_before_another_digit_is_no_dotted_quad and the differential's count (28561 where 10000). I read dotted_quad against glibc's inet_pton4 (saw_digit && *tp == 0 refuses, a fourth digit overflows 255, a trailing dot fails at octets == 4) and numbers_and_dots against inet_aton_end (strtoul base 0, so 0x, 08 and 1x end on a character that is no space; the white-space set; max[] by count of parts): I find no text of the tests' tables on which they part. Not a measurement.
  • align(4): inaddr.rs:16, asserted with the size at internet_addresses.rs:233.
  • The boot helper: boot_netcase and holding_server, one each, both callers moved.
  • m1, m2, m3 again at this head: guest 1, guest 1, host 101 and guest 1.

The stall records

The fold is justified by the evidence; it does not merge two defects under one name.

  • virt_mask_windows's second wait (tests/toyos.rs:1938) spans test_rs_counters_read, test_rs_trace_read and the shutdown (tests/virtsmpcase/system.toml). The pipe ABI carries a datagram socket's broadcast permission from std and libc to netstack, and the option read is gone #783's reviewer read the 94fd25e71 stall as a different wait of the harness, which it is; that says nothing of the guest's step.
  • The one virt_mask_windows stall with a console (round 5, guest-suite) ends on the read's spawn record, 14.557, 14.558, 14.585 on cpu4, as virt_el1_smp's does in the same second (13.577, 13.578, 13.590).
  • 94fd25e71, from m1-suite.log: the guest's unmap_touch line reached the harness at 01:59:52, the window of what it said while waited on is empty, and the verdict is at 02:00:07. A guest that had finished the read would have said its end; virt_smp beside it took 01:59:56 to 02:00:09 over that same step. That bounds the silence to before the read's end and names no cause, and the record says no more than that.
  • b9430ed61 has no console and no bound; the record says so.
  • The alternative, the deleted file kept, was a sibling with "Owner: none yet" and an exit that named no cause.

Every number of the new records is in the logs: 75.99 and 63.37, 1.52x, 366 s and 808 s, 35 of 37, both FAIL lines at 01:50:02, the six lines, virt_smp's 13.936 to 19.120 over five seconds, the launch order, the two alone runs at 32.47 and 37.04 in 4 s each (round 5); 59.30 69.33 70.48 and 85.73 69.02 67.23, 34 of 36, 1794 s and 703 s, 2765 ms and 1.94x, STALL ... (38s), 21.786 and 34.278, 13.481 and 15.702, and the two passes at c95d0ffc0 at 12.82 to 36.76 and 36.76 to 42.53 (m1, m2 and their exit records). The b9430ed61 figures are the deleted file's own.

BLOCKER

None open.

NOTE

  • issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md, "Not carried by accept on smoltcp" — the reason given is false of this head — "a hand-over at accept would carry one set after the connection was queued too": with socket.rs:591 every set after bind is refused and keeps nothing, so a listener's nodelay is frozen at bind and is exactly the option it held when any connection began. What is left is the half the file itself calls "a program told yes and given no" (set before bind: 0, read back 1 on the listener, clear on every accepted socket), and at this head accept could remove it as connect does at :208-215, unseen by a test as m4 is. The orchestrator decides again on the true premise: the hand-over at accept (then socket.rs changes and that hunk comes back here), or the file says why not in words that are true.
  • same file, Exit — it waits on "the stage of issues/toyos-has-its-own-network-stack.md that gives the pipe ABI a listener's option", and no line of the track names that stage or this file; its listener-defects line names three other files. The track names this one there, or its holder never meets it.
  • userland/libc/src/socket.rs:124-127, with :667 and :686 — a null buffer or a null length is not handled as the host does: getsockname and getpeername answer 0 and write nothing, where the host answers -1 EFAULT (Linux reads the buffer only when the length is not 0). Unchanged by this stage and held by no test; answer's contract excludes it. Filed in issues/ with libc as owner, not fixed on the way.
  • issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md — prose: "Two earlier stalls of virt_mask_windows": by the logs' clocks the 94fd25e71 stall (02:00:07) is ten minutes after the 68cf1f870 one (01:50:02). And "the fork compiler miscompiles, for AArch64 ... being measured by a per-function census on another branch" cites nothing a reader can open; the head that lands carries issues/the-forks-llvm-deletes-a-loops-exit-on-a-no-wrap-flag-scalar-evolution-gives-the-wrong-value.md, which has the fault on both architectures, and the clause names that file.
  • Body — prose: "The pipe ABI carries a datagram socket's broadcast permission from std and libc to netstack, and the option read is gone #783 has not landed" and the paragraph on be7a52dc4 are no longer true; main is 0d6cd9a60.

The head that lands

It is this head with main merged, and with the notes above. If the merge is git's own (its tree is git merge-tree --write-tree of the two, and its diff against main is the 26 files) and the notes change only issues/, the orchestrator may land on reading the three jobs below from their own logs, not from the checks' colour, with no further round. A change to userland/libc or to a test comes back. A red in any job is a defect and comes back; it is not run again to green.

host, on ubuntu-24.04, at that head: the step cargo run -- --ci host exits 0 and ends "Host: N step(s), all green"; and inside it toyos-libc-copies prints each of these as ... ok:

  • socket_options::an_options_value_crosses_as_the_hosts_does — the measurement of Linux's ENOPROTOOPT and EOPNOTSUPP. Sent back by: a panic at socket_options.rs:181 whose row reads set NoDelay of a datagram socket ..., get NoDelay of a datagram socket ... or NoDelay of a datagram socket: a null length. Its left: is what Linux answered; sockopt.rs:27 and the test's refused take that word, darwin() stays, and it comes back.
  • internet_addresses::a_dotted_quad_reads_as_the_host_reads_it — glibc's inet_pton, the leading zero among its texts. Sent back by: a panic at internet_addresses.rs:99, which names the text.
  • internet_addresses::an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it — Linux's getsockname at lengths 0 to 32, the first two bytes compared there too. Sent back by: any getsockname into N or into N panic.
  • internet_addresses::a_buffer_too_short_for_the_text_is_refused_as_the_host_refuses_it and internet_addresses::another_family_is_refused_as_the_host_refuses_it — glibc's ENOSPC and EAFNOSUPPORT (97 there) and an untouched buffer.
  • internet_addresses::numbers_and_dots_read_as_the_host_reads_them, internet_addresses::an_address_is_written_as_the_host_writes_it, internet_addresses::a_sockaddr_holds_its_port_and_address_in_network_order, internet_addresses::a_number_with_a_zero_before_another_digit_is_no_dotted_quad, internet_addresses::a_number_past_32_bits_or_without_digits_is_no_address, prototypes::every_prototype_is_a_definition_and_every_definition_is_declared, errno_codes::every_code_libc_answers_with_is_errno_h_s.

A test of that list absent from the log is a red.

toolchain / build: the LLVM, compiler and freestanding restores hit. libc is linked into std (src/libc.rs), so the sysroot's key is expected to miss, be built by --ci bootstrap and be saved by its one save step with the other three skipped; the key and which of the two happened are read off the log, and a hit on a key no run of this head saved is reported, not passed over.

guest / suite: run and not skipped; its sysroot restore hits the key toolchain / build names; cargo run -- --ci guest exits 0; every test green, by name libc_sockets, netstack_socket_churn, virt_mask_windows, virt_el1_smp and virt_smp. A stall of one of the last three is another occurrence for the counters record, with that runner's figures, and is still a red.

LAND AFTER NAMED CHANGES

Japabu and others added 2 commits October 9, 2026 05:37
… 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 94fd25e is ten minutes after the two at 68cf1f8, not before
them, and its compiler clause names the LLVM fault's file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu Japabu changed the title libc binds a datagram socket at its first send, keeps a TCP_NODELAY for connect, holds an IPv4 address as its octets, and truncates one to its caller's buffer libc binds a datagram socket at its first send, keeps a TCP_NODELAY for connect and for the connections a listener accepts, holds an IPv4 address as its octets, and truncates one to its caller's buffer Oct 9, 2026
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Answer to the round-2 review (#787 (comment)), at 2c8a4ad58. The body is corrected to this head. Logs are named by round and step.

The merge. origin/main was 0d6cd9a60. The merge, f1b8b85e7, is git's own with no conflict: its tree is eae4f5926b06, the one git merge-tree --write-tree 0d6cd9a60 7d82642f3 writes, and git diff --stat 0d6cd9a60 f1b8b85e7 is the stage's 26 files, +1200 −357 (round 7, merge). The head on top of it is 27 files against main: one issue of the stage's deleted, one of main's changed, one filed.

The five NOTEs

1. accept hands the listener's kept TCP_NODELAY over, the orchestrator's decision on the true premise: a set after bind is refused, so the listener's value is frozen at bind and is the option it held when any connection began. This changes userland/libc, so the hunk comes back to the review:

diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 600916d99..4cbc660ee 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);

A hand-over refused because the peer already reset closes the connection and fails the accept with the request's errno, ENOTCONN, as connect's path does; issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md says it of both calls.

Tested, with a change inside tests/toyos.rs and the case alone. A peer can dial into the case's existing boot: QEMU's monitor adds a forward at run time, and the harness already had the monitor (QmpMonitor), the hook on a guest's line (run_test_hooked) and BootOptions { qmp }. Measured first on QEMU 11.1.1 with no guest: hostfwd_add net0 tcp:127.0.0.1:0-:7001 binds a loopback port of QEMU's choosing and info usernet names it. nodelay_kept.c gains a listener that set the option before bind and reads 1 on it, says both listeners wait, and reads the option on each accepted socket: 1 from that listener, 0 from the one that held none. tests/common is untouched.

  • Guest, round 7, guest-libc-sockets: exit 0, 28 lines, nodelay_kept: ok.
  • Host oracle, the same source on Darwin 27.0.0 with a loopback peer that dials both listeners in once the case says it waits (round 7, host-case-worktree, run on the worktree before the commit, the file unchanged since): the first 25 lines are the guest's, both accepted sockets' reads among them; the last three, a listener's set after bind, are the recorded difference.
  • Mutation (h), the hunk above reverted: guest exit 1, read from its connection: 0 <-- WRONG, 1 wrong.
  • Mutation (i), the request deleted and the value still stored: guest exit 0, green. The read is libc's entry, as getsockopt answers from it. That is m4's like at accept, and issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md now measures it and names accept in its exit. Not a test to add here: nothing reads netstack's side on either call.
  • m2 and (f), round 6's patches unchanged, which still apply: guest exit 1 each. m2 now turns four lines (read before connect, read after connect, read from that listener, read from its connection); (f) the listener's three, as before.

The issue. issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md is deleted, and git grep of its slug at the head exits 1. What remained of it, a set after bind refused by name until the pipe ABI gives a listener its option, is what issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md (on main since #777) says and exits on. That file is made true of libc as it stands: the refusal's errno, the value kept before bind and handed over at accept, nodelay_kept.c as the test that holds both and whose three lines its exit turns. It also named TcpGetOption, which #783 removed from the tree; the name is gone.

2. The track names that file in its listener-defects line, with what its exit waits on: the pipe ABI giving a listener its option, which the node has (Node::set_listener_nodelay) and nothing ships.

3. Filed, not fixed: issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md, libc its owner. The host's answers are its exit's oracle and are measured, Darwin 27.0.0 (round 7, host-null-sockname): a null buffer with length 16 and a null length are -1 EFAULT from both calls; a null buffer with length 0 is 0 with 16 written back. libc answers 0 to all three and writes nothing. Linux is unread there too.

4. The counters record. "Two earlier stalls" is "Two other stalls", and the 94fd25e71 entry says it came ten minutes after the two at 68cf1f870, by the runs' logs. The compiler clause names issues/the-forks-llvm-deletes-a-loops-exit-on-a-no-wrap-flag-scalar-evolution-gives-the-wrong-value.md and says what that file says: every function but the ones it names is not measured.

5. The body no longer says #783 has not landed, and its paragraph on be7a52dc4 is gone.

Gates at 2c8a4ad58, clean tree

gate exit
cargo run -- --build-only 0
cargo run -- --ci host 0 "Host: 78 step(s), all green"
cargo run -- --clippy 0
cargo test --test toyos-build -- --jobs 1 --nocapture libc_sockets 0 1 passed, 1 total
m2, (f), (h) guest 1 each tree clean after each
(i) guest 0 green, recorded
cargo test --test toyos-build -- --jobs 4 --nocapture 0 37 passed, 37 total; load 33.19 to 35.33 on 14 cores, ceilings at 1.00x; no stall

The two new patches

Each applied with git apply --check and git apply, run, restored with git apply -R.

h-accept-carries-nothing.patch:

diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 4cbc660ee..cd5e74534 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -290,9 +290,6 @@ 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];
@@ -302,14 +299,6 @@ 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);
@@ -326,7 +315,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,
+        nodelay: false,
         broadcast: false,
     };
     let new_fd = alloc_socket(new_entry);

i-accept-stores-and-sends-nothing.patch:

diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 4cbc660ee..b5b3826d1 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -302,14 +302,6 @@ 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);

The host measurements

nodelay_kept on the host, its two listeners dialled once it says they wait:

Darwin 27.0.0
$ cc -Wall -Wextra -Werror -o nodelay_kept tests/netcase/nodelay_kept.c
EXIT=0
$ ./nodelay_kept 127.0.0.1 <the peer's stream port> <a free port> <another>, each listener dialled once the case says it waits
the peer's address is one: 1
a fresh stream socket's TCP_NODELAY: 0
set before connect: 0
read before connect: 1
connect: 0
read after connect: 1
cleared on the connection: 0
read after the clear: 0
set on the connection: 0
read after the set: 1
set on a second socket: 0
and cleared before connect: 0
connect: 0
read after connect: 0
set on a third socket before bind: 0
bind it: 0
listen on it: 0
read from that listener: 1
bind a fourth socket: 0
listen on it: 0
nodelay_kept: both listeners wait for a peer
accept from the listener that held the option: 1
read from its connection: 1
accept from the listener that held none: 1
read from its connection: 0
set on the listener: 0  <-- WRONG
which is refused as not connected: 0  <-- WRONG
read from the listener: 1  <-- WRONG
nodelay_kept: 3 wrong
EXIT=1

getsockname and getpeername with null pointers, on a connected stream:

Darwin 27.0.0
CC_EXIT=0
getsockname null buffer, length 0: 0 errno 0 (-)
  length after: 16
getsockname null buffer, length 16: -1 errno 14 (Bad address)
  length after: 16
getsockname null length: -1 errno 14 (Bad address)
getpeername null buffer, length 0: 0 errno 0 (-)
  length after: 16
getpeername null buffer, length 16: -1 errno 14 (Bad address)
  length after: 16
getpeername null length: -1 errno 14 (Bad address)
accept null buffer, null length: 0 errno 0 (-)
EXIT=0

@Japabu
Japabu marked this pull request as ready for review October 9, 2026 04:08
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3 of #787 at 2c8a4ad58, against .claude/agents/reviewer.md. Narrow: git diff 7d82642f3 2c8a4ad58 less the merge of main, which is git diff f1b8b85e7 2c8a4ad58, 10 files, +174 −76: userland/libc/src/socket.rs +13 −2, tests +88 −14 (tests/toyos.rs, tests/netcase/nodelay_kept.c), the rest issues/. Read only: nothing built or run. Logs read are the stage's own, named by round and step.

Net lines of the branch against main (git diff --shortstat origin/main...2c8a4ad58): 27 files, +1304 −363. Production (userland/libc, headers included) +301 −179, net +122; tests +791 −63; issues/ +212 −121. The round's production growth is the hand-over in accept, a behaviour (h) turns red. Accepted.

The merge is git's own. git merge-tree --write-tree 0d6cd9a60 7d82642f3 exits 0 and writes eae4f5926b06; f1b8b85e7^{tree} is that tree, its parents are 7d82642f3 and 0d6cd9a60, and origin/main is still 0d6cd9a60, the branch's merge base.

Round 2

No BLOCKER was open. Its five NOTEs:

  1. The hand-over at accept — CLOSED, by the code and by (h); judged below.
  2. The track names the listener issue — CLOSED. issues/toyos-has-its-own-network-stack.md's listener-defects line names it with what its exit waits on; Node::set_listener_nodelay is userland/netstack/node/src/listeners.rs:135.
  3. The null buffer — CLOSED as filed. issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md: its ToyOS column is fill_sockaddr's early return and the two calls' 0 (socket.rs:124-127, :678, :697); its Darwin rows are round 7, host-null-sockname, line for line; owner libc; the exit is a test in tests/libc-arch a host check can turn red.
  4. The counters record — CLOSED. "Two other stalls", and the 94fd25e71 entry placed ten minutes after the two at 68cf1f870. The compiler clause names a file that exists at this head and says what it says: "Exposed: the kernel, the loader and userland, on both architectures", and under "Not measured", "any other function of the kernel, the loader or userland, on either architecture".
  5. The body — CLOSED for what round 2 named; what is false of it now is under NOTE.

The hunk in accept

  • The rule is right. bind sets netstack_id (socket.rs:255); from then setsockopt sends the listener's id to netstack (:602-603), handle_tcp_set_option answers an id that is no stream ERR_NOT_CONNECTED (userland/netstack/src/main.rs:859-861), and line 608 is not reached, for a set and for a clear. So entry.nodelay of a bound socket is what it was at bind, and a copy of it taken before accept blocks is the option the listener held when any connection to it began: the host's rule as a-listening-sockets-tcp-nodelay-… has it measured (set before the connection began, the accepted socket has it). Held by (f): guest 1, the listener's three lines, at this head.
  • Failing the accept is the right answer here, and returning the connection without the option is not. The request can be refused for a connection netstack has just handed out only when netstack no longer holds the stream (the peer reset it and bridge_piped let the id go) or when the request itself did not get through. A connection returned on that would be an fd for a stream netstack does not have, with an entry that reads 1 for an option nothing holds: the "told yes, given no" this hunk removes, behind an error swallowed on a guess at its cause. POSIX gives accept a failure for a connection aborted before it returned (ECONNABORTED); Linux returns the socket and reports the reset on its first read. The word differs: ENOTCONN. It is no new word at this call: a queued connection that is gone before the accept request is already answered ERR_NOT_CONNECTED (listen::Accept::Nothing, main.rs:1040-1042), so accept says ENOTCONN of a peer that reset a moment earlier with or without this hunk, and a second word for the same event a few instructions later would be the worse design. The record of it is under NOTE.
  • Nothing leaks on that path. accepted.rx and accepted.tx are Pipe(OwnedHandle) and close where accepted drops (toyos/src/lib.rs:114-118); into_raw is past the return. tcp_close is asked for the id; its answer is discarded, as on connect's path and on the full-table path below. No slot of SOCKETS is taken before the return. fill_sockaddr is after the hunk, so the caller's buffer and length are untouched on failure. The notify byte read is the refused connection's own.
  • No sibling. accept is libc's only caller of tcp_accept; std's is the other half of the listener issue and is recorded there.

The test

  • Tier. The body says why QEMU: the accepted socket's option needs a connection netstack accepted from a peer, socket.rs has no host build, and the boot is one the case already has. Accepted.
  • Waited on by event, and bounded. The dial is run_test_hooked's action on the guest's own line, nodelay_kept: both listeners wait for a peer, printed after both binds; netstack listens from bind, so no order between the dial and accept is assumed. No sleep, no retry. A dial QEMU refuses is an Err the test names before it reads the case's verdict; a dial that reaches no listener leaves the case in accept, which the harness's quiet ceiling ends by name. The monitor's reads are under Qmp's 20 s read timeout and a refused hostfwd_add is a non-empty answer, an Err by name.
  • No fixed host port. hostfwd_add net0 tcp:127.0.0.1:0-:<port>: the host's port is the kernel's choice per QEMU, read back from info usernet by the guest port in the TCP[HOST_FORWARD] row. 7001 and 7002 are guest ports, each guest's own; git grep -w finds them nowhere else under tests/ or userland/netstack/src. Two suites side by side share nothing.
  • Each listener takes one connection, so the test stays out of issues/a-connect-between-two-accepts-is-reset.md's window.
  • The move off smoltcp. Nothing in the harness names the stack: slirp dials the guest's leased address and the case binds the wildcard. The 25 lines the host oracle shares (round 7, host-case-worktree, both accepted sockets' reads among them) are the lines any stack owes; the three on the refused set are the ones the listener issue's exit turns, as it says.
  • (h), the hunk reverted, at 2c8a4ad58: git apply --check 0, guest exit 1, read from its connection: 0 <-- WRONG after accept from the listener that held the option: 1, nodelay_kept: 1 wrong, the other listener's read 0 and unmarked; restored, tree clean. It is red at the line that names the claim.
  • (i), the request deleted and the value stored: guest exit 0, green. Accepted as round 2 accepted m4: socket.rs is on none of root CLAUDE.md's high-risk subjects, the pipe ABI has no option read for a test to ask, and issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md records the measurement and names accept in its exit, with an owner. Still true: no test sees whether either hand-over reaches netstack.

The issues

  • issues/a-connection-libc-accepts-does-not-carry-its-listeners-tcp-nodelay.md is deleted; git grep of its slug at 2c8a4ad58 exits 1, and so does TcpGetOption outside rust/.
  • issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md is true of this head: the refusal and its errno, the value kept before bind, the hand-over, the case that holds them, std's half.
  • The null-buffer issue, the counters corrections and the track's line: as above.

Evidence at 2c8a4ad58

Each log opens with its command, the head and an empty git status --porcelain, and ends with its exit. Round 7: build, exit 0. ci-host, exit 0, "Host: 78 step(s), all green", toyos-libc-copies 46 passed. clippy, exit 0. guest-libc-sockets, exit 0, "1 passed, 1 total", nodelay_kept: ok with both accepted sockets' reads. guest-suite, exit 0, "37 passed, 37 total", load 33.19 at its start and 35.33 at its end, ceilings at 1.00x, PASS libc_sockets (12s), no stall. m2 and (f): guest 1 each.

BLOCKER

None.

NOTE

  • issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md, Exit condition — the file now records two compromises of libc's, connect and accept failing ENOTCONN with the connection closed when the peer resets before the hand-over, and its exit reads neither: it is met by a guest test whose nodelay() answers Ok, which the file itself says asks netstack nothing. Met as written, both failures stand with no record left. The exit gains what ends them: the option request on a stream its peer reset is answered, and a connect and an accept whose peer resets before the hand-over answer as the host does, by a test that can be red. issues/ only.
  • Body — prose: "this pull request is a draft, which that job skips" and "guest / suite under KVM and the Linux host job have not run: the pull request is a draft" are false of the record: the pull request is ready and ci is running at 2c8a4ad58.

The head that lands, and the three jobs

host and toolchain / build were pending at 2c8a4ad58 when this was written, and guest / suite had not started; none is judged here. Round 2's list stands as written, with these changes:

  • toolchain / build: unchanged in kind. socket.rs changed again, so the sysroot's key is a new one and is expected to miss and be saved by this run.
  • guest / suite: libc_sockets green there is now also the first reading of the runner's QEMU, on Linux under KVM, for what the harness was measured on with QEMU 11.1.1 alone: that it takes hostfwd_add net0 tcp:127.0.0.1:0-:<port> on a running guest and that info usernet names the host port in the fourth column of its TCP[HOST_FORWARD] row. Sent back by FAIL libc_sockets with any of: QEMU forwards nothing to the guest's port, QEMU names no host port forwarded to the guest's, the host could not dial a port QEMU forwards to the guest, a qmp: panic, a stall with nodelay_kept: both listeners wait for a peer as the case's last line, or nodelay_kept ending 1 with a <-- WRONG on accept from the listener that held … or read from its connection. The first three and the stall are the harness's on that QEMU and come back to tests/toyos.rs; none is run again to green.
  • host: unchanged.

The first NOTE changes only issues/ and moves the head. If that is all that changes, the orchestrator may land with no further round, on reading the three jobs from their own logs at the head that lands, not at 2c8a4ad58 and not from the checks' colour. A change to userland/libc or to a test comes back.

LAND AFTER NAMED CHANGES

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI at 5d9c996e2, read from each job's own log (the orchestrator).

  • host (ubuntu-24.04): [ci] Host: 79 step(s), all green, and in toyos-libc-copies each of the twelve tests rounds 2 and 3 named prints ... ok: socket_options::an_options_value_crosses_as_the_hosts_does (so Linux answers the datagram row as libc does: the two errnos implemented by reading are now measured), internet_addresses::a_dotted_quad_reads_as_the_host_reads_it (glibc's leading zero), …::an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it (Linux's getsockname at every length), …::a_buffer_too_short_for_the_text_is_refused_as_the_host_refuses_it, …::another_family_is_refused_as_the_host_refuses_it, …::numbers_and_dots_read_as_the_host_reads_them, …::an_address_is_written_as_the_host_writes_it, …::a_sockaddr_holds_its_port_and_address_in_network_order, …::a_number_with_a_zero_before_another_digit_is_no_dotted_quad, …::a_number_past_32_bits_or_without_digits_is_no_address, prototypes::every_prototype_is_a_definition_and_every_definition_is_declared, errno_codes::every_code_libc_answers_with_is_errno_h_s. No panic in either module.
  • toolchain / build: LLVM, compiler and freestanding restored; the sysroot 4896c285f7e3a61a built and saved by this run.
  • guest / suite (KVM): the sysroot restored from that key; test result: ok. 37 passed, 37 total; libc_sockets PASS (the runner's QEMU takes the monitor's port forward and names the host port, so the harness holds there too), netstack_socket_churn, virt_mask_windows, virt_smp, virt_el1_smp each PASS by name; no FAIL line.
    The head differs from the reviewed 2c8a4ad58 in one issue file (+4 -1). git merge-tree against main at 44871bee9 exits 0 with no conflict.

@Japabu
Japabu deleted the wt/toyos-libc-sockets branch October 9, 2026 04:59
Japabu added a commit that referenced this pull request Oct 9, 2026
One conflict, in the track's line on the broadcast permission. #787 changed
that line's last sentence only: the C case now runs the sequence without
`bind` too, held by `tests/netcase/sendto_unbound.c`, where the sentence
waited on an issue #787 closes and deletes. This branch rewrote the rest of
the line. The line is this branch's with #787's ending. The line after it,
on a logged refusal waiting in the stack, stays deleted: #787 did not touch
it, and this branch met its exit.

#787's other hunk in the track, the listener's `TCP_NODELAY` issue named
among the listener defects, merged by itself.

No line of the merged tree names any of the four issue files #787 deleted,
by path or by bare name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant