// 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"; /// The original format: magic, version, curve, then records until zero padding. const VERSION_V1: u8 = 1; /// The current format: the same, plus the image's byte extent and record count. const VERSION: u8 = 2; const HEADER_LEN_V1: usize = 6; const HEADER_LEN_V2: usize = 6 + 8 + 8; 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()) }; // Less than the shortest header is not an image at all; `digest` reports the rest. if result.is_ok() && image.len() < HEADER_LEN_V1 { 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. /// What a header says, in both formats. struct Header { version: u8, curve: u8, image_bytes: Option, record_count: Option, } /// Read the header, or say what is wrong with it. fn parse_header(image: &[u8]) -> Result { if image.len() < 5 || &image[0..4] != MAGIC { return Err("not-a-store"); } let version = image[4]; let curve = image[5]; match version { VERSION_V1 => Ok(Header { version, curve, image_bytes: None, record_count: None, }), VERSION => { if image.len() < HEADER_LEN_V2 { return Err("short-v2-header"); } let mut word = [0u8; 8]; word.copy_from_slice(&image[6..14]); let image_bytes = u64::from_le_bytes(word); word.copy_from_slice(&image[14..22]); let record_count = u64::from_le_bytes(word); // A header that does not cover itself is corrupt, and guessing at the extent // of an image means possibly reading somebody else's bytes. if (image_bytes as usize) < HEADER_LEN_V2 { return Err("bad-extent"); } Ok(Header { version, curve, image_bytes: Some(image_bytes), record_count: Some(record_count), }) } _ => Err("unsupported-version"), } } fn digest(image: &[u8]) -> Line { let mut line = Line::new(); let header = match parse_header(image) { Ok(h) => h, Err(what) => { let _ = write!(line, "error={what}"); return line; } }; let end = match header.image_bytes { Some(n) => core::cmp::min(n as usize, image.len()), None => image.len(), }; let mut h = FNV_OFFSET; let mut count: u64 = 0; let mut value_bytes: u64 = 0; let mut errors: u64 = 0; let mut off = if header.version == VERSION_V1 { HEADER_LEN_V1 } else { HEADER_LEN_V2 }; let mut remaining = header.record_count; while off + RECORD_FIXED <= end { if remaining == Some(0) { break; } let frame = &image[off..off + RECORD_FIXED]; // Padding is not a record — but only if it is padding all the way down, and only // in v1, where nothing else says where the records stop. A v2 image states its // count, so a zero frame inside that count is a record like any other. if remaining.is_none() && 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; // A promised record that is not there is a truncated image, and saying so beats // returning a shorter list that looks complete. if value_at + value_len > end { 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; if let Some(n) = remaining.as_mut() { *n -= 1; } } // The count was a promise; not meeting it is an error the caller must see. if let Some(n) = remaining { if n > 0 { errors += 1; } } // Field order matches cube-image's `digest` line so the two diff directly. let _ = write!( line, "digest version={} curve={} bytes={} records={} value_bytes={} fnv1a64={:016x} errors={}", header.version, header.curve, image.len(), count, value_bytes, h, errors ); 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()) } }