//! K423 owns one RX DMA queue; the K428 feature also validates one real packet. use super::*; #[cfg(feature = "virtio-net-rx-completion-test")] use sha2::{Digest, Sha256}; #[cfg(feature = "virtio-net-arp-request-test")] use turkos_kernel::net_arp; #[cfg(feature = "virtio-net-ipv4-header-test")] use turkos_kernel::net_ipv4; #[cfg(feature = "virtio-net-rx-completion-test")] use turkos_kernel::{net_frame, program_hash}; use turkos_kernel::{net_mac, virtio_mmio, virtio_ring}; #[cfg(feature = "virtio-net-rx-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-RX-ERROR: "); write_line(reason); park() } 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 lease = virtio_ring::DmaLease::allocate(allocator, boot.hardware.ram_base, boot.hardware.ram_size) .unwrap_or_else(|_| fail("DMA allocation")); let [desc, avail, used, buffer] = lease.frames().unwrap_or_else(|_| fail("DMA frames")); virtio_ring::split_ring_pages( desc, avail, used, 8, boot.hardware.ram_base, boot.hardware.ram_size, ) .unwrap_or_else(|_| fail("RX split ring geometry")); let descriptor = virtio_ring::Descriptor { addr_le: buffer.to_le(), len_le: 2048u32.to_le(), flags_le: virtio_ring::DESC_F_WRITE.to_le(), next_le: 0, }; // SAFETY: the four frames are uniquely leased and Normal-RAM identity // mapped. Zeroing happens before the queue is exposed to the device. unsafe { for frame in [desc, avail, used, buffer] { core::ptr::write_bytes(frame as *mut u8, 0, memory::PAGE_SIZE as usize); } write_volatile(desc as *mut virtio_ring::Descriptor, descriptor); write_volatile((avail + 4) as *mut u16, 0u16.to_le()); write_volatile((buffer + 2048) as *mut u8, 0xa5); } if free_before.checked_sub(allocator.remaining_pages()) != Some(4) { fail("DMA accounting after allocation"); } // SAFETY: K322 mapped the entire 0x200-byte modern MMIO slot as Device. // The features are intentionally minimal: MAC and VERSION_1 only. let (offered_low, offered_high, queue_max) = unsafe { write(base, 0x70, 0); if read(base, 0x70) != 0 || read(base, 0x44) != 0 { fail("reset before feature negotiation"); } 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"); } write(base, 0x30, 0); let max = read(base, 0x34); if max < 8 || read(base, 0x44) != 0 { fail("RX queue capacity or stale ready"); } 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("RX QueueReady readback"); } lease.mark_ready().unwrap_or_else(|_| fail("DMA ready")); write(base, 0x70, 0x0f); if read(base, 0x70) != 0x0f { fail("DRIVER_OK rejected"); } (low, high, max) }; if lease.release(allocator) != Err(virtio_ring::Error::Active) { fail("active RX DMA released"); } unsafe { write_volatile((avail + 2) as *mut u16, 1u16.to_le()); asm!("dmb oshst", options(nostack, preserves_flags)); write(base, 0x50, 0); } write_str("TURKOS/NET-RX: QUEUE=0x"); write_hex(0); write_str(" SIZE=0x"); write_hex(8); write_str(" MAX=0x"); write_hex(queue_max as usize); write_str(" OFFERED-LOW=0x"); write_hex(offered_low as usize); write_str(" OFFERED-HIGH=0x"); write_hex(offered_high as usize); write_str(" DMA=0x"); write_hex(4); write_str(" READY=0x"); write_hex(1); write_str("\r\n"); #[cfg(feature = "virtio-net-rx-completion-test")] { write_line("TURKOS/NET-RX-COMPLETE: WAITING-FRAME"); 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("RX deadline overflow")); let deadline = virtio_ring::deadline_after(counter(), budget).unwrap_or_else(|_| fail("RX deadline")); let used_index = loop { let index = unsafe { u16::from_le(read_volatile((used + 2) as *const u16)) }; if index != 0 { break index; } if virtio_ring::deadline_expired(counter(), deadline) { fail("RX completion timeout"); } core::hint::spin_loop(); }; let (used_id, used_len) = unsafe { asm!("dmb oshld", options(nostack, preserves_flags)); ( u32::from_le(read_volatile((used + 4) as *const u32)), u32::from_le(read_volatile((used + 8) as *const u32)), ) }; if net_frame::validate_fixture_completion(used_index, used_id, used_len, 60).is_err() { fail("RX used completion length/id"); } // This synthetic counterexample must fail the same gate; no bytes // beyond the device-reported used length are exposed as a packet. if net_frame::validate_fixture_completion(used_index, used_id, used_len - 1, 60) != Err(net_frame::Error::ShortResponse) { fail("partial RX completion accepted"); } let mut frame = [0u8; 60]; for (index, byte) in frame.iter_mut().enumerate() { *byte = unsafe { read_volatile((buffer + 12 + index as u64) as *const u8) }; } let header = net_frame::copy_ethernet_header(&frame) .unwrap_or_else(|_| fail("received Ethernet header")); #[cfg(feature = "virtio-net-broadcast-test")] { let mut own = [0u8; 6]; for (index, byte) in own.iter_mut().enumerate() { *byte = unsafe { read_volatile((base + 0x100 + index as u64) as *const u8) }; } if net_mac::validate(net_mac::VIRTIO_NET_F_MAC, own).is_err() || own != [0x52, 0x54, 0x00, 0x12, 0x34, 0x56] || net_mac::classify_destination(header[..6].try_into().unwrap(), own) != Ok(net_mac::Destination::Broadcast) || net_mac::classify_destination(own, own) != Ok(net_mac::Destination::OwnUnicast) || net_mac::classify_destination([0x52, 0x54, 0, 0x12, 0x34, 0x57], own) != Err(net_mac::DestinationError::AlienUnicast) || net_mac::classify_destination([0x01, 0, 0x5e, 0, 0, 1], own) != Err(net_mac::DestinationError::UnsupportedMulticast) { fail("broadcast destination policy"); } write_line( "TURKOS/NET-BCAST: OWN=525400123456 DEST=ffffffffffff CLASS=BROADCAST FOREIGN=REJECT MULTICAST=REJECT", ); } #[cfg(feature = "virtio-net-unicast-test")] { let mut own = [0u8; 6]; for (index, byte) in own.iter_mut().enumerate() { *byte = unsafe { read_volatile((base + 0x100 + index as u64) as *const u8) }; } let destination: [u8; 6] = header[..6].try_into().unwrap(); if net_mac::validate(net_mac::VIRTIO_NET_F_MAC, own).is_err() || own != [0x52, 0x54, 0x00, 0x12, 0x34, 0x56] || net_mac::classify_destination(destination, own) != Ok(net_mac::Destination::OwnUnicast) || net_mac::classify_destination([0xff; 6], own) != Ok(net_mac::Destination::Broadcast) || net_mac::classify_destination([0x52, 0x54, 0, 0x12, 0x34, 0x57], own) != Err(net_mac::DestinationError::AlienUnicast) { fail("unicast destination policy"); } write_line( "TURKOS/NET-UCAST: OWN=525400123456 DEST=525400123456 CLASS=OWN-UNICAST BROADCAST=OTHER FOREIGN=REJECT", ); } let expected_destination = if cfg!(feature = "virtio-net-broadcast-test") { [0xff; 6] } else { [0x52, 0x54, 0x00, 0x12, 0x34, 0x56] }; let expected_type = if cfg!(feature = "virtio-net-arp-request-test") { net_frame::EtherType::Arp } else { net_frame::EtherType::Ipv4 }; if header[..6] != expected_destination || header[6..12] != [0x02, 0x00, 0x00, 0x00, 0x00, 0x01] || net_frame::classify_ethertype(&frame) != Ok(expected_type) { fail("received Ethernet fixture bytes"); } #[cfg(not(any( feature = "virtio-net-ipv4-header-test", feature = "virtio-net-arp-request-test" )))] if frame[14..] .iter() .enumerate() .any(|(index, byte)| *byte != 0x80 + index as u8) { fail("received Ethernet fixture bytes"); } #[cfg(feature = "virtio-net-arp-request-test")] { let request = net_arp::parse_request(&frame[14..]).unwrap_or_else(|_| fail("ARP request header")); if request.sender_mac != [0x02, 0, 0, 0, 0, 1] || request.sender_ipv4 != [192, 0, 2, 1] || request.target_mac != [0; 6] || request.target_ipv4 != [192, 0, 2, 2] || frame[42..].iter().any(|byte| *byte != 0) { fail("ARP request fixture bytes"); } write_line( "TURKOS/NET-ARP-REQ: HTYPE=1 PTYPE=0800 HLEN=6 PLEN=4 OPER=1 SHA=020000000001 SPA=c0000201 TPA=c0000202 REPLY=UNTESTED", ); } #[cfg(feature = "virtio-net-ipv4-header-test")] { let geometry = net_ipv4::header_geometry(&frame[14..]) .unwrap_or_else(|_| fail("IPv4 header geometry")); if geometry.header_bytes != 20 || geometry.total_bytes != 28 { fail("IPv4 fixture geometry"); } #[cfg(feature = "virtio-net-ipv4-checksum-test")] if net_ipv4::verify_header_checksum(&frame[14..]) != Ok(geometry) { fail("IPv4 header checksum"); } let expected_header: [u8; 20] = [ 0x45, 0, 0, 28, 0, 1, 0, 0, 64, 17, 0xf6, 0xcc, 192, 0, 2, 1, 192, 0, 2, 2, ]; if frame[14..34] != expected_header || frame[34..42] .iter() .enumerate() .any(|(index, byte)| *byte != 0xa0 + index as u8) || frame[42..].iter().any(|byte| *byte != 0) { fail("IPv4 fixture bytes"); } #[cfg(not(feature = "virtio-net-ipv4-checksum-test"))] write_line( "TURKOS/NET-IPV4: VERSION=4 IHL=5 HEADER=20 TOTAL=28 AVAILABLE=46 CHECKSUM=UNTESTED", ); #[cfg(feature = "virtio-net-ipv4-checksum-test")] write_line( "TURKOS/NET-IPV4-CRC: VERSION=4 IHL=5 HEADER=20 TOTAL=28 AVAILABLE=46 CHECKSUM=VALID", ); } let digest: [u8; 32] = Sha256::digest(frame).into(); let digest_hex = program_hash::hex_lower(&digest); write_str("TURKOS/NET-RX-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(" FRAME=0x"); write_hex(frame.len()); write_str(" SHORT-71=REJECT SHA256="); write_str(core::str::from_utf8(&digest_hex).unwrap_or_else(|_| fail("RX digest hex"))); write_str("\r\n"); } // Reset removes device ownership before the frame allocator sees pages. // Reset removes device ownership before the frame allocator sees pages. unsafe { write(base, 0x70, 0); asm!("dsb sy", options(nostack, preserves_flags)); if read(base, 0x70) != 0 || read(base, 0x44) != 0 { fail("RX reset acknowledgement"); } } lease .mark_reset() .unwrap_or_else(|_| fail("DMA reset state")); let guard = unsafe { read_volatile((buffer + 2048) as *const u8) }; if guard != 0xa5 { fail("RX buffer guard"); } if lease.release(allocator).is_err() || allocator.remaining_pages() != free_before { fail("DMA release/accounting"); } write_str("TURKOS/NET-RX: ACTIVE-RELEASE=BLOCKED RESET-READY=0x"); write_hex(0); write_str(" RELEASED=0x"); write_hex(4); 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(guard as usize); write_str("\r\n"); #[cfg(not(feature = "virtio-net-rx-completion-test"))] write_line("TURKOS/NET-RX: PASS OWNED=4 RESET=1 PACKETS=UNTESTED"); #[cfg(all( feature = "virtio-net-rx-completion-test", not(feature = "virtio-net-broadcast-test"), not(feature = "virtio-net-unicast-test") ))] write_line("TURKOS/NET-RX-COMPLETE: PASS PACKETS=1 RESET=1 DMA-RELEASED=4"); #[cfg(feature = "virtio-net-broadcast-test")] #[cfg(not(feature = "virtio-net-arp-request-test"))] write_line("TURKOS/NET-BCAST: PASS PACKETS=1 RESET=1 DMA-RELEASED=4"); #[cfg(feature = "virtio-net-arp-request-test")] write_line("TURKOS/NET-ARP-REQ: PASS PACKETS=1 RESET=1 DMA-RELEASED=4"); #[cfg(feature = "virtio-net-unicast-test")] #[cfg(not(feature = "virtio-net-ipv4-header-test"))] write_line("TURKOS/NET-UCAST: PASS PACKETS=1 RESET=1 DMA-RELEASED=4"); #[cfg(feature = "virtio-net-ipv4-header-test")] #[cfg(not(feature = "virtio-net-ipv4-checksum-test"))] write_line("TURKOS/NET-IPV4: PASS PACKETS=1 RESET=1 DMA-RELEASED=4"); #[cfg(feature = "virtio-net-ipv4-checksum-test")] write_line("TURKOS/NET-IPV4-CRC: PASS PACKETS=1 RESET=1 DMA-RELEASED=4"); park() }