Repository navigation
Both NIC drivers say their transmit room and wake netstack when it returns; a link change leaves the ring the part's - #782
Conversation
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Mutation patches for the negative controls, against 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" |
|
Review of 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
NOTE
What the head that lands must show
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
|
Mutation patches against 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 thatm04-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-backm23-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-backm24-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() |
|
Round 2 logs, at r2-i219-test.log r2-netstack-test.log r2-build-system-lib.log r2-harness-checks.log r2-clippy-i219.log (exit 0) r2-build-only.log (last lines) r2-guest.log r2-mutations.log Failing tests per mutation, from each r2-arms.log r2-armA-virtio-queue-of-one.guest.log r2-armB-queue-of-one-and-room-always.guest.log r2-armC-no-room-after-a-frame-until-an-interrupt.guest.log r2-armD-no-room-after-a-frame-ever.guest.log r2-spelled-armB.guest.log ( Host load ( The driver tests, netstack tests and both lints did not append their exit to their log; the invoking shell printed |
|
Review of 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 Round 1's BLOCKERs
Round 1's NOTEs: the cause is per part ( BLOCKER
NOTE
SEND BACK |
…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
…tage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 3 logs, at r3-01-red-first-wake-count (the round-3 test and stub hook over the driver of r3-02-i219-test (whole) r3-03-clippy-i219 (whole) r3-04-build-only (first 2 and last 6 lines of 493) r3-05-mutations (whole) tests red per mutation, from each mutation's own test log r3-06-netstack-test (whole) r3-07-guest (whole) r3-08-arms (whole) arm C (whole) arm D (the harness's lines; the guest's boot log between them is cut) arm E (the harness's lines; the guest's boot log between them is cut) arm F (the harness's lines and netstack's; the backtrace and the guest's log are cut) |
…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
|
Round 4 logs, at r4-01-i219-test (whole) r4-02-clippy-i219 (whole) r4-03-netstack-test (whole) r4-04-build-only (first 2 and last 6 lines of 84) r4-05-guest (whole) r4-06-mutations (whole) tests red per mutation, from each mutation's own test log |
|
Review of Net lines against Round 2's BLOCKERs
Round 1's evidence BLOCKERs, still open
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
NOTE
What the T14 boot must show, and what it cannotFor the Intel BLOCKER to close, a boot of an image whose
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, What CI must show, and landing
SEND BACK |
…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
|
The T14 on this driver, final head One boot, judged with
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: |
|
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. The orders that leave an arm uncounted, all reachable in this boot:
Whether a frame offered next can wait on a wake that never comes: no.
What this rests on outside the code is the part raising
NOTE
LAND — read |
|
Correction to my T14 reading above: "the ring filled seven times" is false of the code. |
|
CI at |
…, 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
…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
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 inissues/, two source comments corrected, and merges ofmainat #774 and #769.git diff 9c3b81538 ab49658de -- toyos-i219/src userland/netstack/srcis fourteen comment lines oftoyos-i219/src/lib.rsand 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 at9c3b81538, and atab49658dethe driver's 88 tests and its lint were run again, both exit 0 (r6-01,r6-02).What changed, per decision
8bf696534, moves only.Cardgoes touserland/netstack/src/card.rs;Client,Request,PendingConn,ClientRx, the two response words and the three constants that bound a pending client go toclient.rs. Beside the moved lines: twomodlines, theuselines,pubon 31 lines, one deleted banner. The four poller-sizing constants stay inmain.rs.The change (
6d2dd8bb2,12e4fb269,61b00f16c,9c3b81538).toyos_i219::I219::tx_roomcounts room from the ring after a reclaim, andtx_reserverefuses 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.wake_on_roomunmasks the transmit cause, then counts again;begin_passmasks it. The 82574 unmasksTXDW | TXQ0; the I219 unmasksTXDWalone. virtio arms nothing: its device notifies for every used buffer on a queue withavail.flags0 and noEVENT_IDX(virtio 1.2 §2.7.7).descriptors.strandedand left in the ring. From the pass that reads the link up over them the part is owed their write-back withinSTRANDED_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.LSCsays 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.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::unsentanddescriptors.unsentare deleted;openismain's single bring-up again.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" aTCTL.CTof 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".Countersandinspectcarry, 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 inICR"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.LSCalone. 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 inICRat the first pass and is a message the momentIMSis written: the arm had no case and is deleted. With the link down the pass that finds the link is nowLSC'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,LSCraised no message and the arm had a case.DmaNic::room(one line), used bytransmit, byreceive, 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.TxQueue, memory alone, so two host tests drive it over a plain allocation.Issues.
netstacks-i219-drops-a-transmit-burst-past-its-ring.mdcloses: its exit's driver half isa_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.mdcloses 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.mdis 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 norestart), thatopen'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 ofthe-lan-is-not-yet-production-grade.mdnow 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:
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:15fcand the harness has no Intel card. That the PCH's MAC raises a message for an unmaskedTXDWis 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_churnrun alone (--jobs 1), the patch reversed; patches are in the mutation comment, logs in the round-3 logs comment.used.idxunmoved since the pass beforeused.idxbefore and after; the twelfth ends netstack so the lines are keptThe arms were measured at
3605c5b3f;userland/netstack/src/virtio_net.rsis 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):
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_backanda_frame_offered_to_a_full_transmit_queue_is_not_takenrun on the host. The loop's guard andreceive's refusal are read by no test; they go withDmaNicat 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_askedwith this round's stub hook, against the driver at12e4fb269: 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).tx_reservea_full_transmit_ring_answers_room_0a_written_back_descriptor_returns_roombegin_passleaves the cause unmaskedthe_transmit_cause_is_armed_only_when_askedwake_on_roomunmasks nothingTXDWalonethe_transmit_cause_is_armed_only_when_askeda_ring_a_link_change_left_full_stays_the_parts,a_ring_never_written_back_is_refuseda_link_that_is_down_has_no_roomthe_transmit_cause_is_armed_only_when_askeda_link_that_is_down_has_no_roomthe_transmit_cause_is_armed_only_when_askeda_ring_never_written_back_is_refuseda_ring_never_written_back_is_refuseda_ring_a_link_change_left_full_stays_the_partsa_ring_a_link_change_left_full_stays_the_partsa_full_transmit_queue_has_no_room_until_the_device_gives_heads_backBoth link-change tests run on both parts.
Held by reading, not by a test: that netstack's loop takes
pass_due_ininto 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'scargo test -p toyos-i219r4-01cargo clippy -p toyos-i219 --all-targets, the adopted lints,-D warningsr4-02cargo test --manifest-path userland/netstack/Cargo.tomlr4-03cargo run -- --build-onlyr4-04cargo test --test toyos-build -- --jobs 2 netstack_socket_churn iommu_virtio_platformr4-05r4-063605c5b3fr3-08,r3-armFNot run by me:
cargo run -- --ci hostand 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
uptimegave 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, at61b00f16cand at3605c5b3f, 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
🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A