Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 92 additions & 5 deletions issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
23 changes: 0 additions & 23 deletions issues/libc-drops-a-tcp-nodelay-set-before-connect.md

This file was deleted.

27 changes: 27 additions & 0 deletions issues/libc-getaddrinfo-ignores-its-service-and-its-hints.md
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 0 additions & 26 deletions issues/libc-reads-an-ipv4-address-in-host-order.md

This file was deleted.

32 changes: 0 additions & 32 deletions issues/libc-sends-from-a-datagram-socket-it-never-bound.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading