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.