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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
status: open
kind: defect
opened: 2026-10-09
---

# A handshake reset before it ends hands its option to the next connection

netstack on smoltcp gives a connection the `TCP_NODELAY` its listener held
when its SYN arrived (`userland/netstack/src/listen.rs`), which is a host's
rule (`userland/netstack/node/tests/host.rs`), except for the connection that
follows a handshake its peer reset before it ended.

Read from the two sources, not run. A listener there is one smoltcp socket. A
set of the listener's option is written to that socket only while it is in
`Listen` (`Listening::set_nodelay`); once a SYN has arrived the set is kept in
`Listening` for the socket that listens next, which `Listening::open` gives it
after an accept, or after a reset that left the socket `Closed`
(`Listening::settle`). smoltcp 0.12.0 puts a socket that takes an RST in
`SynReceived` straight back to `Listen` (`src/socket/tcp.rs`, the
`(State::SynReceived, TcpControl::Rst)` arm: `self.tuple = None;
self.set_state(State::Listen)`), with the Nagle switch it had. No pass of
netstack sees that socket `Closed`, so nothing writes the listener's option to
it again.

The sequence: the listener holds the option, a SYN arrives, the owner clears
the option, the peer resets, as a SYN scan does. The socket listens again with
Nagle's algorithm off, and the next connection begins with `TCP_NODELAY` at a
listener that held none when its SYN arrived; its accept answers the option
on. The mirror gives a connection without the option at a listener whose
`getsockopt` reads 1. A set that reaches the socket while it listens again, or
the accept that replaces it, ends the difference: one connection a reset
handshake is affected, if the owner changed the option during that handshake.

No test holds it, and none is to be built: the owner's ruling is that no new
test is built on smoltcp. The node, which replaces this stack, has the rule
for this case: [tcp] keeps the options with the listener and copies them to a
connection when its SYN arrives (LS-10), a reset handshake leaves nothing
behind, and
`a_handshake_reset_before_it_ends_leaves_the_next_connection_its_listeners_option`
(`userland/netstack/node/tests/listeners.rs`) plays the sequence both ways
round and reads the next connection's option from the accept's answer.

**Exit condition**: netd runs on the node and `userland/netstack/src/listen.rs`
and smoltcp are deleted, with that test of the node green.

**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`, at the
move.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
status: open
kind: defect
opened: 2026-10-09
---

# A listener's TCP_NODELAY has no setter in std or the forks, and a C program cannot order the waiting case

A host takes `TCP_NODELAY` on a listening socket and gives it to the
connections that begin afterwards. Read on macOS 27 on arm64, by libc's
`setsockopt` and `getsockopt` over loopback: set before the connection began,
the accepted socket has the option; set after the connection was established
and before the accept, it does not. `userland/netstack/node/tests/host.rs`
asks the same of every host the suite runs on, and sets the option in the
second case only once the listener is readable, which is the host saying the
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.

`tests/host.rs` is green on Linux too: the `host` check runs it on
`ubuntu-24.04`, whose log names
`a_host_gives_a_connection_the_nodelay_its_listener_had_when_it_began ... ok`.

The pipe ABI carries both halves (`toyos/src/net.rs`): a bind's request
carries its listener's options and `MsgType::TcpListenerSetOption` sets one
afterwards, and an accept's answer says what its connection holds
(`TcpOptions`). netstack answers the rule for both
(`userland/netstack/src/listen.rs`), but for the connection after a reset
handshake
(`issues/a-handshake-reset-before-it-ends-hands-its-option-to-the-next-connection.md`),
and the `libc_sockets` guest test reads it: `nodelay_accepted` by `toyos::net`, for a connection that
waited when the option was set, its wake read first, and for the next one
dialled; `tests/netcase/nodelay_kept.c` by libc's `setsockopt` and
`getsockopt`, for a listener's set before `bind`, its set and its clear after,
and the connections dialled after each. What no program reads yet:

- A C program cannot set the option while a connection waits and know that
one does: libc's `poll` watches kernel handles and a socket descriptor is
none (`userland/libc/src/posix_io.rs`), so the only call that says a
connection waits is the `accept` that takes it. `nodelay_kept.c` therefore
holds the half of the rule a second dial orders, and not the first.
- No Rust program can set the option on a listener. `std::net::TcpListener`
has no call for it on any host, where a program reaches the socket through
socket2 by its descriptor. On ToyOS, read from the forks at the commits the
lock names and not run: socket2 (`1d90250`, `src/sys/toyos.rs`) keeps a
table of its own sockets, so a std listener's descriptor names none of
them; on a listener of its own its `setsockopt` of `TCP_NODELAY` answers
`Ok` and hands the option to nobody, and its `accept` records the accepted
socket's as off whatever the answer says; its bind names no option
(`toyos::net::tcp_bind`, which `tcp_bind_with` stands beside only until
the fork's bind passes its own). mio (`d1edcaa`,
`src/net/tcp/toyos_stream.rs`) answers `nodelay()` true for every stream
and its `set_nodelay` sends nothing. std's `TcpListener::accept`
(`sdk/std/sys/net/connection.rs`) takes the accepted stream's option from
the answer, which no test can turn red while no std listener can hold one.

The node (`userland/netstack/node/src/listeners.rs`) takes a listener's
option in its `listen` and in `Node::set_listener_nodelay`, and answers an
accept with what its stream's connection took from its listener; nothing ships
it until the move.

**Exit condition**: on ToyOS a program that sets `TCP_NODELAY` on a
listening socket and accepts one connection begun before and one begun after
reads the option on each as that test has a host answer, by libc's
`getsockopt`, which needs libc's `poll` to answer for a listening socket, and
by std's `nodelay()`, which needs a listener a Rust program can set the option
on: the socket2 fork's, with the lock naming the commit that hands its
listener's option to netstack in its bind and reads its accepted socket's from
the answer, and mio's that reads a stream's; and `toyos::net` has one bind
call, which takes its options.

**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`; the
fork commits are on socket2's and mio's `toyos` branches.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ 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.
answered, to hand over a `TCP_NODELAY` set before it: a peer that resets
between netstack's answer and that request makes it fail `ENOTCONN`, with the
connection closed, which no host's `connect` answers. Its `accept` asks
nothing after its answer, which carries the connection's option.

