Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
status: open
kind: defect
opened: 2026-10-09
---

# A link that goes and returns inside one pass of the Intel driver is reported as no change

`toyos-i219`'s `begin_pass` (`toyos-i219/src/lib.rs`) reads the link only on
a pass that acknowledged `LSC`, and answers `link_changed: self.link !=
before`: the state after the pass compared with the state before it. `LSC` is
one bit, so a link that went down and came back at the same speed and duplex
before one pass acknowledged it is read as the link it was, and the pass
answers no change. `Nic::begin_pass` (`userland/netstack/src/i219.rs`) hands
netstack a link only where that is true, so `main`'s `link_up` stays true and
no pass tells the name's responder it had no link.

RFC 6762 §8 asks for probing and announcing whenever a responder "receives an indication of a network interface "Link Change"
event". The part gave that indication, the `LSC` cause, and the driver held
it and dropped it: the name is neither probed for nor announced on that
link, which may be another network's.

## What is known and what is not

Read from the source, and not measured. Not known: whether a real link can
go and return between two passes. A pass begins on the `LSC`
interrupt, so it needs netstack kept off the CPU for a whole renegotiation.
No machine has been read across a link change at all
(`issues/no-machine-has-read-an-intel-nic-across-a-link-change.md`).

## Owner

The driver's `Pass`, under stage 2 of
`issues/the-lan-is-not-yet-production-grade.md`.

## Exit

