Compare commits

...
10 Commits
Author SHA1 Message Date
CUBELinux 7938966d6a cubelinux: initialise STORE_FILE, the mutex that was never initialised
STORE_FILE is declared `unsafe(uninit) static ... Mutex<Option<StoreFile>> = None`
and nothing ever called STORE_FILE.init(). Its sibling SPACE_STARTS is initialised
explicitly in CubeStoreModule::init; this one was simply missed.

The failure mode is why it hid. A zeroed mutex satisfies the uncontended fast path
— the count reads 0, which means "unlocked" — so one writer at a time works and
nothing looks wrong. The first CONTENDED lock takes __mutex_lock_slowpath, which
splices the task into the mutex's wait list; that list is uninitialised, so its head
is NULL and the splice stores through it. A write to address 0 in kernel mode, after
which the task returns with interrupts disabled and preemption held — a machine that
cannot panic, log, or recover.

Found by verify-file-store.sh MODE=race: twelve concurrent puts to a store that is a
file on the root filesystem oopsed in cubelinux_store::store_file, while twelve
sequential ones passed with exact log accounting (816 bytes of log, generation 13).
The same signature is on the box, where the store had several writers and the box
froze with no panic despite panic=30.
2026-09-22 21:10:12 -04:00
surface-camera-build da0a19786e cube(2): one frame for every walk — the class mask is in it
CUBE_OP_ENUM and CUBE_OP_RANGE returned `key | value_len | value`, so a caller
could walk a store and not learn what any record it walked past *was*. A store
could not answer `entries_flagged` from a kernel store at all, and the only way
to find out was to open the device and read the format directly — which is the
second reader of the format this interface exists to make unnecessary.

