Skip to main content

strat9_kernel/boot/
virtio_blk.rs

1//! VirtIO block device driver for boot partition access.
2//!
3//! Minimal read-only driver for reading modules from the FAT32 boot partition.
4//! Supports VirtIO MMIO transport (common on RISC-V/ARM64 QEMU) with a
5//! working virtqueue-based block read implementation.
6//!
7//! Uses the kernel frame allocator (available after buddy init) to allocate
8//! physically-contiguous vring and bounce-buffer memory.
9
10use super::block_device::BlockDevice;
11
12// ── Legacy VirtIO MMIO register offsets ────────────────────────────────
13const REG_DEVICE_FEATURES: u64 = 0x000;
14const REG_DRIVER_FEATURES: u64 = 0x008;
15const REG_QUEUE_SEL: u64 = 0x010;
16const REG_QUEUE_NUM_MAX: u64 = 0x014;
17const REG_QUEUE_NUM: u64 = 0x018;
18const REG_QUEUE_ALIGN: u64 = 0x01C;
19const REG_QUEUE_PFN: u64 = 0x020;
20const REG_QUEUE_NOTIFY: u64 = 0x024;
21const REG_STATUS: u64 = 0x030;
22const REG_CAPACITY: u64 = 0x100;
23
24// ── VirtIO device status flags ─────────────────────────────────────────
25const STATUS_ACK: u32 = 0x01;
26const STATUS_DRIVER: u32 = 0x02;
27const STATUS_FEATURES_OK: u32 = 0x08;
28const STATUS_DRIVER_OK: u32 = 0x04;
29const STATUS_FAILED: u32 = 0x80;
30
31// ── VRING descriptor flags ────────────────────────────────────────────
32const VRING_DESC_F_NEXT: u16 = 0x01;
33const VRING_DESC_F_WRITE: u16 = 0x02;
34
35// ── Block request types ───────────────────────────────────────────────
36const REQ_TYPE_IN: u32 = 0;
37
38// ── Queue configuration ───────────────────────────────────────────────
39/// Small queue size is enough for single-block boot reads (header+data+status = 3 descs).
40const BOOT_QUEUE_SIZE: u16 = 8;
41
42/// VirtIO block request header (must match device layout exactly).
43#[repr(C)]
44struct BootBlockHeader {
45    request_type: u32,
46    reserved: u32,
47    sector: u64,
48}
49
50/// VirtIO block request status byte.
51#[repr(u8)]
52#[derive(PartialEq, Eq)]
53enum BootBlockStatus {
54    Ok = 0,
55    Error = 1,
56    Unsupported = 2,
57}
58
59/// VRING descriptor (16 bytes).
60#[repr(C)]
61struct VringDesc {
62    addr: u64,
63    len: u32,
64    flags: u16,
65    next: u16,
66}
67
68/// Available ring header (followed by `queue_size` u16 ring entries + optional used_event).
69#[repr(C)]
70struct VringAvail {
71    flags: u16,
72    idx: u16,
73    // ring: [u16; queue_size],
74    // used_event: u16 (if VIRTIO_F_EVENT_IDX)
75}
76
77/// Used ring element (8 bytes).
78#[repr(C)]
79struct VringUsedElem {
80    id: u32,
81    len: u32,
82}
83
84/// Used ring header (followed by `queue_size` VringUsedElem entries + optional avail_event).
85#[repr(C)]
86struct VringUsed {
87    flags: u16,
88    idx: u16,
89    // ring: [VringUsedElem; queue_size],
90    // avail_event: u16 (if VIRTIO_F_EVENT_IDX)
91}
92
93/// Minimal boot-time virtqueue with a static vring allocation.
94///
95/// Legacy MMIO layout requires the vring to occupy physically-contiguous
96/// pages with the three sections page-aligned:
97///   ┌─ Page 0: descriptor table (page-aligned)
98///   ├─ Page 1: available ring    (page-aligned)
99///   └─ Page 2: used ring         (tail, may share page 3)
100struct BootVirtqueue {
101    /// Physical address of the vring base (written to QueuePFN >> 12).
102    vring_phys: u64,
103    /// Virtual (HHDM) address of the vring base.
104    vring_virt: u64,
105    /// Queue size.
106    queue_size: u16,
107    /// Next available index.
108    avail_idx: u16,
109    /// Last observed used index.
110    last_used_idx: u16,
111}
112
113impl BootVirtqueue {
114    /// Allocate and initialise a boot-time virtqueue.
115    ///
116    /// # Safety
117    /// `device_base` must be a valid MMIO VirtIO device address.
118    unsafe fn init(device_base: u64, queue_size: u16) -> Result<Self, &'static str> {
119        if !queue_size.is_power_of_two() {
120            return Err("Boot virtqueue size must be power of 2");
121        }
122
123        let desc_size = queue_size as usize * core::mem::size_of::<VringDesc>();
124        let avail_size = core::mem::size_of::<VringAvail>() + queue_size as usize * 2;
125        let used_elem_size = core::mem::size_of::<VringUsedElem>();
126        let used_size = core::mem::size_of::<VringUsed>() + queue_size as usize * used_elem_size;
127
128        let page_size: usize = 4096;
129        let avail_offset = (desc_size + page_size - 1) / page_size * page_size;
130        let used_offset = (avail_offset + avail_size + page_size - 1) / page_size * page_size;
131        let total_size = used_offset + used_size;
132        let total_pages = (total_size + page_size - 1) / page_size;
133        let order = total_pages.next_power_of_two().trailing_zeros() as u8;
134
135        // Allocate physically-contiguous pages for the vring.
136        let frame = crate::sync::with_irqs_disabled(|token| {
137            crate::memory::allocate_phys_contiguous(token, order)
138        })
139        .map_err(|_| "Failed to allocate boot vring")?;
140
141        let vring_phys = frame.start_address.as_u64();
142        let vring_virt = crate::memory::phys_to_virt(vring_phys);
143
144        // Zero the whole region.
145        core::ptr::write_bytes(vring_virt as *mut u8, 0, total_size);
146
147        // Select queue 0.
148        core::ptr::write_volatile((device_base + REG_QUEUE_SEL) as *mut u32, 0);
149
150        // Write queue size.
151        core::ptr::write_volatile((device_base + REG_QUEUE_NUM) as *mut u32, queue_size as u32);
152
153        // Write queue alignment (legacy MMIO).
154        core::ptr::write_volatile(
155            (device_base + REG_QUEUE_ALIGN) as *mut u32,
156            page_size as u32,
157        );
158
159        // Write PFN = phys_addr >> 12.
160        let pfn = (vring_phys >> 12) as u32;
161        core::ptr::write_volatile((device_base + REG_QUEUE_PFN) as *mut u32, pfn);
162
163        Ok(Self {
164            vring_phys,
165            vring_virt,
166            queue_size,
167            avail_idx: 0,
168            last_used_idx: 0,
169        })
170    }
171
172    /// Submit a single-block read request and busy-poll for completion.
173    ///
174    /// Uses a 3-descriptor chain: header (device-read) | data (device-write) | status (device-write).
175    ///
176    /// # Safety
177    /// `device_base` must be a valid MMIO VirtIO device address.
178    unsafe fn read_block(
179        &mut self,
180        device_base: u64,
181        lba: u64,
182        bounce_phys: u64,
183        bounce_virt: u64,
184        buf: &mut [u8],
185    ) -> Result<(), ()> {
186        let qs = self.queue_size as usize;
187        let desc_off = 0;
188        let avail_off = ((qs * core::mem::size_of::<VringDesc>()) + 4095) / 4096 * 4096;
189        let used_off =
190            ((avail_off + core::mem::size_of::<VringAvail>() + qs * 2) + 4095) / 4096 * 4096;
191
192        let desc_ptr = (self.vring_virt + desc_off as u64) as *mut VringDesc;
193        let avail_ptr = (self.vring_virt + avail_off as u64) as *mut VringAvail;
194        let used_ptr = (self.vring_virt + used_off as u64) as *mut VringUsed;
195
196        // Write header into the bounce buffer (first 16 bytes).
197        let header_ptr = bounce_virt as *mut BootBlockHeader;
198        core::ptr::write(
199            header_ptr,
200            BootBlockHeader {
201                request_type: REQ_TYPE_IN,
202                reserved: 0,
203                sector: lba,
204            },
205        );
206
207        // Status byte at the end of the bounce buffer (past the data region).
208        let data_len = buf.len() as u32;
209        let status_off = data_len as u64; // status follows data in the same frame
210        let status_ptr = (bounce_virt + status_off) as *mut u8;
211        core::ptr::write(status_ptr, 0xFF);
212
213        // Descriptor 0: header (device-read, 16 bytes)
214        core::ptr::write(
215            desc_ptr,
216            VringDesc {
217                addr: bounce_phys,
218                len: core::mem::size_of::<BootBlockHeader>() as u32,
219                flags: VRING_DESC_F_NEXT,
220                next: 1,
221            },
222        );
223        // Descriptor 1: data (device-write)
224        core::ptr::write(
225            desc_ptr.add(1),
226            VringDesc {
227                addr: bounce_phys + core::mem::size_of::<BootBlockHeader>() as u64,
228                len: data_len,
229                flags: VRING_DESC_F_NEXT | VRING_DESC_F_WRITE,
230                next: 2,
231            },
232        );
233        // Descriptor 2: status (device-write, 1 byte)
234        core::ptr::write(
235            desc_ptr.add(2),
236            VringDesc {
237                addr: bounce_phys + status_off,
238                len: 1,
239                flags: VRING_DESC_F_WRITE,
240                next: 0,
241            },
242        );
243
244        // Place head index into the available ring.
245        let avail_ring_ptr = (self.vring_virt
246            + avail_off as u64
247            + core::mem::size_of::<VringAvail>() as u64) as *mut u16;
248        let avail_slot = (self.avail_idx as usize) % qs;
249        core::ptr::write(avail_ring_ptr.add(avail_slot), 0u16); // head descriptor index
250
251        // Update avail index with a write barrier.
252        core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
253        core::ptr::write(&mut (*avail_ptr).idx, self.avail_idx.wrapping_add(1));
254        self.avail_idx = self.avail_idx.wrapping_add(1);
255
256        // Notify the device.
257        core::ptr::write_volatile((device_base + REG_QUEUE_NOTIFY) as *mut u32, 0u32);
258
259        // Busy-poll for completion (boot context : no IRQs, no scheduler).
260        let mut spins = 0u32;
261        loop {
262            let used_idx = core::ptr::read_volatile(&(*used_ptr).idx);
263            if self.last_used_idx != used_idx {
264                let used_ring_ptr =
265                    (self.vring_virt + used_off as u64 + core::mem::size_of::<VringUsed>() as u64)
266                        as *mut VringUsedElem;
267                let slot = (self.last_used_idx as usize) % qs;
268                let _elem = core::ptr::read_volatile(used_ring_ptr.add(slot));
269                self.last_used_idx = self.last_used_idx.wrapping_add(1);
270                break;
271            }
272            spins = spins.saturating_add(1);
273            if spins >= 5_000_000 {
274                crate::serial_println!("[virtio-blk] boot read timeout lba={:#x}", lba);
275                return Err(());
276            }
277            core::hint::spin_loop();
278        }
279
280        // Check status.
281        let status = core::ptr::read(status_ptr);
282        if status != BootBlockStatus::Ok as u8 {
283            return Err(());
284        }
285
286        // Copy data from bounce buffer to final buffer.
287        let src = (bounce_virt + core::mem::size_of::<BootBlockHeader>() as u64) as *const u8;
288        core::ptr::copy_nonoverlapping(src, buf.as_mut_ptr(), data_len as usize);
289
290        Ok(())
291    }
292}
293
294impl Drop for BootVirtqueue {
295    fn drop(&mut self) {
296        // The vring frames are leaked for now (kernel lifetime).
297        // In a production kernel we would track the frame and free it.
298    }
299}
300
301/// VirtIO block device (boot-time MMIO transport)
302pub struct VirtioBlkDevice {
303    /// Base address of the device registers (MMIO BAR)
304    base_addr: u64,
305    /// Block size in bytes
306    block_size: u64,
307    /// Device features
308    features: u32,
309    /// Boot-time virtqueue
310    queue: Option<BootVirtqueue>,
311}
312
313impl VirtioBlkDevice {
314    /// Create a new VirtIO block device from MMIO base address.
315    ///
316    /// # Safety
317    ///
318    /// `base_addr` must point to valid VirtIO device registers.
319    pub unsafe fn new(base_addr: u64) -> Self {
320        Self {
321            base_addr,
322            block_size: 512, // Default, updated during init
323            features: 0,
324            queue: None,
325        }
326    }
327
328    /// Initialize the VirtIO block device.
329    ///
330    /// # Safety
331    ///
332    /// Must only be called once with a valid device address.
333    pub unsafe fn init(&mut self) -> Result<(), &'static str> {
334        crate::serial_println!("[virtio-blk] Initializing at {:#x}", self.base_addr);
335
336        // Reset device
337        let status = self.read_status();
338        crate::serial_println!("[virtio-blk] Device status: {:#x}", status);
339        self.write_status(0);
340
341        // Acknowledge + DRIVER
342        self.add_status(STATUS_ACK);
343        self.add_status(STATUS_DRIVER);
344
345        // Read device features
346        self.features = self.read_device_features();
347        crate::serial_println!("[virtio-blk] Features: {:#x}", self.features);
348
349        // We do not need special features; write 0 (but negotiate version 1 if available).
350        // For now, pass-through: accept device defaults.
351        // VIRTIO_F_VERSION_1 (bit 32) would require reading upper 32-bit half.
352        // Legacy MMIO works either way.
353        self.write_driver_features(0);
354
355        // FEATURES_OK
356        self.add_status(STATUS_FEATURES_OK);
357        if self.read_status() & STATUS_FEATURES_OK == 0 {
358            return Err("Device rejected feature negotiation");
359        }
360
361        // Read capacity from device config.
362        let capacity_lo = core::ptr::read_volatile((self.base_addr + REG_CAPACITY) as *const u32);
363        let capacity_hi =
364            core::ptr::read_volatile((self.base_addr + REG_CAPACITY + 4) as *const u32);
365        let capacity = ((capacity_hi as u64) << 32) | (capacity_lo as u64);
366        crate::serial_println!("[virtio-blk] Capacity: {} blocks", capacity);
367
368        // Set up the boot virtqueue (must be before DRIVER_OK per spec).
369        let queue = BootVirtqueue::init(self.base_addr, BOOT_QUEUE_SIZE)?;
370        self.queue = Some(queue);
371
372        // DRIVER_OK
373        self.add_status(STATUS_DRIVER_OK);
374
375        crate::serial_println!("[virtio-blk] Initialized successfully");
376        Ok(())
377    }
378
379    unsafe fn read_status(&self) -> u32 {
380        core::ptr::read_volatile((self.base_addr + REG_STATUS) as *const u32)
381    }
382
383    unsafe fn write_status(&self, val: u32) {
384        core::ptr::write_volatile((self.base_addr + REG_STATUS) as *mut u32, val);
385    }
386
387    unsafe fn add_status(&self, bit: u32) {
388        let s = self.read_status();
389        self.write_status(s | bit);
390    }
391
392    unsafe fn read_device_features(&self) -> u32 {
393        core::ptr::read_volatile((self.base_addr + REG_DEVICE_FEATURES) as *const u32)
394    }
395
396    unsafe fn write_driver_features(&self, features: u32) {
397        core::ptr::write_volatile((self.base_addr + REG_DRIVER_FEATURES) as *mut u32, features);
398    }
399
400    /// Create a VirtIO block device from a Device Tree node.
401    ///
402    /// # Safety
403    ///
404    /// `dtb_ptr` must point to a valid flattened device tree.
405    pub unsafe fn from_dtb(dtb_ptr: u64) -> Option<Self> {
406        // TODO: Parse DTB to find virtio-blk device address
407        // For now, use a hardcoded address for QEMU x86_64
408        #[cfg(target_arch = "x86_64")]
409        let base_addr = 0xfebf0000u64; // Typical QEMU virtio-blk MMIO address
410
411        #[cfg(not(target_arch = "x86_64"))]
412        let base_addr = {
413            crate::serial_println!("[virtio-blk] DTB parsing not implemented for this arch");
414            return None;
415        };
416
417        let mut device = VirtioBlkDevice::new(base_addr);
418        if device.init().is_err() {
419            return None;
420        }
421        Some(device)
422    }
423}
424
425impl BlockDevice for VirtioBlkDevice {
426    fn read_block(&mut self, lba: u64, buf: &mut [u8]) -> Result<(), ()> {
427        let queue = self.queue.as_mut().ok_or(())?;
428        let block_sz = self.block_size as usize;
429
430        // Allocate a bounce frame (header + data + status fit in one 4K page).
431        let bounce_frame =
432            crate::sync::with_irqs_disabled(|token| crate::memory::allocate_frame(token))
433                .map_err(|_| ())?;
434        let bounce_phys = bounce_frame.start_address.as_u64();
435        let bounce_virt = crate::memory::phys_to_virt(bounce_phys);
436
437        let result =
438            unsafe { queue.read_block(self.base_addr, lba, bounce_phys, bounce_virt, buf) };
439
440        crate::sync::with_irqs_disabled(|token| {
441            crate::memory::free_frame(token, bounce_frame);
442        });
443
444        result
445    }
446
447    fn block_size(&self) -> u64 {
448        self.block_size
449    }
450}