//! K430: four preallocated RX packet buffers; five real host injections. //! A fifth frame has no posted descriptor and must not allocate another page. use super::*; use sha2::{Digest, Sha256}; use turkos_kernel::{net_mac, net_pool, program_hash, virtio_mmio, virtio_ring}; 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 counter() -> u64 { let value: u64; unsafe { asm!("mrs {value}, CNTPCT_EL0", value = out(reg) value, options(nomem, nostack, preserves_flags)) }; value } fn fail(reason: &str) -> ! { write_str("TURKOS/NET-POOL-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 ring_lease = virtio_ring::DmaLease::allocate(allocator, boot.hardware.ram_base, boot.hardware.ram_size) .unwrap_or_else(|_| fail("ring DMA allocation")); let mut buffer_lease = virtio_ring::DmaLease::allocate(allocator, boot.hardware.ram_base, boot.hardware.ram_size) .unwrap_or_else(|_| fail("buffer DMA allocation")); let ring_frames = ring_lease.frames().unwrap_or_else(|_| fail("ring frames")); let buffer_frames = buffer_lease .frames() .unwrap_or_else(|_| fail("buffer frames")); let [desc, avail, used, first_buffer] = ring_frames; virtio_ring::split_ring_pages( desc, avail, used, 8, boot.hardware.ram_base, boot.hardware.ram_size, ) .unwrap_or_else(|_| fail("ring geometry")); let mut pool = net_pool::PacketPool::new( [ first_buffer, buffer_frames[0], buffer_frames[1], buffer_frames[2], ], boot.hardware.ram_base, boot.hardware.ram_size, ) .unwrap_or_else(|_| fail("packet pool frames")); if free_before.checked_sub(allocator.remaining_pages()) != Some(8) { fail("fixed eight-page allocation"); } let mut addresses = [0u64; 4]; // SAFETY: both leases own disjoint identity-mapped Normal-RAM frames. // Descriptors and available entries are complete before QueueReady. unsafe { for page in ring_frames.iter().chain(buffer_frames.iter()) { core::ptr::write_bytes(*page as *mut u8, 0, memory::PAGE_SIZE as usize); } for (index, address) in addresses.iter_mut().enumerate() { let (slot, frame) = pool.claim().unwrap_or_else(|_| fail("pool claim")); if slot != index { fail("pool slot order"); } *address = frame; write_volatile( (desc + index as u64 * 16) as *mut virtio_ring::Descriptor, virtio_ring::Descriptor { addr_le: frame.to_le(), len_le: 2048u32.to_le(), flags_le: virtio_ring::DESC_F_WRITE.to_le(), next_le: 0, }, ); write_volatile( (avail + 4 + index as u64 * 2) as *mut u16, (index as u16).to_le(), ); write_volatile((frame + 2048) as *mut u8, 0xa5); } } if pool.claim() != Err(net_pool::Error::Full) || pool.live() != 4 || allocator.remaining_pages() != free_before - 8 { fail("fifth buffer did not fail closed"); } let queue_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/VERSION_1 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("FEATURES_OK rejected"); } write(base, 0x30, 0); let max = read(base, 0x34); if max < 8 || read(base, 0x44) != 0 { fail("queue capacity/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("QueueReady readback"); } ring_lease .mark_ready() .unwrap_or_else(|_| fail("ring DMA ready")); buffer_lease .mark_ready() .unwrap_or_else(|_| fail("buffer DMA ready")); write(base, 0x70, 0x0f); if read(base, 0x70) != 0x0f { fail("DRIVER_OK rejected"); } max }; if ring_lease.release(allocator) != Err(virtio_ring::Error::Active) || buffer_lease.release(allocator) != Err(virtio_ring::Error::Active) { fail("active pool DMA released"); } unsafe { write_volatile((avail + 2) as *mut u16, 4u16.to_le()); asm!("dmb oshst", options(nostack, preserves_flags)); write(base, 0x50, 0); } write_str("TURKOS/NET-POOL: CAP=0x"); write_hex(pool.capacity()); write_str(" LIVE=0x"); write_hex(pool.live()); write_str(" DMA=0x"); write_hex(8); write_str(" MAX=0x"); write_hex(queue_max as usize); write_str(" FIFTH=FULL FREE-BEFORE=0x"); write_hex(free_before as usize); write_str(" FREE-AFTER-ALLOC=0x"); write_hex(allocator.remaining_pages() as usize); write_str("\r\n"); write_line("TURKOS/NET-POOL: WAITING-FOUR"); 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("deadline overflow")); let deadline = virtio_ring::deadline_after(counter(), budget).unwrap_or_else(|_| fail("deadline")); loop { let index = unsafe { u16::from_le(read_volatile((used + 2) as *const u16)) }; if index == 4 { break; } if index > 4 { fail("used over capacity"); } if virtio_ring::deadline_expired(counter(), deadline) { fail("four RX completions timeout"); } core::hint::spin_loop(); } unsafe { asm!("dmb oshld", options(nostack, preserves_flags)) }; let mut seen = [false; 4]; for index in 0..4u64 { let id = unsafe { u32::from_le(read_volatile((used + 4 + index * 8) as *const u32)) }; let len = unsafe { u32::from_le(read_volatile((used + 8 + index * 8) as *const u32)) }; if id >= 4 || len != 72 || seen[id as usize] { fail("used id/length/duplicate"); } seen[id as usize] = true; } let mut digest = Sha256::new(); for (packet, address) in addresses.iter().enumerate() { let mut frame = [0u8; 60]; for (offset, byte) in frame.iter_mut().enumerate() { *byte = unsafe { read_volatile((address + 12 + offset as u64) as *const u8) }; } if frame[..14] != [ 0x52, 0x54, 0x00, 0x12, 0x34, 0x56, 0x02, 0, 0, 0, 0, 1, 0x08, 0, ] || frame[14..] .iter() .enumerate() .any(|(offset, byte)| *byte != 0x40 + (packet as u8) * 16 + offset as u8) { write_str("TURKOS/NET-POOL-DIAG: SLOT=0x"); write_hex(packet); write_str(" HEADER="); for byte in &frame[..14] { write_hex(*byte as usize); } write_str(" PAYLOAD-FIRST=0x"); write_hex(frame[14] as usize); write_str(" PAYLOAD-LAST=0x"); write_hex(frame[59] as usize); write_str("\r\n"); fail("RX packet fixture bytes"); } digest.update(frame); } let digest_bytes: [u8; 32] = digest.finalize().into(); let digest_hex = program_hash::hex_lower(&digest_bytes); write_str("TURKOS/NET-POOL: USED=0x"); write_hex(4); write_str(" FRAME-BYTES=0x"); write_hex(60); write_str(" SHA256="); write_str(core::str::from_utf8(&digest_hex).unwrap_or_else(|_| fail("digest hex"))); write_str("\r\n"); write_line("TURKOS/NET-POOL: FOUR-COMPLETE SEND-FIFTH"); let short_budget = frequency .checked_mul(1) .unwrap_or_else(|| fail("fifth deadline overflow")); let fifth_deadline = virtio_ring::deadline_after(counter(), short_budget) .unwrap_or_else(|_| fail("fifth deadline")); while !virtio_ring::deadline_expired(counter(), fifth_deadline) { if unsafe { u16::from_le(read_volatile((used + 2) as *const u16)) } != 4 { fail("fifth packet consumed without descriptor"); } core::hint::spin_loop(); } if pool.live() != 4 || allocator.remaining_pages() != free_before - 8 { fail("pool grew under fifth packet"); } unsafe { write(base, 0x70, 0); asm!("dsb sy", options(nostack, preserves_flags)); if read(base, 0x70) != 0 || read(base, 0x44) != 0 { fail("reset ack"); } } ring_lease .mark_reset() .unwrap_or_else(|_| fail("ring reset")); buffer_lease .mark_reset() .unwrap_or_else(|_| fail("buffer reset")); for (index, address) in addresses.iter().enumerate() { if unsafe { read_volatile((address + 2048) as *const u8) } != 0xa5 { fail("packet buffer guard"); } pool.release(index) .unwrap_or_else(|_| fail("pool slot release")); } if pool.live() != 0 || ring_lease.release(allocator).is_err() || buffer_lease.release(allocator).is_err() || allocator.remaining_pages() != free_before { fail("pool/DMA return accounting"); } write_str("TURKOS/NET-POOL: FIFTH-USED=0x"); write_hex(4); write_str(" CAP=0x"); write_hex(4); write_str(" LIVE=0x"); write_hex(pool.live()); write_str(" RELEASED=0x"); write_hex(8); write_str(" FREE-AFTER=0x"); write_hex(allocator.remaining_pages() as usize); write_str(" GUARDS=OK\r\n"); write_line("TURKOS/NET-POOL: PASS RECEIVED=4 FIFTH=NOT-CONSUMED OOM=NO"); park() }