The mask is the writer's and is stamped once, at the moment the record's class is known for certain; every later reader is spared re-deriving it. It means nothing to this side — which bits are which class is a vocabulary's business, and a kernel that interpreted one would be inventing a vocabulary. 0 is "no class", which is what every record written before the field existed reads as, so a caller that does not classify is not writing a special value. A store whose image is the legacy packed layout has no field to put a mask in and drops it: that layout cannot carry a class, and saying otherwise would be a lie about the bytes on disk. The field is appended, so sizeof(cube_args) grows from 80 to 88 — still distinct from the other three argument blocks, which is what the size-first dispatch depends on.
450 lines
15 KiB
C
450 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, __u16 flags);
|
|
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,
|
|
args.flags);
|
|
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;
|
|
}
|