**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; 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.
`connect` whose peer resets before the hand-over answers as the host does,
held by a test that can be red.

**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
status: open
kind: defect
opened: 2026-10-09
---

# libc's accept writes its caller's address before it can still fail

`accept` (`userland/libc/src/socket.rs`) fills the caller's `addr` and
`addrlen` with the peer's address as soon as netstack answers, and only then
asks `alloc_socket` for a descriptor. With the table of `MAX_SOCKETS` full
that fails: `accept` closes the connection and returns -1 with `ENOMEM`,
having already written the caller's buffer and its length. Read from the
code, not run: no test fills the table, and no host was asked what it leaves
in the buffer of an `accept` it refuses for want of a descriptor.

It is the one failure of `accept` left that touches the caller's buffer, now
that it asks netstack nothing after its answer. The word is not POSIX's
either, which names `EMFILE` for a process out of descriptors.

**Exit condition**: `accept` takes its descriptor before it writes `addr`,
and a guest C case that fills the socket table reads the buffer and its
length unchanged after the refused `accept`, with `EMFILE`.

**Owner**: libc, `userland/libc`; whoever next changes its `accept`.
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@ 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
answers what a stream holds once it is a client's: the pipe ABI says it once,
in the accept's answer (`TcpOptions`, `toyos/src/net.rs`), and has no option
read, and std's `nodelay()` and libc's `getsockopt` answer the value their own
side stored. So a setter of a stream's option 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
(`tests/netcase/nodelay_kept.c`, the `libc_sockets` test) stays green. The
only effect of the option is when a small write leaves the machine, which a
QEMU test may not time.

A listener's is read: what an accepted socket answers is what netstack said of
its connection, so libc's `bind` naming no option in its request, or its
`setsockopt` on a listener storing the value and sending nothing, turns that
case red.

**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.
turns red when libc's `connect` or `setsockopt` on a connection, 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ opened: 2026-10-03

# `run_test` and its ceiling verdict serve only `--debug`'s `run`

`QemuInstance::run_test`, `run_test_hooked` and `run_test_paced`
`QemuInstance::run_test` and `run_test_paced`
(`tests/common/qemu.rs`), and `ceiling_verdict`, the verdict their read loop
ends a wait on, have one caller: interactive debug mode's `run <test>`
(`tests/toyos.rs`, `qemu.run_test(test_name, Duration::from_secs(60))`). Every
Expand All @@ -18,8 +18,9 @@ path.

## Measured

`git grep -c 'run_test(\|run_test_hooked(\|run_test_paced(' <rev> -- tests/toyos.rs tests/common`
counts 40 lines in `tests/toyos.rs` and 34 across sixteen other `tests/common`
`git grep -c 'run_test(\|run_test_hooked(\|run_test_paced(' <rev> -- tests/toyos.rs tests/common`,
over the three functions those revisions had (`run_test_hooked` is deleted
since), counts 40 lines in `tests/toyos.rs` and 34 across sixteen other `tests/common`
modules at `06788146b^`, and one in `tests/toyos.rs`, the debug loop's, at
`06788146b`, the commit that cut the guest suite; the five in
`tests/common/qemu.rs` are the three functions' own.
Expand All @@ -32,5 +33,5 @@ first cut left them that one caller.
## What would close it

`--debug`'s `run` reads its test through a wait the suite uses, and
`run_test`, `run_test_hooked`, `run_test_paced`, `ceiling_verdict` and the
`run_test`, `run_test_paced`, `ceiling_verdict` and the
checks and readers that exist for them are deleted.
26 changes: 0 additions & 26 deletions issues/std-holds-a-streams-nodelay-per-duplicate.md

This file was deleted.

2 changes: 1 addition & 1 deletion issues/toyos-has-its-own-network-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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; 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.
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-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md`: the pipe ABI gives a listener its option in its bind's request and by `MsgType::TcpListenerSetOption` and an accept's answer its connection's (`TcpOptions`, `toyos/src/net.rs`), netd on smoltcp answers them but for `issues/a-handshake-reset-before-it-ends-hands-its-option-to-the-next-connection.md`, which the move ends with smoltcp, and the move maps the bind to `Node::listen`, whose `nodelay` the request's is, the set to `Node::set_listener_nodelay` and the answer's options to `Accepted`'s; what is left of its exit is libc's `poll` and the socket2 and mio forks.

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.

Expand Down
Loading
Loading