The second half of "the OS stores itself". The store was already the kernel's; what was missing was the kernel *saying* something of its own rather than a client doing it. At its first write of a boot the driver now appends one record describing the boot it is having — its own version banner, the wall-clock time, and the store device it resolved — to a reserved space, through the same append path every other mutation uses. Three things had to be decided, and each had a wrong answer that looked right: WHERE IT HOOKS. "At store init" does not exist and must not be invented: there is no init-time open, deliberately, 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. The moment is the FIRST WRITE, which needs no ordering at all and is the semantically right one — the kernel records itself when it becomes the writer. A boot in which the kernel only reads writes no record, which is honest rather than a gap. WHICH SPACE. 0xFD was the first choice, following the convention that a reserved space is one repeated byte. 0xFD is the OS KEYSTORE — the space the kill switch exists to destroy — and writing boot records into it would have been a serious bug. Only the userspace name table catches this (format_space in crates/cube-command) because the kernel keeps no table of space names, so the table is now written down where the constant is: 0x00 root, 0xFF edges, 0xFE portal, 0xFD keystore, 0xFC boot. The record lives at (0,0,0) in 0xFC: one record, the current boot. WHAT IT SAYS. boot=<epoch seconds> device=<resolved path> kernel=<the version banner>, banner last and unquoted so everything after the final = is the kernel's own words rather than a field this code parsed. Raw epoch seconds rather than a date: 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. The banner comes from linux_banner and the time from ktime_get_real_ts64. WHY IT IS OFF BY DEFAULT. Not caution, but an invariant. 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 record the kernel injects that the caller never asked for would turn those comparisons into non-comparisons. So it is cube_boot_record=1 on the kernel command line, parsed in C beside cube_store= for the reason that parameter is in C (this kernel's Rust cannot express a string parameter), and kernel/verify-boot-record.sh is the gate that turns it on. A failure to record is logged and never propagated: the record is worth having and is not a precondition for the caller's write. It is attempted once per boot rather than retried per write, because a store that will not take it will not take it later, and one warning is information where a stream of them is noise.
303 lines
9.7 KiB
C
303 lines
9.7 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 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;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|