A host test in `toyos-i219` in which a pass that acknowledged `LSC` with
`STATUS` as it was before reports the change, and netstack's name has no
link on that pass.
42 changes: 0 additions & 42 deletions issues/netstack-answers-for-its-name-without-claiming-it.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 @@ -38,7 +38,7 @@ What the node does not yet meet:
- 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 needs its socket's permission, where smoltcp sends it for every socket. 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. The node enforces it and nothing that ships does: `Node::udp_set_broadcast` hands the permission to `Udp::set_broadcast`, and a send without it is refused `udp.broadcast-not-permitted` and answered `Refused::PermissionDenied` (`userland/netstack/node/src/datagram.rs`), which `a_broadcast_needs_its_permission` (US-21, `userland/netstack/node/tests/datagram.rs`) holds, with both broadcast addresses read off the wire in link-broadcast frames once the socket holds it and refused again once it is taken back, and `each_refusal_is_answered_in_the_pipes_word_for_it` holds the word. The permission of the call goes with the datagram to the route it leaves by: [udp] queues it with what its socket held, a closed socket's included, and `Ip::send_udp` refuses a link broadcast to one without it, `ip.broadcast-not-permitted`, counted and logged, so a prefix a DHCP server renews under a held address between the call and the frame makes no broadcast of a datagram accepted for a host (`s_udp_us_021_shard_a_datagram_accepted_for_a_host_is_no_broadcast_after_a_renewal_narrows_the_prefix` and its two neighbours in `toyos-net-shard/tests/udp.rs`, and from a server's ACK `a_datagram_accepted_for_a_host_is_no_broadcast_after_the_server_renews_a_narrower_prefix` in the node's tests); netd on smoltcp answers the request done and sends to a broadcast address for every socket. 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, at the move: netd answers `MsgType::UdpSetOption`'s `OPT_BROADCAST` by `Node::udp_set_broadcast` and writes `ERR_PERMISSION_DENIED` for `Refused::PermissionDenied`. 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.
- 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): `issues/netstack-answers-for-its-name-without-claiming-it.md`, which is `toyos-mdns`'s and so the node's and the shipped netstack's alike. A link that returns under a held lease is announced on (§8.3, `a_held_name_is_announced_again_when_the_link_returns`, `userland/netstack/node/tests/name.rs`) and not probed on, which §8 asks first, so each return is two more multicasts of an unprobed record. Exit: that issue's.
- The machine's name is probed for and defended (RFC 6762 §8, §9) by `toyos-mdns` (`userland/netstack/mdns`), on the node and in the netstack that ships. What that does not yet meet. Every machine is named `toyos-t14` (`dhcp::HOSTNAME`, `userland/netstack/src/dhcp.rs`) and a responder that loses its name holds none, so of two on one network the second answers to no name. A name lost under the probe stays lost after the other host has left, or a forger has stopped, until a link after none: two packets from any host on the link buy that, a response to take a held name back to probing and a response under the probe. No path back exists that a peer cannot hold shut, since a host that answers every probe is what a holder of the name is. The owner's open question, with nothing built for it: whether a lost name is probed for again on an interval. RFC 6762 permits probing again at §8.1's rate ("wait five seconds after any failed probe attempt before trying again") and does not require it; the owner's ruling so far is that of two same-named machines the second should "Hold no name, say so". The caller does not say how a message was addressed, so two rules that turn on it are not applied: a response is read however it came, where §6 reads a unicast one only within two seconds of a question that asked for one; and a message from a source outside the subnet is ignored even when it was sent to the group, where §11 deems that on the link "regardless of source IP address", so a host of the same name on another subnet of the link is never a conflict. Exit: each machine's name is its own, and a test with two guests on one network finds each by its name; and the caller says how a message was addressed, with a test in which a unicast response nobody asked for takes no name and one in which a response sent to the group from another subnet does. Not measured, each with its exit: the shipped responder's claim on the T14's wired card, which no `system.toml` in the tree gives netstack (when the outbound rows land, their boot shows the lease line, then the claim line, and no loss line); and the link's return on metal (the orchestrator's attended session with the owner, never automated). A link that goes and returns inside one pass of the Intel driver reaches the name as no change: `issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md`.
- A socket's datagrams that wait for a next hop wait in the one queue it has, `toyos_net_udp::limits::TX_DATAGRAMS` of them: with all 16 waiting for one next hop, the socket's sends to every other destination are refused `udp.tx-queue-full`, `Refused::ResourceExhausted` to a client, until that hop answers or [ip] gives it up, 3 s after its first request left (`BROADCAST_SOLICIT` requests a `RETRANS` apart). It is no regression by this track's earlier record of what ships, that smoltcp kept each datagram in its own socket until the neighbour answered. What a closed socket had accepted waits in the closed sender's `CLOSED_DATAGRAMS` the same way, so 16 datagrams of closed sockets that wait for one silent host have the next closes' datagrams discarded, `udp.tx-discarded-on-close`, for those 3 s: the line on US-53 among stage 3's departures. `s_udp_us_025_what_waits_for_a_next_hop_is_bounded_by_its_sockets_queue` holds the first as it stands. Owner: the worker of the move, with which a program first reaches this queue. Exit, by the move's first run on the T14 or in a guest: it shows a program refused for it, and a socket's bound is counted per next hop; or the owner rules the one queue is the bound.
- A link that goes down drops no waiting datagram by itself: every waiting one is handed to [ip] again at the next transmit opportunity, which refuses it for want of a route (`route.no-source-address`), and one for which the link is back before that opportunity waits for its next hop anew, where the readers' link-down rule (NUD-23) drops what waits at once. `s_udp_us_024_shard_a_waiting_datagram_whose_route_goes_is_dropped` holds the first as it stands, and `a_waiting_datagram_whose_link_returns_before_an_opportunity_waits_anew_and_leaves`, beside it in `toyos-net-shard/tests/udp.rs` and under no scenario's id, the second. Owner: the worker of the move. Exit, before the move: that second test's last assertion is inverted and finds the datagram that waited gone, or the specifications have it wait.
- `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.
Expand Down
9 changes: 7 additions & 2 deletions tests/toyos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2962,18 +2962,23 @@ fn holding_server() -> Result<u16, String> {
Ok(port)
}

/// Boot `tests/netcase` with these binaries staged, to netstack's lease: its
/// jobs name their peer by an address.
/// Boot `tests/netcase` with these binaries staged, to netstack's lease, since
/// its jobs name their peer by an address, and then to the claim of its name:
/// a responder whose probing never ends leaves the machine nameless, and
/// nothing else on a boot reads the name.
fn boot_netcase(
c_bins: &[(String, Vec<u8>)],
rust_bins: &[(String, Vec<u8>)],
options: BootOptions,
) -> Result<QemuInstance, String> {
const LEASED: &str = "netstack: DHCP: lease ";
/// `userland/netstack/src/mdns.rs`'s line for a probing no host answered.
const CLAIMED: &str = "netstack: mDNS: no host answered for ";
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}"))?;
await_marker(&mut qemu, &mut console, CLAIMED, "netstack to claim its name").map_err(|e| format!("{e}\n{console}"))?;
Ok(qemu)
}

Expand Down
Loading
Loading