From 087c6b0e607b9d014429f11393e33eb6d38e38b6 Mon Sep 17 00:00:00 2001 From: surface-camera-build Date: Mon, 21 Sep 2026 18:05:10 -0400 Subject: [PATCH] cubelinux: the kernel records its own boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second half of "the OS stores itself". The store was already the kernel's; what was missing was the kernel *saying* something of its own rather than a client doing it. At its first write of a boot the driver now appends one record describing the boot it is having — its own version banner, the wall-clock time, and the store device it resolved — to a reserved space, through the same append path every other mutation uses. Three things had to be decided, and each had a wrong answer that looked right: WHERE IT HOOKS. "At store init" does not exist and must not be invented: there is no init-time open, deliberately, 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. The moment is the FIRST WRITE, which needs no ordering at all and is the semantically right one — the kernel records itself when it becomes the writer. A boot in which the kernel only reads writes no record, which is honest rather than a gap. WHICH SPACE. 0xFD was the first choice, following the convention that a reserved space is one repeated byte. 0xFD is the OS KEYSTORE — the space the kill switch exists to destroy — and writing boot records into it would have been a serious bug. Only the userspace name table catches this (format_space in crates/cube-command) because the kernel keeps no table of space names, so the table is now written down where the constant is: 0x00 root, 0xFF edges, 0xFE portal, 0xFD keystore, 0xFC boot. The record lives at (0,0,0) in 0xFC: one record, the current boot. WHAT IT SAYS. boot= device= kernel=, banner last and unquoted so everything after the final = is the kernel's own words rather than a field this code parsed. Raw epoch seconds rather than a date: 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. The banner comes from linux_banner and the time from ktime_get_real_ts64. WHY IT IS OFF BY DEFAULT. Not caution, but an invariant. 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 record the kernel injects that the caller never asked for would turn those comparisons into non-comparisons. So it is cube_boot_record=1 on the kernel command line, parsed in C beside cube_store= for the reason that parameter is in C (this kernel's Rust cannot express a string parameter), and kernel/verify-boot-record.sh is the gate that turns it on. A failure to record is logged and never propagated: the record is worth having and is not a precondition for the caller's write. It is attempted once per boot rather than retried per write, because a store that will not take it will not take it later, and one warning is information where a stream of them is noise. --- drivers/cube/cube_syscall.c | 29 +++++ drivers/cube/cubelinux_store.rs | 192 ++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) diff --git a/drivers/cube/cube_syscall.c b/drivers/cube/cube_syscall.c index b45b92d87..19eaf332a 100644 --- a/drivers/cube/cube_syscall.c +++ b/drivers/cube/cube_syscall.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -84,6 +85,34 @@ const char *cubelinux_store_device(void) return store_device_path; } +/* + * Whether the kernel should record its own boots in the store. + * + * Off unless asked for, and the reason is not caution: the gate method of this tree is that the + * store the kernel produces is comparable, byte for byte, with the store userspace produces from the + * same mutations. A record the kernel injects that the caller never asked for would turn two of + * those comparisons into non-comparisons. So it is a command-line switch, beside `cube_store=`, + * and the gate that proves it is the one that turns it on. + * + * cube_store=/var/lib/cubelinux/store.img cube_boot_record=1 + * + * The hook itself is not here: it lives in the Rust driver and fires at the first write of a boot, + * which is named and argued where it is implemented (`BOOT_SPACE` in cubelinux_store.rs). + */ +static bool boot_record_enabled; + +static int __init cube_boot_record_setup(char *str) +{ + boot_record_enabled = (str[0] == '1'); + return 1; +} +__setup("cube_boot_record=", cube_boot_record_setup); + +bool cubelinux_boot_record_enabled(void) +{ + return boot_record_enabled; +} + /* No value may be larger than this in one call. A coordinate store is not a bulk-file path; * a caller with more than this to store has more than one record to store. */ #define CUBE_MAX_VALUE (16u * 1024u * 1024u) diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index 966e92147..f3f550ad5 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -41,6 +41,7 @@ //! ``` use core::fmt::{self, Write}; +use core::sync::atomic::{AtomicBool, Ordering}; // The store's format, in one file, shared with userspace. // @@ -133,6 +134,13 @@ const RECORD_FIXED: usize = cube_format::RECORD_FIXED; 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. @@ -188,6 +196,56 @@ 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 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; @@ -1339,6 +1397,134 @@ unsafe fn coord_key(space: *const u8, x: u64, y: u64, z: u64) -> ([u8; 32], [u8; (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), + 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. /// /// # Safety @@ -1353,6 +1539,7 @@ pub unsafe extern "C" fn cubelinux_kernel_put( value: *const u8, len: usize, ) -> i32 { + ensure_boot_record(); let (sp, key) = unsafe { coord_key(space, x, y, z) }; let mut bytes = KVVec::::new(); if len > 0 { @@ -2778,6 +2965,7 @@ pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8 /// `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, @@ -2799,6 +2987,7 @@ pub unsafe extern "C" fn cubelinux_kernel_del(space: *const u8, x: u64, y: u64, /// `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), @@ -2886,6 +3075,9 @@ impl MiscDevice for CubeStore { 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.