Both walks now return the flag scan's frame: `key(24) | flags(2) | value_len |
value`, with the space ahead of it when the scope is every space. One frame,
one packer, one `Batch::offer`; the separate `offer_flagged` and `pack_record`
are gone, and so is the reason for them to disagree.

The class comes from wherever the value did: an addressed image's index entry
(the binary search already read it and used to throw it away), a log entry's
mask, or zero for a packed v1/v2 record, which has no field to carry one. The
merge in `SpaceWalker` hands it back alongside the key and the value for the
same reason — a record the log supplied carries the class its writer stamped,
and dropping it there is what left a listing unable to say what it was listing.
2026-09-22 02:20:31 -04:00
surface-camera-build 1081b5f1e2 cube(2): CUBE_OP_GET answers with the record's class
The last hole in the substrate: a caller could write a class through the syscall
but not read one back. `CUBE_OP_GET` now fills `args.flags` from the same index
entry the address came from, so learning what a record *is* costs nothing beyond
a read that was going to happen.

One field for both directions, because it is one thing — the class of this
record. A write states it, a read learns it, and neither is a special case of the
other. `find` hands the mask back with the address for the same reason: it is in
the same stride the binary search already read, so wanting both does not mean
searching twice. A read that finds nothing leaves 0 rather than a stale class for
the caller to believe.

The size of `cube_args` does not change, which matters because `size` is what
says which argument block arrived.
2026-09-22 01:34:23 -04:00
surface-camera-build 72e72fe7a8 cubelinux: an addressed image is readable without a log
merged_digest answered "no log" before it looked at the layout, so a bare
v3/v4 image fell through to the packed walk — which starts at the v2 header
length and reads index entries as record frames. A bare addressed image with
no log beside it therefore read as a one-record store with an empty value.

An addressed image is a complete store on its own: its index holds every
record and says where each one is, so a log is an addition rather than a
requirement. A packed image is not, which is why "no log" still means what it
meant for v1/v2.

Found by verify-image-read the moment userspace started writing v4: while
every userspace image was v2 the fallback happened to be the right walk. The
kernel's own v4 images always had a log header beside them, so no other gate
could have caught it.
2026-09-22 01:02:39 -04:00
surface-camera-build 29ae3b53f1 cube_format: a geometry encodes the version it is, not a constant
V3::encode wrote a hardcoded VERSION_V3. It had no callers, so the mistake
cost nothing — and then userspace's v4 writer became the first caller, and a
v4 geometry would have been published under a v3 header: every reader walking
a 42-byte index 40 bytes at a time, finding a store that is silently wrong.
One wrong byte, found before the first v4 image was written rather than after.
2026-09-22 01:02:29 -04:00
surface-camera-build 9586e114e7 cube(2): CUBE_OP_PUT takes a class mask
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.
2026-09-22 00:37:09 -04:00
surface-camera-build 71552fc157 cube(2): CUBE_OP_FLAG_SCAN takes a scope — one space, or every space
"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.
2026-09-22 00:31:56 -04:00
surface-camera-build 41e437c07a 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.
2026-09-22 00:08:19 -04:00
surface-camera-build 0267831b18 cube: fix v2 log framing — never write a v2 entry under a v1 header
The v4 migration made the kernel write v2 log entries (flags field) but
append only wrote a log header when the region had no magic. A store
formatted by userspace already carries a v1 header, so the kernel appended
v2 entries under it and every reader framed them as v1: the length landed
on the flags field, the walk stopped at the first entry, and get/fold/boot
saw an empty log.

Now append insists the header matches what it writes: a v1 log that still
holds entries is folded into the image first (the actual v4 migration),
then the entry lands in a fresh v2 log; an empty v1 log is upgraded in
place. The fold is factored into fold_now, shared by append and sync.
2026-09-21 23:41:02 -04:00
surface-camera-build a111d7e8b2 cubelinux: v4 — a 16-bit class mask in the index and the log entry
The first half of the flag substrate (DESIGN-flag-vocabularies.md): the shared
format file now defines VERSION_V4, whose index entry is `key | flags(u16) |
value_off | value_len`, and WAL version 2, whose entry carries the same mask
before its length. The mask is a raw u16 — its bits are a vocabulary's business,
never the format's.

Backward compatible, and pinned as such: a v3 index entry and a v1 log entry read
as a zero mask ("no class"), which a scan treats as matching nothing, so a store
folded before the flag existed degrades to "unclassified" rather than "matches
everything". `V3::decode` accepts both versions and `index_stride()` names the
one that differs; `wal_entry` keys its stride off the log's own version byte.

The readers and writers that actually move bytes (the driver's serialize and
append, and `cube-store-raw`) are separate and are the next commit; this is the
shared definition and the arithmetic a reader derives from it.
2026-09-21 22:38:43 -04:00
4 changed files with 1224 additions and 202 deletions
+87 -18
View File
@@ -30,6 +30,10 @@ pub const VERSION_V1: u8 = 1;
pub const VERSION_V2: u8 = 2;
/// The addressed format: the same, plus a space table, a fixed-size index, and packed values.
pub const VERSION_V3: u8 = 3;
/// v3, plus a 16-bit class mask in each index entry, so a scan by flag is a seek rather than a
/// walk. The mask is the flag substrate (DESIGN-flag-vocabularies.md): a raw `u16` whose bits are a
/// vocabulary's business, written at put time and read by `CUBE_OP_FLAG_SCAN`.
pub const VERSION_V4: u8 = 4;
pub const HEADER_LEN_V1: usize = 6;
pub const HEADER_LEN_V2: usize = 6 + 8 + 8;
@@ -39,19 +43,28 @@ pub const HEADER_LEN_V3: usize = 4 + 1 + 1 + 8 + 8 + 8 + 8 + 8;
pub const SPACE_ID_LEN: usize = 32;
pub const RAW_KEY_LEN: usize = 24;
/// The class mask's width: one `u16` per record.
pub const FLAGS_LEN: usize = 2;
/// A packed record's fixed part: `space | key | value_len(u64)`, with the value behind it.
pub const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8;
/// A v3 index entry: `key | value_off(u64) | value_len(u64)`.
pub const INDEX_ENTRY: usize = RAW_KEY_LEN + 8 + 8;
/// A v4 index entry: `key | flags(u16) | value_off(u64) | value_len(u64)`.
pub const INDEX_ENTRY_V4: usize = RAW_KEY_LEN + FLAGS_LEN + 8 + 8;
/// A v3 space-table row: `space | first index(u64) | records(u64)`.
pub const SPACE_ENTRY: usize = SPACE_ID_LEN + 8 + 8;
/// The log's framing, which the fold and the readers both parse.
pub const WAL_MAGIC: &[u8; 4] = b"CUBW";
/// The original log entry: no class mask.
pub const WAL_VERSION: u8 = 1;
/// The flagged log entry: the entry carries a `u16` class mask before its length.
pub const WAL_VERSION_V2: u8 = 2;
pub const WAL_HEADER_LEN: usize = 6;
/// `op(1) | crc(4) | space(32) | key(24) | len(4)`, with the value behind it.
pub const ENTRY_FIXED: usize = 1 + 4 + SPACE_ID_LEN + RAW_KEY_LEN + 4;
/// v2's entry: the same, with a `flags(2)` field before the length.
pub const ENTRY_FIXED_V2: usize = 1 + 4 + SPACE_ID_LEN + RAW_KEY_LEN + FLAGS_LEN + 4;
/// `CUBE_OP_PUT`: an entry that stores a value.
pub const WAL_OP_WRITE: u8 = 1;
@@ -74,7 +87,7 @@ impl Header {
pub fn records_off(&self) -> usize {
if self.version == VERSION_V1 {
HEADER_LEN_V1
} else if self.version == VERSION_V3 {
} else if self.version == VERSION_V3 || self.version == VERSION_V4 {
HEADER_LEN_V3
} else {
HEADER_LEN_V2
@@ -134,7 +147,7 @@ pub fn parse_header(bytes: &[u8]) -> Result<Header, Bad> {
record_count: Some(record_count),
})
}
VERSION_V3 => {
VERSION_V3 | VERSION_V4 => {
if bytes.len() < HEADER_LEN_V3 {
return Err(Bad::Magic);
}
@@ -153,6 +166,9 @@ pub fn parse_header(bytes: &[u8]) -> Result<Header, Bad> {
/// The v3 tables' geometry: where the index is, where the values start, and how many of each.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V3 {
/// The version this geometry belongs to: `VERSION_V3` or `VERSION_V4`, which differ only in
/// the index entry's stride (the class mask adds two bytes).
pub version: u8,
pub record_count: u64,
pub space_count: u64,
pub index_off: u64,
@@ -160,16 +176,27 @@ pub struct V3 {
}
impl V3 {
/// The width of one index entry for this geometry's version.
pub fn index_stride(&self) -> usize {
if self.version == VERSION_V4 {
INDEX_ENTRY_V4
} else {
INDEX_ENTRY
}
}
/// Read the tables' geometry, refusing anything that does not add up.
///
/// The three equalities here are the whole point of the format: a reader computes a record's
/// place as `index_off + i * INDEX_ENTRY`, and that is only a place if the index really starts
/// place as `index_off + i * stride`, and that is only a place if the index really starts
/// there and really is that wide.
pub fn decode(bytes: &[u8]) -> Result<Self, Bad> {
if bytes.len() < HEADER_LEN_V3 || &bytes[0..4] != MAGIC || bytes[4] != VERSION_V3 {
let version = bytes.get(4).copied().ok_or(Bad::Magic)?;
if bytes.len() < HEADER_LEN_V3 || &bytes[0..4] != MAGIC || (version != VERSION_V3 && version != VERSION_V4) {
return Err(Bad::Magic);
}
let geometry = V3 {
version,
record_count: le_u64(bytes, 14),
space_count: le_u64(bytes, 22),
index_off: le_u64(bytes, 30),
@@ -180,7 +207,7 @@ impl V3 {
return Err(Bad::Extent);
}
let table_end = HEADER_LEN_V3 as u64 + geometry.space_count * SPACE_ENTRY as u64;
let index_end = geometry.index_off + geometry.record_count * INDEX_ENTRY as u64;
let index_end = geometry.index_off + geometry.record_count * geometry.index_stride() as u64;
if geometry.index_off != table_end
|| geometry.values_off != index_end
|| geometry.values_off > image_bytes
@@ -192,12 +219,18 @@ impl V3 {
/// Write the header this geometry describes. `image_bytes` is filled in by the caller's
/// arithmetic, since only it knows how long the values are.
///
/// The version written is the geometry's own rather than a constant. A v4 geometry has a
/// 42-byte index, and a header saying v3 would tell every reader to walk it 40 bytes at a time
/// — one wrong byte that silently mis-addresses the whole store. This had no callers when it
/// was written, so the mistake cost nothing; it has one now, and finding it before the first
/// v4 image is written is the whole value of fixing it here.
pub fn encode(&self, out: &mut [u8], curve: u8, image_bytes: u64) -> Result<usize, Bad> {
if out.len() < HEADER_LEN_V3 {
return Err(Bad::Extent);
}
out[0..4].copy_from_slice(MAGIC);
out[4] = VERSION_V3;
out[4] = self.version;
out[5] = curve;
out[6..14].copy_from_slice(&image_bytes.to_le_bytes());
out[14..22].copy_from_slice(&self.record_count.to_le_bytes());
@@ -229,14 +262,24 @@ impl V3 {
if at >= self.record_count {
return None;
}
let off = self.index_off as usize + at as usize * INDEX_ENTRY;
if off + INDEX_ENTRY > bytes.len() {
let stride = self.index_stride();
let off = self.index_off as usize + at as usize * stride;
if off + stride > bytes.len() {
return None;
}
// The flag field exists only in v4; a v3 entry reads as a zero mask, which is honest —
// "no class" — and matches nothing in a scan.
let flags = if self.version == VERSION_V4 {
le_u16(bytes, off + RAW_KEY_LEN)
} else {
0
};
let vo = off + RAW_KEY_LEN + if self.version == VERSION_V4 { FLAGS_LEN } else { 0 };
let entry = IndexEntry {
key: bytes[off..off + RAW_KEY_LEN].try_into().ok()?,
value_off: le_u64(bytes, off + RAW_KEY_LEN),
value_len: le_u64(bytes, off + RAW_KEY_LEN + 8),
flags,
value_off: le_u64(bytes, vo),
value_len: le_u64(bytes, vo + 8),
};
// A value that is not inside the image is a truncated image, not an empty one.
if entry.value_off < self.values_off
@@ -258,10 +301,12 @@ impl V3 {
}
}
/// A v3 index entry: the key, and where its value lies.
/// An addressed index entry: the key, its class mask, and where its value lies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexEntry<'a> {
pub key: &'a [u8; RAW_KEY_LEN],
/// The class mask (v4), or zero (v3, where no mask was written).
pub flags: u16,
pub value_off: u64,
pub value_len: u64,
}
@@ -304,16 +349,25 @@ pub struct WalEntry<'a> {
pub space: &'a [u8; SPACE_ID_LEN],
pub key: &'a [u8; RAW_KEY_LEN],
pub op: u8,
/// The class mask (WAL version 2), or zero (version 1, where no mask was written).
pub flags: u16,
pub value: &'a [u8],
pub next: usize,
}
/// Read one log entry at `off`.
/// Read one log entry at `off`. The log slice includes its header, so the version at `log[4]`
/// decides the entry's stride: version 2 carries a two-byte class mask before the length.
///
/// The checksum covers space, key, length and value, so a torn tail is stopped at rather than
/// applied — the rule the fold and every reader share.
/// The checksum covers space, key, the mask, length and value, so a torn tail is stopped at rather
/// than applied — the rule the fold and every reader share.
pub fn wal_entry(log: &[u8], off: usize) -> Option<WalEntry<'_>> {
if off + ENTRY_FIXED > log.len() {
let version = log.get(4).copied().unwrap_or(WAL_VERSION);
let (fixed, len_at) = if version == WAL_VERSION_V2 {
(ENTRY_FIXED_V2, RAW_KEY_LEN + FLAGS_LEN + SPACE_ID_LEN + 5)
} else {
(ENTRY_FIXED, 61)
};
if off + fixed > log.len() {
return None;
}
let op = log[off];
@@ -321,19 +375,25 @@ pub fn wal_entry(log: &[u8], off: usize) -> Option<WalEntry<'_>> {
return None;
}
let crc = le_u32(log, off + 1);
let len = le_u32(log, off + 61) as usize;
let frame_end = off + ENTRY_FIXED + len;
let len = le_u32(log, off + len_at) as usize;
let frame_end = off + fixed + len;
if frame_end > log.len() {
return None;
}
if crc32(&log[off + 5..frame_end]) != crc {
return None;
}
let flags = if version == WAL_VERSION_V2 {
le_u16(log, off + SPACE_ID_LEN + RAW_KEY_LEN + 5)
} else {
0
};
Some(WalEntry {
space: log[off + 5..off + 37].try_into().ok()?,
key: log[off + 37..off + 61].try_into().ok()?,
op,
value: &log[off + ENTRY_FIXED..frame_end],
flags,
value: &log[off + fixed..frame_end],
next: frame_end,
})
}
@@ -357,6 +417,15 @@ pub fn le_u32(bytes: &[u8], at: usize) -> u32 {
u32::from_le_bytes(w)
}
/// A little-endian `u16` at `at`, with the same rule.
pub fn le_u16(bytes: &[u8], at: usize) -> u16 {
let mut w = [0u8; 2];
if at + 2 <= bytes.len() {
w.copy_from_slice(&bytes[at..at + 2]);
}
u16::from_le_bytes(w)
}
/// CRC-32 (IEEE 802.3), bitwise.
///
/// A corruption check, not a security check: it catches a torn write or a flipped bit, and says
+73 -5
View File
@@ -30,9 +30,9 @@
* 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);
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);
void *buf, size_t len, __u16 *out_flags);
int cubelinux_kernel_del(const __u8 *space, __u64 x, __u64 y, __u64 z);
int cubelinux_kernel_sync(void);
@@ -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,
__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);
@@ -179,14 +188,16 @@ static long cube_args_op(unsigned int op, void __user *uargs)
break;
}
ret = cubelinux_kernel_put(args.coord.space, args.coord.x,
args.coord.y, args.coord.z, buf, args.len);
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);
args.coord.y, args.coord.z, buf, args.len,
&args.flags);
if (got < 0) {
ret = got;
break;
@@ -361,7 +372,62 @@ 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.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.
@@ -378,5 +444,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;
}
File diff suppressed because it is too large Load Diff
+103 -6
View File
@@ -34,6 +34,23 @@ struct cube_args {
* out: on -ERANGE, the bytes that would be needed;
* on success for CUBE_OP_GET, the bytes read.
*/
__u16 flags; /* in: CUBE_OP_PUT — the class mask to stamp on the record;
* out: CUBE_OP_GET — the mask the record carries.
*
* One field for both directions because it is one thing: the
* class of this record. A write states it, a read learns it,
* and neither is a special case of the other. 0 is "no class",
* which is what every record written before the field existed
* reads as — so not classifying is not writing a special value,
* and a read that found nothing leaves 0 rather than a stale
* class for the caller to believe.
*
* 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. A read from such a store answers 0 for the same reason.
*/
__u16 reserved; /* must be 0 */
};
#define CUBE_OP_PUT 1 /* store bytes at a coordinate */
@@ -43,15 +60,21 @@ struct cube_args {
#define CUBE_OP_ENUM 5 /* walk the records of a space, in batches */
#define CUBE_OP_SPACES 6 /* walk the spaces that hold records */
#define CUBE_OP_RANGE 7 /* walk the records of a space that lie in a box */
#define CUBE_OP_FLAG_SCAN 8 /* walk the records of a space whose class mask matches */
/*
* The walk's argument block: its own block rather than a wider `cube_args`, because it needs a
* cursor and a buffer, and the coordinate would otherwise be both an input and an output.
*
* Records are packed as `key(24) | value_len(u32, little-endian) | value`, in the store's own
* order — space first, then key — which is the order a checkpoint writes them and the order the
* userspace store returns them, so a kernel listing and a userspace listing can be compared
* directly. The space is not repeated per record: the caller named it.
* Records are packed as `key(24) | flags(2, little-endian) | value_len(u32, little-endian) |
* value`, in the store's own order — space first, then key — which is the order a checkpoint writes
* them and the order the userspace store returns them, so a kernel listing and a userspace listing
* can be compared directly. The space is not repeated per record: the caller named it.
*
* The class mask is in the frame because a listing has to be able to say what a record *is*, and a
* caller that cannot see it has to open the store itself to find out — which is the second reader
* of the format this interface exists to make unnecessary. A record whose layout carries no mask (a
* packed v1/v2 image) reads as zero: "no class", the same answer every other reader gives.
*
* A walk ends when the cursor stops moving, and that is the only end signal: a batch holds as
* many whole records as fit, so most batches come back short, and reading a short batch as the
@@ -80,8 +103,9 @@ struct cube_enum_args {
* replaced.
*
* A region is a box, inclusive on both corners. Records come back packed exactly as CUBE_OP_ENUM
* packs them — `key(24) | value_len(u32, little-endian) | value`, in the store's own order — so a
* kernel region answer and a userspace one can be compared byte for byte.
* packs them — `key(24) | flags(2, little-endian) | value_len(u32, little-endian) | value`, in the
* store's own order — so a kernel region answer and a userspace one can be compared byte for
* byte.
*
* The cursor is a COUNT OF RECORDS ALREADY RETURNED, and it is the end-of-walk signal for the same
* reason as the walk's: a batch holds as many whole records as fit, so most batches come back
@@ -89,6 +113,9 @@ struct cube_enum_args {
* counts — a walk counts the records of the space, a region walk counts the records *in the box*,
* because those are the records it returns.
*
* Records come back in the same frame the walk uses, class mask included:
* `key(24) | flags(2) | value_len(u32) | value`.
*
* Implementation, because it is what makes this cheap: **the kernel seeks.** The box's two corner
* keys bound every key inside it (`cube_format::key_span`), so a v3 image's sorted index is
* binary-searched for the foot of that span and read forward to its head. The span is a BOUND, not
@@ -111,4 +138,74 @@ struct cube_range_args {
*/
};
/*
* The flag scan's argument block: `CUBE_OP_FLAG_SCAN`, the class-mask half of the store's
* classification substrate (DESIGN-flag-vocabularies.md). Its own block for the walks' reason — it
* needs a cursor and a buffer — and versioned by `size` like the other three.
*
* `mask` is a raw 16-bit class mask and this interface does not interpret a bit of it: the
* vocabulary that owns the bits (events today, sealing / lineage / lifecycle later) is the only
* thing that knows what they mean. `mode` says how to read the mask:
*
* CUBE_FLAG_ANY the record shares at least one bit with `mask` — "every error"
* CUBE_FLAG_ALL the record carries every bit of `mask` — "every Wi-Fi error"
*
* A `mask` of zero matches *nothing*, not everything: naming no class is asking no question, and a
* scan that answered a walk's worth of records to an empty question would be a walk wearing a
* scan's hat.
*
* The cursor counts **matches already returned**, as the region walk's counts records in its box,
* and for the same reason: the records examined before a match are not matches, so the index
* position of the cursor-th match is not arithmetic. A batch holds as many whole records as fit, so
* most batches come back short; reading a short batch as the end truncates the answer. A finished
* scan answers with no records and the cursor unchanged.
*
* Records come back as `key(24) | flags(2, little-endian) | value_len(u32, little-endian) | value`
* — the walk's frame with the class mask in it. The mask travels because a scan's answer has to say
* what class each record answered with: a record can carry bits beyond the one asked for, and no
* other operation returns a mask.
*
* Under CUBE_SPACE_EVERY the frame gains a leading `space(32)`, because it has to: a walk's frame
* leaves the space out on the grounds that the caller named it, and a caller who named no space
* cannot be told which one a record came from any other way. A coordinate is meaningless without
* its space, so an every-space answer that omitted it 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.
*/
struct cube_flag_scan_args {
__u32 size; /* sizeof(struct cube_flag_scan_args) as the caller built it */
__u32 op; /* CUBE_OP_FLAG_SCAN */
__u8 space[32]; /* in: the space to scan, or ignored with CUBE_SPACE_EVERY */
__u16 mask; /* in: the class mask to match; 0 matches nothing */
__u16 mode; /* in: CUBE_FLAG_ANY or CUBE_FLAG_ALL */
__u32 every_space; /* in: CUBE_SPACE_ONE or CUBE_SPACE_EVERY — see below */
__u64 cursor; /* in: 0 to start, or what the last call returned;
* out: what to pass next — see the end-of-scan rule above
*/
__u64 value; /* user pointer: where to put the records */
__u64 len; /* in: the buffer's capacity;
* out: bytes written, or on -ERANGE what would be needed
*/
};
#define CUBE_FLAG_ANY 0 /* the record shares at least one bit with the mask */
#define CUBE_FLAG_ALL 1 /* the record carries every bit of the mask */
/*
* The scan's scope. A space is a hard partition, so this is a choice between two different
* questions and never a filter that can be widened by accident:
*
* CUBE_SPACE_ONE the records of `space` — "this class here"
* CUBE_SPACE_EVERY the records of every space — "this class anywhere"
*
* It is a field and 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` are both in use), so a sentinel would
* be a space somebody could name. The field occupies what was padding, so `sizeof` is unchanged —
* which matters, because `size` is what says which argument block arrived and `cube_args` is
* exactly eight bytes wider. A caller that zeroes its block (and every caller does) gets
* CUBE_SPACE_ONE, which is the narrower question.
*/
#define CUBE_SPACE_ONE 0 /* scan only `space` */
#define CUBE_SPACE_EVERY 1 /* scan every space */
#endif /* _UAPI_LINUX_CUBE_H */