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:
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
obj-$(CONFIG_CUBELINUX_STORE) += cubelinux_store.o
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user