//! P12 real virtio-blk transport for the disposable experimental mount. //! The host picks write/replay/read and an optional pause checkpoint per boot //! through `-append` (FDT /chosen/bootargs). Every completed request prints //! the used-ring index/id/len, the device status byte and the header type and //! sector read back from DMA memory. Default Image has no such entry point; //! production-write stays blocked. use super::*; use turkos_kernel::{ journal_disk::{self, Block, BlockIo, Error, Mode, Mount, Plan, Step}, virtio_mmio, virtio_ring, }; const QUEUE: u16 = 8; struct Disk { // Transport frames belong to a verified-reset DMA lease. base: u64, capacity: u64, frames: [u64; 4], next: u16, frequency: u64, lease: virtio_ring::DmaLease, writable: bool, flush: bool, pause: u32, step: u32, counts: [usize; 3], free_open: u64, free_leased: u64, } global_asm!( ".section .bss.p12_stack,\"aw\",%nobits", ".balign 4096", ".global __p12_stack_guard", "__p12_stack_guard: .skip 64", ".skip 32704", ".global __p12_stack_top", "__p12_stack_top:", ".section .text", ".global kernel_p12_enter", "kernel_p12_enter:", "adrp x9, __p12_stack_top", "add x9, x9, :lo12:__p12_stack_top", "mov sp, x9", "b kernel_p12_run", ); #[allow(improper_ctypes)] unsafe extern "C" { fn kernel_p12_enter(boot: *const BootData, allocator: *mut memory::FrameAllocator) -> !; static mut __p12_stack_guard: u64; } const CANARY: u64 = 0x5031_3253_5441_434b; /// `NAME=0x` without leading zeros keeps the per-request lines short. fn hex_field(name: &str, value: usize) { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut digits = [0u8; 16]; let mut start = digits.len(); let mut rest = value; loop { start -= 1; digits[start] = HEX[rest & 15]; rest >>= 4; if rest == 0 { break; } } write_str(name); write_str("=0x"); write_str(core::str::from_utf8(&digits[start..]).unwrap()); } impl Disk { fn open( boot: &BootData, allocator: &mut memory::FrameAllocator, pause: u32, ) -> Result { let mut device = None; for node in boot.virtio_mmio.as_slice() { let b = node.base; unsafe { if read_volatile(b as *const u32) != virtio_mmio::MAGIC || read_volatile((b + 4) as *const u32) != 2 { return Err(Error::Io); } match read_volatile((b + 8) as *const u32) { 0 => {} 2 if device.replace(b).is_none() => {} _ => return Err(Error::Io), } } } let base = device.ok_or(Error::Io)?; let free_open = allocator.remaining_pages(); let lease = virtio_ring::DmaLease::allocate( allocator, boot.hardware.ram_base, boot.hardware.ram_size, ) .map_err(|_| Error::Io)?; let frames = lease.frames().map_err(|_| Error::Io)?; let mut disk = Self { base, capacity: 0, frames, next: 0, frequency: 0, lease, writable: false, flush: false, pause, step: 0, counts: [0; 3], free_open, free_leased: allocator.remaining_pages(), }; unsafe { for frame in frames { core::ptr::write_bytes(frame as *mut u8, 0, 4096); } disk.set(0x70, 0); if disk.get(0x70) != 0 { return Err(Error::Io); } disk.set(0x70, 3); disk.set(0x14, 0); let low = disk.get(0x10); disk.set(0x14, 1); let high = disk.get(0x10); // A read-only device offers RO; only a writable one gets FLUSH. let (l, h) = if low & virtio_mmio::BLK_F_RO != 0 { virtio_mmio::select_modern_read_only_features(low, high) } else { virtio_mmio::select_experimental_flush_features(low, high) } .map_err(|_| Error::Io)?; disk.writable = l & virtio_mmio::BLK_F_RO == 0; disk.flush = l & virtio_mmio::BLK_F_FLUSH != 0; disk.set(0x24, 0); disk.set(0x20, l); disk.set(0x24, 1); disk.set(0x20, h); disk.set(0x70, 0x0b); if disk.get(0x70) != 0x0b { return Err(Error::Io); } disk.set(0x30, 0); if disk.get(0x34) < u32::from(QUEUE) || disk.get(0x44) != 0 { return Err(Error::Io); } virtio_ring::split_ring_pages( frames[0], frames[1], frames[2], u32::from(QUEUE), boot.hardware.ram_base, boot.hardware.ram_size, ) .map_err(|_| Error::Io)?; disk.set(0x38, u32::from(QUEUE)); for (at, address) in [(0x80, frames[0]), (0x90, frames[1]), (0xa0, frames[2])] { disk.set(at, address as u32); disk.set(at + 4, (address >> 32) as u32); } asm!("dmb oshst", options(nostack, preserves_flags)); disk.set(0x44, 1); if disk.get(0x44) != 1 { return Err(Error::Io); } disk.lease.mark_ready().map_err(|_| Error::Io)?; disk.set(0x70, 0x0f); if disk.get(0x70) != 0x0f { return Err(Error::Io); } disk.capacity = u64::from(disk.get(0x100)) | (u64::from(disk.get(0x104)) << 32); asm!("mrs {v}, CNTFRQ_EL0", v = out(reg) disk.frequency, options(nomem, nostack, preserves_flags)); } if disk.frequency == 0 { return Err(Error::Io); } Ok(disk) } unsafe fn get(&self, at: u64) -> u32 { unsafe { read_volatile((self.base + at) as *const u32) } } unsafe fn set(&self, at: u64, v: u32) { unsafe { write_volatile((self.base + at) as *mut u32, v) } } /// Submit one request (0 IN, 1 OUT, 4 FLUSH) and wait for its used entry. fn request(&mut self, operation: u32, lba: u64, buffer: &mut Block) -> Result<(), Error> { if (operation != 4 && lba >= self.capacity) || (operation != 0 && !self.writable) || (operation == 4 && !self.flush) { return Err(Error::Io); } self.next = self.next.checked_add(1).ok_or(Error::Io)?; let [desc, avail, used, _] = self.frames; let lease_error = |_| Error::Io; let header = self.lease.request_subregion(0, 16).map_err(lease_error)?; let status = self.lease.request_subregion(16, 1).map_err(lease_error)?; let data = self .lease .request_subregion(512, 512) .map_err(lease_error)?; let (chain, count) = match operation { 0 => { let chain = virtio_ring::read_descriptor_chain(header, data, status); self.lease .validate_read_chain(&chain) .map_err(lease_error)?; (chain, 3) } 1 => { let chain = virtio_ring::write_descriptor_chain(header, data, status); virtio_ring::validate_write_descriptor_chain(&chain).map_err(lease_error)?; (chain, 3) } // FLUSH: device-readable header, device-writable status only. 4 => ( [ virtio_ring::Descriptor { addr_le: header.to_le(), len_le: 16u32.to_le(), flags_le: virtio_ring::DESC_F_NEXT.to_le(), next_le: 1u16.to_le(), }, virtio_ring::Descriptor { addr_le: status.to_le(), len_le: 1u32.to_le(), flags_le: virtio_ring::DESC_F_WRITE.to_le(), next_le: 0, }, virtio_ring::Descriptor::default(), ], 2, ), _ => return Err(Error::Io), }; unsafe { for (index, d) in chain.into_iter().take(count).enumerate() { write_volatile((desc as *mut virtio_ring::Descriptor).add(index), d); } let h = match operation { 0 => virtio_ring::block_read_header(lba), 1 => virtio_ring::block_write_header(lba), _ => virtio_ring::block_flush_header(), }; write_volatile(header as *mut virtio_ring::BlockReadHeader, h); write_volatile(status as *mut u8, 0xff); if operation == 1 { core::ptr::copy_nonoverlapping(buffer.as_ptr(), data as *mut u8, 512); } let slot = u64::from((self.next - 1) % QUEUE); write_volatile((avail + 4 + slot * 2) as *mut u16, 0); asm!("dmb oshst", options(nostack, preserves_flags)); write_volatile((avail + 2) as *mut u16, self.next.to_le()); asm!("dmb oshst", options(nostack, preserves_flags)); self.set(0x50, 0); } let deadline = virtio_ring::deadline_after(counter(), self.frequency * 2).map_err(lease_error)?; let observed = loop { let n = unsafe { u16::from_le(read_volatile((used + 2) as *const u16)) }; if n != self.next - 1 || virtio_ring::deadline_expired(counter(), deadline) { break n; } core::hint::spin_loop(); }; unsafe { asm!("dmb oshld", options(nostack, preserves_flags)) }; // Everything printed below is read back from shared DMA memory. let entry = used + 4 + u64::from((self.next - 1) % QUEUE) * 8; let (id, len, s, kind, sector) = unsafe { let h = read_volatile(header as *const virtio_ring::BlockReadHeader); ( u32::from_le(read_volatile(entry as *const u32)), u32::from_le(read_volatile((entry + 4) as *const u32)), read_volatile(status as *const u8), u32::from_le(h.type_le), u64::from_le(h.sector_le), ) }; write_str("TURKOS/P12-IO: "); hex_field("SEQ", self.next as usize); write_str(match kind { 0 => " OP=IN", 1 => " OP=OUT", 4 => " OP=FLUSH", _ => " OP=UNKNOWN", }); hex_field(" SECTOR", sector as usize); hex_field(" USED", observed as usize); if observed != self.next { // The request may still be device-owned; the lease stays active. write_line(if observed == self.next - 1 { " TIMEOUT" } else { " UNEXPECTED" }); return Err(Error::Io); } hex_field(" ID", id as usize); hex_field(" LEN", len as usize); hex_field(" STATUS", s as usize); write_str("\r\n"); self.counts[match operation { 0 => 0, 1 => 1, _ => 2, }] += 1; let expected_len = if operation == 0 { 513 } else { 1 }; if id != 0 || len != expected_len || kind != operation || (operation != 4 && sector != lba) { return Err(Error::Io); } virtio_ring::read_block_status(s).map_err(lease_error)?; if operation == 0 { unsafe { core::ptr::copy_nonoverlapping(data as *const u8, buffer.as_mut_ptr(), 512) }; } Ok(()) } fn close(&mut self, allocator: &mut memory::FrameAllocator) { // The queue is still live: returning its frames now must be refused. let rejected = self.lease.release(allocator) == Err(virtio_ring::Error::Active); let held = allocator.remaining_pages(); let (status, ready) = unsafe { self.set(0x70, 0); (self.get(0x70), self.get(0x44)) }; if status == 0 && ready == 0 { self.lease .mark_reset() .unwrap_or_else(|_| fail("lease reset")); self.lease .release(allocator) .unwrap_or_else(|_| fail("lease release")); } write_str("TURKOS/P12-DMA: FRAMES=0x4"); hex_field(" FREE-OPEN", self.free_open as usize); hex_field(" FREE-LEASED", self.free_leased as usize); write_str(if rejected { " ACTIVE-RELEASE=REJECTED" } else { " ACTIVE-RELEASE=ACCEPTED" }); hex_field(" FREE-HELD", held as usize); hex_field(" RESET-STATUS", status as usize); hex_field(" QUEUE-READY", ready as usize); hex_field(" FREE-AFTER", allocator.remaining_pages() as usize); write_str("\r\n"); if !rejected || status != 0 || ready != 0 { fail("DMA ownership"); } write_str("TURKOS/P12-IO-TOTAL: "); hex_field("IN", self.counts[0]); hex_field(" OUT", self.counts[1]); hex_field(" FLUSH", self.counts[2]); write_str("\r\n"); if unsafe { read_volatile(&raw const __p12_stack_guard) } != CANARY { fail("stack canary"); } write_line("TURKOS/P12-STACK: DEDICATED=32768 CANARY=PASS"); } } impl BlockIo for Disk { fn read(&mut self, lba: u64, out: &mut Block) -> Result<(), Error> { self.request(0, lba, out) } fn write(&mut self, lba: u64, bytes: &Block) -> Result<(), Error> { let mut buffer = *bytes; self.request(1, lba, &mut buffer) } fn flush(&mut self) -> Result<(), Error> { self.request(4, 0, &mut [0; 512]) } fn checkpoint(&mut self, step: u32, label: Step) { self.step = step; write_str("TURKOS/P12-STEP: "); hex_field("N", step as usize); hex_field(" SEQ", self.next as usize); write_str(" "); write_line(label.label()); if self.pause == step { write_str("TURKOS/P12: CHECKPOINT-PAUSE "); hex_field("N", step as usize); write_str("\r\n"); park(); } } } fn fail(message: &str) -> ! { write_str("TURKOS/P12-FATAL: "); write_line(message); park() } fn counter() -> u64 { let v; unsafe { asm!("mrs {v}, CNTPCT_EL0", v = out(reg) v, options(nomem, nostack, preserves_flags)); } v } fn print_digest(bytes: &[u8; 32]) { const HEX: &[u8; 16] = b"0123456789abcdef"; for b in bytes { let a = [HEX[(b >> 4) as usize], HEX[(b & 15) as usize]]; write_str(core::str::from_utf8(&a).unwrap()); } } fn print_plan(phase: &str, plan: &Plan, freed: Option) { write_str("TURKOS/P12-JOURNAL: PHASE="); write_str(phase); hex_field(" EMPTY", plan.empty as usize); hex_field(" INCOMPLETE", plan.incomplete as usize); hex_field(" COMMITTED", plan.committed as usize); hex_field(" APPLY", plan.apply as usize); hex_field(" ALREADY", plan.already as usize); hex_field(" BITMAP-CHANGES", plan.bitmap_changes as usize); write_str("\r\nTURKOS/P12-OWNERS: PHASE="); write_str(phase); let o = plan.ownership; hex_field(" ALLOCATED", o.allocated as usize); hex_field(" OWNED", o.owned as usize); hex_field(" RELEASED", o.released as usize); hex_field(" CLAIMED", o.claimed as usize); hex_field(" UNKNOWN", o.unknown as usize); if let Some(freed) = freed { hex_field(" UNKNOWN-FREED", freed as usize); } write_str("\r\n"); } pub(super) fn run(boot: &BootData, allocator: &mut memory::FrameAllocator) -> ! { // Keep main's boot frame live and give the mount a BSS-reserved stack; // do not enlarge the ordinary Image's stack for this experiment. unsafe { write_volatile(&raw mut __p12_stack_guard, CANARY); kernel_p12_enter(boot, allocator) } } #[unsafe(no_mangle)] extern "C" fn kernel_p12_run(boot: &BootData, allocator: &mut memory::FrameAllocator) -> ! { // SAFETY: read_boot_data bounded this identity-mapped DTB; the frame // allocator never hands out its reserved range. let blob = unsafe { core::slice::from_raw_parts(boot.dtb_address as *const u8, boot.dtb_size) }; let params = journal_disk::chosen_bootargs(blob) .and_then(journal_disk::parse_params) .unwrap_or_else(|error| { write_str("TURKOS/P12-ERROR: "); write_line(error.label()); write_line("TURKOS/P12: REJECTED"); park() }); write_str(match params.mode { Mode::Write => "TURKOS/P12-MODE: WRITE", Mode::Replay => "TURKOS/P12-MODE: REPLAY", Mode::Read => "TURKOS/P12-MODE: READ", }); hex_field(" PAUSE", params.pause as usize); write_str("\r\n"); let mut disk = Disk::open(boot, allocator, params.pause).unwrap_or_else(|_| fail("open transport")); write_str("TURKOS/P12-DEVICE: "); hex_field("RO", !disk.writable as usize); hex_field(" FLUSH", disk.flush as usize); hex_field(" CAPACITY", disk.capacity as usize); write_str("\r\n"); let capacity = disk.capacity; let result = (|| { if (params.mode == Mode::Read) == disk.writable { return Err(Error::DeviceMode); } let mut mount = Mount::open(&mut disk, capacity)?; let before = mount.plan(&mut disk)?; print_plan("BEFORE", &before, None); match params.mode { Mode::Write => { mount.write_pair(&mut disk)?; write_line("TURKOS/P12-WRITE: INODES=0x2 COMMITTED=0x2 PASS"); } Mode::Replay => { let outcome = mount.replay(&mut disk)?; write_str("TURKOS/P12-REPLAY: "); hex_field("APPLIED", outcome.applied as usize); hex_field(" ALREADY", outcome.already as usize); hex_field(" WRITES", outcome.writes as usize); write_str("\r\n"); } Mode::Read => {} } // A fresh mount re-reads every block from the device, not this boot's // in-memory metadata. let fresh = Mount::open(&mut disk, capacity)?; let after = fresh.plan(&mut disk)?; print_plan("AFTER", &after, Some(fresh.unknown_freed(&before))); for file in fresh.files(&mut disk)? { write_str("TURKOS/P12-FILE: "); hex_field("INODE", file.inode as usize); hex_field(" LBA", file.lba as usize); hex_field(" GEN", file.generation as usize); write_str(" SHA256="); print_digest(&file.sha256); write_str("\r\n"); } Ok::<(), Error>(()) })(); if let Err(error) = result { write_str("TURKOS/P12-ERROR: "); write_str(error.label()); hex_field(" AFTER-STEP", disk.step as usize); write_str("\r\n"); } disk.close(allocator); write_line(if result.is_ok() { "TURKOS/P12: PASS" } else { "TURKOS/P12: REJECTED" }); park() }