From 477dcf945406790712060d67f1e90215a4757ee6 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:36 +0000 Subject: [PATCH 01/10] linux: connect() with no route out is ENETUNREACH, not ECONNREFUSED Guest TCP leaves through the mixnet and nowhere else. When the mixnet holds no gateway, net.sockets answers the connect with E_NO_TRANSPORT (6); the personality turned every non-zero status into ECONNREFUSED, so a program was told a peer had answered and refused it when no packet could have left the machine. Status 6 is now ENETUNREACH, the errno Linux gives when there is no route. Every other refusal stays ECONNREFUSED, and a call that got no reply stays EIO. The mixnet stays the only way out: nothing here opens a direct path. --- userland/capsule_linux/src/linux/abi/errno.rs | 1 + .../capsule_linux/src/linux/net/connect.rs | 20 ++++++++++++------- userland/capsule_linux/src/linux/net/ops.rs | 6 ++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/userland/capsule_linux/src/linux/abi/errno.rs b/userland/capsule_linux/src/linux/abi/errno.rs index 7cbcaebde..4e4ce63f9 100644 --- a/userland/capsule_linux/src/linux/abi/errno.rs +++ b/userland/capsule_linux/src/linux/abi/errno.rs @@ -49,6 +49,7 @@ pub const ENOTCONN: i64 = 107; pub const ENOTSOCK: i64 = 88; pub const ENOTSUP: i64 = 95; pub const EAFNOSUPPORT: i64 = 97; +pub const ENETUNREACH: i64 = 101; pub const ECONNREFUSED: i64 = 111; pub const EINPROGRESS: i64 = 115; diff --git a/userland/capsule_linux/src/linux/net/connect.rs b/userland/capsule_linux/src/linux/net/connect.rs index ce5deac6d..82b62343d 100644 --- a/userland/capsule_linux/src/linux/net/connect.rs +++ b/userland/capsule_linux/src/linux/net/connect.rs @@ -24,7 +24,7 @@ use crate::linux::guest::{Guest, Kind}; use super::addr::inet; use super::call::call; use super::dns::host_for; -use super::ops::{OP_CONNECT, OP_CONNECT_HOST}; +use super::ops::{NET_E_NO_TRANSPORT, OP_CONNECT, OP_CONNECT_HOST}; pub fn connect(guest: &mut Guest, fd: u64, at: u64, len: u64) -> u64 { /* @@ -48,11 +48,7 @@ pub fn connect(guest: &mut Guest, fd: u64, at: u64, len: u64) -> u64 { body.extend_from_slice(&handle.to_le_bytes()); body.extend_from_slice(&ip); body.extend_from_slice(&port.to_le_bytes()); - match call(OP_CONNECT, &body, 0) { - Some((0, _)) => errno::ok(0), - Some(_) => errno::fail(errno::ECONNREFUSED), - None => errno::fail(errno::EIO), - } + outcome(call(OP_CONNECT, &body, 0)) } fn by_host(handle: u32, host: &[u8], port: u16) -> u64 { @@ -64,8 +60,18 @@ fn by_host(handle: u32, host: &[u8], port: u16) -> u64 { body.extend_from_slice(&port.to_le_bytes()); body.push(host.len() as u8); body.extend_from_slice(host); - match call(OP_CONNECT_HOST, &body, 0) { + outcome(call(OP_CONNECT_HOST, &body, 0)) +} + +/* + * What a connect reply means to the guest. No transport is a mixnet holding + * no gateway: there is no route out, which a tool has to read as unreachable + * and not as a peer that answered and refused. + */ +fn outcome(reply: Option<(u16, Vec)>) -> u64 { + match reply { Some((0, _)) => errno::ok(0), + Some((NET_E_NO_TRANSPORT, _)) => errno::fail(errno::ENETUNREACH), Some(_) => errno::fail(errno::ECONNREFUSED), None => errno::fail(errno::EIO), } diff --git a/userland/capsule_linux/src/linux/net/ops.rs b/userland/capsule_linux/src/linux/net/ops.rs index b5392771c..782c0f64c 100644 --- a/userland/capsule_linux/src/linux/net/ops.rs +++ b/userland/capsule_linux/src/linux/net/ops.rs @@ -27,6 +27,12 @@ pub const OP_POLL: u16 = 13; /// The socket kinds the server offers: 1 stream, 2 datagram, 3 mixnet. pub const KIND_MIXNET: u16 = 3; +/// net.sockets' status for "connect had no transport to give the socket": the +/// mixnet holds no gateway, so there is no route out, not a peer that refused. +/// Kept in sync with E_NO_TRANSPORT in +/// userland/capsule_net_sockets/src/protocol/errno.rs. +pub const NET_E_NO_TRANSPORT: u16 = 6; + /// The address family the server takes. It is not AF_INET: the number /// is the server's own and the two only look alike. pub const DOMAIN: u16 = 4; From f64c1cdab900cc00d564dbd9c4c407dfea5983ce Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 02/10] virtio-net, virtio-gpu: order ring writes against the index that publishes them virtio-net wrote descriptors, avail ring slots and the frame itself, then the avail index, with no fence anywhere. The descriptor and ring stores are volatile, but the frame bytes on the TX side are a plain copy, and Rust only orders volatile accesses against each other: the compiler is free to sink the copy past the index store that tells the device to read it. On the RX side the used element and the frame were read after the used index with nothing tying them to it. x86 hardware keeps these in order today; the language does not, and a weakly ordered CPU would not either. A release fence now sits before every avail index store (prime, refill, TX post) and an acquire fence after the used index is seen to move, the same pairing virtio-gpu already used. virtio-gpu had the acquire in the wrong place: after reading the used entry rather than before, so it ordered nothing the entry depended on. It moves ahead of the reads. --- .../capsule_driver_virtio_gpu/src/device/virtqueue/used.rs | 3 ++- userland/capsule_driver_virtio_net/src/queue/post.rs | 4 ++++ userland/capsule_driver_virtio_net/src/queue/post_packet.rs | 4 ++++ userland/capsule_driver_virtio_net/src/rx.rs | 4 ++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/userland/capsule_driver_virtio_gpu/src/device/virtqueue/used.rs b/userland/capsule_driver_virtio_gpu/src/device/virtqueue/used.rs index 07bd58e1b..b0f335b3e 100644 --- a/userland/capsule_driver_virtio_gpu/src/device/virtqueue/used.rs +++ b/userland/capsule_driver_virtio_gpu/src/device/virtqueue/used.rs @@ -34,10 +34,11 @@ pub struct UsedEntry { pub fn read_entry(layout: QueueLayout, ring_slot: u16) -> UsedEntry { let slot = ring_slot % layout.queue_size; let p = used_ring_entry(layout, slot); + // Order these reads after the used index that proved them written. + fence(Ordering::Acquire); unsafe { let id = read_volatile(p); let len = read_volatile(p.add(1)); - fence(Ordering::Acquire); UsedEntry { id, len } } } diff --git a/userland/capsule_driver_virtio_net/src/queue/post.rs b/userland/capsule_driver_virtio_net/src/queue/post.rs index 4e0f7a8db..2edc07f52 100644 --- a/userland/capsule_driver_virtio_net/src/queue/post.rs +++ b/userland/capsule_driver_virtio_net/src/queue/post.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . use core::ptr::{read_volatile, write_volatile}; +use core::sync::atomic::{fence, Ordering}; use super::RxQueue; use crate::constants::{RING_SLOTS, VQ_AVAIL_OFFSET, VQ_DESC_OFFSET, VRING_DESC_F_WRITE}; @@ -35,6 +36,8 @@ impl RxQueue { write_volatile(slot.add(14).cast::(), 0u16); write_volatile(avail.add(2 + i as usize), i); } + // The device may read a slot the moment the index covers it. + fence(Ordering::Release); write_volatile(avail.add(1), self.buf_count); } } @@ -45,6 +48,7 @@ impl RxQueue { let idx = read_volatile(avail.add(1)); let pos = (idx % RING_SLOTS) as usize; write_volatile(avail.add(2 + pos), slot); + fence(Ordering::Release); write_volatile(avail.add(1), idx.wrapping_add(1)); } } diff --git a/userland/capsule_driver_virtio_net/src/queue/post_packet.rs b/userland/capsule_driver_virtio_net/src/queue/post_packet.rs index 86c3fe090..707a8e7e9 100644 --- a/userland/capsule_driver_virtio_net/src/queue/post_packet.rs +++ b/userland/capsule_driver_virtio_net/src/queue/post_packet.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . use core::ptr::{read_volatile, write_volatile}; +use core::sync::atomic::{fence, Ordering}; use super::TxQueue; use crate::constants::{RING_SLOTS, VQ_AVAIL_OFFSET, VQ_DESC_OFFSET}; @@ -34,6 +35,9 @@ impl TxQueue { let idx = read_volatile(avail.add(1)); let pos = (idx % RING_SLOTS) as usize; write_volatile(avail.add(AVAIL_RING_OFFSET / 2 + pos), slot); + // The frame bytes are plain stores; volatile alone would let them + // land after the index that tells the device to read them. + fence(Ordering::Release); write_volatile(avail.add(1), idx.wrapping_add(1)); } } diff --git a/userland/capsule_driver_virtio_net/src/rx.rs b/userland/capsule_driver_virtio_net/src/rx.rs index fdf89cbdf..d296145c4 100644 --- a/userland/capsule_driver_virtio_net/src/rx.rs +++ b/userland/capsule_driver_virtio_net/src/rx.rs @@ -20,6 +20,8 @@ +use core::sync::atomic::{fence, Ordering}; + use crate::constants::{RING_SLOTS, VIRTIO_NET_HDR_LEN}; use crate::queue::RxQueue; @@ -39,6 +41,8 @@ pub unsafe fn take_one(rx: &mut RxQueue) -> Option> { if used == rx.last_used { return None; } + // The element and the frame are only valid once the index is seen. + fence(Ordering::Acquire); let ring_pos = rx.last_used % RING_SLOTS; let (desc_id, used_len) = rx.used_elem_at(ring_pos); From 673f49f85e2cf34dd92c0b1adc263a3f1451dd61 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 03/10] virtio-net, virtio-gpu: leave at once when the device is not there Both capsules re-ran their whole setup in a yield loop for ten seconds when no device matched, then exited 0. The broker is filled from PCI during kernel init, before the first capsule starts, so discovery gives the same answer on every pass; the loop only burned the CPU through that part of boot. On a PC, which has neither device, that was ten seconds of every boot. While virtio-net sat in it, net_core's link probes to driver.virtio_net0 went unanswered and each one waited out its timeout. A failed discovery now exits 2, as e1000, rtl8139 and rtl8169 do for an absent chip. The bounded retry stays for a device that is present and fails a later step. Booted with an e1000 on plain VGA, so with neither device present, both now leave with code 2 as they start and the desktop comes up on the GOP framebuffer. With no virtio-net present, net_core had logged "ipc.call unanswered driver.virtio_net0" five times in a boot; with this it logs it none. --- userland/capsule_driver_virtio_gpu/src/main.rs | 12 ++++++++---- userland/capsule_driver_virtio_net/src/main.rs | 13 +++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/userland/capsule_driver_virtio_gpu/src/main.rs b/userland/capsule_driver_virtio_gpu/src/main.rs index fa103d135..809b0bb8a 100644 --- a/userland/capsule_driver_virtio_gpu/src/main.rs +++ b/userland/capsule_driver_virtio_gpu/src/main.rs @@ -31,10 +31,8 @@ use nonos_libc::{heap_init, mk_exit, mk_service_register, mk_time_millis, mk_yie const SERVICE_NAME: &[u8] = b"driver.virtio_gpu0"; const SERVICE_PORT: u32 = 4226; -// Give the device a bounded window to appear, then exit cleanly. On hardware -// with no virtio-gpu (real hardware presents through the GOP framebuffer) it would -// otherwise retry forever; degrading to a clean exit frees the slot and lets -// the compositor fall back. +// Bounded retry for a device that is present but fails a setup step; a clean +// exit after it frees the slot and lets the compositor fall back to GOP. const PROBE_DEADLINE_MS: i64 = 10_000; #[no_mangle] @@ -42,6 +40,12 @@ pub unsafe extern "C" fn _start() -> ! { if heap_init().is_err() { mk_exit(1); } + // The broker lists every PCI function before the first capsule starts, so + // with no virtio-gpu the retry below only spun through ten seconds of boot + // on real hardware. Leave at once (2, absent); the compositor takes GOP. + if discover::find_virtio_gpu().is_none() { + mk_exit(2); + } let start = mk_time_millis(); let driver = loop { match setup::run() { diff --git a/userland/capsule_driver_virtio_net/src/main.rs b/userland/capsule_driver_virtio_net/src/main.rs index 70930d07f..67ba2e2a1 100644 --- a/userland/capsule_driver_virtio_net/src/main.rs +++ b/userland/capsule_driver_virtio_net/src/main.rs @@ -32,8 +32,7 @@ mod tx; use nonos_libc::{heap_init, mk_exit, mk_time_millis, mk_yield}; -// Bounded probe: exit cleanly if no virtio-net appears, instead of spinning -// forever on hardware that has none (real machines use their physical NIC). +// Bounded retry for a device that is present but fails a setup step. const PROBE_DEADLINE_MS: i64 = 10_000; #[no_mangle] @@ -42,6 +41,16 @@ pub unsafe extern "C" fn _start() -> ! { mk_exit(1); } + /* + * The broker lists every PCI function before the first capsule starts, so + * a machine without a virtio-net has none to wait for. Retrying discovery + * here spun for ten seconds of boot while net_core's link probes to this + * name went unanswered; leave the way the wired drivers do (2, absent). + */ + if discover::find_virtio_net().is_none() { + mk_exit(2); + } + let start = mk_time_millis(); let mut driver = loop { match setup::run() { From 03c8dc952cea4846d1085c25312b6d70f58ac06a Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 04/10] e1000: answer the stack, queue TX and reclaim it, reset like the manual, take short frames The driver never answered the stack. It tagged replies with its own magic (0x4E45_3130) and sent them to KERNEL_REPLY_ENDPOINT, an inbox nothing has read since the stack moved into capsules. net_core and net_l2 speak NNET (0x4E4E_4554) and wait for the answer to their own call, which is how virtio-net replies. Requests are now taken with mk_ipc_recv_from and every answer goes back to the capsule that asked, with mk_ipc_reply, under the NNET tag. The ops and payload layouts already matched. Booted as the only NIC, the e1000 came up and net_core's probe got nothing ("link probe driver.e1000_0 no-answer"). With this it binds and takes the lease itself: "bind: interface up on driver.e1000_0", "lease 10.0.2.15/24 gw 10.0.2.2", and the mixnet directory fetch connects and handshakes over it as it does over virtio-net. TX posted every frame unconditionally and then spun on DD. When the spin ran out the handler answered E_IO but left the descriptor live with TDT already moved, and the next call wrote over the same slot without looking. On silicon the 8254x sets no DD while the link is down (auto-negotiation after SLU, a pulled cable, a blocking switch port), so every send timed out; after 31 of them TDT caught up with TDH, which the part reads as an empty ring, and later posts overwrote buffers it might still be fetching. A frame answered E_IO was also still sent once the link came up, so a retry went out twice. QEMU's model sets DD with the link down, which is why it never showed. TX now keeps a clean index: reclaim walks it over finished descriptors, a ring with no free slot answers E_AGAIN (one slot always stays empty), and a frame is answered once it is queued. The reset wrote CTRL and the receive address within microseconds of RST clearing. A global reset starts an EEPROM auto-load that rewrites RAL0/RAH0 and parts of CTRL, so the drawn station address could be replaced by the factory one afterwards and unicast would go to the wrong filter. The sequence now follows e1000_reset_hw: mask, stop RX and TX, wait 10 ms for bus-master cycles in flight, reset, keep off the part for the first millisecond, poll RST clear against a time bound rather than a spin count, then wait 20 ms for the auto-load (5 ms on 82540/5/6, 20 on 82541/7) before anything is written. Frames under 60 bytes were refused. TCTL.PSP has the part pad them, and ARP (42) and a bare TCP ACK (54) are shorter than that, so IPv4 stopped right after DHCP. A bare header is now the minimum. The driver bound its INTx line and never used it: it polls and IMS is never set. The discovery filter also skipped the NIC when firmware left Interrupt Line at 0xFF, which UEFI commonly does, and a bound line is masked until acked, so a shared GSI stayed masked for any other device on it. No line is bound now, and the capsule gives up the Irq capability in its manifest and in the kernel's spawn spec. Release fences sit before the TDT and RDT writes and the ring hand-over, and an acquire follows the DD reads, as in virtio-net. e1000_proofs: 14 passed. The host shim gains Deadline, the fixture drops the IRQ grant, and the request fixture carries the NNET tag. The no-entropy test still holds the transmitter and receiver to never having been written, so the quiesce writes TCTL as 0. --- src/hardware/e1000_capsule/spawn.rs | 5 +- userland/capsule_driver_e1000/Capsule.mk | 7 ++- .../src/constants/frame.rs | 5 +- userland/capsule_driver_e1000/src/discover.rs | 8 ++- .../capsule_driver_e1000/src/init/reset.rs | 60 +++++++++++++++---- .../capsule_driver_e1000/src/init/rx_setup.rs | 4 ++ .../capsule_driver_e1000/src/init/tx_setup.rs | 4 ++ .../src/protocol/endpoint.rs | 22 ------- .../src/protocol/header.rs | 5 +- .../capsule_driver_e1000/src/protocol/mod.rs | 2 - userland/capsule_driver_e1000/src/queue/rx.rs | 4 ++ userland/capsule_driver_e1000/src/queue/tx.rs | 33 ++++++++-- .../capsule_driver_e1000/src/server/error.rs | 21 ++++--- .../src/server/handlers/health.rs | 4 +- .../src/server/handlers/link_status.rs | 15 ++--- .../src/server/handlers/mac_address.rs | 15 ++--- .../src/server/handlers/rx_packet.rs | 18 +++--- .../src/server/handlers/stats.rs | 10 ++-- .../src/server/handlers/tx_packet.rs | 37 +++++++----- .../capsule_driver_e1000/src/server/runner.rs | 25 ++++---- .../capsule_driver_e1000/src/setup/dma.rs | 16 ++--- .../capsule_driver_e1000/src/setup/driver.rs | 4 +- .../capsule_driver_e1000/src/setup/irq.rs | 35 ----------- .../capsule_driver_e1000/src/setup/mod.rs | 1 - .../src/setup/rollback.rs | 7 +-- .../src/setup/sequence.rs | 21 ++++--- userland/e1000_proofs/libc_shim/src/lib.rs | 19 +++++- .../e1000_proofs/src/conformance/memory.rs | 1 - userland/e1000_proofs/src/e1000_tests.rs | 2 +- 29 files changed, 219 insertions(+), 191 deletions(-) delete mode 100644 userland/capsule_driver_e1000/src/protocol/endpoint.rs delete mode 100644 userland/capsule_driver_e1000/src/setup/irq.rs diff --git a/src/hardware/e1000_capsule/spawn.rs b/src/hardware/e1000_capsule/spawn.rs index 6ceb7901e..422c3cbff 100644 --- a/src/hardware/e1000_capsule/spawn.rs +++ b/src/hardware/e1000_capsule/spawn.rs @@ -15,8 +15,8 @@ // along with this program. If not, see . //! Spawn the e1000 driver capsule with the broker capability -//! bundle. PCI MMIO + INTx + DMA driver — needs IPC | Memory | -//! Driver | DeviceEnum | Mmio | Irq | Dma. No Network cap: frame +//! bundle. PCI MMIO + DMA driver, polled — needs IPC | Memory | +//! Crypto | Driver | DeviceEnum | Mmio | Dma. No Network cap: frame //! transport over IPC, not a network-service authority. use super::client::REPLY_INBOX; @@ -62,7 +62,6 @@ pub fn spawn_driver_e1000_capsule() -> Result<(), SpawnError> { | Capability::Driver.bit() | Capability::DeviceEnum.bit() | Capability::Mmio.bit() - | Capability::Irq.bit() | Capability::Dma.bit(), debug_tag: b"[DRIVER-E1000] load_elf_executable error:", }; diff --git a/userland/capsule_driver_e1000/Capsule.mk b/userland/capsule_driver_e1000/Capsule.mk index 7ba1f5b02..611958a84 100644 --- a/userland/capsule_driver_e1000/Capsule.mk +++ b/userland/capsule_driver_e1000/Capsule.mk @@ -1,4 +1,4 @@ -# e1000 — Intel 8254x gigabit NIC. PCI MMIO + INTx + DMA with +# e1000 — Intel 8254x gigabit NIC. PCI MMIO + DMA, polled, with # separate RX and TX rings (four DMA grants total). Frame-level # transport over IPC; no socket or routing policy. `Network` cap # is intentionally absent — that authority belongs to a future @@ -15,11 +15,12 @@ CAPSULE_FEATURE := nonos-capsule-driver-e1000 CAPSULE_NAMESPACE := systems.nonos.driver.e1000_0 CAPSULE_SERVICE_ENDPOINT := service:4210:driver.e1000_0 CAPSULE_REPLY_ENDPOINT := reply:4211:endpoint.4294967308 -# IPC|Memory|Crypto|Driver|DeviceEnum|Mmio|Irq|Dma = 0xF8039 +# IPC|Memory|Crypto|Driver|DeviceEnum|Mmio|Dma = 0xB8039. No Irq: the driver +# polls and binds no line. # Crypto (0x20) is what the CryptoRandom syscall is gated on. The station address # is drawn rather than read out of the EEPROM, and that draw fails closed, so # without this the card has no address to transmit under. -CAPSULE_REQUIRED_CAPS := 0xF8039 +CAPSULE_REQUIRED_CAPS := 0xB8039 CAPSULE_KERNEL_MIRROR := src/hardware/e1000_capsule include nonos-mk/capsule.mk diff --git a/userland/capsule_driver_e1000/src/constants/frame.rs b/userland/capsule_driver_e1000/src/constants/frame.rs index d56622a15..cd258ba70 100644 --- a/userland/capsule_driver_e1000/src/constants/frame.rs +++ b/userland/capsule_driver_e1000/src/constants/frame.rs @@ -27,5 +27,8 @@ const ETH_HEADER_LEN: usize = 14; const MTU: usize = 1500; pub const MAC_LEN: usize = 6; -pub const MIN_ETHERNET_FRAME: usize = 60; +/// A bare header is the shortest frame taken. TCTL.PSP has the part pad +/// anything under 60 bytes, and an ARP (42) or a bare TCP ACK (54) is shorter +/// than that: refusing them stranded IPv4 right after DHCP. +pub const MIN_ETHERNET_FRAME: usize = ETH_HEADER_LEN; pub const MAX_ETHERNET_FRAME: usize = MTU + ETH_HEADER_LEN; diff --git a/userland/capsule_driver_e1000/src/discover.rs b/userland/capsule_driver_e1000/src/discover.rs index 9219638ae..9dfd3f694 100644 --- a/userland/capsule_driver_e1000/src/discover.rs +++ b/userland/capsule_driver_e1000/src/discover.rs @@ -25,7 +25,6 @@ const PCI_SUBCLASS_ETHERNET: u8 = 0x00; #[derive(Clone, Copy)] pub struct Found { pub device_id: u64, - pub irq_line: u8, pub bar0_size: u64, } @@ -40,14 +39,17 @@ pub fn find_e1000() -> Option { if !is_match(r) { continue; } - if r.irq_pin == 0 || r.irq_line == 0xFF || r.bar_count == 0 { + // Interrupt routing is not asked for: the driver polls. UEFI firmware + // often leaves Interrupt Line at 0xFF, and filtering on it skipped a + // working NIC on exactly the machines this driver is for. + if r.bar_count == 0 { continue; } let bar0 = r.bars[0]; if bar0.kind != BAR_KIND_MMIO || bar0.size == 0 { continue; } - return Some(Found { device_id: r.device_id, irq_line: r.irq_line, bar0_size: bar0.size }); + return Some(Found { device_id: r.device_id, bar0_size: bar0.size }); } None } diff --git a/userland/capsule_driver_e1000/src/init/reset.rs b/userland/capsule_driver_e1000/src/init/reset.rs index e50fc5d78..9f0c73d5c 100644 --- a/userland/capsule_driver_e1000/src/init/reset.rs +++ b/userland/capsule_driver_e1000/src/init/reset.rs @@ -14,34 +14,61 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Hardware reset + IRQ quiesce + link bring-up. CTRL.RST is -//! self-clearing; the loop bound is generous because the device -//! takes a few microseconds to settle. After reset the firmware -//! restores most defaults but leaves all IMS bits set, so the -//! capsule masks every cause through IMC and reads ICR to clear -//! any latched bits before enabling the link. - -use crate::constants::regs::{REG_CTRL, REG_ICR, REG_IMC}; +//! Hardware reset + IRQ quiesce + link bring-up, in the order the 8254x +//! needs on silicon: +//! +//! 1. Mask every cause and stop both DMA engines, then give bus-master +//! cycles already in flight time to drain. Firmware (PXE, UEFI UNDI) or a +//! previous instance can leave RCTL.EN set, and a reset landing mid-DMA +//! is a known hang on PCI-X parts. +//! 2. Set CTRL.RST and poll for it to self-clear, reading nothing in the +//! first microsecond the manual says the part is unreachable. +//! 3. Wait out the EEPROM auto-load the reset starts. It rewrites RAL0/RAH0 +//! and parts of CTRL, so a MAC or SLU written before it finishes can be +//! put back to the factory value: unicast then goes to the wrong filter. +//! 4. Mask again, clear latched causes, and bring the link up. + +use nonos_libc::Deadline; + +use crate::constants::regs::{REG_CTRL, REG_ICR, REG_IMC, REG_RCTL, REG_STATUS, REG_TCTL}; use crate::constants::status::{CTRL_ASDE, CTRL_LRST, CTRL_RST, CTRL_SLU}; use crate::regs::Regs; -const RESET_POLL_BUDGET: u32 = 100_000; +/// Linux e1000_reset_hw's drain before the reset. +const DMA_DRAIN_MS: u64 = 10; +/// The part is not addressable for about a microsecond after RST is set. +const RST_SETTLE_MS: u64 = 1; +/// Bound on RST self-clearing; it takes microseconds on a working part. +const RST_CLEAR_MS: u64 = 50; +/// EEPROM auto-load after a global reset: 5 ms on 82540/82545/82546, +/// 20 ms on 82541/82547, so the longer one covers every listed part. +const EEPROM_RELOAD_MS: u64 = 20; pub fn run(regs: &Regs) -> Result<(), &'static str> { // SAFETY: eK@nonos.systems — `regs` carries a base from a // valid broker MmioMap grant; offsets are 32-bit aligned per // the 8254x manual. unsafe { + // Both engines off. Nothing else is set here: a card that never gets a + // station address is left with neither enable bit ever written. + regs.w32(REG_IMC, 0xFFFF_FFFF); + regs.w32(REG_RCTL, 0); + regs.w32(REG_TCTL, 0); + let _ = regs.r32(REG_STATUS); + hold_ms(DMA_DRAIN_MS); + let ctrl = regs.r32(REG_CTRL); regs.w32(REG_CTRL, ctrl | CTRL_RST); - let mut spins = 0u32; + hold_ms(RST_SETTLE_MS); + let deadline = Deadline::after_ms(RST_CLEAR_MS); while regs.r32(REG_CTRL) & CTRL_RST != 0 { - spins += 1; - if spins > RESET_POLL_BUDGET { + if deadline.expired() { return Err("CTRL.RST did not self-clear"); } core::hint::spin_loop(); } + hold_ms(EEPROM_RELOAD_MS); + regs.w32(REG_IMC, 0xFFFF_FFFF); let _ = regs.r32(REG_ICR); let mut ctrl = regs.r32(REG_CTRL); @@ -51,3 +78,12 @@ pub fn run(regs: &Regs) -> Result<(), &'static str> { } Ok(()) } + +// At least `ms` milliseconds: uptime counts whole milliseconds, so a deadline +// `ms` ahead can fall due up to one early. +fn hold_ms(ms: u64) { + let until = Deadline::after_ms(ms + 1); + while !until.expired() { + core::hint::spin_loop(); + } +} diff --git a/userland/capsule_driver_e1000/src/init/rx_setup.rs b/userland/capsule_driver_e1000/src/init/rx_setup.rs index 2e5120112..8e5438fd1 100644 --- a/userland/capsule_driver_e1000/src/init/rx_setup.rs +++ b/userland/capsule_driver_e1000/src/init/rx_setup.rs @@ -19,6 +19,8 @@ //! and finally enables the receiver via RCTL. RDT points at the //! last valid descriptor index per the 8254x manual. +use core::sync::atomic::{fence, Ordering}; + use crate::constants::queue::{RX_DESC_COUNT, RX_RING_BYTES}; use crate::constants::regs::{REG_RCTL, REG_RDBAH, REG_RDBAL, REG_RDH, REG_RDLEN, REG_RDT}; use crate::constants::status::{RCTL_BAM, RCTL_BSIZE_2048, RCTL_EN, RCTL_SECRC}; @@ -37,6 +39,8 @@ pub fn program(regs: &Regs, rx: &RxRing, ring_phys: u64) { *d = RxDesc::default(); d.buffer_addr = rx.buffer_phys(i as u16); } + // The ring was written with plain stores; the part reads it from here on. + fence(Ordering::Release); regs.w32(REG_RDBAL, (ring_phys & 0xFFFF_FFFF) as u32); regs.w32(REG_RDBAH, (ring_phys >> 32) as u32); regs.w32(REG_RDLEN, RX_RING_BYTES as u32); diff --git a/userland/capsule_driver_e1000/src/init/tx_setup.rs b/userland/capsule_driver_e1000/src/init/tx_setup.rs index fd030aa2e..b6e6710fe 100644 --- a/userland/capsule_driver_e1000/src/init/tx_setup.rs +++ b/userland/capsule_driver_e1000/src/init/tx_setup.rs @@ -19,6 +19,8 @@ //! (`0x00602008`), and enables the transmitter via TCTL with the //! pad-short-packet bit and a 16-retry collision threshold. +use core::sync::atomic::{fence, Ordering}; + use crate::constants::queue::{TX_DESC_COUNT, TX_RING_BYTES}; use crate::constants::regs::{ REG_TCTL, REG_TDBAH, REG_TDBAL, REG_TDH, REG_TDLEN, REG_TDT, REG_TIPG, @@ -39,6 +41,8 @@ pub fn program(regs: &Regs, tx: &TxRing, ring_phys: u64) { for i in 0..TX_DESC_COUNT { *descs.add(i) = TxDesc::default(); } + // The ring was written with plain stores; the part reads it from here on. + fence(Ordering::Release); regs.w32(REG_TDBAL, (ring_phys & 0xFFFF_FFFF) as u32); regs.w32(REG_TDBAH, (ring_phys >> 32) as u32); regs.w32(REG_TDLEN, TX_RING_BYTES as u32); diff --git a/userland/capsule_driver_e1000/src/protocol/endpoint.rs b/userland/capsule_driver_e1000/src/protocol/endpoint.rs deleted file mode 100644 index 25488de1b..000000000 --- a/userland/capsule_driver_e1000/src/protocol/endpoint.rs +++ /dev/null @@ -1,22 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! Reply inbox the kernel-side client owns. Slot 12 in the -//! per-service reply numbering (ramfs=1, keyring=2, entropy=3, -//! crypto=4, vfs=5, virtio_rng=6, market=7, virtio_blk=8, -//! virtio_net=9, ps2_input=A, xhci=B, e1000=C). - -pub const KERNEL_REPLY_ENDPOINT: u64 = 0x1_0000_000C; diff --git a/userland/capsule_driver_e1000/src/protocol/header.rs b/userland/capsule_driver_e1000/src/protocol/header.rs index 8a0162b7f..db8682373 100644 --- a/userland/capsule_driver_e1000/src/protocol/header.rs +++ b/userland/capsule_driver_e1000/src/protocol/header.rs @@ -14,7 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pub const MAGIC: u32 = 0x4E45_3130; +/// "NNET", the NIC protocol net_core and net_l2 speak (virtio-net's too). +/// The per-driver tag this replaced made every request from the stack +/// undecodable, so the wired NICs never served it. +pub const MAGIC: u32 = 0x4E4E_4554; pub const VERSION: u16 = 1; pub const HDR_LEN: usize = 20; diff --git a/userland/capsule_driver_e1000/src/protocol/mod.rs b/userland/capsule_driver_e1000/src/protocol/mod.rs index 8f1131383..adce81f35 100644 --- a/userland/capsule_driver_e1000/src/protocol/mod.rs +++ b/userland/capsule_driver_e1000/src/protocol/mod.rs @@ -16,7 +16,6 @@ mod decode; mod encode; -mod endpoint; mod errno; mod header; mod limits; @@ -24,7 +23,6 @@ mod ops; pub use decode::decode_request; pub use encode::{encode_response_header, write_status}; -pub use endpoint::KERNEL_REPLY_ENDPOINT; pub use errno::{E_AGAIN, E_INVAL, E_IO, E_MSGSIZE}; pub use header::{Request, HDR_LEN, RESP_HDR_LEN}; pub use limits::{ diff --git a/userland/capsule_driver_e1000/src/queue/rx.rs b/userland/capsule_driver_e1000/src/queue/rx.rs index 62d0e6f48..1636f1c8d 100644 --- a/userland/capsule_driver_e1000/src/queue/rx.rs +++ b/userland/capsule_driver_e1000/src/queue/rx.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use core::sync::atomic::{fence, Ordering}; + use crate::constants::queue::{RX_BUFFER_LEN, RX_DESC_COUNT, RX_STATUS_DD, RX_STATUS_EOP}; use crate::constants::MAX_ETHERNET_FRAME; @@ -60,6 +62,8 @@ impl RxRing { if status & RX_STATUS_DD == 0 { return None; } + // Length, errors and the frame are only the part's once DD is seen. + fence(Ordering::Acquire); let errors = unsafe { read_volatile(addr_of!((*desc).errors)) }; let len = unsafe { read_volatile(addr_of!((*desc).length)) }; let idx = self.head; diff --git a/userland/capsule_driver_e1000/src/queue/tx.rs b/userland/capsule_driver_e1000/src/queue/tx.rs index df4539ea7..90aac9af9 100644 --- a/userland/capsule_driver_e1000/src/queue/tx.rs +++ b/userland/capsule_driver_e1000/src/queue/tx.rs @@ -15,9 +15,16 @@ // along with this program. If not, see . //! TX ring state. `post` programs the next descriptor with -//! `EOP|IFCS|RS` and bumps the tail; `done(idx)` polls the -//! per-slot DD bit so the server loop knows the descriptor and -//! its buffer can be reused. +//! `EOP|IFCS|RS` and bumps the tail; `reclaim` walks `clean` forward +//! over descriptors the part has marked DD, and `full` refuses a post +//! that would land on one it still owns. +//! +//! The part holds descriptors it cannot send: with the link down it stops +//! DMA and sets no DD, so a slot is only reusable once `reclaim` has seen +//! it done. One slot always stays empty, or a full ring would move TDT +//! onto TDH, which the part reads as an empty one. + +use core::sync::atomic::{fence, Ordering}; use crate::constants::queue::{ TX_BUFFER_LEN, TX_CMD_EOP, TX_CMD_IFCS, TX_CMD_RS, TX_DESC_COUNT, TX_STATUS_DD, @@ -31,6 +38,8 @@ pub struct TxRing { pub buffer_user_va: u64, pub buffer_device_addr: u64, pub tail: u16, + /// Oldest descriptor not yet seen done; `clean == tail` is an empty ring. + pub clean: u16, } /* @@ -41,7 +50,7 @@ pub struct TxRing { */ impl TxRing { pub fn new(ring_user_va: u64, buffer_user_va: u64, buffer_device_addr: u64) -> Self { - Self { ring_user_va, buffer_user_va, buffer_device_addr, tail: 0 } + Self { ring_user_va, buffer_user_va, buffer_device_addr, tail: 0, clean: 0 } } /// # Safety @@ -78,6 +87,20 @@ impl TxRing { } pub fn done(&self, idx: u16) -> bool { - unsafe { read_volatile(addr_of!((*self.descriptor(idx)).status)) & TX_STATUS_DD != 0 } + let dd = unsafe { read_volatile(addr_of!((*self.descriptor(idx)).status)) } & TX_STATUS_DD; + fence(Ordering::Acquire); + dd != 0 + } + + /// Advance `clean` over every descriptor the part has finished, in order. + pub fn reclaim(&mut self) { + while self.clean != self.tail && self.done(self.clean) { + self.clean = (self.clean + 1) % (TX_DESC_COUNT as u16); + } + } + + /// Whether posting one more descriptor would reach one the part still owns. + pub fn full(&self) -> bool { + (self.tail + 1) % (TX_DESC_COUNT as u16) == self.clean } } diff --git a/userland/capsule_driver_e1000/src/server/error.rs b/userland/capsule_driver_e1000/src/server/error.rs index ae8968207..c17123845 100644 --- a/userland/capsule_driver_e1000/src/server/error.rs +++ b/userland/capsule_driver_e1000/src/server/error.rs @@ -18,19 +18,24 @@ //! the response shape stays uniform: 20-byte echo header followed //! by a four-byte status code. -use nonos_libc::mk_ipc_send; +use nonos_libc::mk_ipc_reply; -use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, RESP_HDR_LEN, STATUS_LEN, -}; +use crate::protocol::{encode_response_header, write_status, Request, RESP_HDR_LEN, STATUS_LEN}; -pub fn reply_with_status(tx: &mut [u8], req: &Request, status: i32) { +/// Answer the capsule that sent the request. Replies used to go to a fixed +/// kernel-side inbox, which nothing reads now that the stack is a capsule, +/// so every call from net_core timed out. +pub fn reply(sender: u32, tx: &[u8], len: usize) { + let _ = mk_ipc_reply(sender, tx.as_ptr(), len); +} + +pub fn reply_with_status(sender: u32, tx: &mut [u8], req: &Request, status: i32) { encode_response_header(tx, req, STATUS_LEN as u32); write_status(&mut tx[RESP_HDR_LEN..], status); - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), RESP_HDR_LEN + STATUS_LEN); + reply(sender, tx, RESP_HDR_LEN + STATUS_LEN); } -pub fn reply_decode_failed(tx: &mut [u8], status: i32) { +pub fn reply_decode_failed(sender: u32, tx: &mut [u8], status: i32) { let req = Request { op: 0, flags: 0, request_id: 0, payload_len: 0 }; - reply_with_status(tx, &req, status); + reply_with_status(sender, tx, &req, status); } diff --git a/userland/capsule_driver_e1000/src/server/handlers/health.rs b/userland/capsule_driver_e1000/src/server/handlers/health.rs index 74921d372..bf7e99f5f 100644 --- a/userland/capsule_driver_e1000/src/server/handlers/health.rs +++ b/userland/capsule_driver_e1000/src/server/handlers/health.rs @@ -19,6 +19,6 @@ use crate::protocol::Request; use crate::server::error::reply_with_status; -pub fn handle(req: &Request, tx: &mut [u8]) { - reply_with_status(tx, req, 0); +pub fn handle(sender: u32, req: &Request, tx: &mut [u8]) { + reply_with_status(sender, tx, req, 0); } diff --git a/userland/capsule_driver_e1000/src/server/handlers/link_status.rs b/userland/capsule_driver_e1000/src/server/handlers/link_status.rs index 6c3afa4da..1360ee74e 100644 --- a/userland/capsule_driver_e1000/src/server/handlers/link_status.rs +++ b/userland/capsule_driver_e1000/src/server/handlers/link_status.rs @@ -19,17 +19,16 @@ //! sampled on every call so a topology change between two probes //! is observable to the kernel client. -use nonos_libc::mk_ipc_send; - use crate::constants::regs::REG_STATUS; use crate::constants::status::STATUS_LU; use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, LINK_STATUS_PAYLOAD_LEN, - RESP_HDR_LEN, STATUS_LEN, + encode_response_header, write_status, Request, LINK_STATUS_PAYLOAD_LEN, RESP_HDR_LEN, + STATUS_LEN, }; +use crate::server::error::reply; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { // SAFETY: eK@nonos.systems — `driver.regs` carries the broker // MmioMap base for BAR0; `REG_STATUS` is a 4-byte-aligned offset // documented in the 8254x manual. @@ -39,9 +38,5 @@ pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { encode_response_header(tx, req, payload_len); write_status(&mut tx[RESP_HDR_LEN..], 0); tx[RESP_HDR_LEN + STATUS_LEN] = if up { 1 } else { 0 }; - let _ = mk_ipc_send( - KERNEL_REPLY_ENDPOINT, - tx.as_ptr(), - RESP_HDR_LEN + STATUS_LEN + LINK_STATUS_PAYLOAD_LEN, - ); + reply(sender, tx, RESP_HDR_LEN + STATUS_LEN + LINK_STATUS_PAYLOAD_LEN); } diff --git a/userland/capsule_driver_e1000/src/server/handlers/mac_address.rs b/userland/capsule_driver_e1000/src/server/handlers/mac_address.rs index 87e758c55..a7f826e97 100644 --- a/userland/capsule_driver_e1000/src/server/handlers/mac_address.rs +++ b/userland/capsule_driver_e1000/src/server/handlers/mac_address.rs @@ -18,23 +18,18 @@ //! bring-up. The kernel client treats an all-zero MAC as a hard //! error so a misprogrammed RAL/RAH never silently passes a validation. -use nonos_libc::mk_ipc_send; - use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, MAC_ADDRESS_PAYLOAD_LEN, - RESP_HDR_LEN, STATUS_LEN, + encode_response_header, write_status, Request, MAC_ADDRESS_PAYLOAD_LEN, RESP_HDR_LEN, + STATUS_LEN, }; +use crate::server::error::reply; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { let payload_len = STATUS_LEN as u32 + MAC_ADDRESS_PAYLOAD_LEN as u32; encode_response_header(tx, req, payload_len); write_status(&mut tx[RESP_HDR_LEN..], 0); tx[RESP_HDR_LEN + STATUS_LEN..RESP_HDR_LEN + STATUS_LEN + MAC_ADDRESS_PAYLOAD_LEN] .copy_from_slice(&driver.mac); - let _ = mk_ipc_send( - KERNEL_REPLY_ENDPOINT, - tx.as_ptr(), - RESP_HDR_LEN + STATUS_LEN + MAC_ADDRESS_PAYLOAD_LEN, - ); + reply(sender, tx, RESP_HDR_LEN + STATUS_LEN + MAC_ADDRESS_PAYLOAD_LEN); } diff --git a/userland/capsule_driver_e1000/src/server/handlers/rx_packet.rs b/userland/capsule_driver_e1000/src/server/handlers/rx_packet.rs index 8e8edc94c..d6a26a117 100644 --- a/userland/capsule_driver_e1000/src/server/handlers/rx_packet.rs +++ b/userland/capsule_driver_e1000/src/server/handlers/rx_packet.rs @@ -14,21 +14,21 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; +use core::sync::atomic::{fence, Ordering}; use crate::constants::regs::REG_RDT; use crate::protocol::{ - encode_response_header, write_status, Request, E_AGAIN, E_IO, KERNEL_REPLY_ENDPOINT, - RESP_HDR_LEN, RX_PAYLOAD_PREFIX_LEN, STATUS_LEN, + encode_response_header, write_status, Request, E_AGAIN, E_IO, RESP_HDR_LEN, + RX_PAYLOAD_PREFIX_LEN, STATUS_LEN, }; -use crate::server::error::reply_with_status; +use crate::server::error::{reply, reply_with_status}; use crate::setup::Driver; -pub fn handle(driver: &mut Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &mut Driver, req: &Request, tx: &mut [u8]) { let (idx, len) = match driver.rx.consume() { Some(p) => p, None => { - reply_with_status(tx, req, E_AGAIN); + reply_with_status(sender, tx, req, E_AGAIN); return; } }; @@ -36,7 +36,7 @@ pub fn handle(driver: &mut Driver, req: &Request, tx: &mut [u8]) { unsafe { driver.regs.w32(REG_RDT, idx as u32); } - reply_with_status(tx, req, E_IO); + reply_with_status(sender, tx, req, E_IO); return; } let body_len = RX_PAYLOAD_PREFIX_LEN + len as usize; @@ -55,8 +55,10 @@ pub fn handle(driver: &mut Driver, req: &Request, tx: &mut [u8]) { unsafe { core::ptr::copy_nonoverlapping(src, tx[body_off..].as_mut_ptr(), n); } + // The copy out of the buffer ends before the part may write it again. + fence(Ordering::Release); unsafe { driver.regs.w32(REG_RDT, idx as u32); } - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), prefix_off + body_len); + reply(sender, tx, prefix_off + body_len); } diff --git a/userland/capsule_driver_e1000/src/server/handlers/stats.rs b/userland/capsule_driver_e1000/src/server/handlers/stats.rs index f644243a2..d4b56c971 100644 --- a/userland/capsule_driver_e1000/src/server/handlers/stats.rs +++ b/userland/capsule_driver_e1000/src/server/handlers/stats.rs @@ -14,17 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::constants::queue::{RX_DESC_COUNT, TX_DESC_COUNT}; use crate::constants::regs::{REG_RCTL, REG_RDH, REG_RDT, REG_STATUS, REG_TCTL, REG_TDH, REG_TDT}; use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, RESP_HDR_LEN, - STATS_PAYLOAD_LEN, STATUS_LEN, + encode_response_header, write_status, Request, RESP_HDR_LEN, STATS_PAYLOAD_LEN, STATUS_LEN, }; +use crate::server::error::reply; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { let payload_len = STATUS_LEN as u32 + STATS_PAYLOAD_LEN as u32; encode_response_header(tx, req, payload_len); write_status(&mut tx[RESP_HDR_LEN..], 0); @@ -32,7 +30,7 @@ pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { for v in live_regs(driver) { put32(tx, &mut o, v); } - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), RESP_HDR_LEN + payload_len as usize); + reply(sender, tx, RESP_HDR_LEN + payload_len as usize); } fn live_regs(driver: &Driver) -> [u32; 12] { diff --git a/userland/capsule_driver_e1000/src/server/handlers/tx_packet.rs b/userland/capsule_driver_e1000/src/server/handlers/tx_packet.rs index 434b876f6..c0db5d640 100644 --- a/userland/capsule_driver_e1000/src/server/handlers/tx_packet.rs +++ b/userland/capsule_driver_e1000/src/server/handlers/tx_packet.rs @@ -14,25 +14,37 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use core::sync::atomic::{fence, Ordering}; + use crate::constants::queue::TX_DESC_COUNT; use crate::constants::regs::REG_TDT; use crate::constants::{MAX_ETHERNET_FRAME, MIN_ETHERNET_FRAME}; -use crate::protocol::{Request, E_INVAL, E_IO, E_MSGSIZE, MAX_TX_PAYLOAD_BYTES}; +use crate::protocol::{Request, E_AGAIN, E_INVAL, E_MSGSIZE, MAX_TX_PAYLOAD_BYTES}; use crate::server::error::reply_with_status; use crate::setup::Driver; -const TX_DD_POLL_BUDGET: u32 = 1_000_000; - -pub fn handle(driver: &mut Driver, req: &Request, body: &[u8], tx: &mut [u8]) { +/* + * A frame is answered once it is queued, not once it is on the wire. Waiting + * for DD held the caller for as long as the link was down, reported E_IO for a + * frame the part still sent later (so a retry sent it twice), and left the + * descriptor posted for the next call to overwrite. Completion is now what + * `reclaim` observes, and a ring with no free slot says so. + */ +pub fn handle(sender: u32, driver: &mut Driver, req: &Request, body: &[u8], tx: &mut [u8]) { if req.payload_len as usize != body.len() { - reply_with_status(tx, req, E_MSGSIZE); + reply_with_status(sender, tx, req, E_MSGSIZE); return; } if body.len() < MIN_ETHERNET_FRAME || body.len() > MAX_ETHERNET_FRAME || body.len() as u32 > MAX_TX_PAYLOAD_BYTES { - reply_with_status(tx, req, E_INVAL); + reply_with_status(sender, tx, req, E_INVAL); + return; + } + driver.tx.reclaim(); + if driver.tx.full() { + reply_with_status(sender, tx, req, E_AGAIN); return; } let dst = driver.tx.buffer_va(driver.tx.tail) as *mut u8; @@ -41,17 +53,10 @@ pub fn handle(driver: &mut Driver, req: &Request, body: &[u8], tx: &mut [u8]) { } let idx = driver.tx.post(body.len() as u16); let next_tdt = ((idx as u32) + 1) % (TX_DESC_COUNT as u32); + // The frame bytes are plain stores; the tail write is what lets the part read them. + fence(Ordering::Release); unsafe { driver.regs.w32(REG_TDT, next_tdt); } - let mut spins = 0u32; - while !driver.tx.done(idx) { - spins += 1; - if spins > TX_DD_POLL_BUDGET { - reply_with_status(tx, req, E_IO); - return; - } - core::hint::spin_loop(); - } - reply_with_status(tx, req, 0); + reply_with_status(sender, tx, req, 0); } diff --git a/userland/capsule_driver_e1000/src/server/runner.rs b/userland/capsule_driver_e1000/src/server/runner.rs index e12d35eaa..2b8037790 100644 --- a/userland/capsule_driver_e1000/src/server/runner.rs +++ b/userland/capsule_driver_e1000/src/server/runner.rs @@ -16,7 +16,7 @@ use alloc::vec; -use nonos_libc::mk_ipc_recv; +use nonos_libc::mk_ipc_recv_from; use crate::constants::MAX_ETHERNET_FRAME; use crate::protocol::{ @@ -39,32 +39,33 @@ pub fn run(driver: &mut Driver) -> ! { let mut tx = vec![0u8; tx_len]; loop { - let n = mk_ipc_recv(SERVICE_INBOX, rx.as_mut_ptr(), rx_len, 0); - if n <= 0 { + let mut sender: u32 = 0; + let n = mk_ipc_recv_from(SERVICE_INBOX, rx.as_mut_ptr(), rx_len, 0, &mut sender); + if n <= 0 || sender == 0 { continue; } let len = n as usize; let req = match decode_request(&rx[..len]) { Some(r) => r, None => { - reply_decode_failed(&mut tx, E_INVAL); + reply_decode_failed(sender, &mut tx, E_INVAL); continue; } }; let body_end = HDR_LEN.saturating_add(req.payload_len as usize); if body_end != len { - reply_with_status(&mut tx, &req, E_MSGSIZE); + reply_with_status(sender, &mut tx, &req, E_MSGSIZE); continue; } let body = &rx[HDR_LEN..body_end]; match req.op { - OP_HEALTHCHECK => handlers::health::handle(&req, &mut tx), - OP_LINK_STATUS => handlers::link_status::handle(driver, &req, &mut tx), - OP_MAC_ADDRESS => handlers::mac_address::handle(driver, &req, &mut tx), - OP_TX_PACKET => handlers::tx_packet::handle(driver, &req, body, &mut tx), - OP_RX_PACKET => handlers::rx_packet::handle(driver, &req, &mut tx), - OP_STATS => handlers::stats::handle(driver, &req, &mut tx), - _ => reply_with_status(&mut tx, &req, E_INVAL), + OP_HEALTHCHECK => handlers::health::handle(sender, &req, &mut tx), + OP_LINK_STATUS => handlers::link_status::handle(sender, driver, &req, &mut tx), + OP_MAC_ADDRESS => handlers::mac_address::handle(sender, driver, &req, &mut tx), + OP_TX_PACKET => handlers::tx_packet::handle(sender, driver, &req, body, &mut tx), + OP_RX_PACKET => handlers::rx_packet::handle(sender, driver, &req, &mut tx), + OP_STATS => handlers::stats::handle(sender, driver, &req, &mut tx), + _ => reply_with_status(sender, &mut tx, &req, E_INVAL), } } } diff --git a/userland/capsule_driver_e1000/src/setup/dma.rs b/userland/capsule_driver_e1000/src/setup/dma.rs index 3a1cea2ff..ad572b648 100644 --- a/userland/capsule_driver_e1000/src/setup/dma.rs +++ b/userland/capsule_driver_e1000/src/setup/dma.rs @@ -19,7 +19,7 @@ //! every prior grant in reverse on failure so the broker never //! holds a partial setup. -use nonos_libc::{mk_dma_map, DmaMapOut, IrqBindOut, MmioMapOut}; +use nonos_libc::{mk_dma_map, DmaMapOut, MmioMapOut}; use crate::constants::queue::{ RX_BUFFER_POOL_BYTES, RX_RING_BYTES, TX_BUFFER_POOL_BYTES, TX_RING_BYTES, @@ -48,27 +48,21 @@ pub fn map_rings_and_buffers( device_id: u64, claim_epoch: u64, mmio: &MmioMapOut, - irq: &IrqBindOut, ) -> Result<(DmaMapOut, DmaMapOut, DmaMapOut, DmaMapOut), &'static str> { let rx_ring = alloc(device_id, claim_epoch, RX_RING_BYTES as u64).ok_or_else(|| { - rollback::after(device_id, mmio, irq, &[]); + rollback::after(device_id, mmio, &[]); "dma map failed (rx ring)" })?; let rx_buf = alloc(device_id, claim_epoch, RX_BUFFER_POOL_BYTES as u64).ok_or_else(|| { - rollback::after(device_id, mmio, irq, &[rx_ring.grant_id]); + rollback::after(device_id, mmio, &[rx_ring.grant_id]); "dma map failed (rx buffers)" })?; let tx_ring = alloc(device_id, claim_epoch, TX_RING_BYTES as u64).ok_or_else(|| { - rollback::after(device_id, mmio, irq, &[rx_buf.grant_id, rx_ring.grant_id]); + rollback::after(device_id, mmio, &[rx_buf.grant_id, rx_ring.grant_id]); "dma map failed (tx ring)" })?; let tx_buf = alloc(device_id, claim_epoch, TX_BUFFER_POOL_BYTES as u64).ok_or_else(|| { - rollback::after( - device_id, - mmio, - irq, - &[tx_ring.grant_id, rx_buf.grant_id, rx_ring.grant_id], - ); + rollback::after(device_id, mmio, &[tx_ring.grant_id, rx_buf.grant_id, rx_ring.grant_id]); "dma map failed (tx buffers)" })?; Ok((rx_ring, rx_buf, tx_ring, tx_buf)) diff --git a/userland/capsule_driver_e1000/src/setup/driver.rs b/userland/capsule_driver_e1000/src/setup/driver.rs index dfd9be9c8..e406025be 100644 --- a/userland/capsule_driver_e1000/src/setup/driver.rs +++ b/userland/capsule_driver_e1000/src/setup/driver.rs @@ -19,7 +19,7 @@ //! shutdown releases the grants in reverse order so the broker //! sees a clean teardown even when the capsule exits voluntarily. -use nonos_libc::{mk_device_release, mk_dma_unmap, mk_irq_unbind, mk_mmio_unmap}; +use nonos_libc::{mk_device_release, mk_dma_unmap, mk_mmio_unmap}; use crate::constants::MAC_LEN; use crate::queue::{RxRing, TxRing}; @@ -28,7 +28,6 @@ use crate::regs::Regs; pub struct Driver { pub device_id: u64, pub mmio_grant: u64, - pub irq_grant: u64, pub rx_ring_grant: u64, pub rx_buffer_grant: u64, pub tx_ring_grant: u64, @@ -51,7 +50,6 @@ impl Driver { let _ = mk_dma_unmap(self.tx_ring_grant); let _ = mk_dma_unmap(self.rx_buffer_grant); let _ = mk_dma_unmap(self.rx_ring_grant); - let _ = mk_irq_unbind(self.irq_grant); let _ = mk_mmio_unmap(self.mmio_grant); let _ = mk_device_release(self.device_id); } diff --git a/userland/capsule_driver_e1000/src/setup/irq.rs b/userland/capsule_driver_e1000/src/setup/irq.rs deleted file mode 100644 index 693475909..000000000 --- a/userland/capsule_driver_e1000/src/setup/irq.rs +++ /dev/null @@ -1,35 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! IRQ phase. Bind the device's INTx line to a broker IRQ slot. -//! On failure the prior MMIO grant is unmapped and the device -//! claim released so the broker is never left holding a partial -//! setup. MSI-X migration is a separate slice. - -use nonos_libc::{mk_device_release, mk_irq_bind, mk_mmio_unmap, IrqBindOut, MmioMapOut}; - -use crate::discover::Found; - -pub fn bind(dev: Found, claim_epoch: u64, mmio: &MmioMapOut) -> Result { - let mut out = IrqBindOut { grant_id: 0, vector: 0 }; - let r = mk_irq_bind(dev.device_id, claim_epoch, dev.irq_line as u32, 0, 0, &mut out); - if r < 0 { - let _ = mk_mmio_unmap(mmio.grant_id); - let _ = mk_device_release(dev.device_id); - return Err("irq bind failed"); - } - Ok(out) -} diff --git a/userland/capsule_driver_e1000/src/setup/mod.rs b/userland/capsule_driver_e1000/src/setup/mod.rs index 0424c3f97..3edb2f738 100644 --- a/userland/capsule_driver_e1000/src/setup/mod.rs +++ b/userland/capsule_driver_e1000/src/setup/mod.rs @@ -17,7 +17,6 @@ mod claim; mod dma; mod driver; -mod irq; mod mmio; mod rollback; mod sequence; diff --git a/userland/capsule_driver_e1000/src/setup/rollback.rs b/userland/capsule_driver_e1000/src/setup/rollback.rs index 9836204b8..831df6333 100644 --- a/userland/capsule_driver_e1000/src/setup/rollback.rs +++ b/userland/capsule_driver_e1000/src/setup/rollback.rs @@ -18,15 +18,12 @@ //! fails partway. Best-effort: an `EINVAL` from a doubly-released //! grant is harmless because the broker has already revoked it. -use nonos_libc::{ - mk_device_release, mk_dma_unmap, mk_irq_unbind, mk_mmio_unmap, IrqBindOut, MmioMapOut, -}; +use nonos_libc::{mk_device_release, mk_dma_unmap, mk_mmio_unmap, MmioMapOut}; -pub fn after(device_id: u64, mmio: &MmioMapOut, irq: &IrqBindOut, dma_grants: &[u64]) { +pub fn after(device_id: u64, mmio: &MmioMapOut, dma_grants: &[u64]) { for &g in dma_grants.iter().rev() { let _ = mk_dma_unmap(g); } - let _ = mk_irq_unbind(irq.grant_id); let _ = mk_mmio_unmap(mmio.grant_id); let _ = mk_device_release(device_id); } diff --git a/userland/capsule_driver_e1000/src/setup/sequence.rs b/userland/capsule_driver_e1000/src/setup/sequence.rs index 93ea12ca4..b123c3c50 100644 --- a/userland/capsule_driver_e1000/src/setup/sequence.rs +++ b/userland/capsule_driver_e1000/src/setup/sequence.rs @@ -14,11 +14,16 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! End-to-end broker handshake: discover -> claim -> MMIO -> IRQ -//! -> RX ring DMA -> RX buffer DMA -> TX ring DMA -> TX buffer -//! DMA. Returns a `Driver` with all grants taken and ring states -//! initialised; the hardware bring-up step in `init` programs the -//! device against those rings. +//! End-to-end broker handshake: discover -> claim -> MMIO -> RX ring +//! DMA -> RX buffer DMA -> TX ring DMA -> TX buffer DMA. Returns a +//! `Driver` with all grants taken and ring states initialised; the +//! hardware bring-up step in `init` programs the device against those +//! rings. +//! +//! No interrupt line is bound. The driver polls and never sets IMS, and a +//! bound INTx line is masked until acked: holding one it never services +//! starved any other device sharing that line, and a failed bind (line +//! already held, or reserved) took down a NIC that needed no interrupt. use crate::constants::MAC_LEN; use crate::discover::find_e1000; @@ -26,19 +31,17 @@ use crate::queue::{RxRing, TxRing}; use crate::regs::Regs; use super::driver::Driver; -use super::{claim, dma, irq, mmio}; +use super::{claim, dma, mmio}; pub fn run() -> Result { let dev = find_e1000().ok_or("no e1000 device")?; let claim_epoch = claim::claim(dev.device_id)?; let mmio_grant = mmio::map(dev, claim_epoch)?; - let irq_grant = irq::bind(dev, claim_epoch, &mmio_grant)?; let (rx_ring, rx_buf, tx_ring, tx_buf) = - dma::map_rings_and_buffers(dev.device_id, claim_epoch, &mmio_grant, &irq_grant)?; + dma::map_rings_and_buffers(dev.device_id, claim_epoch, &mmio_grant)?; Ok(Driver { device_id: dev.device_id, mmio_grant: mmio_grant.grant_id, - irq_grant: irq_grant.grant_id, rx_ring_grant: rx_ring.grant_id, rx_buffer_grant: rx_buf.grant_id, tx_ring_grant: tx_ring.grant_id, diff --git a/userland/e1000_proofs/libc_shim/src/lib.rs b/userland/e1000_proofs/libc_shim/src/lib.rs index 9536cc0cb..f3b3b94c2 100644 --- a/userland/e1000_proofs/libc_shim/src/lib.rs +++ b/userland/e1000_proofs/libc_shim/src/lib.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! The five things the included driver files take from `nonos_libc`. +//! The things the included driver files take from `nonos_libc`. //! //! `crypto_random` is the one that matters. The station address is drawn //! from it, and the driver's promise is that when there is no entropy the @@ -23,6 +23,23 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, MutexGuard}; +use std::time::{Duration, Instant}; + +/// A deadline on the host clock, with the capsule's API. The reset holds the +/// part for the milliseconds the 8254x manual asks, and the tests wait them. +pub struct Deadline { + end: Instant, +} + +impl Deadline { + pub fn after_ms(timeout_ms: u64) -> Self { + Self { end: Instant::now() + Duration::from_millis(timeout_ms) } + } + + pub fn expired(&self) -> bool { + Instant::now() >= self.end + } +} static ENTROPY: AtomicBool = AtomicBool::new(true); static TURN: Mutex<()> = Mutex::new(()); diff --git a/userland/e1000_proofs/src/conformance/memory.rs b/userland/e1000_proofs/src/conformance/memory.rs index 28b59615e..f20f0766d 100644 --- a/userland/e1000_proofs/src/conformance/memory.rs +++ b/userland/e1000_proofs/src/conformance/memory.rs @@ -56,7 +56,6 @@ impl Memory { Driver { device_id: 1, mmio_grant: 0, - irq_grant: 0, rx_ring_grant: 0, rx_buffer_grant: 0, tx_ring_grant: 0, diff --git a/userland/e1000_proofs/src/e1000_tests.rs b/userland/e1000_proofs/src/e1000_tests.rs index 03ef15481..f021b5afa 100644 --- a/userland/e1000_proofs/src/e1000_tests.rs +++ b/userland/e1000_proofs/src/e1000_tests.rs @@ -110,7 +110,7 @@ fn post_programs_exactly_the_tail_descriptor_and_wraps() { #[test] fn decode_never_panics_and_reads_fields_from_their_offsets() { - const MAGIC: u32 = 0x4E45_3130; // the wire tag from protocol/header.rs + const MAGIC: u32 = 0x4E4E_4554; // the NNET wire tag from protocol/header.rs for seed in 1..100_000u64 { let mut s = seed; let blen = (xorshift(&mut s) % 40) as usize; From b12f1c8a51d2e998253228e7265c99a8e36721dc Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 05/10] rtl8139: answer the stack, come up under UEFI, draw the address, fix the ring The driver never answered the stack. It tagged replies with its own magic (0x4E52_3839) and sent them to KERNEL_REPLY_ENDPOINT, an inbox nothing has read since the stack moved into capsules. net_core and net_l2 speak NNET (0x4E4E_4554) and wait for the answer to their own call, which is how virtio-net replies. Requests are now taken with mk_ipc_recv_from and every answer goes back to the capsule that asked, with mk_ipc_reply, under the NNET tag. The ops and payload layouts already matched. Discovery skipped any card whose PCI Interrupt Line read 0xFF. OVMF leaves it there, as UEFI firmware commonly does, so booted under OVMF with an RTL8139 as its only NIC the driver exited 2, absent. The line was only ever bound to be acked; the driver polls. It binds none now, sets IMR to 0 so an unserviced source cannot hold a shared INTx asserted for another device, and gives up the Irq capability. The station address was the factory one, read out of IDR: the identifier nonos_mac exists to keep off the wire, where e1000 and rtl8169 already drew theirs. It is now drawn through nonos_mac too, written as dwords with the config lock open as 8139too writes it, read back, and fails closed. The capsule gains Crypto, which CryptoRandom is gated on, in its manifest and in the kernel's spawn spec. Booted under OVMF as the only NIC, it now binds and takes the lease itself: "bind: interface up on driver.rtl8139_0", "lease 10.0.2.15/24 gw 10.0.2.2", and the mixnet directory fetch connects and handshakes over it. QEMU's `info network` shows the card at b6:54:0b:29:cc:83 rather than its configured 52:54:00:12:34:56, so the lease was taken under the drawn address. RCR set no RBLEN bits, so the chip used an 8K+16 ring while every offset here was taken against 32K. After about 8 KB received the chip wrote at the start of the ring and the driver read zeros at 8K: ROK clear, an error, and every later receive failed the same way. QEMU honours RBLEN too (8192 << RBLEN), so this was reachable in emulation. RBLEN is now 10, the 32K+16 ring the allocation was already sized for. With RCR.WRAP set, a frame that crosses the end of the ring is written on past it into the slack, not back at the start, but reads wrapped modulo the ring and took the tail of that frame from offset 0: stale bytes once a lap. Reads are linear now, and a read past the allocation returns zero. The ring proofs held the modulo behaviour; they now hold the linear one, over a buffer the size of the real allocation, and still check that no read leaves it. The chip does not pad short frames and frames under 60 bytes were refused, so ARP (42) and bare TCP ACKs (54) were never sent. A bare header is the minimum now and send zero-pads to 60. A bad header (ROK clear, or a length no good frame carries) returned without moving, so the same header was read forever. Receive now restarts from the top of the ring the way 8139too's rx_err does. A good frame too big for the caller (a tagged full-size one) is skipped instead of stalling, and 0xFFF0 (still arriving) is left for the next poll. TX walked TSD0-3 with a cursor that stayed put when a send failed, while the chip's own pointer moved on, so every later send waited on a descriptor the chip would never look at again. TX is now four slots with a cursor and a reclaim index, as 8139too keeps cur_tx and dirty_tx: a slot is finished on TOK, TUN or TABT, an abort writes TCR.CLRABT to restart the transmitter, a full ring answers E_AGAIN, and the early-TX threshold starts at 256 bytes rather than 8. Rx and Tx are enabled before RCR and TCR are written, as 8139too and the BSD drivers do. rtl8139_proofs: 7 passed. --- src/hardware/rtl8139_capsule/spawn.rs | 5 +- userland/capsule_driver_rtl8139/Capsule.mk | 9 ++- userland/capsule_driver_rtl8139/Cargo.lock | 5 ++ userland/capsule_driver_rtl8139/Cargo.toml | 1 + .../src/constants/frame.rs | 7 ++- .../src/constants/mod.rs | 2 +- .../src/constants/regs.rs | 13 ++++ .../capsule_driver_rtl8139/src/discover.rs | 8 +-- .../capsule_driver_rtl8139/src/init/mac.rs | 39 +++++++++--- .../capsule_driver_rtl8139/src/init/mod.rs | 2 + .../capsule_driver_rtl8139/src/init/run.rs | 20 ++++-- .../src/init/rx_setup.rs | 37 ++++++++--- .../src/init/tx_setup.rs | 10 ++- .../src/protocol/endpoint.rs | 17 ----- .../src/protocol/header.rs | 5 +- .../src/protocol/mod.rs | 2 - .../src/rx/read_frame.rs | 29 ++++++--- .../capsule_driver_rtl8139/src/rx/recv_one.rs | 4 -- .../capsule_driver_rtl8139/src/rx/ring_u8.rs | 14 ++++- .../src/server/error.rs | 21 ++++--- .../src/server/handlers/health.rs | 4 +- .../src/server/handlers/link_status.rs | 18 ++---- .../src/server/handlers/mac_address.rs | 11 ++-- .../src/server/handlers/rx_packet.rs | 16 +++-- .../src/server/handlers/stats.rs | 17 +++-- .../src/server/handlers/tx_packet.rs | 22 ++++--- .../src/server/runner.rs | 23 +++---- .../capsule_driver_rtl8139/src/setup/dma.rs | 9 ++- .../src/setup/driver.rs | 7 ++- .../capsule_driver_rtl8139/src/setup/irq.rs | 34 ---------- .../capsule_driver_rtl8139/src/setup/mod.rs | 1 - .../src/setup/rollback.rs | 7 +-- .../src/setup/sequence.rs | 7 +-- userland/capsule_driver_rtl8139/src/tx/mod.rs | 3 +- .../src/tx/poll_done.rs | 37 ----------- .../capsule_driver_rtl8139/src/tx/reclaim.rs | 52 +++++++++++++++ .../capsule_driver_rtl8139/src/tx/send.rs | 22 ++++--- userland/rtl8139_proofs/Cargo.lock | 2 +- userland/rtl8139_proofs/src/rtl_tests.rs | 63 +++++++++++-------- 39 files changed, 354 insertions(+), 251 deletions(-) delete mode 100644 userland/capsule_driver_rtl8139/src/protocol/endpoint.rs delete mode 100644 userland/capsule_driver_rtl8139/src/setup/irq.rs delete mode 100644 userland/capsule_driver_rtl8139/src/tx/poll_done.rs create mode 100644 userland/capsule_driver_rtl8139/src/tx/reclaim.rs diff --git a/src/hardware/rtl8139_capsule/spawn.rs b/src/hardware/rtl8139_capsule/spawn.rs index 52d562030..fbd5ab12f 100644 --- a/src/hardware/rtl8139_capsule/spawn.rs +++ b/src/hardware/rtl8139_capsule/spawn.rs @@ -50,9 +50,12 @@ pub fn spawn_driver_rtl8139_capsule() -> Result<(), SpawnError> { target_triple: TARGET_TRIPLE, requested_caps: Capability::IPC.bit() | Capability::Memory.bit() + // The station address is drawn rather than read out of the IDR, + // and CryptoRandom is gated on this capability. The draw fails + // closed, so without it the card never comes up. + | Capability::Crypto.bit() | Capability::Driver.bit() | Capability::DeviceEnum.bit() - | Capability::Irq.bit() | Capability::Dma.bit() | Capability::Pio.bit(), debug_tag: b"[DRIVER-RTL8139] load_elf_executable error:", diff --git a/userland/capsule_driver_rtl8139/Capsule.mk b/userland/capsule_driver_rtl8139/Capsule.mk index 4e73684ba..b476afe6d 100644 --- a/userland/capsule_driver_rtl8139/Capsule.mk +++ b/userland/capsule_driver_rtl8139/Capsule.mk @@ -1,4 +1,4 @@ -# RTL8139 — Realtek 8139 Fast Ethernet NIC. PCI PIO + INTx + DMA. +# RTL8139 — Realtek 8139 Fast Ethernet NIC. PCI PIO + DMA, polled. # Frame-level transport only: no socket, routing, ARP, or IP policy. # Signing, certificate, manifest, and trust-anchor flow are inherited # from nonos-mk/capsule.mk. @@ -12,7 +12,10 @@ CAPSULE_FEATURE := nonos-capsule-driver-rtl8139 CAPSULE_NAMESPACE := systems.nonos.driver.rtl8139_0 CAPSULE_SERVICE_ENDPOINT := service:4212:driver.rtl8139_0 CAPSULE_REPLY_ENDPOINT := reply:4213:endpoint.4294967309 -# IPC|Memory|Driver|DeviceEnum|Irq|Dma|Pio = 0x1D8019 -CAPSULE_REQUIRED_CAPS := 0x1D8019 +# IPC|Memory|Crypto|Driver|DeviceEnum|Dma|Pio = 0x198039 +# Crypto (0x20) is what the CryptoRandom syscall is gated on. The station address +# is drawn rather than read out of the IDR, and that draw fails closed, so +# without this the card never comes up. No Irq: the driver polls. +CAPSULE_REQUIRED_CAPS := 0x198039 include nonos-mk/capsule.mk diff --git a/userland/capsule_driver_rtl8139/Cargo.lock b/userland/capsule_driver_rtl8139/Cargo.lock index 4d4de1200..4213cd65e 100644 --- a/userland/capsule_driver_rtl8139/Cargo.lock +++ b/userland/capsule_driver_rtl8139/Cargo.lock @@ -24,9 +24,14 @@ dependencies = [ name = "nonos_capsule_driver_rtl8139" version = "0.3.0" dependencies = [ + "nonos_mac", "nonos_userland_libc", ] +[[package]] +name = "nonos_mac" +version = "0.3.0" + [[package]] name = "nonos_userland_libc" version = "0.3.0" diff --git a/userland/capsule_driver_rtl8139/Cargo.toml b/userland/capsule_driver_rtl8139/Cargo.toml index 549d0914c..b0f682d28 100644 --- a/userland/capsule_driver_rtl8139/Cargo.toml +++ b/userland/capsule_driver_rtl8139/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [dependencies] nonos_libc = { package = "nonos_userland_libc", path = "../libc" } +nonos_mac = { path = "../nonos_mac" } [profile.release] panic = "abort" diff --git a/userland/capsule_driver_rtl8139/src/constants/frame.rs b/userland/capsule_driver_rtl8139/src/constants/frame.rs index 7414c0b3a..3e464523c 100644 --- a/userland/capsule_driver_rtl8139/src/constants/frame.rs +++ b/userland/capsule_driver_rtl8139/src/constants/frame.rs @@ -15,5 +15,10 @@ // along with this program. If not, see . pub const MAC_LEN: usize = 6; -pub const MIN_ETHERNET_FRAME: usize = 60; +/// A bare header is the shortest frame taken. ARP (42 bytes) and a bare TCP +/// ACK (54) are shorter than the wire minimum, and refusing them stranded +/// IPv4 right after DHCP. +pub const MIN_ETHERNET_FRAME: usize = 14; +/// The part does not pad short frames itself, so `send` does, to this. +pub const MIN_WIRE_FRAME: usize = 60; pub const MAX_ETHERNET_FRAME: usize = 1514; diff --git a/userland/capsule_driver_rtl8139/src/constants/mod.rs b/userland/capsule_driver_rtl8139/src/constants/mod.rs index 6a15762bd..a6ce0b099 100644 --- a/userland/capsule_driver_rtl8139/src/constants/mod.rs +++ b/userland/capsule_driver_rtl8139/src/constants/mod.rs @@ -19,4 +19,4 @@ mod frame; pub mod pci; pub mod regs; -pub use frame::{MAC_LEN, MAX_ETHERNET_FRAME, MIN_ETHERNET_FRAME}; +pub use frame::{MAC_LEN, MAX_ETHERNET_FRAME, MIN_ETHERNET_FRAME, MIN_WIRE_FRAME}; diff --git a/userland/capsule_driver_rtl8139/src/constants/regs.rs b/userland/capsule_driver_rtl8139/src/constants/regs.rs index 5735373bd..4359bce24 100644 --- a/userland/capsule_driver_rtl8139/src/constants/regs.rs +++ b/userland/capsule_driver_rtl8139/src/constants/regs.rs @@ -24,8 +24,13 @@ pub const REG_IMR: u16 = 0x3C; pub const REG_ISR: u16 = 0x3E; pub const REG_TCR: u16 = 0x40; pub const REG_RCR: u16 = 0x44; +/// EEPROM command register, which holds the config-write lock over IDR. +pub const REG_CFG9346: u16 = 0x50; pub const REG_MSR: u16 = 0x58; +pub const CFG9346_UNLOCK: u8 = 0xC0; +pub const CFG9346_LOCK: u8 = 0x00; + pub const CMD_RESET: u8 = 0x10; pub const CMD_RX_ENABLE: u8 = 0x08; pub const CMD_TX_ENABLE: u8 = 0x04; @@ -47,8 +52,16 @@ pub const RCR_ACCEPT_MULTI: u32 = 1 << 2; pub const RCR_ACCEPT_BCAST: u32 = 1 << 3; pub const RCR_WRAP: u32 = 1 << 7; pub const RCR_MXDMA_UNLIMITED: u32 = 7 << 8; +/// RBLEN = 10, a 32K+16 ring. Left at 00 the part wraps at 8K while every +/// offset here is taken against 32K, and receive stops after about 8 KB. +pub const RCR_RBLEN_32K: u32 = 0b10 << 11; pub const TCR_MXDMA_UNLIMITED: u32 = 7 << 8; +/// Restarts a transmitter halted by an aborted frame. +pub const TCR_CLEAR_ABORT: u32 = 1 << 0; pub const TX_STATUS_OK: u32 = 1 << 15; pub const TX_STATUS_UNDERRUN: u32 = 1 << 14; pub const TX_STATUS_ABORT: u32 = 1 << 30; +/// TSD early-transmit threshold in 32-byte units: 8 is 256 bytes, where +/// Linux 8139too starts. Zero is 8 bytes, which underruns on a busy bus. +pub const TSD_ERTXTH_256: u32 = 8 << 16; diff --git a/userland/capsule_driver_rtl8139/src/discover.rs b/userland/capsule_driver_rtl8139/src/discover.rs index 3c139eeb7..a79bcb8d2 100644 --- a/userland/capsule_driver_rtl8139/src/discover.rs +++ b/userland/capsule_driver_rtl8139/src/discover.rs @@ -28,7 +28,6 @@ const MAX_DEVICES: usize = 32; #[derive(Debug, Clone, Copy)] pub struct Found { pub device_id: u64, - pub irq_line: u8, pub pio_bar_index: u8, pub command_bits: u16, } @@ -43,13 +42,12 @@ pub fn find_rtl8139() -> Option { if !is_supported(r) { continue; } - if r.irq_pin == 0 || r.irq_line == 0xFF { - continue; - } + // Interrupt routing is not asked for: the driver polls. UEFI firmware + // often leaves Interrupt Line at 0xFF, and filtering on it skipped a + // present RTL8139 as absent. if let Some(pio_bar_index) = first_pio_bar(r) { return Some(Found { device_id: r.device_id, - irq_line: r.irq_line, pio_bar_index, command_bits: command_bits(r), }); diff --git a/userland/capsule_driver_rtl8139/src/init/mac.rs b/userland/capsule_driver_rtl8139/src/init/mac.rs index 5b55b88bf..976ff4a40 100644 --- a/userland/capsule_driver_rtl8139/src/init/mac.rs +++ b/userland/capsule_driver_rtl8139/src/init/mac.rs @@ -14,18 +14,43 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::constants::regs::REG_MAC0; +use nonos_mac::apply; + +use crate::constants::regs::{CFG9346_LOCK, CFG9346_UNLOCK, REG_CFG9346, REG_MAC0}; use crate::constants::MAC_LEN; use crate::pio::Pio; -pub fn read(pio: &Pio) -> Result<[u8; MAC_LEN], &'static str> { +/// Draw a station address and program it into the IDR registers. +/// +/// Replaces reading the factory address out of them: that address is unique to +/// the chip and every network the machine joins would log it. Fails closed, as +/// the other drivers do; the factory address is not a fallback. +pub fn program(pio: &Pio) -> Result<[u8; MAC_LEN], &'static str> { let mut mac = [0u8; MAC_LEN]; - for (i, byte) in mac.iter_mut().enumerate() { + let rc = nonos_libc::crypto_random(mac.as_mut_ptr(), MAC_LEN); + if rc < 0 || (rc as usize) != MAC_LEN { + return Err("rtl8139 no entropy for station address"); + } + apply(&mut mac); + + // IDR takes writes only with the config lock open, as dwords, the way + // 8139too's set_mac_address writes it; the lock closes on every path. + pio.w8(REG_CFG9346, CFG9346_UNLOCK)?; + let wrote = write_idr(pio, &mac); + pio.w8(REG_CFG9346, CFG9346_LOCK)?; + wrote?; + + let mut readback = [0u8; MAC_LEN]; + for (i, byte) in readback.iter_mut().enumerate() { *byte = pio.r8(REG_MAC0 + i as u16)?; } - if mac == [0; MAC_LEN] || mac == [0xFF; MAC_LEN] { - Err("rtl8139 invalid mac") - } else { - Ok(mac) + if readback != mac { + return Err("rtl8139 station address did not take"); } + Ok(mac) +} + +fn write_idr(pio: &Pio, mac: &[u8; MAC_LEN]) -> Result<(), &'static str> { + pio.w32(REG_MAC0, u32::from_le_bytes([mac[0], mac[1], mac[2], mac[3]]))?; + pio.w32(REG_MAC0 + 4, mac[4] as u32 | (mac[5] as u32) << 8) } diff --git a/userland/capsule_driver_rtl8139/src/init/mod.rs b/userland/capsule_driver_rtl8139/src/init/mod.rs index 4b46526d8..86000769a 100644 --- a/userland/capsule_driver_rtl8139/src/init/mod.rs +++ b/userland/capsule_driver_rtl8139/src/init/mod.rs @@ -21,3 +21,5 @@ mod rx_setup; mod tx_setup; pub use run::bring_up; +pub use rx_setup::restart as restart_rx; +pub use tx_setup::TCR; diff --git a/userland/capsule_driver_rtl8139/src/init/run.rs b/userland/capsule_driver_rtl8139/src/init/run.rs index fe4b82d7d..b4d31f133 100644 --- a/userland/capsule_driver_rtl8139/src/init/run.rs +++ b/userland/capsule_driver_rtl8139/src/init/run.rs @@ -15,18 +15,30 @@ // along with this program. If not, see . use crate::constants::regs::{ - CMD_RX_ENABLE, CMD_TX_ENABLE, ISR_ENABLED, REG_CMD, REG_IMR, REG_ISR, + CMD_RX_ENABLE, CMD_TX_ENABLE, REG_CMD, REG_IMR, REG_ISR, }; use crate::setup::Driver; use super::{mac, reset, rx_setup, tx_setup}; +/* + * Rx and Tx are enabled before RCR and TCR are written. Linux 8139too, and + * the BSD rl and rtk drivers, all do it in this order ("Must enable Tx/Rx + * before setting transfer thresholds!"): on silicon where those writes do + * not take while the engines are off, RCR keeps its reset value and accepts + * nothing. + */ pub fn bring_up(driver: &mut Driver) -> Result<(), &'static str> { reset::run(&driver.pio)?; - driver.mac = mac::read(&driver.pio)?; + driver.mac = mac::program(&driver.pio)?; rx_setup::program(driver)?; tx_setup::program(driver)?; + driver.pio.w8(REG_CMD, CMD_RX_ENABLE | CMD_TX_ENABLE)?; + rx_setup::configure(driver)?; + tx_setup::configure(driver)?; driver.pio.w16(REG_ISR, 0xFFFF)?; - driver.pio.w16(REG_IMR, ISR_ENABLED)?; - driver.pio.w8(REG_CMD, CMD_RX_ENABLE | CMD_TX_ENABLE) + // The driver polls and binds no line, so the part raises none: an + // unmasked source nobody services holds a shared INTx asserted for + // every other device on it. ISR still latches, which is all it reads. + driver.pio.w16(REG_IMR, 0) } diff --git a/userland/capsule_driver_rtl8139/src/init/rx_setup.rs b/userland/capsule_driver_rtl8139/src/init/rx_setup.rs index 59aa1b06c..8b6f8a937 100644 --- a/userland/capsule_driver_rtl8139/src/init/rx_setup.rs +++ b/userland/capsule_driver_rtl8139/src/init/rx_setup.rs @@ -16,19 +16,42 @@ use crate::constants::dma::RX_BUF_DATA_BYTES; use crate::constants::regs::{ - RCR_ACCEPT_BCAST, RCR_ACCEPT_MULTI, RCR_ACCEPT_PHYS, RCR_MXDMA_UNLIMITED, RCR_WRAP, REG_CAPR, - REG_RBSTART, REG_RCR, + CMD_RX_ENABLE, CMD_TX_ENABLE, RCR_ACCEPT_BCAST, RCR_ACCEPT_MULTI, RCR_ACCEPT_PHYS, + RCR_MXDMA_UNLIMITED, RCR_RBLEN_32K, RCR_WRAP, REG_CAPR, REG_CMD, REG_RBSTART, REG_RCR, }; use crate::setup::Driver; +/// Receive configuration. Written only once the receiver is enabled (see +/// `run`): on parts where RCR does not take while RE is clear, the accept +/// bits would otherwise fall back to their reset value and nothing arrives. +pub const RCR: u32 = RCR_ACCEPT_PHYS + | RCR_ACCEPT_MULTI + | RCR_ACCEPT_BCAST + | RCR_WRAP + | RCR_MXDMA_UNLIMITED + | RCR_RBLEN_32K; + pub fn program(driver: &mut Driver) -> Result<(), &'static str> { driver.rx_offset = 0; driver.pio.w32(REG_RBSTART, driver.rx_device_addr as u32)?; - driver.pio.w16(REG_CAPR, capr_for(0))?; - driver.pio.w32( - REG_RCR, - RCR_ACCEPT_PHYS | RCR_ACCEPT_MULTI | RCR_ACCEPT_BCAST | RCR_WRAP | RCR_MXDMA_UNLIMITED, - ) + driver.pio.w16(REG_CAPR, capr_for(0)) +} + +pub fn configure(driver: &Driver) -> Result<(), &'static str> { + driver.pio.w32(REG_RCR, RCR) +} + +/* + * Receive from the top of the ring again, the way Linux 8139too's rx_err + * does. Used when the header at the read position is one the part never + * writes for a good frame: after a FIFO overrun real silicon can leave the + * ring position lost, and waiting on that header would stop receive for good. + */ +pub fn restart(driver: &mut Driver) -> Result<(), &'static str> { + driver.pio.w8(REG_CMD, CMD_TX_ENABLE)?; + driver.pio.w8(REG_CMD, CMD_RX_ENABLE | CMD_TX_ENABLE)?; + configure(driver)?; + program(driver) } fn capr_for(offset: usize) -> u16 { diff --git a/userland/capsule_driver_rtl8139/src/init/tx_setup.rs b/userland/capsule_driver_rtl8139/src/init/tx_setup.rs index 1b5cb69a2..40845ec88 100644 --- a/userland/capsule_driver_rtl8139/src/init/tx_setup.rs +++ b/userland/capsule_driver_rtl8139/src/init/tx_setup.rs @@ -18,11 +18,19 @@ use crate::constants::dma::{TX_SLOT_BYTES, TX_SLOT_COUNT}; use crate::constants::regs::{REG_TCR, REG_TXADDR0, TCR_MXDMA_UNLIMITED}; use crate::setup::Driver; +/// Transmit configuration, written once the transmitter is enabled (see `run`). +pub const TCR: u32 = TCR_MXDMA_UNLIMITED; + pub fn program(driver: &mut Driver) -> Result<(), &'static str> { for idx in 0..TX_SLOT_COUNT { let addr = driver.tx_device_addr + (idx * TX_SLOT_BYTES) as u64; driver.pio.w32(REG_TXADDR0 + (idx as u16 * 4), addr as u32)?; } driver.tx_cur = 0; - driver.pio.w32(REG_TCR, TCR_MXDMA_UNLIMITED) + driver.tx_dirty = 0; + Ok(()) +} + +pub fn configure(driver: &Driver) -> Result<(), &'static str> { + driver.pio.w32(REG_TCR, TCR) } diff --git a/userland/capsule_driver_rtl8139/src/protocol/endpoint.rs b/userland/capsule_driver_rtl8139/src/protocol/endpoint.rs deleted file mode 100644 index 2aedb7562..000000000 --- a/userland/capsule_driver_rtl8139/src/protocol/endpoint.rs +++ /dev/null @@ -1,17 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -pub const KERNEL_REPLY_ENDPOINT: u64 = 0x1_0000_000D; diff --git a/userland/capsule_driver_rtl8139/src/protocol/header.rs b/userland/capsule_driver_rtl8139/src/protocol/header.rs index 83787ec16..f610b5fa5 100644 --- a/userland/capsule_driver_rtl8139/src/protocol/header.rs +++ b/userland/capsule_driver_rtl8139/src/protocol/header.rs @@ -14,7 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pub const MAGIC: u32 = 0x4E52_3839; +/// "NNET", the NIC protocol net_core and net_l2 speak (virtio-net's too). +/// The per-driver tag this replaced made every request from the stack +/// undecodable, so the wired NICs never served it. +pub const MAGIC: u32 = 0x4E4E_4554; pub const VERSION: u16 = 1; pub const HDR_LEN: usize = 20; pub const RESP_HDR_LEN: usize = HDR_LEN; diff --git a/userland/capsule_driver_rtl8139/src/protocol/mod.rs b/userland/capsule_driver_rtl8139/src/protocol/mod.rs index 8f1131383..adce81f35 100644 --- a/userland/capsule_driver_rtl8139/src/protocol/mod.rs +++ b/userland/capsule_driver_rtl8139/src/protocol/mod.rs @@ -16,7 +16,6 @@ mod decode; mod encode; -mod endpoint; mod errno; mod header; mod limits; @@ -24,7 +23,6 @@ mod ops; pub use decode::decode_request; pub use encode::{encode_response_header, write_status}; -pub use endpoint::KERNEL_REPLY_ENDPOINT; pub use errno::{E_AGAIN, E_INVAL, E_IO, E_MSGSIZE}; pub use header::{Request, HDR_LEN, RESP_HDR_LEN}; pub use limits::{ diff --git a/userland/capsule_driver_rtl8139/src/rx/read_frame.rs b/userland/capsule_driver_rtl8139/src/rx/read_frame.rs index 3b7b81222..c3e0cae7d 100644 --- a/userland/capsule_driver_rtl8139/src/rx/read_frame.rs +++ b/userland/capsule_driver_rtl8139/src/rx/read_frame.rs @@ -16,15 +16,22 @@ use core::sync::atomic::{compiler_fence, Ordering}; -use nonos_libc::mk_irq_ack; - use super::advance::advance; use super::copy_ring::copy_ring; use super::ring_u16::ring_u16; use crate::constants::regs::RX_STATUS_OK; use crate::constants::MAX_ETHERNET_FRAME; +use crate::init::restart_rx; use crate::setup::Driver; +/// The length the part shows while a frame is still being written (early RX). +const RX_STILL_ARRIVING: usize = 0xFFF0; +/// Longest frame plus CRC the part hands up with ROK set (Linux 8139too's +/// MAX_ETH_FRAME_SIZE + 4); a longer length is a corrupt header. +const RX_MAX_RAW: usize = 1792 + 4; +/// Shortest raw length a real header carries. +const RX_MIN_RAW: usize = 8; + pub(super) fn read_frame( driver: &mut Driver, out: &mut [u8], @@ -34,17 +41,25 @@ pub(super) fn read_frame( let off = driver.rx_offset; let status = ring_u16(base, off); let raw_len = ring_u16(base, off + 2) as usize; - if (status & RX_STATUS_OK) == 0 || raw_len <= 4 { - let _ = mk_irq_ack(driver.irq_grant); - return Err("rtl8139 rx descriptor error"); + if raw_len == RX_STILL_ARRIVING { + return Ok(None); + } + /* + * Returning here without moving on read the same header forever: one bad + * header ended receive until the capsule restarted. A header no good frame + * carries means the position is lost, so receive starts over. + */ + if (status & RX_STATUS_OK) == 0 || raw_len < RX_MIN_RAW || raw_len > RX_MAX_RAW { + restart_rx(driver)?; + return Err("rtl8139 rx ring restarted"); } let frame_len = raw_len - 4; + // A good frame the caller cannot take (a tagged full-size one): skip it. if frame_len > MAX_ETHERNET_FRAME || frame_len > out.len() { - let _ = mk_irq_ack(driver.irq_grant); + advance(driver, raw_len)?; return Err("rtl8139 rx frame too large"); } copy_ring(base, off + 4, out, frame_len); advance(driver, raw_len)?; - let _ = mk_irq_ack(driver.irq_grant); Ok(Some(frame_len)) } diff --git a/userland/capsule_driver_rtl8139/src/rx/recv_one.rs b/userland/capsule_driver_rtl8139/src/rx/recv_one.rs index 5ebb8dbd5..6b98b4734 100644 --- a/userland/capsule_driver_rtl8139/src/rx/recv_one.rs +++ b/userland/capsule_driver_rtl8139/src/rx/recv_one.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_irq_ack; - use super::read_frame::read_frame; use crate::constants::regs::{ CMD_RX_BUF_EMPTY, ISR_ENABLED, ISR_RX_ERR, ISR_RX_FIFO_OVERFLOW, ISR_RX_OVERFLOW, REG_CMD, @@ -29,11 +27,9 @@ pub fn recv_one(driver: &mut Driver, out: &mut [u8]) -> Result, &' driver.pio.w16(REG_ISR, isr & ISR_ENABLED)?; } if (isr & (ISR_RX_ERR | ISR_RX_OVERFLOW | ISR_RX_FIFO_OVERFLOW)) != 0 { - let _ = mk_irq_ack(driver.irq_grant); return Err("rtl8139 rx interrupt error"); } if (driver.pio.r8(REG_CMD)? & CMD_RX_BUF_EMPTY) != 0 { - let _ = mk_irq_ack(driver.irq_grant); return Ok(None); } read_frame(driver, out) diff --git a/userland/capsule_driver_rtl8139/src/rx/ring_u8.rs b/userland/capsule_driver_rtl8139/src/rx/ring_u8.rs index 2068a2410..54dc4ef59 100644 --- a/userland/capsule_driver_rtl8139/src/rx/ring_u8.rs +++ b/userland/capsule_driver_rtl8139/src/rx/ring_u8.rs @@ -14,8 +14,18 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::constants::dma::RX_BUF_DATA_BYTES; +use crate::constants::dma::RX_BUF_BYTES; +/* + * Linear, not modulo the ring. RCR.WRAP is set, so a frame that crosses the + * end of the ring is written on past it into the slack after, not back at + * the start: taking its tail from offset 0 handed up stale bytes once a lap. + * Offsets stay below the allocation (the header is inside the ring and the + * frame is length-checked first), and anything that would not reads as zero. + */ pub(super) fn ring_u8(base: u64, off: usize) -> u8 { - unsafe { core::ptr::read_volatile((base + (off % RX_BUF_DATA_BYTES) as u64) as *const u8) } + if off >= RX_BUF_BYTES { + return 0; + } + unsafe { core::ptr::read_volatile((base + off as u64) as *const u8) } } diff --git a/userland/capsule_driver_rtl8139/src/server/error.rs b/userland/capsule_driver_rtl8139/src/server/error.rs index 7eedd252d..6bebade1f 100644 --- a/userland/capsule_driver_rtl8139/src/server/error.rs +++ b/userland/capsule_driver_rtl8139/src/server/error.rs @@ -14,19 +14,24 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; +use nonos_libc::mk_ipc_reply; -use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, RESP_HDR_LEN, STATUS_LEN, -}; +use crate::protocol::{encode_response_header, write_status, Request, RESP_HDR_LEN, STATUS_LEN}; -pub fn reply_with_status(tx: &mut [u8], req: &Request, status: i32) { +/// Answer the capsule that sent the request. Replies used to go to a fixed +/// kernel-side inbox, which nothing reads now that the stack is a capsule, +/// so every call from net_core timed out. +pub fn reply(sender: u32, tx: &[u8], len: usize) { + let _ = mk_ipc_reply(sender, tx.as_ptr(), len); +} + +pub fn reply_with_status(sender: u32, tx: &mut [u8], req: &Request, status: i32) { encode_response_header(tx, req, STATUS_LEN as u32); write_status(&mut tx[RESP_HDR_LEN..], status); - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), RESP_HDR_LEN + STATUS_LEN); + reply(sender, tx, RESP_HDR_LEN + STATUS_LEN); } -pub fn reply_decode_failed(tx: &mut [u8], status: i32) { +pub fn reply_decode_failed(sender: u32, tx: &mut [u8], status: i32) { let req = Request { op: 0, flags: 0, request_id: 0, payload_len: 0 }; - reply_with_status(tx, &req, status); + reply_with_status(sender, tx, &req, status); } diff --git a/userland/capsule_driver_rtl8139/src/server/handlers/health.rs b/userland/capsule_driver_rtl8139/src/server/handlers/health.rs index 09630f8dc..4e484ff65 100644 --- a/userland/capsule_driver_rtl8139/src/server/handlers/health.rs +++ b/userland/capsule_driver_rtl8139/src/server/handlers/health.rs @@ -17,6 +17,6 @@ use crate::protocol::Request; use crate::server::error::reply_with_status; -pub fn handle(req: &Request, tx: &mut [u8]) { - reply_with_status(tx, req, 0); +pub fn handle(sender: u32, req: &Request, tx: &mut [u8]) { + reply_with_status(sender, tx, req, 0); } diff --git a/userland/capsule_driver_rtl8139/src/server/handlers/link_status.rs b/userland/capsule_driver_rtl8139/src/server/handlers/link_status.rs index 9969ae899..ed8199384 100644 --- a/userland/capsule_driver_rtl8139/src/server/handlers/link_status.rs +++ b/userland/capsule_driver_rtl8139/src/server/handlers/link_status.rs @@ -14,30 +14,24 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::constants::regs::{MSR_LINK_BAD, REG_MSR}; use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, LINK_STATUS_PAYLOAD_LEN, - RESP_HDR_LEN, STATUS_LEN, + encode_response_header, write_status, Request, LINK_STATUS_PAYLOAD_LEN, RESP_HDR_LEN, + STATUS_LEN, }; -use crate::server::error::reply_with_status; +use crate::server::error::{reply, reply_with_status}; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { let link_up = match driver.pio.r8(REG_MSR) { Ok(v) => ((v & MSR_LINK_BAD) == 0) as u8, Err(_) => { - reply_with_status(tx, req, -5); + reply_with_status(sender, tx, req, -5); return; } }; encode_response_header(tx, req, (STATUS_LEN + LINK_STATUS_PAYLOAD_LEN) as u32); write_status(&mut tx[RESP_HDR_LEN..], 0); tx[RESP_HDR_LEN + STATUS_LEN] = link_up; - let _ = mk_ipc_send( - KERNEL_REPLY_ENDPOINT, - tx.as_ptr(), - RESP_HDR_LEN + STATUS_LEN + LINK_STATUS_PAYLOAD_LEN, - ); + reply(sender, tx, RESP_HDR_LEN + STATUS_LEN + LINK_STATUS_PAYLOAD_LEN); } diff --git a/userland/capsule_driver_rtl8139/src/server/handlers/mac_address.rs b/userland/capsule_driver_rtl8139/src/server/handlers/mac_address.rs index 1a5711b4a..35a232d54 100644 --- a/userland/capsule_driver_rtl8139/src/server/handlers/mac_address.rs +++ b/userland/capsule_driver_rtl8139/src/server/handlers/mac_address.rs @@ -14,18 +14,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, MAC_ADDRESS_PAYLOAD_LEN, - RESP_HDR_LEN, STATUS_LEN, + encode_response_header, write_status, Request, MAC_ADDRESS_PAYLOAD_LEN, RESP_HDR_LEN, + STATUS_LEN, }; +use crate::server::error::reply; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { encode_response_header(tx, req, (STATUS_LEN + MAC_ADDRESS_PAYLOAD_LEN) as u32); write_status(&mut tx[RESP_HDR_LEN..], 0); let off = RESP_HDR_LEN + STATUS_LEN; tx[off..off + MAC_ADDRESS_PAYLOAD_LEN].copy_from_slice(&driver.mac); - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), off + MAC_ADDRESS_PAYLOAD_LEN); + reply(sender, tx, off + MAC_ADDRESS_PAYLOAD_LEN); } diff --git a/userland/capsule_driver_rtl8139/src/server/handlers/rx_packet.rs b/userland/capsule_driver_rtl8139/src/server/handlers/rx_packet.rs index 383217236..c07d8c76a 100644 --- a/userland/capsule_driver_rtl8139/src/server/handlers/rx_packet.rs +++ b/userland/capsule_driver_rtl8139/src/server/handlers/rx_packet.rs @@ -14,26 +14,24 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::protocol::{ - encode_response_header, write_status, Request, E_AGAIN, E_IO, KERNEL_REPLY_ENDPOINT, - RESP_HDR_LEN, RX_PAYLOAD_PREFIX_LEN, STATUS_LEN, + encode_response_header, write_status, Request, E_AGAIN, E_IO, RESP_HDR_LEN, + RX_PAYLOAD_PREFIX_LEN, STATUS_LEN, }; use crate::rx::recv_one; -use crate::server::error::reply_with_status; +use crate::server::error::{reply, reply_with_status}; use crate::setup::Driver; -pub fn handle(driver: &mut Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &mut Driver, req: &Request, tx: &mut [u8]) { let body_off = RESP_HDR_LEN + STATUS_LEN + RX_PAYLOAD_PREFIX_LEN; let frame_len = match recv_one(driver, &mut tx[body_off..]) { Ok(Some(n)) => n, Ok(None) => { - reply_with_status(tx, req, E_AGAIN); + reply_with_status(sender, tx, req, E_AGAIN); return; } Err(_) => { - reply_with_status(tx, req, E_IO); + reply_with_status(sender, tx, req, E_IO); return; } }; @@ -41,5 +39,5 @@ pub fn handle(driver: &mut Driver, req: &Request, tx: &mut [u8]) { encode_response_header(tx, req, STATUS_LEN as u32 + body_len as u32); write_status(&mut tx[RESP_HDR_LEN..], 0); tx[RESP_HDR_LEN + STATUS_LEN..body_off].copy_from_slice(&(frame_len as u32).to_le_bytes()); - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), body_off + frame_len); + reply(sender, tx, body_off + frame_len); } diff --git a/userland/capsule_driver_rtl8139/src/server/handlers/stats.rs b/userland/capsule_driver_rtl8139/src/server/handlers/stats.rs index 8351d20f0..6bec8e482 100644 --- a/userland/capsule_driver_rtl8139/src/server/handlers/stats.rs +++ b/userland/capsule_driver_rtl8139/src/server/handlers/stats.rs @@ -14,23 +14,22 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - +use crate::constants::dma::TX_SLOT_COUNT; use crate::constants::regs::{ REG_CAPR, REG_CMD, REG_ISR, REG_MSR, REG_RCR, REG_TCR, REG_TXSTATUS0, }; use crate::protocol::{ - encode_response_header, write_status, Request, E_IO, KERNEL_REPLY_ENDPOINT, RESP_HDR_LEN, - STATS_PAYLOAD_LEN, STATUS_LEN, + encode_response_header, write_status, Request, E_IO, RESP_HDR_LEN, STATS_PAYLOAD_LEN, + STATUS_LEN, }; -use crate::server::error::reply_with_status; +use crate::server::error::{reply, reply_with_status}; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { let regs = match live_regs(driver) { Ok(v) => v, Err(()) => { - reply_with_status(tx, req, E_IO); + reply_with_status(sender, tx, req, E_IO); return; } }; @@ -41,7 +40,7 @@ pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { for v in regs { put32(tx, &mut o, v); } - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), RESP_HDR_LEN + payload_len as usize); + reply(sender, tx, RESP_HDR_LEN + payload_len as usize); } fn live_regs(driver: &Driver) -> Result<[u32; 12], ()> { @@ -57,7 +56,7 @@ fn live_regs(driver: &Driver) -> Result<[u32; 12], ()> { driver.pio.r32(REG_TXSTATUS0 + 8).map_err(|_| ())?, driver.pio.r32(REG_TXSTATUS0 + 12).map_err(|_| ())?, driver.rx_offset as u32, - driver.tx_cur as u32, + (driver.tx_cur % TX_SLOT_COUNT) as u32, ]) } diff --git a/userland/capsule_driver_rtl8139/src/server/handlers/tx_packet.rs b/userland/capsule_driver_rtl8139/src/server/handlers/tx_packet.rs index 38185d284..a90d6a0ad 100644 --- a/userland/capsule_driver_rtl8139/src/server/handlers/tx_packet.rs +++ b/userland/capsule_driver_rtl8139/src/server/handlers/tx_packet.rs @@ -15,25 +15,33 @@ // along with this program. If not, see . use crate::constants::{MAX_ETHERNET_FRAME, MIN_ETHERNET_FRAME}; -use crate::protocol::{Request, E_INVAL, E_IO, E_MSGSIZE, MAX_TX_PAYLOAD_BYTES}; +use crate::protocol::{Request, E_AGAIN, E_INVAL, E_IO, E_MSGSIZE, MAX_TX_PAYLOAD_BYTES}; use crate::server::error::reply_with_status; use crate::setup::Driver; -use crate::tx::send; +use crate::tx::{full, reclaim, send}; -pub fn handle(driver: &mut Driver, req: &Request, body: &[u8], tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &mut Driver, req: &Request, body: &[u8], tx: &mut [u8]) { if req.payload_len as usize != body.len() { - reply_with_status(tx, req, E_MSGSIZE); + reply_with_status(sender, tx, req, E_MSGSIZE); return; } if body.len() < MIN_ETHERNET_FRAME || body.len() > MAX_ETHERNET_FRAME || body.len() as u32 > MAX_TX_PAYLOAD_BYTES { - reply_with_status(tx, req, E_INVAL); + reply_with_status(sender, tx, req, E_INVAL); + return; + } + if reclaim(driver).is_err() { + reply_with_status(sender, tx, req, E_IO); + return; + } + if full(driver) { + reply_with_status(sender, tx, req, E_AGAIN); return; } match send(driver, body) { - Ok(()) => reply_with_status(tx, req, 0), - Err(_) => reply_with_status(tx, req, E_IO), + Ok(()) => reply_with_status(sender, tx, req, 0), + Err(_) => reply_with_status(sender, tx, req, E_IO), } } diff --git a/userland/capsule_driver_rtl8139/src/server/runner.rs b/userland/capsule_driver_rtl8139/src/server/runner.rs index 5506760f4..b65613da2 100644 --- a/userland/capsule_driver_rtl8139/src/server/runner.rs +++ b/userland/capsule_driver_rtl8139/src/server/runner.rs @@ -16,7 +16,7 @@ use alloc::vec; -use nonos_libc::mk_ipc_recv; +use nonos_libc::mk_ipc_recv_from; use crate::constants::MAX_ETHERNET_FRAME; use crate::protocol::{ @@ -42,26 +42,27 @@ pub fn run(driver: &mut Driver) -> ! { } fn dispatch_once(driver: &mut Driver, rx: &mut [u8], tx: &mut [u8]) { - let n = mk_ipc_recv(SERVICE_INBOX, rx.as_mut_ptr(), rx.len(), 0); - if n <= 0 { + let mut sender: u32 = 0; + let n = mk_ipc_recv_from(SERVICE_INBOX, rx.as_mut_ptr(), rx.len(), 0, &mut sender); + if n <= 0 || sender == 0 { return; } let len = n as usize; let req = match decode_request(&rx[..len]) { Some(r) => r, None => { - reply_decode_failed(tx, E_INVAL); + reply_decode_failed(sender, tx, E_INVAL); return; } }; let body = &rx[HDR_LEN..len]; match req.op { - OP_HEALTHCHECK => handlers::health::handle(&req, tx), - OP_LINK_STATUS => handlers::link_status::handle(driver, &req, tx), - OP_MAC_ADDRESS => handlers::mac_address::handle(driver, &req, tx), - OP_TX_PACKET => handlers::tx_packet::handle(driver, &req, body, tx), - OP_RX_PACKET => handlers::rx_packet::handle(driver, &req, tx), - OP_STATS => handlers::stats::handle(driver, &req, tx), - _ => reply_with_status(tx, &req, E_INVAL), + OP_HEALTHCHECK => handlers::health::handle(sender, &req, tx), + OP_LINK_STATUS => handlers::link_status::handle(sender, driver, &req, tx), + OP_MAC_ADDRESS => handlers::mac_address::handle(sender, driver, &req, tx), + OP_TX_PACKET => handlers::tx_packet::handle(sender, driver, &req, body, tx), + OP_RX_PACKET => handlers::rx_packet::handle(sender, driver, &req, tx), + OP_STATS => handlers::stats::handle(sender, driver, &req, tx), + _ => reply_with_status(sender, tx, &req, E_INVAL), } } diff --git a/userland/capsule_driver_rtl8139/src/setup/dma.rs b/userland/capsule_driver_rtl8139/src/setup/dma.rs index 0fe07ce0d..f1d795f1f 100644 --- a/userland/capsule_driver_rtl8139/src/setup/dma.rs +++ b/userland/capsule_driver_rtl8139/src/setup/dma.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::{mk_dma_map, DmaMapOut, IrqBindOut, PioGrantOut}; +use nonos_libc::{mk_dma_map, DmaMapOut, PioGrantOut}; use crate::constants::dma::{RX_BUF_BYTES, TX_BUF_BYTES}; @@ -40,18 +40,17 @@ pub fn map_all( device_id: u64, epoch: u64, pio: &PioGrantOut, - irq: &IrqBindOut, ) -> Result<(DmaMapOut, DmaMapOut), &'static str> { let rx = alloc(device_id, epoch, RX_BUF_BYTES as u64).ok_or_else(|| { - rollback::after_irq(device_id, pio, irq, &[]); + rollback::after_pio(device_id, pio, &[]); "rx dma failed" })?; let tx = alloc(device_id, epoch, TX_BUF_BYTES as u64).ok_or_else(|| { - rollback::after_irq(device_id, pio, irq, &[rx.grant_id]); + rollback::after_pio(device_id, pio, &[rx.grant_id]); "tx dma failed" })?; if rx.device_addr > u32::MAX as u64 || tx.device_addr > u32::MAX as u64 { - rollback::after_irq(device_id, pio, irq, &[tx.grant_id, rx.grant_id]); + rollback::after_pio(device_id, pio, &[tx.grant_id, rx.grant_id]); return Err("rtl8139 requires 32-bit dma"); } Ok((rx, tx)) diff --git a/userland/capsule_driver_rtl8139/src/setup/driver.rs b/userland/capsule_driver_rtl8139/src/setup/driver.rs index eaa658e54..f59afa0a6 100644 --- a/userland/capsule_driver_rtl8139/src/setup/driver.rs +++ b/userland/capsule_driver_rtl8139/src/setup/driver.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::{mk_device_release, mk_dma_unmap, mk_irq_unbind, mk_pio_release}; +use nonos_libc::{mk_device_release, mk_dma_unmap, mk_pio_release}; use crate::constants::MAC_LEN; use crate::pio::Pio; @@ -22,7 +22,6 @@ use crate::pio::Pio; pub struct Driver { pub device_id: u64, pub pio_grant: u64, - pub irq_grant: u64, pub rx_grant: u64, pub tx_grant: u64, pub rx_user_va: u64, @@ -30,7 +29,10 @@ pub struct Driver { pub tx_user_va: u64, pub tx_device_addr: u64, pub rx_offset: usize, + /// Frames handed to the part, counting up; the slot is `tx_cur % 4`. pub tx_cur: usize, + /// Frames the part has finished with; `tx_cur - tx_dirty` are in flight. + pub tx_dirty: usize, pub pio: Pio, pub mac: [u8; MAC_LEN], } @@ -39,7 +41,6 @@ impl Driver { pub fn release(&self) { let _ = mk_dma_unmap(self.tx_grant); let _ = mk_dma_unmap(self.rx_grant); - let _ = mk_irq_unbind(self.irq_grant); let _ = mk_pio_release(self.pio_grant); let _ = mk_device_release(self.device_id); } diff --git a/userland/capsule_driver_rtl8139/src/setup/irq.rs b/userland/capsule_driver_rtl8139/src/setup/irq.rs deleted file mode 100644 index 212204ea5..000000000 --- a/userland/capsule_driver_rtl8139/src/setup/irq.rs +++ /dev/null @@ -1,34 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use nonos_libc::{mk_device_release, mk_irq_bind, mk_pio_release, IrqBindOut, PioGrantOut}; - -use crate::discover::Found; - -pub fn bind(dev: Found, claim_epoch: u64, pio: &PioGrantOut) -> Result { - let mut out = IrqBindOut { grant_id: 0, vector: 0 }; - let r = mk_irq_bind(dev.device_id, claim_epoch, dev.irq_line as u32, 0, 0, &mut out); - if r < 0 { - after_pio(dev.device_id, pio); - return Err("irq bind failed"); - } - Ok(out) -} - -pub fn after_pio(device_id: u64, pio: &PioGrantOut) { - let _ = mk_pio_release(pio.grant_id); - let _ = mk_device_release(device_id); -} diff --git a/userland/capsule_driver_rtl8139/src/setup/mod.rs b/userland/capsule_driver_rtl8139/src/setup/mod.rs index c3dd5c6e4..a9315f5d1 100644 --- a/userland/capsule_driver_rtl8139/src/setup/mod.rs +++ b/userland/capsule_driver_rtl8139/src/setup/mod.rs @@ -17,7 +17,6 @@ mod claim; mod dma; mod driver; -mod irq; mod pci; mod pio_grant; mod rollback; diff --git a/userland/capsule_driver_rtl8139/src/setup/rollback.rs b/userland/capsule_driver_rtl8139/src/setup/rollback.rs index 8b1a240c1..dba4df308 100644 --- a/userland/capsule_driver_rtl8139/src/setup/rollback.rs +++ b/userland/capsule_driver_rtl8139/src/setup/rollback.rs @@ -14,15 +14,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::{ - mk_device_release, mk_dma_unmap, mk_irq_unbind, mk_pio_release, IrqBindOut, PioGrantOut, -}; +use nonos_libc::{mk_device_release, mk_dma_unmap, mk_pio_release, PioGrantOut}; -pub fn after_irq(device_id: u64, pio: &PioGrantOut, irq: &IrqBindOut, dma_grants: &[u64]) { +pub fn after_pio(device_id: u64, pio: &PioGrantOut, dma_grants: &[u64]) { for grant in dma_grants { let _ = mk_dma_unmap(*grant); } - let _ = mk_irq_unbind(irq.grant_id); let _ = mk_pio_release(pio.grant_id); let _ = mk_device_release(device_id); } diff --git a/userland/capsule_driver_rtl8139/src/setup/sequence.rs b/userland/capsule_driver_rtl8139/src/setup/sequence.rs index e44fc5460..04743684d 100644 --- a/userland/capsule_driver_rtl8139/src/setup/sequence.rs +++ b/userland/capsule_driver_rtl8139/src/setup/sequence.rs @@ -19,19 +19,17 @@ use crate::discover::find_rtl8139; use crate::pio::Pio; use super::driver::Driver; -use super::{claim, dma, irq, pci, pio_grant}; +use super::{claim, dma, pci, pio_grant}; pub fn run() -> Result { let dev = find_rtl8139().ok_or("no rtl8139 device")?; let epoch = claim::claim(dev.device_id)?; pci::enable(dev, epoch)?; let pio = pio_grant::grant(dev, epoch)?; - let irq = irq::bind(dev, epoch, &pio)?; - let (rx, tx) = dma::map_all(dev.device_id, epoch, &pio, &irq)?; + let (rx, tx) = dma::map_all(dev.device_id, epoch, &pio)?; Ok(Driver { device_id: dev.device_id, pio_grant: pio.grant_id, - irq_grant: irq.grant_id, rx_grant: rx.grant_id, tx_grant: tx.grant_id, rx_user_va: rx.user_va, @@ -40,6 +38,7 @@ pub fn run() -> Result { tx_device_addr: tx.device_addr, rx_offset: 0, tx_cur: 0, + tx_dirty: 0, pio: Pio::new(pio.grant_id), mac: [0u8; MAC_LEN], }) diff --git a/userland/capsule_driver_rtl8139/src/tx/mod.rs b/userland/capsule_driver_rtl8139/src/tx/mod.rs index 9ba7f0c1d..2afb0f51b 100644 --- a/userland/capsule_driver_rtl8139/src/tx/mod.rs +++ b/userland/capsule_driver_rtl8139/src/tx/mod.rs @@ -14,7 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -mod poll_done; +mod reclaim; mod send; +pub use reclaim::{full, reclaim}; pub use send::send; diff --git a/userland/capsule_driver_rtl8139/src/tx/poll_done.rs b/userland/capsule_driver_rtl8139/src/tx/poll_done.rs deleted file mode 100644 index f3cd58557..000000000 --- a/userland/capsule_driver_rtl8139/src/tx/poll_done.rs +++ /dev/null @@ -1,37 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use core::sync::atomic::{compiler_fence, Ordering}; - -use crate::constants::regs::{TX_STATUS_ABORT, TX_STATUS_OK, TX_STATUS_UNDERRUN}; -use crate::setup::Driver; - -const TX_POLL_BUDGET: u32 = 1_000_000; - -pub(super) fn poll_done(driver: &Driver, status_reg: u16) -> Result<(), &'static str> { - for _ in 0..TX_POLL_BUDGET { - compiler_fence(Ordering::Acquire); - let status = driver.pio.r32(status_reg)?; - if (status & (TX_STATUS_ABORT | TX_STATUS_UNDERRUN)) != 0 { - return Err("rtl8139 tx error"); - } - if (status & TX_STATUS_OK) != 0 { - return Ok(()); - } - core::hint::spin_loop(); - } - Err("rtl8139 tx timeout") -} diff --git a/userland/capsule_driver_rtl8139/src/tx/reclaim.rs b/userland/capsule_driver_rtl8139/src/tx/reclaim.rs new file mode 100644 index 000000000..b416352aa --- /dev/null +++ b/userland/capsule_driver_rtl8139/src/tx/reclaim.rs @@ -0,0 +1,52 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Walk `tx_dirty` over the descriptors the part has finished with. +//! +//! The part works through TSD0-3 strictly in order with a pointer of its own, +//! so the driver has to move with it: a descriptor is finished once its status +//! says the frame went (TOK), went after an underrun re-fetch (TUN), or was +//! given up on (TABT), and each of those moves the part on to the next one. +//! Staying on a slot the part has left made every later send wait on a +//! descriptor it would never look at again. An abort also halts the +//! transmitter until TCR.CLRABT is written, as Linux 8139too does. + +use crate::constants::dma::TX_SLOT_COUNT; +use crate::constants::regs::{ + REG_TCR, REG_TXSTATUS0, TCR_CLEAR_ABORT, TX_STATUS_ABORT, TX_STATUS_OK, TX_STATUS_UNDERRUN, +}; +use crate::init::TCR; +use crate::setup::Driver; + +pub fn reclaim(driver: &mut Driver) -> Result<(), &'static str> { + while driver.tx_dirty != driver.tx_cur { + let idx = driver.tx_dirty % TX_SLOT_COUNT; + let status = driver.pio.r32(REG_TXSTATUS0 + (idx as u16 * 4))?; + if status & (TX_STATUS_OK | TX_STATUS_UNDERRUN | TX_STATUS_ABORT) == 0 { + break; + } + if status & TX_STATUS_ABORT != 0 { + driver.pio.w32(REG_TCR, TCR | TCR_CLEAR_ABORT)?; + } + driver.tx_dirty = driver.tx_dirty.wrapping_add(1); + } + Ok(()) +} + +/// Whether every slot holds a frame the part has not finished with. +pub fn full(driver: &Driver) -> bool { + driver.tx_cur.wrapping_sub(driver.tx_dirty) >= TX_SLOT_COUNT +} diff --git a/userland/capsule_driver_rtl8139/src/tx/send.rs b/userland/capsule_driver_rtl8139/src/tx/send.rs index 97190566e..151cfc6ca 100644 --- a/userland/capsule_driver_rtl8139/src/tx/send.rs +++ b/userland/capsule_driver_rtl8139/src/tx/send.rs @@ -16,21 +16,29 @@ use core::sync::atomic::{compiler_fence, Ordering}; -use super::poll_done::poll_done; use crate::constants::dma::{TX_SLOT_BYTES, TX_SLOT_COUNT}; -use crate::constants::regs::REG_TXSTATUS0; +use crate::constants::regs::{REG_TXSTATUS0, TSD_ERTXTH_256}; +use crate::constants::MIN_WIRE_FRAME; use crate::setup::Driver; +/* + * Hand one frame to the next slot. The caller has reclaimed and checked for + * room, so the part is finished with this slot. The frame is answered once it + * is queued: the part sends it when it can, and `reclaim` sees it done. The + * part does not pad, so a short frame is zero-filled to the 60-byte minimum. + */ pub fn send(driver: &mut Driver, frame: &[u8]) -> Result<(), &'static str> { - let idx = driver.tx_cur; + let idx = driver.tx_cur % TX_SLOT_COUNT; let va = driver.tx_user_va + (idx * TX_SLOT_BYTES) as u64; + let wire = frame.len().max(MIN_WIRE_FRAME); unsafe { - core::ptr::copy_nonoverlapping(frame.as_ptr(), va as *mut u8, frame.len()); + let dst = va as *mut u8; + core::ptr::copy_nonoverlapping(frame.as_ptr(), dst, frame.len()); + core::ptr::write_bytes(dst.add(frame.len()), 0, wire - frame.len()); } compiler_fence(Ordering::Release); let status_reg = REG_TXSTATUS0 + (idx as u16 * 4); - driver.pio.w32(status_reg, frame.len() as u32)?; - poll_done(driver, status_reg)?; - driver.tx_cur = (idx + 1) % TX_SLOT_COUNT; + driver.pio.w32(status_reg, wire as u32 | TSD_ERTXTH_256)?; + driver.tx_cur = driver.tx_cur.wrapping_add(1); Ok(()) } diff --git a/userland/rtl8139_proofs/Cargo.lock b/userland/rtl8139_proofs/Cargo.lock index 7d7b9a392..2d6571098 100644 --- a/userland/rtl8139_proofs/Cargo.lock +++ b/userland/rtl8139_proofs/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "rtl8139_proofs" -version = "0.1.0" +version = "0.3.0" diff --git a/userland/rtl8139_proofs/src/rtl_tests.rs b/userland/rtl8139_proofs/src/rtl_tests.rs index d3dfdc225..a41e72fa0 100644 --- a/userland/rtl8139_proofs/src/rtl_tests.rs +++ b/userland/rtl8139_proofs/src/rtl_tests.rs @@ -1,5 +1,5 @@ // NONOS Operating System (AGPL-3.0-or-later) -use crate::constants::dma::RX_BUF_DATA_BYTES; +use crate::constants::dma::{RX_BUF_BYTES, RX_BUF_DATA_BYTES}; use crate::constants::MAX_ETHERNET_FRAME; use crate::protocol::{decode_request, encode_response_header, Request, HDR_LEN}; use crate::ring::{copy, u16_at, u8_at}; @@ -9,25 +9,34 @@ use alloc::boxed::Box; use alloc::vec::Vec; // The device fills a 32 KiB byte ring with per-packet records: a status -// word, a raw length, then the frame, and the driver walks it by offset -// arithmetic that wraps at the data size. The primitives below are the only -// way the walk touches memory, so their property is the isolation property: -// every read lands inside the ring for every offset, the u16 assembly wraps -// correctly at the seam, and the wrapping copy fills exactly the caller's -// buffer and nothing beyond it. +// word, a raw length, then the frame. RCR.WRAP is set, so a record that +// crosses the end of the ring is written on past it into the slack the +// allocation carries (RX_BUF_BYTES), not back at the start, and the driver +// reads linearly. The primitives below are the only way the walk touches +// memory, so their property is the isolation property: no read leaves the +// allocation for any offset, the u16 assembly is little-endian across the +// seam, and the copy fills exactly the caller's buffer and nothing beyond it. fn pattern(i: usize) -> u8 { (i % 251) as u8 } -fn ring_buf() -> Box<[u8; RX_BUF_DATA_BYTES]> { - let mut b = Box::new([0u8; RX_BUF_DATA_BYTES]); +fn ring_buf() -> Box<[u8; RX_BUF_BYTES]> { + let mut b = Box::new([0u8; RX_BUF_BYTES]); for (i, byte) in b.iter_mut().enumerate() { *byte = pattern(i); } b } +fn expected(off: usize) -> u8 { + if off < RX_BUF_BYTES { + pattern(off) + } else { + 0 + } +} + fn xorshift(state: &mut u64) -> u64 { *state ^= *state << 13; *state ^= *state >> 7; @@ -36,7 +45,7 @@ fn xorshift(state: &mut u64) -> u64 { } #[test] -fn every_byte_read_lands_at_the_wrapped_offset() { +fn no_read_leaves_the_allocation() { let buf = ring_buf(); let base = buf.as_ptr() as u64; let edges = [ @@ -45,46 +54,48 @@ fn every_byte_read_lands_at_the_wrapped_offset() { RX_BUF_DATA_BYTES - 1, RX_BUF_DATA_BYTES, RX_BUF_DATA_BYTES + 1, + RX_BUF_BYTES - 1, + RX_BUF_BYTES, + RX_BUF_BYTES + 1, 7 * RX_BUF_DATA_BYTES + 13, usize::MAX / 2, usize::MAX - 1, ]; for &off in &edges { - assert_eq!(u8_at(base, off), pattern(off % RX_BUF_DATA_BYTES), "offset {off}"); + assert_eq!(u8_at(base, off), expected(off), "offset {off}"); } let mut s = 1u64; for _ in 0..200_000 { - let off = xorshift(&mut s) as usize; - assert_eq!(u8_at(base, off), pattern(off % RX_BUF_DATA_BYTES)); + let off = xorshift(&mut s) as usize % (2 * RX_BUF_BYTES); + assert_eq!(u8_at(base, off), expected(off), "offset {off}"); } } #[test] -fn u16_reads_assemble_little_endian_and_wrap_at_the_seam() { +fn u16_reads_assemble_little_endian_and_run_on_past_the_seam() { let buf = ring_buf(); let base = buf.as_ptr() as u64; assert_eq!(u16_at(base, 0), u16::from_le_bytes([pattern(0), pattern(1)])); - // The device can leave a header at the last ring byte; the high byte must - // come from the start of the ring, not from past its end. + // With WRAP set the byte after the ring's last one is in the slack, where + // the device wrote it, not at the start of the ring. let seam = RX_BUF_DATA_BYTES - 1; - assert_eq!(u16_at(base, seam), u16::from_le_bytes([pattern(seam), pattern(0)])); - assert_eq!( - u16_at(base, 3 * RX_BUF_DATA_BYTES - 1), - u16::from_le_bytes([pattern(seam), pattern(0)]) - ); + assert_eq!(u16_at(base, seam), u16::from_le_bytes([pattern(seam), pattern(seam + 1)])); + // The last byte of the allocation pairs with nothing past it. + let last = RX_BUF_BYTES - 1; + assert_eq!(u16_at(base, last), u16::from_le_bytes([pattern(last), 0])); } #[test] -fn the_wrapping_copy_fills_the_frame_from_the_wrapped_ring() { +fn a_frame_crossing_the_seam_is_read_from_the_slack() { let buf = ring_buf(); let base = buf.as_ptr() as u64; - // A frame that starts near the end of the ring and wraps through the seam. + // A frame that starts near the end of the ring and runs past it. let start = RX_BUF_DATA_BYTES - 5; let mut out = [0u8; 64]; let n = out.len(); copy(base, start, &mut out, n); for (i, b) in out.iter().enumerate() { - assert_eq!(*b, pattern((start + i) % RX_BUF_DATA_BYTES), "byte {i}"); + assert_eq!(*b, pattern(start + i), "byte {i}"); } } @@ -98,7 +109,7 @@ fn an_oversized_length_never_writes_past_the_caller_buffer() { let out_len = 32; copy(base, 7, &mut guarded[..out_len], usize::MAX / 2); for (i, b) in guarded.iter().enumerate().take(out_len) { - assert_eq!(*b, pattern((7 + i) % RX_BUF_DATA_BYTES), "byte {i}"); + assert_eq!(*b, pattern(7 + i), "byte {i}"); } for b in guarded.iter().skip(out_len) { assert_eq!(*b, 0xEE, "the copy must never write past the caller's slice"); @@ -118,7 +129,7 @@ fn the_frame_gate_constants_fit_the_ring_and_the_reply() { #[test] fn decode_never_panics_and_reads_fields_from_their_offsets() { - const MAGIC: u32 = 0x4E52_3839; // the wire tag from protocol/header.rs + const MAGIC: u32 = 0x4E4E_4554; // the NNET wire tag from protocol/header.rs for seed in 1..100_000u64 { let mut s = seed; let blen = (xorshift(&mut s) % 40) as usize; From 198772c3937d438cfa950b4a7873babf54e6c28d Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 06/10] rtl8169: answer the stack, come up at all, ring the ring that exists The driver never answered the stack. It tagged replies with its own magic (0x4E52_3639) and sent them to KERNEL_REPLY_ENDPOINT, an inbox nothing has read since the stack moved into capsules. net_core and net_l2 speak NNET (0x4E4E_4554) and wait for the answer to their own call, which is how virtio-net replies. Requests are now taken with mk_ipc_recv_from and every answer goes back to the capsule that asked, with mk_ipc_reply, under the NNET tag. The ops and payload layouts already matched. It could not have come up on any card in any case. The station address is drawn with CryptoRandom, which is gated on the Crypto capability, and neither the manifest nor the kernel's spawn spec granted it: the draw failed closed and bring-up stopped with "no entropy for station address". Both grant it now, as they already did for e1000 and rtl8821ce. Discovery skipped any card whose PCI Interrupt Line read 0xFF, which is what UEFI firmware commonly leaves there, so on those PCs a present RTL8111/8168 was reported absent. The line was only ever bound to be acked; the driver polls. It binds none now, sets IMR to 0 so an unserviced source cannot hold a shared INTx asserted for another device, and gives up the Irq capability. The TX doorbell wrote TPPoll bit 7, which polls the high-priority ring. Only the normal-priority ring (TNPDS) is ever set up; THPDS is never written. On a real card the part fetched a descriptor from whatever THPDS held after reset, the real descriptor kept OWN, the first send timed out and every later one saw "busy". No frame was ever transmitted. The doorbell is bit 6, NPQ, as in Linux r8169. Both rings could fall a slot behind the part for good. On an RX descriptor error the slot was re-armed but the cursor stayed, while the part had already moved on; low-rate traffic like ARP then sat unread until fifteen more frames wrapped the ring. On TX the cursor moved only after a clean completion, so a TER or a slow completion (link still negotiating) left it on a slot the part had left. The RX cursor now advances past a bad descriptor, and the TX cursor advances the moment OWN is handed over; the OWN check on the next slot is the full-ring test, answered with E_AGAIN. The station address went into IDR with byte writes. The part takes IDR as dwords; byte writes are dropped on silicon, the readback check failed, and bring-up stopped there. It is now two 32-bit writes, high dword first, each read back to post it, as rtl_rar_set does. RxConfig and TxConfig were written with the receiver and transmitter off. On the 8169 and 8168B-F those writes can be lost and the accept bits never take. TE|RE now go on first, then the two configs, still after the station address, so the part is never enabled under the factory one (the bring-up proof that watches for that still passes). Frames under 60 bytes were refused. ARP (42) and a bare TCP ACK (54) are shorter than that; they are now zero-padded to 60, which also covers the 8168 revisions that pad short frames wrong. No QEMU model exists for this chip. rtl8169_proofs: 8 passed; the bring-up proof now holds IMR at 0. --- src/hardware/rtl8169_capsule/spawn.rs | 5 +- userland/capsule_driver_rtl8169/Capsule.mk | 9 ++-- .../src/constants/frame.rs | 7 ++- .../src/constants/mod.rs | 2 +- .../src/constants/regs.rs | 6 ++- .../capsule_driver_rtl8169/src/discover.rs | 8 ++- .../capsule_driver_rtl8169/src/init/mac.rs | 14 ++++-- .../capsule_driver_rtl8169/src/init/run.rs | 20 ++++++-- .../src/init/rx_setup.rs | 6 +++ .../src/init/tx_setup.rs | 6 +++ .../src/protocol/endpoint.rs | 17 ------- .../src/protocol/header.rs | 5 +- .../src/protocol/mod.rs | 2 - .../capsule_driver_rtl8169/src/rx/recv_one.rs | 9 ++-- .../src/server/error.rs | 21 +++++--- .../src/server/handlers/health.rs | 4 +- .../src/server/handlers/link_status.rs | 15 ++---- .../src/server/handlers/mac_address.rs | 11 ++--- .../src/server/handlers/rx_packet.rs | 16 +++--- .../src/server/handlers/stats.rs | 10 ++-- .../src/server/handlers/tx_packet.rs | 18 ++++--- .../src/server/runner.rs | 23 ++++----- .../capsule_driver_rtl8169/src/setup/dma.rs | 16 ++---- .../src/setup/driver.rs | 4 +- .../capsule_driver_rtl8169/src/setup/irq.rs | 30 ------------ .../capsule_driver_rtl8169/src/setup/mod.rs | 1 - .../src/setup/rollback.rs | 7 +-- .../src/setup/sequence.rs | 6 +-- userland/capsule_driver_rtl8169/src/tx/mod.rs | 3 +- .../src/tx/poll_done.rs | 34 ------------- .../capsule_driver_rtl8169/src/tx/send.rs | 49 +++++++++---------- .../src/tests/bring_up_tests.rs | 4 +- userland/rtl8169_proofs/src/tests/memory.rs | 1 - 33 files changed, 165 insertions(+), 224 deletions(-) delete mode 100644 userland/capsule_driver_rtl8169/src/protocol/endpoint.rs delete mode 100644 userland/capsule_driver_rtl8169/src/setup/irq.rs delete mode 100644 userland/capsule_driver_rtl8169/src/tx/poll_done.rs diff --git a/src/hardware/rtl8169_capsule/spawn.rs b/src/hardware/rtl8169_capsule/spawn.rs index 6b0da059d..1107aaf47 100644 --- a/src/hardware/rtl8169_capsule/spawn.rs +++ b/src/hardware/rtl8169_capsule/spawn.rs @@ -50,10 +50,13 @@ pub fn spawn_driver_rtl8169_capsule() -> Result<(), SpawnError> { target_triple: TARGET_TRIPLE, requested_caps: Capability::IPC.bit() | Capability::Memory.bit() + // The station address is drawn rather than read out of the IDR, + // and CryptoRandom is gated on this capability. The draw fails + // closed, so without it the card never comes up. + | Capability::Crypto.bit() | Capability::Driver.bit() | Capability::DeviceEnum.bit() | Capability::Mmio.bit() - | Capability::Irq.bit() | Capability::Dma.bit(), debug_tag: b"[DRIVER-RTL8169] load_elf_executable error:", }; diff --git a/userland/capsule_driver_rtl8169/Capsule.mk b/userland/capsule_driver_rtl8169/Capsule.mk index a108f45f7..5b8cd895a 100644 --- a/userland/capsule_driver_rtl8169/Capsule.mk +++ b/userland/capsule_driver_rtl8169/Capsule.mk @@ -1,4 +1,4 @@ -# RTL8169 — Realtek 8168/8169 gigabit NIC. PCI MMIO + INTx + DMA. +# RTL8169 — Realtek 8168/8169 gigabit NIC. PCI MMIO + DMA, polled. # Raw Ethernet frames only; network policy belongs to the net-stack # capsule. Signing, certificate, and manifest rules come from the # shared hybrid-signature capsule macro. @@ -12,7 +12,10 @@ CAPSULE_FEATURE := nonos-capsule-driver-rtl8169 CAPSULE_NAMESPACE := systems.nonos.driver.rtl8169_0 CAPSULE_SERVICE_ENDPOINT := service:4214:driver.rtl8169_0 CAPSULE_REPLY_ENDPOINT := reply:4215:endpoint.4294967310 -# IPC|Memory|Driver|DeviceEnum|Mmio|Irq|Dma = 0xF8019 -CAPSULE_REQUIRED_CAPS := 0xF8019 +# IPC|Memory|Crypto|Driver|DeviceEnum|Mmio|Dma = 0xB8039 +# Crypto (0x20) is what the CryptoRandom syscall is gated on. The station address +# is drawn rather than read out of the IDR, and that draw fails closed, so +# without this the card never comes up. No Irq: the driver polls. +CAPSULE_REQUIRED_CAPS := 0xB8039 include nonos-mk/capsule.mk diff --git a/userland/capsule_driver_rtl8169/src/constants/frame.rs b/userland/capsule_driver_rtl8169/src/constants/frame.rs index f7eac730b..413cb0c28 100644 --- a/userland/capsule_driver_rtl8169/src/constants/frame.rs +++ b/userland/capsule_driver_rtl8169/src/constants/frame.rs @@ -18,5 +18,10 @@ const ETH_HEADER_LEN: usize = 14; const MTU: usize = 1500; pub const MAC_LEN: usize = 6; -pub const MIN_ETHERNET_FRAME: usize = 60; +/// A bare header is the shortest frame taken; ARP (42) and a bare TCP ACK (54) +/// are shorter than the wire minimum, and refusing them stranded IPv4. +pub const MIN_ETHERNET_FRAME: usize = ETH_HEADER_LEN; +/// `send` pads to this: several 8168 revisions do not pad short frames right +/// (Linux pads them in software for the same reason). +pub const MIN_WIRE_FRAME: usize = 60; pub const MAX_ETHERNET_FRAME: usize = MTU + ETH_HEADER_LEN; diff --git a/userland/capsule_driver_rtl8169/src/constants/mod.rs b/userland/capsule_driver_rtl8169/src/constants/mod.rs index 5c8fd24bc..ec42a691d 100644 --- a/userland/capsule_driver_rtl8169/src/constants/mod.rs +++ b/userland/capsule_driver_rtl8169/src/constants/mod.rs @@ -19,4 +19,4 @@ pub mod pci; pub mod queue; pub mod regs; -pub use frame::{MAC_LEN, MAX_ETHERNET_FRAME, MIN_ETHERNET_FRAME}; +pub use frame::{MAC_LEN, MAX_ETHERNET_FRAME, MIN_ETHERNET_FRAME, MIN_WIRE_FRAME}; diff --git a/userland/capsule_driver_rtl8169/src/constants/regs.rs b/userland/capsule_driver_rtl8169/src/constants/regs.rs index 12f0a4764..6a1351029 100644 --- a/userland/capsule_driver_rtl8169/src/constants/regs.rs +++ b/userland/capsule_driver_rtl8169/src/constants/regs.rs @@ -36,7 +36,11 @@ pub const REG_RXDESC_ADDR_HI: usize = 0xE8; pub const CMD_RESET: u8 = 0x10; pub const CMD_RX_ENABLE: u8 = 0x08; pub const CMD_TX_ENABLE: u8 = 0x04; -pub const TX_POLL_HPQ: u8 = 0x80; +/// TPPoll bit 6 polls the normal-priority ring, the one TNPDS points at and +/// the only one set up. Bit 7 (0x80) is the high-priority ring, whose base +/// (THPDS) is never written: ringing it sent the part to fetch from zero, so +/// no frame was ever sent. +pub const TX_POLL_NPQ: u8 = 0x40; pub const TX_CONFIG_IFG: u32 = 3 << 24; pub const TX_CONFIG_DMA: u32 = 7 << 8; diff --git a/userland/capsule_driver_rtl8169/src/discover.rs b/userland/capsule_driver_rtl8169/src/discover.rs index f38671263..51c5583bc 100644 --- a/userland/capsule_driver_rtl8169/src/discover.rs +++ b/userland/capsule_driver_rtl8169/src/discover.rs @@ -28,7 +28,6 @@ const MAX_DEVICES: usize = 32; #[derive(Debug, Clone, Copy)] pub struct Found { pub device_id: u64, - pub irq_line: u8, pub bar_index: u8, pub bar_size: u64, pub command_bits: u16, @@ -44,13 +43,12 @@ pub fn find_rtl8169() -> Option { if !is_supported(r) { continue; } - if r.irq_pin == 0 || r.irq_line == 0xFF { - continue; - } + // Interrupt routing is not asked for: the driver polls. UEFI firmware + // often leaves Interrupt Line at 0xFF, and filtering on it skipped a + // present card as absent on exactly the PCs this driver is for. if let Some((bar_index, bar_size)) = first_mmio_bar(r) { return Some(Found { device_id: r.device_id, - irq_line: r.irq_line, bar_index, bar_size, command_bits: command_bits(r), diff --git a/userland/capsule_driver_rtl8169/src/init/mac.rs b/userland/capsule_driver_rtl8169/src/init/mac.rs index 44bd950f4..e8b7b6e9f 100644 --- a/userland/capsule_driver_rtl8169/src/init/mac.rs +++ b/userland/capsule_driver_rtl8169/src/init/mac.rs @@ -32,12 +32,18 @@ pub fn program(regs: &Regs) -> Result<[u8; MAC_LEN], &'static str> { } apply(&mut mac); - // IDR writes are dropped while the config lock is set. + /* + * IDR writes are dropped while the config lock is set, and the part takes + * them only as whole dwords (reads may be any width): byte writes were + * dropped on silicon, the readback below failed, and the NIC never came up. + * High dword first, each read back to post it, as Linux rtl_rar_set does. + */ unsafe { regs.w8(REG_CFG9346, CFG9346_UNLOCK); - for (i, byte) in mac.iter().enumerate() { - regs.w8(REG_MAC0 + i, *byte); - } + regs.w32(REG_MAC0 + 4, mac[4] as u32 | (mac[5] as u32) << 8); + let _ = regs.r32(REG_MAC0 + 4); + regs.w32(REG_MAC0, u32::from_le_bytes([mac[0], mac[1], mac[2], mac[3]])); + let _ = regs.r32(REG_MAC0); regs.w8(REG_CFG9346, CFG9346_LOCK); } diff --git a/userland/capsule_driver_rtl8169/src/init/run.rs b/userland/capsule_driver_rtl8169/src/init/run.rs index f0dca6788..7b32d5447 100644 --- a/userland/capsule_driver_rtl8169/src/init/run.rs +++ b/userland/capsule_driver_rtl8169/src/init/run.rs @@ -15,21 +15,35 @@ // along with this program. If not, see . use crate::constants::regs::{ - CMD_RX_ENABLE, CMD_TX_ENABLE, ISR_ENABLED, REG_CMD, REG_IMR, REG_ISR, + CMD_RX_ENABLE, CMD_TX_ENABLE, REG_CMD, REG_IMR, REG_ISR, }; use crate::setup::Driver; use super::{mac, reset, rx_setup, tx_setup}; +/* + * TE|RE go on before RxConfig and TxConfig are written, as Linux rtl_hw_start + * does on every chip: on the 8169 and the 8168B-F an RxConfig written with + * the receiver off can be lost, the accept bits never take, and nothing is + * received. The station address is still programmed first, so the part is + * never enabled under the factory one. + */ pub fn bring_up(driver: &mut Driver) -> Result<(), &'static str> { reset::run(&driver.regs)?; driver.mac = mac::program(&driver.regs)?; rx_setup::program(&driver.regs, &driver.rx); tx_setup::program(&driver.regs, &driver.tx); unsafe { - driver.regs.w16(REG_ISR, 0xFFFF); - driver.regs.w16(REG_IMR, ISR_ENABLED); driver.regs.w8(REG_CMD, CMD_RX_ENABLE | CMD_TX_ENABLE); } + rx_setup::configure(&driver.regs); + tx_setup::configure(&driver.regs); + unsafe { + driver.regs.w16(REG_ISR, 0xFFFF); + // The driver polls and binds no line, so the part raises none: an + // unmasked source nobody services holds a shared INTx asserted for + // every other device on it. ISR still latches, which is all it reads. + driver.regs.w16(REG_IMR, 0); + } Ok(()) } diff --git a/userland/capsule_driver_rtl8169/src/init/rx_setup.rs b/userland/capsule_driver_rtl8169/src/init/rx_setup.rs index 2bfa44464..0b672fdd2 100644 --- a/userland/capsule_driver_rtl8169/src/init/rx_setup.rs +++ b/userland/capsule_driver_rtl8169/src/init/rx_setup.rs @@ -42,6 +42,12 @@ pub fn program(regs: &Regs, rx: &RxRing) { regs.w16(REG_RMS, BUFFER_SIZE as u16); regs.w32(REG_RXDESC_ADDR_LO, rx.desc_da as u32); regs.w32(REG_RXDESC_ADDR_HI, (rx.desc_da >> 32) as u32); + } +} + +/// RxConfig, written once the receiver is enabled (see `run`). +pub fn configure(regs: &Regs) { + unsafe { regs.w32( REG_RX_CONFIG, RX_CONFIG_ACCEPT_PHYS diff --git a/userland/capsule_driver_rtl8169/src/init/tx_setup.rs b/userland/capsule_driver_rtl8169/src/init/tx_setup.rs index 2db456862..a3d8c4921 100644 --- a/userland/capsule_driver_rtl8169/src/init/tx_setup.rs +++ b/userland/capsule_driver_rtl8169/src/init/tx_setup.rs @@ -35,6 +35,12 @@ pub fn program(regs: &Regs, tx: &TxRing) { unsafe { regs.w32(REG_TXDESC_ADDR_LO, tx.desc_da as u32); regs.w32(REG_TXDESC_ADDR_HI, (tx.desc_da >> 32) as u32); + } +} + +/// TxConfig, written once the transmitter is enabled (see `run`). +pub fn configure(regs: &Regs) { + unsafe { regs.w32(REG_TX_CONFIG, TX_CONFIG_IFG | TX_CONFIG_DMA); } } diff --git a/userland/capsule_driver_rtl8169/src/protocol/endpoint.rs b/userland/capsule_driver_rtl8169/src/protocol/endpoint.rs deleted file mode 100644 index d59bb1d80..000000000 --- a/userland/capsule_driver_rtl8169/src/protocol/endpoint.rs +++ /dev/null @@ -1,17 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -pub const KERNEL_REPLY_ENDPOINT: u64 = 0x1_0000_000E; diff --git a/userland/capsule_driver_rtl8169/src/protocol/header.rs b/userland/capsule_driver_rtl8169/src/protocol/header.rs index dda75274c..f610b5fa5 100644 --- a/userland/capsule_driver_rtl8169/src/protocol/header.rs +++ b/userland/capsule_driver_rtl8169/src/protocol/header.rs @@ -14,7 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pub const MAGIC: u32 = 0x4E52_3639; +/// "NNET", the NIC protocol net_core and net_l2 speak (virtio-net's too). +/// The per-driver tag this replaced made every request from the stack +/// undecodable, so the wired NICs never served it. +pub const MAGIC: u32 = 0x4E4E_4554; pub const VERSION: u16 = 1; pub const HDR_LEN: usize = 20; pub const RESP_HDR_LEN: usize = HDR_LEN; diff --git a/userland/capsule_driver_rtl8169/src/protocol/mod.rs b/userland/capsule_driver_rtl8169/src/protocol/mod.rs index 8f1131383..adce81f35 100644 --- a/userland/capsule_driver_rtl8169/src/protocol/mod.rs +++ b/userland/capsule_driver_rtl8169/src/protocol/mod.rs @@ -16,7 +16,6 @@ mod decode; mod encode; -mod endpoint; mod errno; mod header; mod limits; @@ -24,7 +23,6 @@ mod ops; pub use decode::decode_request; pub use encode::{encode_response_header, write_status}; -pub use endpoint::KERNEL_REPLY_ENDPOINT; pub use errno::{E_AGAIN, E_INVAL, E_IO, E_MSGSIZE}; pub use header::{Request, HDR_LEN, RESP_HDR_LEN}; pub use limits::{ diff --git a/userland/capsule_driver_rtl8169/src/rx/recv_one.rs b/userland/capsule_driver_rtl8169/src/rx/recv_one.rs index 8e28a1494..79de2a6df 100644 --- a/userland/capsule_driver_rtl8169/src/rx/recv_one.rs +++ b/userland/capsule_driver_rtl8169/src/rx/recv_one.rs @@ -16,8 +16,6 @@ use core::sync::atomic::{compiler_fence, Ordering}; -use nonos_libc::mk_irq_ack; - use super::rearm::rearm; use crate::constants::queue::{BUFFER_SIZE, RX_DESC_COUNT}; use crate::constants::regs::{ @@ -35,14 +33,12 @@ pub fn recv_one(driver: &mut Driver, out: &mut [u8]) -> Result, &' } } if (isr & ISR_RER) != 0 { - let _ = mk_irq_ack(driver.irq_grant); return Err("rtl8169 rx interrupt error"); } compiler_fence(Ordering::Acquire); let idx = driver.rx.cur; let d = unsafe { desc(driver.rx.desc_va, idx) }; if (d.opts1 & DESC_OWN) != 0 { - let _ = mk_irq_ack(driver.irq_grant); return Ok(None); } let len = (d.opts1 & DESC_LEN_MASK) as usize; @@ -52,8 +48,10 @@ pub fn recv_one(driver: &mut Driver, out: &mut [u8]) -> Result, &' || len - 4 > MAX_ETHERNET_FRAME || len - 4 > out.len() { + // The part has already moved past this slot; staying on it left the + // cursor one behind the part until the whole ring had filled again. rearm(driver, idx); - let _ = mk_irq_ack(driver.irq_grant); + driver.rx.cur = (idx + 1) % RX_DESC_COUNT; return Err("rtl8169 rx descriptor error"); } let frame_len = len - 4; @@ -66,6 +64,5 @@ pub fn recv_one(driver: &mut Driver, out: &mut [u8]) -> Result, &' } rearm(driver, idx); driver.rx.cur = (idx + 1) % RX_DESC_COUNT; - let _ = mk_irq_ack(driver.irq_grant); Ok(Some(frame_len)) } diff --git a/userland/capsule_driver_rtl8169/src/server/error.rs b/userland/capsule_driver_rtl8169/src/server/error.rs index 7eedd252d..6bebade1f 100644 --- a/userland/capsule_driver_rtl8169/src/server/error.rs +++ b/userland/capsule_driver_rtl8169/src/server/error.rs @@ -14,19 +14,24 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; +use nonos_libc::mk_ipc_reply; -use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, RESP_HDR_LEN, STATUS_LEN, -}; +use crate::protocol::{encode_response_header, write_status, Request, RESP_HDR_LEN, STATUS_LEN}; -pub fn reply_with_status(tx: &mut [u8], req: &Request, status: i32) { +/// Answer the capsule that sent the request. Replies used to go to a fixed +/// kernel-side inbox, which nothing reads now that the stack is a capsule, +/// so every call from net_core timed out. +pub fn reply(sender: u32, tx: &[u8], len: usize) { + let _ = mk_ipc_reply(sender, tx.as_ptr(), len); +} + +pub fn reply_with_status(sender: u32, tx: &mut [u8], req: &Request, status: i32) { encode_response_header(tx, req, STATUS_LEN as u32); write_status(&mut tx[RESP_HDR_LEN..], status); - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), RESP_HDR_LEN + STATUS_LEN); + reply(sender, tx, RESP_HDR_LEN + STATUS_LEN); } -pub fn reply_decode_failed(tx: &mut [u8], status: i32) { +pub fn reply_decode_failed(sender: u32, tx: &mut [u8], status: i32) { let req = Request { op: 0, flags: 0, request_id: 0, payload_len: 0 }; - reply_with_status(tx, &req, status); + reply_with_status(sender, tx, &req, status); } diff --git a/userland/capsule_driver_rtl8169/src/server/handlers/health.rs b/userland/capsule_driver_rtl8169/src/server/handlers/health.rs index 09630f8dc..4e484ff65 100644 --- a/userland/capsule_driver_rtl8169/src/server/handlers/health.rs +++ b/userland/capsule_driver_rtl8169/src/server/handlers/health.rs @@ -17,6 +17,6 @@ use crate::protocol::Request; use crate::server::error::reply_with_status; -pub fn handle(req: &Request, tx: &mut [u8]) { - reply_with_status(tx, req, 0); +pub fn handle(sender: u32, req: &Request, tx: &mut [u8]) { + reply_with_status(sender, tx, req, 0); } diff --git a/userland/capsule_driver_rtl8169/src/server/handlers/link_status.rs b/userland/capsule_driver_rtl8169/src/server/handlers/link_status.rs index 87bd1d720..db582110f 100644 --- a/userland/capsule_driver_rtl8169/src/server/handlers/link_status.rs +++ b/userland/capsule_driver_rtl8169/src/server/handlers/link_status.rs @@ -14,23 +14,18 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::constants::regs::{PHY_STATUS_LINK_UP, REG_PHY_STATUS}; use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, LINK_STATUS_PAYLOAD_LEN, - RESP_HDR_LEN, STATUS_LEN, + encode_response_header, write_status, Request, LINK_STATUS_PAYLOAD_LEN, RESP_HDR_LEN, + STATUS_LEN, }; +use crate::server::error::reply; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { let link = unsafe { driver.regs.r8(REG_PHY_STATUS) } & PHY_STATUS_LINK_UP; encode_response_header(tx, req, STATUS_LEN as u32 + LINK_STATUS_PAYLOAD_LEN as u32); write_status(&mut tx[RESP_HDR_LEN..], 0); tx[RESP_HDR_LEN + STATUS_LEN] = if link != 0 { 1 } else { 0 }; - let _ = mk_ipc_send( - KERNEL_REPLY_ENDPOINT, - tx.as_ptr(), - RESP_HDR_LEN + STATUS_LEN + LINK_STATUS_PAYLOAD_LEN, - ); + reply(sender, tx, RESP_HDR_LEN + STATUS_LEN + LINK_STATUS_PAYLOAD_LEN); } diff --git a/userland/capsule_driver_rtl8169/src/server/handlers/mac_address.rs b/userland/capsule_driver_rtl8169/src/server/handlers/mac_address.rs index d843d0262..b2db63874 100644 --- a/userland/capsule_driver_rtl8169/src/server/handlers/mac_address.rs +++ b/userland/capsule_driver_rtl8169/src/server/handlers/mac_address.rs @@ -14,18 +14,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, MAC_ADDRESS_PAYLOAD_LEN, - RESP_HDR_LEN, STATUS_LEN, + encode_response_header, write_status, Request, MAC_ADDRESS_PAYLOAD_LEN, RESP_HDR_LEN, + STATUS_LEN, }; +use crate::server::error::reply; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { encode_response_header(tx, req, STATUS_LEN as u32 + MAC_ADDRESS_PAYLOAD_LEN as u32); write_status(&mut tx[RESP_HDR_LEN..], 0); let off = RESP_HDR_LEN + STATUS_LEN; tx[off..off + MAC_ADDRESS_PAYLOAD_LEN].copy_from_slice(&driver.mac); - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), off + MAC_ADDRESS_PAYLOAD_LEN); + reply(sender, tx, off + MAC_ADDRESS_PAYLOAD_LEN); } diff --git a/userland/capsule_driver_rtl8169/src/server/handlers/rx_packet.rs b/userland/capsule_driver_rtl8169/src/server/handlers/rx_packet.rs index 383217236..c07d8c76a 100644 --- a/userland/capsule_driver_rtl8169/src/server/handlers/rx_packet.rs +++ b/userland/capsule_driver_rtl8169/src/server/handlers/rx_packet.rs @@ -14,26 +14,24 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::protocol::{ - encode_response_header, write_status, Request, E_AGAIN, E_IO, KERNEL_REPLY_ENDPOINT, - RESP_HDR_LEN, RX_PAYLOAD_PREFIX_LEN, STATUS_LEN, + encode_response_header, write_status, Request, E_AGAIN, E_IO, RESP_HDR_LEN, + RX_PAYLOAD_PREFIX_LEN, STATUS_LEN, }; use crate::rx::recv_one; -use crate::server::error::reply_with_status; +use crate::server::error::{reply, reply_with_status}; use crate::setup::Driver; -pub fn handle(driver: &mut Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &mut Driver, req: &Request, tx: &mut [u8]) { let body_off = RESP_HDR_LEN + STATUS_LEN + RX_PAYLOAD_PREFIX_LEN; let frame_len = match recv_one(driver, &mut tx[body_off..]) { Ok(Some(n)) => n, Ok(None) => { - reply_with_status(tx, req, E_AGAIN); + reply_with_status(sender, tx, req, E_AGAIN); return; } Err(_) => { - reply_with_status(tx, req, E_IO); + reply_with_status(sender, tx, req, E_IO); return; } }; @@ -41,5 +39,5 @@ pub fn handle(driver: &mut Driver, req: &Request, tx: &mut [u8]) { encode_response_header(tx, req, STATUS_LEN as u32 + body_len as u32); write_status(&mut tx[RESP_HDR_LEN..], 0); tx[RESP_HDR_LEN + STATUS_LEN..body_off].copy_from_slice(&(frame_len as u32).to_le_bytes()); - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), body_off + frame_len); + reply(sender, tx, body_off + frame_len); } diff --git a/userland/capsule_driver_rtl8169/src/server/handlers/stats.rs b/userland/capsule_driver_rtl8169/src/server/handlers/stats.rs index 12cee3924..be2a43838 100644 --- a/userland/capsule_driver_rtl8169/src/server/handlers/stats.rs +++ b/userland/capsule_driver_rtl8169/src/server/handlers/stats.rs @@ -14,19 +14,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::mk_ipc_send; - use crate::constants::queue::{RX_DESC_COUNT, TX_DESC_COUNT}; use crate::constants::regs::{ REG_CMD, REG_IMR, REG_ISR, REG_PHY_STATUS, REG_RMS, REG_RX_CONFIG, REG_TX_CONFIG, }; use crate::protocol::{ - encode_response_header, write_status, Request, KERNEL_REPLY_ENDPOINT, RESP_HDR_LEN, - STATS_PAYLOAD_LEN, STATUS_LEN, + encode_response_header, write_status, Request, RESP_HDR_LEN, STATS_PAYLOAD_LEN, STATUS_LEN, }; +use crate::server::error::reply; use crate::setup::Driver; -pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &Driver, req: &Request, tx: &mut [u8]) { let payload_len = STATUS_LEN as u32 + STATS_PAYLOAD_LEN as u32; encode_response_header(tx, req, payload_len); write_status(&mut tx[RESP_HDR_LEN..], 0); @@ -34,7 +32,7 @@ pub fn handle(driver: &Driver, req: &Request, tx: &mut [u8]) { for v in live_regs(driver) { put32(tx, &mut o, v); } - let _ = mk_ipc_send(KERNEL_REPLY_ENDPOINT, tx.as_ptr(), RESP_HDR_LEN + payload_len as usize); + reply(sender, tx, RESP_HDR_LEN + payload_len as usize); } fn live_regs(driver: &Driver) -> [u32; 12] { diff --git a/userland/capsule_driver_rtl8169/src/server/handlers/tx_packet.rs b/userland/capsule_driver_rtl8169/src/server/handlers/tx_packet.rs index 38185d284..8ddf898b8 100644 --- a/userland/capsule_driver_rtl8169/src/server/handlers/tx_packet.rs +++ b/userland/capsule_driver_rtl8169/src/server/handlers/tx_packet.rs @@ -15,25 +15,27 @@ // along with this program. If not, see . use crate::constants::{MAX_ETHERNET_FRAME, MIN_ETHERNET_FRAME}; -use crate::protocol::{Request, E_INVAL, E_IO, E_MSGSIZE, MAX_TX_PAYLOAD_BYTES}; +use crate::protocol::{Request, E_AGAIN, E_INVAL, E_MSGSIZE, MAX_TX_PAYLOAD_BYTES}; use crate::server::error::reply_with_status; use crate::setup::Driver; -use crate::tx::send; +use crate::tx::{busy, send}; -pub fn handle(driver: &mut Driver, req: &Request, body: &[u8], tx: &mut [u8]) { +pub fn handle(sender: u32, driver: &mut Driver, req: &Request, body: &[u8], tx: &mut [u8]) { if req.payload_len as usize != body.len() { - reply_with_status(tx, req, E_MSGSIZE); + reply_with_status(sender, tx, req, E_MSGSIZE); return; } if body.len() < MIN_ETHERNET_FRAME || body.len() > MAX_ETHERNET_FRAME || body.len() as u32 > MAX_TX_PAYLOAD_BYTES { - reply_with_status(tx, req, E_INVAL); + reply_with_status(sender, tx, req, E_INVAL); return; } - match send(driver, body) { - Ok(()) => reply_with_status(tx, req, 0), - Err(_) => reply_with_status(tx, req, E_IO), + if busy(driver) { + reply_with_status(sender, tx, req, E_AGAIN); + return; } + send(driver, body); + reply_with_status(sender, tx, req, 0); } diff --git a/userland/capsule_driver_rtl8169/src/server/runner.rs b/userland/capsule_driver_rtl8169/src/server/runner.rs index 81e23510f..bcbeccaf4 100644 --- a/userland/capsule_driver_rtl8169/src/server/runner.rs +++ b/userland/capsule_driver_rtl8169/src/server/runner.rs @@ -16,7 +16,7 @@ use alloc::vec; -use nonos_libc::mk_ipc_recv; +use nonos_libc::mk_ipc_recv_from; use crate::constants::MAX_ETHERNET_FRAME; use crate::protocol::{ @@ -37,27 +37,28 @@ pub fn run(driver: &mut Driver) -> ! { let mut rx = vec![0u8; rx_len]; let mut tx = vec![0u8; tx_len]; loop { - let n = mk_ipc_recv(SERVICE_INBOX, rx.as_mut_ptr(), rx_len, 0); - if n <= 0 { + let mut sender: u32 = 0; + let n = mk_ipc_recv_from(SERVICE_INBOX, rx.as_mut_ptr(), rx_len, 0, &mut sender); + if n <= 0 || sender == 0 { continue; } let len = n as usize; let req = match decode_request(&rx[..len]) { Some(r) => r, None => { - reply_decode_failed(&mut tx, E_INVAL); + reply_decode_failed(sender, &mut tx, E_INVAL); continue; } }; let body = &rx[HDR_LEN..len]; match req.op { - OP_HEALTHCHECK => handlers::health::handle(&req, &mut tx), - OP_LINK_STATUS => handlers::link_status::handle(driver, &req, &mut tx), - OP_MAC_ADDRESS => handlers::mac_address::handle(driver, &req, &mut tx), - OP_TX_PACKET => handlers::tx_packet::handle(driver, &req, body, &mut tx), - OP_RX_PACKET => handlers::rx_packet::handle(driver, &req, &mut tx), - OP_STATS => handlers::stats::handle(driver, &req, &mut tx), - _ => reply_with_status(&mut tx, &req, E_INVAL), + OP_HEALTHCHECK => handlers::health::handle(sender, &req, &mut tx), + OP_LINK_STATUS => handlers::link_status::handle(sender, driver, &req, &mut tx), + OP_MAC_ADDRESS => handlers::mac_address::handle(sender, driver, &req, &mut tx), + OP_TX_PACKET => handlers::tx_packet::handle(sender, driver, &req, body, &mut tx), + OP_RX_PACKET => handlers::rx_packet::handle(sender, driver, &req, &mut tx), + OP_STATS => handlers::stats::handle(sender, driver, &req, &mut tx), + _ => reply_with_status(sender, &mut tx, &req, E_INVAL), } } } diff --git a/userland/capsule_driver_rtl8169/src/setup/dma.rs b/userland/capsule_driver_rtl8169/src/setup/dma.rs index 854593a71..1f70190a9 100644 --- a/userland/capsule_driver_rtl8169/src/setup/dma.rs +++ b/userland/capsule_driver_rtl8169/src/setup/dma.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::{mk_dma_map, DmaMapOut, IrqBindOut, MmioMapOut}; +use nonos_libc::{mk_dma_map, DmaMapOut, MmioMapOut}; use crate::constants::queue::{RX_BUFFER_BYTES, RX_RING_BYTES, TX_BUFFER_BYTES, TX_RING_BYTES}; @@ -40,27 +40,21 @@ pub fn map_all( device_id: u64, epoch: u64, mmio: &MmioMapOut, - irq: &IrqBindOut, ) -> Result<(DmaMapOut, DmaMapOut, DmaMapOut, DmaMapOut), &'static str> { let rx_ring = alloc(device_id, epoch, RX_RING_BYTES as u64).ok_or_else(|| { - rollback::after(device_id, mmio, irq, &[]); + rollback::after(device_id, mmio, &[]); "rx ring dma failed" })?; let rx_buf = alloc(device_id, epoch, RX_BUFFER_BYTES as u64).ok_or_else(|| { - rollback::after(device_id, mmio, irq, &[rx_ring.grant_id]); + rollback::after(device_id, mmio, &[rx_ring.grant_id]); "rx buffer dma failed" })?; let tx_ring = alloc(device_id, epoch, TX_RING_BYTES as u64).ok_or_else(|| { - rollback::after(device_id, mmio, irq, &[rx_buf.grant_id, rx_ring.grant_id]); + rollback::after(device_id, mmio, &[rx_buf.grant_id, rx_ring.grant_id]); "tx ring dma failed" })?; let tx_buf = alloc(device_id, epoch, TX_BUFFER_BYTES as u64).ok_or_else(|| { - rollback::after( - device_id, - mmio, - irq, - &[tx_ring.grant_id, rx_buf.grant_id, rx_ring.grant_id], - ); + rollback::after(device_id, mmio, &[tx_ring.grant_id, rx_buf.grant_id, rx_ring.grant_id]); "tx buffer dma failed" })?; Ok((rx_ring, rx_buf, tx_ring, tx_buf)) diff --git a/userland/capsule_driver_rtl8169/src/setup/driver.rs b/userland/capsule_driver_rtl8169/src/setup/driver.rs index 984136899..61fd4455b 100644 --- a/userland/capsule_driver_rtl8169/src/setup/driver.rs +++ b/userland/capsule_driver_rtl8169/src/setup/driver.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::{mk_device_release, mk_dma_unmap, mk_irq_unbind, mk_mmio_unmap}; +use nonos_libc::{mk_device_release, mk_dma_unmap, mk_mmio_unmap}; use crate::constants::MAC_LEN; use crate::queue::{RxRing, TxRing}; @@ -23,7 +23,6 @@ use crate::regs::Regs; pub struct Driver { pub device_id: u64, pub mmio_grant: u64, - pub irq_grant: u64, pub rx_ring_grant: u64, pub rx_buffer_grant: u64, pub tx_ring_grant: u64, @@ -40,7 +39,6 @@ impl Driver { let _ = mk_dma_unmap(self.tx_ring_grant); let _ = mk_dma_unmap(self.rx_buffer_grant); let _ = mk_dma_unmap(self.rx_ring_grant); - let _ = mk_irq_unbind(self.irq_grant); let _ = mk_mmio_unmap(self.mmio_grant); let _ = mk_device_release(self.device_id); } diff --git a/userland/capsule_driver_rtl8169/src/setup/irq.rs b/userland/capsule_driver_rtl8169/src/setup/irq.rs deleted file mode 100644 index ed2806b29..000000000 --- a/userland/capsule_driver_rtl8169/src/setup/irq.rs +++ /dev/null @@ -1,30 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use nonos_libc::{mk_device_release, mk_irq_bind, mk_mmio_unmap, IrqBindOut, MmioMapOut}; - -use crate::discover::Found; - -pub fn bind(dev: Found, claim_epoch: u64, mmio: &MmioMapOut) -> Result { - let mut out = IrqBindOut { grant_id: 0, vector: 0 }; - let r = mk_irq_bind(dev.device_id, claim_epoch, dev.irq_line as u32, 0, 0, &mut out); - if r < 0 { - let _ = mk_mmio_unmap(mmio.grant_id); - let _ = mk_device_release(dev.device_id); - return Err("irq bind failed"); - } - Ok(out) -} diff --git a/userland/capsule_driver_rtl8169/src/setup/mod.rs b/userland/capsule_driver_rtl8169/src/setup/mod.rs index 6ef5e2e8a..876531e5d 100644 --- a/userland/capsule_driver_rtl8169/src/setup/mod.rs +++ b/userland/capsule_driver_rtl8169/src/setup/mod.rs @@ -17,7 +17,6 @@ mod claim; mod dma; mod driver; -mod irq; mod mmio; mod pci; mod rollback; diff --git a/userland/capsule_driver_rtl8169/src/setup/rollback.rs b/userland/capsule_driver_rtl8169/src/setup/rollback.rs index 146beeef3..28d50174d 100644 --- a/userland/capsule_driver_rtl8169/src/setup/rollback.rs +++ b/userland/capsule_driver_rtl8169/src/setup/rollback.rs @@ -14,15 +14,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use nonos_libc::{ - mk_device_release, mk_dma_unmap, mk_irq_unbind, mk_mmio_unmap, IrqBindOut, MmioMapOut, -}; +use nonos_libc::{mk_device_release, mk_dma_unmap, mk_mmio_unmap, MmioMapOut}; -pub fn after(device_id: u64, mmio: &MmioMapOut, irq: &IrqBindOut, dma_grants: &[u64]) { +pub fn after(device_id: u64, mmio: &MmioMapOut, dma_grants: &[u64]) { for grant in dma_grants { let _ = mk_dma_unmap(*grant); } - let _ = mk_irq_unbind(irq.grant_id); let _ = mk_mmio_unmap(mmio.grant_id); let _ = mk_device_release(device_id); } diff --git a/userland/capsule_driver_rtl8169/src/setup/sequence.rs b/userland/capsule_driver_rtl8169/src/setup/sequence.rs index c8e84b69c..6efef7fd5 100644 --- a/userland/capsule_driver_rtl8169/src/setup/sequence.rs +++ b/userland/capsule_driver_rtl8169/src/setup/sequence.rs @@ -20,19 +20,17 @@ use crate::queue::{RxRing, TxRing}; use crate::regs::Regs; use super::driver::Driver; -use super::{claim, dma, irq, mmio, pci}; +use super::{claim, dma, mmio, pci}; pub fn run() -> Result { let dev = find_rtl8169().ok_or("no rtl8169 device")?; let claim_epoch = claim::claim(dev.device_id)?; pci::enable_bus_master(dev, claim_epoch)?; let mmio = mmio::map(dev, claim_epoch)?; - let irq = irq::bind(dev, claim_epoch, &mmio)?; - let (rx_ring, rx_buf, tx_ring, tx_buf) = dma::map_all(dev.device_id, claim_epoch, &mmio, &irq)?; + let (rx_ring, rx_buf, tx_ring, tx_buf) = dma::map_all(dev.device_id, claim_epoch, &mmio)?; Ok(Driver { device_id: dev.device_id, mmio_grant: mmio.grant_id, - irq_grant: irq.grant_id, rx_ring_grant: rx_ring.grant_id, rx_buffer_grant: rx_buf.grant_id, tx_ring_grant: tx_ring.grant_id, diff --git a/userland/capsule_driver_rtl8169/src/tx/mod.rs b/userland/capsule_driver_rtl8169/src/tx/mod.rs index 9ba7f0c1d..66c892cd6 100644 --- a/userland/capsule_driver_rtl8169/src/tx/mod.rs +++ b/userland/capsule_driver_rtl8169/src/tx/mod.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -mod poll_done; mod send; -pub use send::send; +pub use send::{busy, send}; diff --git a/userland/capsule_driver_rtl8169/src/tx/poll_done.rs b/userland/capsule_driver_rtl8169/src/tx/poll_done.rs deleted file mode 100644 index 677072358..000000000 --- a/userland/capsule_driver_rtl8169/src/tx/poll_done.rs +++ /dev/null @@ -1,34 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use core::sync::atomic::{compiler_fence, Ordering}; - -use crate::constants::regs::DESC_OWN; -use crate::queue::desc::desc; -use crate::setup::Driver; - -const TX_POLL_BUDGET: u32 = 1_000_000; - -pub(super) fn poll_done(driver: &Driver, idx: usize) -> Result<(), &'static str> { - for _ in 0..TX_POLL_BUDGET { - compiler_fence(Ordering::Acquire); - if (unsafe { desc(driver.tx.desc_va, idx) }.opts1 & DESC_OWN) == 0 { - return Ok(()); - } - core::hint::spin_loop(); - } - Err("rtl8169 tx timeout") -} diff --git a/userland/capsule_driver_rtl8169/src/tx/send.rs b/userland/capsule_driver_rtl8169/src/tx/send.rs index faaf7d066..d7f8c15b2 100644 --- a/userland/capsule_driver_rtl8169/src/tx/send.rs +++ b/userland/capsule_driver_rtl8169/src/tx/send.rs @@ -16,49 +16,46 @@ use core::sync::atomic::{compiler_fence, Ordering}; -use super::poll_done::poll_done; use crate::constants::queue::TX_DESC_COUNT; -use crate::constants::regs::{ - DESC_EOR, DESC_FS, DESC_LS, DESC_OWN, ISR_ENABLED, ISR_TER, REG_ISR, REG_TX_POLL, TX_POLL_HPQ, -}; +use crate::constants::regs::{DESC_EOR, DESC_FS, DESC_LS, DESC_OWN, REG_TX_POLL, TX_POLL_NPQ}; +use crate::constants::MIN_WIRE_FRAME; use crate::queue::desc::{desc, desc_mut, Descriptor}; use crate::setup::Driver; -pub fn send(driver: &mut Driver, frame: &[u8]) -> Result<(), &'static str> { +/// Whether the part still owns the next slot, so a frame has nowhere to go. +pub fn busy(driver: &Driver) -> bool { + (unsafe { desc(driver.tx.desc_va, driver.tx.cur) }.opts1 & DESC_OWN) != 0 +} + +/* + * Hand one frame to the part and answer once it is queued. The cursor moves + * on the moment OWN goes over: the part walks the ring with a pointer of its + * own and moves past the slot when it finishes, so a cursor held back on a + * TX error or a slow completion (link still negotiating, PAUSE frames) was + * one slot behind the part for good, and every later send saw "busy". + * The caller checks `busy` first; OWN still set on the next slot is a full + * ring. + */ +pub fn send(driver: &mut Driver, frame: &[u8]) { let idx = driver.tx.cur; - if (unsafe { desc(driver.tx.desc_va, idx) }.opts1 & DESC_OWN) != 0 { - return Err("rtl8169 tx descriptor busy"); - } + let wire = frame.len().max(MIN_WIRE_FRAME); unsafe { - core::ptr::copy_nonoverlapping( - frame.as_ptr(), - driver.tx.buffer_va(idx) as *mut u8, - frame.len(), - ); + let dst = driver.tx.buffer_va(idx) as *mut u8; + core::ptr::copy_nonoverlapping(frame.as_ptr(), dst, frame.len()); + core::ptr::write_bytes(dst.add(frame.len()), 0, wire - frame.len()); } compiler_fence(Ordering::Release); let eor = if idx == TX_DESC_COUNT - 1 { DESC_EOR } else { 0 }; let addr = driver.tx.buffer_da(idx); let d = Descriptor { - opts1: DESC_OWN | DESC_FS | DESC_LS | eor | frame.len() as u32, + opts1: DESC_OWN | DESC_FS | DESC_LS | eor | wire as u32, opts2: 0, addr_lo: addr as u32, addr_hi: (addr >> 32) as u32, }; unsafe { desc_mut(driver.tx.desc_va, idx, d); - driver.regs.w8(REG_TX_POLL, TX_POLL_HPQ); - } - poll_done(driver, idx)?; - let isr = unsafe { driver.regs.r16(REG_ISR) }; - if isr != 0 { - unsafe { - driver.regs.w16(REG_ISR, isr & ISR_ENABLED); - } - } - if (isr & ISR_TER) != 0 { - return Err("rtl8169 tx interrupt error"); + driver.regs.w8(REG_TX_POLL, TX_POLL_NPQ); } driver.tx.cur = (idx + 1) % TX_DESC_COUNT; - Ok(()) } diff --git a/userland/rtl8169_proofs/src/tests/bring_up_tests.rs b/userland/rtl8169_proofs/src/tests/bring_up_tests.rs index 675ac6b80..e54717342 100644 --- a/userland/rtl8169_proofs/src/tests/bring_up_tests.rs +++ b/userland/rtl8169_proofs/src/tests/bring_up_tests.rs @@ -24,7 +24,7 @@ use nonos_libc::entropy; use super::memory::Memory; use super::model::{idr, live, resetting_part, window, FACTORY}; -use crate::constants::regs::{CMD_RX_ENABLE, ISR_ENABLED, REG_CMD, REG_IMR}; +use crate::constants::regs::{CMD_RX_ENABLE, REG_CMD, REG_IMR}; use crate::init::bring_up; /* @@ -56,5 +56,5 @@ fn the_part_is_never_enabled_while_it_still_carries_the_factory_address() { assert_eq!(d.mac, idr(&bar)); let cmd = bar.wrote8(REG_CMD); assert_eq!(cmd & 0x1C, 0x0C, "TE bit 2 and RE bit 3 on, RST bit 4 off, per the datasheet"); - assert_eq!(bar.wrote16(REG_IMR), ISR_ENABLED); + assert_eq!(bar.wrote16(REG_IMR), 0, "the driver polls: no chip interrupt is unmasked"); } diff --git a/userland/rtl8169_proofs/src/tests/memory.rs b/userland/rtl8169_proofs/src/tests/memory.rs index 4f354cf79..e407f2a04 100644 --- a/userland/rtl8169_proofs/src/tests/memory.rs +++ b/userland/rtl8169_proofs/src/tests/memory.rs @@ -52,7 +52,6 @@ impl Memory { Driver { device_id: 1, mmio_grant: 2, - irq_grant: 3, rx_ring_grant: 4, rx_buffer_grant: 5, tx_ring_grant: 6, From d616df0964c9f2dabe53344aa6b44078fdf3eb33 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 07/10] net_core: say which NIC the stack bound "bind: interface up" did not say which interface. With a wired port and a WiFi link both present the choice is the first thing to know, and on a boot with one NIC it is what ties the lease that follows to a driver. The line now names the candidate, for example "bind: interface up on driver.e1000_0". --- userland/capsule_net_core/src/setup.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/userland/capsule_net_core/src/setup.rs b/userland/capsule_net_core/src/setup.rs index c6a85b42e..e74ed69c0 100644 --- a/userland/capsule_net_core/src/setup.rs +++ b/userland/capsule_net_core/src/setup.rs @@ -69,7 +69,7 @@ pub fn bound_port() -> u32 { // position in the WiFi-then-wired order, so a change logs once, not per tick. static PROBE_SEEN: [AtomicU32; 8] = [const { AtomicU32::new(0) }; 8]; -fn discover_nic() -> Option { +fn discover_nic() -> Option<(u32, &'static str)> { let count = WIFI_NICS.len() + WIRED_NICS.len(); let start = PROBE_CURSOR.load(Ordering::Relaxed); let mut probes = 0usize; @@ -85,7 +85,7 @@ fn discover_nic() -> Option { match device::link_up(port) { Some(true) => { PROBE_CURSOR.store(idx, Ordering::Relaxed); - return Some(port); + return Some((port, name)); } verdict => { let code = if verdict.is_none() { 2 } else { 1 }; @@ -136,7 +136,7 @@ fn probe_log(name: &str, verdict: Option) { /// or the bound link drops. A no-op once bound to the best link, so it is cheap to /// call on a timer from the server loop. pub fn reevaluate() { - let Some(best) = discover_nic() else { + let Some((best, name)) = discover_nic() else { return; }; if best == BOUND_PORT.load(Ordering::Acquire) { @@ -155,9 +155,22 @@ pub fn reevaluate() { }; state::store(net_state); BOUND_PORT.store(best, Ordering::Release); - bind_log(b"[NET-CORE] bind: interface up"); + bind_up_log(name); } fn bind_log(msg: &[u8]) { let _ = nonos_libc::mk_debug(msg.as_ptr(), msg.len()); } + +// Which NIC the stack bound. With a wired port and a WiFi link both present +// the choice is the first thing to know, and "interface up" alone does not +// say it. +fn bind_up_log(name: &str) { + let mut line = [0u8; 96]; + let tag: &[u8] = b"[NET-CORE] bind: interface up on "; + let n = tag.len(); + line[..n].copy_from_slice(tag); + let m = name.len().min(line.len() - n); + line[n..n + m].copy_from_slice(&name.as_bytes()[..m]); + bind_log(&line[..n + m]); +} From 5afe5ed38af5e3d13227f9d87735ec6c117ec2d5 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 08/10] build: nonos-mk-ethernet-prod, the desktop with the wired NIC drivers No profile carried e1000, rtl8139 or rtl8169, so none of them had ever been booted with the stack. This target is the desktop profile plus those three, under the same attestation. QEMU models the e1000 and the RTL8139, so each can be booted as the only NIC and has to take the lease itself; the RTL8169 has no QEMU model and shows that a driver whose chip is absent exits and the boot goes on. --- mk/20-build.mk | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mk/20-build.mk b/mk/20-build.mk index 3fa720bde..beca8f181 100644 --- a/mk/20-build.mk +++ b/mk/20-build.mk @@ -3,7 +3,7 @@ # STARK attestation for the kernel and every capsule. This is where make, # make qemu, and make from-config all resolve their real work. -.PHONY: nonos-mk-check-driver-ahci-keys nonos-mk-check-driver-e1000-keys nonos-mk-check-driver-hda-keys nonos-mk-check-driver-i2c-hid-keys nonos-mk-check-driver-i2c-pci-keys nonos-mk-check-driver-iwlwifi-keys nonos-mk-check-driver-nvme-keys nonos-mk-check-driver-rtl8139-keys nonos-mk-check-driver-rtl8169-keys nonos-mk-check-driver-rtl8821ce-keys nonos-mk-check-driver-usb-msc-keys nonos-mk-check-driver-virtio-gpu-keys nonos-mk-check-ps2-input-keys nonos-mk-check-ramfs-keys nonos-mk-check-virtio-blk-keys nonos-mk-check-virtio-net-keys nonos-mk-check-virtio-rng-keys nonos-mk-check-xhci-keys nonos-mk-crypto nonos-mk-driver-ahci nonos-mk-driver-ahci-sign nonos-mk-driver-e1000 nonos-mk-driver-e1000-sign nonos-mk-driver-hda nonos-mk-driver-hda-sign nonos-mk-driver-i2c-hid nonos-mk-driver-i2c-hid-sign nonos-mk-driver-i2c-pci nonos-mk-driver-i2c-pci-sign nonos-mk-driver-iwlwifi nonos-mk-driver-iwlwifi-sign nonos-mk-driver-nvme nonos-mk-driver-nvme-sign nonos-mk-driver-rtl8139 nonos-mk-driver-rtl8139-sign nonos-mk-driver-rtl8169 nonos-mk-driver-rtl8169-sign nonos-mk-driver-rtl8821ce nonos-mk-driver-rtl8821ce-sign nonos-mk-driver-usb-msc nonos-mk-driver-usb-msc-sign nonos-mk-driver-virtio-gpu nonos-mk-driver-virtio-gpu-sign nonos-mk-entropy nonos-mk-keyring nonos-mk-market nonos-mk-proof-io nonos-mk-proof-io-sign nonos-mk-ps2-input nonos-mk-ps2-input-sign nonos-mk-ramfs nonos-mk-ramfs-sign nonos-mk-vfs nonos-mk-virtio-blk nonos-mk-virtio-blk-sign nonos-mk-virtio-net nonos-mk-virtio-net-sign nonos-mk-virtio-rng nonos-mk-virtio-rng-sign nonos-mk-wallpaper nonos-mk-xhci nonos-mk-xhci-sign nonos-mk-all-capsules-attested nonos-mk-attest nonos-mk-attestation nonos-mk-attestation-receipt nonos-mk-bootloader nonos-mk-capsules nonos-mk-check nonos-mk-check-trust-keys nonos-mk-check-trust-manifest nonos-mk-core nonos-mk-core-attested nonos-mk-desktop-gui-prod nonos-mk-smp-prod nonos-mk-ensure-zk-keys nonos-mk-esp nonos-mk-from-config nonos-mk-host-trust-verify nonos-mk-libc nonos-mk-live-production-proof nonos-mk-marketplace-abi nonos-mk-marketplace-index-tool nonos-mk-menuconfig nonos-mk-sign nonos-mk-terminal-test nonos-mk-trust-policy nonos-mk-usb-img nonos-mk-userland-clean nonos-mk-verify-capsule-attest nonos-mk-verify-trust nonos-mk-zerostate nonos-mk-zk-report nonos-mk-zk-tools nonos-mk-zk-verify-live +.PHONY: nonos-mk-check-driver-ahci-keys nonos-mk-check-driver-e1000-keys nonos-mk-check-driver-hda-keys nonos-mk-check-driver-i2c-hid-keys nonos-mk-check-driver-i2c-pci-keys nonos-mk-check-driver-iwlwifi-keys nonos-mk-check-driver-nvme-keys nonos-mk-check-driver-rtl8139-keys nonos-mk-check-driver-rtl8169-keys nonos-mk-check-driver-rtl8821ce-keys nonos-mk-check-driver-usb-msc-keys nonos-mk-check-driver-virtio-gpu-keys nonos-mk-check-ps2-input-keys nonos-mk-check-ramfs-keys nonos-mk-check-virtio-blk-keys nonos-mk-check-virtio-net-keys nonos-mk-check-virtio-rng-keys nonos-mk-check-xhci-keys nonos-mk-crypto nonos-mk-driver-ahci nonos-mk-driver-ahci-sign nonos-mk-driver-e1000 nonos-mk-driver-e1000-sign nonos-mk-driver-hda nonos-mk-driver-hda-sign nonos-mk-driver-i2c-hid nonos-mk-driver-i2c-hid-sign nonos-mk-driver-i2c-pci nonos-mk-driver-i2c-pci-sign nonos-mk-driver-iwlwifi nonos-mk-driver-iwlwifi-sign nonos-mk-driver-nvme nonos-mk-driver-nvme-sign nonos-mk-driver-rtl8139 nonos-mk-driver-rtl8139-sign nonos-mk-driver-rtl8169 nonos-mk-driver-rtl8169-sign nonos-mk-driver-rtl8821ce nonos-mk-driver-rtl8821ce-sign nonos-mk-driver-usb-msc nonos-mk-driver-usb-msc-sign nonos-mk-driver-virtio-gpu nonos-mk-driver-virtio-gpu-sign nonos-mk-entropy nonos-mk-keyring nonos-mk-market nonos-mk-proof-io nonos-mk-proof-io-sign nonos-mk-ps2-input nonos-mk-ps2-input-sign nonos-mk-ramfs nonos-mk-ramfs-sign nonos-mk-vfs nonos-mk-virtio-blk nonos-mk-virtio-blk-sign nonos-mk-virtio-net nonos-mk-virtio-net-sign nonos-mk-virtio-rng nonos-mk-virtio-rng-sign nonos-mk-wallpaper nonos-mk-xhci nonos-mk-xhci-sign nonos-mk-all-capsules-attested nonos-mk-attest nonos-mk-attestation nonos-mk-attestation-receipt nonos-mk-bootloader nonos-mk-capsules nonos-mk-check nonos-mk-check-trust-keys nonos-mk-check-trust-manifest nonos-mk-core nonos-mk-core-attested nonos-mk-desktop-gui-prod nonos-mk-smp-prod nonos-mk-ethernet-prod nonos-mk-ensure-zk-keys nonos-mk-esp nonos-mk-from-config nonos-mk-host-trust-verify nonos-mk-libc nonos-mk-live-production-proof nonos-mk-marketplace-abi nonos-mk-marketplace-index-tool nonos-mk-menuconfig nonos-mk-sign nonos-mk-terminal-test nonos-mk-trust-policy nonos-mk-usb-img nonos-mk-userland-clean nonos-mk-verify-capsule-attest nonos-mk-verify-trust nonos-mk-zerostate nonos-mk-zk-report nonos-mk-zk-tools nonos-mk-zk-verify-live # ZK attestation: transparent enrolled-secret tools @@ -1179,6 +1179,18 @@ nonos-mk-install-prod: $(DESKTOP_GUI_CAPSULE_ARTIFACTS) $(driver-nvme_ARTIFACTS) nonos-mk-check-deps nonos-mk-ensure-signing-key $(call nonos_kernel_build,microkernel-desktop-gui + nvme + install,microkernel-desktop-gui$(_boot_comma)nonos-stark-attest$(_boot_comma)nonos-capsule-driver-nvme) +# nonos-mk-ethernet-prod: the desktop profile with the wired NIC drivers in it. +# QEMU models the e1000 and the RTL8139, so each boots against its own device +# and has to take a lease through it; the RTL8169 has no QEMU model and is here +# to show a driver whose chip is absent exits and lets the boot go on. +ETHERNET_DRIVER_ARTIFACTS := $(driver-e1000_ARTIFACTS) $(driver-rtl8139_ARTIFACTS) \ + $(driver-rtl8169_ARTIFACTS) + +nonos-mk-ethernet-prod: $(DESKTOP_GUI_CAPSULE_ARTIFACTS) $(ETHERNET_DRIVER_ARTIFACTS) \ + nonos-mk-verify-desktop-gui-capsules \ + nonos-mk-check-deps nonos-mk-ensure-signing-key + $(call nonos_kernel_build,microkernel-desktop-gui + wired NICs,microkernel-desktop-gui$(_boot_comma)nonos-stark-attest$(_boot_comma)nonos-capsule-driver-e1000$(_boot_comma)nonos-capsule-driver-rtl8139$(_boot_comma)nonos-capsule-driver-rtl8169) + # nonos-mk-smp-prod: the desktop profile with the secondary CPUs turned on. # Same capsule set and the same attestation, so a difference between this boot # and the single-CPU one is the AP bring-up and nothing else. From bf57b3c5d0834b6bd4ef1be5193297d15e7af347 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 09/10] iwlwifi: the MAC clock bit, the RF-kill bit, and the firmware file layout Setup polled CSR_GP_CNTRL bit 1 for MAC clock ready. In the v1 CSR layout that every family probed here uses, bit 1 is undefined and the flag is bit 0, so the poll timed out on every card and the capsule exited before serving anything. RF-kill was read from INIT_DONE, the bit setup had just set itself, so the airplane-mode switch was never reported; it is HW_RF_KILL_SW, bit 27, set while the radio is allowed on. Bz-family parts (BE200) use the v2 layout and are still not handled. The legacy firmware parser read the version at offset 8, which is the start of the 64-byte name ("Core", "rele", "jenk" in the bundled images), so every image was refused as an unknown API. The header is 88 bytes: version at 72, build at 76, TLVs from 88. The section types were one off (20/21/33): 19 is the runtime image, 20 INIT, 32 paging, so the INIT and WoWLAN images were staged and the runtime one never was. The gen3 path already had 19. The API ceiling moves to 86 to take the newest bundled image. Checked against the six bundled .ucode files: the version at 72 is the API in each file name (29, 36, 46, 77, 84, 86) and the TLV walk from 88 ends exactly at the end of every file. The blob test written for this was never declared in the proofs crate; it is wired in now and runs against the real 7265D-29 image (four runtime sections, 364400 bytes). iwlwifi_proofs: 76 passed. This gets the capsule past its first register and its firmware file. It does not boot the firmware: the per-family load paths, the RX and TX queues and the post-ALIVE command sequence are still to be written. --- .../src/constants/mod.rs | 11 +++++++-- .../src/firmware/stage/stage_firmware.rs | 6 +++-- .../src/firmware/tlv.rs | 24 ++++++++++++++----- userland/capsule_driver_iwlwifi/src/init.rs | 9 ++++--- userland/iwlwifi_proofs/src/lib.rs | 4 ++++ 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/userland/capsule_driver_iwlwifi/src/constants/mod.rs b/userland/capsule_driver_iwlwifi/src/constants/mod.rs index 12c3ccb2e..ceb6063ce 100644 --- a/userland/capsule_driver_iwlwifi/src/constants/mod.rs +++ b/userland/capsule_driver_iwlwifi/src/constants/mod.rs @@ -53,10 +53,16 @@ pub const CSR_INT_MASK: usize = 0x00C; pub const CSR_FH_INT_STATUS: usize = 0x010; pub const CSR_GP_CNTRL: usize = 0x024; pub const CSR_HW_REV: usize = 0x028; -pub const GP_CNTRL_MAC_CLOCK_READY: u32 = 0x0000_0002; +// The CSR_GP_CNTRL layout of Linux iwl_csr_v1, which covers every family +// probed here up to AX210. Bit 1 is undefined there: polling it for the MAC +// clock timed out on every card, so setup never got past this register. +// Bz-family parts (BE200) use the v2 layout, which is not implemented. +pub const GP_CNTRL_MAC_CLOCK_READY: u32 = 0x0000_0001; pub const GP_CNTRL_INIT_DONE: u32 = 0x0000_0004; pub const GP_CNTRL_MAC_ACCESS_REQ: u32 = 0x0000_0008; pub const GP_CNTRL_XTAL_ON: u32 = 0x0000_0400; +/// Set while the hardware RF-kill switch lets the radio on. +pub const GP_CNTRL_HW_RF_KILL_SW: u32 = 0x0800_0000; pub const ALL_INTS_MASK: u32 = 0xFFFF_FFFF; pub const INT_MASK_DISABLED: u32 = 0; pub const INT_COALESCING_TIMEOUT: u32 = 64; @@ -66,7 +72,8 @@ pub const ALIVE_POLL_ITERS: usize = 2_000_000; pub const IWL_FW_MAGIC: u32 = 0x0A4C_5749; pub const FW_API_VERSION_MASK: u32 = 0xFFFF; pub const MIN_FW_API_VERSION: u16 = 22; -pub const MAX_FW_API_VERSION: u16 = 77; +/// The newest image bundled (so-a0-gf-a0-86); 77 refused it outright. +pub const MAX_FW_API_VERSION: u16 = 86; // Host-command / transmit-queue interface. Once the firmware is alive, the // driver hands it commands through a TFD ring per transmit queue. The diff --git a/userland/capsule_driver_iwlwifi/src/firmware/stage/stage_firmware.rs b/userland/capsule_driver_iwlwifi/src/firmware/stage/stage_firmware.rs index d08460684..8b1f0632a 100644 --- a/userland/capsule_driver_iwlwifi/src/firmware/stage/stage_firmware.rs +++ b/userland/capsule_driver_iwlwifi/src/firmware/stage/stage_firmware.rs @@ -17,7 +17,9 @@ use super::count_section::count_section; use super::stage_section::stage_section; use super::state::FirmwareStageState; -use crate::firmware::tlv::{le32, parse_header, TLV_PAGING, TLV_SEC_INIT, TLV_SEC_RT}; +use crate::firmware::tlv::{ + le32, parse_header, TLV_HEADER_LEN, TLV_PAGING, TLV_SEC_INIT, TLV_SEC_RT, +}; pub fn stage_firmware(data: &[u8], dma_user_va: u64, dma_len: u64) -> Option { let h = parse_header(data)?; @@ -28,7 +30,7 @@ pub fn stage_firmware(data: &[u8], dma_user_va: u64, dma_len: u64) -> Option Option
{ - if data.len() < 20 { + if data.len() < TLV_HEADER_LEN { return None; } let zero = le32(data, 0)?; @@ -23,7 +35,7 @@ pub fn parse_header(data: &[u8]) -> Option
{ if zero != 0 || magic != IWL_FW_MAGIC { return None; } - let ver = le32(data, 8)?; + let ver = le32(data, VER_OFF)?; let api = (ver & FW_API_VERSION_MASK) as u16; if !(MIN_FW_API_VERSION..=MAX_FW_API_VERSION).contains(&api) { return None; @@ -32,7 +44,7 @@ pub fn parse_header(data: &[u8]) -> Option
{ major: ((ver >> 24) & 0xFF) as u16, minor: ((ver >> 16) & 0xFF) as u16, api, - build: le32(data, 12)?, + build: le32(data, BUILD_OFF)?, }) } diff --git a/userland/capsule_driver_iwlwifi/src/init.rs b/userland/capsule_driver_iwlwifi/src/init.rs index be4b28a08..9629ef0af 100644 --- a/userland/capsule_driver_iwlwifi/src/init.rs +++ b/userland/capsule_driver_iwlwifi/src/init.rs @@ -8,8 +8,9 @@ use crate::constants::{ ALL_INTS_MASK, APM_POLL_ITERS, CSR_FH_INT_STATUS, CSR_GP_CNTRL, CSR_HW_REV, CSR_INT, - CSR_INT_COALESCING, CSR_INT_MASK, GP_CNTRL_INIT_DONE, GP_CNTRL_MAC_ACCESS_REQ, - GP_CNTRL_MAC_CLOCK_READY, GP_CNTRL_XTAL_ON, INT_COALESCING_TIMEOUT, INT_MASK_DISABLED, + CSR_INT_COALESCING, CSR_INT_MASK, GP_CNTRL_HW_RF_KILL_SW, GP_CNTRL_INIT_DONE, + GP_CNTRL_MAC_ACCESS_REQ, GP_CNTRL_MAC_CLOCK_READY, GP_CNTRL_XTAL_ON, INT_COALESCING_TIMEOUT, + INT_MASK_DISABLED, }; use crate::regs::Regs; @@ -34,6 +35,8 @@ pub fn bring_up(regs: Regs) -> Result { Ok(InitState { hw_rev: regs.read32(CSR_HW_REV), gp_cntrl, - rf_kill: gp_cntrl & GP_CNTRL_INIT_DONE == 0, + // INIT_DONE is the bit this function just set, so it said nothing + // about the airplane-mode switch; this is the bit that does. + rf_kill: gp_cntrl & GP_CNTRL_HW_RF_KILL_SW == 0, }) } diff --git a/userland/iwlwifi_proofs/src/lib.rs b/userland/iwlwifi_proofs/src/lib.rs index f0888c3f4..e6821ea56 100644 --- a/userland/iwlwifi_proofs/src/lib.rs +++ b/userland/iwlwifi_proofs/src/lib.rs @@ -22,6 +22,8 @@ pub mod constants; pub mod load; #[path = "../../capsule_driver_iwlwifi/src/regs.rs"] pub mod regs; +#[path = "../../capsule_driver_iwlwifi/src/firmware/tlv.rs"] +pub mod tlv; // The 802.11 frame layer: pure IEEE encoding, no hardware, so it is checked // exactly rather than modeled. `src/dot11/mod.rs` pulls in the real files. @@ -91,3 +93,5 @@ mod gen3_image_tests; mod gen3_tests; #[cfg(test)] mod prph_scratch_tests; +#[cfg(test)] +mod blob_scan_tests; From 180b1ec978809e67eb6280870ab0f4b8866a52fb Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 11:25:37 +0000 Subject: [PATCH 10/10] rtl8821ce: install the group key where the AP numbers it The GTK KDE in message 3 carries the key's index, and find_gtk threw it away. The driver then installed every group key in CAM slot 1. With the sec engine looking group-addressed frames up by the CCMP KeyID, that works until the AP rekeys: hostapd starts at 1 and swaps between 1 and 2 on every rekey, so after the first one every broadcast frame (ARP requests, DHCP, router adverts) came in under index 2, found an empty slot, and was dropped. The index now travels from the supplicant through the MLME and the join outcome to install_gtk and the session that removes it. TX data frames were also tagged SEC_TYPE_CCMP in the descriptor after the station had already encrypted them in software (header, CCMP header, MIC). rtw88 sets the security type only for frames with a hardware key; a frame tagged for hardware CCMP after software encryption risks being encrypted twice and failing the AP's MIC check. The tag is now zero. nonos_wifi_core_proofs gains a handshake test that drives the real supplicant with the KDE naming index 1, 2 and 3 (and with the Tx bit set) and holds it to the index sent: 14 passed. rtl8821ce_proofs does not build on main (sec.rs wants a crate::constants the proofs crate never declares, and its tests look for the firmware under the capsule directory rather than nonos-bootloader/firmware/realtek, where it is), so these edits are checked by the capsule build. --- .../capsule_driver_rtl8821ce/src/assoc.rs | 6 +- userland/capsule_driver_rtl8821ce/src/link.rs | 14 ++- .../src/serve/connect.rs | 11 ++- userland/nonos_wifi_core/src/mlme/state.rs | 5 + .../src/wpa/supplicant/state.rs | 10 ++ .../src/wpa/supplicant/step.rs | 17 ++-- userland/nonos_wifi_core_proofs/src/lib.rs | 2 + .../src/supplicant_tests.rs | 99 +++++++++++++++++++ 8 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 userland/nonos_wifi_core_proofs/src/supplicant_tests.rs diff --git a/userland/capsule_driver_rtl8821ce/src/assoc.rs b/userland/capsule_driver_rtl8821ce/src/assoc.rs index 31e023e83..054c493b3 100644 --- a/userland/capsule_driver_rtl8821ce/src/assoc.rs +++ b/userland/capsule_driver_rtl8821ce/src/assoc.rs @@ -58,7 +58,7 @@ pub trait Radio { /// How a join attempt ended. pub enum Outcome { /// Associated: the pairwise and group keys and the AP to install them for. - Joined { bssid: [u8; 6], channel: u8, ptk: [u8; 16], gtk: [u8; 16] }, + Joined { bssid: [u8; 6], channel: u8, ptk: [u8; 16], gtk: [u8; 16], gtk_id: u8 }, /// The association was refused or the handshake broke. Refused, /// The AP stopped responding before the join completed. @@ -351,7 +351,7 @@ fn report(mlme: &Mlme, outcome: Outcome, c: Counters) -> Report { // Pull the negotiated keys out of a connected machine. fn finish(mlme: &Mlme) -> Outcome { - let (Some(tk), Some(gtk)) = (mlme.tk(), mlme.gtk()) else { + let (Some(tk), Some(gtk), Some(gtk_id)) = (mlme.tk(), mlme.gtk(), mlme.gtk_id()) else { return Outcome::Refused; }; let mut ptk = [0u8; 16]; @@ -361,5 +361,5 @@ fn finish(mlme: &Mlme) -> Outcome { } ptk.copy_from_slice(&tk[..16]); group.copy_from_slice(>k[..16]); - Outcome::Joined { bssid: mlme.bssid(), channel: mlme.channel(), ptk, gtk: group } + Outcome::Joined { bssid: mlme.bssid(), channel: mlme.channel(), ptk, gtk: group, gtk_id } } diff --git a/userland/capsule_driver_rtl8821ce/src/link.rs b/userland/capsule_driver_rtl8821ce/src/link.rs index 15a67c0c3..5236e39b7 100644 --- a/userland/capsule_driver_rtl8821ce/src/link.rs +++ b/userland/capsule_driver_rtl8821ce/src/link.rs @@ -38,7 +38,7 @@ use crate::regs::Mmio; use crate::rx::ring::{RxState, RX_BUF_STRIDE, RX_DESC_COUNT}; use crate::rx::{poll_one, program as rx_program}; use crate::sec::{clear_cam, write_cam, Key, CAM_AES}; -use crate::tx::desc::{FrameMeta, DESC_RATE_6M, SEC_TYPE_CCMP}; +use crate::tx::desc::{FrameMeta, DESC_RATE_6M}; use crate::tx::regs::QSEL_BE; use crate::tx::ring::{TxState, TX_DESC_COUNT}; use crate::tx::{enqueue, program as tx_program}; @@ -308,13 +308,11 @@ impl LinkPort for RtlLink { // never registers one after association), so a rate-controlled data frame // is dropped before it reaches the air; a fixed rate transmits, exactly as // the fixed-rate handshake frames already do. - let meta = FrameMeta { - qsel: QSEL_BE, - bmc: false, - rate: Some(DESC_RATE_6M), - seq, - sec_type: SEC_TYPE_CCMP, - }; + // No security type: `tx_frame` has already encrypted the frame in + // software (header, CCMP header, MIC). A descriptor tagged CCMP asks + // the MAC to encrypt it again, as rtw88 does only for frames with a + // hardware key, and the AP then fails the MIC on every data frame. + let meta = FrameMeta { qsel: QSEL_BE, bmc: false, rate: Some(DESC_RATE_6M), seq, sec_type: 0 }; let ok = enqueue(&self.mmio, &self.tx_ring, &self.tx_buffers, &mut self.tx_state, &mpdu, &meta); if ok { diff --git a/userland/capsule_driver_rtl8821ce/src/serve/connect.rs b/userland/capsule_driver_rtl8821ce/src/serve/connect.rs index 31ecf50d1..b934a7f82 100644 --- a/userland/capsule_driver_rtl8821ce/src/serve/connect.rs +++ b/userland/capsule_driver_rtl8821ce/src/serve/connect.rs @@ -34,9 +34,10 @@ use crate::status; use super::radio::read_mac; use super::{SCAN_CHANNELS, SCAN_FRAME_MAX}; -/// The pairwise and group key slots a connection installs into the CAM. +/// The pairwise key's index. The group key goes in the slot its own index +/// names: with the engine looking group-addressed frames up by the CCMP +/// KeyID, a fixed slot 1 went dark after the AP's first rekey moved it to 2. const PAIRWISE_KEY_ID: u8 = 0; -const GROUP_KEY_ID: u8 = 1; /// Receive passes to spend joining before giving up. The driver's clock is not /// reliable in this capsule (all its other timeouts are poll counts), so the /// join is bounded in passes. Large enough to cover authentication, association @@ -142,11 +143,11 @@ pub(super) fn connect( report.state, ); let code = match report.outcome { - Outcome::Joined { bssid, channel, ptk, gtk } => { + Outcome::Joined { bssid, channel, ptk, gtk, gtk_id } => { set_rf(regs, channel, Bw::W20); if !keys.install_ptk(&ptk, PAIRWISE_KEY_ID, &bssid) { -3 - } else if !keys.install_gtk(>k, GROUP_KEY_ID) { + } else if !keys.install_gtk(>k, gtk_id) { -4 } else { // The keys are in the CAM, so turn the sec engine on for receive @@ -157,7 +158,7 @@ pub(super) fn connect( // but unencrypted and the access point dropped them. crate::sec::enable_sec_engine(regs); link.associate(bssid, ptk); - *session = Some(Session { bssid, key_id: PAIRWISE_KEY_ID, gtk_id: GROUP_KEY_ID }); + *session = Some(Session { bssid, key_id: PAIRWISE_KEY_ID, gtk_id }); status::debug(b"[rtl8821ce] connect: associated\n"); 0 } diff --git a/userland/nonos_wifi_core/src/mlme/state.rs b/userland/nonos_wifi_core/src/mlme/state.rs index 8913c3686..d0d36cd81 100644 --- a/userland/nonos_wifi_core/src/mlme/state.rs +++ b/userland/nonos_wifi_core/src/mlme/state.rs @@ -105,6 +105,11 @@ impl Mlme { self.supplicant.as_ref().filter(|_| self.state == MlmeState::Connected).map(|s| s.gtk()) } + /// The group key's index, valid once Connected. + pub fn gtk_id(&self) -> Option { + self.supplicant.as_ref().filter(|_| self.state == MlmeState::Connected).map(|s| s.gtk_id()) + } + /// The station MAC and the joined AP's BSSID, needed by the data path to /// address encrypted frames. Meaningful once a BSS has been selected. pub fn our_mac(&self) -> [u8; 6] { diff --git a/userland/nonos_wifi_core/src/wpa/supplicant/state.rs b/userland/nonos_wifi_core/src/wpa/supplicant/state.rs index a7f107bbe..b607444b2 100644 --- a/userland/nonos_wifi_core/src/wpa/supplicant/state.rs +++ b/userland/nonos_wifi_core/src/wpa/supplicant/state.rs @@ -49,6 +49,8 @@ pub struct Supplicant { /// Group temporal key, unwrapped from message 3. pub(super) gtk: [u8; 32], pub(super) gtk_len: usize, + /// The group key's index from its KDE. + pub(super) gtk_id: u8, } impl Supplicant { @@ -65,6 +67,7 @@ impl Supplicant { ptk: [0u8; 48], gtk: [0u8; 32], gtk_len: 0, + gtk_id: 0, } } @@ -82,6 +85,13 @@ impl Supplicant { &self.gtk[..self.gtk_len] } + /// The group key's index (1-3), valid once Connected. Group-addressed + /// frames name their key by it, and an AP moves it between 1 and 2 on + /// every rekey, so it has to be installed where the AP says. + pub fn gtk_id(&self) -> u8 { + self.gtk_id + } + // KCK: signs EAPOL MICs. KEK: unwraps the group key. Both are slices of the // PTK, named here so the handshake code reads like the standard. pub(super) fn kck(&self) -> &[u8] { diff --git a/userland/nonos_wifi_core/src/wpa/supplicant/step.rs b/userland/nonos_wifi_core/src/wpa/supplicant/step.rs index 6c0e71c18..ee7e67cea 100644 --- a/userland/nonos_wifi_core/src/wpa/supplicant/step.rs +++ b/userland/nonos_wifi_core/src/wpa/supplicant/step.rs @@ -117,14 +117,19 @@ impl Supplicant { let Some(len) = aes_unwrap(&self.kek(), wrapped, &mut plain) else { return false; }; - find_gtk(&plain[..len], &mut self.gtk).map(|n| self.gtk_len = n).is_some() + find_gtk(&plain[..len], &mut self.gtk) + .map(|(n, id)| { + self.gtk_len = n; + self.gtk_id = id; + }) + .is_some() } } -// Scan an unwrapped key-data buffer for the GTK KDE and copy the key out. -// KDE: 0xDD, length, 00 0F AC (RSN OUI), 0x01 (GTK), key-id byte, reserved, -// then the group key. -fn find_gtk(data: &[u8], out: &mut [u8; 32]) -> Option { +// Scan an unwrapped key-data buffer for the GTK KDE and copy the key out, +// returning its length and index. KDE: 0xDD, length, 00 0F AC (RSN OUI), +// 0x01 (GTK), key-id byte (index in bits 0-1), reserved, then the group key. +fn find_gtk(data: &[u8], out: &mut [u8; 32]) -> Option<(usize, u8)> { let mut i = 0usize; while i + 2 <= data.len() { let tag = data[i]; @@ -143,7 +148,7 @@ fn find_gtk(data: &[u8], out: &mut [u8; 32]) -> Option { return None; } out[..gtk.len()].copy_from_slice(gtk); - return Some(gtk.len()); + return Some((gtk.len(), data[i + 6] & 0x03)); } if tag == 0x00 { break; // padding diff --git a/userland/nonos_wifi_core_proofs/src/lib.rs b/userland/nonos_wifi_core_proofs/src/lib.rs index b26b7b6eb..51a0164db 100644 --- a/userland/nonos_wifi_core_proofs/src/lib.rs +++ b/userland/nonos_wifi_core_proofs/src/lib.rs @@ -21,3 +21,5 @@ mod netif_tests; #[cfg(test)] mod station_tests; +#[cfg(test)] +mod supplicant_tests; diff --git a/userland/nonos_wifi_core_proofs/src/supplicant_tests.rs b/userland/nonos_wifi_core_proofs/src/supplicant_tests.rs new file mode 100644 index 000000000..dbe6ce8ae --- /dev/null +++ b/userland/nonos_wifi_core_proofs/src/supplicant_tests.rs @@ -0,0 +1,99 @@ +// NONOS Operating System (AGPL-3.0-or-later) +//! The group key's index survives the four-way handshake. A simulated AP +//! drives the real `Supplicant` through messages 1 and 3 with the GTK KDE +//! naming index 1, then index 2, and the proof holds the supplicant (and the +//! MLME accessor the drivers read) to the index the AP sent. Group-addressed +//! frames name their key by this index and an AP moves it between 1 and 2 on +//! every rekey, so a driver that installs the group key anywhere else stops +//! decrypting broadcast (ARP, DHCP, router advertisements) after the first one. + +use nonos_wifi_core::ccmp::aes::Aes128; +use nonos_wifi_core::eapol::build::build_key_frame; +use nonos_wifi_core::eapol::parse::{KEY_INFO_MIC, KEY_INFO_PAIRWISE, KEY_INFO_VERSION2}; +use nonos_wifi_core::wpa::ptk::{pmk, ptk}; +use nonos_wifi_core::wpa::supplicant::{State, Supplicant}; + +const AA: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; +const SPA: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; +const ANONCE: [u8; 32] = [0xA1; 32]; +const SNONCE: [u8; 32] = [0x52; 32]; +const GTK: [u8; 16] = [ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, +]; + +// RFC 3394 AES key wrap: the AP's half of the group-key delivery. +fn aes_wrap(kek: &[u8; 16], plain: &[u8]) -> Vec { + assert!(plain.len().is_multiple_of(8) && !plain.is_empty()); + let n = plain.len() / 8; + let aes = Aes128::new(kek); + let mut a = [0xA6u8; 8]; + let mut r: Vec<[u8; 8]> = + (0..n).map(|i| plain[i * 8..i * 8 + 8].try_into().unwrap()).collect(); + for j in 0..6u64 { + for (i, ri) in r.iter_mut().enumerate() { + let mut block = [0u8; 16]; + block[..8].copy_from_slice(&a); + block[8..].copy_from_slice(&ri[..]); + aes.encrypt_block(&mut block); + let t = (n as u64) * j + (i as u64) + 1; + a.copy_from_slice(&block[..8]); + for (k, ak) in a.iter_mut().enumerate() { + *ak ^= (t >> (56 - 8 * k)) as u8; + } + ri.copy_from_slice(&block[8..]); + } + } + let mut out = a.to_vec(); + for block in &r { + out.extend_from_slice(block); + } + out +} + +// GTK KDE: dd 00 0f ac 01 , wrapped. +fn wrapped_gtk_kde(kek: &[u8; 16], key_id_byte: u8) -> Vec { + let mut kde = vec![0xDD, 22, 0x00, 0x0f, 0xac, 0x01, key_id_byte, 0x00]; + kde.extend_from_slice(>K); + aes_wrap(kek, &kde) +} + +fn message1() -> Vec { + let mut out = [0u8; 160]; + let info = KEY_INFO_VERSION2 | KEY_INFO_PAIRWISE; + let n = build_key_frame(&mut out, info, &[0, 0, 0, 0, 0, 0, 0, 1], &ANONCE, &[], &[0u8; 16]) + .unwrap(); + out[..n].to_vec() +} + +fn message3(kck: &[u8], kek: &[u8; 16], key_id_byte: u8) -> Vec { + let mut out = [0u8; 160]; + let info = KEY_INFO_VERSION2 | KEY_INFO_PAIRWISE | KEY_INFO_MIC; + let kd = wrapped_gtk_kde(kek, key_id_byte); + let n = build_key_frame(&mut out, info, &[0, 0, 0, 0, 0, 0, 0, 2], &ANONCE, &kd, kck).unwrap(); + out[..n].to_vec() +} + +fn handshake(key_id_byte: u8) -> Supplicant { + let key = pmk(b"ThisIsAPassword", b"ThisIsASSID"); + let p = ptk(&key, &AA, &SPA, &ANONCE, &SNONCE); + let mut sup = Supplicant::new(key, AA, SPA, SNONCE); + sup.step(&message1()); + assert_eq!(sup.state(), State::PtkDerived); + sup.step(&message3(&p[0..16], p[16..32].try_into().unwrap(), key_id_byte)); + assert_eq!(sup.state(), State::Connected); + assert_eq!(sup.gtk(), >K); + sup +} + +#[test] +fn the_group_key_keeps_the_index_the_ap_sent() { + assert_eq!(handshake(0x01).gtk_id(), 1); + assert_eq!(handshake(0x02).gtk_id(), 2, "the index after an AP's first rekey"); + assert_eq!(handshake(0x03).gtk_id(), 3); +} + +#[test] +fn only_the_index_bits_are_taken() { + // Bit 2 of the byte is the Tx flag, not part of the index. + assert_eq!(handshake(0x04 | 0x02).gtk_id(), 2); +}