// SPDX-License-Identifier: GPL-2.0 //! CUBELinux store reader — the kernel reading CUBE coordinates off a block device. //! //! The store is addressed by coordinate, and the pinned image's layout is: //! //! ```text //! [MAGIC "CUBE" 4][version 1][curve tag] //! records: [SpaceId 32][Key 24][value length u64 LE][value] //! ``` //! //! This module reads that image from a block device and reports what it holds: the record //! count, the total value bytes, and an FNV-1a digest over every `(space, key, length, //! value)` in order. The digest exists so the kernel and userspace can be *compared* //! rather than assumed to agree — `cube-image digest ` prints the same line in the //! same field order, and the QEMU gate fails if they differ by a byte. //! //! # Why a reader, and only a reader //! //! The store's write authority has not moved yet. `PLAN-kernel-cubelinux.md` records the //! decision (the kernel owns the store) and the hazard that makes the order matter: a //! kernel that writes while a userspace daemon still holds the same image in memory loses //! one of the two writers' work, silently. Until that is settled, a reader is the correct //! amount of authority for the kernel to hold. //! //! # Interface //! //! `/dev/cubelinux` — reading it performs the read (through the kernel's own file layer) //! and returns one line. Nothing happens at module init, so there is no ordering to get //! wrong against the block driver that provides the device. //! //! ```text //! digest curve=0 bytes=1141 records=11 value_bytes=431 fnv1a64=161113085b1573b2 errors=0 //! ``` use core::fmt::{self, Write}; use kernel::{ bindings, c_str, device::Device, fs::{File, Kiocb}, iov::IovIterDest, miscdevice::{MiscDevice, MiscDeviceOptions, MiscDeviceRegistration}, prelude::*, sync::aref::ARef, }; module! { type: CubeStoreModule, name: "cubelinux_store", authors: ["CUBE OS"], description: "CUBELinux store reader (coordinates in the kernel)", license: "GPL", } /// The layout, restated here because the kernel cannot depend on the userspace crates. /// `crates/cube-store-raw` is the source of truth; the digest comparison is what keeps /// this copy honest. const MAGIC: &[u8; 4] = b"CUBE"; const VERSION: u8 = 1; const HEADER_LEN: usize = 6; const SPACE_ID_LEN: usize = 32; const RAW_KEY_LEN: usize = 24; const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8; /// The device to read. A module parameter is the obvious next step; for the boot gate one /// fixed name is honest and has one less way to be wrong. const STORE_DEVICE: &core::ffi::CStr = c_str!("/dev/vda"); /// Refuse to pull an unbounded device into memory. The gate's images are tiny; this is the /// guard against a wrong device name turning a read into an allocation storm. const MAX_BYTES: usize = 64 * 1024 * 1024; /// FNV-1a offset basis. const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; /// A line of output, built in place. No allocation: the read path formats into a fixed /// buffer, so it cannot fail for want of memory while holding a file open. struct Line { buf: [u8; 256], len: usize, } impl Line { fn new() -> Self { Line { buf: [0; 256], len: 0, } } fn as_bytes(&self) -> &[u8] { &self.buf[..self.len] } } impl Write for Line { fn write_str(&mut self, s: &str) -> fmt::Result { let bytes = s.as_bytes(); if self.len + bytes.len() > self.buf.len() { return Err(fmt::Error); } self.buf[self.len..self.len + bytes.len()].copy_from_slice(bytes); self.len += bytes.len(); Ok(()) } } /// FNV-1a 64 over a run of bytes, continuing from `h`. fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 { for b in bytes { h ^= *b as u64; h = h.wrapping_mul(0x0000_0100_0000_01b3); } h } /// Read the whole store image through the kernel's file layer. /// /// `kernel_read` on the block device rather than a raw bio: it is the path this kernel /// version exposes to Rust, it goes through the page cache the way any other read does, and /// it needs no C helper. If the store ever has to be read before the VFS is up (a root /// filesystem, say), that is the moment to reach for the block layer directly. fn read_image(image: &mut KVVec) -> Result<()> { // O_RDONLY is 0 in Linux; filp_open takes the raw flags word. // SAFETY: STORE_DEVICE is a NUL-terminated C string literal, and filp_open either // returns a valid `struct file *` or an error pointer, which is checked below. let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::(), 0, 0) }; let file = kernel::error::from_err_ptr(file)?; if file.is_null() { return Err(EINVAL); } let mut pos: bindings::loff_t = 0; let mut chunk = KVVec::::with_capacity(4096, GFP_KERNEL)?; chunk.extend_from_slice(&[0u8; 4096][..], GFP_KERNEL)?; let mut result = Ok(()); loop { // SAFETY: `file` is a live `struct file *` from filp_open; `chunk` is a 4096-byte // kernel buffer we own; `pos` is a valid loff_t. kernel_read reads at most // `chunk.len()` bytes into the buffer and does not retain either pointer. let n = unsafe { bindings::kernel_read( file, chunk.as_mut_ptr().cast::(), chunk.len(), &mut pos, ) }; if n < 0 { result = Err(Error::from_errno(n as i32)); break; } if n == 0 { break; // end of device } let n = n as usize; if let Err(e) = image.extend_from_slice(&chunk[..n], GFP_KERNEL) { result = Err(e.into()); break; } if image.len() >= MAX_BYTES { break; } } // SAFETY: `file` came from filp_open and has not been closed; the owner argument is // only meaningful for locks that no one holds here. unsafe { bindings::filp_close(file, core::ptr::null_mut()) }; if result.is_ok() && image.len() < HEADER_LEN { return Err(EINVAL); } result } /// Walk the records of a store image, digesting each one, exactly as /// `cube-store-raw::iter_records` + `cube-image digest` do. /// /// The two rules that are easy to get wrong, both from the userspace parser: a record whose /// value runs past the buffer is a truncated record and an error, and an all-zero frame is /// *end of records* only when every remaining byte is also zero — otherwise a real record /// at the origin, followed by padding, would be read as an empty one. fn digest(image: &[u8]) -> Result { let mut line = Line::new(); if image.len() < HEADER_LEN || &image[0..4] != MAGIC { let _ = write!(line, "error=not-a-store"); return Ok(line); } let curve = image[5]; if image[4] != VERSION { let _ = write!(line, "error=version-{}", image[4]); return Ok(line); } let mut h = FNV_OFFSET; let mut count: u64 = 0; let mut value_bytes: u64 = 0; let mut errors: u64 = 0; let mut off = HEADER_LEN; while off + RECORD_FIXED <= image.len() { let frame = &image[off..off + RECORD_FIXED]; // Padding is not a record — but only if it is padding all the way down. if frame.iter().all(|b| *b == 0) && image[off..].iter().all(|b| *b == 0) { break; } let space = &image[off..off + SPACE_ID_LEN]; let key = &image[off + SPACE_ID_LEN..off + SPACE_ID_LEN + RAW_KEY_LEN]; let mut len_bytes = [0u8; 8]; len_bytes.copy_from_slice(&image[off + SPACE_ID_LEN + RAW_KEY_LEN..off + RECORD_FIXED]); let value_len = u64::from_le_bytes(len_bytes) as usize; let value_at = off + RECORD_FIXED; if value_at + value_len > image.len() { errors += 1; break; } let value = &image[value_at..value_at + value_len]; h = fnv1a64(space, h); h = fnv1a64(key, h); h = fnv1a64(&(value_len as u64).to_le_bytes(), h); h = fnv1a64(value, h); count += 1; value_bytes += value_len as u64; off = value_at + value_len; } // Field order matches cube-image's `digest` line so the two diff directly. let _ = write!( line, "digest curve={} bytes={} records={} value_bytes={} fnv1a64={:016x} errors={}", curve, image.len(), count, value_bytes, h, errors ); Ok(line) } /// The module's registration; holds the misc device for as long as the module lives. #[pin_data] struct CubeStoreModule { #[pin] _miscdev: MiscDeviceRegistration, } impl kernel::InPlaceModule for CubeStoreModule { fn init(_module: &'static ThisModule) -> impl PinInit { pr_info!("cubelinux: store reader registered at /dev/cubelinux\n"); try_pin_init!(Self { _miscdev <- MiscDeviceRegistration::register(MiscDeviceOptions { name: c_str!("cubelinux"), }), }) } } #[pin_data] struct CubeStore { dev: ARef, } #[vtable] impl MiscDevice for CubeStore { type Ptr = Pin>; fn open(_file: &File, misc: &MiscDeviceRegistration) -> Result>> { KBox::try_pin_init( try_pin_init! { CubeStore { dev: ARef::from(misc.device()) } }, GFP_KERNEL, ) } fn read_iter(mut kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterDest<'_>) -> Result { // The work happens here, on demand, rather than at init: by the time anything can // open this device, the block driver that provides the store is certainly up. let mut image = KVVec::::new(); let line = match read_image(&mut image) { Ok(()) => digest(&image)?, Err(e) => { let mut line = Line::new(); let _ = write!(line, "error={:?}", e); line } }; pr_info!("cubelinux: {}\n", core::str::from_utf8(line.as_bytes()).unwrap_or("")); let me = kiocb.file(); dev_info!(me.dev, "cubelinux: store read complete\n"); iov.simple_read_from_buffer(kiocb.ki_pos_mut(), line.as_bytes()) } }