"Every error anywhere" and "every error here" are different questions, and a space is a hard partition, so the scope is a field rather than a widening. It is not a reserved space id because there is no such id to reserve: every 32-byte value is a legitimate space, root 0x00 and edge 0xFF…FF among them, so a sentinel would be a space somebody could name. The field occupies what was padding, which keeps sizeof unchanged — and that matters, because size is what says which argument block arrived and cube_args is exactly eight bytes wider. An every-space answer carries each frame's space, and that is not decoration: a walk's frame omits the space on the grounds that the caller named it, and this caller named none. A coordinate is meaningless without its space, so an answer that left it out would be unusable rather than merely terse. The space leads because the (space, key) pair it forms is the order records are stored in and returned in — so an every-space scan answers in exactly the order a checkpoint writes. The walk visits the space table's order and puts a space only the log writes into in its place in that same order, which is the one thing a plain walk of the table would miss entirely. A scope or mode this build does not know is refused rather than defaulted: silently answering a narrower question than the one asked is as quiet a way to be wrong as answering a wider one.
449 lines
15 KiB
C
449 lines
15 KiB
C
// 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 <linux/init.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/string.h>
|
|
#include <linux/syscalls.h>
|
|
#include <linux/types.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/cube.h>
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/*
|
|
* 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 region walk (CUBE_OP_RANGE). The box travels as its six numbers for the same reason the
|
|
* coordinate does: the format knowledge stays on the Rust side, which owns the key the box has to
|
|
* become.
|
|
*/
|
|
int cubelinux_kernel_range(const __u8 *space,
|
|
__u64 lo_x, __u64 lo_y, __u64 lo_z,
|
|
__u64 hi_x, __u64 hi_y, __u64 hi_z,
|
|
__u64 cursor, void *buf, size_t cap,
|
|
__u64 *out_len, __u64 *out_cursor);
|
|
|
|
/*
|
|
* The flag scan (CUBE_OP_FLAG_SCAN). The mask and its mode travel as plain numbers: which bits mean
|
|
* what is a vocabulary's business, and the kernel never interprets one — it compares masks, which is
|
|
* what lets a new vocabulary attach without a format change.
|
|
*/
|
|
int cubelinux_kernel_flag_scan(const __u8 *space, __u16 mask, __u16 mode,
|
|
__u32 every_space, __u64 cursor, void *buf, size_t cap,
|
|
__u64 *out_len, __u64 *out_cursor);
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/*
|
|
* 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)
|
|
|
|
/* 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;
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
/*
|
|
* The region walk — CUBE_OP_RANGE — which travels in `struct cube_range_args`.
|
|
*
|
|
* Deliberately the same shape as the space walk above, because it is the same contract with one
|
|
* more input: 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. Nobody guesses a size
|
|
* and nobody gets half a record.
|
|
*
|
|
* The two things a caller must know beyond the walk's rules: the cursor counts the records **in the
|
|
* box** rather than the records of the space (those are the records being returned), and the kernel
|
|
* may *examine* more records than it returns, because it seeks on the box's key span and the span
|
|
* is a bound rather than the set. That over-coverage is the honest cost of the seek, not a defect.
|
|
*/
|
|
static long cube_range_op(void __user *uargs)
|
|
{
|
|
struct cube_range_args r;
|
|
void *buf = NULL;
|
|
long ret = 0;
|
|
u64 out_len = 0, out_cursor = 0;
|
|
|
|
if (copy_from_user(&r, uargs, sizeof(r)))
|
|
return -EFAULT;
|
|
if (r.size != sizeof(struct cube_range_args) || r.op != CUBE_OP_RANGE)
|
|
return -EINVAL;
|
|
|
|
/*
|
|
* An inverted box is empty, not an error: there is nothing in it, and saying so is the honest
|
|
* answer. Answering it here also keeps the empty case away from the seek, where an inverted
|
|
* span would be a range whose start is above its end.
|
|
*/
|
|
if (r.lo[0] > r.hi[0] || r.lo[1] > r.hi[1] || r.lo[2] > r.hi[2]) {
|
|
r.len = 0;
|
|
if (copy_to_user(uargs, &r, sizeof(r)))
|
|
return -EFAULT;
|
|
return 0;
|
|
}
|
|
|
|
if (r.len > CUBE_MAX_WALK)
|
|
return -E2BIG;
|
|
if (r.len > 0) {
|
|
buf = kvmalloc(r.len, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
ret = cubelinux_kernel_range(r.space, r.lo[0], r.lo[1], r.lo[2],
|
|
r.hi[0], r.hi[1], r.hi[2],
|
|
r.cursor, buf, r.len, &out_len, &out_cursor);
|
|
if (ret == 0) {
|
|
if (out_len > 0 && copy_to_user((void __user *)r.value, buf, out_len))
|
|
ret = -EFAULT;
|
|
r.len = out_len;
|
|
r.cursor = out_cursor;
|
|
if (copy_to_user(uargs, &r, sizeof(r)))
|
|
ret = -EFAULT;
|
|
} else if (ret == -ERANGE) {
|
|
/* Nothing was written; `len` now says how much one record needs. */
|
|
r.len = out_len;
|
|
if (copy_to_user(uargs, &r, sizeof(r)))
|
|
ret = -EFAULT;
|
|
}
|
|
|
|
kvfree(buf);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* The flag scan — CUBE_OP_FLAG_SCAN — which travels in `struct cube_flag_scan_args`.
|
|
*
|
|
* The walks' shape again, because it is the walks' contract: 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.
|
|
*
|
|
* The one thing a caller must know beyond the walk's rules: the cursor counts the records that
|
|
* **matched**, not the records examined, because the records the mask rejected are not answers.
|
|
*/
|
|
static long cube_flag_scan_op(void __user *uargs)
|
|
{
|
|
struct cube_flag_scan_args f;
|
|
void *buf = NULL;
|
|
long ret = 0;
|
|
u64 out_len = 0, out_cursor = 0;
|
|
|
|
if (copy_from_user(&f, uargs, sizeof(f)))
|
|
return -EFAULT;
|
|
if (f.size != sizeof(struct cube_flag_scan_args) || f.op != CUBE_OP_FLAG_SCAN)
|
|
return -EINVAL;
|
|
if (f.mode != CUBE_FLAG_ANY && f.mode != CUBE_FLAG_ALL)
|
|
return -EINVAL;
|
|
if (f.every_space != CUBE_SPACE_ONE && f.every_space != CUBE_SPACE_EVERY)
|
|
return -EINVAL;
|
|
|
|
if (f.len > CUBE_MAX_WALK)
|
|
return -E2BIG;
|
|
if (f.len > 0) {
|
|
buf = kvmalloc(f.len, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
ret = cubelinux_kernel_flag_scan(f.space, f.mask, f.mode, f.every_space,
|
|
f.cursor, buf, f.len, &out_len, &out_cursor);
|
|
if (ret == 0) {
|
|
if (out_len > 0 && copy_to_user((void __user *)f.value, buf, out_len))
|
|
ret = -EFAULT;
|
|
f.len = out_len;
|
|
f.cursor = out_cursor;
|
|
if (copy_to_user(uargs, &f, sizeof(f)))
|
|
ret = -EFAULT;
|
|
} else if (ret == -ERANGE) {
|
|
/* Nothing was written; `len` now says how much one record needs. */
|
|
f.len = out_len;
|
|
if (copy_to_user(uargs, &f, sizeof(f)))
|
|
ret = -EFAULT;
|
|
}
|
|
|
|
kvfree(buf);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* One syscall, four 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);
|
|
if (size == sizeof(struct cube_range_args))
|
|
return cube_range_op(uargs);
|
|
if (size == sizeof(struct cube_flag_scan_args))
|
|
return cube_flag_scan_op(uargs);
|
|
return -EINVAL;
|
|
}
|