diff --git a/Makefile b/Makefile index 3252209df..fd6555e0d 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ NAME = CUBELinux # release. The base version stays in VERSION/PATCHLEVEL/SUBLEVEL above (visible in # `make kernelversion`), while `uname -r` and /lib/modules report the CUBELinux # release, with any -dirty or SCM suffix still appended by setlocalversion. -CUBELINUX_VERSION = CUBELinux.0.5 +CUBELINUX_VERSION = CUBELinux.0.6 # *DOCUMENTATION* # To see a list of typical targets execute "make help" diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl index 8a4ac4841..8931eab48 100644 --- a/arch/x86/entry/syscalls/syscall_64.tbl +++ b/arch/x86/entry/syscalls/syscall_64.tbl @@ -440,3 +440,4 @@ 547 x32 pwritev2 compat_sys_pwritev64v2 # This is the end of the legacy x32 range. Numbers 548 and above are # not special and are not to be used for x32-specific syscalls. +548 common cube sys_cube diff --git a/drivers/cube/Makefile b/drivers/cube/Makefile index adbe17385..96d156468 100644 --- a/drivers/cube/Makefile +++ b/drivers/cube/Makefile @@ -1,2 +1,2 @@ # SPDX-License-Identifier: GPL-2.0 -obj-$(CONFIG_CUBELINUX_STORE) += cubelinux_store.o +obj-$(CONFIG_CUBELINUX_STORE) += cubelinux_store.o cube_syscall.o diff --git a/drivers/cube/cube_syscall.c b/drivers/cube/cube_syscall.c new file mode 100644 index 000000000..7190923bd --- /dev/null +++ b/drivers/cube/cube_syscall.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CUBELinux: the `cube(2)` syscall — the kernel's coordinate interface. + * + * The store's operations live in Rust (drivers/cube/cubelinux_store.rs) and are already + * proven: they append to a write-ahead log durably, replay it, fold it into the image, and + * survive a torn tail. What was missing was a way for a program to *call* them. This file is + * that way and nothing else. + * + * Why C for the entry point: syscalls are defined by `SYSCALL_DEFINE*`, which is a C macro + * that registers the function in the syscall table with the right calling convention. Rust in + * this kernel cannot define one, so the entry point, the user copies and the argument + * validation are here, and the Rust side sees only kernel memory and validated numbers. That + * split is deliberate: everything that touches a userspace pointer is in one place, and + * everything that touches the store's bytes is in the other. + */ + +#include +#include +#include +#include +#include + +/* + * Implemented in Rust. The coordinate is passed as its parts rather than as a struct, so the + * format knowledge on the Rust side stays in the module that owns it — including the Morton + * encoding, which must produce exactly the key a userspace reader decodes. + */ +int cubelinux_kernel_put(const __u8 *space, __u64 x, __u64 y, __u64 z, + const void *value, size_t len); +ssize_t cubelinux_kernel_get(const __u8 *space, __u64 x, __u64 y, __u64 z, + void *buf, size_t len); +int cubelinux_kernel_del(const __u8 *space, __u64 x, __u64 y, __u64 z); +int cubelinux_kernel_sync(void); + +/* 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) +{ + struct cube_args args; + void *buf = NULL; + long ret = 0; + + if (copy_from_user(&args, uargs, sizeof(args))) + return -EFAULT; + + /* + * The size is the caller's, and it must be the one this kernel implements: a caller + * built against a later block would otherwise have fields silently ignored. + */ + if (args.size != sizeof(struct cube_args)) + return -EINVAL; + + switch (op) { + case CUBE_OP_PUT: + case CUBE_OP_GET: + break; + case CUBE_OP_DEL: + case CUBE_OP_SYNC: + break; + default: + return -EINVAL; + } + + if (op == CUBE_OP_PUT || op == CUBE_OP_GET) { + if (args.len > CUBE_MAX_VALUE) + return -E2BIG; + if (args.len > 0) { + buf = kvmalloc(args.len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + } + } + + switch (op) { + case CUBE_OP_PUT: + if (args.len > 0 && + copy_from_user(buf, (void __user *)args.value, args.len)) { + ret = -EFAULT; + break; + } + ret = cubelinux_kernel_put(args.coord.space, args.coord.x, + args.coord.y, args.coord.z, buf, args.len); + break; + + case CUBE_OP_GET: { + ssize_t got; + + got = cubelinux_kernel_get(args.coord.space, args.coord.x, + args.coord.y, args.coord.z, buf, args.len); + if (got < 0) { + ret = got; + break; + } + if ((u64)got > args.len) { + /* + * Too small. Tell the caller how much it needs, so a read is two + * calls at worst and never a guess. + */ + args.len = (u64)got; + if (copy_to_user(uargs, &args, sizeof(args))) + ret = -EFAULT; + else + ret = -ERANGE; + break; + } + if (got > 0 && copy_to_user((void __user *)args.value, buf, got)) { + ret = -EFAULT; + break; + } + args.len = (u64)got; + if (copy_to_user(uargs, &args, sizeof(args))) + ret = -EFAULT; + break; + } + + case CUBE_OP_DEL: + ret = cubelinux_kernel_del(args.coord.space, args.coord.x, + args.coord.y, args.coord.z); + break; + + case CUBE_OP_SYNC: + ret = cubelinux_kernel_sync(); + break; + } + + kvfree(buf); + return ret; +} diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index f4c5430b3..78c053d51 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -856,10 +856,10 @@ struct Mutation { } /// Build a log entry: `op | crc32 | space | key | len | value`. -fn encode_entry(space: &[u8; 32], key: &[u8; 24], value: &[u8]) -> Result, AllocError> { +fn encode_entry(op: u8, space: &[u8; 32], key: &[u8; 24], value: &[u8]) -> Result, AllocError> { let total = ENTRY_FIXED + value.len(); let mut entry = KVVec::::with_capacity(total, GFP_KERNEL)?; - entry.extend_from_slice(&[1u8], 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)?; @@ -922,8 +922,13 @@ fn older_copy(generation: u64) -> usize { /// 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) -> Result, Error> { - let entry = encode_entry(&m.space, &m.key, m.value.as_slice())?; +fn append( + image: &[u8], + layout: &Layout, + m: &Mutation, + op: u8, +) -> Result, Error> { + let entry = encode_entry(op, &m.space, &m.key, m.value.as_slice())?; // 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. @@ -1097,6 +1102,201 @@ fn checkpoint(ctl: &Control, merged: &mut Merged) -> Result { result.map(|_| new_image.len() as u64) } +/// 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)) +} + +/// `CUBE_OP_PUT`: store bytes at a coordinate. +/// +/// # 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, +) -> i32 { + 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, + 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), + } +} + +/// `CUBE_OP_GET`: read a coordinate. +/// +/// 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. +/// +/// # 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) }; + let (device, layout) = match device_and_layout() { + Ok(pair) => pair, + Err(e) => return -(e.to_errno() as isize), + }; + 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 (mut merged, _applied) = match build_merged( + &live[..core::cmp::min(header.image_bytes.unwrap_or(live.len() as u64) as usize, live.len())], + 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(); + + // The last entry of the coordinate's group is the one that counts: newest write wins, and + // a removal means there is nothing there. + let mut found: Option<&Entry> = None; + for e in entries { + if e.space == sp && e.key == key { + found = Some(e); + } + } + match found { + None => -2, // -ENOENT + Some(e) if e.deleted => -2, + Some(e) => { + let value = merged.value(e); + 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 + } + } +} + +/// `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 { + let (sp, key) = unsafe { coord_key(space, x, y, z) }; + let mutation = Mutation { + space: sp, + key, + 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 { + let (device, layout) = match device_and_layout() { + Ok(pair) => pair, + Err(e) => return -(e.to_errno() as i32), + }; + let ctl = match layout.control { + Some(c) => c, + None => return -22, // -EINVAL: a bare image has no spare slot to fold into + }; + let live = &device[layout.image_off..]; + let header = match parse_header(live) { + Ok(h) => h, + Err(_) => return -22, + }; + let window = log_window(&device, &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 -22, + }; + match checkpoint(&ctl, &mut merged) { + 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 { @@ -1193,7 +1393,7 @@ impl MiscDevice for CubeStore { key: morton_encode(x, y, z), value, }; - match append(&device, &layout, &mutation) { + match append(&device, &layout, &mutation, 1) { Ok(_) => { dev_info!( me.dev, diff --git a/include/uapi/linux/cube.h b/include/uapi/linux/cube.h new file mode 100644 index 000000000..a7673902e --- /dev/null +++ b/include/uapi/linux/cube.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * CUBELinux: the coordinate interface. + * + * The kernel's native interface to the store. Operations are the verbs the command language + * already defines — one language, and this is its kernel form (DESIGN-cube-interface.md). + * + * A coordinate is not a name and not a path: it is *where* a record is, and knowing it is the + * authorisation to use it. Nothing here resolves a name, and nothing enumerates. + */ +#ifndef _UAPI_LINUX_CUBE_H +#define _UAPI_LINUX_CUBE_H + +#include + +/* Which cube, and where in it. 32-byte space: unguessable, deliberately. */ +struct cube_coord { + __u8 space[32]; + __u64 x; + __u64 y; + __u64 z; +}; + +/* + * The argument block. `size` first, and checked: an interface that cannot grow is an + * interface that has to be replaced, and syscall numbers are permanent. + */ +struct cube_args { + __u32 size; /* sizeof(struct cube_args) as the caller built it */ + __u32 op; /* CUBE_OP_* */ + struct cube_coord coord; /* unused by CUBE_OP_SYNC */ + __u64 value; /* user pointer: bytes to write, or where to put them */ + __u64 len; /* in: bytes offered, or the buffer's capacity. + * out: on -ERANGE, the bytes that would be needed; + * on success for CUBE_OP_GET, the bytes read. + */ +}; + +#define CUBE_OP_PUT 1 /* store bytes at a coordinate */ +#define CUBE_OP_GET 2 /* read them back */ +#define CUBE_OP_DEL 3 /* remove the record */ +#define CUBE_OP_SYNC 4 /* fold the log into the image */ + +#endif /* _UAPI_LINUX_CUBE_H */