Files
cubelinux-kernel/drivers/cube/cubelinux_store.rs
T
surface-camera-build 72e72fe7a8 cubelinux: an addressed image is readable without a log
merged_digest answered "no log" before it looked at the layout, so a bare
v3/v4 image fell through to the packed walk — which starts at the v2 header
length and reads index entries as record frames. A bare addressed image with
no log beside it therefore read as a one-record store with an empty value.

An addressed image is a complete store on its own: its index holds every
record and says where each one is, so a log is an addition rather than a
requirement. A packed image is not, which is why "no log" still means what it
meant for v1/v2.

Found by verify-image-read the moment userspace started writing v4: while
every userspace image was v2 the fallback happened to be the right walk. The
kernel's own v4 images always had a log header beside them, so no other gate
could have caught it.
2026-09-22 01:02:39 -04:00

4324 lines
171 KiB
Rust

// 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]
//! ```
//!
//! A store on a device is that image followed by a **write-ahead log** — the mutations since
//! the last checkpoint, in the same `op | crc32 | space | key | len | value` framing
//! `cube-store/src/wal.rs` writes. The log is a delta: the image is authoritative and
//! self-contained, and a reader that ignores the log loses only the mutations recorded after
//! the last checkpoint. `DESIGN-cubelinux-write-path.md` is why the write path looks like
//! this at all.
//!
//! This module reads both and reports the store they *describe* — the image with the log
//! applied: the record count, the total value bytes, and an FNV-1a digest over every
//! `(space, key, length, value)` in the order a checkpoint would write them. 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 core::sync::atomic::{AtomicBool, Ordering};
// The store's format, in one file, shared with userspace.
//
// `crates/cube-format` includes this same path, so the layout, its constants and the arithmetic a
// reader derives from them have one description rather than one per build system. What is not here
// is I/O or allocation: this driver reads with its own buffers, and userspace already has the whole
// image. The constants below are aliases of the shared ones, and the two functions that *validate*
// a header delegate to it — validation is where a second description would be read as truth.
#[path = "cube_format.rs"]
#[allow(dead_code)] // the two builds use different subsets of one API
mod cube_format;
use kernel::{
alloc::AllocError,
bindings, c_str,
device::Device,
fs::{File, Kiocb},
iov::{IovIterDest, IovIterSource},
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] = cube_format::MAGIC;
/// The original format: magic, version, curve, then records until zero padding.
const VERSION_V1: u8 = cube_format::VERSION_V1;
/// The packed format: the same, plus the image's byte extent and record count.
/// The addressed format: the same, plus a fixed-size index, a space table, and packed values.
///
/// v2 is a packed list — `space | key | len | value`, repeated — so record N's offset is the sum of
/// every record before it. A coordinate gives the key, and the key gives ORDER, and order is not an
/// address: no coordinate can be turned into a place, and not even a binary search is possible,
/// because the middle record's offset cannot be computed either. Every lookup, therefore, walks.
///
/// v3 puts the addresses in the image:
///
/// ```text
/// [header 46] magic, version, curve, extents, counts
/// [space table: space_count x 48] space | first index | records
/// [index: record_count x 40] key | value offset | value length
/// [values: packed, in index order]
/// ```
///
/// The index is fixed-size and sorted by key, so a lookup is a binary search over arithmetic
/// addresses (`index_off + i * 40`), a listing starts at its space's first index entry and streams,
/// asking which spaces exist is the space table, and a spatial range is a contiguous run of index
/// entries. An index entry is 40 bytes against v2's 64-byte frame, so the image also gets smaller.
const VERSION_V3: u8 = cube_format::VERSION_V3;
/// v3, plus a 16-bit class mask in each index entry.
const VERSION_V4: u8 = cube_format::VERSION_V4;
const HEADER_LEN_V1: usize = cube_format::HEADER_LEN_V1;
const HEADER_LEN_V2: usize = cube_format::HEADER_LEN_V2;
/// magic(4) version(1) curve(1) image_bytes(8) record_count(8) space_count(8) index_off(8)
/// values_off(8).
const HEADER_LEN_V3: usize = cube_format::HEADER_LEN_V3;
/// `key(24) | value_off(8) | value_len(8)`.
const INDEX_ENTRY: usize = cube_format::INDEX_ENTRY;
/// `key(24) | flags(2) | value_off(8) | value_len(8)`.
const INDEX_ENTRY_V4: usize = cube_format::INDEX_ENTRY_V4;
const FLAGS_LEN: usize = cube_format::FLAGS_LEN;
/// `space(32) | first index(8) | records(8)`.
const SPACE_ENTRY: usize = cube_format::SPACE_ENTRY;
const SPACE_ID_LEN: usize = cube_format::SPACE_ID_LEN;
const RAW_KEY_LEN: usize = cube_format::RAW_KEY_LEN;
const RECORD_FIXED: usize = cube_format::RECORD_FIXED;
// The device the store lives on, as the `cube_store=` boot parameter resolved it.
//
// This was a constant — *"for the boot gate one fixed name is honest and has one less way to
// be wrong"* — and that was true while a virtual machine was the only place this driver ran.
// It runs on the box now, the box's device is not `/dev/vda`, and a constant cannot be both.
// So the box names its device on the kernel command line:
//
// cube_store=/dev/nvme0n1p2
//
// The parameter is declared in C (`cube_syscall.c`) because this kernel's Rust can express only
// *integer* module parameters — `rust/kernel/module_param.rs` implements `ModuleParam` through
// `ParseInt` and nothing else — and a path is not an integer. It is a `__setup` parameter rather
// than a module parameter for the naming reason recorded in that file.
//
// (A `///` here would be an unused doc comment: it would attach to the `extern` block, which
// documents nothing.)
extern "C" {
/// Returns a pointer to a static, NUL-terminated buffer holding the path.
fn cubelinux_store_device() -> *const core::ffi::c_char;
/// Whether the kernel was asked to record its own boots (`cube_boot_record=1`).
///
/// Parsed in C for the same reason the device path is: this kernel's Rust can express only
/// integer module parameters, and a command-line flag beside `cube_store=` belongs where that
/// parameter lives.
fn cubelinux_boot_record_enabled() -> bool;
}
/// The store's path, NUL-terminated, owned by the C side for the life of the kernel.
fn store_device() -> *const u8 {
// SAFETY: `cubelinux_store_device` returns a pointer to a static buffer that the module
// parameter filled during boot and that nothing writes afterwards, so it stays valid and
// NUL-terminated for as long as this runs.
unsafe { cubelinux_store_device().cast::<u8>() }
}
/// Refuse to pull an unbounded device into memory: the guard against a wrong device name
/// turning a read into an allocation storm. It is sized for a *store device*, not for an image.
///
/// `Control::layout` gives each of the two slots a quarter of the device, so a device must be at
/// least four times the image it holds — and a *sealed* image is larger than the plaintext one,
/// because every record gains an envelope. A sealed 18.8 MiB store therefore needs a ~76 MiB
/// device, which the original 64 MiB cap refused. 128 MiB leaves room for both to grow while
/// still being a bound: pointing this at a whole 119 GiB disk is still refused.
const MAX_BYTES: usize = 128 * 1024 * 1024;
/// The log's own magic, distinct from the image's so a reader that opens the wrong one
/// cannot mistake it for the other.
const WAL_MAGIC: [u8; 4] = *cube_format::WAL_MAGIC;
const WAL_VERSION: u8 = cube_format::WAL_VERSION;
/// The flagged log entry: the same, with a two-byte class mask before the length.
const WAL_VERSION_V2: u8 = cube_format::WAL_VERSION_V2;
/// Log header: magic(4) + version(1) + curve tag(1).
const WAL_HEADER_LEN: usize = cube_format::WAL_HEADER_LEN;
/// Log entry, before the value: op(1) + crc32(4) + space(32) + key(24) + len(4).
const ENTRY_FIXED: usize = cube_format::ENTRY_FIXED;
/// The flagged entry: the same, with `flags(2)` before the length.
const ENTRY_FIXED_V2: usize = cube_format::ENTRY_FIXED_V2;
/// One log entry's fixed size, and the offset of its length, for a log version. Version 1 has no
/// class mask; version 2 carries a two-byte one before the length.
fn wal_frame(version: u8) -> (usize, usize) {
if version == WAL_VERSION_V2 {
(ENTRY_FIXED_V2, RAW_KEY_LEN + FLAGS_LEN + SPACE_ID_LEN + 5)
} else {
(ENTRY_FIXED, 61)
}
}
/// The log begins at the first 4 KiB boundary at or after the image. Fixed by geometry so
/// no superblock is needed to find it, and stated by the v2 header's extent.
///
/// That is the *bare* layout: an image on a device with a log after it. A device that is
/// meant to be checkpointed has a control block instead (see `Control`), because folding a
/// log into the only copy of an image is not atomic and a crash in the middle of it loses the
/// store.
const LOG_ALIGN: usize = 4096;
/// Control block magic, distinct from the image's `CUBE` and the log's `CUBW`.
const CTL_MAGIC: [u8; 4] = *b"CUBS";
const CTL_VERSION: u8 = 1;
/// One control copy, and where the two live.
const CTL_COPY_LEN: usize = 2048;
const CTL_COPY_A: usize = 0;
const CTL_COPY_B: usize = 2048;
/// Where the two image slots begin.
const CTL_DATA_OFF: u64 = 4096;
/// Bytes of a control copy covered by its checksum.
const CTL_SUMMED: usize = 44;
/// The one mutation this device accepts so far. The coordinate interface's opcodes will be
/// the language's verbs; this is the first of them.
const OP_PUT: u8 = 1;
/// Fold the log into the image.
const OP_SYNC: u8 = 3;
// ── The kernel's record of its own boot ────────────────────────────────────────────────
//
// "The OS stores itself" has two halves. The store is the kernel's, which is the first half; the
// second is the kernel *recording* something of its own rather than a client doing it. This is that
// record: at the first write of a boot the kernel appends one entry describing the boot it is
// having — its own version banner, the wall-clock time, and the device it resolved — to a reserved
// space, through the same append path every other mutation uses.
//
// WHERE IT HOOKS, AND WHY NOT AT INIT. There is no store open at module init, deliberately: the
// driver does nothing until it is asked, so there is no ordering to get wrong against the block
// driver that provides the device. An `__initcall` appending a record would reintroduce exactly that
// ordering problem, so the hook is not init — it is the first *write*. That is also the moment the
// claim is about: the kernel records itself when it becomes the writer. A boot in which the kernel
// only ever reads writes no record, which is honest rather than a gap.
//
// WHY IT IS OFF BY DEFAULT. Recording unconditionally would put a record into every store the
// kernel writes, and the gates' method is that the store the kernel produces is comparable, byte for
// byte, with the store userspace produces from the same mutations. A kernel-injected record the
// caller never asked for would make two of those comparisons stop being comparisons — so the feature
// is enabled by `cube_boot_record=1` on the kernel command line (`cube_syscall.c`), the gate turns it
// on, and the default keeps the invariant the gates rest on.
/// The space the kernel records its own boots in. Reserved, and reserved without a name: the kernel
/// keeps no table of space names, because a name is a userspace convention.
///
/// `0xFC` because the repeated-byte tags below it are taken, and taken by things this must not
/// collide with — which is worth spelling out, because the first choice here was `0xFD` and that is
/// the OS keystore, the space holding key material that the kill switch exists to destroy. Writing
/// boot records into it would have been a serious bug, and only a check of the userspace table
/// (`format_space` in `crates/cube-command`) catches it: the kernel has no name table to consult.
///
/// ```text
/// 0x00 root the data space, what an unqualified coordinate means
/// 0xFF edges association edges (`cube-header::NULL_SPACE`)
/// 0xFE portal portal descriptors (`cube-index::PORTAL_SPACE`)
/// 0xFD keystore the OS keystore (`cube-crypt::KEYSTORE_SPACE`)
/// 0xFC boot this — the kernel's record of its own boots
/// ```
const BOOT_SPACE: [u8; SPACE_ID_LEN] = [0xFC; SPACE_ID_LEN];
/// The class mask the boot record stamps: `EventFlags::BOOT`, bit 7. The kernel cannot link
/// `cube-core`, so the bit is spelled out here and on the userspace side, and the agreement is held
/// by `verify-boot-record` — the same shape as the `0xFC` space id, for the same reason.
const BOOT_FLAGS: u16 = 1 << 7;
/// The coordinate the record lives at. One record, the current boot: each boot overwrites the last,
/// which is the same shape as the userspace boot marker it replaces and is what "which boot is this"
/// needs. History would be a second record per boot, and nobody has asked for one.
const BOOT_POINT: (u64, u64, u64) = (0, 0, 0);
/// Whether this boot has already been recorded. One attempt, claimed with a swap so two callers
/// racing into their first write cannot write two records.
static BOOT_RECORDED: AtomicBool = AtomicBool::new(false);
/// FNV-1a offset basis.
const FNV_OFFSET: u64 = cube_format::FNV_OFFSET;
/// 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.
/// The store device, opened once and kept for the whole boot.
///
/// Every operation used to open the device, read it, and close it again — the open is the dominant
/// cost of a `cube(2)` call, and it was paid on every single one. The path is fixed at boot
/// (`cube_store=` is parsed once), so there is nothing to invalidate: the handle is opened lazily
/// on first use and reused.
///
/// Lazy rather than at module init, for the reason the driver does nothing at init: the block
/// driver that provides the device may not be up yet, and an open at init would reintroduce the
/// ordering problem the driver's own doc says it avoided. A caller whose filesystem view cannot
/// reach the path (a chroot before its `/dev` is mounted) gets the error that open returned; every
/// later caller gets the cached handle regardless of its own root.
/// A cached store-device file. The raw pointer is kept for the whole boot — the path is fixed at
/// boot (`cube_store=` is parsed once), so there is nothing to invalidate, and the device is meant
/// to stay open. The wrapper carries the `Send`/`Sync` the raw pointer lacks, and the global lock
/// serializes access, so no two threads touch the pointer unsynchronized.
struct StoreFile(*mut bindings::file);
// SAFETY: the pointer is valid for the whole boot (the path is fixed and the block driver does not
// unbind), and every use is through the global lock below.
unsafe impl Send for StoreFile {}
unsafe impl Sync for StoreFile {}
kernel::sync::global_lock! {
/// SAFETY: Initialized (to None) before first use.
unsafe(uninit) static STORE_FILE: Mutex<Option<StoreFile>> = None;
}
/// The store device's file, opened on first use and cached for the boot. O_RDWR, because the one
/// handle serves both the reads and the log-append / checkpoint writes.
fn store_file() -> Result<*mut bindings::file> {
let mut guard = STORE_FILE.lock();
if let Some(file) = guard.as_ref() {
return Ok(file.0);
}
// O_RDWR is 2. SAFETY: store_device() is a NUL-terminated C string the module parameter filled
// at boot; filp_open returns a valid `struct file *` or an error pointer, checked below.
let filp = unsafe { bindings::filp_open(store_device(), 2, 0) };
let filp = kernel::error::from_err_ptr(filp)?;
if filp.is_null() {
return Err(EINVAL);
}
*guard = Some(StoreFile(filp));
Ok(filp)
}
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 that the module parameter filled at
// boot, and filp_open either returns a valid `struct file *` or an error pointer, which is
// checked below.
let file = store_file()?;
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;
}
}
// 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 every format, is the shared file's: `version`, `curve`, the image's
// extent, and how many records follow. Same fields, one description.
use cube_format::Header;
/// Read the header, or say what is wrong with it.
///
/// The shared file decides what a header means — that is the point of it being shared — and this
/// only translates a refusal into the words this driver logs.
fn parse_header(image: &[u8]) -> Result<Header, &'static str> {
cube_format::parse_header(image).map_err(|bad| match bad {
cube_format::Bad::Magic => "not-a-store",
cube_format::Bad::Version(_) => "unsupported-version",
cube_format::Bad::Extent => "bad-extent",
cube_format::Bad::Tables => "bad-tables",
})
}
/// The v3 header: what a reader needs to address the image by arithmetic.
struct HeaderV3 {
/// `VERSION_V3` or `VERSION_V4` — they share the geometry and differ only in the index
/// entry's stride.
version: u8,
image_bytes: u64,
record_count: u64,
space_count: u64,
index_off: u64,
values_off: u64,
}
impl HeaderV3 {
/// The width of one index entry for this header's version.
fn stride(&self) -> usize {
if self.version == VERSION_V4 {
INDEX_ENTRY_V4
} else {
INDEX_ENTRY
}
}
fn encode(&self, out: &mut KVVec<u8>, curve: u8) -> Result<(), AllocError> {
out.extend_from_slice(MAGIC, GFP_KERNEL)?;
out.extend_from_slice(&[self.version, curve], GFP_KERNEL)?;
out.extend_from_slice(&self.image_bytes.to_le_bytes(), GFP_KERNEL)?;
out.extend_from_slice(&self.record_count.to_le_bytes(), GFP_KERNEL)?;
out.extend_from_slice(&self.space_count.to_le_bytes(), GFP_KERNEL)?;
out.extend_from_slice(&self.index_off.to_le_bytes(), GFP_KERNEL)?;
out.extend_from_slice(&self.values_off.to_le_bytes(), GFP_KERNEL)?;
Ok(())
}
fn decode(image: &[u8]) -> Result<Self, &'static str> {
// The shared file owns this: the offsets a reader computes (`index_off + i * stride`)
// are only places if the index really starts there and really is that wide, and that is a
// statement about the format rather than about this driver.
let geometry = cube_format::V3::decode(image).map_err(|bad| match bad {
cube_format::Bad::Extent => "bad-extent",
_ => "bad-tables",
})?;
let header = parse_header(image)?;
Ok(HeaderV3 {
version: geometry.version,
image_bytes: header.image_bytes.unwrap_or(0),
record_count: geometry.record_count,
space_count: geometry.space_count,
index_off: geometry.index_off,
values_off: geometry.values_off,
})
}
}
fn digest(image: &[u8]) -> Line {
let mut line = Line::new();
// Where the image is, and which log belongs to it: a store device says so in its control
// block, a bare image says so in its own header.
let layout = match resolve_layout(image) {
Ok(l) => l,
Err(what) => {
let _ = write!(line, "error={what}");
return line;
}
};
let live = &image[layout.image_off..];
let header = match parse_header(live) {
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, live.len()),
None => live.len(),
};
// With a v2 image, the store is the image *plus* whatever log follows it. The order the
// digest is taken in is then the order a checkpoint would write: sorted by coordinate,
// later writes winning. Without a log there is nothing to merge, and the image's own
// order is the answer — which is also what keeps v1 images readable and unchanged.
if header.image_bytes.is_some() {
let log = log_window(image, &layout);
match merged_digest(&live[..end], log, &header) {
Ok(line) => return line,
// A log that exists but cannot be read is worth saying out loud: the digest
// alone would look like a shorter store.
Err(what) if what != "no-log" => {
let mut line = Line::new();
let _ = write!(line, "error={what}");
return line;
}
Err(_) => {}
}
}
let mut h = FNV_OFFSET;
let mut count: u64 = 0;
let mut value_bytes: u64 = 0;
let mut errors: u64 = 0;
let header_len = if header.version == VERSION_V1 {
HEADER_LEN_V1
} else {
HEADER_LEN_V2
} as u64;
let mut off = layout.image_off + header_len as usize;
let mut remaining = header.record_count;
let end = layout.image_off + end;
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,
// `bytes` is a property of the store, not of how it was read: the header plus the
// records, whatever padding the reader happened to see. Reporting what was read made
// the same records disagree between a file and a device — and the gates compare this
// line, so it has to mean the same thing on both sides.
// The records' logical size, and not the layout's: a v2 image and a v3 image of one
// store hold the same records in different shapes, and this line is how the two are
// compared — a figure that changes with the layout would make them look different.
count * RECORD_FIXED as u64 + value_bytes,
count,
value_bytes,
h,
errors
);
line
}
/// What a store device's control block says: where the two image slots and the log are,
/// which slot is live, and how much of the log is in use.
#[derive(Clone, Copy)]
struct Control {
curve: u8,
slot_bytes: u64,
log_capacity: u64,
log_used: u64,
generation: u64,
active_slot: u8,
}
impl Control {
/// Parse a control copy. `None` when it is not one, or its checksum does not hold — a
/// torn update in the middle of a checkpoint is exactly what that checksum is for.
fn decode(b: &[u8]) -> Option<Control> {
if b.len() < CTL_SUMMED + 4 || &b[0..4] != CTL_MAGIC {
return None;
}
let word = |at: usize| -> u64 {
let mut w = [0u8; 8];
w.copy_from_slice(&b[at..at + 8]);
u64::from_le_bytes(w)
};
let mut c = [0u8; 4];
c.copy_from_slice(&b[44..48]);
if u32::from_le_bytes(c) != crc32(&b[..CTL_SUMMED]) {
return None;
}
let control = Control {
curve: b[5],
slot_bytes: word(8),
log_capacity: word(16),
log_used: word(24),
generation: word(32),
active_slot: b[40],
};
if b[4] != CTL_VERSION || control.active_slot > 1 || control.slot_bytes == 0 {
return None;
}
Some(control)
}
/// Encode into a control copy, checksum included.
fn encode(&self, b: &mut [u8]) {
for byte in b.iter_mut() {
*byte = 0;
}
b[0..4].copy_from_slice(&CTL_MAGIC);
b[4] = CTL_VERSION;
b[5] = self.curve;
b[8..16].copy_from_slice(&self.slot_bytes.to_le_bytes());
b[16..24].copy_from_slice(&self.log_capacity.to_le_bytes());
b[24..32].copy_from_slice(&self.log_used.to_le_bytes());
b[32..40].copy_from_slice(&self.generation.to_le_bytes());
b[40] = self.active_slot;
let crc = crc32(&b[..CTL_SUMMED]);
b[44..48].copy_from_slice(&crc.to_le_bytes());
}
fn image_off(&self) -> u64 {
CTL_DATA_OFF + self.active_slot as u64 * self.slot_bytes
}
fn spare_off(&self) -> u64 {
CTL_DATA_OFF + (1 - self.active_slot) as u64 * self.slot_bytes
}
fn log_off(&self) -> u64 {
CTL_DATA_OFF + 2 * self.slot_bytes
}
}
/// The valid control block: the copy with the higher generation. A reader that finds one
/// copy torn uses the other, which is the entire reason there are two.
fn read_control(b: &[u8]) -> Option<Control> {
if b.len() < CTL_COPY_B + CTL_COPY_LEN {
return None;
}
let a = Control::decode(&b[CTL_COPY_A..CTL_COPY_A + CTL_COPY_LEN]);
let b_copy = Control::decode(&b[CTL_COPY_B..CTL_COPY_B + CTL_COPY_LEN]);
match (a, b_copy) {
(Some(x), Some(y)) => Some(if x.generation >= y.generation { x } else { y }),
(Some(x), None) => Some(x),
(None, Some(y)) => Some(y),
(None, None) => None,
}
}
/// Where the image is and what log belongs to it.
struct Layout {
/// Byte offset of the live image.
image_off: usize,
/// Byte offset of its log.
log_off: usize,
/// Bytes of log in use.
log_used: usize,
/// The control block, when this is a store device rather than a bare image.
control: Option<Control>,
}
/// Work out the layout: a store device when the control block is there, a bare image when it
/// is not. Both are readable; only the store layout can be checkpointed atomically, which is
/// why `sync` refuses on the other.
fn resolve_layout(b: &[u8]) -> Result<Layout, &'static str> {
if b.len() >= CTL_COPY_B + CTL_COPY_LEN && &b[0..4] == CTL_MAGIC {
let control = read_control(b).ok_or("control-block-corrupt")?;
let log_off = control.log_off() as usize;
let used = control.log_used as usize;
if log_off + used > b.len() || used > control.log_capacity as usize {
return Err("log-outside-device");
}
return Ok(Layout {
image_off: control.image_off() as usize,
log_off,
log_used: used,
control: Some(control),
});
}
// A bare image. A v1 image has no extent and therefore no log — it is the whole device
// as far as this module is concerned, and it is read the way it always was.
let header = parse_header(b)?;
let log_off = match header.image_bytes {
Some(n) => log_offset(n),
None => b.len(),
};
Ok(Layout {
image_off: 0,
log_off,
// A bare device records no log length, so the log is bounded by the buffer and by
// the entries that parse: `apply_log` stops at the first one that does not.
log_used: b.len().saturating_sub(log_off),
control: None,
})
}
/// One record on its way into the merged store. Fixed size, so a `KVVec` of these sorts
/// in place; values live in a pool beside them and are referenced by offset.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Entry {
space: [u8; 32],
key: [u8; 24],
/// The class mask, carried from the log or the image so a fold does not lose it.
flags: u16,
/// Order in which this entry arrived. Breaks ties between the same coordinate, so the
/// later write wins after sorting.
seq: u32,
value_off: u32,
value_len: u32,
deleted: bool,
}
/// The store as a sorted set of records, which is what a checkpoint writes and what the
/// digest is taken over.
struct Merged {
pool: KVVec<u8>,
entries: KVVec<Entry>,
seq: u32,
/// Log entries that were seen but not applied, because they were torn or malformed.
dropped: u64,
}
impl Merged {
fn new() -> Result<Self, AllocError> {
Ok(Merged {
pool: KVVec::new(),
entries: KVVec::new(),
seq: 0,
dropped: 0,
})
}
/// Add a record. `deleted` marks a removal, which is kept in the list so it can
/// override an older value for the same coordinate.
fn add(&mut self, space: &[u8], key: &[u8], flags: u16, value: &[u8], deleted: bool) -> Result<(), AllocError> {
let mut sp = [0u8; 32];
sp.copy_from_slice(&space[..32]);
let mut k = [0u8; 24];
k.copy_from_slice(&key[..24]);
let value_off = self.pool.len() as u32;
self.pool.extend_from_slice(value, GFP_KERNEL)?;
let seq = self.seq;
self.seq += 1;
self.entries.push(
Entry {
space: sp,
key: k,
flags,
seq,
value_off,
value_len: value.len() as u32,
deleted,
},
GFP_KERNEL,
)?;
Ok(())
}
/// Put the entries in the order a checkpoint would write them: by space, then by key,
/// with the later write of a coordinate last — so a group's final entry decides it.
///
/// This is a heapsort, and the reason is a panic rather than a preference.
/// `slice::sort_unstable` allocates about 3.5 KiB of *kernel stack* per recursion level in
/// this kernel's Rust, so a store with a couple of hundred records overflowed the 16 KiB
/// kernel stack and took the machine down — `BUG: TASK stack guard page was hit`, with the
/// instruction pointer inside
/// `core::slice::sort::unstable::quicksort::<cubelinux_store::Entry>` and `sub rsp, 0xdd8` in
/// the function's first bytes. A heapsort has a constant frame and allocates nothing, so the
/// size of a store cannot decide whether reading it is safe, and the sort cannot fail for want
/// of memory in the middle of a read.
///
/// It is not stable, which costs nothing here: `seq` is part of the ordering key, so the order
/// is total and two entries never compare equal.
fn sort_entries(&mut self) {
let entries = self.entries.as_mut_slice();
let len = entries.len();
if len < 2 {
return;
}
// Build a max-heap, then repeatedly move the maximum to the end.
let mut start = len / 2;
while start > 0 {
start -= 1;
sift_down(entries, start, len);
}
let mut end = len;
while end > 1 {
end -= 1;
entries.swap(0, end);
sift_down(entries, 0, end);
}
}
fn value(&self, e: &Entry) -> &[u8] {
let off = e.value_off as usize;
&self.pool.as_slice()[off..off + e.value_len as usize]
}
}
/// Restore the heap property below `root` over `entries[..end]`.
///
/// Iterative on purpose: the whole point of the heapsort above is that nothing here grows with the
/// size of the store.
fn sift_down(entries: &mut [Entry], mut root: usize, end: usize) {
loop {
let left = 2 * root + 1;
if left >= end {
return;
}
let mut largest = left;
if left + 1 < end && entries[left] < entries[left + 1] {
largest = left + 1;
}
if entries[root] < entries[largest] {
entries.swap(root, largest);
root = largest;
} else {
return;
}
}
}
/// Read the log that follows the image, if there is one, and apply its entries.
///
/// The log is a delta on the image, so its entries override image records for the same
/// coordinate. Recovery is prefix-trusting, exactly as userspace does it: entries are
/// replayed from the start and the first one that is short, mis-framed or fails its
/// checksum ends the log. A fault in the middle cannot be told from a torn tail without a
/// second copy, so the log stops there and reports what it dropped instead of guessing.
fn apply_log(log: &[u8], merged: &mut Merged) -> Result<Option<u64>, &'static str> {
if log.len() < WAL_HEADER_LEN || &log[0..4] != WAL_MAGIC {
return Ok(None);
}
if log[4] != WAL_VERSION && log[4] != WAL_VERSION_V2 {
return Err("unsupported-log-version");
}
let (fixed, len_at) = wal_frame(log[4]);
let mut applied: u64 = 0;
let mut off = WAL_HEADER_LEN;
while off + fixed <= log.len() {
let start = off;
let op = log[off];
if op != 1 && op != 2 {
break;
}
let mut word = [0u8; 4];
word.copy_from_slice(&log[off + 1..off + 5]);
let crc = u32::from_le_bytes(word);
let space = &log[off + 5..off + 37];
let key = &log[off + 37..off + 61];
let flags = if log[4] == WAL_VERSION_V2 {
cube_format::le_u16(log, off + 61)
} else {
0
};
word.copy_from_slice(&log[off + len_at..off + len_at + 4]);
let len = u32::from_le_bytes(word) as usize;
let frame_end = start + fixed + len;
if frame_end > log.len() {
break;
}
// The checksum covers space, key, the mask, length and value, so a corrupted entry is
// stopped at rather than applied.
if crc32(&log[start + 5..frame_end]) != crc {
break;
}
let value = &log[start + fixed..frame_end];
off = frame_end;
merged
.add(space, key, flags, value, op == 2)
.map_err(|_| "out-of-memory")?;
applied += 1;
}
merged.dropped = (log.len() - off) as u64;
Ok(Some(applied))
}
/// CRC-32 (IEEE 802.3), bitwise — the same polynomial and the same coverage as
/// `cube-store/src/wal.rs`, because a log written on either side must validate on both.
fn crc32(bytes: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for byte in bytes {
crc ^= *byte as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
/// The log window that belongs to the live image.
fn log_window<'a>(device: &'a [u8], layout: &Layout) -> &'a [u8] {
let end = core::cmp::min(
layout.log_off + WAL_HEADER_LEN + layout.log_used,
device.len(),
);
if layout.log_off <= end {
&device[layout.log_off..end]
} else {
&[]
}
}
/// The store the image and its log together describe: the image's records, then the log's
/// entries applied over them. This is what a checkpoint writes and what the digest is taken
/// over, so both go through here rather than each walking the bytes its own way.
fn build_merged(image: &[u8], log: &[u8], header: &Header) -> Result<(Merged, u64), &'static str> {
let mut merged = Merged::new().map_err(|_| "out-of-memory")?;
// A v3 image holds its records behind a space table and an index rather than packed one after
// another, so the store it describes is read through that arithmetic. This is the path a fold
// takes once a store HAS been folded — the first fold reads a packed image and writes an
// addressed one, and every fold after that reads an addressed one — so a reader that only knew
// the packed layout could fold a store exactly once. Found on the box, where that is not a
// hypothesis: the live store's first fold succeeded and its second answered -EINVAL.
if header.version == VERSION_V3 || header.version == VERSION_V4 {
let geometry = cube_format::V3::decode(image).map_err(|_| "bad-tables")?;
let mut row = 0u64;
while row < geometry.space_count {
let (space, first, records) = geometry.space_row(image, row).ok_or("truncated-table")?;
let mut i = 0u64;
while i < records {
let entry = geometry
.index_entry(image, first + i)
.ok_or("truncated-index")?;
let value = geometry
.value(image, &entry)
.ok_or("truncated-image")?;
merged
.add(space, entry.key, entry.flags, value, false)
.map_err(|_| "out-of-memory")?;
i += 1;
}
row += 1;
}
let applied = match apply_log(log, &mut merged)? {
Some(n) => n,
None => 0,
};
return Ok((merged, applied));
}
let extent = header.image_bytes.ok_or("v1-has-no-extent")? as usize;
let extent = core::cmp::min(extent, image.len());
let mut off = if header.version == VERSION_V1 {
HEADER_LEN_V1
} else {
HEADER_LEN_V2
};
let mut seen: u64 = 0;
while off + RECORD_FIXED <= extent {
if let Some(c) = header.record_count {
if seen >= c {
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 word = [0u8; 8];
word.copy_from_slice(&image[off + SPACE_ID_LEN + RAW_KEY_LEN..off + RECORD_FIXED]);
let value_len = u64::from_le_bytes(word) as usize;
let value_at = off + RECORD_FIXED;
if value_at + value_len > extent {
return Err("truncated-image");
}
merged
.add(space, key, 0, &image[value_at..value_at + value_len], false)
.map_err(|_| "out-of-memory")?;
seen += 1;
off = value_at + value_len;
}
let applied = match apply_log(log, &mut merged)? {
Some(n) => n,
None => 0,
};
Ok((merged, applied))
}
/// Digest the store the image and its log together describe.
///
/// `image` is the live image's bytes alone; `log` is exactly the window of log that belongs
/// to it. Returns `Err("no-log")` when there is none, so the caller can fall back to reading
/// the image by itself.
fn merged_digest(image: &[u8], log: &[u8], header: &Header) -> Result<Line, &'static str> {
// An addressed image is a complete store on its own: its index holds every record and says
// where each one is, so a log is an *addition* rather than a requirement. A packed image is
// not — without a log there is nothing this path adds over the image's own order, which is why
// it answers "no log" and lets the caller walk the records it can see.
//
// Requiring a log here regardless of layout is what made a bare v3/v4 image read as a *packed*
// one: the caller fell through to a walk that starts at the v2 header length and reads index
// entries as record frames. It went unnoticed while every userspace-written image was v2, which
// is exactly the kind of bug that a migration finds and a unit test does not.
let addressed = header.version == VERSION_V3 || header.version == VERSION_V4;
if !addressed && (log.len() < WAL_HEADER_LEN || &log[0..4] != WAL_MAGIC) {
return Err("no-log");
}
let (mut merged, applied) = build_merged(image, log, header)?;
merged.sort_entries();
let entries = merged.entries.as_slice();
let mut h = FNV_OFFSET;
let mut count: u64 = 0;
let mut value_bytes: u64 = 0;
let mut i = 0;
while i < entries.len() {
let head = &entries[i];
let mut last = i;
while last + 1 < entries.len()
&& entries[last + 1].space == head.space
&& entries[last + 1].key == head.key
{
last += 1;
}
let winner = &entries[last];
i = last + 1;
if winner.deleted {
continue;
}
let value = merged.value(winner);
h = fnv1a64(&winner.space, h);
h = fnv1a64(&winner.key, h);
h = fnv1a64(&(value.len() as u64).to_le_bytes(), h);
h = fnv1a64(value, h);
count += 1;
value_bytes += value.len() as u64;
}
// `bytes` is the records' logical size, not the size of a particular layout: a v2 image and a
// v3 image of one store hold the same records in different shapes, and this line is what the
// two are compared by, so the figure must not depend on which shape is on disk.
let folded = count * RECORD_FIXED as u64 + value_bytes;
let mut line = Line::new();
// The digest line is field-for-field what `cube-image digest` prints, so the two are
// compared by diff. What the log contributed goes on its own line: a padded log region
// makes a byte count meaningless, an entry count does not.
let _ = write!(
line,
"digest version={} curve={} bytes={} records={} value_bytes={} fnv1a64={:016x} errors=0\n",
header.version, header.curve, folded, count, value_bytes, h
);
let _ = write!(line, "log entries={} applied over the image\n", applied);
Ok(line)
}
/// Where a bare image's log starts, given the image's declared extent.
fn log_offset(image_bytes: u64) -> usize {
let n = image_bytes as usize;
(n + LOG_ALIGN - 1) & !(LOG_ALIGN - 1)
}
/// Length of a bare log's valid prefix: the header plus every entry that parses and passes
/// its checksum. This is the append position, and the reason a torn tail is overwritten
/// rather than appended to.
fn log_valid_len(log: &[u8]) -> Result<usize, &'static str> {
if log.len() < WAL_HEADER_LEN {
return Err("short-log");
}
if &log[0..4] != WAL_MAGIC {
return Err("no-log");
}
if log[4] != WAL_VERSION && log[4] != WAL_VERSION_V2 {
return Err("unsupported-log-version");
}
let (fixed, len_at) = wal_frame(log[4]);
let mut off = WAL_HEADER_LEN;
while off + fixed <= log.len() {
let start = off;
let op = log[off];
if op != 1 && op != 2 {
break;
}
let mut word = [0u8; 4];
word.copy_from_slice(&log[off + 1..off + 5]);
let crc = u32::from_le_bytes(word);
word.copy_from_slice(&log[off + len_at..off + len_at + 4]);
let len = u32::from_le_bytes(word) as usize;
// Only a validated entry moves the append point. Advancing first and checking after
// counts a torn entry as part of the prefix, so the next append lands *after* the
// corruption and buries it — the opposite of the rule that a torn tail is overwritten.
let frame_end = start + fixed + len;
if frame_end > log.len() {
break;
}
if crc32(&log[start + 5..frame_end]) != crc {
break;
}
off = frame_end;
}
Ok(off)
}
/// Morton-encode a point into the store's 24-byte key — the same interleaving
/// `cube-core`'s `Curve for Morton` does, because a key written here has to be the key a
/// userspace reader decodes. Bit `i` of each axis lands at bit `3i` of the 192-bit key,
/// counting from the least significant bit, which lives in the *last* byte.
fn morton_encode(x: u64, y: u64, z: u64) -> [u8; 24] {
// The interleave itself lives in the shared file, and this is now one of its callers rather
// than a second copy of it. That matters more than it looks: the driver and userspace have to
// agree on this key byte for byte, and they were two implementations of one rule whose
// agreement was held only by the gates. `crates/cube-format`'s tests pin the axis-to-bit
// correspondence by hand so that neither side can drift without a test failing.
cube_format::morton_key(x, y, z)
}
/// A mutation to append: the byte-plane write the store contract calls `put`, plus the class
/// mask the caller stamps at write time — the moment the event's class is known for certain.
struct Mutation {
space: [u8; 32],
key: [u8; 24],
flags: u16,
value: KVVec<u8>,
}
/// Build a log entry: `op | crc32 | space | key | [flags] | len | value`.
///
/// `version` decides whether the class mask is in the frame, and it must match the log's header:
/// a reader frames every entry by the header's version, so an entry is only well-formed against the
/// log it is written into. Version 1 has no mask field — the legacy log simply cannot carry one —
/// and an entry going into such a log is written without it rather than not written at all.
fn encode_entry(
op: u8,
space: &[u8; 32],
key: &[u8; 24],
flags: u16,
value: &[u8],
version: u8,
) -> Result<KVVec<u8>, AllocError> {
let flagged = version == WAL_VERSION_V2;
let fixed = if flagged { ENTRY_FIXED_V2 } else { ENTRY_FIXED };
let mut entry = KVVec::<u8>::with_capacity(fixed + value.len(), GFP_KERNEL)?;
entry.extend_from_slice(&[op], GFP_KERNEL)?;
entry.extend_from_slice(&[0u8; 4][..], GFP_KERNEL)?;
entry.extend_from_slice(space, GFP_KERNEL)?;
entry.extend_from_slice(key, GFP_KERNEL)?;
if flagged {
entry.extend_from_slice(&flags.to_le_bytes(), GFP_KERNEL)?;
}
entry.extend_from_slice(&(value.len() as u32).to_le_bytes(), GFP_KERNEL)?;
entry.extend_from_slice(value, GFP_KERNEL)?;
// The checksum covers everything after the crc field, so a v1 entry's checksum covers what a v1
// reader reads — the two versions are different framings of the same rule, not two rules.
let crc = crc32(&entry.as_slice()[5..]);
entry.as_mut_slice()[1..5].copy_from_slice(&crc.to_le_bytes());
Ok(entry)
}
/// Write `bytes` at `off` and flush, or say why not. The one place the module writes.
fn write_and_sync(file: *mut bindings::file, off: u64, bytes: &[u8]) -> Result<(), Error> {
let mut at: bindings::loff_t = off as bindings::loff_t;
// SAFETY: the caller passes a live `struct file *`; `bytes` outlives the call; `at` is a
// valid loff_t.
let wrote = unsafe {
bindings::kernel_write(
file,
bytes.as_ptr().cast::<core::ffi::c_void>(),
bytes.len(),
&mut at,
)
};
if wrote < 0 {
return Err(Error::from_errno(wrote as i32));
}
if wrote as usize != bytes.len() {
return Err(EIO);
}
// Durability before acknowledgement: a short write that was never flushed is a write that
// did not happen.
// SAFETY: the caller passes a live, writable file; datasync is 0 (a full sync).
let rc = unsafe { bindings::vfs_fsync(file, 0) };
if rc < 0 {
return Err(Error::from_errno(rc));
}
Ok(())
}
/// The control copy an update should write: the older of the two, so the current one stays
/// readable throughout.
fn older_copy(generation: u64) -> usize {
if generation % 2 == 0 {
CTL_COPY_A
} else {
CTL_COPY_B
}
}
/// Append a mutation to the log and make it durable, returning the updated control block
/// when the layout has one.
///
/// The order is the durability contract: the entry is written and flushed to the device
/// *before* the caller is told it happened. A mutation this function has returned `Ok` for
/// survives a power cut; one it has not may or may not, which is exactly the boundary an
/// acknowledgement is supposed to mark.
///
/// On a store device the entry goes after the log bytes already in use, and the control
/// block's count is raised afterwards — entry first, count second, so a crash between them
/// loses an *unacknowledged* mutation rather than counting one that is not there. A count
/// that ran ahead would make replay read past valid data; a count that lags only forgets.
fn append(
image: &[u8],
layout: &Layout,
m: &Mutation,
op: u8,
) -> Result<Option<Control>, Error> {
// Where it goes: after the bytes in use on a store device, after the valid prefix on a
// bare image, where nothing records the length.
let region_at = core::cmp::min(layout.log_off, image.len());
let region = &image[region_at..];
let (used, capacity) = match layout.control {
// `log_used` counts entry bytes *after* the log's header, so the header is not
// something an append has to account for in the count.
Some(c) => (c.log_used as usize, c.log_capacity as usize),
None => {
// A region with no header is a log that has never been written; it is empty, not
// broken, and the header goes down before the first entry.
let used = match log_valid_len(region) {
Ok(valid) => valid - WAL_HEADER_LEN,
Err("no-log") | Err("short-log") => 0,
Err(what) => {
pr_err!("cubelinux: log region unreadable: {}\n", what);
return Err(EINVAL);
}
};
(used, region.len())
}
};
// The entry's frame has to match the log's header, because the header is what a reader frames
// it by. So the header decides the version, and there are exactly three cases:
//
// * a log that already holds v1 entries must keep taking v1 entries — rewriting the header
// would mis-frame every entry already there. A store *device* folds first instead (that is
// the v4 migration, and it has a spare slot to fold into); a bare image has nowhere to fold
// to, so it keeps writing the log it has. The mask is lost for that append, because a v1
// entry has no field to carry it — a limitation of the legacy layout stated plainly, and
// better than refusing to write to a log the store already holds data in;
// * no header, a v2 header, or a v1 header over an *empty* log: the frame is v2, and the
// header is written (or upgraded) to say so.
let header_version = if region.len() >= WAL_HEADER_LEN && &region[0..4] == WAL_MAGIC {
Some(region[4])
} else {
None
};
let entry_version = match header_version {
// A log that already holds entries and declares v1 keeps taking v1 — rewriting the header
// would mis-frame everything already there. A store *device* first folds instead (that is
// the v4 migration, and it has a spare slot to fold into); a bare image has nowhere to fold
// to, so it keeps writing the log it has.
Some(WAL_VERSION) if used > 0 => {
if layout.control.is_some() {
fold_now(image, layout)?;
let (device, layout) = device_and_layout()?;
return append(&device, &layout, m, op);
}
WAL_VERSION
}
// An empty log that already declares a version: a device's is upgraded to v2, since v2 is
// the layout that carries the class mask and the one a fold turns into a v4 index. A bare
// image keeps what its log says.
Some(v) => {
if layout.control.is_some() {
WAL_VERSION_V2
} else {
v
}
}
// No header at all: this append writes the first one, and so chooses the framing for this
// log's life. A store device gets v2. A bare image gets v1, because the bare layout is the
// legacy one: it has no index, so a mask written into its log could only ever be read back
// by a scan and never be checkpointed — half a feature, bought by making every existing
// reader of that layout grow a version it has no use for. The format that carries a class
// mask is the checkpointable one, and that is the one that gets it.
None => {
if layout.control.is_some() {
WAL_VERSION_V2
} else {
WAL_VERSION
}
}
};
let entry = encode_entry(op, &m.space, &m.key, m.flags, m.value.as_slice(), entry_version)?;
if WAL_HEADER_LEN + used + entry.len() > capacity {
pr_err!(
"cubelinux: the log is full ({} of {} bytes); checkpoint before appending\n",
used,
capacity
);
return Err(ENOSPC);
}
// SAFETY: store_device() is a NUL-terminated C string filled at boot; filp_open returns a
// valid file or an error pointer, which is checked. O_RDWR is 2.
let file = store_file()?;
let mut result: Result<(), Error> = Ok(());
// A log region that has never been written is zeros, not a log: give it a header, the same
// thing the userspace log does when its file does not exist. A header that does not already
// say the version this entry is framed by is (re)written to say it, because the header is what
// a reader frames the entry by — and an entry under a header that disagrees with it is not a
// record, it is a mis-framing.
if region.len() < WAL_HEADER_LEN || &region[0..4] != WAL_MAGIC || region[4] != entry_version {
let mut hdr = [0u8; WAL_HEADER_LEN];
hdr[0..4].copy_from_slice(&WAL_MAGIC);
hdr[4] = entry_version;
hdr[5] = 0; // morton
result = write_and_sync(file, layout.log_off as u64, &hdr);
}
if result.is_ok() {
result = write_and_sync(
file,
(region_at + WAL_HEADER_LEN + used) as u64,
entry.as_slice(),
);
}
// The count second, so replay can never be told about an entry that is not there.
let mut updated = None;
if result.is_ok() {
if let Some(c) = layout.control {
let next = Control {
log_used: c.log_used + entry.len() as u64,
generation: c.generation + 1,
..c
};
let mut buf = [0u8; CTL_COPY_LEN];
next.encode(&mut buf);
result = write_and_sync(file, older_copy(c.generation) as u64, &buf);
if result.is_ok() {
updated = Some(next);
}
}
}
result.map(|_| updated)
}
/// Serialize the merged store into the pinned v2 image.
///
/// The winners are collected first because the header states the image's extent and its
/// record count, and those are only known once the coalescing is done.
fn serialize_image(merged: &mut Merged, curve: u8) -> Result<KVVec<u8>, AllocError> {
merged.sort_entries();
let entries = merged.entries.as_slice();
// The winners first: one live record per coordinate, in the store's own (space, key) order.
let mut winners = KVVec::<u32>::new();
let mut values_len: u64 = 0;
let mut i = 0;
while i < entries.len() {
let head = &entries[i];
let mut last = i;
while last + 1 < entries.len()
&& entries[last + 1].space == head.space
&& entries[last + 1].key == head.key
{
last += 1;
}
if !entries[last].deleted {
winners.push(last as u32, GFP_KERNEL)?;
values_len += entries[last].value_len as u64;
}
i = last + 1;
}
// The space table: where each space's records begin in the index, and how many. It comes from
// the same order the index is written in, so a walk of the store is a walk of this table.
let mut spaces = KVVec::<([u8; SPACE_ID_LEN], u64, u64)>::new();
for (at, w) in winners.as_slice().iter().enumerate() {
let space = merged.entries.as_slice()[*w as usize].space;
match spaces.as_mut_slice().last_mut() {
Some((last_space, _, records)) if *last_space == space => *records += 1,
_ => spaces.push((space, at as u64, 1), GFP_KERNEL)?,
}
}
let header = HeaderV3 {
version: VERSION_V4,
record_count: winners.len() as u64,
space_count: spaces.len() as u64,
index_off: (HEADER_LEN_V3 + spaces.len() * SPACE_ENTRY) as u64,
values_off: (HEADER_LEN_V3 + spaces.len() * SPACE_ENTRY + winners.len() * INDEX_ENTRY_V4) as u64,
image_bytes: 0,
};
let image_bytes = header.values_off + values_len;
let mut out = KVVec::<u8>::with_capacity(image_bytes as usize, GFP_KERNEL)?;
let mut header = header;
header.image_bytes = image_bytes;
header.encode(&mut out, curve)?;
for (space, first, records) in spaces.as_slice() {
out.extend_from_slice(space, GFP_KERNEL)?;
out.extend_from_slice(&first.to_le_bytes(), GFP_KERNEL)?;
out.extend_from_slice(&records.to_le_bytes(), GFP_KERNEL)?;
}
// The index, whose entries are the addresses: a fixed stride is what makes `index_off +
// i * stride` a place a reader can go to without reading anything before it. v4's stride is
// two wider, carrying the class mask beside the address — never inside the value.
let mut value_at = header.values_off;
for w in winners.as_slice() {
let e = &merged.entries.as_slice()[*w as usize];
out.extend_from_slice(&e.key, GFP_KERNEL)?;
out.extend_from_slice(&e.flags.to_le_bytes(), GFP_KERNEL)?;
out.extend_from_slice(&value_at.to_le_bytes(), GFP_KERNEL)?;
// The length is written as 8 bytes even though `Entry` carries it in 4: an index entry
// has a fixed stride and the header's `values_off` is computed from that, so a shorter
// field here would leave the last entries overlapping the values. Both readers assume the
// stride the header declares, which is the point of a fixed stride.
out.extend_from_slice(&(e.value_len as u64).to_le_bytes(), GFP_KERNEL)?;
value_at += e.value_len as u64;
}
// The values, packed in index order: a batch of records is one span of this, which is why a
// listing can read what it returns in one go rather than record by record.
for w in winners.as_slice() {
let e = &merged.entries.as_slice()[*w as usize];
out.extend_from_slice(merged.value(e), GFP_KERNEL)?;
}
Ok(out)
}
/// Fold the log into the image: write the merged store into the spare slot, then make it
/// live.
///
/// The order is what makes this atomic. The new image goes to the slot nothing is reading and
/// is flushed; only then does the control block change, and that change is one small write to
/// the control copy that is *not* current. At every instant there is either the old image with
/// a log that still describes the mutations since it, or the new image with an empty log. A
/// crash in the middle of the copy leaves the previous store intact, which is the entire
/// reason for two slots.
fn checkpoint(ctl: &Control, merged: &mut Merged) -> Result<u64, Error> {
let new_image = serialize_image(merged, ctl.curve)?;
if new_image.len() as u64 > ctl.slot_bytes {
pr_err!(
"cubelinux: checkpoint needs {} bytes but a slot holds {}\n",
new_image.len(),
ctl.slot_bytes
);
return Err(ENOSPC);
}
// SAFETY: as in `append`.
let file = store_file()?;
let mut result = write_and_sync(file, ctl.spare_off(), new_image.as_slice());
if result.is_ok() {
let next = Control {
log_used: 0,
generation: ctl.generation + 1,
active_slot: 1 - ctl.active_slot,
..*ctl
};
let mut buf = [0u8; CTL_COPY_LEN];
next.encode(&mut buf);
result = write_and_sync(file, older_copy(ctl.generation) as u64, &buf);
}
result.map(|_| new_image.len() as u64)
}
/// Fold the current log into the image — the shared body of `sync` and the append-time
/// migration. After it, the image is the pinned v4 shape and the log is empty, so the next
/// append starts a fresh v2 log.
fn fold_now(image: &[u8], layout: &Layout) -> Result<(), Error> {
let ctl = match layout.control {
Some(c) => c,
None => return Err(EINVAL), // a bare image has no spare slot to fold into
};
let live = &image[layout.image_off..];
let header = match parse_header(live) {
Ok(h) => h,
Err(_) => return Err(EINVAL),
};
let window = log_window(image, layout);
let extent = core::cmp::min(
header.image_bytes.unwrap_or(live.len() as u64) as usize,
live.len(),
);
let mut merged = match build_merged(&live[..extent], window, &header) {
Ok((m, _)) => m,
Err(_) => return Err(EINVAL),
};
checkpoint(&ctl, &mut merged)?;
Ok(())
}
/// Everything a syscall needs to reach the store: read the device, resolve its layout, and
/// hand back what was asked for.
///
/// These are the `cube(2)` entry points as the Rust side exposes them. The C shim
/// (cube_syscall.c) owns the user copies and the argument validation; nothing here sees a
/// userspace pointer.
fn device_and_layout() -> Result<(KVVec<u8>, Layout), Error> {
let mut device = KVVec::<u8>::new();
read_image(&mut device)?;
let layout = resolve_layout(&device).map_err(|what| {
pr_err!("cubelinux: cannot resolve the store layout: {}\n", what);
EINVAL
})?;
Ok((device, layout))
}
/// The coordinate's key, and the space, as the store addresses them.
unsafe fn coord_key(space: *const u8, x: u64, y: u64, z: u64) -> ([u8; 32], [u8; 24]) {
let mut sp = [0u8; 32];
// SAFETY: the caller (the syscall shim) passes a pointer to 32 bytes it has already
// copied from userspace into kernel memory.
unsafe { core::ptr::copy_nonoverlapping(space, sp.as_mut_ptr(), 32) };
(sp, morton_encode(x, y, z))
}
// ── The kernel's record of this boot ───────────────────────────────────────────────────
/// Push bytes, or report that the buffer would not grow.
fn push(out: &mut KVVec<u8>, bytes: &[u8]) -> Option<()> {
out.extend_from_slice(bytes, GFP_KERNEL).ok()
}
/// Push a decimal integer. The value is built in a `KVVec`, which is not a `fmt::Write`, and giving
/// the boot record its own output type to format one number into would be a second mechanism for
/// something the digest line already solved differently.
fn push_dec(out: &mut KVVec<u8>, mut n: u64) -> Option<()> {
let mut tmp = [0u8; 20];
let mut at = tmp.len();
loop {
at -= 1;
tmp[at] = b'0' + (n % 10) as u8;
n /= 10;
if n == 0 {
break;
}
}
push(out, &tmp[at..])
}
/// How long a NUL-terminated byte string is, up to `max`.
fn c_len(p: *const u8, max: usize) -> usize {
let mut n = 0;
while n < max {
// SAFETY: the caller guarantees `p` points at `max` readable bytes.
if unsafe { *p.add(n) } == 0 {
break;
}
n += 1;
}
n
}
/// The kernel's own account of the boot it is having, as one line:
/// `boot=<seconds since the epoch> device=<the store it resolved> kernel=<its version banner>`.
///
/// The banner is last and unquoted because it contains spaces, so everything after the final `=`
/// is the kernel's own words rather than a field this code parsed. The time is raw epoch seconds:
/// rendering a calendar date in the kernel is date arithmetic, and a caller with a clock can do it
/// without a kernel bug being the reason a timestamp is wrong.
fn boot_record_value() -> Option<KVVec<u8>> {
let mut out = KVVec::<u8>::new();
let mut ts = bindings::timespec64 {
tv_sec: 0,
tv_nsec: 0,
};
// SAFETY: `ts` is a live, writable `timespec64`, and the call retains no pointer to it.
unsafe { bindings::ktime_get_real_ts64(&mut ts) };
push(&mut out, b"boot=")?;
push_dec(&mut out, ts.tv_sec as u64)?;
push(&mut out, b" device=")?;
// SAFETY: `store_device` returns a static NUL-terminated buffer, valid for the life of the
// kernel; 256 is the length of the buffer the C side fills.
let dev = store_device();
push(&mut out, unsafe {
core::slice::from_raw_parts(dev, c_len(dev, 256))
})?;
push(&mut out, b" kernel=")?;
// SAFETY: `linux_banner` is a static NUL-terminated string the kernel defines. `addr_of!` takes
// its address without creating a reference to a `[c_char; 0]`, which is how bindgen declares an
// array whose size is not in the header. The bound is generous: the banner is one line.
let banner = unsafe {
let p = core::ptr::addr_of!(bindings::linux_banner).cast::<u8>();
core::slice::from_raw_parts(p, c_len(p, 512))
};
// The banner ends in a newline. A record that carries it would make every reader strip one, so
// it is dropped here, where it is known to be a terminator rather than content.
let banner = match banner.last() {
Some(b'\n') => &banner[..banner.len() - 1],
_ => banner,
};
push(&mut out, banner)?;
Some(out)
}
/// Record this boot, once, at the first write the kernel is asked to make.
///
/// Called from every entry point that writes. It is not called from the read paths, so a kernel that
/// only reads leaves no record — see the note above `BOOT_SPACE`. A failure here is logged and never
/// propagated: a record of the boot is worth having, and is not a precondition for the caller's
/// write. It is attempted once per boot rather than retried on every write, because a store that
/// will not take it will not take it later either, and one warning is information where a stream of
/// them is noise.
///
/// The first write pays one extra read of the store's image (this hook resolves the device and
/// layout for itself, and then the caller resolves it again for its own mutation). That is once per
/// boot, and it buys keeping the hook out of the callers' paths entirely.
fn ensure_boot_record() {
// SAFETY: a flag the C side set from the kernel command line during boot.
if !unsafe { cubelinux_boot_record_enabled() } {
return;
}
// Claimed with a swap: two callers racing into their first write must not write two records.
if BOOT_RECORDED.swap(true, Ordering::AcqRel) {
return;
}
let value = match boot_record_value() {
Some(v) => v,
None => {
pr_warn!("cubelinux: not enough memory to build this boot's record\n");
return;
}
};
let (device, layout) = match device_and_layout() {
Ok(pair) => pair,
Err(_) => {
pr_warn!("cubelinux: could not read the store to record this boot\n");
return;
}
};
let mutation = Mutation {
space: BOOT_SPACE,
key: morton_encode(BOOT_POINT.0, BOOT_POINT.1, BOOT_POINT.2),
flags: BOOT_FLAGS,
value,
};
match append(&device, &layout, &mutation, 1) {
Ok(_) => pr_info!("cubelinux: recorded this boot in the store\n"),
Err(_) => pr_warn!("cubelinux: the store would not take this boot's record\n"),
}
}
/// `CUBE_OP_PUT`: store bytes at a coordinate, under a class mask.
///
/// The mask is the writer's and is stamped here, once — this is the moment the record's class is
/// known for certain, and every later reader is spared re-deriving it. It means nothing to this
/// side: which bits are which class is a vocabulary's business, and a kernel that interpreted one
/// would be inventing a vocabulary.
///
/// # Safety
/// `space` must point to 32 readable bytes; `value` to `len` readable bytes when `len` is
/// non-zero.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_put(
space: *const u8,
x: u64,
y: u64,
z: u64,
value: *const u8,
len: usize,
flags: u16,
) -> i32 {
ensure_boot_record();
let (sp, key) = unsafe { coord_key(space, x, y, z) };
let mut bytes = KVVec::<u8>::new();
if len > 0 {
if let Err(_) = bytes.extend_from_slice(
// SAFETY: the shim guarantees `value` holds `len` bytes.
unsafe { core::slice::from_raw_parts(value, len) },
GFP_KERNEL,
) {
return -12; // -ENOMEM
}
}
let mutation = Mutation {
space: sp,
key,
flags,
value: bytes,
};
let (device, layout) = match device_and_layout() {
Ok(pair) => pair,
Err(e) => return -(e.to_errno() as i32),
};
match append(&device, &layout, &mutation, 1) {
Ok(_) => 0,
Err(e) => -(e.to_errno() as i32),
}
}
// ── Reading the store where it lies ─────────────────────────────────────────────────────
//
// A read used to start by reading the whole device into kernel memory, parsing every record into
// a merged pool, and heapsorting the lot — then throwing all of it away. That is why a `get` of
// one record cost as much as listing the store, and why the coordinate bought nothing
// mechanically: knowing where a record is did not make reaching it any cheaper.
//
// The records are already the index. A checkpoint writes them in `(space, key)` order, which is
// the order the coordinate itself computes, and the log that can override them is small. So the
// kernel walks the records where they lie — zero-copy, no pool, no sort — and consults the log as
// an overlay. What an operation costs is proportional to the records it actually touches.
/// How much is read at a time when a length is not known ahead of it.
const VIEW_CHUNK: usize = 256 * 1024;
/// Read exactly `len` bytes at `off`. A short read is an error, not a smaller view: the store says
/// how long its image is, and quietly reading less would turn a truncated store into a shorter one
/// that looks complete.
fn read_exact_at(
file: *mut bindings::file,
off: u64,
len: usize,
out: &mut KVVec<u8>,
scratch: &mut KVVec<u8>,
) -> Result<()> {
out.clear();
out.reserve(len, GFP_KERNEL)?;
let mut pos: bindings::loff_t = off as bindings::loff_t;
let mut left = len;
while left > 0 {
let want = core::cmp::min(left, scratch.len());
// SAFETY: `file` is a live `struct file *`; `scratch` is a kernel buffer we own; `pos` is
// a valid loff_t. kernel_read copies at most `want` bytes and retains neither pointer.
let n = unsafe {
bindings::kernel_read(file, scratch.as_mut_ptr().cast::<core::ffi::c_void>(), want, &mut pos)
};
if n < 0 {
return Err(Error::from_errno(n as i32));
}
if n == 0 {
return Err(EINVAL);
}
let n = n as usize;
out.extend_from_slice(&scratch.as_slice()[..n], GFP_KERNEL)?;
left -= n;
}
Ok(())
}
/// Read from `off` to the end, or to `cap` bytes, whichever comes first.
///
/// For a bare image, whose extent nothing states, and for a log whose length is implied.
fn read_to_end_at(
file: *mut bindings::file,
off: u64,
out: &mut KVVec<u8>,
scratch: &mut KVVec<u8>,
cap: usize,
) -> Result<()> {
out.clear();
let mut pos: bindings::loff_t = off as bindings::loff_t;
while out.len() < cap {
let want = core::cmp::min(scratch.len(), cap - out.len());
// SAFETY: as in `read_exact_at`.
let n = unsafe {
bindings::kernel_read(file, scratch.as_mut_ptr().cast::<core::ffi::c_void>(), want, &mut pos)
};
if n < 0 {
return Err(Error::from_errno(n as i32));
}
if n == 0 {
break;
}
let n = n as usize;
out.extend_from_slice(&scratch.as_slice()[..n], GFP_KERNEL)?;
}
Ok(())
}
/// The bytes one operation needs: the image, and the log beside it.
///
/// The device is four images wide plus its log; reading the device to read the image copies
/// several times what is used, on every call.
struct View {
image: KVVec<u8>,
log: KVVec<u8>,
header: Header,
/// The control block's generation, which a checkpoint changes — the key to what is cached
/// about this image. Zero for a bare image, which has no control block and does not change
/// underneath a reader.
generation: u64,
}
kernel::sync::global_lock! {
/// Where each space's records begin.
///
/// A walk has to start at its space. Records are variable-length, so "where does space S begin"
/// cannot be computed from the key — it can only be found by reading the image once. Finding it
/// once per checkpoint is what makes a listing cost what it returns, and the table is one entry
/// per space (tens of bytes), not one per record. It goes stale on exactly one event: a
/// checkpoint rewriting the image, which changes the control block's generation.
///
/// SAFETY: Initialized in the module initializer before first use.
unsafe(uninit) static SPACE_STARTS: Mutex<Option<SpaceStarts>> = None;
}
struct SpaceStarts {
generation: u64,
image_len: usize,
/// Each space's first record, in the image's order.
starts: KVVec<([u8; SPACE_ID_LEN], usize)>,
}
/// Where the live image's spaces begin, from the cache when it is current and by reading the image
/// when it is not.
fn space_starts(view: &View) -> Result<KVVec<([u8; SPACE_ID_LEN], usize)>> {
let mut guard = SPACE_STARTS.lock();
if let Some(cached) = guard.as_ref() {
if cached.generation == view.generation && cached.image_len == view.image().len() {
let mut out = KVVec::new();
out.extend_from_slice(cached.starts.as_slice(), GFP_KERNEL)?;
return Ok(out);
}
}
let mut starts = KVVec::new();
let mut records = Records::new(view.image(), &view.header);
let mut previous: Option<[u8; SPACE_ID_LEN]> = None;
loop {
let at = records.off;
match records.next() {
Some((space, _, _)) => {
if previous != Some(*space) {
starts.push((*space, at), GFP_KERNEL)?;
previous = Some(*space);
}
}
None => break,
}
}
let mut out = KVVec::new();
out.extend_from_slice(starts.as_slice(), GFP_KERNEL)?;
*guard = Some(SpaceStarts {
generation: view.generation,
image_len: view.image().len(),
starts,
});
Ok(out)
}
impl View {
fn image(&self) -> &[u8] {
self.image.as_slice()
}
fn log(&self) -> &[u8] {
self.log.as_slice()
}
/// Where one space's records begin, if the image holds it.
fn space_start(&self, space: &[u8; SPACE_ID_LEN]) -> Result<Option<usize>> {
let starts = space_starts(self)?;
for (candidate, at) in starts.as_slice() {
if candidate == space {
return Ok(Some(*at));
}
}
Ok(None)
}
}
/// Read the image and its log window — not the whole device.
fn read_view() -> Result<View> {
// O_RDONLY is 0 in Linux; filp_open takes the raw flags word.
// SAFETY: `store_device()` is a NUL-terminated C string the module parameter filled at boot,
// and filp_open returns a valid `struct file *` or an error pointer, checked here.
let file = store_file()?;
let view = read_view_from(file);
let view = view?;
if view.image.len() < HEADER_LEN_V1 {
return Err(EINVAL);
}
Ok(view)
}
fn read_view_from(file: *mut bindings::file) -> Result<View> {
let mut scratch = KVVec::<u8>::with_capacity(VIEW_CHUNK, GFP_KERNEL)?;
scratch.resize(VIEW_CHUNK, 0, GFP_KERNEL)?;
// The control block, or the image's own header when the device is a bare image.
let mut head = KVVec::<u8>::with_capacity(CTL_COPY_B + CTL_COPY_LEN, GFP_KERNEL)?;
read_exact_at(file, 0, CTL_COPY_B + CTL_COPY_LEN, &mut head, &mut scratch)?;
let is_device = &head.as_slice()[0..4] == CTL_MAGIC.as_slice();
let (image_off, log_off, log_used, generation) = if is_device {
let control = read_control(head.as_slice()).ok_or(EINVAL)?;
(
control.image_off() as usize,
control.log_off() as usize,
control.log_used as usize,
control.generation,
)
} else {
(0, 0, 0, 0)
};
// What the image says about itself. A device keeps its image at an offset, so its header is
// read from there; a bare image starts with it.
let mut header = if is_device {
let mut raw = KVVec::<u8>::with_capacity(HEADER_LEN_V2, GFP_KERNEL)?;
match read_exact_at(file, image_off as u64, HEADER_LEN_V2, &mut raw, &mut scratch) {
Ok(()) => parse_header(raw.as_slice()),
// A v1 image is shorter than a v2 header, and saying "not a store" about it would be
// wrong: read the six bytes it does have.
Err(_) => {
read_exact_at(file, image_off as u64, HEADER_LEN_V1, &mut raw, &mut scratch)?;
parse_header(raw.as_slice())
}
}
} else {
parse_header(head.as_slice())
}
.map_err(|what| {
pr_err!("cubelinux: {}\n", what);
EINVAL
})?;
let mut image = KVVec::<u8>::new();
let mut log = KVVec::<u8>::new();
match (is_device, header.image_bytes) {
(true, Some(bytes)) => {
let bytes = bytes as usize;
if bytes > MAX_BYTES {
return Err(EINVAL);
}
read_exact_at(file, image_off as u64, bytes, &mut image, &mut scratch)?;
}
// A v1 image states no extent, so it is read to the end — bounded, because a device that
// never ends must not become a read that never ends.
(true, None) => {
read_to_end_at(file, image_off as u64, &mut image, &mut scratch, MAX_BYTES)?;
}
(false, _) => {
read_to_end_at(file, 0, &mut image, &mut scratch, MAX_BYTES)?;
if header.image_bytes.is_none() {
header.image_bytes = Some(image.len() as u64);
}
}
}
if is_device {
if log_used > 0 {
let len = core::cmp::min(WAL_HEADER_LEN + log_used, MAX_BYTES);
read_exact_at(file, log_off as u64, len, &mut log, &mut scratch)?;
}
} else {
// A bare image's log starts after the image, aligned, and runs to the end.
let at = log_offset(image.len() as u64);
read_to_end_at(file, at as u64, &mut log, &mut scratch, MAX_BYTES)?;
}
Ok(View {
image,
log,
header,
generation,
})
}
/// Walk a store image's records, in the order the image holds them.
///
/// Zero-copy: the value a caller gets is a slice of the image already in memory, so listing a
/// store does not copy its values anywhere.
struct Records<'a> {
image: &'a [u8],
off: usize,
end: usize,
left: Option<u64>,
}
impl<'a> Records<'a> {
/// Begin at `off`, for a caller that has been told where a space starts.
///
/// The record count is dropped: it is a bound on the records ahead of the *first* record, and
/// starting anywhere else makes it a bound that no longer means anything. The image's extent
/// is the bound that still holds.
fn at(image: &'a [u8], header: &Header, off: usize) -> Self {
let mut records = Records::new(image, header);
if off > records.off && off <= records.end {
records.off = off;
records.left = None;
}
records
}
fn new(image: &'a [u8], header: &Header) -> Self {
let header_len = if header.version == VERSION_V1 {
HEADER_LEN_V1
} else {
HEADER_LEN_V2
};
let extent = match header.image_bytes {
Some(n) => core::cmp::min(n as usize, image.len()),
None => image.len(),
};
Records {
image,
off: core::cmp::min(header_len, extent),
end: extent,
left: header.record_count,
}
}
fn next(&mut self) -> Option<(&'a [u8; 32], &'a [u8; 24], &'a [u8])> {
if self.left == Some(0) || self.off + RECORD_FIXED > self.end {
return None;
}
let frame = &self.image[self.off..self.off + RECORD_FIXED];
// A v1 image states no count, so padding ends the walk — but only if every remaining byte
// is zero, because a record at the origin with an empty value is 64 zero bytes and is a
// record.
if self.left.is_none()
&& frame.iter().all(|b| *b == 0)
&& self.image[self.off..].iter().all(|b| *b == 0)
{
return None;
}
let mut word = [0u8; 8];
word.copy_from_slice(&frame[SPACE_ID_LEN + RAW_KEY_LEN..RECORD_FIXED]);
let value_len = u64::from_le_bytes(word) as usize;
let value_at = self.off + RECORD_FIXED;
if value_at + value_len > self.end {
return None;
}
let space: &[u8; SPACE_ID_LEN] = frame[..SPACE_ID_LEN].try_into().ok()?;
let key: &[u8; RAW_KEY_LEN] =
frame[SPACE_ID_LEN..SPACE_ID_LEN + RAW_KEY_LEN].try_into().ok()?;
let value = &self.image[value_at..value_at + value_len];
self.off = value_at + value_len;
if let Some(n) = self.left.as_mut() {
*n -= 1;
}
Some((space, key, value))
}
}
/// What the log says about one coordinate: written, or removed.
enum Effect<'a> {
Write(&'a [u8]),
Delete,
}
/// The log's newest word on `(space, key)`, if it says anything about it.
///
/// The log is the only thing that can override the image — it holds what has been written since
/// the last checkpoint — so this is what decides a read.
fn log_effect<'a>(log: &'a [u8], space: &[u8; SPACE_ID_LEN], key: &[u8; RAW_KEY_LEN]) -> Option<Effect<'a>> {
let mut entries = log_entries(log).ok()?;
let mut found = None;
while let Some((entry_space, entry_key, op, _flags, value_at, value_len)) = entries.next() {
if entry_space == space && entry_key == key {
found = Some(if op == 2 {
Effect::Delete
} else {
Effect::Write(&log[value_at..value_at + value_len])
});
}
}
found
}
/// One log entry, as the walk sees it: `(space, key, op, value)`.
struct LogEntries<'a> {
log: &'a [u8],
off: usize,
/// The entry stride for this log's version, and where its length sits.
fixed: usize,
len_at: usize,
/// Whether this log's entries carry a class mask (version 2). A version 1 entry has no mask
/// field, and reads as "no class" — the same rule the index uses for a v3 entry.
flagged: bool,
}
impl<'a> LogEntries<'a> {
/// The next entry: `(space, key, op, flags, where its value starts, its value's length)`.
///
/// The value's *position* rather than a slice, because a caller collecting edits keeps them in
/// a fixed-size vector and a borrow of the log would tie that vector's type to the log's life.
fn next(&mut self) -> Option<(&'a [u8], &'a [u8], u8, u16, usize, usize)> {
if self.off + self.fixed > self.log.len() {
return None;
}
let start = self.off;
let op = self.log[start];
if op != 1 && op != 2 {
return None;
}
let mut word = [0u8; 4];
word.copy_from_slice(&self.log[start + 1..start + 5]);
let crc = u32::from_le_bytes(word);
let space = &self.log[start + 5..start + 37];
let key = &self.log[start + 37..start + 61];
word.copy_from_slice(&self.log[start + self.len_at..start + self.len_at + 4]);
let len = u32::from_le_bytes(word) as usize;
let frame_end = start + self.fixed + len;
if frame_end > self.log.len() {
return None;
}
// The checksum covers space, key, the mask, length and value, so a torn tail is stopped
// at rather than applied — the same rule the fold uses.
if crc32(&self.log[start + 5..frame_end]) != crc {
return None;
}
let flags = if self.flagged {
cube_format::le_u16(self.log, start + 5 + SPACE_ID_LEN + RAW_KEY_LEN)
} else {
0
};
self.off = frame_end;
Some((space, key, op, flags, start + self.fixed, len))
}
}
/// The log's entries, when there is a valid log to read.
fn log_entries(log: &[u8]) -> Result<LogEntries<'_>, &'static str> {
if log.len() < WAL_HEADER_LEN || &log[0..4] != WAL_MAGIC {
return Err("no-log");
}
if log[4] != WAL_VERSION && log[4] != WAL_VERSION_V2 {
return Err("unsupported-log-version");
}
let (fixed, len_at) = wal_frame(log[4]);
Ok(LogEntries {
log,
off: WAL_HEADER_LEN,
fixed,
len_at,
flagged: log[4] == WAL_VERSION_V2,
})
}
/// One space's override of the image, on its way to being merged with it.
///
/// The key and the value borrow the log rather than copying it, so a walker can hand out slices of
/// bytes that are already in memory — a listing never copies a value.
#[derive(Clone, Copy)]
struct LogEdit<'a> {
key: &'a [u8; RAW_KEY_LEN],
/// Position in the log, so the later of two edits to one key is the one that counts.
seq: u32,
value: &'a [u8],
/// The class mask the writer stamped, carried through the merge so a flag scan sees the log's
/// word on a record and not only the image's.
flags: u16,
deleted: bool,
}
/// Every edit the log holds for one space, in key order, later edits last.
///
/// The image and the log are both in key order, so one merge pass over them is a listing — and a
/// space is what a walk names, so only its own edits are collected.
fn log_edits<'a>(
log: &'a [u8],
space: &[u8; SPACE_ID_LEN],
out: &mut KVVec<LogEdit<'a>>,
) -> Result<()> {
out.clear();
let mut entries = match log_entries(log) {
Ok(e) => e,
Err(_) => return Ok(()),
};
let mut seq: u32 = 0;
while let Some((edit_space, key, op, flags, value_at, value_len)) = entries.next() {
if edit_space == space {
let key: &[u8; RAW_KEY_LEN] = match key.try_into() {
Ok(k) => k,
Err(_) => break,
};
out.push(
LogEdit {
key,
seq,
value: &log[value_at..value_at + value_len],
flags,
deleted: op == 2,
},
GFP_KERNEL,
)?;
}
seq += 1;
}
sort_edits(out.as_mut_slice());
Ok(())
}
fn sort_edits(edits: &mut [LogEdit<'_>]) {
let len = edits.len();
if len < 2 {
return;
}
let mut start = len / 2;
while start > 0 {
start -= 1;
sift_down_edits(edits, start, len);
}
let mut end = len;
while end > 1 {
end -= 1;
edits.swap(0, end);
sift_down_edits(edits, 0, end);
}
}
/// Heapsort, for the same reason the entry sort is one: a constant stack frame, so the size of a
/// log cannot decide whether reading it is safe. `sort_unstable` is used nowhere in this module.
fn sift_down_edits(edits: &mut [LogEdit<'_>], mut root: usize, end: usize) {
loop {
let mut child = root * 2 + 1;
if child >= end {
return;
}
if child + 1 < end
&& (edits[child].key, edits[child].seq) < (edits[child + 1].key, edits[child + 1].seq)
{
child += 1;
}
if (edits[root].key, edits[root].seq) >= (edits[child].key, edits[child].seq) {
return;
}
edits.swap(root, child);
root = child;
}
}
/// The live records of one space: the image's records for it, with the log's edits applied, in key
/// order.
///
/// This is the whole of what a listing is — two sorted sequences and one pass — and it is why a
/// walk costs what it returns rather than what the store holds.
struct SpaceWalker<'a> {
records: Records<'a>,
edits: KVVec<LogEdit<'a>>,
edit_at: usize,
image_next: Option<(&'a [u8; RAW_KEY_LEN], &'a [u8])>,
wanted: [u8; SPACE_ID_LEN],
}
impl<'a> SpaceWalker<'a> {
fn new(view: &'a View, wanted: &[u8; SPACE_ID_LEN]) -> Result<Self> {
let mut edits = KVVec::<LogEdit<'a>>::new();
log_edits(view.log(), wanted, &mut edits)?;
// Start where the space starts: a listing of a small space in a large store must not read
// the records that are not in it.
let mut records = match view.space_start(wanted)? {
Some(at) => Records::at(view.image(), &view.header, at),
None => {
let mut empty = Records::new(view.image(), &view.header);
empty.off = empty.end; // nothing in the image: the log's edits are the space
empty
}
};
let image_next = next_in_space(&mut records, wanted);
Ok(SpaceWalker {
records,
edits,
edit_at: 0,
image_next,
wanted: *wanted,
})
}
/// The next live record, or nothing when the space is walked out.
fn next(&mut self) -> Option<(&'a [u8; RAW_KEY_LEN], &'a [u8])> {
loop {
// The entries of one key arrive in log order, so the last of a key's group is the
// newest word on it and the earlier ones are history.
while self.edit_at + 1 < self.edits.len()
&& self.edits[self.edit_at + 1].key == self.edits[self.edit_at].key
{
self.edit_at += 1;
}
let edit = self.edits.as_slice().get(self.edit_at).copied();
match (edit, self.image_next) {
// The log's word on a coordinate the image holds: it wins.
(Some(e), Some((key, _))) if e.key == key => {
self.edit_at += 1;
self.image_next = next_in_space(&mut self.records, &self.wanted);
if !e.deleted {
return Some((e.key, e.value));
}
}
// A record only the log holds, in its key's place.
(Some(e), Some((key, value))) if e.key < key => {
self.edit_at += 1;
self.image_next = Some((key, value));
if !e.deleted {
return Some((e.key, e.value));
}
}
// The image's own record, which the log says nothing about.
(_, Some((key, value))) => {
self.image_next = next_in_space(&mut self.records, &self.wanted);
return Some((key, value));
}
// The image is walked out; the rest of the log still has records to report.
(Some(e), None) => {
self.edit_at += 1;
if !e.deleted {
return Some((e.key, e.value));
}
}
(None, None) => return None,
}
}
}
}
/// Does this space hold at least one live record?
///
/// A space whose every record the log has removed is not a space to report, and a walk that stops
/// at the first record it finds is the cheapest way to know: it costs one record, not a listing.
fn space_has_records(view: &View, space: &[u8; SPACE_ID_LEN]) -> bool {
match SpaceWalker::new(view, space) {
Ok(mut walker) => walker.next().is_some(),
Err(_) => false,
}
}
/// The distinct spaces the log writes into, ascending. Small by construction: the log is what has
/// been written since the last checkpoint.
fn log_spaces(log: &[u8], out: &mut KVVec<[u8; SPACE_ID_LEN]>) -> Result<()> {
out.clear();
let mut entries = match log_entries(log) {
Ok(e) => e,
Err(_) => return Ok(()),
};
while let Some((space, _, _, _, _, _)) = entries.next() {
let mut s = [0u8; SPACE_ID_LEN];
s.copy_from_slice(&space[..SPACE_ID_LEN]);
if !out.as_slice().contains(&s) {
out.push(s, GFP_KERNEL)?;
}
}
sort_spaces(out.as_mut_slice());
Ok(())
}
/// Heapsort for a store's spaces. The log holds few, but `sort_unstable` is not used anywhere in
/// this module: it allocates about 3.5 KiB of kernel stack per level, which is how reading a
/// store once took the machine down.
fn sort_spaces(spaces: &mut [[u8; SPACE_ID_LEN]]) {
let len = spaces.len();
if len < 2 {
return;
}
let mut start = len / 2;
while start > 0 {
start -= 1;
sift_down_spaces(spaces, start, len);
}
let mut end = len;
while end > 1 {
end -= 1;
spaces.swap(0, end);
sift_down_spaces(spaces, 0, end);
}
}
fn sift_down_spaces(spaces: &mut [[u8; SPACE_ID_LEN]], mut root: usize, end: usize) {
loop {
let mut child = root * 2 + 1;
if child >= end {
return;
}
if child + 1 < end && spaces[child] < spaces[child + 1] {
child += 1;
}
if spaces[root] >= spaces[child] {
return;
}
spaces.swap(root, child);
root = child;
}
}
/// Hand the caller a value: its length, or -ENOENT when there is nothing there.
///
/// When the value does not fit in `len` the bytes are *not* copied and the length is returned
/// anyway, so the caller can size its buffer and ask again — a short read that silently truncated
/// would be worse than an error.
fn copy_out(value: &[u8], buf: *mut u8, len: usize) -> isize {
if value.len() > len {
return value.len() as isize;
}
if !value.is_empty() {
// SAFETY: the shim guarantees `buf` holds `len` bytes and `len >= value.len()`.
unsafe {
core::ptr::copy_nonoverlapping(value.as_ptr(), buf, value.len());
}
}
value.len() as isize
}
/// The next record of one space in an image walk, skipping the records of every other space.
fn next_in_space<'a>(
records: &mut Records<'a>,
wanted: &[u8; SPACE_ID_LEN],
) -> Option<(&'a [u8; RAW_KEY_LEN], &'a [u8])> {
while let Some((space, key, value)) = records.next() {
if space == wanted {
return Some((key, value));
}
}
None
}
/// Packs a walk's records into the caller's buffer, skipping what the cursor has covered.
struct Batch<'a> {
out: &'a mut [u8],
written: usize,
returned: u64,
seen: u64,
cursor: u64,
/// Set when a record did not fit: the size it needs, for the `-ERANGE` answer.
too_big: usize,
/// Whether each frame carries its own space.
///
/// A walk's frame leaves the space out because the caller named it — repeating 32 bytes per
/// record to say what the caller already said is a tax on every listing. A scan that names *no*
/// space cannot do that: a coordinate is meaningless without its space, so an every-space
/// answer has to say which space each record came from or the answer is unusable. Hence a frame
/// that depends on the scope, which is the scope's business and not the record's.
with_space: bool,
}
/// The fixed part of a flag-scan frame, before the value.
fn flagged_frame(with_space: bool) -> usize {
RAW_KEY_LEN + FLAGS_LEN + 4 + if with_space { SPACE_ID_LEN } else { 0 }
}
impl Batch<'_> {
/// Offer one record. `false` means nothing more will fit in this batch.
fn offer(&mut self, key: &[u8], value: &[u8]) -> bool {
self.seen += 1;
if self.seen <= self.cursor {
return true;
}
match pack_record(key, value, self.out, self.written) {
Some(n) => {
self.written += n;
self.returned += 1;
true
}
None => {
self.too_big = RAW_KEY_LEN + 4 + value.len();
false
}
}
}
/// Offer one *flagged* record. The counting rule is `offer`'s, and it is what makes the cursor
/// mean "matches already returned" rather than an index position: only a record that passed the
/// mask is counted, so a filtered walk resumes where it left off.
///
/// `space` is carried only when `with_space` is set — see [`Batch::with_space`].
fn offer_flagged(
&mut self,
space: &[u8; SPACE_ID_LEN],
key: &[u8],
flags: u16,
value: &[u8],
) -> bool {
self.seen += 1;
if self.seen <= self.cursor {
return true;
}
match pack_flagged(space, key, flags, value, self.out, self.written, self.with_space) {
Some(n) => {
self.written += n;
self.returned += 1;
true
}
None => {
self.too_big = flagged_frame(self.with_space) + value.len();
false
}
}
}
}
// ── The region walk: a box, its key span, and what decides membership ────────────────────
/// The inclusive box a region walk answers, and the key span that bounds it.
///
/// The span is the whole mechanism of `CUBE_OP_RANGE`: [`cube_format::key_span`] gives the two
/// corner keys, and **every** key in the box lies between them because the interleave is monotone on
/// each axis. So a sorted index can be binary-searched for the foot and read forward to the head.
///
/// It is a **bound and not the set**: a point *outside* the box can have a key inside the span (the
/// classic Z-order amplification), so [`holds`](Self::holds) is what decides and a caller must ask
/// it about every candidate. Confusing the bound for the set is the mistake that would make this
/// return records from outside the region the caller named — and there is no decomposition here to
/// blame it on, which is why the pair is checked in `cube-format`'s tests.
struct Region {
lo: [u64; 3],
hi: [u64; 3],
foot: [u8; RAW_KEY_LEN],
head: [u8; RAW_KEY_LEN],
}
impl Region {
/// `None` for an empty box — inverted on some axis. An empty box has no span worth building,
/// and building one would produce a range whose start is above its end.
fn new(lo: [u64; 3], hi: [u64; 3]) -> Option<Self> {
let mut i = 0;
while i < 3 {
if lo[i] > hi[i] {
return None;
}
i += 1;
}
let (foot, head) = cube_format::key_span(lo, hi);
Some(Region { lo, hi, foot, head })
}
/// Whether a key's point lies in the box — the test that keeps a bound from becoming a wrong
/// answer. This is why `morton_decode` exists.
fn holds(&self, key: &[u8; RAW_KEY_LEN]) -> bool {
let p = cube_format::morton_decode(key);
p[0] >= self.lo[0]
&& p[0] <= self.hi[0]
&& p[1] >= self.lo[1]
&& p[1] <= self.hi[1]
&& p[2] >= self.lo[2]
&& p[2] <= self.hi[2]
}
/// Whether a key sorts below the span, inside it, or above it.
///
/// A dedicated three-way rather than an `Ordering`, because `Ordering` has no room for it: a key
/// between the foot and the head is `Less` *than the head* and that is exactly the case a walk
/// must keep. Returning `key_cmp(key, head)` and matching on it conflates "inside the span" with
/// "below the foot", which silently skips every record in the region the caller asked for —
/// a mistake this code made for one edit, caught before it built.
fn place(&self, key: &[u8; RAW_KEY_LEN]) -> Span {
if cube_format::key_cmp(key, &self.foot) == core::cmp::Ordering::Less {
Span::Below
} else if cube_format::key_cmp(key, &self.head) == core::cmp::Ordering::Greater {
Span::Above
} else {
Span::Inside
}
}
}
/// Where a key sits relative to a region's key span. See [`Region::place`].
enum Span {
/// Sorts before the span's foot: not in the box, and skippable on a layout with no index.
Below,
/// Sorts within the span: a candidate, which still has to pass [`Region::holds`].
Inside,
/// Sorts past the span's head: the walk is finished, because the records are key-ordered.
Above,
}
// ── v3: the addressed reader ────────────────────────────────────────────────────────────
//
// A v3 image is read at the offsets the header describes, so an operation reads the bytes it names
// and nothing else. This is what the coordinate finally buys: a lookup is a binary search over a
// fixed stride, a listing starts where its space starts, and neither reads the store to find out.
/// A v3 image open for reading, with the few bytes an operation needs read on demand.
struct Addressed {
file: *mut bindings::file,
/// Where the image begins in the device.
image_off: u64,
header: HeaderV3,
/// The log window, which is what can override the image and is small.
log: KVVec<u8>,
/// Reused for every span read, so a lookup does not allocate per probe.
scratch: KVVec<u8>,
}
impl Addressed {
/// Open the store and read its control block, v3 header, and log window — not its records.
fn open() -> Result<Option<Self>> {
let file = store_file()?;
let mut opened = Addressed {
file,
image_off: 0,
header: HeaderV3 {
version: VERSION_V3,
image_bytes: 0,
record_count: 0,
space_count: 0,
index_off: 0,
values_off: 0,
},
log: KVVec::new(),
scratch: KVVec::new(),
};
opened.scratch = KVVec::<u8>::with_capacity(4096, GFP_KERNEL)?;
opened.scratch.resize(4096, 0, GFP_KERNEL)?;
let mut head = KVVec::<u8>::with_capacity(CTL_COPY_B + CTL_COPY_LEN, GFP_KERNEL)?;
read_exact_at(file, 0, CTL_COPY_B + CTL_COPY_LEN, &mut head, &mut opened.scratch)?;
if &head.as_slice()[0..4] != CTL_MAGIC.as_slice() {
return Ok(None); // a bare image, which the walking reader handles
}
let control = read_control(head.as_slice()).ok_or(EINVAL)?;
opened.image_off = control.image_off() as u64;
let mut raw = KVVec::<u8>::with_capacity(HEADER_LEN_V3, GFP_KERNEL)?;
read_exact_at(file, opened.image_off, HEADER_LEN_V3, &mut raw, &mut opened.scratch)?;
if raw.as_slice()[4] != VERSION_V3 && raw.as_slice()[4] != VERSION_V4 {
return Ok(None); // v1 or v2: the walking reader
}
opened.header = HeaderV3::decode(raw.as_slice()).map_err(|what| {
pr_err!("cubelinux: {}\n", what);
EINVAL
})?;
if control.log_used > 0 {
let len = core::cmp::min(WAL_HEADER_LEN + control.log_used as usize, MAX_BYTES);
read_exact_at(file, control.log_off() as u64, len, &mut opened.log, &mut opened.scratch)?;
}
Ok(Some(opened))
}
/// Read `len` bytes at an offset *within the image*.
fn read_image_at(&mut self, off: u64, len: usize, out: &mut KVVec<u8>) -> Result<()> {
if off + len as u64 > self.header.image_bytes {
return Err(EINVAL);
}
read_exact_at(self.file, self.image_off + off, len, out, &mut self.scratch)
}
/// The space table entry for a space: where its records start in the index, and how many.
///
/// The table is sorted by space and fixed-size, so this is a binary search over arithmetic
/// addresses — the same reason the index works.
fn space_entry(&mut self, space: &[u8; SPACE_ID_LEN]) -> Result<Option<(u64, u64)>> {
let mut buf = KVVec::<u8>::new();
let mut lo = 0u64;
let mut hi = self.header.space_count;
while lo < hi {
let mid = lo + (hi - lo) / 2;
let at = HEADER_LEN_V3 as u64 + mid * SPACE_ENTRY as u64;
self.read_image_at(at, SPACE_ENTRY, &mut buf)?;
let entry = buf.as_slice();
let found = &entry[..SPACE_ID_LEN];
if found < &space[..] {
lo = mid + 1;
} else if found > &space[..] {
hi = mid;
} else {
let mut w = [0u8; 8];
w.copy_from_slice(&entry[SPACE_ID_LEN..SPACE_ID_LEN + 8]);
let first = u64::from_le_bytes(w);
w.copy_from_slice(&entry[SPACE_ID_LEN + 8..SPACE_ENTRY]);
return Ok(Some((first, u64::from_le_bytes(w))));
}
}
Ok(None)
}
/// One space-table row by its position: the space, where its records start, and how many.
///
/// [`space_entry`](Self::space_entry) answers "where is *this* space" by binary search, which is
/// what a caller who named a space wants. A caller who named none has to visit every space in
/// the table's own order — which is sorted, so the rows come back in the order a checkpoint
/// wrote them and an every-space walk answers in (space, key) order for free.
fn space_row(&mut self, row: u64) -> Result<Option<([u8; SPACE_ID_LEN], u64, u64)>> {
if row >= self.header.space_count {
return Ok(None);
}
let mut buf = KVVec::<u8>::new();
self.read_image_at(HEADER_LEN_V3 as u64 + row * SPACE_ENTRY as u64, SPACE_ENTRY, &mut buf)?;
let entry = buf.as_slice();
let space: [u8; SPACE_ID_LEN] = match entry[..SPACE_ID_LEN].try_into() {
Ok(s) => s,
Err(_) => return Err(EINVAL),
};
let mut w = [0u8; 8];
w.copy_from_slice(&entry[SPACE_ID_LEN..SPACE_ID_LEN + 8]);
let first = u64::from_le_bytes(w);
w.copy_from_slice(&entry[SPACE_ID_LEN + 8..SPACE_ENTRY]);
Ok(Some((space, first, u64::from_le_bytes(w))))
}
/// One index entry, by its position in the index: `(value_off, value_len, flags)`.
///
/// The class mask comes back with the address because it is in the same stride and costs
/// nothing extra to read — and because a flag scan is exactly a walk that reads it and nothing
/// else. A v3 entry has no mask and answers zero, the same way its log entries do.
fn index_entry(&mut self, at: u64, buf: &mut KVVec<u8>) -> Result<(u64, u64, u16)> {
let stride = self.header.stride();
let off = self.header.index_off + at * stride as u64;
self.read_image_at(off, stride, buf)?;
let entry = buf.as_slice();
let flagged = self.header.version == VERSION_V4;
let flags = if flagged {
cube_format::le_u16(entry, RAW_KEY_LEN)
} else {
0
};
let vo = if flagged { RAW_KEY_LEN + FLAGS_LEN } else { RAW_KEY_LEN };
let mut w = [0u8; 8];
w.copy_from_slice(&entry[vo..vo + 8]);
let value_off = u64::from_le_bytes(w);
w.copy_from_slice(&entry[vo + 8..vo + 16]);
Ok((value_off, u64::from_le_bytes(w), flags))
}
/// The address of `key` inside a space's index range, or nothing if the space does not hold it.
fn find(&mut self, key: &[u8; RAW_KEY_LEN], first: u64, records: u64) -> Result<Option<(u64, u64)>> {
let mut buf = KVVec::<u8>::new();
let mut lo = 0u64;
let mut hi = records;
while lo < hi {
let mid = lo + (hi - lo) / 2;
let stride = self.header.stride();
let off = self.header.index_off + (first + mid) * stride as u64;
self.read_image_at(off, stride, &mut buf)?;
let found = &buf.as_slice()[..RAW_KEY_LEN];
if found < &key[..] {
lo = mid + 1;
} else if found > &key[..] {
hi = mid;
} else {
return self
.index_entry(first + mid, &mut buf)
.map(|(value_off, value_len, _)| Some((value_off, value_len)));
}
}
Ok(None)
}
/// The first index position at or **after** `key`, within a space's index range.
///
/// This is `find` answering the other question. `find` asks "where is this exact key" and is only
/// useful for a coordinate read; a region walk asks "where does this span begin", which is the
/// same binary search over the same arithmetic addresses with `>=` in place of `==`. The walk
/// then reads forward from here and stops when it passes the span's head, so the cost is the
/// span rather than the space — and it is the reason the span needed no decomposition.
fn lower_bound(
&mut self,
key: &[u8; RAW_KEY_LEN],
first: u64,
records: u64,
) -> Result<u64> {
let mut buf = KVVec::<u8>::new();
let mut lo = 0u64;
let mut hi = records;
while lo < hi {
let mid = lo + (hi - lo) / 2;
let stride = self.header.stride();
let off = self.header.index_off + (first + mid) * stride as u64;
self.read_image_at(off, stride, &mut buf)?;
if &buf.as_slice()[..RAW_KEY_LEN] < &key[..] {
lo = mid + 1;
} else {
hi = mid;
}
}
Ok(first + lo)
}
/// The value of an index entry, read from where the index says it is.
fn value_at(&mut self, value_off: u64, value_len: u64) -> Result<KVVec<u8>> { let mut out = KVVec::<u8>::new();
self.read_image_at(value_off, value_len as usize, &mut out)?;
Ok(out)
}
/// Write the log's window back into `buf` — used to hand the log to the merge helpers, which
/// take a slice of a log that has already been read.
fn log(&self) -> &[u8] {
self.log.as_slice()
}
}
/// `CUBE_OP_GET` against a v3 image: the log's newest word, else a binary search.
///
/// # Safety
/// `buf` must hold `len` writable bytes.
unsafe fn v3_get(mut image: Addressed, sp: [u8; SPACE_ID_LEN], key: [u8; RAW_KEY_LEN], buf: *mut u8, len: usize) -> isize {
// The log is owned for the duration: what it says borrows it, and reading the image needs
// `&mut image`. It is what a checkpoint has not folded, so it is small.
let mut log = KVVec::<u8>::new();
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
match log_effect(log.as_slice(), &sp, &key) {
Some(Effect::Delete) => return -2, // -ENOENT
Some(Effect::Write(value)) => return copy_out(value, buf, len),
None => {}
}
let (first, records) = match image.space_entry(&sp) {
Ok(Some(entry)) => entry,
Ok(None) => return -2,
Err(e) => return -(e.to_errno() as isize),
};
let (value_off, value_len) = match image.find(&key, first, records) {
Ok(Some(entry)) => entry,
Ok(None) => return -2,
Err(e) => return -(e.to_errno() as isize),
};
let value = match image.value_at(value_off, value_len) {
Ok(v) => v,
Err(e) => return -(e.to_errno() as isize),
};
copy_out(value.as_slice(), buf, len)
}
/// `CUBE_OP_SPACES` against a v3 image: the space table, and whatever only the log writes.
///
/// # Safety
/// `space_out` must point to 32 writable bytes.
unsafe fn v3_spaces(mut image: Addressed, cursor: u64, space_out: *mut u8) -> i32 {
let mut log_only = KVVec::<[u8; SPACE_ID_LEN]>::new();
if log_spaces(image.log(), &mut log_only).is_err() {
return -12;
}
// The table is sorted and fixed-size: the cursor is an index into the *store's* spaces, which
// is the table's spaces merged with the log's. Reading the table a row at a time is a few dozen
// reads of 48 bytes, and it keeps the merge in one order.
let mut index: u64 = 0;
let mut log_at = 0usize;
let mut buf = KVVec::<u8>::new();
let mut row: u64 = 0;
while row < image.header.space_count {
if image.read_image_at(HEADER_LEN_V3 as u64 + row * SPACE_ENTRY as u64, SPACE_ENTRY, &mut buf).is_err() {
return -12;
}
let mut space = [0u8; SPACE_ID_LEN];
space.copy_from_slice(&buf.as_slice()[..SPACE_ID_LEN]);
while log_at < log_only.len() && log_only.as_slice()[log_at] < space {
let candidate = log_only.as_slice()[log_at];
log_at += 1;
if !space_has_records_v3(&mut image, &candidate) {
continue;
}
if index == cursor {
// SAFETY: the caller guarantees 32 writable bytes at `space_out`.
unsafe { core::ptr::copy_nonoverlapping(candidate.as_ptr(), space_out, SPACE_ID_LEN) };
return 0;
}
index += 1;
}
if log_at < log_only.len() && log_only.as_slice()[log_at] == space {
log_at += 1;
}
row += 1;
if !space_has_records_v3(&mut image, &space) {
continue;
}
if index == cursor {
// SAFETY: as above.
unsafe { core::ptr::copy_nonoverlapping(space.as_ptr(), space_out, SPACE_ID_LEN) };
return 0;
}
index += 1;
}
while log_at < log_only.len() {
let candidate = log_only.as_slice()[log_at];
log_at += 1;
if !space_has_records_v3(&mut image, &candidate) {
continue;
}
if index == cursor {
// SAFETY: as above.
unsafe { core::ptr::copy_nonoverlapping(candidate.as_ptr(), space_out, SPACE_ID_LEN) };
return 0;
}
index += 1;
}
-2 // -ENOENT: no such space; the walk is finished
}
/// Does a v3 image's space hold a live record? One index entry, not a listing.
fn space_has_records_v3(image: &mut Addressed, space: &[u8; SPACE_ID_LEN]) -> bool {
match image.space_entry(space) {
Ok(Some((_, records))) => records > 0,
_ => false,
}
}
/// `CUBE_OP_ENUM` against a v3 image.
///
/// With no log edits for the space — the ordinary case, and always the case just after a
/// checkpoint — the cursor is a count of records already returned and the index is fixed-size, so
/// the batch starts at `first + cursor`: arithmetic, and the cost is the records returned. When the
/// log does hold edits, they interleave with the image's records and the walk has to merge them, so
/// it streams the index from the space's start and counts.
///
/// # Safety
/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s.
unsafe fn v3_enum(
mut image: Addressed,
wanted: [u8; SPACE_ID_LEN],
cursor: u64,
buf: *mut u8,
cap: usize,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
let (first, records) = match image.space_entry(&wanted) {
Ok(Some(entry)) => entry,
Ok(None) => (0, 0),
Err(e) => return -(e.to_errno() as i32),
};
// Own the log: the edits borrow it, and walking the image needs `&mut image`.
let mut log = KVVec::<u8>::new();
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
let mut edits = KVVec::<LogEdit<'_>>::new();
if log_edits(log.as_slice(), &wanted, &mut edits).is_err() {
return -12;
}
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
let mut batch = Batch {
out,
written: 0,
returned: 0,
seen: cursor,
cursor,
too_big: 0,
with_space: false,
};
if edits.is_empty() {
// Straight through the index, from the record the cursor names, reading a page of index
// entries at a time and then the span of values they point at.
let mut entry = KVVec::<u8>::new();
let mut at = first + cursor;
while at < first + records {
let (value_off, value_len, _) = match image.index_entry(at, &mut entry) {
Ok(pair) => pair,
Err(e) => return -(e.to_errno() as i32),
};
let value = match image.value_at(value_off, value_len) {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
};
let key: &[u8; RAW_KEY_LEN] = match entry.as_slice()[..RAW_KEY_LEN].try_into() {
Ok(k) => k,
Err(_) => return -22,
};
if !batch.offer(key, value.as_slice()) {
break;
}
at += 1;
}
} else {
// The log's edits interleave, so the merged order is what the cursor counts. Stream the
// space's index entries rather than all of them: the space is what is being listed.
let mut entry = KVVec::<u8>::new();
let mut at = first;
let mut edit_at = 0usize;
loop {
while edit_at + 1 < edits.len() && edits[edit_at + 1].key == edits[edit_at].key {
edit_at += 1;
}
let edit = edits.as_slice().get(edit_at).copied();
let image_entry = if at < first + records {
match image.index_entry(at, &mut entry) {
Ok(pair) => Some(pair),
Err(e) => return -(e.to_errno() as i32),
}
} else {
None
};
let image_key: Option<&[u8; RAW_KEY_LEN]> = match image_entry {
Some(_) => entry.as_slice()[..RAW_KEY_LEN].try_into().ok(),
None => None,
};
match (edit, image_entry, image_key) {
(Some(e), Some((_, value_len, _)), Some(key)) if e.key == key => {
let _ = value_len;
edit_at += 1;
at += 1;
if !e.deleted && !batch.offer(e.key, e.value) {
break;
}
}
(Some(e), Some((value_off, value_len, _)), Some(key)) if e.key < key => {
edit_at += 1;
if !e.deleted && !batch.offer(e.key, e.value) {
break;
}
let _ = (value_off, value_len);
}
(_, Some((value_off, value_len, _)), Some(_)) => {
let value = match image.value_at(value_off, value_len) {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
};
let key = match entry.as_slice()[..RAW_KEY_LEN].try_into() {
Ok(k) => k,
Err(_) => return -22,
};
at += 1;
if !batch.offer(key, value.as_slice()) {
break;
}
}
(Some(e), None, _) => {
edit_at += 1;
if !e.deleted && !batch.offer(e.key, e.value) {
break;
}
}
(None, None, _) => break,
_ => break,
}
}
}
// SAFETY: both out-pointers are writable under this function's contract.
unsafe {
if batch.too_big > 0 && batch.written == 0 {
*out_len = batch.too_big as u64;
*out_cursor = cursor;
return -34; // -ERANGE
}
*out_len = batch.written as u64;
*out_cursor = cursor + batch.returned;
}
0
}
/// `CUBE_OP_RANGE` against a v3 image: **seek** the index to the span's foot, then read forward.
///
/// This is `v3_enum` with two differences, and both are the operation's whole point:
///
/// * it starts at the **span's foot** rather than at the space's first record, found by a binary
/// search over the fixed stride, so the cost is the span and not the space;
/// * it decides with [`Region::holds`] rather than returning everything, and stops the moment the
/// index walks past the span's head, because the index is sorted by key.
///
/// The log's edits interleave exactly as they do in a walk, so this is the same merge — with one
/// added step, which is the log's half of the seek: an edit sorting below the foot cannot be in the
/// box, because the span bounds every key in it.
///
/// Records **examined** can exceed records **returned**, and that excess is the honest cost of a
/// bound: the span contains keys of points outside the box, and `holds` rejects them.
///
/// # Safety
/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s.
unsafe fn v3_range(
mut image: Addressed,
wanted: [u8; SPACE_ID_LEN],
region: Region,
cursor: u64,
buf: *mut u8,
cap: usize,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
let (first, records) = match image.space_entry(&wanted) {
Ok(Some(entry)) => entry,
Ok(None) => (0, 0),
Err(e) => return -(e.to_errno() as i32),
};
// Own the log: the edits borrow it, and walking the image needs `&mut image`.
let mut log = KVVec::<u8>::new();
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
let mut edits = KVVec::<LogEdit<'_>>::new();
if log_edits(log.as_slice(), &wanted, &mut edits).is_err() {
return -12;
}
// The log's half of the seek.
let mut edit_at = 0usize;
while edit_at < edits.len()
&& cube_format::key_cmp(edits.as_slice()[edit_at].key, &region.foot)
== core::cmp::Ordering::Less
{
edit_at += 1;
}
// The index's half.
let mut at = match image.lower_bound(&region.foot, first, records) {
Ok(p) => p,
Err(e) => return -(e.to_errno() as i32),
};
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
let mut batch = Batch {
out,
written: 0,
returned: 0,
// `seen` is what skips the `cursor` records already returned, and it must start at 0: this
// path re-seeks to the span's foot every batch (there is no `at = first + cursor` here, and
// there cannot be — a span holds records that are *not* returned, so the index position of
// the cursor-th match is not arithmetic), so `offer` is the only thing that counts. Starting
// it at `cursor` would re-serve the span from its foot forever.
seen: 0,
cursor,
too_big: 0,
with_space: false,
};
let mut entry = KVVec::<u8>::new();
loop {
// The entries of one key arrive in log order, so the last of a key's group is the newest
// word on it and the earlier ones are history.
while edit_at + 1 < edits.len()
&& edits.as_slice()[edit_at + 1].key == edits.as_slice()[edit_at].key
{
edit_at += 1;
}
let edit = edits.as_slice().get(edit_at).copied();
let image_entry = if at < first + records {
match image.index_entry(at, &mut entry) {
Ok(pair) => Some(pair),
Err(e) => return -(e.to_errno() as i32),
}
} else {
None
};
let image_key: Option<&[u8; RAW_KEY_LEN]> = match image_entry {
Some(_) => entry.as_slice()[..RAW_KEY_LEN].try_into().ok(),
None => None,
};
// The smallest key still ahead of either stream. If it is past the head, the walk is done:
// the index is sorted by key, so nothing further can be inside the span. This is what makes
// the seek stop at the span instead of reading to the end of the space.
let next_key = match (edit.map(|e| e.key), image_key) {
(Some(e), Some(k)) => {
if e < k {
e
} else {
k
}
}
(Some(e), None) => e,
(None, Some(k)) => k,
(None, None) => break,
};
if cube_format::key_cmp(next_key, &region.head) == core::cmp::Ordering::Greater {
break;
}
match (edit, image_entry, image_key) {
// The log's word on a coordinate the image holds: it wins.
(Some(e), Some(_), Some(key)) if e.key == key => {
edit_at += 1;
at += 1;
if !e.deleted && region.holds(e.key) && !batch.offer(e.key, e.value) {
break;
}
}
// A record only the log holds, in its key's place.
(Some(e), Some(_), Some(key)) if e.key < key => {
edit_at += 1;
if !e.deleted && region.holds(e.key) && !batch.offer(e.key, e.value) {
break;
}
}
// The image's own record, which the log says nothing about.
(_, Some((value_off, value_len, _)), Some(key)) => {
let value = match image.value_at(value_off, value_len) {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
};
at += 1;
if region.holds(key) && !batch.offer(key, value.as_slice()) {
break;
}
}
(Some(e), None, _) => {
edit_at += 1;
if !e.deleted && region.holds(e.key) && !batch.offer(e.key, e.value) {
break;
}
}
(None, None, _) => break,
_ => break,
}
}
// SAFETY: both out-pointers are writable under this function's contract.
unsafe {
if batch.too_big > 0 && batch.written == 0 {
*out_len = batch.too_big as u64;
*out_cursor = cursor;
return -34; // -ERANGE
}
*out_len = batch.written as u64;
*out_cursor = cursor + batch.returned;
}
0
}
// ── The flag scan: classify at write, retrieve by class ──────────────────────────────────
//
// The class mask lives in the index entry (v4) and in the log entry (WAL v2), so a scan by class is
// a walk that reads the mask and tests it. Nothing else changes: the same merge with the log, the
// same order. What it buys is that a record which does not match is never offered, so the caller
// pays for its own class rather than for the store.
/// A record matches when it shares *any* bit with the scan mask: "every error", "every Wi-Fi event".
const FLAG_MODE_ANY: u16 = 0;
/// A record matches when it carries *every* bit of the scan mask: "every Wi-Fi error".
const FLAG_MODE_ALL: u16 = 1;
/// The scan's scope: one named space, or every space. Two questions, not one widened.
const FLAG_SCOPE_ONE: u32 = 0;
const FLAG_SCOPE_EVERY: u32 = 1;
/// Whether a record's class mask answers a scan for `mask` under `mode`.
///
/// A mask of zero matches nothing. Asking "what is this" with no class named is asking no question,
/// and a scan that returned everything on an empty mask would be a walk wearing a scan's hat — so
/// this is a refusal rather than a wildcard. `cube-store`'s `scan_by_flag` states the same rule,
/// and the gate diffs the two.
fn flag_matches(flags: u16, mask: u16, mode: u16) -> bool {
if mask == 0 {
return false;
}
if mode == FLAG_MODE_ALL {
flags & mask == mask
} else {
flags & mask != 0
}
}
/// The answer a finished batch gives: how much was written and what cursor to pass next, or
/// `-ERANGE` with the size the record that did not fit would need.
///
/// # Safety
/// `out_len` and `out_cursor` must point to writable `u64`s.
unsafe fn finish_batch(
batch: &Batch<'_>,
cursor: u64,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
// SAFETY: both out-pointers are writable under this function's contract.
unsafe {
if batch.too_big > 0 && batch.written == 0 {
*out_len = batch.too_big as u64;
*out_cursor = cursor;
return -34; // -ERANGE
}
*out_len = batch.written as u64;
*out_cursor = cursor + batch.returned;
}
0
}
/// Scan one space's live records into `batch`, in key order, offering only those whose mask matches.
///
/// This is [`v3_enum`]'s merge with one difference, and it is the operation's whole point: a record
/// is offered only when its class mask answers the scan, so the cursor counts **matches** rather
/// than records walked. That is why a scan re-walks from the space's first record on every batch (as
/// the region walk does) instead of starting at `first + cursor` the way the plain walk can: the
/// index position of the cursor-th match is not arithmetic, because the records before it are not
/// all matches.
///
/// Returns `false` when `batch` is full, so a caller walking several spaces can stop — which is what
/// the every-space scope needs: one batch, several spaces, and one cursor counting matches across
/// all of them.
fn flag_scan_space(
image: &mut Addressed,
log: &[u8],
wanted: &[u8; SPACE_ID_LEN],
first: u64,
records: u64,
mask: u16,
mode: u16,
batch: &mut Batch<'_>,
) -> Result<bool> {
let mut edits = KVVec::<LogEdit<'_>>::new();
log_edits(log, wanted, &mut edits)?;
let mut entry = KVVec::<u8>::new();
let mut at = first;
let mut edit_at = 0usize;
loop {
// The entries of one key arrive in log order, so the last of a key's group is the newest
// word on it — and the newest word carries the mask that counts.
while edit_at + 1 < edits.len() && edits[edit_at + 1].key == edits[edit_at].key {
edit_at += 1;
}
let edit = edits.as_slice().get(edit_at).copied();
let image_entry = if at < first + records {
Some(image.index_entry(at, &mut entry)?)
} else {
None
};
let image_key: Option<&[u8; RAW_KEY_LEN]> = match image_entry {
Some(_) => entry.as_slice()[..RAW_KEY_LEN].try_into().ok(),
None => None,
};
match (edit, image_entry, image_key) {
// The log's word on a coordinate the image holds: it wins, mask and all.
(Some(e), Some(_), Some(key)) if e.key == key => {
edit_at += 1;
at += 1;
if !e.deleted
&& flag_matches(e.flags, mask, mode)
&& !batch.offer_flagged(wanted, e.key, e.flags, e.value)
{
return Ok(false);
}
}
// A record only the log holds, in its key's place.
(Some(e), Some(_), Some(key)) if e.key < key => {
edit_at += 1;
if !e.deleted
&& flag_matches(e.flags, mask, mode)
&& !batch.offer_flagged(wanted, e.key, e.flags, e.value)
{
return Ok(false);
}
}
// The image's own record, which the log says nothing about.
(_, Some((value_off, value_len, flags)), Some(key)) => {
let value = image.value_at(value_off, value_len)?;
at += 1;
if flag_matches(flags, mask, mode)
&& !batch.offer_flagged(wanted, key, flags, value.as_slice())
{
return Ok(false);
}
}
(Some(e), None, _) => {
edit_at += 1;
if !e.deleted
&& flag_matches(e.flags, mask, mode)
&& !batch.offer_flagged(wanted, e.key, e.flags, e.value)
{
return Ok(false);
}
}
// One side ran out and the other has nothing left that could interleave: this space is
// done, not the batch.
_ => return Ok(true),
}
}
}
/// Offer the log's live word on each coordinate it holds for one space.
///
/// This is the whole answer for a store whose image has no index: a packed record carries no mask
/// field, so none of the image's records can answer a scan and only the log's can. That is not a
/// corner — it is the state of every store between the write that classified something and the fold
/// that moves the mask into the index.
///
/// Returns `false` when `batch` is full.
fn flag_scan_log_space(
log: &[u8],
space: &[u8; SPACE_ID_LEN],
mask: u16,
mode: u16,
batch: &mut Batch<'_>,
) -> Result<bool> {
let mut edits = KVVec::<LogEdit<'_>>::new();
log_edits(log, space, &mut edits)?;
let mut edit_at = 0usize;
while edit_at < edits.len() {
// The entries of one key arrive in log order, so the last of a key's group is the newest
// word on it — and only the newest can match, because it is what a read would return.
while edit_at + 1 < edits.len() && edits[edit_at + 1].key == edits[edit_at].key {
edit_at += 1;
}
let e = edits.as_slice()[edit_at];
edit_at += 1;
if !e.deleted
&& flag_matches(e.flags, mask, mode)
&& !batch.offer_flagged(space, e.key, e.flags, e.value)
{
return Ok(false);
}
}
Ok(true)
}
/// A batch ready to receive a flag scan's matches. `seen` starts at 0 for the reason the region
/// walk's does: this path re-walks from the beginning on every batch, so `offer_flagged` is the only
/// thing that counts. Starting it at `cursor` would skip the first `cursor` matches of a fresh walk
/// *and* still count from there, re-serving the first batch forever.
fn flag_batch<'a>(out: &'a mut [u8], cursor: u64, with_space: bool) -> Batch<'a> {
Batch {
out,
written: 0,
returned: 0,
seen: 0,
cursor,
too_big: 0,
with_space,
}
}
/// `CUBE_OP_FLAG_SCAN` against a v3/v4 image, scoped to one space.
///
/// A v3 image is a store whose records were written before the mask existed. Its entries read as
/// "no class", so a scan over it matches nothing and says so by returning an empty batch — which is
/// the honest answer, not an error.
///
/// # Safety
/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s.
unsafe fn v3_flag_scan(
mut image: Addressed,
wanted: [u8; SPACE_ID_LEN],
mask: u16,
mode: u16,
cursor: u64,
buf: *mut u8,
cap: usize,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
let (first, records) = match image.space_entry(&wanted) {
Ok(Some(entry)) => entry,
Ok(None) => (0, 0),
Err(e) => return -(e.to_errno() as i32),
};
// Own the log: the edits borrow it, and walking the image needs `&mut image`.
let mut log = KVVec::<u8>::new();
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
// The caller named the space, so the frame need not repeat it per record.
let mut batch = flag_batch(out, cursor, false);
if let Err(e) = flag_scan_space(
&mut image,
log.as_slice(),
&wanted,
first,
records,
mask,
mode,
&mut batch,
) {
return -(e.to_errno() as i32);
}
// SAFETY: both out-pointers are writable under this function's contract.
unsafe { finish_batch(&batch, cursor, out_len, out_cursor) }
}
/// `CUBE_OP_FLAG_SCAN` with `CUBE_SPACE_EVERY`: the same question asked of every space.
///
/// One batch and one cursor across all of them, so a caller pages through the whole store's matches
/// as one list — which is what "every event anywhere" has to mean if it is to be usable in batches
/// at all. The spaces are visited in the space table's order, and a space the log writes into that
/// the image has never seen is visited in its place in that same order, so the answer comes back in
/// (space, key) order exactly as a checkpoint writes records.
///
/// # Safety
/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s.
unsafe fn v3_flag_scan_every(
mut image: Addressed,
mask: u16,
mode: u16,
cursor: u64,
buf: *mut u8,
cap: usize,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
let mut log = KVVec::<u8>::new();
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
// The spaces the log writes into, sorted — including any the image has never seen, which are the
// ones a plain walk of the space table would miss entirely.
let mut from_log = KVVec::<[u8; SPACE_ID_LEN]>::new();
if log_spaces(log.as_slice(), &mut from_log).is_err() {
return -12;
}
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
// The caller named no space, so every frame has to say which one its record came from.
let mut batch = flag_batch(out, cursor, true);
// One closure would have to borrow `image` and `batch` mutably at once, so the walk steps by
// hand; `full` is the batch saying it has no room for another record.
let mut full = false;
let mut row = 0u64;
let mut log_at = 0usize;
while row < image.header.space_count && !full {
let (space, first, records) = match image.space_row(row) {
Ok(Some(triple)) => triple,
Ok(None) => break,
Err(e) => return -(e.to_errno() as i32),
};
// A space only the log writes sorts before this one: visit it here, in its place.
while log_at < from_log.len()
&& from_log.as_slice()[log_at] < space
&& !full
{
let candidate = from_log.as_slice()[log_at];
log_at += 1;
match flag_scan_space(&mut image, log.as_slice(), &candidate, 0, 0, mask, mode, &mut batch) {
Ok(keep_going) => full = !keep_going,
Err(e) => return -(e.to_errno() as i32),
}
}
// A space the log also writes into is the same space, not a second one.
if log_at < from_log.len() && from_log.as_slice()[log_at] == space {
log_at += 1;
}
if !full {
match flag_scan_space(&mut image, log.as_slice(), &space, first, records, mask, mode, &mut batch) {
Ok(keep_going) => full = !keep_going,
Err(e) => return -(e.to_errno() as i32),
}
}
row += 1;
}
// Whatever the log writes into that sorts after every space the image has.
while log_at < from_log.len() && !full {
let candidate = from_log.as_slice()[log_at];
log_at += 1;
match flag_scan_space(&mut image, log.as_slice(), &candidate, 0, 0, mask, mode, &mut batch) {
Ok(keep_going) => full = !keep_going,
Err(e) => return -(e.to_errno() as i32),
}
}
// SAFETY: both out-pointers are writable under this function's contract.
unsafe { finish_batch(&batch, cursor, out_len, out_cursor) }
}
///
/// Returns the record's length, or a negative errno. When the value does not fit in `len` the
/// bytes are *not* copied and the length is returned anyway, so the caller can size its buffer
/// and ask again — a short read that silently truncated would be worse than an error.
///
/// The log has the last word, because it holds what has been written since the last checkpoint;
/// only if it says nothing about the coordinate does the image answer. Nothing is copied out of
/// either: the value the caller gets is a slice of bytes already in memory.
///
/// # Safety
/// `space` must point to 32 readable bytes; `buf` to `len` writable bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_get(
space: *const u8,
x: u64,
y: u64,
z: u64,
buf: *mut u8,
len: usize,
) -> isize {
let (sp, key) = unsafe { coord_key(space, x, y, z) };
// A v3 image is addressed, so a read is a binary search and a few bytes. v1 and v2 images are
// packed lists, which the walking reader below handles.
match Addressed::open() {
Ok(Some(image)) => return unsafe { v3_get(image, sp, key, buf, len) },
Ok(None) => {}
Err(e) => return -(e.to_errno() as isize),
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as isize),
};
match log_effect(view.log(), &sp, &key) {
Some(Effect::Delete) => return -2, // -ENOENT
Some(Effect::Write(value)) => return copy_out(value, buf, len),
None => {}
}
// Walk the records where they lie, starting where this space's records start: the image is in
// `(space, key)` order, so the record for a coordinate is passed exactly once, and a coordinate
// in a late space does not pay for every record before it.
let at = match view.space_start(&sp) {
Ok(Some(at)) => at,
Ok(None) => return -2, // -ENOENT: the space is not in the image, and the log already spoke
Err(_) => return -12, // -ENOMEM
};
let mut records = Records::at(view.image(), &view.header, at);
while let Some((record_space, record_key, value)) = records.next() {
if record_space == &sp && record_key == &key {
return copy_out(value, buf, len);
}
}
-2 // -ENOENT
}
/// Pack one record the way the walk's uapi names it: `key(24) | value_len(u32, LE) | value`.
///
/// The space is not in the frame because the caller named it, and the order is the store's own
/// (space, then key), so a listing taken through the kernel and one taken in userspace are
/// byte-for-byte comparable — which is how this is tested.
fn pack_record(key: &[u8], value: &[u8], out: &mut [u8], at: usize) -> Option<usize> {
let need = RAW_KEY_LEN + 4 + value.len();
if at + need > out.len() {
return None;
}
out[at..at + RAW_KEY_LEN].copy_from_slice(&key[..RAW_KEY_LEN]);
out[at + RAW_KEY_LEN..at + RAW_KEY_LEN + 4]
.copy_from_slice(&(value.len() as u32).to_le_bytes());
out[at + RAW_KEY_LEN + 4..at + need].copy_from_slice(value);
Some(need)
}
/// Pack one record for a flag scan:
/// `[space(32) |] key(24) | flags(2, LE) | value_len(u32, LE) | value`.
///
/// The mask travels with the record, unlike the walk's frame, because a scan's answer has to say
/// *what class* each record answered with. A record can carry bits the scan did not ask for, and
/// no other op returns a mask — so dropping it here would make those bits unreachable, which is
/// the opposite of what a classification substrate is for. The field order echoes the v4 index
/// entry (`key | flags | …`), so the wire shape is the index's shape.
///
/// The space leads, and only when `with_space` says so, because it is the one field the frame
/// cannot imply when the caller named no space — and it leads rather than trails because the
/// (space, key) order it forms is the order records are stored in and returned in.
fn pack_flagged(
space: &[u8; SPACE_ID_LEN],
key: &[u8],
flags: u16,
value: &[u8],
out: &mut [u8],
at: usize,
with_space: bool,
) -> Option<usize> {
let need = flagged_frame(with_space) + value.len();
if at + need > out.len() {
return None;
}
let mut at = at;
if with_space {
out[at..at + SPACE_ID_LEN].copy_from_slice(&space[..SPACE_ID_LEN]);
at += SPACE_ID_LEN;
}
out[at..at + RAW_KEY_LEN].copy_from_slice(&key[..RAW_KEY_LEN]);
at += RAW_KEY_LEN;
out[at..at + FLAGS_LEN].copy_from_slice(&flags.to_le_bytes());
at += FLAGS_LEN;
out[at..at + 4].copy_from_slice(&(value.len() as u32).to_le_bytes());
at += 4;
out[at..at + value.len()].copy_from_slice(value);
Some(need)
}
/// `CUBE_OP_ENUM`: walk the records of one space into the caller's buffer, `cursor` records in.
///
/// The cursor is a COUNT OF RECORDS ALREADY RETURNED, not a position in the image. It is opaque to
/// the caller (pass back what you were given) and it survives an append, because the contract is
/// "the records I had not yet seen" rather than a snapshot. A record can be seen twice if the
/// image is rewritten underneath a walk; a caller that needs a snapshot takes one.
///
/// The walk walks. It used to read the whole device, parse every record of every space into a
/// merged pool and heapsort all of it, then discard the lot — so asking for one page of one space
/// cost the entire store, and every batch of a listing paid it again. Now the image is read once,
/// its records are walked where they lie in the order a checkpoint gave them, and the log is
/// consulted as an overlay. The cost is the records the walk touches.
///
/// `out_len` receives the bytes written, or with -ERANGE the size the first record that did not fit
/// would need — exactly as a read reports the size it wants.
///
/// # Safety
/// `space` must point to 32 readable bytes; `buf` must hold `cap` writable bytes; `out_len` and
/// `out_cursor` must each point to a writable `u64`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_enum(
space: *const u8,
cursor: u64,
buf: *mut u8,
cap: usize,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
let mut wanted = [0u8; SPACE_ID_LEN];
// SAFETY: the caller guarantees 32 readable bytes at `space`.
unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) };
match Addressed::open() {
Ok(Some(image)) => {
return unsafe { v3_enum(image, wanted, cursor, buf, cap, out_len, out_cursor) }
}
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
};
// The live records of this space: the image's, with the log's edits applied as an overlay.
// A space is what a walk names, so only its own edits are collected — and the log is what a
// checkpoint has not yet folded, so there are few.
let mut walker = match SpaceWalker::new(&view, &wanted) {
Ok(w) => w,
Err(_) => return -12, // -ENOMEM
};
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
let mut batch = Batch {
out,
written: 0,
returned: 0,
seen: 0,
cursor,
too_big: 0,
with_space: false,
};
while let Some((key, value)) = walker.next() {
if !batch.offer(key, value) {
break;
}
}
// SAFETY: both out-pointers are writable under this function's contract.
unsafe {
if batch.too_big > 0 && batch.written == 0 {
// Not one whole record fits. Say how much it needs, the way a read does.
*out_len = batch.too_big as u64;
*out_cursor = cursor;
return -34; // -ERANGE
}
*out_len = batch.written as u64;
*out_cursor = cursor + batch.returned;
}
0
}
/// `CUBE_OP_RANGE`: the live records of one space whose point lies in the caller's box.
///
/// The cursor means what it means for `CUBE_OP_ENUM` — a **count of records already returned**, so it
/// is opaque, survives an append, and is the only end-of-walk signal — with one difference a caller
/// must know: it counts the records **in the box**, because those are the records this returns.
///
/// The box travels as its six numbers rather than as a struct, for the reason a coordinate does: the
/// key the box has to become is this side's business.
///
/// Two paths, and the difference between them is the honest difference between the layouts. A **v3**
/// image is addressed, so this **seeks**: the box's corner keys bound every key in it, the index is
/// sorted, and a binary search for the span's foot followed by a forward read to its head costs the
/// span rather than the space. A **v1/v2** image is a packed list with no index, so there is nothing
/// to search and the space is walked — the span still stops the walk early, but the cost is the walk.
/// Neither path tells the caller which it got, because the contract is the same either way.
///
/// # Safety
/// `space` must point to 32 readable bytes; `buf` must hold `cap` writable bytes; `out_len` and
/// `out_cursor` must each point to a writable `u64`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_range(
space: *const u8,
lo_x: u64,
lo_y: u64,
lo_z: u64,
hi_x: u64,
hi_y: u64,
hi_z: u64,
cursor: u64,
buf: *mut u8,
cap: usize,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
let mut wanted = [0u8; SPACE_ID_LEN];
// SAFETY: the caller guarantees 32 readable bytes at `space`.
unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) };
let region = match Region::new([lo_x, lo_y, lo_z], [hi_x, hi_y, hi_z]) {
Some(region) => region,
// An empty box. Nothing is in it, so the answer is nothing — and answering here keeps an
// inverted span away from the seek, where it would be a range that starts above its end.
// The shim answers this too; a second caller must not be able to reach the seek without it.
None => {
// SAFETY: both out-pointers are writable under this function's contract.
unsafe {
*out_len = 0;
*out_cursor = cursor;
}
return 0;
}
};
match Addressed::open() {
Ok(Some(image)) => {
return unsafe { v3_range(image, wanted, region, cursor, buf, cap, out_len, out_cursor) }
}
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
};
let mut walker = match SpaceWalker::new(&view, &wanted) {
Ok(w) => w,
Err(_) => return -12, // -ENOMEM
};
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
let mut batch = Batch {
out,
written: 0,
returned: 0,
// Same reason as the v3 path: this re-walks the space from its start every batch, and the
// cursor's skip is `offer`'s own `seen`, which counts the matches and starts at 0.
seen: 0,
cursor,
too_big: 0,
with_space: false,
};
while let Some((key, value)) = walker.next() {
match region.place(key) {
// Below the span: not in the box, and skipping it is the seek's substitute on a layout
// that has no index to search.
Span::Below => continue,
// Above the span: the walk is sorted by key, so nothing further can be in the box.
Span::Above => break,
Span::Inside => {}
}
if region.holds(key) && !batch.offer(key, value) {
break;
}
}
// SAFETY: both out-pointers are writable under this function's contract.
unsafe {
if batch.too_big > 0 && batch.written == 0 {
// Not one whole record fits. Say how much it needs, the way a read does.
*out_len = batch.too_big as u64;
*out_cursor = cursor;
return -34; // -ERANGE
}
*out_len = batch.written as u64;
*out_cursor = cursor + batch.returned;
}
0
}
/// `CUBE_OP_FLAG_SCAN`: the live records whose class mask matches, in key order.
///
/// Its cursor counts **matches already returned**, exactly as the region walk's counts records in
/// the box — for the same reason, and with the same consequence: a batch holds as many whole
/// records as fit, most batches come back short, and reading a short batch as the end truncates the
/// answer. A finished scan answers with no records and the cursor unchanged.
///
/// The answer's frame is `key(24) | flags(2, LE) | value_len(u32, LE) | value` — the walk's frame
/// with the class mask in it, because a scan's answer has to say what class each record answered
/// with and no other op returns a mask.
///
/// `every_space` chooses between two questions rather than widening one: a space is a hard
/// partition, so "this class here" and "this class anywhere" are different answers, and the narrower
/// one is what a caller gets by leaving the field zero.
///
/// # Safety
/// `space` must point to 32 readable bytes; `buf` must hold `cap` writable bytes; `out_len` and
/// `out_cursor` must each point to a writable `u64`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_flag_scan(
space: *const u8,
mask: u16,
mode: u16,
every_space: u32,
cursor: u64,
buf: *mut u8,
cap: usize,
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
let mut wanted = [0u8; SPACE_ID_LEN];
// SAFETY: the caller guarantees 32 readable bytes at `space`.
unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) };
// A mode this build does not know is refused rather than defaulted: a caller that asked for
// "all" and silently got "any" would get a superset, and a superset of a security question is
// the worst way to be wrong. The same for a scope it does not know — a caller that asked for
// "every space" and silently got one space would get a subset, which is the other way to be
// wrong and just as quiet.
if mode != FLAG_MODE_ANY && mode != FLAG_MODE_ALL {
return -22; // -EINVAL
}
if every_space != FLAG_SCOPE_ONE && every_space != FLAG_SCOPE_EVERY {
return -22;
}
let every = every_space == FLAG_SCOPE_EVERY;
match Addressed::open() {
Ok(Some(image)) => {
return unsafe {
if every {
v3_flag_scan_every(image, mask, mode, cursor, buf, cap, out_len, out_cursor)
} else {
v3_flag_scan(image, wanted, mask, mode, cursor, buf, cap, out_len, out_cursor)
}
}
}
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
};
// A v1/v2 image holds packed records, which have no mask field at all — they read as "no class"
// and so can never match a scan. What *can* match is the log: its entries are the flagged ones,
// and they carry the mask the writer stamped. So the answer here is the log's live word on each
// coordinate it holds, in key order — a walk's merge with the image's half of it known in
// advance to be empty of matches.
//
// This is not a corner: it is the state of every store between the write that classified
// something and the fold that moves the mask into the index — which is the ordinary state of a
// kernel that has just recorded an event. Answering it with "nothing" would make the substrate
// work only after a checkpoint, which is the opposite of "classify at write time".
let mut log = KVVec::<u8>::new();
if log.extend_from_slice(view.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
let mut batch = flag_batch(out, cursor, every);
if every {
// Every space the log writes into, sorted — which is every space that can hold a match,
// since none of the image's records can.
let mut from_log = KVVec::<[u8; SPACE_ID_LEN]>::new();
if log_spaces(log.as_slice(), &mut from_log).is_err() {
return -12;
}
for space in from_log.as_slice() {
match flag_scan_log_space(log.as_slice(), space, mask, mode, &mut batch) {
Ok(true) => {}
Ok(false) => break,
Err(e) => return -(e.to_errno() as i32),
}
}
} else if let Err(e) = flag_scan_log_space(log.as_slice(), &wanted, mask, mode, &mut batch) {
return -(e.to_errno() as i32);
}
// SAFETY: both out-pointers are writable under this function's contract.
unsafe { finish_batch(&batch, cursor, out_len, out_cursor) }
}
/// `CUBE_OP_SPACES`: the `cursor`-th distinct space that holds a record, or -ENOENT at the end.
///
/// An index rather than a count of records: a caller that wants a space's records walks it with
/// `CUBE_OP_ENUM` once it has learned the name. Entries are already in (space, key) order, so the
/// distinct spaces come out sorted.
///
/// # Safety
/// `space_out` must point to 32 writable bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8) -> i32 {
match Addressed::open() {
Ok(Some(image)) => return unsafe { v3_spaces(image, cursor, space_out) },
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
};
// Candidates, in ascending order: the spaces the image's records run through (a checkpoint
// writes them in key order, and a space is the first half of the key), and any space the log
// writes into that the image has never seen. One pass over the image collects the first kind;
// the second kind is whatever the log holds, which is small.
let mut log_only = KVVec::<[u8; SPACE_ID_LEN]>::new();
if log_spaces(view.log(), &mut log_only).is_err() {
return -12; // -ENOMEM
}
// A space is only worth reporting if it still holds a record: one whose records the log has all
// removed is not a space anybody can list.
let emit = |space: &[u8; SPACE_ID_LEN], index: &mut u64| -> bool {
if !space_has_records(&view, space) {
return false;
}
if *index == cursor {
// SAFETY: the caller guarantees 32 writable bytes at `space_out`.
unsafe {
core::ptr::copy_nonoverlapping(space.as_ptr(), space_out, SPACE_ID_LEN);
}
return true;
}
*index += 1;
false
};
// The image's own spaces are the table of where they start — no pass over the records, because
// that pass already happened when the table was built.
let starts = match space_starts(&view) {
Ok(s) => s,
Err(_) => return -12, // -ENOMEM
};
let mut index: u64 = 0;
let mut log_at = 0usize;
for (space, _) in starts.as_slice() {
let space: [u8; SPACE_ID_LEN] = *space;
while log_at < log_only.len() && log_only.as_slice()[log_at] < space {
let candidate = log_only.as_slice()[log_at];
log_at += 1;
if emit(&candidate, &mut index) {
return 0;
}
}
// A space the log also writes into is the same space, not a second one.
if log_at < log_only.len() && log_only.as_slice()[log_at] == space {
log_at += 1;
}
if emit(&space, &mut index) {
return 0;
}
}
while log_at < log_only.len() {
let candidate = log_only.as_slice()[log_at];
log_at += 1;
if emit(&candidate, &mut index) {
return 0;
}
}
-2 // -ENOENT: no such space; the walk is finished
}
/// `CUBE_OP_DEL`: remove the record at a coordinate.
///
/// A removal is a log entry, not an erasure: nothing in an append-only store is rewritten in
/// place, and the checkpoint is what finally drops it.
///
/// # Safety
/// `space` must point to 32 readable bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_del(space: *const u8, x: u64, y: u64, z: u64) -> i32 {
ensure_boot_record();
let (sp, key) = unsafe { coord_key(space, x, y, z) };
let mutation = Mutation {
space: sp,
key,
flags: 0,
value: KVVec::new(),
};
let (device, layout) = match device_and_layout() {
Ok(pair) => pair,
Err(e) => return -(e.to_errno() as i32),
};
// A delete is an entry with op=2; `append` writes op=1, so build it here from the same
// framing, and let the log's own reader be the judge of it.
match append(&device, &layout, &mutation, 2) {
Ok(_) => 0,
Err(e) => -(e.to_errno() as i32),
}
}
/// `CUBE_OP_SYNC`: fold the log into the image.
#[unsafe(no_mangle)]
pub extern "C" fn cubelinux_kernel_sync() -> i32 {
ensure_boot_record();
let (device, layout) = match device_and_layout() {
Ok(pair) => pair,
Err(e) => return -(e.to_errno() as i32),
};
match fold_now(&device, &layout) {
Ok(()) => 0,
Err(e) => -(e.to_errno() as i32),
}
}
/// 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");
// SAFETY: called exactly once, in the module initializer, before anything can take it.
unsafe { SPACE_STARTS.init() };
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,
)
}
/// Write one operation, framed as the argument block the coordinate interface will pass.
///
/// ```text
/// put op=1 | space(32) | x(8) | y(8) | z(8) | len(4) | value[len]
/// sync op=3
/// ```
///
/// `put` is the byte plane — bytes at a coordinate, with no header written beside them —
/// which is what the store contract defines and what the differential gate compares
/// against userspace's `cell put`. The header tier sits above this.
///
/// `sync` folds the log into the image. It is a real operation rather than a test hook: a
/// caller that wants the log reclaimed — a shutdown, a snapshot, a handover — is entitled
/// to ask.
fn write_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result<usize> {
let mut buf = KVVec::<u8>::new();
let len = iov.copy_from_iter_vec(&mut buf, GFP_KERNEL)?;
let bytes = buf.as_slice();
if bytes.is_empty() {
return Err(EINVAL);
}
// A write to the store device is the kernel taking the write path, so this is one of the
// places a boot gets recorded — the same hook the syscall's write operations call.
ensure_boot_record();
let me = kiocb.file();
// Both operations need the device: its layout says where the image and log are.
let mut device = KVVec::<u8>::new();
read_image(&mut device)?;
let layout = match resolve_layout(&device) {
Ok(l) => l,
Err(what) => {
dev_err!(me.dev, "cubelinux: cannot resolve the store layout: {}\n", what);
return Err(EINVAL);
}
};
match bytes[0] {
OP_PUT => {
if bytes.len() < 1 + 32 + 24 + 4 {
return Err(EINVAL);
}
let mut space = [0u8; 32];
space.copy_from_slice(&bytes[1..33]);
let axis = |at: usize| -> u64 {
let mut w = [0u8; 8];
w.copy_from_slice(&bytes[at..at + 8]);
u64::from_le_bytes(w)
};
let (x, y, z) = (axis(33), axis(41), axis(49));
let mut w = [0u8; 4];
w.copy_from_slice(&bytes[57..61]);
let value_len = u32::from_le_bytes(w) as usize;
if bytes.len() < 61 + value_len {
return Err(EINVAL);
}
let mut value = KVVec::<u8>::new();
value.extend_from_slice(&bytes[61..61 + value_len], GFP_KERNEL)?;
let mutation = Mutation {
space,
key: morton_encode(x, y, z),
flags: 0,
value,
};
match append(&device, &layout, &mutation, 1) {
Ok(_) => {
dev_info!(
me.dev,
"cubelinux: appended {} bytes for a {} byte value\n",
ENTRY_FIXED + value_len,
value_len
);
Ok(len)
}
Err(e) => {
dev_err!(me.dev, "cubelinux: append failed: {:?}\n", e);
Err(e)
}
}
}
OP_SYNC => {
let ctl = match layout.control {
Some(c) => c,
None => {
// A bare image has one slot, so folding into it is not atomic and a
// crash in the middle would lose the store. Refusing is the honest
// answer; the fix is to format the device as a store.
dev_err!(
me.dev,
"cubelinux: cannot checkpoint a bare image (no control block)\n"
);
return Err(EINVAL);
}
};
// The store the image and its log describe, ready to be written out.
let live = &device[layout.image_off..];
let header = match parse_header(live) {
Ok(h) => h,
Err(what) => {
dev_err!(me.dev, "cubelinux: {}\n", what);
return Err(EINVAL);
}
};
let window = log_window(&device, &layout);
let mut merged = match build_merged(live, window, &header) {
Ok(m) => m,
Err(what) => {
dev_err!(me.dev, "cubelinux: cannot build the store: {}\n", what);
return Err(EINVAL);
}
};
match checkpoint(&ctl, &mut merged.0) {
Ok(bytes) => {
dev_info!(me.dev, "cubelinux: checkpointed {} bytes\n", bytes);
Ok(len)
}
Err(e) => {
dev_err!(me.dev, "cubelinux: checkpoint failed: {:?}\n", e);
Err(e)
}
}
}
other => {
dev_err!(me.dev, "cubelinux: unknown operation {}\n", other);
Err(EINVAL)
}
}
}
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())
}
}