// 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 ` 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::() } } /// 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> = 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) -> 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::::with_capacity(4096, GFP_KERNEL)?; chunk.extend_from_slice(&[0u8; 4096][..], GFP_KERNEL)?; let mut result = Ok(()); loop { // SAFETY: `file` is a live `struct file *` from filp_open; `chunk` is a 4096-byte // kernel buffer we own; `pos` is a valid loff_t. kernel_read reads at most // `chunk.len()` bytes into the buffer and does not retain either pointer. let n = unsafe { bindings::kernel_read( file, chunk.as_mut_ptr().cast::(), chunk.len(), &mut pos, ) }; if n < 0 { result = Err(Error::from_errno(n as i32)); break; } if n == 0 { break; // end of device } let n = n as usize; if let Err(e) = image.extend_from_slice(&chunk[..n], GFP_KERNEL) { result = Err(e.into()); break; } if image.len() >= MAX_BYTES { break; } } // 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 { 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, 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 { // 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 { 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 { 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, } /// 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 { 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, entries: KVVec, seq: u32, /// Log entries that were seen but not applied, because they were torn or malformed. dropped: u64, } impl Merged { fn new() -> Result { 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::` 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, &'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 { // 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 { 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, } /// 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, AllocError> { let flagged = version == WAL_VERSION_V2; let fixed = if flagged { ENTRY_FIXED_V2 } else { ENTRY_FIXED }; let mut entry = KVVec::::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::(), 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, 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 && ®ion[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 || ®ion[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, 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::::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::::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 { 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, Layout), Error> { let mut device = KVVec::::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, 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, 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= device= kernel=`. /// /// 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> { let mut out = KVVec::::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::(); 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::::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, scratch: &mut KVVec, ) -> 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::(), 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, scratch: &mut KVVec, 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::(), 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, log: KVVec, 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> = 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> { 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> { 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 { // 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 { let mut scratch = KVVec::::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::::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::::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::::new(); let mut log = KVVec::::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, } 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> { 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, &'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>, ) -> 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>, 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 { let mut edits = KVVec::>::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 { 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, /// Reused for every span read, so a lookup does not allocate per probe. scratch: KVVec, } impl Addressed { /// Open the store and read its control block, v3 header, and log window — not its records. fn open() -> Result> { 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::::with_capacity(4096, GFP_KERNEL)?; opened.scratch.resize(4096, 0, GFP_KERNEL)?; let mut head = KVVec::::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::::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) -> 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> { let mut buf = KVVec::::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> { if row >= self.header.space_count { return Ok(None); } let mut buf = KVVec::::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) -> 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> { let mut buf = KVVec::::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 { let mut buf = KVVec::::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> { let mut out = KVVec::::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::::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::::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::::new(); if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() { return -12; // -ENOMEM } let mut edits = KVVec::>::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::::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::::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::::new(); if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() { return -12; // -ENOMEM } let mut edits = KVVec::>::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, ®ion.foot) == core::cmp::Ordering::Less { edit_at += 1; } // The index's half. let mut at = match image.lower_bound(®ion.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::::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, ®ion.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 { let mut edits = KVVec::>::new(); log_edits(log, wanted, &mut edits)?; let mut entry = KVVec::::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 { let mut edits = KVVec::>::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::::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::::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 { 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 { 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::::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, } impl kernel::InPlaceModule for CubeStoreModule { fn init(_module: &'static ThisModule) -> impl PinInit { 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, } #[vtable] impl MiscDevice for CubeStore { type Ptr = Pin>; fn open(_file: &File, misc: &MiscDeviceRegistration) -> Result>> { KBox::try_pin_init( try_pin_init! { CubeStore { dev: ARef::from(misc.device()) } }, GFP_KERNEL, ) } /// 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 { let mut buf = KVVec::::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::::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::::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 { // The work happens here, on demand, rather than at init: by the time anything can // open this device, the block driver that provides the store is certainly up. let mut image = KVVec::::new(); let line = match read_image(&mut image) { Ok(()) => digest(&image), Err(e) => { let mut line = Line::new(); let _ = write!(line, "error={:?}", e); line } }; pr_info!("cubelinux: {}\n", core::str::from_utf8(line.as_bytes()).unwrap_or("")); let me = kiocb.file(); dev_info!(me.dev, "cubelinux: store read complete\n"); iov.simple_read_from_buffer(kiocb.ki_pos_mut(), line.as_bytes()) } }