Skip to content

Both NIC drivers say their transmit room and wake netstack when it returns; a link change leaves the ring the part's - #782

Merged
Japabu merged 9 commits into
mainfrom
wt/toyos-move-edge
Oct 9, 2026
Merged

Japabu merged 9 commits into
mainfrom
wt/toyos-move-edge

Conversation

@Japabu

@Japabu Japabu commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Stage "edge" of the move off smoltcp, landed before the move so the credit path runs under the shipped stack first. Head ab49658de: the moves commit, the change, the fix rounds for reviews 1 and 2, the removal of the re-arm, review 3's notes in issues/, two source comments corrected, and merges of main at #774 and #769. git diff 9c3b81538 ab49658de -- toyos-i219/src userland/netstack/src is fourteen comment lines of toyos-i219/src/lib.rs and nothing else (the header's Defer Count and the bound's comment now cite the I219's own 612523 §9.5.4.6 and §9.5.3.3): the gates below were run at 9c3b81538, and at ab49658de the driver's 88 tests and its lint were run again, both exit 0 (r6-01, r6-02).

What changed, per decision

8bf696534, moves only. Card goes to userland/netstack/src/card.rs; Client, Request, PendingConn, ClientRx, the two response words and the three constants that bound a pending client go to client.rs. Beside the moved lines: two mod lines, the use lines, pub on 31 lines, one deleted banner. The four poller-sizing constants stay in main.rs.

