diff --git a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md index 04508750a14..6de1f2db100 100644 --- a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md +++ b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md @@ -8,8 +8,15 @@ opened: 2026-10-04 `test_rs_counters_read` in `tests/virtsmpcase` normally ends about 0.2 s of guest clock after `unmap_touch` does (median 0.17-0.20 s over 570 guests, max -1.46 s). Under host load it sometimes takes seconds, and twice it went silent -long enough for the harness's 15 s quiet bound to call the guest stalled. +1.46 s). Under host load it sometimes takes seconds, and it has gone silent +long enough for the harness's 15 s quiet bound to call the guest stalled: each +silent boot is below. + +`virt_mask_windows` boots the same case on a `mask-windows` kernel and waits +on two things, the job `unmap_touch` and then the boot's last word, so a +silence in the counters read reds it as `STALLED: waiting for the boot's last +word`: that wait spans the read, `test_rs_trace_read` and the shutdown. Its +stalls are recorded here. The first silent one: `virt_el1_smp`, 8 vCPUs under TCG, on a 14-core host at 1-minute load 38-53 while the primary checkout was building the stage-2 @@ -54,9 +61,86 @@ line of the wait and none the guest runs. - **What is not known.** Whether the guest was running, spinning or not being scheduled by the host, and whether it was the guest or its console that stopped. No register capture of it exists either, so it adds a count and a - second profile and brings the exit no closer. The fork compiler miscompiles, - for AArch64, an inclusive range that ends at its integer type's maximum; - whether that reaches this job's code has not been examined. + second profile and brings the exit no closer. + +The third and fourth silent ones, two guests of one run: `virt_el1_smp` and +`virt_mask_windows`, in the whole suite on `wt/toyos-libc-sockets` at +`68cf1f870`, whose diff touches no line of either wait and nothing these +guests start: `tests/virtsmpcase` runs no C program, opens no socket and +starts no netstack. + +- **The lines.** `FAIL virt_el1_smp: STALLED: waiting for the job + test_rs_counters_read to end — it went quiet` and `FAIL virt_mask_windows: + STALLED: waiting for the boot's last word — it went quiet`, both at the same + second of the host's clock; 35 of 37 passed. +- **The load.** `--jobs 4 --nocapture` on the 14-core host, 1-minute load + 75.99 when the run began and 63.37 when it ended, from other worktrees' + builds; liveness ceilings paid at 1.52x; workers 366 s building against + 808 s testing. +- **The waits.** `virt_el1_smp`: `judge_virt_job`'s `await_marker` on + `===TEST_END test_rs_counters_read `. `virt_mask_windows`: its second wait, + `await_marker` on `power::SHUTTING_DOWN`, after `judge_virt_job` had judged + `unmap_touch`. Each was ended by `GUEST_QUIET`. +- **The same profile as the second.** Each guest's last three lines are the + end of `unmap_touch`, the start of the read and the kernel's record of its + spawn, with no thread's exit after it. `virt_el1_smp`: `===TEST_END + unmap_touch exit=0===` at 13.577, `===TEST_START test_rs_counters_read===` + at 13.578, `spawn: /system/bin/test_rs_counters_read pid=13` at 13.590 on + cpu4. `virt_mask_windows`: the same three at 14.557, 14.558 and 14.585, on + cpu4. The two spawn records reached the harness within one second of each + other, and nothing followed from either for the 15 s. +- **What is known.** `virt_smp`, the same case launched one second before + `virt_el1_smp`, did the step in the same seconds and passed: the read began + at guest 13.936 and ended at 19.120, five seconds of the host's clock. + `virt_failed_ap_leaves_no_hole`, launched one second after, ends at + `unmap_touch` and does not wait on the read. Each red, run alone at the + same commit, passed in 4 s, at loads 32.47 and 37.04. +- **What is not known.** Whether the guests ran, spun or were not scheduled: + no register capture exists. Two guests going silent at the same step in the + same second is the first occurrence that bears on the host stopping the + guest, and it shows no more than that. The serial directory the harness + named for that run was not kept; the run's log, outside the tree, is the + only copy. + +Two other stalls of `virt_mask_windows` have no console of the guest, so +the step each stopped in is not read from it: + +- **`wt/toyos-move-abi` at `94fd25e71`.** `FAIL virt_mask_windows: STALLED: + waiting for the boot's last word — it went quiet`, ten minutes after the + two at `68cf1f870` by the two runs' logs, with nothing in what the + guest said while it was waited on, `STALL virt_mask_windows (38s)`; the + whole suite at `--jobs 4`, 34 of 36, workers 1794 s building against 703 s + testing. Load 59.30 69.33 70.48 when the suite began and 85.73 69.02 67.23 + when it ended, 14 cores; fastest boot 2765 ms against the reference + 1424 ms, ceilings at 1.94x. The wait is the second one, ended by + `GUEST_QUIET`, which no host factor stretches. Known: `unmap_touch` had + ended with exit 0, its line reaching the harness 15 s before the verdict, + the guest named by elimination among the three of that case then running. + In the same run `virt_smp` said its `unmap_touch` line at guest 21.786 and + its `counters_read` line at 34.278, 13 s apart on the host's clock and 2 s + inside the bound; `virt_el1_smp`, earlier in that run, 13.481 and 15.702. + The branch changes nothing that guest's kernel or harness is built from. + At `c95d0ffc0` the test passed in the whole suite at load 12.82 rising to + 36.76 and alone at 36.76 to 42.53, neither at the red's load. Not known: + whether the guest was alive or what it did in those 15 s; the red run's + kept serial directory holds eleven UART logs, each an x86-64 boot, and no + PL011's. Whether the silence is `virt_smp`'s 13 s grown past 15 under the + hooks' cost, or a guest that stopped. It was not run again at that commit, + nor on `main` at that load. +- **`wt/toyos-acpi1` at `b9430ed61`.** The same line after 45 s, in the whole + suite; workers 19038 s building against 1375 s testing, load 82.65 83.62 + 77.89 moments after it, 14 cores. Nothing in that branch runs on that + boot. Alone at that load and commit it passed in 14 s. On `main` at + `d47b383cf`, `cargo test -- virt_mask_windows` passed three times: alone at + load 45 rising to 72 (10 s), and twice beside a whole suite at load 74 to + 77 (38 s, with ceilings paid at 8.00x and 31 s between its image and its + first guest line) and 75 to 67 (7 s). + +Not known of any of them: whether the fork compiler's fault reaches +`test_rs_counters_read` or the kernel under it. +`issues/the-forks-llvm-deletes-a-loops-exit-on-a-no-wrap-flag-scalar-evolution-gives-the-wrong-value.md` +has LLVM's ScalarEvolution deleting a loop's exit on both architectures, and +records every function but the ones it names as not measured. The slow ones, with registers: a probe that captured `info registers -a` over QMP whenever the read had not ended 3 s after `unmap_touch` (`debug-slow.patch` @@ -84,3 +168,6 @@ whether its CPUs went idle by a `sched:` line's absence; its diary Exit: the cause of the silence is named from a capture of a silent guest (registers over QMP before anything else touches it), and fixed with the evidence, or shown to be the host stopping the guest. + +Owner: the kernel's AArch64 bring-up, `issues/toyos-runs-on-arm64.md`, held +by the orchestrator's next kernel worker. diff --git a/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md b/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md index 4223534d926..ae9ae8036ff 100644 --- a/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md +++ b/issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md @@ -17,17 +17,26 @@ connection waits to be accepted: a connect returns before the listener's end is established, so the answer rests on that event and not on the order loopback delivers in. A red there prints the host's name and both answers. -ToyOS as it ships does neither half: +ToyOS as it ships does neither half for an option set on a socket that +listens: -- netstack answers `TcpSetOption` and `TcpGetOption` `ERR_NOT_CONNECTED` for +- netstack answers `TcpSetOption` `ERR_NOT_CONNECTED` for any id that is not a stream (`handle_tcp_set_option`, `userland/netstack/src/main.rs`), and libc's `setsockopt` sends a listening socket's id there (`userland/libc/src/socket.rs`), so a C program that sets - the option on its listener is refused. + the option on its listener is refused, -1 and `ENOTCONN`, and the listener + keeps what it held. libc holds a listener from `bind`. One set before that + it keeps, and `accept` hands it to netstack for each connection, as + `connect` does: with every later set refused it is the option the listener + held when any connection began. `tests/netcase/nodelay_kept.c` holds the + refusal, and the accepted socket's read for a listener that held the option + at `bind` and for one that did not; its three lines on the refused set are + the ones this file's exit turns to the host's answers. - std builds every `TcpStream`, an accepted one included, with `nodelay: false` and answers `nodelay()` from that field without asking netstack (`sdk/std/sys/net/connection.rs`). Nothing can make that false - today, since no accepted connection has the option. Once a listener's + today: std has no setter for a listener, and what libc's `accept` hands + over is for a socket of libc's. Once a listener's option reaches the connections it accepts, it is false for each of them. The node (`userland/netstack/node/src/listeners.rs`) has the calls, diff --git a/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md b/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md index d5dda726075..2bee6b0e5b1 100644 --- a/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md +++ b/issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md @@ -29,8 +29,17 @@ entry (`userland/libc/src/socket.rs`). Keeping the id until the client's close request is not the fix: a client that dies sends none, and netstack, having closed its pipe ends, has nothing left that says the client is gone. +libc's `connect` asks the same request of a connection it has just been +answered, to hand over a `TCP_NODELAY` set before it, and its `accept` of one +it has just been given, to hand over its listener's: a peer that resets +between netstack's answer and that request makes either fail `ENOTCONN`, with +the connection closed, which no host's `connect` or `accept` answers. + **Exit condition**: a guest test on `tests/netcase` whose peer resets a stream the client still holds, and `nodelay()` on it answers `Ok`, with -`netstack_socket_churn` still green. +`netstack_socket_churn` still green; and the two compromises above ended: the +option request on a stream its peer reset is answered by netstack, and a libc +`connect` and a libc `accept` whose peer resets before the hand-over answer as +the host does, each held by a test that can be red. **Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. diff --git a/issues/libc-drops-a-tcp-nodelay-set-before-connect.md b/issues/libc-drops-a-tcp-nodelay-set-before-connect.md deleted file mode 100644 index cc081fbfd7b..00000000000 --- a/issues/libc-drops-a-tcp-nodelay-set-before-connect.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# libc drops a TCP_NODELAY set before connect - -Read from the code, not run. `setsockopt` (`userland/libc/src/socket.rs`) -sends `TCP_NODELAY` to netstack only for a socket netstack holds, and a stream -socket is one only from `connect` or `accept`. Set on a socket not yet -connected, the option answers 0, is stored nowhere and is not sent when the -connection is made; `getsockopt` then answers 0 for it, which is true of what -netstack holds and not of what the program was told. Linux keeps the option -across `connect`. `SO_BROADCAST` on a datagram socket not yet bound is kept in -the socket's entry and sent at `bind`; this is the same case with no such -path. - -**Exit**: a `TCP_NODELAY` set before `connect` reaches netstack with the -connection or is refused by name, and a guest C case reads it back after -`connect`. - -**Owner**: libc, in a libc stage of its own under `issues/toyos-has-its-own-network-stack.md`: it touches only `userland/libc` and guest C cases, and lands before the guest tests that follow the move of netd onto the node. diff --git a/issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md b/issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md new file mode 100644 index 00000000000..26a46d1485a --- /dev/null +++ b/issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# libc's getaddrinfo ignores its service and its hints + +Read from the code, not run. `getaddrinfo` (`userland/libc/src/socket.rs`) +names its second and third arguments `_service` and `_hints` and reads +neither. Every answer's port is 0, so the usual C client, +`getaddrinfo(host, "80", &hints, &res)` and `connect(fd, res->ai_addr, +res->ai_addrlen)`, dials port 0. Every answer's `ai_socktype` is +`SOCK_STREAM` and its `ai_protocol` 0 whatever the hints ask, `AI_PASSIVE` +with a null node answers failure where POSIX has the wildcard address, and +`AI_NUMERICHOST` still asks the resolver. Every failure answers -1, which is +none of the `EAI_` codes `include/netdb.h` defines, and `gai_strerror` has +one text. A numeric host is read as `inet_pton` reads one, four decimal +numbers, where POSIX has `inet_addr`'s forms. + +**Exit**: `getaddrinfo` answers the service's port, numeric or refused by +name, and the hints' socket type, protocol and flags, each failure is the +`EAI_` code POSIX names for it, and a guest C case connects to an address +and port `getaddrinfo` answered. + +**Owner**: libc, under `issues/toyos-has-its-own-network-stack.md`: it +touches only `userland/libc` and a guest C case on `tests/netcase`. diff --git a/issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md b/issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md new file mode 100644 index 00000000000..d6403e041af --- /dev/null +++ b/issues/libc-getsockname-and-getpeername-answer-a-null-buffer-with-success.md @@ -0,0 +1,41 @@ +--- +status: open +kind: defect +opened: 2026-10-09 +--- + +# libc's getsockname and getpeername answer a null buffer with success + +`getsockname` and `getpeername` (`userland/libc/src/socket.rs`) answer through +`fill_sockaddr`, which returns having written nothing where the address +buffer or the length pointer is null, and each call then answers 0. A program +that passed a null pointer is told its address was written. + +A host refuses it. Measured on Darwin 27.0.0, one C program, each call on a +connected stream: + +| | `getsockname` | `getpeername` | ToyOS, read from the code | +|---|---|---|---| +| null buffer, length 16 | -1 `EFAULT` | -1 `EFAULT` | 0, the length left at 16 | +| null length | -1 `EFAULT` | -1 `EFAULT` | 0 | +| null buffer, length 0 | 0, the length written 16 | 0, the length written 16 | 0, the length left at 0 | + +Linux is unread; by the review that found this, it reads the buffer only when +the length is not 0, which is Darwin's third row. + +`SockaddrIn::answer` (`userland/libc/src/inaddr.rs`), which the host test +`an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it` +(`tests/libc-arch/src/internet_addresses.rs`) holds against the host's +`getsockname` at every length, excludes both pointers by its contract, so no +test reaches the difference. + +`accept` and `recvfrom` take a null buffer as a caller that wants no address, +which POSIX gives them, and Darwin's `accept` answers a null pair with a +socket; what either answers a buffer with a null length is not read. + +**Exit**: the null cases are in the module a host builds, and a test in +`tests/libc-arch` compares the answer, `errno` and the length written back +with the host's own `getsockname` for the table's three rows, on each host +the `host` check runs on; `getpeername` answers through the same function. + +**Owner**: libc, `userland/libc`; whoever next changes its address calls. diff --git a/issues/libc-reads-an-ipv4-address-in-host-order.md b/issues/libc-reads-an-ipv4-address-in-host-order.md deleted file mode 100644 index 92e8912ff0e..00000000000 --- a/issues/libc-reads-an-ipv4-address-in-host-order.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# libc reads an IPv4 address in host order - -`sockaddr_in`'s `s_addr` is in network byte order: its four bytes in memory -are the address's four octets. libc's `parse_sockaddr` -(`userland/libc/src/socket.rs`) takes them with `s_addr.to_be_bytes()`, which -on a little-endian machine reverses them, and `fill_sockaddr`, `getaddrinfo` -and `inet_addr` write `u32::from_be_bytes(ip)`, reversed the same way. So an -address libc made itself comes back right, and one a program made as POSIX -says does not: `inet_pton` copies the octets in order and `htonl` swaps, and -`connect` reads either backwards. - -Measured in a guest on `tests/netcase`, x86-64, with a host server listening: -`connect` to `{htonl(0x0a000202)}` answered -1, and to -`{inet_addr("10.0.2.2")}` answered 0. - -**Exit**: every address libc reads or writes is its octets in memory order, -and a guest C case connects to an address built with `htonl` and to one built -with `inet_pton`. - -**Owner**: libc, in a libc stage of its own under `issues/toyos-has-its-own-network-stack.md`: it touches only `userland/libc` and guest C cases, and lands before the guest tests that follow the move of netd onto the node. diff --git a/issues/libc-sends-from-a-datagram-socket-it-never-bound.md b/issues/libc-sends-from-a-datagram-socket-it-never-bound.md deleted file mode 100644 index 602498724f8..00000000000 --- a/issues/libc-sends-from-a-datagram-socket-it-never-bound.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# libc sends from a datagram socket it never bound - -netstack holds a datagram socket from `bind` -(`toyos::net::udp_bind`), which is also where libc's socket entry gets its two -pipe ends. `sendto` (`userland/libc/src/socket.rs`) does not bind a socket -that was never bound: it writes the datagram to the entry's `tx_fd`, which is -0 and names whatever handle 0 is in the process, and only then would ask -netstack to send from socket id 0. Measured in a guest on `tests/netcase`, -x86-64, from a job of test-runner's: `sendto` answers -1 with `errno` 5 -(`EIO`), and the job exits 0. Which step refused is not shown: libc answers -`EIO` for a refused write and for several of `udp_send_to`'s refusals alike. -What a process whose handle 0 takes a write sees is unmeasured. POSIX binds an unbound datagram socket to an -ephemeral port at its first send, and a C program that broadcasts, `socket`, -`setsockopt(SO_BROADCAST)`, `sendto`, does exactly this. std has no such -socket: `UdpSocket::bind` is its only constructor. - -That program does not compile either: `userland/libc/include/netinet/in.h` -defines no `INADDR_BROADCAST`. - -**Exit**: the first `sendto` on an unbound datagram socket binds it to a port -netstack chooses, handing over a `SO_BROADCAST` already set as `bind` does, -`netinet/in.h` defines `INADDR_BROADCAST`, and a guest C case sends from a -socket it never bound. - -**Owner**: libc, in a libc stage of its own under `issues/toyos-has-its-own-network-stack.md`: it touches only `userland/libc` and guest C cases, and lands before the guest tests that follow the move of netd onto the node. The C case the track's broadcast line names for the sequence without -`bind` waits on this. diff --git a/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md b/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md new file mode 100644 index 00000000000..e203fc97284 --- /dev/null +++ b/issues/nothing-reads-whether-a-streams-tcp-nodelay-reached-netstack.md @@ -0,0 +1,31 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# Nothing reads whether a stream's TCP_NODELAY reached netstack + +netstack answers `MsgType::TcpSetOption` done or with an error, and nothing +answers what a stream holds: the pipe ABI has no option read +(`toyos/src/net.rs`), and std's `nodelay()` and libc's `getsockopt` answer the +value their own side stored. So a setter that stores the value and sends +nothing is seen by no test. Measured on libc's `connect`, which hands over a +`TCP_NODELAY` set before the connection existed: with the hand-over deleted, +the guest case that sets the option before `connect` and reads it after +(`tests/netcase/nodelay_kept.c`, the `libc_sockets` test) stays green. And on +its `accept`, which hands over the one its listener held at `bind`: with that +request deleted and the value still stored, the same case's read of the +accepted socket stays green. The +only effect of the option is when a small write leaves the machine, which a +QEMU test may not time. + +**Exit**: a test reads what the stack holds for a stream's Nagle switch, on +the node under a host test of its option call or through a guest's wire, and +turns red when libc's `connect` or `accept` or std's `set_nodelay` stores the +value and sends no request. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`, with +the reset-stream option test of +`issues/a-stream-its-peer-reset-refuses-the-option-requests-a-host-answers.md`, +which asks the same request. diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 8a5b0ede3b8..9e27c442df5 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -36,7 +36,7 @@ What the node does not yet meet: - A client that is gone with nothing left in its pipe is finished by [tcp]'s rules for a user who let go: a reset once it has been idle 60 s, where netd on smoltcp reset it 100 s after the client left whatever the peer said. A peer that acknowledges a byte a minute holds such a connection for as long as it has bytes to acknowledge. Exit: the T14 or a guest shows such a peer holding one, and [tcp]'s rule takes a bound from the client's leaving; or the owner rules the idle bound is the one. - `Tcp::ready`, `Tcp::orphans` and `Tcp::options` are additions to [tcp] the specifications do not hold, made for the node's wakes, its places and the copy a stream keeps of its connection's options; their tests (`toyos-net-shard/tcp/tests/held.rs`) name no scenario id. `Shard::listen` refuses an address [ip] does not hold usable, by the rule [udp]'s bind refuses by (`udp.bind-address-not-local`), and counts nothing for it; the node's test of it (`a_listener_is_at_the_address_it_named_and_only_one_the_machine_holds`) names no scenario either. A listener whose address the machine loses stands as a datagram socket bound to it does, with its port and its place and its owner told nothing, and answers again once the address is back (`a_listener_stands_while_its_address_is_lost_and_answers_when_it_is_back`, `userland/netstack/node/tests/lease.rs`); no host was read for it. Exit: the specifications hold the three calls and the refusal, and the tests name their scenarios. - The word each of [udp]'s refusals is answered in on the pipe ABI is `Refused` in `userland/netstack/node/src/datagram.rs`, tested by `each_refusal_is_answered_in_the_pipes_word_for_it` against no reader's scenario; `udp.no-ephemeral-port` has a word and no test that reaches it. Exit: the scenario owed below exists, and the test names its id. -- A client's datagram to a broadcast address is refused `udp.broadcast-not-permitted` and answered invalid input, where smoltcp sends it. The owner's ruling: "Of course carry the permission why would you even ask. The premise is and always has been to do things properly". The pipe ABI carries the permission as far as netd: `MsgType::UdpSetOption` with `OPT_BROADCAST` (`toyos/src/net.rs`), sent by std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) and by libc's `setsockopt` of `SO_BROADCAST` (`userland/libc/src/socket.rs`), which keeps it for a socket not yet bound and sends it at `bind`; the word for a send refused for want of it is `ERR_PERMISSION_DENIED`, `ErrorKind::PermissionDenied` in std and `EACCES` in libc. Nothing enforces it yet: netd on smoltcp answers the request done and sends to a broadcast address for every socket, and the node has no call that takes the permission and answers the refusal invalid input. The search is run. `grep -rnI -E 'SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255|sendto|setsockopt|sys/socket\.h|socket\(' userland/doom/doomgeneric`, the one third-party C program an image builds, at the commit `userland/doom/build.rs` pins, exits 1 with no line; the same without `sys/socket\.h` over `tests/testcases` exits 1 with no line; `SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255` over the trees of `src/libcxx.rs`'s `SOURCES` finds LLVM libc's own macro and its documentation and no caller; and in ToyOS's tree outside the stack's crates the option is named only by the two setters. The socket2 fork's setter reaches nobody: `issues/the-socket2-forks-so-broadcast-setter-does-nothing.md`. Exit, before the move, and the move does not land without it: a slice of its own on the node's datagram sockets gives the node a call that hands the permission to `Udp::set_broadcast` and gives `Refused` (`userland/netstack/node/src/datagram.rs`) the word for `ERR_PERMISSION_DENIED`, and in `each_refusal_is_answered_in_the_pipes_word_for_it` (`userland/netstack/node/tests/datagram.rs`) the two `BroadcastNotPermitted` sends turn from invalid input to that word and the same two sends leave once the socket holds the permission. Exit, after the move, which keeps this line open until both are green on netd on the node, since no test can read the refusal on smoltcp: a guest test, `udp_broadcast_needs_its_permission`, reads `broadcast()` false, then true after `set_broadcast(true)`, on the socket and on a `try_clone` of it, which `broadcast()` answering a constant or a duplicate holding a value of its own turns red, and sends to the limited broadcast address before and after the set and is refused `PermissionDenied` only before, which std's setter reverted to `Ok(())` turns red; and a guest C case reads libc's setter, `socket`, `setsockopt(SO_BROADCAST)`, `bind`, `sendto` to the limited broadcast address, refused `EACCES` without the option and sent with it, and with the option set before `bind`, which `bind` no longer handing the kept option over turns red. The sequence without `bind` waits on `issues/libc-sends-from-a-datagram-socket-it-never-bound.md`. +- A client's datagram to a broadcast address is refused `udp.broadcast-not-permitted` and answered invalid input, where smoltcp sends it. The owner's ruling: "Of course carry the permission why would you even ask. The premise is and always has been to do things properly". The pipe ABI carries the permission as far as netd: `MsgType::UdpSetOption` with `OPT_BROADCAST` (`toyos/src/net.rs`), sent by std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) and by libc's `setsockopt` of `SO_BROADCAST` (`userland/libc/src/socket.rs`), which keeps it for a socket not yet bound and sends it at `bind`; the word for a send refused for want of it is `ERR_PERMISSION_DENIED`, `ErrorKind::PermissionDenied` in std and `EACCES` in libc. Nothing enforces it yet: netd on smoltcp answers the request done and sends to a broadcast address for every socket, and the node has no call that takes the permission and answers the refusal invalid input. The search is run. `grep -rnI -E 'SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255|sendto|setsockopt|sys/socket\.h|socket\(' userland/doom/doomgeneric`, the one third-party C program an image builds, at the commit `userland/doom/build.rs` pins, exits 1 with no line; the same without `sys/socket\.h` over `tests/testcases` exits 1 with no line; `SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255` over the trees of `src/libcxx.rs`'s `SOURCES` finds LLVM libc's own macro and its documentation and no caller; and in ToyOS's tree outside the stack's crates the option is named only by the two setters. The socket2 fork's setter reaches nobody: `issues/the-socket2-forks-so-broadcast-setter-does-nothing.md`. Exit, before the move, and the move does not land without it: a slice of its own on the node's datagram sockets gives the node a call that hands the permission to `Udp::set_broadcast` and gives `Refused` (`userland/netstack/node/src/datagram.rs`) the word for `ERR_PERMISSION_DENIED`, and in `each_refusal_is_answered_in_the_pipes_word_for_it` (`userland/netstack/node/tests/datagram.rs`) the two `BroadcastNotPermitted` sends turn from invalid input to that word and the same two sends leave once the socket holds the permission. Exit, after the move, which keeps this line open until both are green on netd on the node, since no test can read the refusal on smoltcp: a guest test, `udp_broadcast_needs_its_permission`, reads `broadcast()` false, then true after `set_broadcast(true)`, on the socket and on a `try_clone` of it, which `broadcast()` answering a constant or a duplicate holding a value of its own turns red, and sends to the limited broadcast address before and after the set and is refused `PermissionDenied` only before, which std's setter reverted to `Ok(())` turns red; and a guest C case reads libc's setter, `socket`, `setsockopt(SO_BROADCAST)`, `bind`, `sendto` to the limited broadcast address, refused `EACCES` without the option and sent with it, and with the option set before `bind`, which `bind` no longer handing the kept option over turns red. The case runs the sequence without `bind` too, where the first `sendto` binds the socket and hands the kept option over; `tests/netcase/sendto_unbound.c` holds that send for a socket permitted to make it. - A refusal [udp] logs against a client's call waits in the stack until the node's next `receive`, `fire` or `link`, because only those pass over the stack's reports. Exit, at the move and not after it: the datagram calls end in that pass, or netd drains it once per turn of its loop. - The machine's name is told to the node twice, as `toyos_dhcp::HostName` in `Node::new` and as `toyos_mdns::Host` in `Node::answer_as`: the first owns its bytes and the second borrows them. Exit: one type carries the label to both; due at the move if handing `answer_as` its `&'static str` takes a leak. - The node's name is not probed for or defended (RFC 6762 §8.1, §9), as `toyos-mdns` records of itself. It is not announced again when the link comes back, which §8.3 asks and netd does today, where link-up resets its DHCP client and the address is new again: here the lease outlives the link. Exit: `toyos-mdns` does both, on the node's `link`. @@ -45,7 +45,7 @@ What the node does not yet meet: - `toyos_dns::Lookup::on_datagram` still takes a reply's source address and port, which the node's connected sockets have already matched and the node hands it from its own record of the query: the two parameters are read only by netd's resolver on smoltcp. Exit, at the move: they go with that resolver. - `node.address-refused` has no test: `toyos-dhcp` accepts no address or prefix [ip] refuses, by the same `toyos-net-wire` checks in both, so the refusal cannot be reached from the wire. Exit: the client hands [ip] a type that carries the check, and the counter goes. -The listener defects are this track's: `issues/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`, in std's accept. +The listener defects are this track's: `issues/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`, in std's accept; and `issues/a-listening-sockets-tcp-nodelay-is-refused-and-an-accepted-streams-is-answered-from-a-guess.md`, whose exit waits on the pipe ABI giving a listener its option, which the node has (`Node::set_listener_nodelay`) and nothing ships. Owed from the stage 3 specifications: by stage 5, the scenario for netd's mapping of UDP's refusals onto the pipe ABI, and `dhcp.renew-unroutable`, which netd counts where `toyos-net-udp` refuses the renewal `udp.no-route`; by IP hardening, the scenarios for fragment reassembly and path MTU discovery. diff --git a/issues/virt-mask-windows-went-quiet-on-a-loaded-host.md b/issues/virt-mask-windows-went-quiet-on-a-loaded-host.md deleted file mode 100644 index f4184617a12..00000000000 --- a/issues/virt-mask-windows-went-quiet-on-a-loaded-host.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-04 ---- - -# `virt_mask_windows` went quiet inside a whole suite on a loaded host - -In the whole guest suite at `wt/toyos-acpi1`'s `b9430ed61`, `virt_mask_windows` -(AArch64, the `mask-windows` kernel, `tests/virtsmpcase`, eight vCPUs under -TCG) ended `STALLED: waiting for the boot's last word — it went quiet` after -45 s. That run's workers spent 19038 s building against 1375 s testing, and -the host's load average read 82.65 / 83.62 / 77.89 moments after it, on 14 -cores. The same test alone at that load, at the same commit, passed in 14 s. - -Nothing in that branch runs on that boot: its AArch64 changes are stubs no -path reaches, and `tests/virtsmpcase` starts no ACPI server. Owner: none yet. - -`main` at `d47b383cf`, `cargo test -- virt_mask_windows`, three runs, each -EXIT=0: alone at load 45 rising to 72 (PASS, 10 s); beside the branch's own -whole guest suite at load 74 to 77 (PASS, 38 s); and again beside it at load -75 to 67 (PASS, 7 s). The 38 s run printed nothing between its image built -and its first guest line for 31 s, and paid its liveness ceilings at 8.00x -(the others 3.99x and 2.62x). Not reproduced on `main`, and not ruled out -there. - -**Exit**: `virt_mask_windows` run at least as often as it took to see this, -alongside a load like that one, on `main`, reads no stall; or the stall's -cause, named and fixed. diff --git a/tests/common/compile.rs b/tests/common/compile.rs index b573d22212b..d51e6f181e1 100644 --- a/tests/common/compile.rs +++ b/tests/common/compile.rs @@ -49,25 +49,26 @@ fn first_error(stderr: &[u8]) -> String { .to_string() } -/// Compile one of the corpus's files to an object with the toolchain's clang, -/// from the corpus's own directory and by its bare name, as TinyCC's runner -/// does: `__FILE__` is part of what some cases print. -fn compile_one(c: &CSysroot, source: &str, object: &Path) { +/// Compile `source` in `dir` to an object with the toolchain's clang, from +/// that directory and by its bare name. +fn compile_one(c: &CSysroot, dir: &Path, flags: &[&str], source: &str, object: &Path) { let output = Command::new(&c.clang) .args(c.args()) - .args(CORPUS_FLAGS) + .args(flags) .arg("-c") .arg(source) .arg("-o") .arg(object) - .current_dir(testcases_dir()) + .current_dir(dir) .output() .unwrap_or_else(|e| panic!("run {}: {e}", c.clang.display())); assert!(output.status.success(), "{}", first_error(&output.stderr)); } /// Compile a corpus case, and its companion if it has one (`104+_inline.c` -/// for `104_inline`), and return the objects. +/// for `104_inline`), and return the objects. Each is compiled as TinyCC's +/// runner does, in the corpus's directory by its bare name: `__FILE__` is +/// part of what some cases print. pub fn compile_c(name: &str) -> Vec { let c = c_sysroot(); let mut sources = vec![format!("{name}.c")]; @@ -82,12 +83,20 @@ pub fn compile_c(name: &str) -> Vec { .enumerate() .map(|(i, source)| { let object = scratch(name, &format!("-{i}.o")); - compile_one(&c, source, &object); + compile_one(&c, &testcases_dir(), CORPUS_FLAGS, source, &object); object }) .collect() } +/// Compile `name.c` in `dir`, a C program of ToyOS's own outside the corpus, +/// with every warning an error, and return its object. +pub fn compile_own_c(dir: &Path, name: &str) -> Vec { + let object = scratch(name, ".o"); + compile_one(&c_sysroot(), dir, &["-Wall", "-Wextra", "-Werror"], &format!("{name}.c"), &object); + vec![object] +} + /// Link flags a case needs beyond the corpus's, by case name. /// /// An image whose lowest address is 0 hides a table reported from the load bias diff --git a/tests/libc-arch/src/internet_addresses.rs b/tests/libc-arch/src/internet_addresses.rs new file mode 100644 index 00000000000..15cc3988636 --- /dev/null +++ b/tests/libc-arch/src/internet_addresses.rs @@ -0,0 +1,281 @@ +//! libc's IPv4 addresses (`inaddr.rs`) against the host C library's: the +//! texts `inet_pton` and `inet_addr` read, the text `inet_ntop` writes into a +//! buffer of every size, both calls' refusal of another family, a +//! `sockaddr_in`'s bytes against the host's `htonl` and `htons`, and the +//! bytes a buffer of every length is answered against the host's +//! `getsockname`. Where the hosts' own readers part from POSIX's text or from +//! each other, each case says which. + +use std::ffi::{c_char, c_int, c_void, CStr, CString}; +use std::net::UdpSocket; +use std::os::fd::AsRawFd; +use std::ptr; + +use crate::inaddr::{ntop, numbers_and_dots, pton, Refusal, SockaddrIn, AF_INET}; +use crate::socket_options::host_errno; + +unsafe extern "C" { + fn inet_pton(af: c_int, src: *const c_char, dst: *mut c_void) -> c_int; + fn inet_ntop(af: c_int, src: *const c_void, dst: *mut c_char, size: u32) -> *const c_char; + fn inet_addr(cp: *const c_char) -> u32; + fn htonl(hostlong: u32) -> u32; + fn htons(hostshort: u16) -> u16; + fn getsockname(fd: c_int, addr: *mut u8, len: *mut u32) -> c_int; +} + +/// The host's `EAFNOSUPPORT`; `ENOSPC` macOS and Linux number alike. +const EAFNOSUPPORT: c_int = if cfg!(target_os = "macos") { 47 } else { 97 }; +const ENOSPC: c_int = 28; +/// What a buffer holds before a call writes it. +const UNWRITTEN: u8 = 0xAA; + +/// Every text of `min..=max` of `parts` between dots, each followed by each +/// of `ends`. +fn texts(parts: &[&str], counts: std::ops::RangeInclusive, ends: &[&str], mut each: impl FnMut(&str)) { + for count in counts { + for pick in 0..parts.len().pow(count as u32) { + let chosen: Vec<&str> = (0..count).map(|i| parts[pick / parts.len().pow(i as u32) % parts.len()]).collect(); + for end in ends { + each(&format!("{}{end}", chosen.join("."))); + } + } + } +} + +/// libc's `inet_pton` of an `AF_INET` text. +fn ours_pton(text: &str) -> Option<[u8; 4]> { + pton(AF_INET, text.as_bytes()).expect("AF_INET is libc's family") +} + +/// The host's `inet_pton` of `text`. +fn host_pton(text: &str) -> Option<[u8; 4]> { + let c = CString::new(text).unwrap(); + let mut octets = [0u8; 4]; + // SAFETY: a NUL-terminated string, and four bytes to write. + let answer = unsafe { inet_pton(AF_INET, c.as_ptr(), octets.as_mut_ptr().cast()) }; + assert!(answer == 0 || answer == 1, "the host's inet_pton({text:?}) answered {answer}"); + (answer == 1).then_some(octets) +} + +/// The host's `inet_addr` of `text`: its answer's bytes in memory order. +fn host_addr(text: &str) -> [u8; 4] { + let c = CString::new(text).unwrap(); + // SAFETY: a NUL-terminated string. + unsafe { inet_addr(c.as_ptr()) }.to_ne_bytes() +} + +/// libc's `inet_addr`, which answers `INADDR_NONE` for no address. +fn ours_addr(text: &str) -> [u8; 4] { + numbers_and_dots(text.as_bytes()).unwrap_or([0xff; 4]) +} + +const OCTETS: [&str; 10] = ["0", "1", "9", "10", "99", "100", "199", "200", "249", "255"]; + +/// What Darwin's `inet_pton` reads: a number of any count of digits, zeros in +/// front among them, where libc and glibc refuse a zero before another digit +/// and a fourth digit. +fn darwin_reads(text: &str) -> Option<[u8; 4]> { + let parts: Vec<&str> = text + .split('.') + .map(|part| match part.trim_start_matches('0') { + _ if part.is_empty() || !part.bytes().all(|c| c.is_ascii_digit()) => part, + "" => "0", + rest => rest, + }) + .collect(); + ours_pton(&parts.join(".")) +} + +#[test] +fn a_dotted_quad_reads_as_the_host_reads_it() { + let mut parts = OCTETS.to_vec(); + parts.extend(["256", "300", "999", "1000", "", "a", " ", "1 ", " 1", "-1", "+1", "1x", "0x1"]); + parts.extend(["00", "01", "010", "0255", "0001"]); + let (mut read, mut zero_led) = (0, 0); + let mut judge = |text: &str| { + let ours = ours_pton(text); + let darwin = darwin_reads(text); + let want = if cfg!(target_os = "macos") { darwin } else { ours }; + assert_eq!(host_pton(text), want, "inet_pton({text:?}): libc reads {ours:?}"); + read += usize::from(ours.is_some()); + zero_led += usize::from(ours != darwin); + }; + texts(&parts, 1..=4, &[""], &mut judge); + texts(&parts[..12], 5..=5, &[""], &mut judge); + texts(&OCTETS, 4..=4, &[".", " ", "\n", "x"], &mut judge); + assert_eq!(read, OCTETS.len().pow(4), "the texts that are addresses"); + assert!(zero_led > 10_000, "{zero_led} texts were addresses but for a zero in front"); +} + +/// No text has two values: `inet_addr` reads `010` as octal, so `inet_pton` +/// refuses it. +#[test] +fn a_number_with_a_zero_before_another_digit_is_no_dotted_quad() { + assert_eq!(ours_pton("010.0.0.1"), None); + assert_eq!(numbers_and_dots(b"010.0.0.1"), Some([8, 0, 0, 1])); + assert_eq!(ours_pton("10.0.0.00"), None); + assert_eq!(ours_pton("10.0.0.0"), Some([10, 0, 0, 0])); +} + +/// A family neither libc nor the host has an address text for. The host's +/// own second family, IPv6, libc refuses the same way and is not asked. +#[test] +fn another_family_is_refused_as_the_host_refuses_it() { + for af in [0, 1, -1, 12345] { + assert_eq!(pton(af, b"192.0.2.1"), Err(Refusal::Family), "inet_pton, family {af}"); + let mut octets = [UNWRITTEN; 4]; + // SAFETY: a NUL-terminated string, and four bytes to write. + let host = unsafe { inet_pton(af, c"192.0.2.1".as_ptr(), octets.as_mut_ptr().cast()) }; + assert_eq!((host, host_errno(), octets), (-1, EAFNOSUPPORT, [UNWRITTEN; 4]), "the host's inet_pton, family {af}"); + + let (ip, mut ours, mut theirs) = ([192u8, 0, 2, 1], [UNWRITTEN; 16], [UNWRITTEN; 16]); + // SAFETY: four bytes to read and sixteen to write. + let answer = unsafe { ntop(af, ip.as_ptr(), ours.as_mut_ptr(), 16) }; + assert_eq!(answer, Err(Refusal::Family), "inet_ntop, family {af}"); + // SAFETY: as above. + let host = unsafe { inet_ntop(af, ip.as_ptr().cast(), theirs.as_mut_ptr().cast(), 16) }; + assert_eq!((host, host_errno()), (ptr::null(), EAFNOSUPPORT), "the host's inet_ntop, family {af}"); + assert_eq!(ours, theirs, "family {af}: nothing is written"); + } +} + +#[test] +fn numbers_and_dots_read_as_the_host_reads_them() { + let parts = [ + "0", "1", "9", "255", "256", "65535", "65536", "16777215", "16777216", "4294967295", "0x0", "0xff", "0x100", + "0xFFFF", "0X10000", "0xffffff", "0x1000000", "0xffffffff", "0xg", "010", "0377", "0400", "08", "00", "", "a", + "-1", "+1", "1x", + ]; + let mut read = 0; + let mut judge = |text: &str| { + assert_eq!(ours_addr(text), host_addr(text), "inet_addr({text:?})"); + read += usize::from(numbers_and_dots(text.as_bytes()).is_some()); + }; + texts(&parts, 1..=3, &["", " ", "\n", "\t9", " x", "x", "."], &mut judge); + texts(&parts, 4..=4, &["", " x"], &mut judge); + texts(&parts[..6], 5..=5, &[""], &mut judge); + judge(" 192.0.2.1"); + judge("192.0.2.1\x0b"); + judge("192.0.2.1\x0c\r"); + assert!(read > 10_000, "{read} texts were addresses"); +} + +/// Where BSD's reader parts from POSIX's text, "a 32-bit value" in ISO C's +/// integer forms: it wraps a number past 32 bits and takes `0x` with no +/// digit for zero. +#[test] +fn a_number_past_32_bits_or_without_digits_is_no_address() { + assert_eq!(numbers_and_dots(b"4294967296"), None); + assert_eq!(numbers_and_dots(b"0x100000000"), None); + assert_eq!(numbers_and_dots(b"0x.1"), None); + assert_eq!(numbers_and_dots(b"1.0x"), None); +} + +/// `inet_ntop` of `ip` into `size` bytes of a buffer, by libc and by the +/// host: libc's answer and its buffer, then the host's `errno` where it +/// answered none and its buffer. +fn written(ip: [u8; 4], size: u32) -> (Result<(), Refusal>, [u8; 32], Option, [u8; 32]) { + let (mut ours, mut theirs) = ([UNWRITTEN; 32], [UNWRITTEN; 32]); + // SAFETY: four bytes to read, and `size`, at most 32, to write. + let answer = unsafe { ntop(AF_INET, ip.as_ptr(), ours.as_mut_ptr(), size) }; + // SAFETY: as above. + let host = unsafe { inet_ntop(AF_INET, ip.as_ptr().cast(), theirs.as_mut_ptr().cast(), size) }; + assert!(host.is_null() || host == theirs.as_ptr().cast(), "the host answers its buffer"); + (answer, ours, host.is_null().then(host_errno), theirs) +} + +#[test] +fn an_address_is_written_as_the_host_writes_it() { + for position in 0..4 { + for octet in 0..=255u8 { + let mut ip = [7, 77, 177, 0]; + ip[position] = octet; + let (answer, ours, refused, host) = written(ip, 16); + assert_eq!((answer, refused), (Ok(()), None), "{ip:?}"); + assert_eq!(ours, host, "{ip:?}"); + assert!(CStr::from_bytes_until_nul(&ours).is_ok(), "{ip:?}: the text ends in a NUL"); + } + } +} + +/// One address for each length a text has, 7 to 15, into a buffer of every +/// size from none to one past `INET_ADDRSTRLEN`: the text fits where its NUL +/// does, and a buffer it does not fit is left as it was. +#[test] +fn a_buffer_too_short_for_the_text_is_refused_as_the_host_refuses_it() { + let addresses: [[u8; 4]; 9] = [ + [0, 0, 0, 0], + [10, 0, 0, 0], + [10, 10, 0, 0], + [10, 10, 10, 0], + [10, 10, 10, 10], + [100, 10, 10, 10], + [100, 100, 10, 10], + [100, 100, 100, 10], + [255, 255, 255, 255], + ]; + for (ip, text_len) in addresses.into_iter().zip(7u32..) { + for size in 0..=17 { + let (answer, ours, refused, host) = written(ip, size); + let fits = size > text_len; + assert_eq!(answer, if fits { Ok(()) } else { Err(Refusal::Room) }, "{ip:?} into {size}"); + assert_eq!(refused, (!fits).then_some(ENOSPC), "the host's inet_ntop of {ip:?} into {size}"); + assert_eq!(ours, host, "{ip:?} into {size}"); + assert!(ours[size as usize..].iter().all(|&byte| byte == UNWRITTEN), "{ip:?}: a byte past {size}"); + } + } +} + +/// `sockaddr_in` as Linux and `include/netinet/in.h` lay it out: the family, +/// then the port's two bytes and the address's four in network order. +#[test] +fn a_sockaddr_holds_its_port_and_address_in_network_order() { + assert_eq!((size_of::(), align_of::()), (16, 4)); + // SAFETY: sixteen bytes with no padding, read as bytes. + let bytes: [u8; 16] = unsafe { std::mem::transmute(SockaddrIn::new([10, 0, 2, 2], 0x1234)) }; + let family = (AF_INET as u16).to_ne_bytes(); + assert_eq!(bytes, [family[0], family[1], 0x12, 0x34, 10, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0]); + + // What a C program writes: `sin_port = htons(port)` and + // `sin_addr.s_addr = htonl(address)`, each stored as the machine stores it. + let mut written = [0u8; 16]; + written[..2].copy_from_slice(&family); + // SAFETY: the host's byte swaps. + let (port, address) = unsafe { (htons(0x1234), htonl(0x0a00_0202)) }; + written[2..4].copy_from_slice(&port.to_ne_bytes()); + written[4..8].copy_from_slice(&address.to_ne_bytes()); + // SAFETY: every sixteen bytes are a `SockaddrIn`. + let read: SockaddrIn = unsafe { std::mem::transmute(written) }; + assert_eq!(read.endpoint(), Some(([10, 0, 2, 2], 0x1234))); + + written[..2].copy_from_slice(&1u16.to_ne_bytes()); + // SAFETY: as above. + let unix: SockaddrIn = unsafe { std::mem::transmute(written) }; + assert_eq!(unix.endpoint(), None, "AF_UNIX's is no IPv4 address"); +} + +/// A bound socket's address into a buffer of every length to twice its own, +/// by `SockaddrIn::answer` and by the host's `getsockname`: no byte past the +/// length handed in, and the address's own length written back. The first two +/// bytes are compared on Linux, whose layout libc's is; Darwin's are a length +/// and a one-byte family. +#[test] +fn an_address_is_truncated_to_the_callers_buffer_as_the_host_truncates_it() { + let socket = UdpSocket::bind("127.0.0.1:0").unwrap(); + let address = SockaddrIn::new([127, 0, 0, 1], socket.local_addr().unwrap().port()); + for handed in 0..=32u32 { + let (mut ours, mut theirs) = ([UNWRITTEN; 32], [UNWRITTEN; 32]); + let (mut our_len, mut their_len) = (handed, handed); + // SAFETY: a length, and that many of the buffer's 32 bytes. + unsafe { address.answer(ours.as_mut_ptr(), &mut our_len) }; + // SAFETY: as above. + let host = unsafe { getsockname(socket.as_raw_fd(), theirs.as_mut_ptr(), &mut their_len) }; + assert_eq!((host, their_len), (0, our_len), "getsockname into {handed}"); + assert_eq!(our_len, 16, "into {handed}: the address's own length"); + let layout = if cfg!(target_os = "linux") { 0 } else { 2 }; + assert_eq!(ours[layout..], theirs[layout..], "into {handed}"); + let kept = (handed as usize).min(16); + assert!(ours[kept..].iter().all(|&byte| byte == UNWRITTEN), "into {handed}: a byte past the buffer"); + assert!(theirs[kept..].iter().all(|&byte| byte == UNWRITTEN), "the host, into {handed}"); + } +} diff --git a/tests/libc-arch/src/lib.rs b/tests/libc-arch/src/lib.rs index b3c608dd3a1..a6fa47e5dfd 100644 --- a/tests/libc-arch/src/lib.rs +++ b/tests/libc-arch/src/lib.rs @@ -7,8 +7,9 @@ //! `core::str::from_utf8`, the number reader against the host C library's and //! IEEE 754's rounding of hexadecimal input computed exactly, AArch64's //! `long double` widening against compiler-builtins', the errno codes against -//! `include/errno.h`, and the socket option rule against the host's -//! `setsockopt` and `getsockopt`. +//! `include/errno.h`, IPv4 addresses and their texts against the host C +//! library's, and the socket option rule against the host's `setsockopt` and +//! `getsockopt`. #[cfg(test)] extern crate alloc; @@ -26,6 +27,9 @@ mod fdreq; #[path = "../../../userland/libc/src/fparts.rs"] mod fparts; #[cfg(test)] +#[path = "../../../userland/libc/src/inaddr.rs"] +mod inaddr; +#[cfg(test)] #[path = "../../../userland/libc/src/linkreq.rs"] mod linkreq; #[cfg(test)] @@ -66,6 +70,8 @@ mod fparts_differential; #[cfg(test)] mod header; #[cfg(test)] +mod internet_addresses; +#[cfg(test)] mod link_requests; #[cfg(test)] mod listing_reader; diff --git a/tests/libc-arch/src/socket_options.rs b/tests/libc-arch/src/socket_options.rs index 2ac86db4206..04009e70767 100644 --- a/tests/libc-arch/src/socket_options.rs +++ b/tests/libc-arch/src/socket_options.rs @@ -1,7 +1,8 @@ //! libc's socket option rule against the host's own `setsockopt` and -//! `getsockopt`, one table through both: `SO_BROADCAST` on a datagram socket -//! and `TCP_NODELAY` on a stream socket, a value and a buffer of 8, 4, 2, 1 -//! and 0 bytes and a null one of each length, and a null length. Compared are +//! `getsockopt`, one table through both: `SO_BROADCAST` on a datagram socket, +//! `TCP_NODELAY` on a stream socket and `TCP_NODELAY` on a datagram socket, +//! which has no such option, a value and a buffer of 8, 4, 2, 1 and 0 bytes +//! and a null one of each length, and a null length. Compared are //! the return and its `errno`, the length written back, which bytes of the //! buffer were written, and whether what they hold is non-zero: a host reads a //! set option back as any non-zero `int`, Darwin as the option's flag bit. @@ -18,7 +19,7 @@ use crate::header; use crate::sockopt::{self, Kept, Refusal}; const SOCKET_H: &str = include_str!("../../../userland/libc/include/sys/socket.h"); -const IN_H: &str = include_str!("../../../userland/libc/include/netinet/in.h"); +const TCP_H: &str = include_str!("../../../userland/libc/include/netinet/tcp.h"); unsafe extern "C" { fn setsockopt(fd: c_int, level: c_int, name: c_int, value: *const u8, len: u32) -> c_int; @@ -31,6 +32,9 @@ unsafe extern "C" { /// The host's `EFAULT` and `EINVAL`, which macOS and Linux number alike. const EFAULT: c_int = 14; const EINVAL: c_int = 22; +/// Linux's `ENOPROTOOPT` and `EOPNOTSUPP`: Darwin answers neither here. +const ENOPROTOOPT: c_int = 92; +const EOPNOTSUPP: c_int = 95; const LENGTHS: [u32; 5] = [8, 4, 2, 1, 0]; /// What a buffer holds before a read, which no answer's byte is. @@ -40,7 +44,7 @@ const UNWRITTEN: u8 = 0xAA; fn libc_numbers(option: Kept) -> (c_int, c_int) { match option { Kept::Broadcast => (header::int(SOCKET_H, "SOL_SOCKET"), header::int(SOCKET_H, "SO_BROADCAST")), - Kept::NoDelay => (header::int(SOCKET_H, "IPPROTO_TCP"), header::int(IN_H, "TCP_NODELAY")), + Kept::NoDelay => (header::int(SOCKET_H, "IPPROTO_TCP"), header::int(TCP_H, "TCP_NODELAY")), } } @@ -72,12 +76,15 @@ fn refused(refusal: Refusal) -> Answer { Answer::Refused(match refusal { Refusal::Short => EINVAL, Refusal::Fault => EFAULT, + Refusal::NoSuchOption => ENOPROTOOPT, + Refusal::NotSupported => EOPNOTSUPP, }) } -fn host_refusal() -> Answer { +/// The host's `errno`, as its last call left it. +pub(crate) fn host_errno() -> c_int { // SAFETY: the host's errno slot is this thread's. - Answer::Refused(unsafe { *errno_location() }) + unsafe { *errno_location() } } fn read(buf: &[u8; 8], len: u32) -> Answer { @@ -89,61 +96,85 @@ fn read(buf: &[u8; 8], len: u32) -> Answer { struct Pair { option: Kept, fd: c_int, + datagram: bool, kept: bool, } impl Pair { + /// A TCP option asked of a datagram socket, which has none. + fn foreign(&self) -> bool { + self.datagram && self.option == Kept::NoDelay + } + + /// The option by libc's rule, asked of this socket by a setter or a getter. + fn named(&self, setter: bool) -> Result<(), Answer> { + let (level, name) = libc_numbers(self.option); + let named = sockopt::kept(level, name, self.datagram, setter).map_err(refused)?; + assert_eq!(named, Some(self.option)); + Ok(()) + } + /// A setter handed `len` bytes at `value`, by libc's rule and by the host. fn set(&mut self, value: *const u8, len: u32) -> (Answer, Answer) { - let (level, name) = libc_numbers(self.option); - assert_eq!(sockopt::kept(level, name), Some(self.option)); // SAFETY: `value` is null or eight readable bytes, and `len` at most 8. - let ours = match unsafe { sockopt::switch(value, len) } { - Ok(on) => { + let ours = match self.named(true).map(|()| unsafe { sockopt::switch(value, len) }) { + Ok(Ok(on)) => { self.kept = on; Answer::Set } - Err(refusal) => refused(refusal), + Ok(Err(refusal)) => refused(refusal), + Err(refused) => refused, }; let (level, name) = host_numbers(self.option); // SAFETY: as above. let host = match unsafe { setsockopt(self.fd, level, name, value, len) } { 0 => Answer::Set, - _ => host_refusal(), + _ => Answer::Refused(host_errno()), }; (ours, host) } - /// A getter handed `len` bytes at `value`, null when `null`, by libc's rule - /// and by the host. + /// A getter handed `value` and `len`, by libc's rule and by the host. + fn answered(&self, value: *mut u8, len: *mut u32) -> (Option, Option) { + // SAFETY: the caller's. + let ours = self.named(false).and_then(|()| unsafe { sockopt::answer(self.kept as i32, value, len) }.map_err(refused)); + let (level, name) = host_numbers(self.option); + // SAFETY: the caller's. + let host = (unsafe { getsockopt(self.fd, level, name, value, len) } != 0).then(|| Answer::Refused(host_errno())); + (ours.err(), host) + } + + /// A getter handed `len` bytes at a buffer, null when `null`, by libc's + /// rule and by the host. fn get(&self, null: bool, len: u32) -> (Answer, Answer) { - let call = |get: &dyn Fn(*mut u8, *mut u32) -> Option| { + let call = |ours: bool| { let (mut buf, mut len) = ([UNWRITTEN; 8], len); let value = if null { ptr::null_mut() } else { buf.as_mut_ptr() }; - get(value, &mut len).unwrap_or_else(|| read(&buf, len)) + // The buffer is null or eight writable bytes, and `len` at most 8. + let (our_refusal, host_refusal) = self.answered(value, &mut len); + (if ours { our_refusal } else { host_refusal }).unwrap_or_else(|| read(&buf, len)) }; - // SAFETY: the buffer is null or eight writable bytes, and `len` at most 8. - let ours = call(&|value, len| unsafe { sockopt::answer(self.kept as i32, value, len) }.err().map(refused)); - let (level, name) = host_numbers(self.option); - // SAFETY: as above. - let host = call(&|value, len| (unsafe { getsockopt(self.fd, level, name, value, len) } != 0).then(host_refusal)); - (ours, host) + (call(true), call(false)) } } -/// What Darwin answers where it is not what Linux and libc do. It judges a -/// setter's pointer before its length unless the length is 0, and a getter's -/// null buffer holds no byte, which it reports as a read of length 0. -fn darwin(setter: bool, null: bool, len: u32) -> Option { - match (setter, null, len) { - (true, true, 1 | 2) => Some(Answer::Refused(EFAULT)), - (false, true, 1..) => Some(Answer::Read { len: 0, written: [false; 8], nonzero: false }), +/// What Darwin answers where it is not what Linux and libc do. A TCP option +/// asked of a datagram socket it calls invalid, once it has read a setter's +/// value. Of an option the socket has, it judges a setter's pointer before its +/// length unless the length is 0, and a getter's null buffer holds no byte, +/// which it reports as a read of length 0. +fn darwin(foreign: bool, setter: bool, null: bool, len: u32) -> Option { + match (foreign, setter, null, len) { + (true, true, true, 1..) => Some(Answer::Refused(EFAULT)), + (true, ..) => Some(Answer::Refused(EINVAL)), + (false, true, true, 1 | 2) => Some(Answer::Refused(EFAULT)), + (false, false, true, 1..) => Some(Answer::Read { len: 0, written: [false; 8], nonzero: false }), _ => None, } } -fn hold((ours, host): (Answer, Answer), setter: bool, null: bool, len: u32, row: &str) { - let expected = match darwin(setter, null, len) { +fn hold((ours, host): (Answer, Answer), theirs: Option, row: &str) { + let expected = match theirs { Some(theirs) if cfg!(target_os = "macos") => theirs, _ => ours, }; @@ -154,31 +185,47 @@ fn hold((ours, host): (Answer, Answer), setter: bool, null: bool, len: u32, row: fn an_options_value_crosses_as_the_hosts_does() { let datagram = UdpSocket::bind("127.0.0.1:0").unwrap(); let stream = TcpListener::bind("127.0.0.1:0").unwrap(); - for (option, fd) in [(Kept::Broadcast, datagram.as_raw_fd()), (Kept::NoDelay, stream.as_raw_fd())] { - let mut pair = Pair { option, fd, kept: false }; + let rows = [ + (Kept::Broadcast, datagram.as_raw_fd(), true), + (Kept::NoDelay, stream.as_raw_fd(), false), + (Kept::NoDelay, datagram.as_raw_fd(), true), + ]; + for (option, fd, is_datagram) in rows { + let mut pair = Pair { option, fd, datagram: is_datagram, kept: false }; + let foreign = pair.foreign(); + let of = if is_datagram { "a datagram socket" } else { "a stream" }; for on in [1u8, 0, 1] { let value = [on, 0, 0, 0, 0, 0, 0, 0]; for len in LENGTHS { for null in [false, true] { - let row = format!("{option:?} {on}, {len} bytes, null {null}"); + let row = format!("{option:?} of {of} {on}, {len} bytes, null {null}"); let at = if null { ptr::null() } else { value.as_ptr() }; - hold(pair.set(at, len), true, null, len, &format!("set {row}")); - hold(pair.get(null, len), false, null, len, &format!("get {row}")); + let set = pair.set(at, len); + hold(set, darwin(foreign, true, null, len), &format!("set {row}")); + let got = pair.get(null, len); + hold(got, darwin(foreign, false, null, len), &format!("get {row}")); + if foreign { + assert_eq!(set.0, refused(Refusal::NoSuchOption), "set {row}"); + assert_eq!(got.0, refused(Refusal::NotSupported), "get {row}"); + } } } + if foreign { + continue; + } assert_eq!(pair.kept, on != 0, "{option:?}: the value of four bytes or more was kept"); let (ours, host) = pair.get(false, 4); assert_eq!(ours, Answer::Read { len: 4, written: [true, true, true, true, false, false, false, false], nonzero: on != 0 }); assert_eq!(host, ours); } - let (level, name) = host_numbers(option); let mut buf = [UNWRITTEN; 8]; // SAFETY: a null length is the refusal asked for; the buffer is eight bytes. - let host = unsafe { getsockopt(fd, level, name, buf.as_mut_ptr(), ptr::null_mut()) }; - assert_eq!((host, host_refusal()), (-1, Answer::Refused(EFAULT)), "{option:?}: a null length"); - // SAFETY: as above. - let ours = unsafe { sockopt::answer(1, buf.as_mut_ptr(), ptr::null_mut()) }; - assert_eq!((ours, buf), (Err(Refusal::Fault), [UNWRITTEN; 8])); + let (ours, host) = pair.answered(buf.as_mut_ptr(), ptr::null_mut()); + let want = if foreign { Refusal::NotSupported } else { Refusal::Fault }; + assert_eq!((ours, buf), (Some(refused(want)), [UNWRITTEN; 8]), "{option:?} of {of}: a null length"); + // Darwin reads the length first, whichever option is asked. + let theirs = foreign.then_some(Answer::Refused(EFAULT)); + hold((refused(want), host.expect("the host refuses a null length")), theirs, &format!("{option:?} of {of}: a null length")); } } diff --git a/tests/netcase/addr_order.c b/tests/netcase/addr_order.c new file mode 100644 index 00000000000..7276c94e843 --- /dev/null +++ b/tests/netcase/addr_order.c @@ -0,0 +1,101 @@ +/* An IPv4 address is in network byte order wherever a C program hands libc + one or is handed one: connect reaches the same host for an address built + with inet_pton, with htonl, with inet_addr and by getaddrinfo, and + getpeername answers it. argv: the address, as four decimal octets, and the + port of a host that accepts and holds each connection. */ +#include +#include +#include +#include +#include +#include +#include + +static int wrong; + +static void said(const char *what, long got, long want) { + printf("%s: %ld%s\n", what, got, got == want ? "" : " <-- WRONG"); + if (got != want) wrong++; +} + +/* connect a fresh stream socket to `addr` at `port`; the socket, or -1. */ +static int dial(struct in_addr addr, in_port_t port) { + struct sockaddr_in to; + memset(&to, 0, sizeof to); + to.sin_family = AF_INET; + to.sin_port = port; + to.sin_addr = addr; + int fd = socket(AF_INET, SOCK_STREAM, 0); + return connect(fd, (struct sockaddr *)&to, sizeof to) == 0 ? fd : -1; +} + +static void text_of(const char *what, in_addr_t host_order, const char *want) { + struct in_addr a; + char text[INET_ADDRSTRLEN]; + a.s_addr = htonl(host_order); + const char *got = inet_ntop(AF_INET, &a, text, sizeof text); + printf("%s: %s%s\n", what, got ? got : "(null)", got && strcmp(got, want) == 0 ? "" : " <-- WRONG"); + if (!got || strcmp(got, want) != 0) wrong++; +} + +int main(int argc, char **argv) { + if (argc != 3) return 2; + in_port_t port = htons((uint16_t)atoi(argv[2])); + unsigned char octets[4], in_memory[4]; + struct in_addr by_pton, by_htonl, by_inet_addr; + char text[INET_ADDRSTRLEN]; + + /* The octets, read from the text by nothing of the network's. */ + char *at = argv[1]; + for (int i = 0; i < 4; i++) { + octets[i] = (unsigned char)strtoul(at, &at, 10); + at++; + } + in_addr_t host_order = (in_addr_t)octets[0] << 24 | (in_addr_t)octets[1] << 16 | (in_addr_t)octets[2] << 8 | octets[3]; + + said("inet_pton reads the address", inet_pton(AF_INET, argv[1], &by_pton), 1); + memcpy(in_memory, &by_pton, 4); + said("into its octets in memory order", memcmp(in_memory, octets, 4) == 0, 1); + by_htonl.s_addr = htonl(host_order); + said("htonl builds the same four bytes", memcmp(&by_htonl, octets, 4) == 0, 1); + by_inet_addr.s_addr = inet_addr(argv[1]); + said("and so does inet_addr", memcmp(&by_inet_addr, octets, 4) == 0, 1); + said("ntohl gives the number back", ntohl(by_pton.s_addr) == host_order, 1); + + int a = dial(by_pton, port); + said("connect to inet_pton's address", a >= 0, 1); + said("connect to htonl's", dial(by_htonl, port) >= 0, 1); + said("connect to inet_addr's", dial(by_inet_addr, port) >= 0, 1); + + struct sockaddr_in name; + socklen_t len = sizeof name; + memset(&name, 0, sizeof name); + said("getpeername", getpeername(a, (struct sockaddr *)&name, &len), 0); + said("answers the address", memcmp(&name.sin_addr, octets, 4) == 0, 1); + said("and the port", name.sin_port == port, 1); + const char *shown = inet_ntop(AF_INET, &name.sin_addr, text, sizeof text); + said("which inet_ntop writes as it was given", shown && strcmp(shown, argv[1]) == 0, 1); + + struct addrinfo hints, *found = NULL; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + said("getaddrinfo of the address", getaddrinfo(argv[1], NULL, &hints, &found), 0); + if (found) { + struct in_addr by_getaddrinfo = ((struct sockaddr_in *)found->ai_addr)->sin_addr; + said("answers the same four bytes", memcmp(&by_getaddrinfo, octets, 4) == 0, 1); + said("connect to getaddrinfo's", dial(by_getaddrinfo, port) >= 0, 1); + freeaddrinfo(found); + } + + text_of("INADDR_LOOPBACK", INADDR_LOOPBACK, "127.0.0.1"); + text_of("INADDR_BROADCAST", INADDR_BROADCAST, "255.255.255.255"); + text_of("INADDR_ANY", INADDR_ANY, "0.0.0.0"); + + if (wrong) { + printf("addr_order: %d wrong\n", wrong); + return 1; + } + printf("addr_order: ok\n"); + return 0; +} diff --git a/tests/netcase/nodelay_kept.c b/tests/netcase/nodelay_kept.c new file mode 100644 index 00000000000..da71e9f4090 --- /dev/null +++ b/tests/netcase/nodelay_kept.c @@ -0,0 +1,96 @@ +/* A TCP_NODELAY set on a stream socket before connect is kept and holds for + the connection, and one cleared stays cleared. One set before bind is the + listener's, and each connection it accepts begins with it. A listener's set + after bind is refused: netstack has no option for a listener, where a host + takes the set and gives it to the connections that begin afterwards. argv: + the address and the port of a host that accepts and holds each connection, + then the two ports to listen on, which that host dials once WAITING is + said. */ +#include +#include +#include +#include +#include +#include +#include +#include + +#define WAITING "nodelay_kept: both listeners wait for a peer" + +static int wrong; + +static void said(const char *what, long got, long want) { + printf("%s: %ld%s\n", what, got, got == want ? "" : " <-- WRONG"); + if (got != want) wrong++; +} + +/* Whether TCP_NODELAY is set on `fd`: 1 or 0, or -1 where the read fails. */ +static int nodelay(int fd) { + int value = -7; + socklen_t len = sizeof value; + if (getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &value, &len) != 0) return -1; + return value != 0; +} + +int main(int argc, char **argv) { + if (argc != 5) return 2; + struct sockaddr_in peer; + int on = 1, off = 0; + + memset(&peer, 0, sizeof peer); + peer.sin_family = AF_INET; + peer.sin_port = htons((uint16_t)atoi(argv[2])); + said("the peer's address is one", inet_pton(AF_INET, argv[1], &peer.sin_addr), 1); + + int t = socket(AF_INET, SOCK_STREAM, 0); + said("a fresh stream socket's TCP_NODELAY", nodelay(t), 0); + said("set before connect", setsockopt(t, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + said("read before connect", nodelay(t), 1); + said("connect", connect(t, (struct sockaddr *)&peer, sizeof peer), 0); + said("read after connect", nodelay(t), 1); + said("cleared on the connection", setsockopt(t, IPPROTO_TCP, TCP_NODELAY, &off, sizeof off), 0); + said("read after the clear", nodelay(t), 0); + said("set on the connection", setsockopt(t, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + said("read after the set", nodelay(t), 1); + + int u = socket(AF_INET, SOCK_STREAM, 0); + said("set on a second socket", setsockopt(u, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + said("and cleared before connect", setsockopt(u, IPPROTO_TCP, TCP_NODELAY, &off, sizeof off), 0); + said("connect", connect(u, (struct sockaddr *)&peer, sizeof peer), 0); + said("read after connect", nodelay(u), 0); + + struct sockaddr_in any; + memset(&any, 0, sizeof any); + any.sin_family = AF_INET; + int held = socket(AF_INET, SOCK_STREAM, 0); + said("set on a third socket before bind", setsockopt(held, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), 0); + any.sin_port = htons((uint16_t)atoi(argv[3])); + said("bind it", bind(held, (struct sockaddr *)&any, sizeof any), 0); + said("listen on it", listen(held, 1), 0); + said("read from that listener", nodelay(held), 1); + int l = socket(AF_INET, SOCK_STREAM, 0); + any.sin_port = htons((uint16_t)atoi(argv[4])); + said("bind a fourth socket", bind(l, (struct sockaddr *)&any, sizeof any), 0); + said("listen on it", listen(l, 1), 0); + + printf("%s\n", WAITING); + fflush(stdout); + int with = accept(held, NULL, NULL); + said("accept from the listener that held the option", with >= 0, 1); + said("read from its connection", nodelay(with), 1); + int without = accept(l, NULL, NULL); + said("accept from the listener that held none", without >= 0, 1); + said("read from its connection", nodelay(without), 0); + + errno = 0; + said("set on the listener", setsockopt(l, IPPROTO_TCP, TCP_NODELAY, &on, sizeof on), -1); + said("which is refused as not connected", errno == ENOTCONN, 1); + said("read from the listener", nodelay(l), 0); + + if (wrong) { + printf("nodelay_kept: %d wrong\n", wrong); + return 1; + } + printf("nodelay_kept: ok\n"); + return 0; +} diff --git a/tests/netcase/sendto_unbound.c b/tests/netcase/sendto_unbound.c new file mode 100644 index 00000000000..9311a5f92cf --- /dev/null +++ b/tests/netcase/sendto_unbound.c @@ -0,0 +1,75 @@ +/* A datagram socket never bound is bound by its first send, to a port the + stack chooses, as POSIX has it: the usual C broadcast sequence, socket, + setsockopt, sendto, and a connected socket's send. argv: the address and + the port of a host that answers each datagram with itself. */ +#include +#include +#include +#include +#include +#include + +static int wrong; + +static void said(const char *what, long got, long want) { + printf("%s: %ld%s\n", what, got, got == want ? "" : " <-- WRONG"); + if (got != want) wrong++; +} + +/* The port `fd` is bound to, in host order. */ +static unsigned bound_port(int fd) { + struct sockaddr_in name; + socklen_t len = sizeof name; + memset(&name, 0, sizeof name); + if (getsockname(fd, (struct sockaddr *)&name, &len) != 0) return 0; + return ntohs(name.sin_port); +} + +int main(int argc, char **argv) { + if (argc != 3) return 2; + struct sockaddr_in peer, from, all; + socklen_t fromlen = sizeof from; + char buf[16]; + int on = 1, held = 0; + socklen_t heldlen = sizeof held; + + memset(&peer, 0, sizeof peer); + peer.sin_family = AF_INET; + peer.sin_port = htons((uint16_t)atoi(argv[2])); + said("the peer's address is one", inet_pton(AF_INET, argv[1], &peer.sin_addr), 1); + + int s = socket(AF_INET, SOCK_DGRAM, 0); + said("SO_BROADCAST set on a socket never bound", setsockopt(s, SOL_SOCKET, SO_BROADCAST, &on, sizeof on), 0); + said("sendto from it", sendto(s, "ping", 4, 0, (struct sockaddr *)&peer, sizeof peer), 4); + said("the send bound it to a port", bound_port(s) != 0, 1); + + memset(&from, 0, sizeof from); + memset(buf, 0, sizeof buf); + said("the answer came back to that port", recvfrom(s, buf, sizeof buf, 0, (struct sockaddr *)&from, &fromlen), 4); + said("and is what was sent", memcmp(buf, "ping", 4) == 0, 1); + said("from the peer's address", from.sin_addr.s_addr == peer.sin_addr.s_addr, 1); + said("and the peer's port", from.sin_port == peer.sin_port, 1); + + said("the option is read back", getsockopt(s, SOL_SOCKET, SO_BROADCAST, &held, &heldlen), 0); + said("and is still set", held != 0, 1); + memset(&all, 0, sizeof all); + all.sin_family = AF_INET; + all.sin_port = htons(9); + all.sin_addr.s_addr = htonl(INADDR_BROADCAST); + said("sendto the limited broadcast address", sendto(s, "x", 1, 0, (struct sockaddr *)&all, sizeof all), 1); + + int c = socket(AF_INET, SOCK_DGRAM, 0); + said("connect on a second socket never bound", connect(c, (struct sockaddr *)&peer, sizeof peer), 0); + said("send from it", send(c, "pong", 4, 0), 4); + memset(buf, 0, sizeof buf); + said("its answer", recv(c, buf, sizeof buf, 0), 4); + said("is what it sent", memcmp(buf, "pong", 4) == 0, 1); + said("the two sockets hold two ports", bound_port(c) != 0 && bound_port(c) != bound_port(s), 1); + + if (wrong) { + printf("sendto_unbound: %d wrong\n", wrong); + return 1; + } + printf("sendto_unbound: ok\n"); + return 0; +} diff --git a/tests/toyos.rs b/tests/toyos.rs index f0fccd0e971..e67da326128 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -261,6 +261,10 @@ const MACHINE_TESTS: &[&str] = &[ // connections: netstack is one binary that owns its NIC, with no host // build, and the T14's peer is the bench's network. "netstack_socket_churn", + // What libc's socket calls ask of netstack, read back from a peer that + // answers: the calls are libc's requests on netstack's port, netstack has + // no host build, and the T14's peer is the bench's network. + "libc_sockets", // The nested-NMI report is a raw write to the 16550, which the T14 does not // have. "nested_nmi_is_loud", @@ -2923,22 +2927,14 @@ fn scanout_wc(console: &str) -> Result<(), String> { /// are the verdict. fn netstack_socket_churn() -> Result<(), String> { const JOB: &str = "netstack_socket_churn"; - const LEASED: &str = "netstack: DHCP: lease "; let server = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the host server: {e}"))?; let port = server.local_addr().map_err(|e| format!("the host server's port: {e}"))?.port(); // Ends with the process: a guest that never dials leaves it in `accept`. thread::spawn(move || server.incoming().for_each(drop)); - // A second that holds what it accepts and reads none of it, for as long - // as the process lives. - let holder = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the holding server: {e}"))?; - let holding = holder.local_addr().map_err(|e| format!("the holding server's port: {e}"))?.port(); - thread::spawn(move || holder.incoming().collect::>()); + let holding = holding_server()?; let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); - let case = compile::repo_root().join("tests/netcase"); - let mut qemu = QemuInstance::boot_with_options(&case, &[], &[(JOB.to_string(), bin)], BootOptions::default()); - let mut console = qemu.boot_log().to_string(); - await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + let mut qemu = boot_netcase(&[], &[(JOB.to_string(), bin)], BootOptions::default())?; let result = qemu.run_test(&format!("test_rs_netstack_socket_churn {port} {holding}"), Duration::from_secs(120)); if let Some(why) = &result.error { @@ -2953,12 +2949,129 @@ fn netstack_socket_churn() -> Result<(), String> { Ok(()) } +/// A host server that holds each connection it accepts and reads none of it, +/// for as long as the process lives: its port. A guest that never dials +/// leaves it in `accept`. +fn holding_server() -> Result { + let holder = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the holding server: {e}"))?; + let port = holder.local_addr().map_err(|e| format!("the holding server's port: {e}"))?.port(); + thread::spawn(move || holder.incoming().collect::>()); + Ok(port) +} + +/// Boot `tests/netcase` with these binaries staged, to netstack's lease: its +/// jobs name their peer by an address. +fn boot_netcase( + c_bins: &[(String, Vec)], + rust_bins: &[(String, Vec)], + options: BootOptions, +) -> Result { + const LEASED: &str = "netstack: DHCP: lease "; + let case = compile::repo_root().join("tests/netcase"); + let mut qemu = QemuInstance::boot_with_options(&case, c_bins, rust_bins, options); + let mut console = qemu.boot_log().to_string(); + await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + Ok(qemu) +} + +/// Have the guest's network carry what dials a loopback port of QEMU's +/// choosing to each of `guest_ports`: those host ports, in that order. +fn forwards_into(qemu: &QemuInstance, guest_ports: [u16; 2]) -> Result<[u16; 2], String> { + const FORWARD: &str = "TCP[HOST_FORWARD]"; + let mut monitor = qemu::QmpMonitor::open(qemu.qmp_socket()); + for port in guest_ports { + // `net0` is the NIC's backend in `common::qemu`'s argv. + let said = monitor.human(&format!("hostfwd_add net0 tcp:127.0.0.1:0-:{port}")); + if !said.trim().is_empty() { + return Err(format!("QEMU forwards nothing to the guest's port {port}: {said}")); + } + } + // A row is the protocol, a descriptor, the host's address and port, then + // the guest's. + let table = monitor.human("info usernet"); + let mut host_ports = [0u16; 2]; + for (host, guest) in host_ports.iter_mut().zip(guest_ports) { + *host = table + .lines() + .map(|row| row.split_whitespace().collect::>()) + .find(|row| row.first() == Some(&FORWARD) && row.get(5) == Some(&guest.to_string().as_str())) + .and_then(|row| row[3].parse().ok()) + .ok_or_else(|| format!("QEMU names no host port forwarded to the guest's {guest}:\n{table}"))?; + } + Ok(host_ports) +} + +/// libc's sockets as a C program uses them, on one boot of `tests/netcase`: +/// each of its C cases dials a host server that holds what it accepts, or +/// sends to one that answers each datagram with itself, at the address the +/// guest's network gives the host; and once `nodelay_kept` says its two +/// listeners wait, the host dials each through a port QEMU forwards. A case's +/// own comparisons are its verdict. +fn libc_sockets() -> Result<(), String> { + const HOST: &str = "10.0.2.2"; + /// The ports `nodelay_kept` listens on in the guest, which nothing else + /// on its boot binds. + const LISTENERS: [u16; 2] = [7001, 7002]; + /// `nodelay_kept.c`'s `WAITING`. + const WAITING: &str = "nodelay_kept: both listeners wait for a peer"; + let holding = holding_server()?; + let echo = std::net::UdpSocket::bind(("127.0.0.1", 0)).map_err(|e| format!("the answering server: {e}"))?; + let answering = echo.local_addr().map_err(|e| format!("the answering server's port: {e}"))?.port(); + // Ends with the process, as the holding server does. + thread::spawn(move || { + let mut datagram = [0u8; 64]; + while let Ok((len, from)) = echo.recv_from(&mut datagram) { + echo.send_to(&datagram[..len], from).expect("answer a datagram"); + } + }); + + // The address first: every case names its peer by one. + let [held, clear] = LISTENERS; + let cases = [ + ("addr_order", holding.to_string()), + ("nodelay_kept", format!("{holding} {held} {clear}")), + ("sendto_unbound", answering.to_string()), + ]; + let case = compile::repo_root().join("tests/netcase"); + let bins: Vec<(String, Vec)> = cases + .iter() + .map(|(name, _)| (name.to_string(), compile::link_toyos(&compile::compile_own_c(&case, name), name))) + .collect(); + let mut qemu = boot_netcase(&bins, &[], BootOptions { qmp: true, ..Default::default() })?; + let forwarded = forwards_into(&qemu, LISTENERS)?; + // Held to the test's end: a peer gone before the case's `accept` fails its hand-over. + let mut dialled = Vec::new(); + for (name, ports) in cases { + let result = + qemu.run_test_hooked(&format!("test_c_{name} {HOST} {ports}"), Duration::from_secs(120), WAITING, |_| { + dialled.extend(forwarded.map(|port| std::net::TcpStream::connect(("127.0.0.1", port)))); + }); + if let Some(Err(e)) = dialled.iter().find(|dial| dial.is_err()) { + return Err(format!("{name}: the host could not dial a port QEMU forwards to the guest: {e}")); + } + if qemu::VERBOSE.load(std::sync::atomic::Ordering::Relaxed) { + eprintln!(" [libc] {name} ended {:?} and said:\n{}", result.exit_code, result.stdout); + } + if let Some(why) = &result.error { + return Err(format!("{name}: {why}\nthe case said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("{name} ended {:?}:\n{}", result.exit_code, result.stdout)); + } + if !result.stdout.lines().any(|l| l.trim_end().ends_with(&format!("{name}: ok"))) { + return Err(format!("{name} never said it was done:\n{}", result.stdout)); + } + } + Ok(()) +} + /// Run the machine-shape test, which owns its QEMU: the machine shape *is* the /// test. fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { match name { "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config), "netstack_socket_churn" => netstack_socket_churn(), + "libc_sockets" => libc_sockets(), "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config), "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), diff --git a/userland/libc/include/arpa/inet.h b/userland/libc/include/arpa/inet.h index 145d3a58ab8..0376cca209a 100644 --- a/userland/libc/include/arpa/inet.h +++ b/userland/libc/include/arpa/inet.h @@ -9,7 +9,7 @@ extern "C" { int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); -uint32_t inet_addr(const char *cp); +in_addr_t inet_addr(const char *cp); #ifdef __cplusplus } diff --git a/userland/libc/include/netinet/in.h b/userland/libc/include/netinet/in.h index 66a6bf8dabb..4b7e83aaa89 100644 --- a/userland/libc/include/netinet/in.h +++ b/userland/libc/include/netinet/in.h @@ -8,21 +8,26 @@ extern "C" { #endif -#define INADDR_ANY ((uint32_t)0x00000000) -#define INADDR_LOOPBACK ((uint32_t)0x7f000001) -#define INADDR_NONE ((uint32_t)0xffffffff) +typedef uint16_t in_port_t; +typedef uint32_t in_addr_t; -#define INET_ADDRSTRLEN 16 +/* In host byte order: a program passes each through htonl. */ +#define INADDR_ANY ((in_addr_t)0x00000000) +#define INADDR_LOOPBACK ((in_addr_t)0x7f000001) +#define INADDR_BROADCAST ((in_addr_t)0xffffffff) +#define INADDR_NONE ((in_addr_t)0xffffffff) -#define TCP_NODELAY 1 +#define INET_ADDRSTRLEN 16 +/* Both in network byte order: the port's two bytes and the address's four + octets, in memory order. */ struct in_addr { - uint32_t s_addr; + in_addr_t s_addr; }; struct sockaddr_in { - unsigned short sin_family; - uint16_t sin_port; + sa_family_t sin_family; + in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; }; diff --git a/userland/libc/include/netinet/tcp.h b/userland/libc/include/netinet/tcp.h new file mode 100644 index 00000000000..c8a1a4bc495 --- /dev/null +++ b/userland/libc/include/netinet/tcp.h @@ -0,0 +1,6 @@ +#ifndef _NETINET_TCP_H +#define _NETINET_TCP_H + +#define TCP_NODELAY 1 + +#endif diff --git a/userland/libc/src/errno.rs b/userland/libc/src/errno.rs index e07148fd199..6bd30ef43b2 100644 --- a/userland/libc/src/errno.rs +++ b/userland/libc/src/errno.rs @@ -31,12 +31,14 @@ pub(crate) const EBUSY: i32 = 16; pub(crate) const EEXIST: i32 = 17; pub(crate) const ENODEV: i32 = 19; pub(crate) const EINVAL: i32 = 22; +pub(crate) const ENOSPC: i32 = 28; pub(crate) const EPIPE: i32 = 32; pub(crate) const ERANGE: i32 = 34; pub(crate) const EDEADLK: i32 = 35; pub(crate) const ENOSYS: i32 = 38; pub(crate) const EOVERFLOW: i32 = 75; pub(crate) const EILSEQ: i32 = 84; +pub(crate) const ENOPROTOOPT: i32 = 92; pub(crate) const EOPNOTSUPP: i32 = 95; pub(crate) const EAFNOSUPPORT: i32 = 97; pub(crate) const EADDRINUSE: i32 = 98; diff --git a/userland/libc/src/inaddr.rs b/userland/libc/src/inaddr.rs new file mode 100644 index 00000000000..da8edcbc514 --- /dev/null +++ b/userland/libc/src/inaddr.rs @@ -0,0 +1,160 @@ +//! IPv4 addresses and ports as a C program holds them, which is network byte +//! order: an address is its four octets in memory order and a port its two, +//! high first. Every address libc reads from or writes to a C program passes +//! through [`SockaddrIn`], which holds both as bytes, so no integer of either +//! exists to be read in the machine's order, and which answers a caller's +//! buffer no more bytes than it holds. The texts `inet_pton`, `inet_addr` and +//! `inet_ntop` read and write are here too, with those calls' refusals. It +//! reads and sets nothing but what it is handed, so the host tests it +//! (`toyos-libc-copies`) against the host C library's own calls. + +use core::mem::size_of; + +pub(crate) const AF_INET: i32 = 2; + +/// C's `struct sockaddr_in`, as `include/netinet/in.h` lays it out. +#[repr(C, align(4))] +pub(crate) struct SockaddrIn { + sin_family: u16, + sin_port: [u8; 2], + sin_addr: [u8; 4], + sin_zero: [u8; 8], +} + +impl SockaddrIn { + pub(crate) fn new(ip: [u8; 4], port: u16) -> Self { + Self { sin_family: AF_INET as u16, sin_port: port.to_be_bytes(), sin_addr: ip, sin_zero: [0; 8] } + } + + /// The address and port, or `None` for another family's. + pub(crate) fn endpoint(&self) -> Option<([u8; 4], u16)> { + (self.sin_family == AF_INET as u16).then(|| (self.sin_addr, u16::from_be_bytes(self.sin_port))) + } + + /// Answers a caller this address as POSIX has a call store one: as many + /// of its bytes as `*len` holds, and its own length written back. + /// + /// # Safety + /// `len` is a readable and writable length, and `addr` `*len` writable + /// bytes. + pub(crate) unsafe fn answer(&self, addr: *mut u8, len: *mut u32) { + let whole = size_of::(); + core::ptr::copy_nonoverlapping((self as *const Self).cast::(), addr, whole.min(*len as usize)); + *len = whole as u32; + } +} + +/// Why `inet_pton` or `inet_ntop` answers no address. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum Refusal { + /// A family that is not `AF_INET`: `EAFNOSUPPORT`. + Family, + /// `inet_ntop`'s buffer does not hold the text and its NUL: `ENOSPC`. + Room, +} + +/// `inet_pton`: `text`'s address, `None` for a text that is none. +pub(crate) fn pton(af: i32, text: &[u8]) -> Result, Refusal> { + if af != AF_INET { + return Err(Refusal::Family); + } + Ok(dotted_quad(text)) +} + +/// `inet_ntop`: the text of the address at `src` and its NUL into the `size` +/// bytes at `dst`, which a refusal leaves unwritten. +/// +/// # Safety +/// `src` is an `AF_INET` address's four bytes, read once the family is known +/// to be that, and `dst` is `size` writable bytes. +pub(crate) unsafe fn ntop(af: i32, src: *const u8, dst: *mut u8, size: u32) -> Result<(), Refusal> { + if af != AF_INET { + return Err(Refusal::Family); + } + let mut text = [0u8; 16]; + let len = dotted_text(src.cast::<[u8; 4]>().read(), &mut text); + if len as u32 >= size { + return Err(Refusal::Room); + } + core::ptr::copy_nonoverlapping(text.as_ptr(), dst, len + 1); + Ok(()) +} + +/// `inet_pton`'s text, POSIX's `ddd.ddd.ddd.ddd`: four decimal numbers of one +/// to three digits, each at most 255, and nothing else. A number of more than +/// one digit that begins with `0` is refused, as glibc refuses it: `inet_addr` +/// reads such a number as octal, and no text has two values. +pub(crate) fn dotted_quad(text: &[u8]) -> Option<[u8; 4]> { + let mut octets = [0u8; 4]; + let mut parts = text.split(|&c| c == b'.'); + for octet in &mut octets { + let part = parts.next()?; + let zero_led = part.len() > 1 && part[0] == b'0'; + if !(1..=3).contains(&part.len()) || zero_led || !part.iter().all(u8::is_ascii_digit) { + return None; + } + let value = part.iter().fold(0u16, |v, &c| v * 10 + u16::from(c - b'0')); + *octet = u8::try_from(value).ok()?; + } + parts.next().is_none().then_some(octets) +} + +/// One number of `inet_addr`'s text, as ISO C writes an integer constant: +/// `0x` and hexadecimal digits, `0` and octal digits, or decimal digits. +fn number(part: &[u8]) -> Option { + let (digits, radix) = match part { + [b'0', b'x' | b'X', rest @ ..] => (rest, 16), + [b'0', rest @ ..] if !rest.is_empty() => (rest, 8), + _ => (part, 10), + }; + if digits.is_empty() { + return None; + } + digits.iter().try_fold(0u32, |v, &c| v.checked_mul(radix)?.checked_add(char::from(c).to_digit(radix)?)) +} + +/// `inet_addr`'s text: one to four numbers between dots, each but the last +/// one octet and the last every octet that is left, so `127.1` is 127.0.0.1. +/// The address ends at the string's end or at white space, as BSD's reader +/// and glibc's have it; POSIX names no end. +pub(crate) fn numbers_and_dots(text: &[u8]) -> Option<[u8; 4]> { + let end = text.iter().position(|c| matches!(c, b' ' | b'\t'..=b'\r')).unwrap_or(text.len()); + let mut numbers = [0u32; 4]; + let mut count = 0; + for part in text[..end].split(|&c| c == b'.') { + *numbers.get_mut(count)? = number(part)?; + count += 1; + } + let (&last, leading) = numbers[..count].split_last()?; + let mut octets = [0u8; 4]; + for (octet, &n) in octets.iter_mut().zip(leading) { + *octet = u8::try_from(n).ok()?; + } + let left = 4 - leading.len(); + if u64::from(last) >> (8 * left) != 0 { + return None; + } + octets[leading.len()..].copy_from_slice(&last.to_be_bytes()[leading.len()..]); + Some(octets) +} + +/// `ip` as `inet_ntop` writes it into `buf`, NUL-terminated; the text's +/// length without the NUL. `buf` is `INET_ADDRSTRLEN` bytes, which holds the +/// longest. +fn dotted_text(ip: [u8; 4], buf: &mut [u8; 16]) -> usize { + let mut at = 0; + for (i, octet) in ip.into_iter().enumerate() { + if i > 0 { + buf[at] = b'.'; + at += 1; + } + let digits = [octet / 100, octet / 10 % 10, octet % 10]; + let skip = if octet >= 100 { 0 } else if octet >= 10 { 1 } else { 2 }; + for digit in &digits[skip..] { + buf[at] = b'0' + digit; + at += 1; + } + } + buf[at] = 0; + at +} diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 594b72fc83b..94e87be16c6 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -10,6 +10,7 @@ mod elfsym; mod errno; mod fdreq; mod fparts; +mod inaddr; mod link; mod linkreq; mod listing; diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs index 2cdc871b38e..4cbc660eeec 100644 --- a/userland/libc/src/socket.rs +++ b/userland/libc/src/socket.rs @@ -7,27 +7,17 @@ use toyos_abi::RawHandle; use toyos_abi::syscall; use toyos::net::{NetError, TcpSocketId, UdpSocketId, OPT_BROADCAST, OPT_NODELAY}; -use crate::errno::{EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EFAULT, EINVAL, EIO, ENOMEM, ENOTCONN, ETIMEDOUT}; +use crate::errno::{ + EACCES, EADDRINUSE, EAFNOSUPPORT, EBADF, ECONNREFUSED, ECONNRESET, EFAULT, EINVAL, EIO, ENOMEM, ENOPROTOOPT, ENOSPC, + ENOTCONN, EOPNOTSUPP, ETIMEDOUT, +}; +use crate::inaddr::{self, SockaddrIn, AF_INET}; use crate::sockopt::{self, Kept}; // C types matching POSIX type SocklenT = u32; -#[repr(C)] -struct SockaddrIn { - sin_family: u16, - sin_port: u16, // network byte order (big-endian) - sin_addr: InAddr, - sin_zero: [u8; 8], -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct InAddr { - s_addr: u32, // network byte order -} - #[repr(C)] pub struct Sockaddr { sa_family: u16, @@ -46,7 +36,6 @@ pub struct Addrinfo { ai_next: *mut Addrinfo, } -const AF_INET: i32 = 2; const AF_UNSPEC: i32 = 0; const SOCK_STREAM: i32 = 1; const SOCK_DGRAM: i32 = 2; @@ -73,8 +62,9 @@ struct SocketEntry { tx_fd: i32, // write end of tx pipe (client→netstack) notify_fd: i32, // read end of listener notify pipe // What netstack holds for the socket, which `getsockopt` answers from. A - // datagram socket netstack does not hold yet keeps `broadcast` for `bind` - // to hand over. + // socket netstack does not hold yet keeps each for the call that makes it + // there to hand over: `broadcast` for a datagram socket's bind, `nodelay` + // for a stream's `connect` and for each connection a listener accepts. nodelay: bool, broadcast: bool, } @@ -127,13 +117,7 @@ unsafe fn parse_sockaddr(addr: *const Sockaddr, len: SocklenT) -> Option<([u8; 4 if addr.is_null() || (len as usize) < core::mem::size_of::() { return None; } - let sin = &*(addr as *const SockaddrIn); - if sin.sin_family != AF_INET as u16 { - return None; - } - let port = u16::from_be(sin.sin_port); - let ip = sin.sin_addr.s_addr.to_be_bytes(); // s_addr is network order - Some((ip, port)) + (*(addr as *const SockaddrIn)).endpoint() } /// Fill a sockaddr_in from ip + port. @@ -141,12 +125,26 @@ unsafe fn fill_sockaddr(addr: *mut Sockaddr, addrlen: *mut SocklenT, ip: [u8; 4] if addr.is_null() || addrlen.is_null() { return; } - let sin = &mut *(addr as *mut SockaddrIn); - sin.sin_family = AF_INET as u16; - sin.sin_port = port.to_be(); - sin.sin_addr.s_addr = u32::from_be_bytes(ip); - sin.sin_zero = [0; 8]; - *addrlen = core::mem::size_of::() as SocklenT; + SockaddrIn::new(ip, port).answer(addr as *mut u8, addrlen); +} + +/// Bind a datagram socket netstack does not hold yet, handing over a +/// `SO_BROADCAST` set before it existed there. +fn bind_datagram(entry: &mut SocketEntry, ip: [u8; 4], port: u16) -> Result<(), NetError> { + let bound = toyos::net::udp_bind(ip, port)?; + if entry.broadcast { + if let Err(e) = toyos::net::udp_set_option(bound.socket_id, OPT_BROADCAST, 1) { + // `bound`'s pipe ends close where it drops. + let _ = toyos::net::udp_close(bound.socket_id); + return Err(e); + } + } + entry.netstack_id = bound.socket_id.0; + entry.local_port = bound.bound_port; + entry.bound = true; + entry.tx_fd = bound.tx.into_raw().0 as i32; + entry.rx_fd = bound.rx.into_raw().0 as i32; + Ok(()) } // BSD socket API @@ -207,6 +205,14 @@ pub unsafe extern "C" fn connect(fd: i32, addr: *const Sockaddr, addrlen: Sockle Ok(c) => c, Err(e) => { set_errno(net_err_to_errno(e)); return -1; } }; + if entry.nodelay { + if let Err(e) = toyos::net::tcp_set_option(conn.socket_id, OPT_NODELAY, 1) { + // `conn`'s pipe ends close where it drops. + let _ = toyos::net::tcp_close(conn.socket_id); + set_errno(net_err_to_errno(e)); + return -1; + } + } entry.netstack_id = conn.socket_id.0; entry.local_port = conn.local_port; entry.remote_addr = ip; @@ -253,23 +259,10 @@ pub unsafe extern "C" fn bind(fd: i32, addr: *const Sockaddr, addrlen: SocklenT) 0 } SocketKind::Udp => { - let bound = match toyos::net::udp_bind(ip, port) { - Ok(b) => b, - Err(e) => { set_errno(net_err_to_errno(e)); return -1; } - }; - if entry.broadcast { - if let Err(e) = toyos::net::udp_set_option(bound.socket_id, OPT_BROADCAST, 1) { - // `bound`'s pipe ends close where it drops. - let _ = toyos::net::udp_close(bound.socket_id); - set_errno(net_err_to_errno(e)); - return -1; - } + if let Err(e) = bind_datagram(entry, ip, port) { + set_errno(net_err_to_errno(e)); + return -1; } - entry.netstack_id = bound.socket_id.0; - entry.local_port = bound.bound_port; - entry.bound = true; - entry.tx_fd = bound.tx.into_raw().0 as i32; - entry.rx_fd = bound.rx.into_raw().0 as i32; 0 } } @@ -297,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]; @@ -306,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); @@ -322,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); @@ -363,7 +367,7 @@ pub unsafe extern "C" fn send(fd: i32, buf: *const u8, len: usize, _flags: i32) return -1; } sendto(fd, buf, len, _flags, - &make_sockaddr_in(entry.remote_addr, entry.remote_port) as *const SockaddrIn as *const Sockaddr, + &SockaddrIn::new(entry.remote_addr, entry.remote_port) as *const SockaddrIn as *const Sockaddr, core::mem::size_of::() as SocklenT) } } @@ -413,7 +417,7 @@ pub unsafe extern "C" fn sendto( Some(s) => s, None => { set_errno(EBADF); return -1; } }; - let entry = match slot.as_ref() { + let entry = match slot.as_mut() { Some(e) => e, None => { set_errno(EBADF); return -1; } }; @@ -428,6 +432,14 @@ pub unsafe extern "C" fn sendto( Some(v) => v, None => { set_errno(EINVAL); return -1; } }; + // POSIX: a socket not yet bound is bound at its first send, to a + // port the stack chooses. + if entry.netstack_id == 0 { + if let Err(e) = bind_datagram(entry, [0; 4], 0) { + set_errno(net_err_to_errno(e)); + return -1; + } + } // Write data to tx pipe let data = core::slice::from_raw_parts(buf, len); if let Err(_) = syscall::write(RawHandle(entry.tx_fd as u32), data) { @@ -550,6 +562,8 @@ fn option_errno(refusal: sockopt::Refusal) -> i32 { match refusal { sockopt::Refusal::Short => EINVAL, sockopt::Refusal::Fault => EFAULT, + sockopt::Refusal::NoSuchOption => ENOPROTOOPT, + sockopt::Refusal::NotSupported => EOPNOTSUPP, } } @@ -571,20 +585,28 @@ pub unsafe extern "C" fn setsockopt( }; // All other options silently succeed (SO_REUSEADDR, SO_KEEPALIVE, etc.) - let Some(option) = sockopt::kept(level, optname) else { return 0 }; + let option = match sockopt::kept(level, optname, matches!(entry.kind, SocketKind::Udp), true) { + Ok(Some(option)) => option, + Ok(None) => return 0, + Err(refusal) => { set_errno(option_errno(refusal)); return -1; } + }; let on = match sockopt::switch(optval, optlen) { Ok(on) => on, Err(refusal) => { set_errno(option_errno(refusal)); return -1; } }; match option { - Kept::NoDelay 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; + Kept::NoDelay => { + // netstack holds a connection from `connect` or `accept`, and a + // listener from `bind`, whose id names no connection: it refuses + // a listener's set, and nothing is kept for one. + if entry.netstack_id != 0 { + if let Err(e) = toyos::net::tcp_set_option(TcpSocketId(entry.netstack_id), OPT_NODELAY, on as u32) { + set_errno(net_err_to_errno(e)); + return -1; + } } 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`. @@ -617,7 +639,11 @@ pub unsafe extern "C" fn getsockopt( None => { set_errno(EBADF); return -1; } }; - let value = match sockopt::kept(level, optname) { + let option = match sockopt::kept(level, optname, matches!(entry.kind, SocketKind::Udp), false) { + Ok(option) => option, + Err(refusal) => { set_errno(option_errno(refusal)); return -1; } + }; + let value = match option { Some(Kept::NoDelay) => entry.nodelay as i32, Some(Kept::Broadcast) => entry.broadcast as i32, // Every other option reads 0, `SO_ERROR` among them. @@ -689,7 +715,7 @@ pub unsafe extern "C" fn getaddrinfo( let name = core::slice::from_raw_parts(node, name_len); // Try parsing as IPv4 literal first - if let Some(ip) = parse_ipv4(name) { + if let Some(ip) = inaddr::dotted_quad(name) { return build_addrinfo_result(res, &[(ip, 0)]); } @@ -726,10 +752,7 @@ unsafe fn build_addrinfo_result(res: *mut *mut Addrinfo, addrs: &[([u8; 4], u16) return -1; } - (*sa).sin_family = AF_INET as u16; - (*sa).sin_port = port.to_be(); - (*sa).sin_addr.s_addr = u32::from_be_bytes(ip); - (*sa).sin_zero = [0; 8]; + sa.write(SockaddrIn::new(ip, port)); (*ai).ai_flags = 0; (*ai).ai_family = AF_INET; @@ -763,21 +786,25 @@ pub unsafe extern "C" fn gai_strerror(_errcode: i32) -> *const u8 { b"DNS lookup failed\0".as_ptr() } -// inet_pton / inet_ntop / htons / ntohs / htonl / ntohl +// inet_pton / inet_ntop / inet_addr / htons / ntohs / htonl / ntohl #[no_mangle] pub unsafe extern "C" fn inet_pton(af: i32, src: *const u8, dst: *mut u8) -> i32 { - if af != AF_INET || src.is_null() || dst.is_null() { - return 0; - } - let len = super::string::strlen(src); - let s = core::slice::from_raw_parts(src, len); - match parse_ipv4(s) { - Some(ip) => { + let text = core::slice::from_raw_parts(src, super::string::strlen(src)); + match inaddr::pton(af, text) { + Ok(Some(ip)) => { ptr::copy_nonoverlapping(ip.as_ptr(), dst, 4); 1 } - None => 0, + Ok(None) => 0, + Err(refusal) => { set_errno(address_errno(refusal)); -1 } + } +} + +fn address_errno(refusal: inaddr::Refusal) -> i32 { + match refusal { + inaddr::Refusal::Family => EAFNOSUPPORT, + inaddr::Refusal::Room => ENOSPC, } } @@ -788,48 +815,9 @@ pub unsafe extern "C" fn inet_ntop( dst: *mut u8, size: SocklenT, ) -> *const u8 { - if af != AF_INET || src.is_null() || dst.is_null() || size < 16 { - return ptr::null(); - } - let a = *src; - let b = *src.add(1); - let c = *src.add(2); - let d = *src.add(3); - let mut buf = [0u8; 16]; - let n = fmt_ip4(&mut buf, a, b, c, d); - if n as u32 >= size { - return ptr::null(); - } - ptr::copy_nonoverlapping(buf.as_ptr(), dst, n + 1); - dst as *const u8 -} - -fn fmt_ip4(buf: &mut [u8; 16], a: u8, b: u8, c: u8, d: u8) -> usize { - let mut pos = 0; - for (i, octet) in [a, b, c, d].iter().enumerate() { - if i > 0 { - buf[pos] = b'.'; - pos += 1; - } - pos += fmt_u8(&mut buf[pos..], *octet); - } - buf[pos] = 0; - pos -} - -fn fmt_u8(buf: &mut [u8], val: u8) -> usize { - if val >= 100 { - buf[0] = b'0' + val / 100; - buf[1] = b'0' + (val / 10) % 10; - buf[2] = b'0' + val % 10; - 3 - } else if val >= 10 { - buf[0] = b'0' + val / 10; - buf[1] = b'0' + val % 10; - 2 - } else { - buf[0] = b'0' + val; - 1 + match inaddr::ntop(af, src, dst, size) { + Ok(()) => dst as *const u8, + Err(refusal) => { set_errno(address_errno(refusal)); ptr::null() } } } @@ -853,58 +841,10 @@ pub unsafe extern "C" fn ntohl(netlong: u32) -> u32 { u32::from_be(netlong) } +/// `INADDR_NONE` for a text that is no address, which is also what +/// 255.255.255.255 reads as. #[no_mangle] pub unsafe extern "C" fn inet_addr(cp: *const u8) -> u32 { - if cp.is_null() { - return u32::MAX; // INADDR_NONE - } - let len = super::string::strlen(cp); - let s = core::slice::from_raw_parts(cp, len); - match parse_ipv4(s) { - Some(ip) => u32::from_be_bytes(ip), - None => u32::MAX, - } -} - -// Helpers - -fn parse_ipv4(s: &[u8]) -> Option<[u8; 4]> { - let mut octets = [0u8; 4]; - let mut octet_idx = 0; - let mut val: u16 = 0; - let mut has_digit = false; - - for &c in s { - if c >= b'0' && c <= b'9' { - val = val * 10 + (c - b'0') as u16; - if val > 255 { - return None; - } - has_digit = true; - } else if c == b'.' { - if !has_digit || octet_idx >= 3 { - return None; - } - octets[octet_idx] = val as u8; - octet_idx += 1; - val = 0; - has_digit = false; - } else { - return None; - } - } - if !has_digit || octet_idx != 3 { - return None; - } - octets[3] = val as u8; - Some(octets) -} - -fn make_sockaddr_in(ip: [u8; 4], port: u16) -> SockaddrIn { - SockaddrIn { - sin_family: AF_INET as u16, - sin_port: port.to_be(), - sin_addr: InAddr { s_addr: u32::from_be_bytes(ip) }, - sin_zero: [0; 8], - } + let text = core::slice::from_raw_parts(cp, super::string::strlen(cp)); + inaddr::numbers_and_dots(text).map_or(u32::MAX, u32::from_ne_bytes) } diff --git a/userland/libc/src/sockopt.rs b/userland/libc/src/sockopt.rs index 0c67a100869..fc19f7da1ac 100644 --- a/userland/libc/src/sockopt.rs +++ b/userland/libc/src/sockopt.rs @@ -18,12 +18,16 @@ pub(crate) enum Kept { Broadcast, } -/// The kept option `optname` names at `level`. -pub(crate) fn kept(level: i32, optname: i32) -> Option { +/// The kept option `optname` names at `level`, asked of a datagram socket or a +/// stream. A datagram socket has no option at TCP's level, which Linux says +/// before it reads a value or a length, and in two words: to a setter +/// [`Refusal::NoSuchOption`], to a getter [`Refusal::NotSupported`]. +pub(crate) fn kept(level: i32, optname: i32, datagram: bool, setter: bool) -> Result, Refusal> { match (level, optname) { - (IPPROTO_TCP, TCP_NODELAY) => Some(Kept::NoDelay), - (SOL_SOCKET, SO_BROADCAST) => Some(Kept::Broadcast), - _ => None, + (IPPROTO_TCP, _) if datagram => Err(if setter { Refusal::NoSuchOption } else { Refusal::NotSupported }), + (IPPROTO_TCP, TCP_NODELAY) => Ok(Some(Kept::NoDelay)), + (SOL_SOCKET, SO_BROADCAST) => Ok(Some(Kept::Broadcast)), + _ => Ok(None), } } @@ -33,6 +37,10 @@ pub(crate) enum Refusal { Short, /// A null pointer where bytes are read or written: `EFAULT`. Fault, + /// A setter's level the socket's protocol has no option at: `ENOPROTOOPT`. + NoSuchOption, + /// A getter's: `EOPNOTSUPP`. + NotSupported, } /// What a setter is told: whether the `int` at `optval` is non-zero. The