cube(2): CUBE_OP_FLAG_SCAN — classify at write, retrieve by class

The store's flag field is a substrate, and this is its read half: a v4
index entry carries a 16-bit class mask, and a scan by class is a walk that
reads the mask and tests it. Nothing about the merge changes — the same log
overlay, the same order — so a caller pays for its own class rather than for
the store.

The op takes a space, a mask, a mode (any/all), a cursor and a buffer, in
its own size-versioned block. A mask of zero matches nothing, because naming
no class is asking no question. Records come back as
key | flags(2) | value_len | value: the mask travels, since a record can
carry bits the scan did not name and no other op returns a mask.

Both layouts the mask can be in are read. With the record still in the log
it comes from the v2 log entry; after a fold it comes from the v4 index
entry. The non-indexed path is not a corner — it is the state of every store
between the write that classified something and the fold, so answering it
with 'nothing' would make the substrate work only after a checkpoint.

Also settles what an append writes into which log, since the entry's frame
has to match the header a reader frames it by: a log that already holds v1
entries keeps taking v1 entries (a device folds first — that is the v4
migration — and a bare image keeps the log it has), an empty log is framed
v2 on a device and left alone on a bare image, and a log with no header is
framed v2 on a device and v1 on a bare image. The bare layout is the legacy
one: it has no index for a mask to be folded into, so a mask written there
could only be scanned and never checkpointed — half a feature, bought by
making every existing reader of that layout grow a version it cannot use.
This commit is contained in:
surface-camera-build
2026-09-22 00:08:19 -04:00
parent 0267831b18
commit 41e437c07a
3 changed files with 549 additions and 46 deletions
+65 -1
View File
@@ -56,6 +56,15 @@ int cubelinux_kernel_range(const __u8 *space,
__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,
__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);
@@ -361,7 +370,60 @@ static long cube_range_op(void __user *uargs)
}
/*
* One syscall, three argument blocks. They share a prefix `size`, then `op` so the size the
* 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.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.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.
@@ -378,5 +440,7 @@ SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs)
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;
}