|
|
|
@@ -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())
|
|
|
|
|
}
|
|
|
|
|
}
|