CUBELinux.0.2: the kernel reads the CUBE store off a block device

The first CUBE code in the kernel, and deliberately only a reader: the write
authority has not moved yet, and PLAN-kernel-cubelinux.md records both that
decision and the hazard that makes the order matter — a kernel writing while a
userspace daemon still holds the same image loses one of the two writers' work,
silently. A reader cannot do that.

- drivers/cube/: a Rust module exposing /dev/cubelinux. Reading it reads the
  pinned image from the block device through the kernel's own file layer
  (filp_open + kernel_read — the path this kernel version binds for Rust, and
  the reason no C helper was needed), parses the records, and returns one line:

    digest curve=0 bytes=8400896 records=35318 value_bytes=6139148 fnv1a64=5e20f98455387b08 errors=0

  The work happens on read, not at init, so there is no initcall ordering to get
  wrong against the block driver that provides the device.

- The format is restated in the kernel (32-byte space, 24-byte key, 8-byte LE
  length, value), including the two rules the userspace parser documents: a value
  that runs past the buffer is a truncated record, and an all-zero frame ends the
  records only when every remaining byte is zero — the rule that keeps a real
  record at the origin from being read as padding.

- The digest is the point. A record count alone lets two different images agree;
  folding the bytes in means the kernel and userspace are *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.

Gate, on both images:
  curated  11 records,      4,096 bytes, fnv1a64=161113085b1573b2  — match
  snapshot 35,318 records, 8,400,896 bytes, fnv1a64=5e20f98455387b08 — match

The tree carries CONFIG_CUBELINUX_STORE=y on top of defconfig + RUST; a tree
without it boots and simply has no /dev/cubelinux.
This commit is contained in:
CUBELinux build
2026-09-18 20:22:02 -04:00
parent e4ae67ea7f
commit cee3e554d9
6 changed files with 328 additions and 1 deletions
+1 -1
View File
@@ -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"
+2
View File
@@ -39,6 +39,8 @@ source "drivers/parport/Kconfig"
source "drivers/pnp/Kconfig"
source "drivers/cube/Kconfig"
source "drivers/block/Kconfig"
source "drivers/nvme/Kconfig"
+3
View File
@@ -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/
+19
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CUBELINUX_STORE) += cubelinux_store.o
+301
View File
@@ -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 <image>` 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<u8>) -> 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::<u8>(), 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::<u8>::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::<core::ffi::c_void>(),
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<Line> {
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<CubeStore>,
}
impl kernel::InPlaceModule for CubeStoreModule {
fn init(_module: &'static ThisModule) -> impl PinInit<Self, Error> {
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<Device>,
}
#[vtable]
impl MiscDevice for CubeStore {
type Ptr = Pin<KBox<Self>>;
fn open(_file: &File, misc: &MiscDeviceRegistration<Self>) -> Result<Pin<KBox<Self>>> {
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<usize> {
// 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::<u8>::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("<bad utf8>"));
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())
}
}