diff --git a/drivers/cube/cube_syscall.c b/drivers/cube/cube_syscall.c index 7190923bd..b45b92d87 100644 --- a/drivers/cube/cube_syscall.c +++ b/drivers/cube/cube_syscall.c @@ -15,8 +15,10 @@ * everything that touches the store's bytes is in the other. */ +#include #include #include +#include #include #include #include @@ -33,11 +35,66 @@ ssize_t cubelinux_kernel_get(const __u8 *space, __u64 x, __u64 y, __u64 z, int cubelinux_kernel_del(const __u8 *space, __u64 x, __u64 y, __u64 z); int cubelinux_kernel_sync(void); +/* + * The walk (CUBE_OP_ENUM / CUBE_OP_SPACES). Its argument block travels through the same syscall + * but is a different struct, so these take the pieces rather than a pointer to one — and the C + * side keeps owning everything that touches a userspace pointer. + */ +int cubelinux_kernel_enum(const __u8 *space, __u64 cursor, void *buf, size_t cap, + __u64 *out_len, __u64 *out_cursor); +int cubelinux_kernel_spaces(__u64 cursor, __u8 *space_out); + +/* The store device path, resolved from the `cube_store=` boot parameter at boot. */ +const char *cubelinux_store_device(void); + +/* + * Where the store lives. + * + * A fixed `/dev/vda` was honest while a virtual machine was the only place this ran; the same + * driver now has to read the box's own device as well, and a constant cannot be both. So it is + * a boot parameter, and the box names its device on the kernel command line: + * + * cube_store=/dev/nvme0n1p2 + * + * (`__setup` rather than `module_param_string`, deliberately. Built-in code registers module + * parameters under its *object's* name — `MODULE_PARAM_PREFIX` is `KBUILD_MODNAME "."` when + * MODULE is not defined — so a `module_param_string` here would answer to + * `cube_syscall.store_device`, named after this file rather than after the driver. That is a + * name nobody would guess and one more thing to get wrong at 3am. A `__setup` parameter is + * named exactly as written.) + * + * This lives in 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. The Rust side asks for the string; it does not store it. + * + * The default keeps the boot gate's shape, so the QEMU rehearsal is unchanged. + */ +static char store_device_path[256] = "/dev/vda"; + +static int __init cube_store_setup(char *str) +{ + strscpy(store_device_path, str, sizeof(store_device_path)); + return 1; +} +__setup("cube_store=", cube_store_setup); + +/* The Rust half reads the path through this; the storage stays here. */ +const char *cubelinux_store_device(void) +{ + return store_device_path; +} + /* 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) -SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs) +/* The walk's buffer ceiling: the caller offers whatever it likes, up to this. */ +#define CUBE_MAX_WALK (16u * 1024u * 1024u) + +/* + * The coordinate operations — put, get, del, sync — which travel in `struct cube_args`. + */ +static long cube_args_op(unsigned int op, void __user *uargs) { struct cube_args args; void *buf = NULL; @@ -129,3 +186,88 @@ SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs) kvfree(buf); return ret; } + +/* + * The walk — enum and spaces — which travel in `struct cube_enum_args`: its own block so that the + * coordinate is not both an input and an output, and so the interface can grow by getting a new + * size rather than being replaced. + * + * A batch fills the caller's buffer and returns how much was used plus the cursor to pass next. + * A record that does not fit ends the batch; a record that cannot fit in any buffer the caller + * offered comes back as -ERANGE with `len` saying what it would need, exactly as a read does. So + * nobody guesses a size and nobody gets half a record. + */ +static long cube_enum_op(unsigned int op, void __user *uargs) +{ + struct cube_enum_args e; + void *buf = NULL; + long ret = 0; + u64 out_len = 0, out_cursor = 0; + + if (copy_from_user(&e, uargs, sizeof(e))) + return -EFAULT; + if (e.size != sizeof(struct cube_enum_args)) + return -EINVAL; + + if (op == CUBE_OP_SPACES) { + __u8 found[32]; + + ret = cubelinux_kernel_spaces(e.cursor, found); + if (ret < 0) + return ret; + memcpy(e.space, found, sizeof(found)); + /* An index here, not a count of records: hand back the one after this space. */ + e.cursor = e.cursor + 1; + e.len = 0; + if (copy_to_user(uargs, &e, sizeof(e))) + return -EFAULT; + return 0; + } + + if (op != CUBE_OP_ENUM) + return -EINVAL; + if (e.len > CUBE_MAX_WALK) + return -E2BIG; + if (e.len > 0) { + buf = kvmalloc(e.len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + } + + ret = cubelinux_kernel_enum(e.space, e.cursor, buf, e.len, &out_len, &out_cursor); + if (ret == 0) { + if (out_len > 0 && copy_to_user((void __user *)e.value, buf, out_len)) + ret = -EFAULT; + e.len = out_len; + e.cursor = out_cursor; + if (copy_to_user(uargs, &e, sizeof(e))) + ret = -EFAULT; + } else if (ret == -ERANGE) { + /* Nothing was written; `len` now says how much one record needs. */ + e.len = out_len; + if (copy_to_user(uargs, &e, sizeof(e))) + ret = -EFAULT; + } + + kvfree(buf); + return ret; +} + +/* + * One syscall, two argument blocks. They share a prefix — `size`, then `op` — so the size the + * caller declares is what says which one arrived. That is the whole point of putting `size` + * first: an interface that cannot grow has to be replaced, and this one grows by being given a + * new block with a new size. + */ +SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs) +{ + __u32 size; + + if (copy_from_user(&size, uargs, sizeof(size))) + return -EFAULT; + if (size == sizeof(struct cube_args)) + return cube_args_op(op, uargs); + if (size == sizeof(struct cube_enum_args)) + return cube_enum_op(op, uargs); + return -EINVAL; +} diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index c6f663826..94aa26fb3 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -75,13 +75,44 @@ const SPACE_ID_LEN: usize = 32; const RAW_KEY_LEN: usize = 24; const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8; -/// The device to read. A module parameter is the obvious next step; for the boot gate one -/// fixed name is honest and has one less way to be wrong. -const STORE_DEVICE: &core::ffi::CStr = c_str!("/dev/vda"); +// 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; +} -/// Refuse to pull an unbounded device into memory. The gate's images are tiny; this is the -/// guard against a wrong device name turning a read into an allocation storm. -const MAX_BYTES: usize = 64 * 1024 * 1024; +/// 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. @@ -170,9 +201,10 @@ fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 { /// filesystem, say), that is the moment to reach for the block layer directly. 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 literal, and filp_open either - // returns a valid `struct file *` or an error pointer, which is checked below. - let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::(), 0, 0) }; + // 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 = unsafe { bindings::filp_open(store_device(), 0, 0) }; let file = kernel::error::from_err_ptr(file)?; if file.is_null() { return Err(EINVAL); @@ -1017,9 +1049,9 @@ fn append( return Err(ENOSPC); } - // SAFETY: STORE_DEVICE is a NUL-terminated literal; filp_open returns a valid file or an - // error pointer, which is checked. O_RDWR is 2. - let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::(), 2, 0) }; + // 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 = unsafe { bindings::filp_open(store_device(), 2, 0) }; let file = kernel::error::from_err_ptr(file)?; if file.is_null() { return Err(EINVAL); @@ -1134,7 +1166,7 @@ fn checkpoint(ctl: &Control, merged: &mut Merged) -> Result { } // SAFETY: as in `append`. - let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::(), 2, 0) }; + let file = unsafe { bindings::filp_open(store_device(), 2, 0) }; let file = kernel::error::from_err_ptr(file)?; if file.is_null() { return Err(EINVAL); @@ -1295,6 +1327,202 @@ pub unsafe extern "C" fn cubelinux_kernel_get( } } +/// 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) +} + +/// `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 is O(records) per call, deliberately. The alternative is a merged index held in the +/// kernel and invalidated by every write — more to be wrong about than a listing is worth, and the +/// live store's 69,636 records make a page of listing tens of milliseconds. +/// +/// `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) }; + + let (device, layout) = match device_and_layout() { + Ok(pair) => pair, + Err(e) => return -(e.to_errno() as i32), + }; + let live = &device[layout.image_off..]; + let header = match parse_header(live) { + Ok(h) => h, + Err(what) => { + pr_err!("cubelinux: {}\n", what); + return -22; // -EINVAL + } + }; + let window = log_window(&device, &layout); + let end = core::cmp::min( + header.image_bytes.unwrap_or(live.len() as u64) as usize, + live.len(), + ); + let (mut merged, _applied) = match build_merged(&live[..end], window, &header) { + Ok(m) => m, + Err(what) => { + pr_err!("cubelinux: cannot build the store: {}\n", what); + return -22; + } + }; + merged.sort_entries(); + let entries = merged.entries.as_slice(); + // SAFETY: the shim guarantees `cap` writable bytes at `buf`. + let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) }; + + let mut written = 0usize; + let mut returned: u64 = 0; + let mut seen: u64 = 0; + let mut first_too_big: usize = 0; + + let mut i = 0usize; + while i < entries.len() { + let head = &entries[i]; + // The winner of a coordinate is the LAST of its group: the newest write, or a removal. + 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 || winner.space != wanted { + continue; + } + seen += 1; + if seen <= cursor { + continue; + } + match pack_record(&winner.key, merged.value(winner), out, written) { + Some(n) => { + written += n; + returned += 1; + } + None => { + first_too_big = RAW_KEY_LEN + 4 + winner.value_len as usize; + break; + } + } + } + + // SAFETY: both out-pointers are writable under this function's contract. + unsafe { + if first_too_big > 0 && written == 0 { + // Not one whole record fits. Say how much it needs, the way a read does. + *out_len = first_too_big as u64; + *out_cursor = cursor; + return -34; // -ERANGE + } + *out_len = written as u64; + *out_cursor = cursor + returned; + } + 0 +} + +/// `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 { + let (device, layout) = match device_and_layout() { + Ok(pair) => pair, + Err(e) => return -(e.to_errno() as i32), + }; + let live = &device[layout.image_off..]; + let header = match parse_header(live) { + Ok(h) => h, + Err(what) => { + pr_err!("cubelinux: {}\n", what); + return -22; + } + }; + let window = log_window(&device, &layout); + let end = core::cmp::min( + header.image_bytes.unwrap_or(live.len() as u64) as usize, + live.len(), + ); + let (mut merged, _applied) = match build_merged(&live[..end], window, &header) { + Ok(m) => m, + Err(what) => { + pr_err!("cubelinux: cannot build the store: {}\n", what); + return -22; + } + }; + merged.sort_entries(); + let entries = merged.entries.as_slice(); + + let mut index: u64 = 0; + let mut previous: Option<[u8; SPACE_ID_LEN]> = None; + let mut i = 0usize; + 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; + } + if previous != Some(winner.space) { + if index == cursor { + // SAFETY: the caller guarantees 32 writable bytes at `space_out`. + unsafe { + core::ptr::copy_nonoverlapping(winner.space.as_ptr(), space_out, SPACE_ID_LEN); + } + return 0; + } + index += 1; + previous = Some(winner.space); + } + } + -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 diff --git a/include/uapi/linux/cube.h b/include/uapi/linux/cube.h index e7e2cd04a..7d18efef8 100644 --- a/include/uapi/linux/cube.h +++ b/include/uapi/linux/cube.h @@ -51,13 +51,20 @@ struct cube_args { * order — space first, then key — which is the order a checkpoint writes them and the order the * userspace store returns them, so a kernel listing and a userspace listing can be compared * directly. The space is not repeated per record: the caller named it. + * + * A walk ends when the cursor stops moving, and that is the only end signal: a batch holds as + * many whole records as fit, so most batches come back short, and reading a short batch as the + * end truncates a listing to its first batch. CUBE_OP_ENUM answers a finished walk with no + * records and the cursor unchanged; CUBE_OP_SPACES answers it with -ENOENT, because asking for + * the space after the last one is asking for a space that is not there. -ERANGE keeps its usual + * meaning: not one whole record fits, and `len` says how much one needs. */ struct cube_enum_args { __u32 size; /* sizeof(struct cube_enum_args) as the caller built it */ __u32 op; /* CUBE_OP_ENUM or CUBE_OP_SPACES */ __u8 space[32]; /* in: the space to walk; out: the space found (CUBE_OP_SPACES) */ __u64 cursor; /* in: 0 to start, or what the last call returned; - * out: what to pass next + * out: what to pass next — see the end-of-walk rule above */ __u64 value; /* user pointer: where to put the records */ __u64 len; /* in: the buffer's capacity;