diff --git a/Makefile b/Makefile index 3a5b7dcd9..37ec68cba 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ NAME = CUBELinux # release. The base version stays in VERSION/PATCHLEVEL/SUBLEVEL above (visible in # `make kernelversion`), while `uname -r` and /lib/modules report the CUBELinux # release, with any -dirty or SCM suffix still appended by setlocalversion. -CUBELINUX_VERSION = CUBELinux.0.1 +CUBELINUX_VERSION = CUBELinux.0.2 # *DOCUMENTATION* # To see a list of typical targets execute "make help" diff --git a/drivers/Kconfig b/drivers/Kconfig index c0f1fb893..71ae77d96 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -39,6 +39,8 @@ source "drivers/parport/Kconfig" source "drivers/pnp/Kconfig" +source "drivers/cube/Kconfig" + source "drivers/block/Kconfig" source "drivers/nvme/Kconfig" diff --git a/drivers/Makefile b/drivers/Makefile index ccc05f1ea..7e5c269d3 100644 --- a/drivers/Makefile +++ b/drivers/Makefile @@ -199,3 +199,6 @@ obj-y += resctrl/ obj-$(CONFIG_DIBS) += dibs/ obj-$(CONFIG_S390) += s390/ + +# CUBELinux: the coordinate store, in the kernel +obj-$(CONFIG_CUBELINUX_STORE) += cube/ diff --git a/drivers/cube/Kconfig b/drivers/cube/Kconfig new file mode 100644 index 000000000..1ad6b45d0 --- /dev/null +++ b/drivers/cube/Kconfig @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# CUBELinux: the coordinate store, in the kernel. + +config CUBELINUX_STORE + bool "CUBELinux store reader" + depends on RUST && BLOCK + help + Read the CUBELinux store image from a block device and report what it + holds: record count, value bytes, and a digest over every record. + + This is the first CUBE code in the kernel, and it is deliberately a + reader. The store's write authority is a separate decision with its own + consequences (see PLAN-kernel-cubelinux.md): until it is taken, the + kernel must not touch a store that a userspace daemon may also hold. + + Say Y to expose /dev/cubelinux. Reading it performs the read and returns + one line, in the same field order the userspace tool prints, so the two + can be compared directly. diff --git a/drivers/cube/Makefile b/drivers/cube/Makefile new file mode 100644 index 000000000..adbe17385 --- /dev/null +++ b/drivers/cube/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_CUBELINUX_STORE) += cubelinux_store.o diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs new file mode 100644 index 000000000..62ddd56d4 --- /dev/null +++ b/drivers/cube/cubelinux_store.rs @@ -0,0 +1,301 @@ +// 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()) + } +}