The change (6d2dd8bb2, 12e4fb269, 61b00f16c, 9c3b81538).

  • Room before a frame. toyos_i219::I219::tx_room counts room from the ring after a reclaim, and tx_reserve refuses through that one count. virtio's room is its free heads after a reclaim. Both scratch buffers and both drop counts are deleted; a frame offered with no room is netstack's own bug and panics by name.
  • The wake, per part. wake_on_room unmasks the transmit cause, then counts again; begin_pass masks it. The 82574 unmasks TXDW | TXQ0; the I219 unmasks TXDW alone. virtio arms nothing: its device notifies for every used buffer on a queue with avail.flags 0 and no EVENT_IDX (virtio 1.2 §2.7.7).
  • A link that is down has no room, and a link change over unsent descriptors leaves them the part's, on both parts. The 82574 datasheet's Defer Count (317694 §10.2.7) and the I219 datasheet's own (612523 §9.5.4.6) count a transmit deferred because "the link is not up": each part keeps the frame for the link that returns. So the descriptors are counted descriptors.stranded and left in the ring. From the pass that reads the link up over them the part is owed their write-back within STRANDED_DEADLINE_NANOS, 12.98 s; when they come the ring goes on; a part that has not written them back by then is refused, PassRefused::Stranded, and netstack ends with "cannot be driven on". netstack takes that deadline into its wait (Card::pass_due_in), because a part that never writes back sends nothing to say so. LSC says the link changed, not that it is down: a link that reads up on both sides of it starts the same deadline and loses nothing.
    • Neither part is reset to take a ring back. Rounds 1 and 2 reset the function and ran open's bring-up a second time. Nothing Intel publishes says that is safe on the I219 (see "The I219 and a reset over a live ring"), the 82574's own document says it has no need of it, and no machine in reach could run it. rearm, the split of the bring-up it needed, Pass::rearmed, PassRefused::Rearm, Counters::unsent and descriptors.unsent are deleted; open is main's single bring-up again.
    • One bound for both parts. Both documents give the same sixteen attempts, the 82574's in TCTL.CT (§10.2.6.1) and the I219's in its Power Management Control register (PHY address 01, page 769, register 21, bits 8:1, default 0x0F), so the number is one: fifteen frames of a whole buffer on a half-duplex link at 10 Mb/s, each sent the "total of 16 attempts" a TCTL.CT of 15 allows, each behind a back-off that "clamps to the maximum number of slot times after 10 retries", a slot being the collision window of "64 bytes for 10/100 Mb/s".
  • The counts. Counters and inspect carry, on the Intel parts:
    • transmit.full: times a caller with a frame found no room on a link that is up;
    • transmit.wake_armed: times the cause was unmasked for it;
    • transmit.wake_taken: passes a message began, while the cause was unmasked, whose only unmasked cause read was the transmit one (review 2's first BLOCKER). §7.4.3 keeps a cause in ICR "regardless of the state of the mask bit", so a pass begun by a client, a timer or a received frame reads it too and is not counted;
    • descriptors.stranded: descriptors a link change left in the ring.
  • The link is refreshed on LSC alone. The arm that also refreshed on every pass with the link down was kept for a link that came up before the mask was written. §7.4.3 records the cause masked or not and signals when an unmasked bit is set, so that link change is in ICR at the first pass and is a message the moment IMS is written: the arm had no case and is deleted. With the link down the pass that finds the link is now LSC's message alone. What the T14 boot reads: the line "link up at 1000 Mb/s full duplex, N ms after the driver came up" appears, N beside the 2720 to 2820 the earlier runs read. If it never appears and no lease follows, LSC raised no message and the arm had a case.
  • The smoltcp glue. DmaNic::room (one line), used by transmit, by receive, and by the loop's wait. smoltcp answers "now" for a segment its device refused (smoltcp-0.12.0/src/socket/tcp.rs:2563, iface/interface/mod.rs:668), so without the third use the loop passes continuously while the card has no room.
  • virtio's transmit queue is TxQueue, memory alone, so two host tests drive it over a plain allocation.

Issues.

  • netstacks-i219-drops-a-transmit-burst-past-its-ring.md closes: its exit's driver half is a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told; the netstack half is held by reading and by arm C.
  • netstacks-transmit-drop-report-cadence-has-no-deterministic-test.md closes with its subject.
  • the-intel-nics-room-and-wake-are-unread-on-hardware.md: the exit is the T14 boot's counts, and it now says how the ring has to be filled for them to mean anything.
  • no-machine-has-read-an-intel-nic-across-a-link-change.md is new: the one rule both parts follow at a link change, what both documents say and no machine has read, what the rule costs (stale or lost frames on the link that returns, a deadline that does not bound a busy half-duplex segment, a refusal that is safe only while netstack's row has no restart), that open's reset is the one reset left with the unknown exposure, and that its exit waits on a link partner a row commands, which stage 2 of the-lan-is-not-yet-production-grade.md now owes by a line. It is a file of its own, not a section of the one above, because that one closes on the first T14 boot and this one does not.
  • the-intel-driver-writes-txdctl-and-tidv-against-the-82574-datasheets-newer-revision.md: unchanged in its exit; it now names the T14 boot as the reading of a write-back per descriptor with bit 22 clear on the I219. What another driver writes there was not looked up.

The I219 and a reset over a live ring

No Intel document that could be found states a sequence, a status bit, or that the reset is safe. No other operating system's driver was opened. Fetched from intel.com and searched as text, all silent on a reset with descriptors published:

document number
Ethernet Connection I219 datasheet 612523 rev 2.02
500 Series on-package PCH datasheet, volumes 1 (§18) and 2 (§8) 631119-007, 631120-002
100 Series PCH datasheet volume 2 (§8), and specification update 332691, 332692-015
400 Series PCH datasheet volume 2 (§9), and specification update 620855-002, 620856-009
300 Series PCH specification update 337349-012
7th generation U/Y platform I/O specification update 334660-009
Ethernet Connection I218 specification update rev 1.0
82574 datasheet, and specification update 317694 rev 3.4

intel.com's own search returns no I219 specification update, sighting or application note, and no specification update for the 500 Series on-package PCH. The three GbE chapters publish the function's FLR capability and nine memory-mapped registers, and say nothing of the rings.

What the search did find: the Defer Count, in the 82574's datasheet (§10.2.7) and in the I219's own (612523 §9.5.4.6, which my round-3 search missed and review 3 found): a transmit deferred because "the link is not up". The sentence this branch carried through round 2, that no document says what either part does with a descriptor it holds when the link goes away, was false of both parts. The first unknown of the I219 narrows to "no machine has read it"; the rule the sentence gives is the one both parts follow.

open's reset at every start is the one reset left, and on the I219 it has the same exposure and is not guarded: nothing found says what the guard is. The issue records it.

What no machine has read

Everything this stage does on the Intel parts is read only by the driver's model. No image gives netstack pci:8086:15fc and the harness has no Intel card. That the PCH's MAC raises a message for an unmasked TXDW is the 82574's §7.4.1 taken on trust; the T14 reading is the outbound rows branch's boot. No row can stage a link change.

The virtio refusal, measured

Each arm is a checked patch applied to the head, netstack_socket_churn run alone (--jobs 1), the patch reversed; patches are in the mutation comment, logs in the round-3 logs comment.

arm patch exit reading
C no room after a frame until an interrupt is taken 0 green
D no room after a frame, ever 1 red: stalled waiting for the lease
E, as review 2 spelled it C, releasing only on an interrupt taken with the receive queue's used.idx unmoved since the pass before 1 red, not green: stalled waiting for the lease, twice
F C, and every interrupt record says its message count and both queues' used.idx before and after; the twelfth ends netstack so the lines are kept 1, by design see below

The arms were measured at 3605c5b3f; userland/netstack/src/virtio_net.rs is byte-identical at this head. E was the reviewer's design and is red for the reason F's records show: F reads interrupts taken with only the transmit ring moved (records 6 and 10, and eleven messages against eight receive elements), so the device does interrupt for a used transmit buffer, and E stalls only because slirp's answer to the first frame is already in the receive ring when that frame's interrupt is read.

E is red because its premise does not hold on slirp, not because the transmit interrupt is missing. The review expected the DISCOVER's completion to release the queue "before any OFFER exists". E never released and did not trip its own assertion, so no interrupt was taken with the receive ring unmoved: the first frame's answer was in the receive ring by the time its completion's interrupt was read, and with no room nothing further was sent to raise another.

F reads the question directly. Records 3 to 12 of one run (1 and 2 precede the job and are not in the kept tail):

record 3: 1 message(s), receive used.idx 2 -> 3, transmit used.idx 3 -> 4
record 4: 1 message(s), receive used.idx 3 -> 4, transmit used.idx 4 -> 5
record 5: 1 message(s), receive used.idx 4 -> 5, transmit used.idx 5 -> 6
record 6: 1 message(s), receive used.idx 5 -> 5, transmit used.idx 6 -> 7
record 7: 1 message(s), receive used.idx 5 -> 6, transmit used.idx 7 -> 8
record 8: 1 message(s), receive used.idx 6 -> 7, transmit used.idx 8 -> 9
record 9: 1 message(s), receive used.idx 7 -> 8, transmit used.idx 9 -> 10
record 10: 1 message(s), receive used.idx 8 -> 8, transmit used.idx 10 -> 11
record 11: 1 message(s), receive used.idx 8 -> 9, transmit used.idx 11 -> 12
record 12: 2 message(s), receive used.idx 9 -> 10, transmit used.idx 12 -> 13

Records 6 and 10 are interrupts taken with the receive ring where the pass before left it and the transmit ring one further. Counted without any argument about timing: eleven messages against eight receive elements. QEMU's device does interrupt for a used transmit buffer, as virtio 1.2 §2.7.7 says. One run, on a loaded host; nothing ships for it.

The driver's own refusal does not depend on QEMU: a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back and a_frame_offered_to_a_full_transmit_queue_is_not_taken run on the host. The loop's guard and receive's refusal are read by no test; they go with DmaNic at the move.

High-risk checks

Oracle: Intel's documents, as quoted at each site and modelled in the stub; virtio 1.2 §2.7.7 for arm F.

Red first. the_transmit_cause_is_armed_only_when_asked with this round's stub hook, against the driver at 12e4fb269: exit 101, left: (0, (1, 1, 1)), right: (0, (1, 1, 0)) (r3-01).

Negative controls: twenty-one mutations, each a checked patch applied, built (all exit 0), run, reversed by one script that ends on an empty git status. Every one exits 101. Five of round 3's went with the re-arm they mutated (its m07, m09, m10, m15, m20).

mutation tests red
m01 room check removed from tx_reserve 6, among them a_full_transmit_ring_answers_room_0
m02 room without the reclaim 4, among them a_written_back_descriptor_returns_room
m03 begin_pass leaves the cause unmasked the_transmit_cause_is_armed_only_when_asked
m04 wake_on_room unmasks nothing that, and the burst test
m05 the 82574 unmasks TXDW alone that, and the burst test
m06 the I219 unmasks bit 22 too the_transmit_cause_is_armed_only_when_asked
m07 a link change over unsent descriptors strands nothing a_ring_a_link_change_left_full_stays_the_parts, a_ring_never_written_back_is_refused
m08 room with the link down a_link_that_is_down_has_no_room
m11 a wake counted on a pass nobody waited on the_transmit_cause_is_armed_only_when_asked
m12 a wake counted with the link down a_link_that_is_down_has_no_room
m13 a wake counted without its message the_transmit_cause_is_armed_only_when_asked
m14 a wake counted beside another unmasked cause the same
m16 a ring never written back is never refused a_ring_never_written_back_is_refused
m17 the deadline runs with the link down the same
m18 a second link change counts the descriptors again both link-change tests
m19 a second link change does not restart the deadline a_ring_never_written_back_is_refused
m21 a descriptor written back stays owed a_ring_a_link_change_left_full_stays_the_parts
m22 a write-back owed on a link that is down the same
m23 the deadline is never due both link-change tests
m24 a ring written back with nobody asking is still refused a_ring_a_link_change_left_full_stays_the_parts
n01 virtio's room without the reclaim a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back

Both link-change tests run on both parts.

Held by reading, not by a test: that netstack's loop takes pass_due_in into its wait (no host test builds the loop); "unmasked then counted" against "counted then unmasked" (the stub is single-threaded).

Gates, at 9c3b81538, whose sources are this head's

step command exit log
driver tests (88) cargo test -p toyos-i219 0 r4-01
driver lint cargo clippy -p toyos-i219 --all-targets, the adopted lints, -D warnings 0 r4-02
netstack tests (29) cargo test --manifest-path userland/netstack/Cargo.toml 0 r4-03
image cargo run -- --build-only 0 r4-04
guests (2 passed) cargo test --test toyos-build -- --jobs 2 netstack_socket_churn iommu_virtio_platform 0 r4-05
mutations (21) one script 0, each mutation 101 r4-06
arms C, D, E, F, at 3605c5b3f one script, and F alone C 0, D 1, E 1, F 1 r3-08, r3-armF

Not run by me: cargo run -- --ci host and the whole guest suite. Both are CI's on this stage's brief; the draft's checks are skipped, which is not green. No T14 boot has been made. The two guest filters are the two that boot netstack on virtio.

Host load, one-minute average as uptime gave it: 104 falling to 63 around the guests at this head; 68 to 78 around arms C, D and E; 123 to 129 around arm F. D and E end on the harness's liveness ceiling, as D did in round 2; C passed under the same load. Arm E was run twice, at 61b00f16c and at 3605c5b3f, red both times in the same place; no guest test was run again to get a different verdict.

Net lines against main: +1382 / −528. Production +801 / −403, of which the moves commit is +288 / −274; tests and stub +421 / −53; issues +160 / −72. Removing the re-arm was +321 / −543.

Unsure of

  • The deadline is a refusal on a number: 12.98 s is the slowest a full ring leaves by §10.2.6.1 with nobody else on the wire. A half-duplex segment busy with other stations is not bounded by any document, and a link that slow would be refused. At 1000 Mb/s full duplex, the T14's, review 2's 12 µs a frame makes a full ring 0.18 ms.
  • Frames a link change strands leave on the link that returns, up to a ring of them and as old as the outage; if that link is another network they are fifteen stale frames on it. Rounds 1 and 2 dropped them by the reset.
  • An I219 that does not write stranded descriptors back ends netstack, where the reset might have recovered it or hung it; nothing read says which.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A

Japabu and others added 3 commits October 8, 2026 22:18
Moves only. `Card` and its `impl` go to `card.rs`; `Client`, `Request`,
`PendingConn`, `ClientRx`, the two response words and the three constants
that bound a pending client go to `client.rs`. The four poller-sizing
constants stay in `main.rs`: they size the loop's poller, and one of them
names `resolve::MAX_LOOKUPS`.

What `git show --color-moved` shows beside the moved lines: the two `mod`
lines, the `use` lines each file now needs, `pub` on the items, fields and
methods `main.rs` names across the module boundary, and one section banner
(`// --- One request, and the client waiting for its answer ---`) that is
deleted because the file's name now says it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…turns

A frame is no longer written into a scratch buffer and dropped when the
transmit ring is full. `toyos_i219::I219::tx_room` counts the room from
`tx_next` and `tx_clean` after a reclaim; `wake_on_room` unmasks the
transmit-done cause (`TXDW`, and `TXQ0` for a part in MSI-X mode), counts
again, and `begin_pass` masks it. Unmasked first and counted after, so a
write-back between a count of zero and the unmasking is not waited on.
virtio's room is its free heads after a reclaim; its device already
interrupts for every used transmit buffer, so there is nothing to arm.

netstack's `DmaNic` answers smoltcp `None` from `transmit` with no room, and
from `receive` too, because smoltcp takes a transmit token with every frame
it receives. smoltcp keeps what it could not send and asks "now" on every
pass, so the loop leaves smoltcp's deadline out of its wait while the card
has no room: the card's claim begins the pass that can send.

Deleted: both scratch buffers, `Counters::tx_dropped`, virtio's drop count,
and the two clauses of the report lines that no frame can reach any more (a
frame offered with no room, or longer than a buffer, is netstack's own bug
and panics by name).

`issues/netstacks-i219-drops-a-transmit-burst-past-its-ring.md` closes: its
exit was `transmit` answering `None` and a host test pushing more than a
ring through without a drop, which is
`a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told` at the driver.
`issues/netstacks-transmit-drop-report-cadence-has-no-deterministic-test.md`
closes with its subject: no driver counts a transmit drop, so none can report
one from `tx`. What no hardware has read of the new path is filed as
`issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches for the negative controls, against 6d2dd8bb2 (unchanged by the merge at 882a8bacb). Each was applied with git apply --check then git apply, built (cargo build -p toyos-i219, exit 0), run (cargo test -p toyos-i219, exit 101) and reversed with git apply -R; git status --porcelain printed nothing afterwards.

m1-room-check-removed.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1233,9 +1233,6 @@
             self.counters.too_long = self.counters.too_long.saturating_add(1);
             return None;
         }
-        if self.tx_room() == 0 {
-            return None;
-        }
         let index = self.tx_next;
         Some(TxSlot { index, at: OFF_TX_BUFS + index * TX_BUF_BYTES, len })
     }

m2-room-without-reclaim.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1197,7 +1197,6 @@
     /// descriptor is never handed out, and a full ring is [`TX_RING`]` - 1` in
     /// flight.
     pub fn tx_room(&mut self) -> usize {
-        self.reclaim_tx();
         (self.tx_clean + TX_RING - 1 - self.tx_next) % TX_RING
     }
 

m3-pass-leaves-the-cause-unmasked.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1039,7 +1039,6 @@
         // Before the room this pass's sends ask for: a ring still full arms it
         // again in [`Self::wake_on_room`].
         if self.tx_wake {
-            self.regs.write(regs::IMC, cause::TX_DONE);
             self.tx_wake = false;
         }
 

m4-wake-unmasks-nothing.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1212,7 +1212,6 @@
     /// answered 0 waits on its claim.
     pub fn wake_on_room(&mut self) -> usize {
         if !self.tx_wake {
-            self.regs.write(regs::IMS, cause::TX_DONE);
             self.tx_wake = true;
         }
         self.tx_room()

m5-the-classic-name-alone.patch

--- a/toyos-i219/src/regs.rs
+++ b/toyos-i219/src/regs.rs
@@ -388,7 +388,7 @@
     /// every one this driver publishes does, and with `IDE` clear no timer
     /// holds the cause back. **Not in [`ENABLED`]**: it is unmasked while a
     /// caller waits on a full transmit ring, and never otherwise.
-    pub const TX_DONE: u32 = TXDW | TXQ0;
+    pub const TX_DONE: u32 = TXDW;
 
     /// What §4.6.5 tells a driver to unmask: "Suggested bits include RXT, RXO,
     /// RXDMT and LSC. There is no reason to enable the transmit interrupts."

The runner:

#!/bin/sh
# Apply each mutation as a checked patch, run the driver's tests, restore.
cd "$1" || exit 2
out="$2"
for p in "$out"/mutations/m*.patch; do
  name=$(basename "$p" .patch)
  git apply --check "$p" || { echo "$name: PATCH DOES NOT APPLY"; exit 2; }
  git apply "$p"
  cargo build -p toyos-i219 > "$out/mutations/$name.build.log" 2>&1; echo "$name BUILD_EXIT=$?"
  cargo test -p toyos-i219 > "$out/mutations/$name.log" 2>&1; echo "$name TEST_EXIT=$?"
  git apply -R "$p"
done
git status --porcelain --ignore-submodules=none
echo "TREE_CLEAN_CHECK_DONE"

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 882a8bacb against origin/main at a4416fe6c, round 1. Read only: no build, no guest, no metal.

Net lines: +656 / −449 (+207). Moves commit +288 / −274. The change: production +164 / −72, tests and stub +170 / −32, issues +35 / −72. The production growth is the room count, the wake and their contracts against two deleted drop paths; accepted.

BLOCKER

  • Evidence — cargo run -- --ci host has not run at this head — the body says so and the draft's host, toolchain and guest checks are SKIPPED, which reads as green and is not. The head that lands shows host green, with its command, exit and log.
  • Evidence — the guest tests the change reaches have not run — two of the suite ran; every boot starts netstack on the virtio card. The head that lands shows guest / suite green, whole.
  • Evidence — no hardware has read the Intel path, and this change targets it — tx_room, the IMS/IMC writes and the wake are read only by toyos-i219/src/stub.rs, whose raise delivers a message for TXQ0 alone on both parts, so the I219's own path (MSI, the classic TXDW) is modelled nowhere. That no image hands netstack 8086:15fc means landing breaks no boot; it does not make the reading exist. It can be taken before landing: the T14 flashes a branch, not main. No pushed branch carries the outbound boot yet (origin/wt/toyos-lanrows and origin/wt/toyos-t14lan have no outboundcase). What the boot must show is under "What the head that lands must show".
  • userland/netstack/src/virtio_net.rs:626, userland/netstack/src/main.rs:70 — the refusal on virtio, which is the path every guest and the shipped image run, is measured nowhere — the body: "Whether the queue of 16 ever filled in them was not observed". netstack_socket_churn and iommu_virtio_platform stay green with DmaNic::room answering true, with receive's check gone and with the loop's guard gone, unless the queue happens to fill. One cheap measurement tells it, and nothing ships for it. Arm A: tx_free: RefCell::new((0..1).collect()) at virtio_net.rs:520, then netstack_socket_churn must stay green (refuse, sleep on the claim, QEMU's interrupt, every byte). Arm B: arm A plus fn room(&self) -> bool { true } at main.rs:70, which must end netstack with "a frame was offered to a transmit queue that had said it has no room" and red the test: that is the proof arm A's queue filled. Post both logs.
  • issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md:33 — its exit names a reading nothing in the tree produces — "its readback shows the pass after the full ring begun by the card's interrupt": Card::inspect puts descriptors.sent and wire.sent and no key for a ring found full, a wake armed or a transmit cause taken; i219::Nic::begin_pass drops Pass::causes; report no longer says anything about transmit. A stalled ring and an idle one read the same from the stick, and descriptors.sent == wire.sent holds on a boot that never filled the ring. The driver already knows both numbers (wake_on_room answering 0, begin_pass reading TX_DONE); they go in Counters and inspect, and the exit names them. This replaces the instrument the diff deletes: the drop line is what found the defect on the T14, and the plan's one-boot measurement reads "the driver's drop count", which no longer exists.
  • issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md:16-19, 31-35 — the issue names the link-down unknown and its exit does not read it, so meeting the exit closes the issue with it open — and no test holds it: the stub's transmit (stub.rs:1944) sends whatever the link is, so a ring full across a link-down cannot be staged in the model. Reading the driver: begin_pass on LSC only refreshes STATUS; nothing re-arms or flushes the transmit ring. The state is reachable in ordinary use, not only as a fault: with the cable out, every DISCOVER, ARP request, retransmit and mDNS frame takes a descriptor, and the sixteenth finds none. Publicly, Linux's e1000e watchdog resets the adapter on exactly this ("We've lost link, so the controller stops DMA, but we've got queued Tx work that's never going to get done"), which says the family does not write those descriptors back while the link is down; whether it does when the link returns is the unread part. Against the drop it replaces: while the link is down the two are the same outage; if the part writes back on link-up both recover, the new code by the wake or the next received frame; if it never does, the old code said so once a pass and the new code is a dead card that says nothing. Not worse in what it serves, worse in what it tells. Either the driver makes it moot (the transmit ring re-armed on link-down, with the stub holding frames while the link is down and a test that reds without it), or the exit gains a clause that reads a ring filled with the link down and the link returned, with the counts above.
  • unrecorded compromise — "virtio's full queue is exercised by no test" has no issue — the deleted netstacks-transmit-drop-report-cadence-has-no-deterministic-test.md is the tree's only record of when QEMU's queue fills (only when the host deschedules its main loop: 330 drops at 3.02x, 0 at 1.30x) and of the two ways to stage it on any host. Its subject, the drop report, is gone and its close stands; the fact it measured is now what the refusal path's coverage depends on, and the move inherits it. It is recorded with an owner and an exit, or arm A above becomes a host test of VirtioNet over plain memory as that issue proposed.

NOTE

  • toyos-i219/src/regs.rs:391 — TXQ0 is the 82574's name for bit 22; on the I219 the publicly documented e1000e register map gives bit 22 of ICR/IMS to the uncorrectable-ECC cause (ECCER, pch_lpt and later), so on the T14 wake_on_room also unmasks that, and a pass would read it as transmit-done once the counts above exist. From memory of the Linux driver, not from the datasheet: verify in the I219 document before the boot, and make TX_DONE per part if it holds.
  • citations, as far as the tree and the public register map go — §10.2.4.5 on IMS, §10.2.4.6 on IMC, §10.2.4.9 on IVAR with bit 11 as the TxQ0 entry's valid bit, §7.2.4.2 on RS, §4.6.5 on the suggested mask: each sits on the register the tree already cites it for, and the IVAR layout matches the public one. To verify against the 82574 document before hardware: that TXQ0 is set by the same event as TXDW; what a write to IMS does for a cause already set in ICR (lib.rs:1207 says "raised nothing"; the stub's IMS arm sends none; a level-derived part sends one at once, which costs one pass and loses nothing, so the code holds either way and only the sentence is at stake); that TXDCTL's GRAN | WTHRESH = 1 with TIDV and TADV zero is a write-back per descriptor, since the wake now rests on it; and what the part does with published descriptors while the link is down.
  • toyos-i219/src/lib.rs:1198, :1041, :1211 — read and found right: room is 15 on an empty ring and 0 at tx_next + 1 == tx_clean at every wrap; unmask-then-count loses no write-back; begin_pass runs every loop iteration, so the cause is never left armed past one pass and a completion while armed costs at most one extra pass.
  • userland/netstack/src/main.rs:82 — refusing receive cannot deadlock on virtio or on a part that writes back: room returns by the device's progress, and a pass begun by any event reclaims from memory. Frames left in the receive ring meanwhile are the card's to overrun and count.
  • userland/netstack/src/i219.rs:337, virtio_net.rs:649 — the panic is netstack's own logic only: one token is granted per counted room and consumed once before the next is asked for, room only grows between the two, and smoltcp bounds a frame at 1514 against buffers of 2048 and 4096. No peer, device or client reaches it.
  • userland/netstack/src/main.rs:1515 — the guard is right by reading (every smoltcp deadline is a frame to send or take) and no test can read it before the move: its absence is a spin, which no guest asserts on and no count records. It goes with DmaNic.
  • toyos-i219/src/lib.rs:411 — Counters::too_long has no reader left but its own test: netstack panics on the refusal and prints nothing. Delete the count and keep the refusal.
  • moves commit 8bf696534 — checked line for line: beside the moves, two mod, the use lines, 31 pub and one banner. Every pub is on something main.rs names.
  • merge — git merge-tree 882a8bacb origin/wt/toyos-move-abi is clean.
  • netstack's lint — src/clippy.rs has no shape for any userland program; the nine findings are issues/userland-programs-are-never-linted.md's, unchanged by this branch.
  • closes — netstacks-i219-drops-a-transmit-burst-past-its-ring.md: its exit's driver half is met by a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told; the netstack half ("through one poll") is held by reading and by arm A once run. No citation to either deleted file is left in the tree.
  • body — the gate table gives commands and exits and no log; the kept load record for the head's guest run reads 41 and 33 (one-minute), not "61 to 99"; "closes on its exit (below)" points at nothing below.

What the head that lands must show

  • host green and guest / suite green at that head, run, not skipped.
  • Arms A and B.
  • One T14 boot of that head whose netstack row names pci:8086:15fc: the card handed over, link up, a lease, no "offered to a transmit ring that refuses it" line; then a ring that filled and a wake that was taken, by the counts above. The outbound job as planned writes no byte and will not fill 15 descriptors. Without a manual step: after the lease, the job sends four rings' worth of full-size datagrams from one socket to the router's discard port, then asks inspect; that touches no public service and prints no address. The link-down reading needs the cable and is the owner's step unless the driver makes it moot.

SEND BACK

…change left is taken back, and the wait is counted

Read against Intel's own documents this round (82574 datasheet revision 3.4,
the I219 datasheet 612523 and the PCH's volume 2, 631120), which the first
round did not have.

The transmit cause is per part. The 82574's `TxQ0` "indicates transmit queue
0 write back" and is the name MSI-X mode maps to a vector (§10.2.4.1, §7.4.2),
so it keeps both names. The PCH's function has an MSI capability and no MSI-X
one (631120 §8.1.15), and no document says what its bit 22 is, so the I219
unmasks `TXDW` alone. The stub now models that part in MSI mode, where it had
modelled both by `IVAR`, and delivers a cause already recorded the moment it
is unmasked (§7.4.3).

No Intel document says what either part does with a descriptor it holds when
its link goes away, so the driver stops depending on it: there is no room
while the link is down, and a pass that reads `LSC` over unsent descriptors
resets the function and brings it up again. §10.2.6.7 lets software write the
ring's head only after a reset, so `open`'s bring-up is split out and run a
second time, with the multicast table kept. The stub holds frames while its
link is down and, by a permit, never sends the ones it held.

`Counters` and `inspect` gain a ring found full, a wake armed, a wake taken
and frames given back unsent; `Counters::too_long` goes with its last reader.

virtio's transmit queue becomes `TxQueue`, which is memory alone, and two host
tests drive it over a plain allocation: no room with every head in flight,
room back by exactly the heads the device used, and a frame offered to a full
queue dying by name.

Filed: the two values the bring-up writes against revision 3.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu Japabu changed the title Both NIC drivers say their transmit room and wake netstack when it returns; no frame is dropped past the ring Both NIC drivers say their transmit room and wake netstack when it returns; no frame is dropped past the ring, and a link change gives the ring back Oct 8, 2026
@Japabu

Japabu commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Mutation patches against 9c3b81538, and the arm patches measured at 3605c5b3f (userland/netstack/src/virtio_net.rs is byte-identical at both). This comment replaces the earlier rounds' patches, which no longer apply. Each mutation was checked with git apply --check, applied, built, run and reversed by one script, and the tree was clean after (r4-06; the arms r3-08).

m01-room-check-removed-from-reserve.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1399,7 +1399,7 @@
         // §7.2.10.1: one legacy descriptor carries one buffer, and this
         // driver's is `TX_BUF_BYTES`. A longer frame is refused rather than
         // truncated into one.
-        if len > TX_BUF_BYTES || self.tx_room() == 0 {
+        if len > TX_BUF_BYTES {
             return None;
         }
         let index = self.tx_next;

m02-room-without-reclaim.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1226,7 +1226,6 @@
     /// Transmit descriptors published and not written back, the written-back
     /// ones reclaimed first.
     fn unsent(&mut self) -> usize {
-        self.reclaim_tx();
         (self.tx_next + TX_RING - self.tx_clean) % TX_RING
     }

m03-pass-leaves-the-cause-unmasked.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1149,9 +1149,6 @@
         // Before the room this pass's sends ask for: a ring still full arms it
         // again in [`Self::wake_on_room`].
         let waited = core::mem::take(&mut self.tx_wake);
-        if waited {
-            self.regs.write(regs::IMC, self.part.tx_done());
-        }
 
         // Once, and written back. §10.2.4.1's case 3 says a read with no
         // interrupt asserted has no side effect at all, so a driver that

m04-wake-unmasks-nothing.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1383,7 +1383,6 @@
         }
         self.counters.tx_full = self.counters.tx_full.saturating_add(1);
         if !self.tx_wake {
-            self.regs.write(regs::IMS, self.part.tx_done());
             self.tx_wake = true;
             self.counters.tx_wake_armed = self.counters.tx_wake_armed.saturating_add(1);
         }

m05-82574-classic-name-alone.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -755,7 +755,7 @@
     /// it has, and what its bit 22 means no Intel document publishes.
     fn tx_done(self) -> u32 {
         match self {
-            Self::E82574 => cause::TXDW | cause::TXQ0,
+            Self::E82574 => cause::TXDW,
             Self::I219 => cause::TXDW,
         }
     }

m06-i219-unmasks-bit-22-too.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -756,7 +756,7 @@
     fn tx_done(self) -> u32 {
         match self {
             Self::E82574 => cause::TXDW | cause::TXQ0,
-            Self::I219 => cause::TXDW,
+            Self::I219 => cause::TXDW | cause::TXQ0,
         }
     }
 }

m07-a-link-change-over-unsent-descriptors-strands-nothing.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1192,7 +1192,7 @@
         if causes & cause::LSC != 0 {
             self.refresh_link();
             let unsent = self.unsent();
-            if unsent > 0 {
+            if false {
                 // A second change over the same descriptors counts none of
                 // them twice, and the deadline runs from the newest.
                 let counted = self.stranded.map_or(0, |stranded| stranded.left);

m08-room-with-the-link-down.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1350,9 +1350,6 @@
     /// descriptor is never handed out, and a full ring is [`TX_RING`]` - 1` in
     /// flight.
     pub fn tx_room(&mut self) -> usize {
-        if !self.link.is_up() {
-            return 0;
-        }
         TX_RING - 1 - self.unsent()
     }

m11-a-wake-counted-on-a-pass-nobody-waited-on.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1178,8 +1178,7 @@
         // state of the mask bit", so the transmit cause is in `ICR` on a pass
         // anything began. It is the wake only where a message came and no
         // other unmasked cause is there to have raised it.
-        if waited
-            && messages > 0
+        if messages > 0
             && causes & self.part.tx_done() != 0
             && causes & (cause::ENABLED | cause::ENABLED_MSIX) == 0
         {

m12-wake-counted-with-the-link-down.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1378,7 +1378,7 @@
     /// is the link's own cause, which is never masked.
     pub fn wake_on_room(&mut self) -> usize {
         let room = self.tx_room();
-        if room > 0 || !self.link.is_up() {
+        if room > 0 {
             return room;
         }
         self.counters.tx_full = self.counters.tx_full.saturating_add(1);

m13-a-wake-counted-without-its-message.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1179,7 +1179,6 @@
         // anything began. It is the wake only where a message came and no
         // other unmasked cause is there to have raised it.
         if waited
-            && messages > 0
             && causes & self.part.tx_done() != 0
             && causes & (cause::ENABLED | cause::ENABLED_MSIX) == 0
         {

m14-a-wake-counted-beside-another-unmasked-cause.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1181,7 +1181,6 @@
         if waited
             && messages > 0
             && causes & self.part.tx_done() != 0
-            && causes & (cause::ENABLED | cause::ENABLED_MSIX) == 0
         {
             self.counters.tx_wake_taken = self.counters.tx_wake_taken.saturating_add(1);
         }

m16-a-ring-never-written-back-is-never-refused.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1206,7 +1206,7 @@
         }
         if let (Some(Stranded { left, since }), true) = (self.stranded, self.link.is_up()) {
             let waited = self.clock.nanos().saturating_sub(since);
-            if waited >= STRANDED_DEADLINE_NANOS {
+            if false {
                 return Err(PassRefused::Stranded { left, after_nanos: waited });
             }
         }

m17-the-deadline-runs-with-the-link-down.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1204,7 +1204,7 @@
         if self.stranded.is_some() && self.link.is_up() {
             self.reclaim_tx();
         }
-        if let (Some(Stranded { left, since }), true) = (self.stranded, self.link.is_up()) {
+        if let (Some(Stranded { left, since }), _) = (self.stranded, self.link.is_up()) {
             let waited = self.clock.nanos().saturating_sub(since);
             if waited >= STRANDED_DEADLINE_NANOS {
                 return Err(PassRefused::Stranded { left, after_nanos: waited });

m18-a-second-link-change-counts-the-descriptors-again.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1195,7 +1195,7 @@
             if unsent > 0 {
                 // A second change over the same descriptors counts none of
                 // them twice, and the deadline runs from the newest.
-                let counted = self.stranded.map_or(0, |stranded| stranded.left);
+                let counted = 0;
                 self.counters.stranded =
                     self.counters.stranded.saturating_add((unsent - counted) as u32);
                 self.stranded = Some(Stranded { left: unsent, since: self.clock.nanos() });

m19-a-second-link-change-does-not-restart-the-deadline.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1198,7 +1198,7 @@
                 let counted = self.stranded.map_or(0, |stranded| stranded.left);
                 self.counters.stranded =
                     self.counters.stranded.saturating_add((unsent - counted) as u32);
-                self.stranded = Some(Stranded { left: unsent, since: self.clock.nanos() });
+                self.stranded = Some(Stranded { left: unsent, since: self.stranded.map_or_else(|| self.clock.nanos(), |stranded| stranded.since) });
             }
         }
         if self.stranded.is_some() && self.link.is_up() {

m21-a-descriptor-written-back-stays-owed.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1442,7 +1442,7 @@
             // back while any is owed is one of them.
             self.stranded = match self.stranded {
                 Some(Stranded { left, since }) if left > 1 => Some(Stranded { left: left - 1, since }),
-                _ => None,
+                other => other,
             };
         }
     }

m22-a-write-back-owed-on-a-link-that-is-down.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1220,7 +1220,7 @@
     pub fn pass_due_in(&self) -> Option<u64> {
         let Stranded { since, .. } = self.stranded?;
         let waited = self.clock.nanos().saturating_sub(since);
-        self.link.is_up().then(|| STRANDED_DEADLINE_NANOS.saturating_sub(waited))
+        Some(STRANDED_DEADLINE_NANOS.saturating_sub(waited))
     }
 
     /// Transmit descriptors published and not written back, the written-back

m23-the-deadline-is-never-due.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1220,7 +1220,7 @@
     pub fn pass_due_in(&self) -> Option<u64> {
         let Stranded { since, .. } = self.stranded?;
         let waited = self.clock.nanos().saturating_sub(since);
-        self.link.is_up().then(|| STRANDED_DEADLINE_NANOS.saturating_sub(waited))
+        false.then(|| STRANDED_DEADLINE_NANOS.saturating_sub(waited))
     }
 
     /// Transmit descriptors published and not written back, the written-back

m24-the-deadline-of-a-ring-written-back-stays.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1201,9 +1201,6 @@
                 self.stranded = Some(Stranded { left: unsent, since: self.clock.nanos() });
             }
         }
-        if self.stranded.is_some() && self.link.is_up() {
-            self.reclaim_tx();
-        }
         if let (Some(Stranded { left, since }), true) = (self.stranded, self.link.is_up()) {
             let waited = self.clock.nanos().saturating_sub(since);
             if waited >= STRANDED_DEADLINE_NANOS {

n01-virtio-room-without-reclaim.patch

--- a/userland/netstack/src/virtio_net.rs
+++ b/userland/netstack/src/virtio_net.rs
@@ -657,9 +657,6 @@
     /// and §2.7.7 has the device notify for every buffer it uses on such a
     /// queue.
     fn room(&mut self) -> usize {
-        while let Some((head, _)) = self.rings.poll_used() {
-            self.free.push(head);
-        }
         self.free.len()
     }

armC-no-room-after-a-frame-until-an-interrupt.patch (one run, reversed; ships in nothing)

--- a/userland/netstack/src/virtio_net.rs
+++ b/userland/netstack/src/virtio_net.rs
@@ -542,7 +542,10 @@
     /// `Card::begin_pass`'s call, made once for both drivers.
     pub fn take_interrupt(&self) -> Result<u32, SyscallError> {
         match self.dev.irq() {
-            Ok(record) => Ok(record.count),
+            Ok(record) => {
+                self.tx.borrow_mut().waiting = false;
+                Ok(record.count)
+            }
             Err(SyscallError::WouldBlock) => Ok(0),
             Err(why) => Err(why),
         }
@@ -642,11 +645,12 @@
     dma: Window,
     dma_device_addr: u64,
     doorbell: Doorbell,
+    waiting: bool,
 }
 
 impl TxQueue {
     fn new(rings: Rings, dma: Window, dma_device_addr: u64, doorbell: Doorbell) -> Self {
-        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell }
+        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell, waiting: false }
     }
 
     /// How many frames the queue takes now, every head the device has
@@ -657,6 +661,9 @@
     /// and §2.7.7 has the device notify for every buffer it uses on such a
     /// queue.
     fn room(&mut self) -> usize {
+        if self.waiting {
+            return 0;
+        }
         while let Some((head, _)) = self.rings.poll_used() {
             self.free.push(head);
         }
@@ -677,6 +684,7 @@
             NET_HDR_SIZE + len <= TX_BUF_SIZE,
             "netstack: a {len}-byte frame does not fit this NIC's transmit buffer"
         );
+        self.waiting = true;
         let head = self
             .free
             .pop()

armD-no-room-after-a-frame-ever.patch (one run, reversed; ships in nothing)

--- a/userland/netstack/src/virtio_net.rs
+++ b/userland/netstack/src/virtio_net.rs
@@ -642,11 +642,12 @@
     dma: Window,
     dma_device_addr: u64,
     doorbell: Doorbell,
+    waiting: bool,
 }
 
 impl TxQueue {
     fn new(rings: Rings, dma: Window, dma_device_addr: u64, doorbell: Doorbell) -> Self {
-        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell }
+        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell, waiting: false }
     }
 
     /// How many frames the queue takes now, every head the device has
@@ -657,6 +658,9 @@
     /// and §2.7.7 has the device notify for every buffer it uses on such a
     /// queue.
     fn room(&mut self) -> usize {
+        if self.waiting {
+            return 0;
+        }
         while let Some((head, _)) = self.rings.poll_used() {
             self.free.push(head);
         }
@@ -677,6 +681,7 @@
             NET_HDR_SIZE + len <= TX_BUF_SIZE,
             "netstack: a {len}-byte frame does not fit this NIC's transmit buffer"
         );
+        self.waiting = true;
         let head = self
             .free
             .pop()

armE-no-room-after-a-frame-until-an-interrupt-the-receive-queue-cannot-have-raised.patch (one run, reversed; ships in nothing)

diff --git a/userland/netstack/src/virtio_net.rs b/userland/netstack/src/virtio_net.rs
index 759dc1526..2d43f0baa 100644
--- a/userland/netstack/src/virtio_net.rs
+++ b/userland/netstack/src/virtio_net.rs
@@ -351,6 +351,8 @@ pub struct VirtioNet {
     tx: RefCell<TxQueue>,
     reported: Latch<u32>,
     mac: [u8; 6],
+    rx_seen: std::cell::Cell<u16>,
+    released: std::cell::Cell<bool>,
 }
 
 impl VirtioNet {
@@ -507,6 +509,8 @@ impl VirtioNet {
             tx: RefCell::new(TxQueue::new(tx, dma, dma_device_addr, tx_doorbell)),
             reported: Latch::default(),
             mac,
+            rx_seen: std::cell::Cell::new(0),
+            released: std::cell::Cell::new(false),
         };
 
         // Every receive buffer posted before a frame can arrive.
@@ -542,8 +546,26 @@ impl VirtioNet {
     /// `Card::begin_pass`'s call, made once for both drivers.
     pub fn take_interrupt(&self) -> Result<u32, SyscallError> {
         match self.dev.irq() {
-            Ok(record) => Ok(record.count),
-            Err(SyscallError::WouldBlock) => Ok(0),
+            Ok(record) => {
+                // Arm E: the release is an interrupt taken with the receive
+                // queue's `used.idx` where the pass before left it, and the
+                // first one with no receive buffer ever used — so it cannot
+                // have been the receive queue's.
+                let rx_used: u16 = self.rx.borrow().used.read(USED_IDX_OFF);
+                if rx_used == self.rx_seen.replace(rx_used) {
+                    assert!(
+                        self.released.replace(true) || rx_used == 0,
+                        "arm E: the first release came with receive used.idx at {rx_used}"
+                    );
+                    self.tx.borrow_mut().waiting = false;
+                }
+                Ok(record.count)
+            }
+            Err(SyscallError::WouldBlock) => {
+                let rx_used: u16 = self.rx.borrow().used.read(USED_IDX_OFF);
+                self.rx_seen.set(rx_used);
+                Ok(0)
+            }
             Err(why) => Err(why),
         }
     }
@@ -642,11 +664,12 @@ struct TxQueue {
     dma: Window,
     dma_device_addr: u64,
     doorbell: Doorbell,
+    waiting: bool,
 }
 
 impl TxQueue {
     fn new(rings: Rings, dma: Window, dma_device_addr: u64, doorbell: Doorbell) -> Self {
-        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell }
+        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell, waiting: false }
     }
 
     /// How many frames the queue takes now, every head the device has
@@ -657,6 +680,9 @@ impl TxQueue {
     /// and §2.7.7 has the device notify for every buffer it uses on such a
     /// queue.
     fn room(&mut self) -> usize {
+        if self.waiting {
+            return 0;
+        }
         while let Some((head, _)) = self.rings.poll_used() {
             self.free.push(head);
         }
@@ -677,6 +703,7 @@ impl TxQueue {
             NET_HDR_SIZE + len <= TX_BUF_SIZE,
             "netstack: a {len}-byte frame does not fit this NIC's transmit buffer"
         );
+        self.waiting = true;
         let head = self
             .free
             .pop()

armF-every-interrupt-record-says-which-used-ring-moved.patch (one run, reversed; ships in nothing)

diff --git a/userland/netstack/src/virtio_net.rs b/userland/netstack/src/virtio_net.rs
index 759dc1526..c2476ba21 100644
--- a/userland/netstack/src/virtio_net.rs
+++ b/userland/netstack/src/virtio_net.rs
@@ -351,6 +351,7 @@ pub struct VirtioNet {
     tx: RefCell<TxQueue>,
     reported: Latch<u32>,
     mac: [u8; 6],
+    seen: std::cell::Cell<(u16, u16, u32)>,
 }
 
 impl VirtioNet {
@@ -507,6 +508,7 @@ impl VirtioNet {
             tx: RefCell::new(TxQueue::new(tx, dma, dma_device_addr, tx_doorbell)),
             reported: Latch::default(),
             mac,
+            seen: std::cell::Cell::new((0, 0, 0)),
         };
 
         // Every receive buffer posted before a frame can arrive.
@@ -542,8 +544,24 @@ impl VirtioNet {
     /// `Card::begin_pass`'s call, made once for both drivers.
     pub fn take_interrupt(&self) -> Result<u32, SyscallError> {
         match self.dev.irq() {
-            Ok(record) => Ok(record.count),
-            Err(SyscallError::WouldBlock) => Ok(0),
+            Ok(record) => {
+                let rx: u16 = self.rx.borrow().used.read(USED_IDX_OFF);
+                let tx: u16 = self.tx.borrow().rings.used.read(USED_IDX_OFF);
+                let (was_rx, was_tx, nth) = self.seen.replace((rx, tx, self.seen.get().2 + 1));
+                crate::say!(
+                    "arm F: record {nth}: {} message(s), receive used.idx {was_rx} -> {rx}, transmit used.idx {was_tx} -> {tx}",
+                    record.count
+                );
+                assert!(nth < 12, "arm F: twelve records read");
+                self.tx.borrow_mut().waiting = false;
+                Ok(record.count)
+            }
+            Err(SyscallError::WouldBlock) => {
+                let rx: u16 = self.rx.borrow().used.read(USED_IDX_OFF);
+                let tx: u16 = self.tx.borrow().rings.used.read(USED_IDX_OFF);
+                self.seen.set((rx, tx, self.seen.get().2));
+                Ok(0)
+            }
             Err(why) => Err(why),
         }
     }
@@ -642,11 +660,12 @@ struct TxQueue {
     dma: Window,
     dma_device_addr: u64,
     doorbell: Doorbell,
+    waiting: bool,
 }
 
 impl TxQueue {
     fn new(rings: Rings, dma: Window, dma_device_addr: u64, doorbell: Doorbell) -> Self {
-        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell }
+        Self { rings, free: (0..TX_QUEUE_SIZE).rev().collect(), dma, dma_device_addr, doorbell, waiting: false }
     }
 
     /// How many frames the queue takes now, every head the device has
@@ -657,6 +676,9 @@ impl TxQueue {
     /// and §2.7.7 has the device notify for every buffer it uses on such a
     /// queue.
     fn room(&mut self) -> usize {
+        if self.waiting {
+            return 0;
+        }
         while let Some((head, _)) = self.rings.poll_used() {
             self.free.push(head);
         }
@@ -677,6 +699,7 @@ impl TxQueue {
             NET_HDR_SIZE + len <= TX_BUF_SIZE,
             "netstack: a {len}-byte frame does not fit this NIC's transmit buffer"
         );
+        self.waiting = true;
         let head = self
             .free
             .pop()

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 logs, at 12e4fb269, by step. Each is the command's own output, cut to its verdict lines where it is long; EXIT= is the command's exit code.

r2-i219-test.log

test result: ok. 88 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.60s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

r2-netstack-test.log

test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

r2-build-system-lib.log

test result: ok. 352 passed; 0 failed; 11 ignored; 0 measured; 0 filtered out; finished in 27.24s
EXIT=0

r2-harness-checks.log

test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
EXIT=0

r2-clippy-i219.log (exit 0)

    Finished `dev` profile [optimized + debuginfo] target(s) in 1.47s

r2-build-only.log (last lines)

21:45:54 Build finished.
EXIT=0

r2-guest.log

21:45:55   RUN   netstack_socket_churn
21:45:55   RUN   iommu_virtio_platform
21:46:10   PASS  netstack_socket_churn  (10s)
21:46:28   PASS  iommu_virtio_platform  (26s)
21:46:28 test result: ok. 2 passed, 2 total (33.6s; workers: 13s building, 36s testing)
EXIT=0

r2-mutations.log

m01-room-check-removed-from-reserve BUILD_EXIT=0
m01-room-check-removed-from-reserve TEST_EXIT=101
m02-room-without-reclaim BUILD_EXIT=0
m02-room-without-reclaim TEST_EXIT=101
m03-pass-leaves-the-cause-unmasked BUILD_EXIT=0
m03-pass-leaves-the-cause-unmasked TEST_EXIT=101
m04-wake-unmasks-nothing BUILD_EXIT=0
m04-wake-unmasks-nothing TEST_EXIT=101
m05-82574-classic-name-alone BUILD_EXIT=0
m05-82574-classic-name-alone TEST_EXIT=101
m06-i219-unmasks-bit-22-too BUILD_EXIT=0
m06-i219-unmasks-bit-22-too TEST_EXIT=101
m07-link-change-takes-no-ring-back BUILD_EXIT=0
m07-link-change-takes-no-ring-back TEST_EXIT=101
m08-room-with-the-link-down BUILD_EXIT=0
m08-room-with-the-link-down TEST_EXIT=101
m09-rearm-leaves-its-own-link-change BUILD_EXIT=0
m09-rearm-leaves-its-own-link-change TEST_EXIT=101
m10-rearm-loses-the-multicast-table BUILD_EXIT=0
m10-rearm-loses-the-multicast-table TEST_EXIT=101
m11-any-transmit-cause-counted-a-wake BUILD_EXIT=0
m11-any-transmit-cause-counted-a-wake TEST_EXIT=101
m12-wake-counted-with-the-link-down BUILD_EXIT=0
m12-wake-counted-with-the-link-down TEST_EXIT=101
n01-virtio-room-without-reclaim BUILD_EXIT=0
n01-virtio-room-without-reclaim TEST_EXIT=101
TREE_CLEAN_CHECK_DONE

Failing tests per mutation, from each r2-<mutation>.test.log:

r2-m01-room-check-removed-from-reserve: a_link_that_is_down_has_no_room a_full_transmit_ring_answers_room_0 a_ring_the_link_left_full_is_taken_back a_ring_that_cannot_be_taken_back_is_refused a_seeded_workload_loses_nothing_and_invents_nothing a_written_back_descriptor_returns_room the_transmit_cause_is_armed_only_when_asked test result: FAILED. 81 passed; 7 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.42s 
r2-m02-room-without-reclaim: a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told a_ring_the_link_left_full_is_taken_back a_seeded_workload_loses_nothing_and_invents_nothing a_written_back_descriptor_returns_room the_transmit_cause_is_armed_only_when_asked transmit_descriptors_are_reclaimed_under_batched_write_back test result: FAILED. 82 passed; 6 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.31s 
r2-m03-pass-leaves-the-cause-unmasked: the_transmit_cause_is_armed_only_when_asked test result: FAILED. 87 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.81s 
r2-m04-wake-unmasks-nothing: the_transmit_cause_is_armed_only_when_asked a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told test result: FAILED. 86 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s 
r2-m05-82574-classic-name-alone: the_transmit_cause_is_armed_only_when_asked a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told test result: FAILED. 86 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s 
r2-m06-i219-unmasks-bit-22-too: the_transmit_cause_is_armed_only_when_asked test result: FAILED. 87 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.87s 
r2-m07-link-change-takes-no-ring-back: a_ring_that_cannot_be_taken_back_is_refused a_ring_the_link_left_full_is_taken_back a_seeded_workload_loses_nothing_and_invents_nothing test result: FAILED. 85 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.60s 
r2-m08-room-with-the-link-down: a_link_that_is_down_has_no_room a_ring_the_link_left_full_is_taken_back a_seeded_workload_loses_nothing_and_invents_nothing test result: FAILED. 85 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.88s 
r2-m09-rearm-leaves-its-own-link-change: a_ring_the_link_left_full_is_taken_back test result: FAILED. 87 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.35s 
r2-m10-rearm-loses-the-multicast-table: a_ring_the_link_left_full_is_taken_back test result: FAILED. 87 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.82s 
r2-m11-any-transmit-cause-counted-a-wake: the_transmit_cause_is_armed_only_when_asked test result: FAILED. 87 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s 
r2-m12-wake-counted-with-the-link-down: a_link_that_is_down_has_no_room test result: FAILED. 87 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.64s 
r2-n01-virtio-room-without-reclaim: a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back test result: FAILED. 28 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 

r2-arms.log

armA-virtio-queue-of-one GUEST_EXIT=0
armB-queue-of-one-and-room-always GUEST_EXIT=0
armC-no-room-after-a-frame-until-an-interrupt GUEST_EXIT=0
armD-no-room-after-a-frame-ever GUEST_EXIT=1
ARMS_DONE

r2-armA-virtio-queue-of-one.guest.log

21:47:21   RUN   netstack_socket_churn
21:47:45   PASS  netstack_socket_churn  (10s)
21:47:45 test result: ok. 1 passed, 1 total (23.9s; workers: 14s building, 10s testing)

r2-armB-queue-of-one-and-room-always.guest.log

21:47:46   RUN   netstack_socket_churn
21:48:07   PASS  netstack_socket_churn  (11s)
21:48:07 test result: ok. 1 passed, 1 total (20.9s; workers: 10s building, 11s testing)

r2-armC-no-room-after-a-frame-until-an-interrupt.guest.log

21:48:11   RUN   netstack_socket_churn
21:48:36   PASS  netstack_socket_churn  (12s)
21:48:36 test result: ok. 1 passed, 1 total (25.5s; workers: 14s building, 12s testing)

r2-armD-no-room-after-a-frame-ever.guest.log

21:48:37   RUN   netstack_socket_churn
21:49:08 FAIL netstack_socket_churn: STALLED: waiting for netstack's lease — it went quiet
21:49:08     netstack_socket_churn: STALLED: waiting for netstack's lease — it went quiet
21:49:08 test result: FAILED. 0 passed, 1 failed, 0 invalidated, 1 total (30.4s; workers: 11s building, 20s testing)

r2-spelled-armB.guest.log (fn room(&self) -> bool { true }, exit 1: the image does not build)

error: methods `tx_room` and `wake_on_room` are never used
    = note: `-D dead-code` implied by `-D warnings`
error: methods `tx_room` and `wake_on_room` are never used
error: method `tx_room` is never used
error: method `room` is never used
error: could not compile `netstack` (bin "netstack") due to 4 previous errors
21:50:42 test result: FAILED. 0 passed, 1 failed, 0 invalidated, 1 total (4.3s; workers: 4s building, 3ms testing)
error: test failed, to rerun pass `--test toyos-build`

Host load (uptime, load averages) around the guest run and around each arm:

guest: 34.28 49.28 47.67;33.79 48.68 47.48;36.14 47.54 47.10;
r2-armA-virtio-queue-of-one: 51.70 50.35 48.19;50.22 50.24 48.22;
r2-armB-queue-of-one-and-room-always: 50.22 50.24 48.22;45.49 49.02 47.83;
r2-armC-no-room-after-a-frame-until-an-interrupt: 45.49 49.02 47.83;49.25 49.74 48.13;
r2-armD-no-room-after-a-frame-ever: 49.25 49.74 48.13;41.55 47.61 47.41;

The driver tests, netstack tests and both lints did not append their exit to their log; the invoking shell printed I219_TEST_EXIT=0, NETSTACK_TEST_EXIT=0, CLIPPY_I219_EXIT=0, CLIPPY_NETSTACK_EXIT=101 (the nine findings main has).

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 12e4fb269 against origin/main at a4416fe6c, round 2. Read only: no build, no guest, no metal. Read: the round's diff (882a8bacb..12e4fb269), the changed files whole, the step logs, and the three Intel documents the implementer fetched, as text.

Net lines, branch: +1440 / −710. Production +885 / −583, of which the moves commit is +288 / −274; tests and stub +475 / −55; issues +80 / −72. The round alone: +919 / −396, about 200 of each being open's bring-up moved into program and rings_and_mask so it can run twice. The growth is the re-arm, the four counts and TxQueue; accepted as far as the BLOCKERs below leave it standing.

Round 1's BLOCKERs

  • host at the head — OPEN. gh pr checks at 12e4fb269: host, toolchain, guest all skipping. The body says so.
  • the guest suite — OPEN. Two of the suite ran (r2-guest.log, exit 0); the suite has not.
  • no hardware has read the Intel path — OPEN. No T14 boot exists. The half about the model is closed: the stub's raise now delivers the I219's classic causes in MSI mode and PARTS runs the room and wake tests on both parts.
  • the virtio refusal is measured nowhere — CLOSED for the refusal. a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back and a_frame_offered_to_a_full_transmit_queue_is_not_taken drive TxQueue over a plain allocation and read the avail ring as the device does; n01 exits 101 with the first red. Arm D exits 1 ("STALLED: waiting for netstack's lease"), so DmaNic::transmit does ask before it sends; arm C exits 0, so a refused frame is kept and leaves later. Arms A and B were my design and prove nothing, as the body shows: B green. What C leaves unseparated is a new BLOCKER below.
  • the issue's exit names a reading nothing produces — OPEN. The four keys exist and the exit names them, but transmit.wake_taken does not read what the exit needs of it: first BLOCKER below.
  • the link-down unknown has no test and no exit clause — CLOSED as written: the driver takes the ring back, the stub strands a ring by a permit, and m07 exits 101 with a_ring_the_link_left_full_is_taken_back red. The means it was closed by is the second BLOCKER below.
  • "virtio's full queue is exercised by no test" unrecorded — CLOSED by the two host tests.

Round 1's NOTEs: the cause is per part (Part::tx_done; m05, m06 exit 101); Counters::too_long is gone and the refusal stays; the five sentences I asked to have verified are in the fetched text of 317694 revision 3.4 where the sites quote them (§7.4.3's "an interrupt is signaled when unmasked bits in this register are set", TCTL.EN's "Software should combine this with a reset", TDH's "after a reset (hardware reset or CTRL.RST) and before enabling the transmit function", TxQ0 "Indicates transmit queue 0 write back", TXDCTL's "When GRAN=1b all descriptors are written back"). The mutation that stayed green on its first run is m09 (the re-arm leaves its own LSC in ICR); r2-m09…test.log now reds a_ring_the_link_left_full_is_taken_back at "E82574, strands true, seen down false: the bring-up's own link change took the ring back again", exit 101.

BLOCKER

  • toyos-i219/src/lib.rs:1171 — transmit.wake_taken counts any pass, not the wake — the condition is waited && causes & tx_done != 0, and 317694 §7.4.3 says "The cause bit stores the interrupt event regardless of the state of the mask bit": TXDW is in ICR after every write-back whether or not the part raised a message for it. A pass begun by anything else while the cause is armed counts a wake: a received frame, a client's request (main.rs:1530-1561: the T14 job's own next send, and the inspect request that asks for the counts), the 1 ms wait of issues/netstack-passes-every-millisecond-while-a-request-is-pending.md, mDNS, the resolver. So on a part that raises nothing for an unmasked TXDW, which is exactly what issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md says is taken on trust, the exit's "transmit.wake_taken at least 1" is still met and the issue closes unread. issues/toyos-i219-refuses-a-part-outside-msi-x-mode-at-ivar.md is still open on whether any counted interrupt has been seen from that function. The driver has what it needs in hand: messages and causes. Count a wake only on a pass that took a message and read no other unmasked cause (messages > 0, causes & (ENABLED | ENABLED_MSIX) == 0, the transmit cause set), so the message can only have been that cause's. The test it needs, red at this head: in the_transmit_cause_is_armed_only_when_asked, after wake_on_room() answered 0 and nic.run(), a part that recorded the cause and sent no message for it (the claim's pending count dropped, or a permit for it), then one_pass: waits must read (1, 1, 0); this head reads (1, 1, 1). m11 holds waited and nothing holds the message.
  • toyos-i219/src/lib.rs:1182, :1202 — on the I219 the re-arm resets the function exactly when its transmit ring is not empty, and nothing read says that is safe on that part — the two sentences the site quotes are the 82574's. I searched the fetched text of 612523 and 631120: neither says anything about a reset over published descriptors. The one public source I know that does speak says the opposite, and I have it from memory, not from a document in reach: Intel's own e1000e driver (netdev.c, e1000_flush_desc_rings, for the PCH parts from Sunrise Point on, which the T14's is) carries a comment that on the I219 the descriptor rings must be emptied before the hardware is reset and that failing to leaves the part in a hang only a PCI reset releases; it reads a status bit in the function's configuration space and, where that asks for it, pushes one dummy descriptor through with the transmitter enabled before CTRL.RST. The tree's own issues/the-t14-hung-after-rebooting-with-its-i219-faulted.md is a T14 freeze with that part left holding live rings. Against what the re-arm replaces: a stalled ring was a card that sends nothing; a hung function is a card nothing but a power cycle brings back, and netstack dead by PassRefused::Rearm. The stub cannot red on this: it models 317694. This is the cheap reading the branch built past: read that function in the driver's source. If it says what I remember, the I219's arm of the re-arm does that sequence before its reset, the stub models the state that asks for it, and a test reds without it; if it does not, the body says what the source says instead and this falls to a NOTE. Either way the answer to "is the full reset the documented safe sequence" is: for the 82574, yes, by the two quoted sentences; for the I219, not shown.
  • userland/netstack/src/virtio_net.rs:654-665, card.rs:149-151 — that QEMU's transmit-completion interrupt ends the sleep is still a guess on the path every guest and the shipped image run — arm C releases on any interrupt taken, and in the churn test a reply follows every frame, so a device that never notified for a used transmit buffer passes C. Virtio 1.2 §2.7.7 is the oracle and one run reads it. Arm E: arm C, with take_interrupt clearing waiting only where the receive queue's used.idx has not moved since the previous pass, so the interrupt taken can only be the transmit queue's; netstack_socket_churn must stay green (the DISCOVER's own completion releases it before any OFFER exists). Post its log beside C's and D's. Nothing ships for it.

NOTE

  • what the T14 boot must show, at a head that carries the corrected count — the netstack row names pci:8086:15fc; the bring-up lines; "link up at 1000 Mb/s full duplex" with its "ms after the driver came up" beside the 2.72 to 2.82 s the earlier runs read, because at this head a down link has no room, main.rs:1515 then takes no deadline of smoltcp's, and the pass that finds the link is LSC's message or nothing; a lease; no "cannot be driven on", no "offered to a transmit ring that refuses it", no "the link changed over unsent frames" line (a part that raises LSC twice at link-up with the DISCOVER in flight would reset itself on every link-up, and this is the boot that says whether it does); then transmit.full, transmit.wake_armed and transmit.wake_taken each at least 1, the gap between armed and taken stated, descriptors.unsent 0, descriptors.sent equal to wire.sent. A boot whose transmit.full reads 0 has read nothing and closes nothing: a datagram socket queues at most 16 (main.rs:679) against 15 descriptors, and at 1000 Mb/s a full-size frame is off the ring about 12 µs after it is published, so 60 datagrams from one socket, one request each, may never find the ring full. The job queues more than a ring from several sockets before one pass.
  • what that boot cannot show — the reset over unsent frames on either part; anything of the 82574, TXQ0 included. With the second BLOCKER closed by the vendor driver's sequence the link-down path would rest on the stub and that source, and the issue's "Not in the exit" paragraph is then an honest record. Resting on the stub alone it is not acceptable for the I219.
  • issues/the-intel-driver-writes-txdctl-and-tidv-against-the-82574-datasheets-newer-revision.md — filing is right for this stage. Both sentences are in revision 3.4 as quoted (TXDCTL "Rsv 22 0x0 Reserved. Must be set to 1b for proper operation"; TIDV "A value of 0 is not allowed"). TIDV's 0 is the register's reset value and the same section limits the timer to descriptors with IDE set, which the driver never publishes: inert. Bit 22 is in the register the wake's write-back policy lives in, the bring-up has always written it clear, and the T14 has sent frames that way; the boot above is the reading of a write-back per descriptor with the bit clear on the I219. If that boot shows armed ahead of taken, or descriptors.sent short of wire.sent, bit 22 is this stage's and not stage 2's. The issue is narrower than what is public: from memory, the same vendor driver sets bit 22 of TXDCTL on the PCH parts as well as the 82574 family; verify there and say so in the issue, whose exit names the 82574 alone. Its owner exists (issues/the-lan-is-not-yet-production-grade.md, "2. A gigabit driver that holds") and its exit is one a test can fail.
  • TX_DONE on the I219 as TXDW alone — sound as far as documents go: the function has MSI and no MSI-X, bit 22 is touched for nothing, and it agrees with what round 1 recalled of the public register map. It is confirmed only by the corrected transmit.wake_taken above 0 on the T14.
  • the re-arm, read from where it runs — bounded: every wait in reset, phy::wake and phy::bring_up has a deadline and this round adds no wait; the fixed ones are the cited ones open already had. It runs with every cause masked (reset writes IMC on both sides), tx_wake is already taken, both rings are republished with zeroed status over the same grant, the IOMMU domain, bus mastering and the MSI capability are configuration space a CTRL.RST does not reach, and the station address is the same NVM reload open reads. No Frame is alive across begin_pass: smoltcp consumes its receive token inside poll. A narrower re-arm is not the 82574 document's: TCTL.EN cleared leaves the FIFO, and the head is software's to write only after a reset.
  • flapping — no storm of the driver's own making: a reset needs LSC with a descriptor unsent, nothing is published while the link reads down, and the re-arm clears the LSC its own bring-up raised before it reads the link (m09). The wire can still buy one reset per down-edge under traffic, which on the I219 is a pass as long as a bring-up in which netstack serves no client, and a second negotiation. Losing frames received and not yet handed up at that edge is acceptable: the link that carried their answers is gone. A LSC read with the link up on both sides also resets and drops up to a ring of good frames; the body should say so.
  • toyos-i219/src/lib.rs:1202 — the reset clears the MAC's statistics registers and rearm does not take them first, so after any re-arm wire.sent is short of descriptors.sent by what was sent since the last inspect, which reads as lost frames. self.wire() before program.
  • toyos-i219/src/lib.rs:1179 — the || !self.link.is_up() refresh is kept for a link whose LSC "is never delivered", and with the link down nothing now begins the pass it would run on. Either that case exists and is a dead network until a client speaks, or the arm is dead code. The T14 boot's link-up time says which; then one of the two goes.
  • userland/netstack/src/main.rs:82, :1515 — receive's refusal and the loop's guard are still read by no test; D reads transmit's alone. No host test can build DmaNic over a Card, and both go with it at the move. Still accepted, on that ground only: the stack that replaces it carries its own test of a frame not taken off the receive ring without room to answer it.
  • evidence — the logs comment is each step's test result line, with the exits printed by the invoking shell and not in the log. I read the full logs and they agree, but that is not what a landing stands on: host carries the driver's, netstack's and the build system's tests, and its log is the record.
  • host at the head that lands — run, not skipped, exit 0, with the workspace tests (toyos-i219's among them), the userland/netstack step, clippy clean and every "apps for" step. guest / suite — the whole suite green at that head, run. Both need the draft marked ready.
  • may the orchestrator land on reading host, guest / suite and the T14 boot — not from this head. The three BLOCKERs above change the driver and add a measurement; that head is reviewed for what changed. After a round that leaves no BLOCKER but the three evidence ones, yes: he reads the two checks and the boot against the list above.

SEND BACK

Japabu and others added 2 commits October 9, 2026 00:24
…one, and the I219 is never reset over a ring it holds

`transmit.wake_taken` counted any pass that read the transmit cause while it
was unmasked, and §7.4.3 keeps a cause in `ICR` whether or not a message was
raised for it: a part that raises nothing for an unmasked `TXDW` still met the
hardware issue's exit. A wake is now a pass a message began whose only unmasked
cause is the transmit one. The test was red at 12e4fb2 with `(1, 1, 1)` where
it wants `(1, 1, 0)`.

The re-arm reset the function exactly when its transmit ring was not empty, on
the 82574's two sentences. Nothing Intel publishes that could be found says
that is safe on the PCH's MAC, and nothing says what the sequence would be:
the I219 and PCH datasheets, five PCH specification updates and the I218's are
silent on a reset over published descriptors. So the I219 is no longer reset
there. The descriptors a link change finds unsent stay the part's, counted
`descriptors.stranded`; it is owed their write-back within the slowest time
§10.2.6.1 lets a full ring leave, from the pass that read the link up; the
ring goes on when they come, and a part that has not written them back is
refused by name. netstack takes that deadline into its wait, since a part that
never writes back sends nothing to say so. The 82574 keeps its reset, and its
document turned out to say what it does otherwise: the Defer Count counts a
transmit deferred because "The link is not up".

The reset takes the MAC's statistics, so the re-arm reads them first. The
link is refreshed on `LSC` alone: the cause is recorded masked or not, so the
arm kept for a link change that raises none had no case.

`issues/no-machine-has-read-an-intel-nic-across-a-link-change.md` records what
is unknown of the I219 and that `open`'s reset has the same exposure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu Japabu changed the title Both NIC drivers say their transmit room and wake netstack when it returns; no frame is dropped past the ring, and a link change gives the ring back Both NIC drivers say their transmit room and wake netstack when it returns; the I219 is never reset over a ring it holds Oct 8, 2026
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3 logs, at 3605c5b3f unless a log's first line says otherwise. Each block is the command, its output and EXIT= with the command's own exit code as its last line; a path under the checkout reads <worktree>. Where a block is cut it says to what.

r3-01-red-first-wake-count (the round-3 test and stub hook over the driver of 12e4fb269; whole)

$ cargo test -p toyos-i219 the_transmit_cause_is_armed_only_when_asked   # the driver at 12e4fb269, the round-3 test and stub hook
uptime: load averages: 48.31 62.77 59.13
   Compiling toyos-i219 v0.1.0 (<worktree>/toyos-i219)
    Finished `test` profile [optimized + debuginfo] target(s) in 12.42s
     Running unittests src/lib.rs (target/debug/deps/toyos_i219-b5aed6d9feab4306)

running 1 test
test tests::the_transmit_cause_is_armed_only_when_asked ... FAILED

failures:

---- tests::the_transmit_cause_is_armed_only_when_asked stdout ----

thread 'tests::the_transmit_cause_is_armed_only_when_asked' (192105063) panicked at toyos-i219/src/tests.rs:749:9:
assertion `left == right` failed: seed 24: a cause no message came for was counted a wake
  left: (0, (1, 1, 1))
 right: (0, (1, 1, 0))
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::the_transmit_cause_is_armed_only_when_asked

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 87 filtered out; finished in 0.00s

error: test failed, to rerun pass `-p toyos-i219 --lib`
EXIT=101

r3-02-i219-test (whole)

$ cargo test -p toyos-i219   # at 3605c5b3f
uptime: load averages: 77.83 102.98 90.95
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Finished `test` profile [optimized + debuginfo] target(s) in 1.27s
     Running unittests src/lib.rs (target/debug/deps/toyos_i219-b5aed6d9feab4306)

running 90 tests
test tests::a_claim_that_stops_answering_is_handed_up_and_not_read_as_quiet ... ok
test tests::a_buffer_given_back_out_of_turn_does_not_carry_the_tail_over_an_older_one ... ok
test tests::a_flag_another_agent_holds_is_neither_asked_over_nor_cleared ... ok
test tests::a_flag_that_never_comes_does_not_stop_the_reset ... ok
test tests::a_frame_longer_than_a_transmit_buffer_is_refused_and_not_truncated ... ok
test tests::a_frame_goes_out_whole ... ok
test tests::a_link_drop_does_not_lose_the_ring ... ok
test tests::a_lying_length_is_refused_and_the_frames_behind_it_still_arrive ... ok
test tests::a_grant_that_never_comes_is_refused_by_name_and_the_request_withdrawn ... ok
test tests::a_frame_arrives_whole ... ok
test tests::a_flag_the_part_already_let_go_is_not_written_again ... ok
test tests::a_part_is_told_which_vector_each_cause_uses ... ok
test tests::a_part_that_does_not_take_ivar_is_refused ... ok
test tests::a_part_that_does_not_take_rctl_is_refused ... ok
test tests::a_part_with_no_station_address_is_refused ... ok
test tests::a_partner_on_an_unconfigured_phy_raises_no_link ... ok
test tests::a_phy_in_reach_is_asked_once_and_left_powered ... ok
test tests::a_phy_left_in_loopback_or_configured_by_hand_raises_no_link ... ok
test tests::a_link_that_is_down_has_no_room ... ok
test tests::a_full_transmit_ring_answers_room_0 ... ok
test tests::a_part_that_grants_over_the_engines_standing_bit_is_brought_up ... ok
test tests::a_ladder_stopped_on_smbus_leaves_the_mac_off_it ... ok
test tests::a_multicast_group_sets_the_one_table_bit_its_address_hashes_to ... ok
test tests::a_phy_that_is_not_the_one_this_map_describes_is_refused_by_its_identifier ... ok
test tests::a_refused_descriptor_does_not_carry_the_tail_over_a_frame_still_held ... ok
test tests::a_register_nothing_decodes_is_refused_and_never_written ... ok
test tests::a_part_that_takes_none_of_the_datasheets_latitudes_is_brought_up_the_same_way ... ok
test tests::a_phy_left_in_low_power_link_up_links_at_a_gigabit ... ok
test tests::a_phy_reset_restores_the_defaults_and_not_the_state_the_claim_inherited ... ok
test tests::a_ring_the_link_left_full_is_taken_back ... ok
test tests::a_phy_left_out_of_reach_is_climbed_to_before_the_reset ... ok
test tests::a_ring_that_cannot_be_taken_back_is_refused ... ok
test tests::a_grant_that_comes_late_inside_the_bound_is_taken ... ok
test tests::a_window_that_reads_ones_is_refused ... ok
test tests::a_spurious_interrupt_costs_a_pass_and_nothing_else ... ok
test tests::a_window_or_grant_too_small_is_refused ... ok
test tests::a_reset_that_never_finishes_is_refused ... ok
test tests::a_seeded_workload_loses_nothing_and_invents_nothing ... ok
test tests::bring_up_programs_what_the_initialization_sections_name ... ok
test tests::causes_are_acknowledged_by_writing_them_back ... ok
test tests::a_platform_that_asks_for_low_power_link_up_in_d0a_gets_it ... ok
test tests::a_release_over_a_window_of_ones_writes_nothing ... ok
test tests::a_ring_the_pch_mac_never_writes_back_is_refused ... ok
test tests::a_written_back_descriptor_returns_room ... ok
test tests::every_reading_of_the_other_two_ownership_bits_names_who_stands_beside_the_flag ... ok
test tests::frames_come_up_in_order_under_batched_and_reordered_write_back ... ok
test tests::null_descriptor_padding_is_not_a_frame ... ok
test tests::one_pass_hands_up_at_most_its_budget ... ok
test tests::ones_inside_the_grant_wait_do_not_leave_the_request_standing ... ok
test tests::ones_on_the_way_out_do_not_leave_the_flag_standing ... ok
test tests::ones_through_the_whole_release_bound_write_nothing ... ok
test tests::quiesce_stops_what_a_previous_holder_left_running ... ok
test tests::refusals::a_descriptor_with_nothing_in_it_is_refused ... ok
test tests::refusals::a_descriptor_without_end_of_packet_is_refused ... ok
test tests::every_ask_carries_the_parts_own_word_and_keeps_the_pace ... ok
test tests::refusals::a_frame_the_hardware_reported_an_error_on_is_refused ... ok
test tests::refusals::more_bytes_than_the_buffer_holds_is_refused ... ok
test tests::the_82574s_own_phy_register_map_is_refused_by_name ... ok
test tests::the_driver_and_the_macs_statistics_count_the_same_frames ... ok
test tests::an_mdi_read_the_part_could_not_complete_is_refused_by_name ... ok
test tests::the_head_register_does_run_ahead_of_memory ... ok
test tests::the_link_going_away_and_coming_back_is_seen ... ok
test tests::the_oem_bits_carry_the_platforms_d0a_policy_and_nothing_else ... ok
test tests::an_interconnect_in_transition_is_waited_out_and_never_written_over ... ok
test tests::the_ownership_claim_leaves_the_rest_of_the_register_standing ... ok
test tests::the_oem_bits_restart_follows_the_control_restart_where_a_phy_reset_is_allowed ... ok
test tests::a_master_that_never_goes_quiet_does_not_stop_the_bring_up ... ok
test tests::host_wake_up_left_armed_is_cleared_behind_the_reset ... ok
test tests::the_phy_is_brought_up_and_the_mac_sees_the_link_it_raises ... ok
test tests::the_receiver_reporting_on_itself_is_counted ... ok
test tests::the_refusal_names_each_agent_standing_beside_the_flag ... ok
test tests::the_refusal_names_the_last_reading_before_the_deadline ... ok
test tests::the_pchs_mac_gets_its_three_registers_and_the_82574_none ... ok
test tests::the_power_step_is_what_lets_an_mdi_cycle_run_at_all ... ok
test tests::transmit_descriptors_are_reclaimed_under_batched_write_back ... ok
test tests::what_one_power_reading_decides ... ok
test tests::what_the_wake_and_the_full_reset_write ... ok
test tests::a_request_registered_while_the_engine_holds_it_is_still_granted ... ok
test tests::the_transmit_cause_is_armed_only_when_asked ... ok
test tests::the_phy_is_found_at_whichever_address_answers_its_identifier ... ok
test tests::the_pch_mac_keeps_the_ring_a_link_change_left_full ... ok
test tests::a_phy_reset_the_firmware_blocks_is_never_issued ... ok
test tests::a_mac_that_keeps_its_phy_down_is_a_phy_no_cycle_reaches ... ok
test tests::a_phy_no_rung_reaches_is_refused_after_the_reset ... ok
test tests::an_mdi_transaction_that_never_reports_ready_is_refused_by_name ... ok
test tests::the_word_to_the_firmware_goes_with_the_driver ... ok
test tests::no_two_accesses_to_the_arbitration_are_nearer_than_the_pace ... ok
test tests::the_interface_is_given_back_on_every_path_that_took_it ... ok
test tests::the_advertised_abilities_are_what_the_link_resolves_to ... ok
test tests::a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told ... ok

test result: ok. 90 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.22s

   Doc-tests toyos_i219

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

EXIT=0

r3-03-clippy-i219 (whole)

$ cargo clippy -p toyos-i219 --all-targets -- -W clippy::checked_conversions -W clippy::default_trait_access -W clippy::manual_midpoint -W clippy::redundant_clone -W clippy::unchecked_time_subtraction -W clippy::unnecessary_semicolon -D warnings   # at 3605c5b3f
    Checking toyos-i219 v0.1.0 (<worktree>/toyos-i219)
    Finished `dev` profile [optimized + debuginfo] target(s) in 2.60s
EXIT=0

r3-04-build-only (first 2 and last 6 lines of 493)

$ cargo run -- --build-only   # at 3605c5b3f
uptime: load averages: 81.23 61.42 58.24
...
22:44:40 root: symlink 'bin/tone' -> '/system/bin/toybox'
22:44:42 Signed with this checkout's throwaway key SHA256:<key> at version 1791498915
22:44:46 Build finished.
Boot image: <worktree>/target/bootable.img
EXIT=0
uptime: load averages: 114.22 105.42 84.46

r3-05-mutations (whole)

$ sh run-mutations.sh <worktree> <mutations>   # at 3605c5b3f
uptime: load averages: 73.44 76.23 80.83
m01-room-check-removed-from-reserve BUILD_EXIT=0
m01-room-check-removed-from-reserve TEST_EXIT=101
m02-room-without-reclaim BUILD_EXIT=0
m02-room-without-reclaim TEST_EXIT=101
m03-pass-leaves-the-cause-unmasked BUILD_EXIT=0
m03-pass-leaves-the-cause-unmasked TEST_EXIT=101
m04-wake-unmasks-nothing BUILD_EXIT=0
m04-wake-unmasks-nothing TEST_EXIT=101
m05-82574-classic-name-alone BUILD_EXIT=0
m05-82574-classic-name-alone TEST_EXIT=101
m06-i219-unmasks-bit-22-too BUILD_EXIT=0
m06-i219-unmasks-bit-22-too TEST_EXIT=101
m07-82574-link-change-takes-no-ring-back BUILD_EXIT=0
m07-82574-link-change-takes-no-ring-back TEST_EXIT=101
m08-room-with-the-link-down BUILD_EXIT=0
m08-room-with-the-link-down TEST_EXIT=101
m09-rearm-leaves-its-own-link-change BUILD_EXIT=0
m09-rearm-leaves-its-own-link-change TEST_EXIT=101
m10-rearm-loses-the-multicast-table BUILD_EXIT=0
m10-rearm-loses-the-multicast-table TEST_EXIT=101
m11-a-wake-counted-on-a-pass-nobody-waited-on BUILD_EXIT=0
m11-a-wake-counted-on-a-pass-nobody-waited-on TEST_EXIT=101
m12-wake-counted-with-the-link-down BUILD_EXIT=0
m12-wake-counted-with-the-link-down TEST_EXIT=101
m13-a-wake-counted-without-its-message BUILD_EXIT=0
m13-a-wake-counted-without-its-message TEST_EXIT=101
m14-a-wake-counted-beside-another-unmasked-cause BUILD_EXIT=0
m14-a-wake-counted-beside-another-unmasked-cause TEST_EXIT=101
m15-the-i219-is-reset-over-the-ring BUILD_EXIT=0
m15-the-i219-is-reset-over-the-ring TEST_EXIT=101
m16-a-ring-never-written-back-is-never-refused BUILD_EXIT=0
m16-a-ring-never-written-back-is-never-refused TEST_EXIT=101
m17-the-deadline-runs-with-the-link-down BUILD_EXIT=0
m17-the-deadline-runs-with-the-link-down TEST_EXIT=101
m18-a-second-link-change-counts-the-descriptors-again BUILD_EXIT=0
m18-a-second-link-change-counts-the-descriptors-again TEST_EXIT=101
m19-a-second-link-change-does-not-restart-the-deadline BUILD_EXIT=0
m19-a-second-link-change-does-not-restart-the-deadline TEST_EXIT=101
m20-the-reset-takes-the-statistics BUILD_EXIT=0
m20-the-reset-takes-the-statistics TEST_EXIT=101
m21-a-descriptor-written-back-stays-owed BUILD_EXIT=0
m21-a-descriptor-written-back-stays-owed TEST_EXIT=101
m22-a-write-back-owed-on-a-link-that-is-down BUILD_EXIT=0
m22-a-write-back-owed-on-a-link-that-is-down TEST_EXIT=101
m23-the-deadline-is-never-due BUILD_EXIT=0
m23-the-deadline-is-never-due TEST_EXIT=101
m24-the-deadline-of-a-ring-written-back-stays BUILD_EXIT=0
m24-the-deadline-of-a-ring-written-back-stays TEST_EXIT=101
n01-virtio-room-without-reclaim BUILD_EXIT=0
n01-virtio-room-without-reclaim TEST_EXIT=101
git status --porcelain --ignore-submodules=none:
TREE_CLEAN_CHECK_DONE
EXIT=0
uptime: load averages: 68.42 70.90 77.00

tests red per mutation, from each mutation's own test log

r3-m01-room-check-removed-from-reserve.test.log: EXIT=101: a_link_that_is_down_has_no_room a_full_transmit_ring_answers_room_0 a_ring_that_cannot_be_taken_back_is_refused a_ring_the_link_left_full_is_taken_back a_seeded_workload_loses_nothing_and_invents_nothing a_written_back_descriptor_returns_room a_ring_the_pch_mac_never_writes_back_is_refused the_transmit_cause_is_armed_only_when_asked the_pch_mac_keeps_the_ring_a_link_change_left_full
r3-m02-room-without-reclaim.test.log: EXIT=101: a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told a_ring_the_link_left_full_is_taken_back a_seeded_workload_loses_nothing_and_invents_nothing a_written_back_descriptor_returns_room the_transmit_cause_is_armed_only_when_asked transmit_descriptors_are_reclaimed_under_batched_write_back
r3-m03-pass-leaves-the-cause-unmasked.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r3-m04-wake-unmasks-nothing.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told
r3-m05-82574-classic-name-alone.test.log: EXIT=101: a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told the_transmit_cause_is_armed_only_when_asked
r3-m06-i219-unmasks-bit-22-too.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r3-m07-82574-link-change-takes-no-ring-back.test.log: EXIT=101: a_ring_that_cannot_be_taken_back_is_refused a_ring_the_link_left_full_is_taken_back a_seeded_workload_loses_nothing_and_invents_nothing
r3-m08-room-with-the-link-down.test.log: EXIT=101: a_link_that_is_down_has_no_room a_ring_the_link_left_full_is_taken_back a_seeded_workload_loses_nothing_and_invents_nothing
r3-m09-rearm-leaves-its-own-link-change.test.log: EXIT=101: a_ring_the_link_left_full_is_taken_back
r3-m10-rearm-loses-the-multicast-table.test.log: EXIT=101: a_ring_the_link_left_full_is_taken_back
r3-m11-a-wake-counted-on-a-pass-nobody-waited-on.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r3-m12-wake-counted-with-the-link-down.test.log: EXIT=101: a_link_that_is_down_has_no_room
r3-m13-a-wake-counted-without-its-message.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r3-m14-a-wake-counted-beside-another-unmasked-cause.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r3-m15-the-i219-is-reset-over-the-ring.test.log: EXIT=101: a_ring_the_pch_mac_never_writes_back_is_refused the_pch_mac_keeps_the_ring_a_link_change_left_full
r3-m16-a-ring-never-written-back-is-never-refused.test.log: EXIT=101: a_ring_the_pch_mac_never_writes_back_is_refused
r3-m17-the-deadline-runs-with-the-link-down.test.log: EXIT=101: a_ring_the_pch_mac_never_writes_back_is_refused
r3-m18-a-second-link-change-counts-the-descriptors-again.test.log: EXIT=101: a_ring_the_pch_mac_never_writes_back_is_refused the_pch_mac_keeps_the_ring_a_link_change_left_full
r3-m19-a-second-link-change-does-not-restart-the-deadline.test.log: EXIT=101: a_ring_the_pch_mac_never_writes_back_is_refused
r3-m20-the-reset-takes-the-statistics.test.log: EXIT=101: a_ring_the_link_left_full_is_taken_back
r3-m21-a-descriptor-written-back-stays-owed.test.log: EXIT=101: the_pch_mac_keeps_the_ring_a_link_change_left_full
r3-m22-a-write-back-owed-on-a-link-that-is-down.test.log: EXIT=101: the_pch_mac_keeps_the_ring_a_link_change_left_full
r3-m23-the-deadline-is-never-due.test.log: EXIT=101: a_ring_the_pch_mac_never_writes_back_is_refused the_pch_mac_keeps_the_ring_a_link_change_left_full
r3-m24-the-deadline-of-a-ring-written-back-stays.test.log: EXIT=101: the_pch_mac_keeps_the_ring_a_link_change_left_full
r3-n01-virtio-room-without-reclaim.test.log: EXIT=101: virtio_net::tests::a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back

r3-06-netstack-test (whole)

$ cargo test --manifest-path userland/netstack/Cargo.toml   # at 3605c5b3f
uptime: load averages: 69.68 100.40 90.18
   Compiling netstack v0.0.0 (<worktree>/userland/netstack)
    Finished `test` profile [optimized + debuginfo] target(s) in 2.72s
     Running unittests src/main.rs (target/debug/deps/netstack-a95aecfc537c00aa)

running 29 tests
test listen::tests::a_connection_closed_by_both_ends_is_spent ... ok
test listen::tests::a_cut_connection_whose_neighbour_entry_ran_out_is_kept_until_its_reset_leaves ... ok
test listen::tests::a_cut_connection_whose_next_hop_answers_no_arp_is_let_go_after_the_reset_bound ... ok
test listen::tests::a_finished_handshake_is_owed_one_wake ... ok
test listen::tests::a_peer_gone_silent_is_reset_at_the_ceiling ... ok
test listen::tests::a_peer_that_answers_and_never_closes_is_reset_at_the_ceiling ... ok
test listen::tests::a_peer_that_closes_with_its_last_ack_is_a_connection ... ok
test listen::tests::a_peer_that_acknowledges_nothing_of_a_full_send_buffer_is_reset_at_the_ceiling ... ok
test listen::tests::a_peer_that_resets_before_it_is_taken_frees_the_port ... ok
test listen::tests::a_wake_spent_on_a_reset_connection_announces_the_next ... ok
test listen::tests::an_aborted_connection_is_spent_once_its_reset_has_left ... ok
test listen::tests::an_aborted_connection_whose_next_hop_answers_no_arp_is_cut_at_the_ceiling ... ok
test listen::tests::an_accept_refused_for_room_is_woken_again_when_room_returns ... ok
test listen::tests::an_accept_refused_for_its_pipes_is_woken_again ... ok
test mdns::tests::wake_in_asks_for_what_the_record_owes_and_nothing_else ... ok
test resolve::tests::a_lookup_holds_a_socket_per_query_that_left_and_none_once_ended ... ok
test resolve::tests::a_lookup_is_not_carried_by_a_later_ones_schedule ... ok
test resolve::tests::a_query_leaves_from_no_port_a_client_holds ... ok
test resolve::tests::a_lookup_whose_client_left_is_let_go_at_once ... ok
test resolve::tests::a_server_that_answers_no_arp_holds_up_no_other_server ... ok
test resolve::tests::a_server_that_never_answers_is_asked_at_each_waits_end ... ok
test resolve::tests::a_server_with_no_route_holds_up_no_other_server ... ok
test resolve::tests::an_alias_answered_while_queries_are_stuck_restarts_the_lookup ... ok
test resolve::tests::the_lookup_past_the_cap_is_refused_as_exhausted ... ok
test virtio_net::tests::a_head_past_the_table_is_refused ... ok
test virtio_net::tests::a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back ... ok
test virtio_net::tests::a_head_with_no_chain_is_refused ... ok
test virtio_net::tests::more_bytes_than_the_chain_was_given_is_refused ... ok
test virtio_net::tests::a_frame_offered_to_a_full_transmit_queue_is_not_taken - should panic ... ok

test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

EXIT=0

r3-07-guest (whole)

$ cargo test --test toyos-build -- --jobs 2 netstack_socket_churn iommu_virtio_platform   # at 3605c5b3f
uptime: load averages: 69.68 100.40 90.18
    Finished `test` profile [optimized + debuginfo] target(s) in 1.83s
     Running tests/toyos.rs (target/debug/deps/toyos_build-d7e46964d69c4f2b)

22:50:01 running 2 tests, 2 wide

22:50:01   RUN   iommu_virtio_platform
22:50:01   RUN   netstack_socket_churn
22:50:01   BUILD x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn
22:50:01   BUILD x86_64 kernel, loader, ROOT of tests/netcase, for iommu_virtio_platform
22:50:15 [build-lock] waiting for the artifact lock (artifact staging) — held by pid 92035 (artifact staging), 1s so far
22:50:17 [build-lock] artifact staging acquired after 1.4s
22:50:17 [build-lock] waiting for the artifact lock (artifact staging) — held by pid 92035 (artifact staging), 0s so far
22:50:17   BUILT x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn  (17s)
22:50:17 [build-lock] artifact staging acquired after 169.4ms
22:50:17   BUILD x86_64 ROOT of tests/netcase, for netstack_socket_churn
22:50:24   BUILT x86_64 kernel, loader, ROOT of tests/netcase, for iommu_virtio_platform  (23s)
22:50:30   BUILT x86_64 ROOT of tests/netcase, for netstack_socket_churn  (13s)
22:50:45   [iommu] headless: 3 virtio function(s) behind a unit = true, the audio function 00:04.0 among them
22:50:47   PASS  netstack_socket_churn  (17s)
22:50:54   [iommu] headless-no-iommu: 2 virtio function(s) behind a unit = false, the audio function 00:04.0 among them; the NIC's claim refused for want of a unit
22:50:54   BUILD x86_64 kernel boot-actuators,test-actuators, ROOT of tests/testcases, for iommu_virtio_platform
22:54:03   BUILT x86_64 kernel boot-actuators,test-actuators, ROOT of tests/testcases, for iommu_virtio_platform  (188s)
22:54:14   [iommu] declined: [ 9.740 cpu0 kernel] virtio-sound: NOT INITIALISED — PCI 00:04.0 refused the feature set 0x100000000 the driver accepted, leaving DEVICE_STATUS=0x3 without FEATURES_OK
22:54:14   PASS  iommu_virtio_platform  (43s)
22:54:14   --- 4 guests, 1 of them not the shipping kernel, 2 kernel build(s): ["", "boot-actuators,test-actuators"]

22:54:14 host: fastest boot 9579 ms against the reference 1424 ms — liveness ceilings paid at 6.73x
22:54:14 host: 14 core(s); a guest wider than that waits vcpus/cores longer again
22:54:14 test result: ok. 2 passed, 2 total (253.7s; workers: 240s building, 60s testing)
EXIT=0
uptime: load averages: 77.50 81.77 83.74

r3-08-arms (whole)

$ sh run-arms.sh <worktree> <logs>   # at 3605c5b3f
uptime: load averages: 77.50 81.77 83.74
armC-no-room-after-a-frame-until-an-interrupt GUEST_EXIT=0
armD-no-room-after-a-frame-ever GUEST_EXIT=1
armE-no-room-after-a-frame-until-an-interrupt-the-receive-queue-cannot-have-raised GUEST_EXIT=1
git status --porcelain --ignore-submodules=none:
ARMS_DONE
EXIT=0
uptime: load averages: 73.44 76.23 80.83

arm C (whole)

$ cargo test --test toyos-build -- --jobs 1 netstack_socket_churn   # with armC-no-room-after-a-frame-until-an-interrupt.patch applied
uptime: load averages: 77.50 81.77 83.74
    Finished `test` profile [optimized + debuginfo] target(s) in 0.82s
     Running tests/toyos.rs (target/debug/deps/toyos_build-d7e46964d69c4f2b)

22:54:15 running 1 tests, 1 wide

22:54:15   RUN   netstack_socket_churn
22:54:15   BUILD x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn
22:54:23   BUILT x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn  (7s)
22:54:23   BUILD x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn
22:54:38   BUILT x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn  (16s)
22:54:55   PASS  netstack_socket_churn  (17s)
22:54:55   --- 1 guests, 0 of them not the shipping kernel, 1 kernel build(s): [""]

22:54:55 host: fastest boot 16038 ms against the reference 1424 ms — liveness ceilings paid at 8.00x
22:54:55 host: 14 core(s); a guest wider than that waits vcpus/cores longer again
22:54:55 test result: ok. 1 passed, 1 total (40.0s; workers: 23s building, 17s testing)
EXIT=0
uptime: load averages: 71.55 79.26 82.71

arm D (the harness's lines; the guest's boot log between them is cut)

$ cargo test --test toyos-build -- --jobs 1 netstack_socket_churn   # with armD-no-room-after-a-frame-ever.patch applied
uptime: load averages: 74.15 79.67 82.83
22:54:57 running 1 tests, 1 wide
22:54:57   RUN   netstack_socket_churn
22:54:57   BUILD x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn
22:55:04   BUILT x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn  (7s)
22:55:04   BUILD x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn
22:55:18   BUILT x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn  (15s)
22:55:58 FAIL netstack_socket_churn: STALLED: waiting for netstack's lease — it went quiet
22:55:58   STALL netstack_socket_churn  (39s)
22:55:58   --- 1 guests, 0 of them not the shipping kernel, 1 kernel build(s): [""]
22:55:58 host: fastest boot 23134 ms against the reference 1424 ms — liveness ceilings paid at 8.00x
22:55:58 host: 14 core(s); a guest wider than that waits vcpus/cores longer again
22:55:58 failures:
22:55:58     netstack_socket_churn: STALLED: waiting for netstack's lease — it went quiet
22:55:58 1 of those reds are the ceiling: netstack_socket_churn
22:55:58 test result: FAILED. 0 passed, 1 failed, 0 invalidated, 1 total (60.8s; workers: 21s building, 39s testing)
22:55:58 [toyos] this red run's serial logs are kept at <worktree>/target/red-run-serial/toyos-tmp-62855-0
error: test failed, to rerun pass `--test toyos-build`
EXIT=1
uptime: load averages: 67.75 76.70 81.50

arm E (the harness's lines; the guest's boot log between them is cut)

$ cargo test --test toyos-build -- --jobs 1 netstack_socket_churn   # with armE-no-room-after-a-frame-until-an-interrupt-the-receive-queue-cannot-have-raised.patch applied
uptime: load averages: 67.75 76.70 81.50
22:56:00 running 1 tests, 1 wide
22:56:00   RUN   netstack_socket_churn
22:56:00   BUILD x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn
22:56:11   BUILT x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn  (11s)
22:56:11   BUILD x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn
22:56:43   BUILT x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn  (32s)
22:57:27 FAIL netstack_socket_churn: STALLED: waiting for netstack's lease — it went quiet
22:57:27   STALL netstack_socket_churn  (44s)
22:57:27   --- 1 guests, 0 of them not the shipping kernel, 1 kernel build(s): [""]
22:57:27 host: fastest boot 27924 ms against the reference 1424 ms — liveness ceilings paid at 8.00x
22:57:27 host: 14 core(s); a guest wider than that waits vcpus/cores longer again
22:57:27 failures:
22:57:27     netstack_socket_churn: STALLED: waiting for netstack's lease — it went quiet
22:57:27 1 of those reds are the ceiling: netstack_socket_churn
22:57:27 test result: FAILED. 0 passed, 1 failed, 0 invalidated, 1 total (87.2s; workers: 43s building, 44s testing)
22:57:27 [toyos] this red run's serial logs are kept at <worktree>/target/red-run-serial/toyos-tmp-72153-0
error: test failed, to rerun pass `--test toyos-build`
EXIT=1
uptime: load averages: 73.44 76.23 80.83

arm F (the harness's lines and netstack's; the backtrace and the guest's log are cut)

$ cargo test --test toyos-build -- --jobs 1 netstack_socket_churn   # with armF applied, at 3605c5b3f
uptime: load averages: 122.66 107.50 85.44
22:46:01 running 1 tests, 1 wide
22:46:01   RUN   netstack_socket_churn
22:46:01   BUILD x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn
22:46:12 external deps changed: cleaning <worktree>/tests/toyos-rust-tests/target/toyos
22:46:12 external deps changed: cleaning <worktree>/tests/toyos-rust-tests/target/x86_64-unknown-toyos
22:46:28   BUILT x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn  (27s)
22:46:28   BUILD x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn
22:47:38   BUILT x86_64 kernel, loader, ROOT of tests/netcase, for netstack_socket_churn  (70s)
22:48:00 FAIL netstack_socket_churn: the job ended Some(101):
arm F: record 3: 1 message(s), receive used.idx 2 -> 3, transmit used.idx 3 -> 4
arm F: record 4: 1 message(s), receive used.idx 3 -> 4, transmit used.idx 4 -> 5
arm F: record 5: 1 message(s), receive used.idx 4 -> 5, transmit used.idx 5 -> 6
arm F: record 6: 1 message(s), receive used.idx 5 -> 5, transmit used.idx 6 -> 7
arm F: record 7: 1 message(s), receive used.idx 5 -> 6, transmit used.idx 7 -> 8
arm F: record 8: 1 message(s), receive used.idx 6 -> 7, transmit used.idx 8 -> 9
arm F: record 9: 1 message(s), receive used.idx 7 -> 8, transmit used.idx 9 -> 10
arm F: record 10: 1 message(s), receive used.idx 8 -> 8, transmit used.idx 10 -> 11
arm F: record 11: 1 message(s), receive used.idx 8 -> 9, transmit used.idx 11 -> 12
arm F: record 12: 2 message(s), receive used.idx 9 -> 10, transmit used.idx 12 -> 13
thread 'main' (1) panicked at userland/netstack/src/virtio_net.rs:555:17:
arm F: twelve records read
thread 'main' (1) panicked at src/bin/netstack_socket_churn.rs:111:33:
22:48:00   FAIL  netstack_socket_churn  (22s)
22:48:00   --- 1 guests, 0 of them not the shipping kernel, 1 kernel build(s): [""]
22:48:00 host: fastest boot 19795 ms against the reference 1424 ms — liveness ceilings paid at 8.00x
22:48:00 host: 14 core(s); a guest wider than that waits vcpus/cores longer again
22:48:00 failures:
22:48:00     netstack_socket_churn: the job ended Some(101):
22:48:00 test result: FAILED. 0 passed, 1 failed, 0 invalidated, 1 total (119.7s; workers: 98s building, 22s testing)
22:48:00 [toyos] this red run's serial logs are kept at <worktree>/target/red-run-serial/toyos-tmp-85311-0
error: test failed, to rerun pass `--test toyos-build`
EXIT=1
uptime: load averages: 128.95 116.38 93.47

…the part's, and the re-arm is gone

The 82574 datasheet's Defer Count counts a transmit deferred because "The
link is not up", so the path built for the I219 serves both parts: no room
while the link is down, the descriptors a link change finds unsent counted
and left in the ring, their write-back owed within one bound from the pass
that reads the link up, and a part that has not written them back refused by
name. `rearm`, the split of the bring-up it needed, `Pass::rearmed`,
`PassRefused::Rearm`, `Counters::unsent` and `descriptors.unsent` are deleted,
with the two tests and the stub modelling only they used; `open` is main's
single bring-up again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu Japabu changed the title Both NIC drivers say their transmit room and wake netstack when it returns; the I219 is never reset over a ring it holds Both NIC drivers say their transmit room and wake netstack when it returns; a link change leaves the ring the part's Oct 8, 2026
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Round 4 logs, at 9c3b81538. Each block is the command, its output and EXIT= with the command's own exit code as its last line; a path under the checkout reads <worktree>. Where a block is cut it says to what. The arms' logs are in the round-3 logs comment and were not run again: this round changed no virtio code.

r4-01-i219-test (whole)

$ cargo test -p toyos-i219   # at 9c3b81538
uptime: load averages: 108.39 75.68 74.70
    Finished `test` profile [optimized + debuginfo] target(s) in 0.46s
     Running unittests src/lib.rs (target/debug/deps/toyos_i219-b5aed6d9feab4306)

running 88 tests
test tests::a_flag_that_never_comes_does_not_stop_the_reset ... ok
test tests::a_frame_arrives_whole ... ok
test tests::a_flag_another_agent_holds_is_neither_asked_over_nor_cleared ... ok
test tests::a_frame_goes_out_whole ... ok
test tests::a_grant_that_never_comes_is_refused_by_name_and_the_request_withdrawn ... ok
test tests::a_link_drop_does_not_lose_the_ring ... ok
test tests::a_frame_longer_than_a_transmit_buffer_is_refused_and_not_truncated ... ok
test tests::a_grant_that_comes_late_inside_the_bound_is_taken ... ok
test tests::a_full_transmit_ring_answers_room_0 ... ok
test tests::a_flag_the_part_already_let_go_is_not_written_again ... ok
test tests::a_ladder_stopped_on_smbus_leaves_the_mac_off_it ... ok
test tests::a_claim_that_stops_answering_is_handed_up_and_not_read_as_quiet ... ok
test tests::a_buffer_given_back_out_of_turn_does_not_carry_the_tail_over_an_older_one ... ok
test tests::a_lying_length_is_refused_and_the_frames_behind_it_still_arrive ... ok
test tests::a_part_is_told_which_vector_each_cause_uses ... ok
test tests::a_part_that_does_not_take_ivar_is_refused ... ok
test tests::a_part_that_does_not_take_rctl_is_refused ... ok
test tests::a_part_with_no_station_address_is_refused ... ok
test tests::a_phy_in_reach_is_asked_once_and_left_powered ... ok
test tests::a_partner_on_an_unconfigured_phy_raises_no_link ... ok
test tests::a_link_that_is_down_has_no_room ... ok
test tests::a_phy_left_in_loopback_or_configured_by_hand_raises_no_link ... ok
test tests::a_part_that_grants_over_the_engines_standing_bit_is_brought_up ... ok
test tests::a_refused_descriptor_does_not_carry_the_tail_over_a_frame_still_held ... ok
test tests::a_register_nothing_decodes_is_refused_and_never_written ... ok
test tests::a_master_that_never_goes_quiet_does_not_stop_the_bring_up ... ok
test tests::a_multicast_group_sets_the_one_table_bit_its_address_hashes_to ... ok
test tests::a_part_that_takes_none_of_the_datasheets_latitudes_is_brought_up_the_same_way ... ok
test tests::a_reset_that_never_finishes_is_refused ... ok
test tests::a_release_over_a_window_of_ones_writes_nothing ... ok
test tests::a_phy_left_in_low_power_link_up_links_at_a_gigabit ... ok
test tests::a_platform_that_asks_for_low_power_link_up_in_d0a_gets_it ... ok
test tests::a_phy_left_out_of_reach_is_climbed_to_before_the_reset ... ok
test tests::a_phy_reset_restores_the_defaults_and_not_the_state_the_claim_inherited ... ok
test tests::a_spurious_interrupt_costs_a_pass_and_nothing_else ... ok
test tests::a_window_or_grant_too_small_is_refused ... ok
test tests::a_window_that_reads_ones_is_refused ... ok
test tests::a_ring_never_written_back_is_refused ... ok
test tests::a_seeded_workload_loses_nothing_and_invents_nothing ... ok
test tests::a_phy_that_is_not_the_one_this_map_describes_is_refused_by_its_identifier ... ok
test tests::bring_up_programs_what_the_initialization_sections_name ... ok
test tests::causes_are_acknowledged_by_writing_them_back ... ok
test tests::a_written_back_descriptor_returns_room ... ok
test tests::a_ring_a_link_change_left_full_stays_the_parts ... ok
test tests::every_reading_of_the_other_two_ownership_bits_names_who_stands_beside_the_flag ... ok
test tests::an_mdi_read_the_part_could_not_complete_is_refused_by_name ... ok
test tests::every_ask_carries_the_parts_own_word_and_keeps_the_pace ... ok
test tests::null_descriptor_padding_is_not_a_frame ... ok
test tests::frames_come_up_in_order_under_batched_and_reordered_write_back ... ok
test tests::ones_inside_the_grant_wait_do_not_leave_the_request_standing ... ok
test tests::ones_on_the_way_out_do_not_leave_the_flag_standing ... ok
test tests::ones_through_the_whole_release_bound_write_nothing ... ok
test tests::one_pass_hands_up_at_most_its_budget ... ok
test tests::quiesce_stops_what_a_previous_holder_left_running ... ok
test tests::refusals::a_descriptor_with_nothing_in_it_is_refused ... ok
test tests::refusals::a_descriptor_without_end_of_packet_is_refused ... ok
test tests::refusals::a_frame_the_hardware_reported_an_error_on_is_refused ... ok
test tests::refusals::more_bytes_than_the_buffer_holds_is_refused ... ok
test tests::the_82574s_own_phy_register_map_is_refused_by_name ... ok
test tests::the_driver_and_the_macs_statistics_count_the_same_frames ... ok
test tests::the_head_register_does_run_ahead_of_memory ... ok
test tests::the_link_going_away_and_coming_back_is_seen ... ok
test tests::the_oem_bits_carry_the_platforms_d0a_policy_and_nothing_else ... ok
test tests::an_interconnect_in_transition_is_waited_out_and_never_written_over ... ok
test tests::a_request_registered_while_the_engine_holds_it_is_still_granted ... ok
test tests::host_wake_up_left_armed_is_cleared_behind_the_reset ... ok
test tests::the_oem_bits_restart_follows_the_control_restart_where_a_phy_reset_is_allowed ... ok
test tests::the_receiver_reporting_on_itself_is_counted ... ok
test tests::the_refusal_names_each_agent_standing_beside_the_flag ... ok
test tests::the_ownership_claim_leaves_the_rest_of_the_register_standing ... ok
test tests::the_refusal_names_the_last_reading_before_the_deadline ... ok
test tests::the_pchs_mac_gets_its_three_registers_and_the_82574_none ... ok
test tests::transmit_descriptors_are_reclaimed_under_batched_write_back ... ok
test tests::what_one_power_reading_decides ... ok
test tests::what_the_wake_and_the_full_reset_write ... ok
test tests::the_phy_is_brought_up_and_the_mac_sees_the_link_it_raises ... ok
test tests::a_mac_that_keeps_its_phy_down_is_a_phy_no_cycle_reaches ... ok
test tests::the_power_step_is_what_lets_an_mdi_cycle_run_at_all ... ok
test tests::the_phy_is_found_at_whichever_address_answers_its_identifier ... ok
test tests::the_transmit_cause_is_armed_only_when_asked ... ok
test tests::a_phy_reset_the_firmware_blocks_is_never_issued ... ok
test tests::an_mdi_transaction_that_never_reports_ready_is_refused_by_name ... ok
test tests::a_phy_no_rung_reaches_is_refused_after_the_reset ... ok
test tests::the_word_to_the_firmware_goes_with_the_driver ... ok
test tests::no_two_accesses_to_the_arbitration_are_nearer_than_the_pace ... ok
test tests::the_advertised_abilities_are_what_the_link_resolves_to ... ok
test tests::the_interface_is_given_back_on_every_path_that_took_it ... ok
test tests::a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told ... ok

test result: ok. 88 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.49s

   Doc-tests toyos_i219

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

EXIT=0

r4-02-clippy-i219 (whole)

$ cargo clippy -p toyos-i219 --all-targets -- -W clippy::checked_conversions -W clippy::default_trait_access -W clippy::manual_midpoint -W clippy::redundant_clone -W clippy::unchecked_time_subtraction -W clippy::unnecessary_semicolon -D warnings   # at 9c3b81538
    Blocking waiting for file lock on package cache
    Checking toyos-i219 v0.1.0 (<worktree>/toyos-i219)
    Finished `dev` profile [optimized + debuginfo] target(s) in 2.82s
EXIT=0

r4-03-netstack-test (whole)

$ cargo test --manifest-path userland/netstack/Cargo.toml   # at 9c3b81538
uptime: load averages: 106.92 75.92 74.79
   Compiling netstack v0.0.0 (<worktree>/userland/netstack)
    Finished `test` profile [optimized + debuginfo] target(s) in 4.92s
     Running unittests src/main.rs (target/debug/deps/netstack-a95aecfc537c00aa)

running 29 tests
test listen::tests::a_finished_handshake_is_owed_one_wake ... ok
test listen::tests::a_cut_connection_whose_neighbour_entry_ran_out_is_kept_until_its_reset_leaves ... ok
test listen::tests::a_cut_connection_whose_next_hop_answers_no_arp_is_let_go_after_the_reset_bound ... ok
test listen::tests::a_connection_closed_by_both_ends_is_spent ... ok
test listen::tests::a_peer_gone_silent_is_reset_at_the_ceiling ... ok
test listen::tests::a_peer_that_answers_and_never_closes_is_reset_at_the_ceiling ... ok
test listen::tests::a_peer_that_closes_with_its_last_ack_is_a_connection ... ok
test listen::tests::a_peer_that_acknowledges_nothing_of_a_full_send_buffer_is_reset_at_the_ceiling ... ok
test listen::tests::a_peer_that_resets_before_it_is_taken_frees_the_port ... ok
test listen::tests::a_wake_spent_on_a_reset_connection_announces_the_next ... ok
test listen::tests::an_aborted_connection_is_spent_once_its_reset_has_left ... ok
test listen::tests::an_accept_refused_for_its_pipes_is_woken_again ... ok
test listen::tests::an_accept_refused_for_room_is_woken_again_when_room_returns ... ok
test listen::tests::an_aborted_connection_whose_next_hop_answers_no_arp_is_cut_at_the_ceiling ... ok
test mdns::tests::wake_in_asks_for_what_the_record_owes_and_nothing_else ... ok
test resolve::tests::a_lookup_holds_a_socket_per_query_that_left_and_none_once_ended ... ok
test resolve::tests::a_lookup_is_not_carried_by_a_later_ones_schedule ... ok
test resolve::tests::a_lookup_whose_client_left_is_let_go_at_once ... ok
test resolve::tests::a_query_leaves_from_no_port_a_client_holds ... ok
test resolve::tests::a_server_that_answers_no_arp_holds_up_no_other_server ... ok
test resolve::tests::a_server_that_never_answers_is_asked_at_each_waits_end ... ok
test resolve::tests::an_alias_answered_while_queries_are_stuck_restarts_the_lookup ... ok
test resolve::tests::a_server_with_no_route_holds_up_no_other_server ... ok
test resolve::tests::the_lookup_past_the_cap_is_refused_as_exhausted ... ok
test virtio_net::tests::a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back ... ok
test virtio_net::tests::a_head_past_the_table_is_refused ... ok
test virtio_net::tests::a_head_with_no_chain_is_refused ... ok
test virtio_net::tests::more_bytes_than_the_chain_was_given_is_refused ... ok
test virtio_net::tests::a_frame_offered_to_a_full_transmit_queue_is_not_taken - should panic ... ok

test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

EXIT=0

r4-04-build-only (first 2 and last 6 lines of 84)

$ cargo run -- --build-only   # at 9c3b81538
uptime: load averages: 108.95 77.35 75.31
...
23:08:45 root: symlink 'bin/tone' -> '/system/bin/toybox'
23:08:47 Signed with this checkout's throwaway key SHA256:<key> at version 1791500899
23:08:49 Build finished.
Boot image: <worktree>/target/bootable.img
EXIT=0
uptime: load averages: 104.15 79.17 76.04

r4-05-guest (whole)

$ cargo test --test toyos-build -- --jobs 2 netstack_socket_churn iommu_virtio_platform   # at 9c3b81538
uptime: load averages: 104.15 79.17 76.04
    Finished `test` profile [optimized + debuginfo] target(s) in 1.72s
     Running tests/toyos.rs (target/debug/deps/toyos_build-d7e46964d69c4f2b)

23:08:51 running 2 tests, 2 wide

23:08:51   RUN   netstack_socket_churn
23:08:51   RUN   iommu_virtio_platform
23:08:51   BUILD x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn
23:08:51   BUILD x86_64 kernel, loader, ROOT of tests/netcase, for iommu_virtio_platform
23:09:03 [build-lock] waiting for the artifact lock (artifact staging) — held by pid 53124 (artifact staging), 3s so far
23:09:03 [build-lock] artifact staging acquired after 230.0ms
23:09:03 [build-lock] waiting for the artifact lock (artifact staging) — held by pid 53124 (artifact staging), 0s so far
23:09:03   BUILT x86_64 netstack_socket_churn of tests/toyos-rust-tests, for netstack_socket_churn  (12s)
23:09:03 [build-lock] artifact staging acquired after 74.6ms
23:09:03   BUILD x86_64 ROOT of tests/netcase, for netstack_socket_churn
23:09:12 [build-lock] waiting for the artifact lock (artifact staging) — held by pid 53124 (artifact staging), 9s so far
23:09:15 [build-lock] artifact staging acquired after 2.3s
23:09:15   BUILT x86_64 kernel, loader, ROOT of tests/netcase, for iommu_virtio_platform  (23s)
23:09:16   BUILT x86_64 ROOT of tests/netcase, for netstack_socket_churn  (13s)
23:09:43   [iommu] headless: 3 virtio function(s) behind a unit = true, the audio function 00:04.0 among them
23:09:46   PASS  netstack_socket_churn  (30s)
23:10:07   [iommu] headless-no-iommu: 2 virtio function(s) behind a unit = false, the audio function 00:04.0 among them; the NIC's claim refused for want of a unit
23:10:07   BUILD x86_64 kernel boot-actuators,test-actuators, ROOT of tests/testcases, for iommu_virtio_platform
23:10:21   BUILT x86_64 kernel boot-actuators,test-actuators, ROOT of tests/testcases, for iommu_virtio_platform  (15s)
23:10:47   [iommu] declined: [21.343 cpu0 kernel] virtio-sound: NOT INITIALISED — PCI 00:04.0 refused the feature set 0x100000000 the driver accepted, leaving DEVICE_STATUS=0x3 without FEATURES_OK
23:10:47   PASS  iommu_virtio_platform  (77s)
23:10:47   --- 4 guests, 1 of them not the shipping kernel, 2 kernel build(s): ["", "boot-actuators,test-actuators"]

23:10:47 host: fastest boot 22774 ms against the reference 1424 ms — liveness ceilings paid at 8.00x
23:10:47 host: 14 core(s); a guest wider than that waits vcpus/cores longer again
23:10:47 test result: ok. 2 passed, 2 total (115.4s; workers: 63s building, 108s testing)
EXIT=0
uptime: load averages: 62.76 73.52 74.34

r4-06-mutations (whole)

$ sh run-mutations.sh <worktree> <mutations>   # at 9c3b81538
uptime: load averages: 62.76 73.52 74.34
m01-room-check-removed-from-reserve BUILD_EXIT=0
m01-room-check-removed-from-reserve TEST_EXIT=101
m02-room-without-reclaim BUILD_EXIT=0
m02-room-without-reclaim TEST_EXIT=101
m03-pass-leaves-the-cause-unmasked BUILD_EXIT=0
m03-pass-leaves-the-cause-unmasked TEST_EXIT=101
m04-wake-unmasks-nothing BUILD_EXIT=0
m04-wake-unmasks-nothing TEST_EXIT=101
m05-82574-classic-name-alone BUILD_EXIT=0
m05-82574-classic-name-alone TEST_EXIT=101
m06-i219-unmasks-bit-22-too BUILD_EXIT=0
m06-i219-unmasks-bit-22-too TEST_EXIT=101
m07-a-link-change-over-unsent-descriptors-strands-nothing BUILD_EXIT=0
m07-a-link-change-over-unsent-descriptors-strands-nothing TEST_EXIT=101
m08-room-with-the-link-down BUILD_EXIT=0
m08-room-with-the-link-down TEST_EXIT=101
m11-a-wake-counted-on-a-pass-nobody-waited-on BUILD_EXIT=0
m11-a-wake-counted-on-a-pass-nobody-waited-on TEST_EXIT=101
m12-wake-counted-with-the-link-down BUILD_EXIT=0
m12-wake-counted-with-the-link-down TEST_EXIT=101
m13-a-wake-counted-without-its-message BUILD_EXIT=0
m13-a-wake-counted-without-its-message TEST_EXIT=101
m14-a-wake-counted-beside-another-unmasked-cause BUILD_EXIT=0
m14-a-wake-counted-beside-another-unmasked-cause TEST_EXIT=101
m16-a-ring-never-written-back-is-never-refused BUILD_EXIT=0
m16-a-ring-never-written-back-is-never-refused TEST_EXIT=101
m17-the-deadline-runs-with-the-link-down BUILD_EXIT=0
m17-the-deadline-runs-with-the-link-down TEST_EXIT=101
m18-a-second-link-change-counts-the-descriptors-again BUILD_EXIT=0
m18-a-second-link-change-counts-the-descriptors-again TEST_EXIT=101
m19-a-second-link-change-does-not-restart-the-deadline BUILD_EXIT=0
m19-a-second-link-change-does-not-restart-the-deadline TEST_EXIT=101
m21-a-descriptor-written-back-stays-owed BUILD_EXIT=0
m21-a-descriptor-written-back-stays-owed TEST_EXIT=101
m22-a-write-back-owed-on-a-link-that-is-down BUILD_EXIT=0
m22-a-write-back-owed-on-a-link-that-is-down TEST_EXIT=101
m23-the-deadline-is-never-due BUILD_EXIT=0
m23-the-deadline-is-never-due TEST_EXIT=101
m24-the-deadline-of-a-ring-written-back-stays BUILD_EXIT=0
m24-the-deadline-of-a-ring-written-back-stays TEST_EXIT=101
n01-virtio-room-without-reclaim BUILD_EXIT=0
n01-virtio-room-without-reclaim TEST_EXIT=101
git status --porcelain --ignore-submodules=none:
TREE_CLEAN_CHECK_DONE
EXIT=0
uptime: load averages: 44.53 57.22 66.68

tests red per mutation, from each mutation's own test log

r4-m01-room-check-removed-from-reserve.test.log: EXIT=101: a_full_transmit_ring_answers_room_0 a_link_that_is_down_has_no_room a_ring_a_link_change_left_full_stays_the_parts a_ring_never_written_back_is_refused a_written_back_descriptor_returns_room the_transmit_cause_is_armed_only_when_asked
r4-m02-room-without-reclaim.test.log: EXIT=101: a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told a_written_back_descriptor_returns_room the_transmit_cause_is_armed_only_when_asked transmit_descriptors_are_reclaimed_under_batched_write_back
r4-m03-pass-leaves-the-cause-unmasked.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r4-m04-wake-unmasks-nothing.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told
r4-m05-82574-classic-name-alone.test.log: EXIT=101: a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told the_transmit_cause_is_armed_only_when_asked
r4-m06-i219-unmasks-bit-22-too.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r4-m07-a-link-change-over-unsent-descriptors-strands-nothing.test.log: EXIT=101: a_ring_never_written_back_is_refused a_ring_a_link_change_left_full_stays_the_parts
r4-m08-room-with-the-link-down.test.log: EXIT=101: a_link_that_is_down_has_no_room
r4-m11-a-wake-counted-on-a-pass-nobody-waited-on.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r4-m12-wake-counted-with-the-link-down.test.log: EXIT=101: a_link_that_is_down_has_no_room
r4-m13-a-wake-counted-without-its-message.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r4-m14-a-wake-counted-beside-another-unmasked-cause.test.log: EXIT=101: the_transmit_cause_is_armed_only_when_asked
r4-m16-a-ring-never-written-back-is-never-refused.test.log: EXIT=101: a_ring_never_written_back_is_refused
r4-m17-the-deadline-runs-with-the-link-down.test.log: EXIT=101: a_ring_never_written_back_is_refused
r4-m18-a-second-link-change-counts-the-descriptors-again.test.log: EXIT=101: a_ring_a_link_change_left_full_stays_the_parts a_ring_never_written_back_is_refused
r4-m19-a-second-link-change-does-not-restart-the-deadline.test.log: EXIT=101: a_ring_never_written_back_is_refused
r4-m21-a-descriptor-written-back-stays-owed.test.log: EXIT=101: a_ring_a_link_change_left_full_stays_the_parts
r4-m22-a-write-back-owed-on-a-link-that-is-down.test.log: EXIT=101: a_ring_a_link_change_left_full_stays_the_parts
r4-m23-the-deadline-is-never-due.test.log: EXIT=101: a_ring_a_link_change_left_full_stays_the_parts a_ring_never_written_back_is_refused
r4-m24-the-deadline-of-a-ring-written-back-stays.test.log: EXIT=101: a_ring_a_link_change_left_full_stays_the_parts
r4-n01-virtio-room-without-reclaim.test.log: EXIT=101: a_full_transmit_queue_has_no_room_until_the_device_gives_heads_back

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 9c3b81538 against origin/main at 6776714db, round 3. Read only: no build, no guest, no metal, and no other operating system's source. Read: 12e4fb269..9c3b81538 less the merge, the driver and netstack hunks as new code, the changed files at the head, the round-3 and round-4 step logs, the arm patches, and the fetched Intel documents as text.

Net lines against main: +1382 / −528. Production +801 / −403 (the moves commit +288 / −274 of it); tests and stub +421 / −53; issues +160 / −72. The growth left is the room count, the per-part wake, the four counts, TxQueue, and about sixty lines for the stranded deadline and its wait. Accepted: without the deadline a part that keeps its ring is a transmit path dead in silence, which is what main has.

Round 2's BLOCKERs

  • transmit.wake_taken counts any pass — CLOSED. toyos-i219/src/lib.rs:1181-1188 counts only waited && messages > 0, the transmit cause set, and no bit of ENABLED | ENABLED_MSIX read. Red first on the driver at 12e4fb269 with this round's test and stub hook: exit 101, left: (0, (1, 1, 1)), right: (0, (1, 1, 0)). At the head m11 (waited), m13 (the message) and m14 (another unmasked cause) each exit 101 on the_transmit_cause_is_armed_only_when_asked, on both parts. What the count can still take for a wake is §7.4.5's spurious message landing on a pass that reads only the recorded transmit cause; one such coincidence is why the boot below states the gap and does not stop at "at least 1".
  • the re-arm resets the I219 over a live ring on nothing read — CLOSED, by removal. rearm, program, rings_and_mask, Programmed, Pass::rearmed, PassRefused::Rearm, Counters::unsent, the descriptors.unsent key, netstack's "the link changed over unsent frames" line and the stub's SLU-raises-LSC arm are in no file at the head (git grep over the driver, netstack, issues/, tests/, src/, the prompts). open's body differs from main's in two field initialisers. begin_pass writes no register on a link change. Twenty-one mutations, each BUILD_EXIT=0 and TEST_EXIT=101, and the script ends on an empty git status. The vendor-source reading I asked for is withdrawn with the orchestrator's ruling; nothing at this head rests on it.
  • QEMU's transmit-completion interrupt ending the sleep is a guess — CLOSED. Arm E as I spelled it was wrong: slirp's answer is in the receive ring before the first completion's record is read, so it never releases. Arm F's log (exit 1 by its own twelfth-record assertion, at 3605c5b3f; git diff 3605c5b3f 9c3b81538 -- userland/netstack/src/virtio_net.rs is empty) reads it without a timing argument: records 3 to 12 carry eleven messages, the receive queue's used.idx moves 2 to 10 and the transmit queue's 3 to 13. Eight receive elements cannot account for eleven messages, nor for ten records each begun by one, with at most one carried over from before record 3; the function has no other source of a message on slirp. Records 6 and 10 alone would not have been enough (a receive element's message can trail the pass that already counted its used.idx); the count is. Virtio 1.2 §2.7.7 is the oracle and this is its reading. One run, load 123 to 129; nothing ships for it.

Round 1's evidence BLOCKERs, still open

  • host at the head — OPEN. The three checks at 9c3b81538 are SKIPPED.
  • the guest suite — OPEN. Two of the suite ran at the head (exit 0); the suite has not.
  • no hardware has read the Intel path — OPEN. No T14 boot exists. This is the shipped Intel driver; the stub is not the part.

No other BLOCKER is open: the code at this head is not sent back for anything a reading found.

The link-change rule, as a design

  • one rule for both parts — sound, and better founded than the branch says: 612523 rev 2.02 §9.5.4.6 is the I219's own Defer Count, with the same sentence ("... reception of XOFF frames, or the link is not up"). The rule also survives the other reading the 82574 document allows (§3.2's "The transmission completes successfully even if the PHY fails to indicate CRS ... or be in a link down situation", counted TNCRS): a part that writes the descriptors back with the link down has them reclaimed at the LSC that reads it up, and nothing is owed. Neither reading needs a register write.
  • what 12.98 s bounds — collisions, not deferral. (TX_RING - 1) * 16 * (TX_BUF_BYTES + 1024 * 64) * 800 ns is 12 976 128 000 ns: a full ring, sixteen attempts each, every attempt behind the largest back-off, at 10 Mb/s. It over-bounds each term (2048 bytes for a 1514-byte frame, the clamp on all sixteen attempts) by more than the preamble, gap and jam it leaves out, and a frame that collides sixteen times is given up and written back, so the ring does leave inside it. It does not bound how long after STATUS.LU a part resumes a deferred transmit (no document read says; the deadline runs from the pass that read LSC, which is later), and it does not bound deference to other stations on a half-duplex segment, which nothing bounds. At the T14's 1000 Mb/s full duplex there is neither collision nor deference and the bound is four orders loose. As a driver-chosen refusal with a documented floor it stands, like MASTER_QUIESCE_DEADLINE_NANOS. Its false positive, a saturated half-duplex 10 Mb/s segment, ends the machine's network; that belongs in the issue, not only in the body.
  • LSC flapping inside the bound — the deadline restarts at every LSC read with a descriptor unsent (lib.rs:1201; m19 holds it), runs only while the link reads up, and counts no descriptor twice (m18). So a link that changes more often than every 12.98 s over a part that writes nothing back is never refused: no room, no frame published, netstack still serving its clients, for as long as the wire flaps. That is the right side to err on: a part is owed an unbroken link before it is called dead, each restart costs a real link change, and nothing spins (pass_due_in is None with the link down and the remaining time with it up).
  • stale frames on the link that returns — acceptable, and not the stack's to prevent: the descriptors are the part's, the one way to take them back is the reset this round removed, and no document read gives another. Up to fifteen frames, as old as the outage. A TCP segment, a DHCP message with a dead transaction id and an ARP request are each absorbed by the protocol that receives them; the one with a cost is an ARP frame carrying the old address onto a different network, which can displace a neighbour's entry until its owner speaks. It is a known weakness and has to be in the tracker: NOTE below.
  • ending netstack at the deadline — the right fail-fast, and it does not walk into the reset. The premise that the supervisor starts it again is false of this tree: [programs.netstack] (system.toml:83-86) carries no restart, and close_when_it_ends closes the ports of a row without it. The function is next reset by a swap netstack or by a reboot, which is the exposure every end of netstack already has on main (the claim's refusal, any panic) and which the issue names. Staying alive with a transmit ring that never empties would be the silent degradation the refusal replaces. What it must not do, and does not, is try anything on the part: nothing documented is left to try.

NOTE

  • issues/no-machine-has-read-an-intel-nic-across-a-link-change.md — three things the branch found and the tracker does not hold: frames a link change strands leave on the link that returns, up to fifteen and as old as the outage (or are lost, by the TNCRS reading); the deadline does not bound a busy half-duplex segment, where it ends the network; and the refusal is safe only while netstack's row has no restart, since with one it becomes an automatic reset over the ring the part kept. Each with the exit it shares with the file.
  • issues/no-machine-has-read-an-intel-nic-across-a-link-change.md, and the pull request body — "The Defer Count is the only sentence on the subject in any document read, and it is the other part's" and "the PCH's MAC publishes no transmit timing of its own" are false of the documents read: 612523 §9.5.4.6 is the I219's Defer Count, and its Power Management Control register (PHY address 01, page 769, register 21, bits 8:1, default 0x0F) is that part's own retry count, the same sixteen attempts. The first unknown narrows to "no machine has read it"; the bound's number is unchanged.
  • issues/no-machine-has-read-an-intel-nic-across-a-link-change.md — the exit waits on "the T14's link partner is something a row commands", which nothing in the tracker owns. Name where that instrument is owed, or the exit is read by nobody.
  • userland/netstack/src/main.rs:1578 — that the loop takes pass_due_in into its wait is read by no test; without the line a silent part is refused on the next unrelated pass, late and not never. Accepted on the ground round 2 accepted the loop's guard on: no host test builds the loop, and it goes at the move. The stack that replaces it carries a test of a wake no interrupt brings.
  • round 2's NOTEs — self.wire() before the reset: moot, no reset is left outside open. The || !self.link.is_up() refresh: deleted on §7.4.3, which is the 82574's sentence about the very thing unread on the T14's part; the boot below decides it, and if it reads late the arm comes back. The forbidden-values issue names the boot and keeps its exit: closed. The logs are posted as command, output and EXIT=: closed. receive's refusal and the loop's guard unread: stands as accepted.

What the T14 boot must show, and what it cannot

For the Intel BLOCKER to close, a boot of an image whose toyos-i219/src and userland/netstack/src are byte-identical to the head that lands (git diff --stat between the two over those paths, empty, posted), with its log in the body:

  • netstack's row names pci:8086:15fc; the bring-up lines; no refusal from open.
  • "link up at 1000 Mb/s full duplex, N ms after the driver came up" with N beside the 2720 to 2820 earlier runs read, before the job's first request to netstack, and a lease that no client's request preceded. With the link down nothing but LSC's message begins the pass that finds the link, and the job's own sockets, sshserver's listen and mDNS's timer all begin passes that would mask a message that never came: an N that sits on one of those is the deleted arm's case.
  • no "cannot be driven on", no "offered to a transmit ring that refuses it".
  • after the fill, from more sockets than one, to a destination that answers nothing: transmit.full, transmit.wake_armed and transmit.wake_taken each at least 1, with all three numbers stated. Taken near armed is the reading; one taken against many armed is not, and is TXDW raising no message with a spurious one counted.
  • descriptors.sent equal to wire.sent, and wire.sent risen by at least the datagrams the job counted as accepted: the part's own statistics, not the driver's, say every queued frame left.
  • descriptors.stranded 0. Above 0 with netstack still serving means an LSC was read over unsent frames on a link that stayed up: not a failure of the part, but the room issue's exit is then not met as written and the reading goes into the link-change issue.
  • a transmit.full of 0 has read nothing and closes nothing.

It cannot show: anything across a link that goes down (the stranded count from a real outage, the deadline, the refusal, whether the I219 sends or drops what it held, the stale frames); a reset over a live ring, open's included; anything of the 82574, TXQ0 and MSI-X mode with it; the bound on a half-duplex link; the wake on a pass a received frame shares, which the count leaves out by design; or that LSC's message finds the link against any partner but this one.

What CI must show, and landing

  • host — run, not skipped, exit 0, at the head that lands: the workspace tests with toyos-i219's 88 among them, the userland/netstack step's 29, clippy clean, every "apps for" step.
  • guest / suite — the whole suite, run, green at that head. Both need the draft marked ready.
  • may the orchestrator land on reading them and the boot — yes. The NOTEs above change issues/ and the body and no source; with them made, he reads host and guest / suite at that head and the T14 boot against the list above, and lands without another round. Any change under toyos-i219/src or userland/netstack/src, or a boot that misses a line of the list, is a round.

SEND BACK

Japabu and others added 2 commits October 9, 2026 01:27
…ments say, and who owes its instrument

Review round 3 of #782, its NOTEs. The I219 datasheet has its own Defer Count
(612523 9.5.4.6) and its own retry count (Power Management Control, PHY
address 01, page 769, register 21, bits 8:1, default 0x0F), so the first
unknown narrows to "no machine has read it". Recorded beside it: stale frames
on the link that returns, or lost by the TNCRS reading; a deadline that does
not bound a busy half-duplex segment; and a refusal that is safe only while
netstack's row has no restart. The link partner a row commands is owed by
stage 2 of the LAN track, which now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
The header had the Defer Count as the 82574's alone and the bound's comment
had the PCH's MAC publishing no transmit timing. 612523 9.5.4.6 is the I219's
own Defer Count and its Power Management Control register carries its own
retry count. Comments only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

The T14 on this driver, final head ab49658de (the orchestrator's reading). The image is staged from the measurement branch wt/toyos-move-rows-edge at d79f365ac, which carries this head; git diff ab49658de d79f365ac -- toyos-i219/src userland/netstack/src is empty. Worktree clean before and after; the image's sha256 checked against the request in the command that flashed it.

One boot, judged with --metal --metal-readback <dir> boot:outbound: exit 0, 2 passed, 0 failed (outbound_router, outbound_internet).

  • netstack: I219: link up at 1000 Mb/s full duplex, 2721 ms after the driver came up; the lease 13337 ms after netstack came up (the base stack on main's driver: 13314 ms).
  • Both anchors: lookup answered, connected.
  • outbound: ring full=7 wake_armed=2 wake_taken=1 untaken=1 stranded=0 descriptors_sent=73 wire_sent=73 speed=1000 taken=60
  • 0 lines saying cannot be driven on, 0 saying offered to a transmit ring that refuses it, no anomaly line about frames left in the ring at a link change.

So the ring filled seven times under 60 back-to-back full-size datagrams, every one was taken, every descriptor sent reached the wire and none was stranded. One number is not yet explained: untaken=1, a wake armed and not counted taken at the moment of the read, with nothing left to send. It is being read against the code before this lands.

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

untaken=1, read against the code at ab49658de (toyos-i219/src/lib.rs, userland/netstack/src/main.rs, card.rs, i219.rs; the job read at d79f365ac only to see what it asks and when). Nothing was run.

Answer: (a), benign. No wake was lost or leaked, and no frame can wait on this number. The code does not let one line say which of three orders it was; it does guarantee it was one of them, and all three are safe for the same reason.

What the two counters are. tx_wake_armed moves at lib.rs:1387-1390, on the first wake_on_room that finds no room since the last begin_pass. tx_wake_taken moves at lib.rs:1183-1188, and only the first begin_pass after an arm can move it for that arm, because lib.rs:1153 takes tx_wake there. It counts only when all four hold: the arm was live, the claim handed over a message (messages > 0), ICR holds the transmit cause, and ICR holds none of RXT0 | RXO | RXDMT0 | LSC (regs.rs:387, :393). So wake_taken is not "wakes delivered": it is "passes provably begun by the transmit cause alone", which is what lib.rs:455-458 says of it.

The orders that leave an arm uncounted, all reachable in this boot:

  1. The pass that ended the arm read a receive or link cause beside TXDW (lib.rs:1186). ICR holds every cause since the previous pass's write-back (lib.rs:1164-1167), so one inbound frame anywhere in that interval is enough: a broadcast on the wire, or an ICMP answer to a datagram sent to a closed port 9. The wake arrived and ended the wait; the pass is attributed to nobody.
  2. The pass that ended the arm was begun by something other than the claim (messages == 0, lib.rs:1184). The job makes this order reachable: a UdpSendTo is answered at the tail of the pass that read it (main.rs:742-743, :1676-1682), and its frame leaves in a later pass's iface.poll (main.rs:1482). The job's next connects, round two's thirty after round one, the two closes and the final inspect after round two, therefore reach the listener while the ring is still draining, and a pass woken on TOKEN_LISTENER or a pending client runs begin_pass, which masks the cause (lib.rs:1154-1156) before the message has been read off the claim. If the message lands after irq.taken() at lib.rs:1143 it begins the next pass, where waited is false, and is not counted either.
  3. The arm was still live when inspect was answered. answer_inspect runs at the tail of a pass (main.rs:1678), after that pass's iface.poll and the device.room() at main.rs:1515, both of which can arm. The least likely here, several passes after the last datagram was taken, but the code does not exclude it.

Whether a frame offered next can wait on a wake that never comes: no.

  • tx_wake_armed and tx_wake_taken are written and reported and read by no branch: their only readers are card.rs:125-126 and the driver's tests. The state is tx_wake and the mask bit, and untaken is no state of the driver.
  • Every begin_pass, whatever began it, ends the arm and masks the cause (lib.rs:1153-1156). Room is then never inferred from the cause: tx_room reads DD off the descriptors (lib.rs:1354-1358, :1432-1449). A sender that finds none in that pass arms again (lib.rs:1387-1390), which is a new wake_armed, not a leaked one.
  • netstack sleeps on the card only after wake_on_room answered 0 (main.rs:71, :1515-1518), and that 0 is counted after the unmask (lib.rs:1388, then :1392). At that moment TX_RING - 1 descriptors are unsent on a link that is up with TXDW unmasked, so the next write-back is a message; one that landed before the unmask is in the recount. With the link down nothing is armed and LSC, never masked, is the wake.
  • A cause cleared by the write-back at lib.rs:1167 between the read and the write loses nothing: the cause is masked by then and the count comes from DD.

What this rests on outside the code is the part raising TXDW for a write-back once unmasked. wake_taken=1 is that reading from the T14's MAC: one pass was begun by a message with TXDW and no other enabled cause.

full=7 against wake_armed=2 is consistent. tx_full counts askings, not fills: every wake_on_room that finds no room on a link that is up (lib.rs:1386), and wake_armed only the first of them per pass (lib.rs:1387). One pass asks at Device::receive (main.rs:82), at each Device::transmit (main.rs:95), on every turn of while iface.poll(..) (main.rs:1482) and once more at main.rs:1515. The five that armed nothing found the cause already unmasked. So the reading is two passes that found the ring full, asked seven times between them.

NOTE

  • The reading's comment, "the ring filled seven times" — false of the code: transmit.full is seven askings in two arms (lib.rs:1386-1390). Prose in the record.
  • tests/toyos-rust-tests/src/bin/outbound.rs:203-206 at d79f365ac (the measurement branch, not this head) — "Nothing is asked of netstack from a round's first request to its last answer" does not cover the frames, which leave after the last answer (main.rs:742-743 against :1482); untaken from that job cannot be read as a defect at any value, and no row judges it. Prose, and outside this pull request.

LAND — read host and guest / suite green at the head that lands: both are skipping at ab49658de while the pull request is a draft.

@Japabu

Japabu commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Correction to my T14 reading above: "the ring filled seven times" is false of the code. transmit.full counts askings that found no room, not fills: the reading is two passes that found the ring full and asked seven times between them. untaken=1 is answered in the review comment that follows it: wake_taken counts only passes provably begun by the transmit cause alone, and no frame can wait on that number.

@Japabu
Japabu marked this pull request as ready for review October 8, 2026 23:58
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI at ab49658de, read from each job's own log (the orchestrator). host: [ci] Host: 79 step(s), all green. guest / suite: test result: ok. 36 passed, 36 total, [ci] Guest: 5 step(s), all green. With the T14 reading and the review's LAND above. Queued.

@Japabu
Japabu added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit b332237 Oct 9, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-move-edge branch October 9, 2026 00:45
Japabu added a commit that referenced this pull request Oct 9, 2026
…, the drivers' room and wake (#782) and the SMI_CMD call (#780), into the listeners stage

No file stopped the merge. One file is both sides': the track, where
main's two removed bullets and two added ones (#779) sit in the node's
and stage 3's lists and this stage's lines in stage 5's, merged by git
and read against both parents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Japabu added a commit that referenced this pull request Oct 9, 2026
…ers (#777), the drivers' room and wake (#782), the SMI_CMD call (#780) and the guest waits (#786), into the resolver rules

One conflict, in issues/toyos-has-its-own-network-stack.md, resolved by hand
with every line of both sides accounted for:

- The stage 5 paragraph: main's "In the tree", which now names streams,
  listeners and the one bound, and this branch's "Still to build on it",
  less the streams and listeners that landed: datagram senders that wait on
  the hop, then the move.
- "What the node does not yet meet": this branch's two lines stand in place
  of the two lines they rewrote, which main had left as they were; the line
  on an answer to a 169.254/16 asker, which #779 deleted with the defect, is
  gone, as is the line on a silent next hop, which #779 deleted with its
  test (no conflict).
- "What stage 3 departs from its specifications": both sides added a line at
  the list's end; both stand, this branch's on NUD-04 and #779's on the
  scenarios the readers' specification lacks.

Every other file merged without a conflict. toyos-dns/src/lib.rs and
userland/netstack/node/src/resolve.rs are byte-identical to b2d9037.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant