//! K424: modern virtio-net RX/TX queues have disjoint owned DMA pages. //! Both rings remain empty; transmitting or receiving a packet is not claimed. use super::*; #[cfg(feature = "virtio-net-tx-completion-test")] use sha2::{Digest, Sha256}; #[cfg(feature = "virtio-net-size-test")] use turkos_kernel::net_frame; #[cfg(feature = "virtio-net-tx-completion-test")] use turkos_kernel::program_hash; use turkos_kernel::{net_mac, virtio_mmio, virtio_ring}; #[cfg(feature = "virtio-net-tx-completion-test")] fn counter() -> u64 { let value: u64; unsafe { asm!("mrs {value}, CNTPCT_EL0", value = out(reg) value, options(nomem, nostack, preserves_flags)) }; value } unsafe fn read(base: u64, offset: u64) -> u32 { unsafe { read_volatile((base + offset) as *const u32) } } unsafe fn write(base: u64, offset: u64, value: u32) { unsafe { write_volatile((base + offset) as *mut u32, value) } } unsafe fn write64(base: u64, offset: u64, address: u64) { unsafe { write(base, offset, address as u32); write(base, offset + 4, (address >> 32) as u32); } } fn fail(reason: &str) -> ! { write_str("TURKOS/NET-TX-ERROR: "); write_line(reason); park() } #[cfg(feature = "virtio-net-tx-completion-test")] pub fn completion_smoke_test(boot: &BootData, allocator: &mut memory::FrameAllocator) -> ! { let mut net = None; for node in boot.virtio_mmio.as_slice() { let magic = unsafe { read(node.base, 0) }; let version = unsafe { read(node.base, 4) }; let id = unsafe { read(node.base, 8) }; if virtio_mmio::validate_magic(magic).is_err() { fail("TX magic"); } if version != 2 { fail("TX modern version required"); } if virtio_mmio::require_net_device(id).is_ok() && net.replace(node.base).is_some() { fail("TX ambiguous net"); } } let base = net.unwrap_or_else(|| fail("TX missing net")); let free_before = allocator.remaining_pages(); let mut rx = virtio_ring::DmaLease::allocate(allocator, boot.hardware.ram_base, boot.hardware.ram_size) .unwrap_or_else(|_| fail("TX RX DMA allocation")); let mut tx = virtio_ring::DmaLease::allocate(allocator, boot.hardware.ram_base, boot.hardware.ram_size) .unwrap_or_else(|_| fail("TX DMA allocation")); let rx_frames = rx.frames().unwrap_or_else(|_| fail("TX RX DMA frames")); let tx_frames = tx.frames().unwrap_or_else(|_| fail("TX DMA frames")); for (index, frame) in rx_frames.iter().chain(tx_frames.iter()).enumerate() { if rx_frames .iter() .chain(tx_frames.iter()) .take(index) .any(|other| other == frame) { fail("TX RX/TX DMA alias"); } } for [desc, avail, used, _] in [rx_frames, tx_frames] { virtio_ring::split_ring_pages( desc, avail, used, 8, boot.hardware.ram_base, boot.hardware.ram_size, ) .unwrap_or_else(|_| fail("TX ring geometry")); } let [_, _, _, rx_buffer] = rx_frames; let [tx_desc, tx_avail, tx_used, tx_buffer] = tx_frames; let mut frame = [0u8; 60]; frame[..14].copy_from_slice(&[ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x52, 0x54, 0x00, 0x12, 0x34, 0x56, 0x08, 0x00, ]); for (index, byte) in frame[14..].iter_mut().enumerate() { *byte = 0xc0 + index as u8; } // Header [0..12] stays zero: no offload, no GSO, num_buffers=0. // SAFETY: both leases own disjoint identity-mapped Normal-RAM frames; // all bytes are written before QueueReady and the available index. unsafe { for page in rx_frames.iter().chain(tx_frames.iter()) { core::ptr::write_bytes(*page as *mut u8, 0, memory::PAGE_SIZE as usize); } write_volatile( tx_desc as *mut virtio_ring::Descriptor, virtio_ring::Descriptor { addr_le: tx_buffer.to_le(), len_le: 72u32.to_le(), flags_le: 0, next_le: 0, }, ); write_volatile((tx_avail + 4) as *mut u16, 0u16.to_le()); for (index, byte) in frame.iter().enumerate() { write_volatile((tx_buffer + 12 + index as u64) as *mut u8, *byte); } write_volatile((tx_buffer + 72) as *mut u8, 0xa5); } if free_before.checked_sub(allocator.remaining_pages()) != Some(8) { fail("TX DMA allocation accounting"); } // SAFETY: K322 validates the DTB MMIO page window and maps it Device. let queue_max = unsafe { write(base, 0x70, 0); if read(base, 0x70) != 0 { fail("TX initial reset"); } write(base, 0x70, 3); write(base, 0x14, 0); let low = read(base, 0x10); write(base, 0x14, 1); let high = read(base, 0x10); if low & net_mac::VIRTIO_NET_F_MAC == 0 || high & virtio_mmio::MODERN_F_VERSION_1 == 0 { fail("TX MAC/VERSION_1 feature missing"); } write(base, 0x24, 0); write(base, 0x20, net_mac::VIRTIO_NET_F_MAC); write(base, 0x24, 1); write(base, 0x20, virtio_mmio::MODERN_F_VERSION_1); write(base, 0x70, 0x0b); if read(base, 0x70) != 0x0b { fail("TX FEATURES_OK rejected"); } let mut tx_max = 0; for (index, [desc, avail, used, _]) in [rx_frames, tx_frames].into_iter().enumerate() { write(base, 0x30, index as u32); let max = read(base, 0x34); if max < 8 || read(base, 0x44) != 0 { fail("TX queue capacity/stale ready"); } if index == 1 { tx_max = max; } write(base, 0x38, 8); write64(base, 0x80, desc); write64(base, 0x90, avail); write64(base, 0xa0, used); asm!("dmb oshst", options(nostack, preserves_flags)); write(base, 0x44, 1); if read(base, 0x44) != 1 { fail("TX QueueReady readback"); } } rx.mark_ready().unwrap_or_else(|_| fail("TX RX DMA ready")); tx.mark_ready().unwrap_or_else(|_| fail("TX DMA ready")); write(base, 0x70, 0x0f); if read(base, 0x70) != 0x0f { fail("TX DRIVER_OK rejected"); } tx_max }; if rx.release(allocator) != Err(virtio_ring::Error::Active) || tx.release(allocator) != Err(virtio_ring::Error::Active) { fail("TX early DMA release before submit"); } let digest: [u8; 32] = Sha256::digest(frame).into(); let digest_hex = program_hash::hex_lower(&digest); write_str("TURKOS/NET-TX-COMPLETE: QUEUE=0x"); write_hex(1); write_str(" SIZE=0x"); write_hex(8); write_str(" MAX=0x"); write_hex(queue_max as usize); write_str(" DMA=0x"); write_hex(8); write_str(" FRAME=0x"); write_hex(frame.len()); write_str(" SHA256="); write_str(core::str::from_utf8(&digest_hex).unwrap_or_else(|_| fail("TX digest hex"))); write_str(" EARLY-RELEASE=BLOCKED\r\n"); unsafe { write_volatile((tx_avail + 2) as *mut u16, 1u16.to_le()); asm!("dmb oshst", options(nostack, preserves_flags)); write(base, 0x50, 1); } write_line("TURKOS/NET-TX-COMPLETE: WAITING-USED"); let frequency: u64; unsafe { asm!("mrs {value}, CNTFRQ_EL0", value = out(reg) frequency, options(nomem, nostack, preserves_flags)) }; let budget = frequency .checked_mul(3) .unwrap_or_else(|| fail("TX deadline overflow")); let deadline = virtio_ring::deadline_after(counter(), budget).unwrap_or_else(|_| fail("TX deadline")); let used_index = loop { let index = unsafe { u16::from_le(read_volatile((tx_used + 2) as *const u16)) }; if index != 0 { break index; } if virtio_ring::deadline_expired(counter(), deadline) { fail("TX completion timeout"); } core::hint::spin_loop(); }; let (used_id, used_len) = unsafe { asm!("dmb oshld", options(nostack, preserves_flags)); ( u32::from_le(read_volatile((tx_used + 4) as *const u32)), u32::from_le(read_volatile((tx_used + 8) as *const u32)), ) }; if used_index != 1 || used_id != 0 || used_len != 0 { fail("TX used index/id/length"); } if rx.release(allocator) != Err(virtio_ring::Error::Active) || tx.release(allocator) != Err(virtio_ring::Error::Active) { fail("TX early DMA release after used"); } for (index, byte) in frame.iter().enumerate() { if unsafe { read_volatile((tx_buffer + 12 + index as u64) as *const u8) } != *byte { fail("TX buffer changed after used"); } } write_str("TURKOS/NET-TX-COMPLETE: USED=0x"); write_hex(used_index as usize); write_str(" ID=0x"); write_hex(used_id as usize); write_str(" LEN=0x"); write_hex(used_len as usize); write_str(" ACTIVE-RELEASE=BLOCKED\r\n"); unsafe { write(base, 0x70, 0); asm!("dsb sy", options(nostack, preserves_flags)); if read(base, 0x70) != 0 { fail("TX reset status"); } for index in 0..2 { write(base, 0x30, index); if read(base, 0x44) != 0 { fail("TX reset QueueReady"); } } } rx.mark_reset().unwrap_or_else(|_| fail("TX RX DMA reset")); tx.mark_reset().unwrap_or_else(|_| fail("TX DMA reset")); if unsafe { read_volatile((tx_buffer + 72) as *const u8) } != 0xa5 || unsafe { read_volatile((rx_buffer + 2048) as *const u8) } != 0 { fail("TX DMA guard"); } if rx.release(allocator).is_err() || tx.release(allocator).is_err() || allocator.remaining_pages() != free_before { fail("TX DMA return accounting"); } write_str("TURKOS/NET-TX-COMPLETE: RESET=0x"); write_hex(0); write_str(" RELEASED=0x"); write_hex(8); write_str(" FREE-BEFORE=0x"); write_hex(free_before as usize); write_str(" FREE-AFTER=0x"); write_hex(allocator.remaining_pages() as usize); write_str(" GUARD=0x"); write_hex(0xa5); write_str("\r\n"); write_line("TURKOS/NET-TX-COMPLETE: PASS PACKETS=1 RESET=1 DMA-RELEASED=8"); park() } #[cfg(not(feature = "virtio-net-tx-completion-test"))] pub fn smoke_test(boot: &BootData, allocator: &mut memory::FrameAllocator) -> ! { let mut net = None; for node in boot.virtio_mmio.as_slice() { let magic = unsafe { read(node.base, 0) }; let version = unsafe { read(node.base, 4) }; let id = unsafe { read(node.base, 8) }; if virtio_mmio::validate_magic(magic).is_err() { fail("magic"); } if version != 2 { fail("modern version required"); } if virtio_mmio::require_net_device(id).is_ok() && net.replace(node.base).is_some() { fail("ambiguous net"); } } let base = net.unwrap_or_else(|| fail("missing net")); let free_before = allocator.remaining_pages(); let mut rx = virtio_ring::DmaLease::allocate(allocator, boot.hardware.ram_base, boot.hardware.ram_size) .unwrap_or_else(|_| fail("RX DMA allocation")); let mut tx = virtio_ring::DmaLease::allocate(allocator, boot.hardware.ram_base, boot.hardware.ram_size) .unwrap_or_else(|_| fail("TX DMA allocation")); let rx_frames = rx.frames().unwrap_or_else(|_| fail("RX DMA frames")); let tx_frames = tx.frames().unwrap_or_else(|_| fail("TX DMA frames")); for (index, frame) in rx_frames.iter().chain(tx_frames.iter()).enumerate() { if rx_frames .iter() .chain(tx_frames.iter()) .take(index) .any(|other| other == frame) { fail("RX/TX frame alias"); } } let [rx_desc, _, _, rx_buffer] = rx_frames; let [tx_desc, _, _, tx_buffer] = tx_frames; for [desc, avail, used, _] in [rx_frames, tx_frames] { virtio_ring::split_ring_pages( desc, avail, used, 8, boot.hardware.ram_base, boot.hardware.ram_size, ) .unwrap_or_else(|_| fail("split ring geometry")); } if free_before.checked_sub(allocator.remaining_pages()) != Some(8) { fail("DMA accounting after allocation"); } // SAFETY: both leases own eight disjoint, identity-mapped Normal-RAM // frames. No queue is exposed to the device until all bytes are ready. unsafe { for frame in rx_frames.iter().chain(tx_frames.iter()) { core::ptr::write_bytes(*frame as *mut u8, 0, memory::PAGE_SIZE as usize); } write_volatile( rx_desc as *mut virtio_ring::Descriptor, virtio_ring::Descriptor { addr_le: rx_buffer.to_le(), len_le: 2048u32.to_le(), flags_le: virtio_ring::DESC_F_WRITE.to_le(), next_le: 0, }, ); write_volatile( tx_desc as *mut virtio_ring::Descriptor, virtio_ring::Descriptor { addr_le: tx_buffer.to_le(), len_le: 64u32.to_le(), flags_le: 0, next_le: 0, }, ); write_volatile((rx_buffer + 2048) as *mut u8, 0xa5); write_volatile((tx_buffer + 64) as *mut u8, 0x5a); let actual_rx = read_volatile(rx_desc as *const virtio_ring::Descriptor); let actual_tx = read_volatile(tx_desc as *const virtio_ring::Descriptor); if u64::from_le(actual_rx.addr_le) != rx_buffer || u32::from_le(actual_rx.len_le) != 2048 || u16::from_le(actual_rx.flags_le) != virtio_ring::DESC_F_WRITE || u64::from_le(actual_tx.addr_le) != tx_buffer || u32::from_le(actual_tx.len_le) != 64 || u16::from_le(actual_tx.flags_le) != 0 { fail("RX/TX descriptor direction"); } } // SAFETY: the DTB-bounded MMIO slots were mapped Device by K322. // Only MAC and VERSION_1 are accepted; no offload or mergeable RX. let (rx_max, tx_max) = unsafe { write(base, 0x70, 0); if read(base, 0x70) != 0 { fail("initial reset"); } write(base, 0x70, 3); write(base, 0x14, 0); let low = read(base, 0x10); write(base, 0x14, 1); let high = read(base, 0x10); if low & net_mac::VIRTIO_NET_F_MAC == 0 || high & virtio_mmio::MODERN_F_VERSION_1 == 0 { fail("MAC or VERSION_1 absent"); } write(base, 0x24, 0); write(base, 0x20, net_mac::VIRTIO_NET_F_MAC); write(base, 0x24, 1); write(base, 0x20, virtio_mmio::MODERN_F_VERSION_1); write(base, 0x70, 0x0b); if read(base, 0x70) != 0x0b { fail("FEATURES_OK rejected"); } let mut maxima = [0u32; 2]; for (index, [desc, avail, used, _]) in [rx_frames, tx_frames].into_iter().enumerate() { write(base, 0x30, index as u32); let max = read(base, 0x34); if max < 8 || read(base, 0x44) != 0 { fail("queue capacity or stale ready"); } maxima[index] = max; write(base, 0x38, 8); write64(base, 0x80, desc); write64(base, 0x90, avail); write64(base, 0xa0, used); asm!("dmb oshst", options(nostack, preserves_flags)); write(base, 0x44, 1); if read(base, 0x44) != 1 { fail("QueueReady readback"); } } rx.mark_ready().unwrap_or_else(|_| fail("RX DMA ready")); tx.mark_ready().unwrap_or_else(|_| fail("TX DMA ready")); write(base, 0x70, 0x0f); if read(base, 0x70) != 0x0f { fail("DRIVER_OK rejected"); } (maxima[0], maxima[1]) }; if rx.release(allocator) != Err(virtio_ring::Error::Active) || tx.release(allocator) != Err(virtio_ring::Error::Active) { fail("active RX/TX DMA released"); } // The two available rings deliberately remain empty for this setup gate. unsafe { for frames in [rx_frames, tx_frames] { if u16::from_le(read_volatile((frames[1] + 2) as *const u16)) != 0 || u16::from_le(read_volatile((frames[2] + 2) as *const u16)) != 0 { fail("unexpected queue traffic"); } } } write_str("TURKOS/NET-TX: RX=0x"); write_hex(0); write_str(" TX=0x"); write_hex(1); write_str(" SIZE=0x"); write_hex(8); write_str(" RXMAX=0x"); write_hex(rx_max as usize); write_str(" TXMAX=0x"); write_hex(tx_max as usize); write_str(" DMA=0x"); write_hex(8); write_str(" DISTINCT=0x"); write_hex(1); write_str(" READY=0x"); write_hex(1); write_str("\r\n"); write_str("TURKOS/NET-TX: RX-DESC=0x"); write_hex(rx_desc as usize); write_str(" RX-AVAIL=0x"); write_hex(rx_frames[1] as usize); write_str(" RX-USED=0x"); write_hex(rx_frames[2] as usize); write_str(" RX-BUFFER=0x"); write_hex(rx_buffer as usize); write_str("\r\n"); write_str("TURKOS/NET-TX: TX-DESC=0x"); write_hex(tx_desc as usize); write_str(" TX-AVAIL=0x"); write_hex(tx_frames[1] as usize); write_str(" TX-USED=0x"); write_hex(tx_frames[2] as usize); write_str(" TX-BUFFER=0x"); write_hex(tx_buffer as usize); write_str("\r\n"); // Empty available rings prevent transmission; reset revokes device // access before either lease returns to the frame allocator. unsafe { write(base, 0x70, 0); asm!("dsb sy", options(nostack, preserves_flags)); if read(base, 0x70) != 0 { fail("reset status"); } for index in 0..2 { write(base, 0x30, index); if read(base, 0x44) != 0 { fail("reset QueueReady"); } } } rx.mark_reset().unwrap_or_else(|_| fail("RX DMA reset")); tx.mark_reset().unwrap_or_else(|_| fail("TX DMA reset")); let rx_guard = unsafe { read_volatile((rx_buffer + 2048) as *const u8) }; let tx_guard = unsafe { read_volatile((tx_buffer + 64) as *const u8) }; if rx_guard != 0xa5 || tx_guard != 0x5a { fail("RX/TX buffer guard"); } if rx.release(allocator).is_err() || tx.release(allocator).is_err() || allocator.remaining_pages() != free_before { fail("RX/TX DMA return accounting"); } write_str( "TURKOS/NET-TX: ACTIVE-RX=BLOCKED ACTIVE-TX=BLOCKED RESET-RX=0 RESET-TX=0 RELEASED=8 FREE-BEFORE=0x", ); write_hex(free_before as usize); write_str(" FREE-AFTER=0x"); write_hex(allocator.remaining_pages() as usize); write_str(" RXGUARD=0x"); write_hex(rx_guard as usize); write_str(" TXGUARD=0x"); write_hex(tx_guard as usize); write_str("\r\n"); #[cfg(feature = "virtio-net-size-test")] { use net_frame::Error; if net_frame::checked_frame_length(12) != Ok(0) || net_frame::checked_frame_length(1526) != Ok(1514) || net_frame::checked_frame_length(11) != Err(Error::ShortVirtioHeader) || net_frame::checked_frame_length(1527) != Err(Error::JumboFrame) || net_frame::checked_frame_length(2048) != Err(Error::JumboFrame) || net_frame::checked_frame_length(2049) != Err(Error::DmaOverrun) { fail("frame length boundary"); } write_str("TURKOS/NET-SIZE: HEADER=0x"); write_hex(net_frame::VIRTIO_HEADER_BYTES); write_str(" MAX-FRAME=0x"); write_hex(net_frame::MAX_ETHERNET_FRAME_BYTES); write_str(" MAX-USED=0x"); write_hex(net_frame::MAX_USED_BYTES); write_str(" BUFFER=0x"); write_hex(net_frame::RX_DMA_BUFFER_BYTES); write_str(" ACCEPT=12,1526 REJECT-SHORT=11 REJECT-JUMBO=1527,2048 REJECT-OVERRUN=2049\r\n"); write_line("TURKOS/NET-SIZE: PASS SYNTHETIC=1 REAL-PACKETS=UNTESTED"); } #[cfg(feature = "virtio-net-header-test")] { let exact = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x08, 0x00, ]; let mut extended = [0u8; 15]; extended[..14].copy_from_slice(&exact); extended[14] = 0xee; if net_frame::copy_ethernet_header(&exact[..13]) != Err(net_frame::Error::ShortEthernetHeader) || net_frame::copy_ethernet_header(&exact) != Ok(exact) || net_frame::copy_ethernet_header(&extended) != Ok(exact) { fail("Ethernet header minimum/copy"); } write_line( "TURKOS/ETH-HEADER: MIN=14 ACCEPT=14,15 REJECT-SHORT=13 HEADER=000102030405060708090a0b0800", ); write_line("TURKOS/ETH-HEADER: PASS SYNTHETIC=1 REAL-PACKETS=UNTESTED"); } #[cfg(feature = "virtio-net-type-test")] { let mut frame = [0u8; 14]; frame[12..14].copy_from_slice(&[0x08, 0x00]); if net_frame::classify_ethertype(&frame) != Ok(net_frame::EtherType::Ipv4) { fail("IPv4 EtherType byte order"); } frame[12..14].copy_from_slice(&[0x08, 0x06]); if net_frame::classify_ethertype(&frame) != Ok(net_frame::EtherType::Arp) { fail("ARP EtherType byte order"); } for kind in [[0x86, 0xdd], [0x81, 0x00], [0x00, 0x08]] { frame[12..14].copy_from_slice(&kind); if net_frame::classify_ethertype(&frame) != Err(net_frame::Error::UnsupportedEtherType) { fail("unknown EtherType accepted"); } } if net_frame::classify_ethertype(&frame[..13]) != Err(net_frame::Error::ShortEthernetHeader) { fail("short EtherType accepted"); } write_line( "TURKOS/ETH-TYPE: IPV4=0800 ARP=0806 DROP=86dd,8100,0008 SHORT=13 ORDER=BIG-ENDIAN", ); write_line("TURKOS/ETH-TYPE: PASS SYNTHETIC=1 REAL-PACKETS=UNTESTED"); } write_line("TURKOS/NET-TX: PASS SEPARATE=1 PACKETS=UNTESTED"); park() }