CUBELinux.0.6: cube(2) — the coordinate interface

The write path was proven but unreachable: its operations lived behind a device
node. This is the interface the decision chose (DESIGN-cube-interface.md) — one
syscall number, an opcode, and a versioned argument block, with operations that
are the verbs the command language already defines: put, get, del, sync.

  long cube(unsigned int op, struct cube_args __user *args)

`size` comes first and is checked, because syscall numbers are permanent and an
interface that cannot grow would have to be replaced. A coordinate is the space and
its three axes; nothing here resolves a name and nothing enumerates.

Split deliberately: the entry point, the user copies and the argument validation are
in C (cube_syscall.c) because `SYSCALL_DEFINE*` is a C macro this kernel has no Rust
equivalent for; everything that touches the store's bytes is in Rust, which passes
the coordinate to the format code as its parts so that Morton encoding stays in the
one module that must get it exactly right. A read that does not fit returns the size
it needs rather than truncating — a short read would be worse than an error.

Number 548: the x86_64 table says numbers 548 and above are available for
non-x32 use.

Gate (kernel/verify-syscall.sh): a static client in the initramfs does four writes
(including an empty value and a second space), reads one back *through the same
interface*, and folds with `sync`. Three different failures are separated — the
calls failing (a broken ABI), a read not returning what a write stored (a wrong key
encoding or index), and the folded image differing (a wrong format, order or merge).

  put 7,0,0 ok (21 bytes)
  put 8,0,0 ok (0 bytes)
  put 9,0,0 ok (28 bytes)
  put 1,2,3 ok (13 bytes)
  get 7,0,0 21 bytes: the kernel wrote this
  sync ok

and the image left on the device is byte-identical to the one userspace writes from
the same mutations, with the log empty. The interface's store is the same store.

All nine gates pass on 0.6.
This commit is contained in:
CUBELinux build
2026-09-18 22:18:18 -04:00
parent 2d4ed0fd9f
commit 3ee59dafff
6 changed files with 383 additions and 7 deletions
+1 -1
View File
@@ -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
+131
View File
@@ -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 <linux/kernel.h>
#include <linux/slab.h>
#include <linux/syscalls.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);
/* 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;
}
+205 -5
View File
@@ -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<KVVec<u8>, AllocError> {
fn encode_entry(op: u8, space: &[u8; 32], key: &[u8; 24], value: &[u8]) -> Result<KVVec<u8>, AllocError> {
let total = ENTRY_FIXED + value.len();
let mut entry = KVVec::<u8>::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<Option<Control>, Error> {
let entry = encode_entry(&m.space, &m.key, m.value.as_slice())?;
fn append(
image: &[u8],
layout: &Layout,
m: &Mutation,
op: u8,
) -> Result<Option<Control>, 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<u64, Error> {
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<u8>, Layout), Error> {
let mut device = KVVec::<u8>::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::<u8>::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,