From 41e437c07a3e048231cd0bf47c7fae65e09e39c8 Mon Sep 17 00:00:00 2001 From: surface-camera-build Date: Tue, 22 Sep 2026 00:08:19 -0400 Subject: [PATCH] =?UTF-8?q?cube(2):=20CUBE=5FOP=5FFLAG=5FSCAN=20=E2=80=94?= =?UTF-8?q?=20classify=20at=20write,=20retrieve=20by=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- drivers/cube/cube_syscall.c | 66 ++++- drivers/cube/cubelinux_store.rs | 483 +++++++++++++++++++++++++++++--- include/uapi/linux/cube.h | 46 +++ 3 files changed, 549 insertions(+), 46 deletions(-) diff --git a/drivers/cube/cube_syscall.c b/drivers/cube/cube_syscall.c index 078d630a9..b2d2444ed 100644 --- a/drivers/cube/cube_syscall.c +++ b/drivers/cube/cube_syscall.c @@ -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; } diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index 74e2ad6f3..5966ece5f 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -1167,24 +1167,34 @@ struct Mutation { value: KVVec, } -/// Build a log entry: `op | crc32 | space | key | flags | len | value`. +/// Build a log entry: `op | crc32 | space | key | [flags] | len | value`. +/// +/// `version` decides whether the class mask is in the frame, and it must match the log's header: +/// a reader frames every entry by the header's version, so an entry is only well-formed against the +/// log it is written into. Version 1 has no mask field — the legacy log simply cannot carry one — +/// and an entry going into such a log is written without it rather than not written at all. fn encode_entry( op: u8, space: &[u8; 32], key: &[u8; 24], flags: u16, value: &[u8], + version: u8, ) -> Result, AllocError> { - let total = ENTRY_FIXED_V2 + value.len(); - let mut entry = KVVec::::with_capacity(total, GFP_KERNEL)?; + let flagged = version == WAL_VERSION_V2; + let fixed = if flagged { ENTRY_FIXED_V2 } else { ENTRY_FIXED }; + let mut entry = KVVec::::with_capacity(fixed + value.len(), 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)?; - entry.extend_from_slice(&flags.to_le_bytes(), GFP_KERNEL)?; + if flagged { + entry.extend_from_slice(&flags.to_le_bytes(), GFP_KERNEL)?; + } entry.extend_from_slice(&(value.len() as u32).to_le_bytes(), GFP_KERNEL)?; entry.extend_from_slice(value, GFP_KERNEL)?; - // The checksum covers everything after the crc field. + // The checksum covers everything after the crc field, so a v1 entry's checksum covers what a v1 + // reader reads — the two versions are different framings of the same rule, not two rules. let crc = crc32(&entry.as_slice()[5..]); entry.as_mut_slice()[1..5].copy_from_slice(&crc.to_le_bytes()); Ok(entry) @@ -1247,8 +1257,6 @@ fn append( m: &Mutation, op: u8, ) -> Result, Error> { - let entry = encode_entry(op, &m.space, &m.key, m.flags, 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. let region_at = core::cmp::min(layout.log_off, image.len()); @@ -1271,21 +1279,62 @@ fn append( (used, region.len()) } }; - // The entries this function writes are v2, so a log whose header is v1 cannot take one: - // the reader frames every entry by the header's version, and a v1 header would mis-frame - // the v2 entries (and the v1 entries already there, if a header were simply rewritten). - // A v1 log that still holds entries is folded into the image first — that is the v4 - // migration — and the entry then lands in a fresh v2 log. A v1 log that is empty only - // needs its header upgraded. - if region.len() >= WAL_HEADER_LEN && ®ion[0..4] == WAL_MAGIC && region[4] == WAL_VERSION && used > 0 { - if layout.control.is_some() { - fold_now(image, layout)?; - let (device, layout) = device_and_layout()?; - return append(&device, &layout, m, op); + + // The entry's frame has to match the log's header, because the header is what a reader frames + // it by. So the header decides the version, and there are exactly three cases: + // + // * a log that already holds v1 entries must keep taking v1 entries — rewriting the header + // would mis-frame every entry already there. A store *device* folds first instead (that is + // the v4 migration, and it has a spare slot to fold into); a bare image has nowhere to fold + // to, so it keeps writing the log it has. The mask is lost for that append, because a v1 + // entry has no field to carry it — a limitation of the legacy layout stated plainly, and + // better than refusing to write to a log the store already holds data in; + // * no header, a v2 header, or a v1 header over an *empty* log: the frame is v2, and the + // header is written (or upgraded) to say so. + let header_version = if region.len() >= WAL_HEADER_LEN && ®ion[0..4] == WAL_MAGIC { + Some(region[4]) + } else { + None + }; + let entry_version = match header_version { + // A log that already holds entries and declares v1 keeps taking v1 — rewriting the header + // would mis-frame everything already there. A store *device* first folds instead (that is + // the v4 migration, and it has a spare slot to fold into); a bare image has nowhere to fold + // to, so it keeps writing the log it has. + Some(WAL_VERSION) if used > 0 => { + if layout.control.is_some() { + fold_now(image, layout)?; + let (device, layout) = device_and_layout()?; + return append(&device, &layout, m, op); + } + WAL_VERSION } - pr_err!("cubelinux: a bare image cannot migrate its v1 log; refold it with the userspace tools\n"); - return Err(EINVAL); - } + // An empty log that already declares a version: a device's is upgraded to v2, since v2 is + // the layout that carries the class mask and the one a fold turns into a v4 index. A bare + // image keeps what its log says. + Some(v) => { + if layout.control.is_some() { + WAL_VERSION_V2 + } else { + v + } + } + // No header at all: this append writes the first one, and so chooses the framing for this + // log's life. A store device gets v2. A bare image gets v1, because the bare layout is the + // legacy one: it has no index, so a mask written into its log could only ever be read back + // by a scan and never be checkpointed — half a feature, bought by making every existing + // reader of that layout grow a version it has no use for. The format that carries a class + // mask is the checkpointable one, and that is the one that gets it. + None => { + if layout.control.is_some() { + WAL_VERSION_V2 + } else { + WAL_VERSION + } + } + }; + + let entry = encode_entry(op, &m.space, &m.key, m.flags, m.value.as_slice(), entry_version)?; if WAL_HEADER_LEN + used + entry.len() > capacity { pr_err!( @@ -1301,14 +1350,15 @@ fn append( let file = store_file()?; let mut result: Result<(), Error> = Ok(()); - // A log region that has never been written is zeros, not a log: give it a header, the - // same thing the userspace log does when its file does not exist. A log region whose - // header is not v2 is upgraded, because the entry being written is v2 and the header is - // what a reader frames it by. - if region.len() < WAL_HEADER_LEN || ®ion[0..4] != WAL_MAGIC || region[4] != WAL_VERSION_V2 { + // A log region that has never been written is zeros, not a log: give it a header, the same + // thing the userspace log does when its file does not exist. A header that does not already + // say the version this entry is framed by is (re)written to say it, because the header is what + // a reader frames the entry by — and an entry under a header that disagrees with it is not a + // record, it is a mis-framing. + if region.len() < WAL_HEADER_LEN || ®ion[0..4] != WAL_MAGIC || region[4] != entry_version { let mut hdr = [0u8; WAL_HEADER_LEN]; hdr[0..4].copy_from_slice(&WAL_MAGIC); - hdr[4] = WAL_VERSION_V2; + hdr[4] = entry_version; hdr[5] = 0; // morton result = write_and_sync(file, layout.log_off as u64, &hdr); } @@ -2050,7 +2100,7 @@ enum Effect<'a> { fn log_effect<'a>(log: &'a [u8], space: &[u8; SPACE_ID_LEN], key: &[u8; RAW_KEY_LEN]) -> Option> { let mut entries = log_entries(log).ok()?; let mut found = None; - while let Some((entry_space, entry_key, op, value_at, value_len)) = entries.next() { + while let Some((entry_space, entry_key, op, _flags, value_at, value_len)) = entries.next() { if entry_space == space && entry_key == key { found = Some(if op == 2 { Effect::Delete @@ -2069,14 +2119,17 @@ struct LogEntries<'a> { /// The entry stride for this log's version, and where its length sits. fixed: usize, len_at: usize, + /// Whether this log's entries carry a class mask (version 2). A version 1 entry has no mask + /// field, and reads as "no class" — the same rule the index uses for a v3 entry. + flagged: bool, } impl<'a> LogEntries<'a> { - /// The next entry: `(space, key, op, where its value starts, its value's length)`. + /// The next entry: `(space, key, op, flags, where its value starts, its value's length)`. /// /// The value's *position* rather than a slice, because a caller collecting edits keeps them in /// a fixed-size vector and a borrow of the log would tie that vector's type to the log's life. - fn next(&mut self) -> Option<(&'a [u8], &'a [u8], u8, usize, usize)> { + fn next(&mut self) -> Option<(&'a [u8], &'a [u8], u8, u16, usize, usize)> { if self.off + self.fixed > self.log.len() { return None; } @@ -2101,8 +2154,13 @@ impl<'a> LogEntries<'a> { if crc32(&self.log[start + 5..frame_end]) != crc { return None; } + let flags = if self.flagged { + cube_format::le_u16(self.log, start + 5 + SPACE_ID_LEN + RAW_KEY_LEN) + } else { + 0 + }; self.off = frame_end; - Some((space, key, op, start + self.fixed, len)) + Some((space, key, op, flags, start + self.fixed, len)) } } @@ -2120,6 +2178,7 @@ fn log_entries(log: &[u8]) -> Result, &'static str> { off: WAL_HEADER_LEN, fixed, len_at, + flagged: log[4] == WAL_VERSION_V2, }) } @@ -2133,6 +2192,9 @@ struct LogEdit<'a> { /// Position in the log, so the later of two edits to one key is the one that counts. seq: u32, value: &'a [u8], + /// The class mask the writer stamped, carried through the merge so a flag scan sees the log's + /// word on a record and not only the image's. + flags: u16, deleted: bool, } @@ -2151,7 +2213,7 @@ fn log_edits<'a>( Err(_) => return Ok(()), }; let mut seq: u32 = 0; - while let Some((edit_space, key, op, value_at, value_len)) = entries.next() { + while let Some((edit_space, key, op, flags, value_at, value_len)) = entries.next() { if edit_space == space { let key: &[u8; RAW_KEY_LEN] = match key.try_into() { Ok(k) => k, @@ -2162,6 +2224,7 @@ fn log_edits<'a>( key, seq, value: &log[value_at..value_at + value_len], + flags, deleted: op == 2, }, GFP_KERNEL, @@ -2314,7 +2377,7 @@ fn log_spaces(log: &[u8], out: &mut KVVec<[u8; SPACE_ID_LEN]>) -> Result<()> { Ok(e) => e, Err(_) => return Ok(()), }; - while let Some((space, _, _, _, _)) = entries.next() { + while let Some((space, _, _, _, _, _)) = entries.next() { let mut s = [0u8; SPACE_ID_LEN]; s.copy_from_slice(&space[..SPACE_ID_LEN]); if !out.as_slice().contains(&s) { @@ -2424,6 +2487,27 @@ impl Batch<'_> { } } } + + /// Offer one *flagged* record. The counting rule is `offer`'s, and it is what makes the cursor + /// mean "matches already returned" rather than an index position: only a record that passed the + /// mask is counted, so a filtered walk resumes where it left off. + fn offer_flagged(&mut self, key: &[u8], flags: u16, value: &[u8]) -> bool { + self.seen += 1; + if self.seen <= self.cursor { + return true; + } + match pack_flagged(key, flags, value, self.out, self.written) { + Some(n) => { + self.written += n; + self.returned += 1; + true + } + None => { + self.too_big = RAW_KEY_LEN + FLAGS_LEN + 4 + value.len(); + false + } + } + } } // ── The region walk: a box, its key span, and what decides membership ──────────────────── @@ -2602,22 +2686,28 @@ impl Addressed { Ok(None) } - /// One index entry, by its position in the index. - fn index_entry(&mut self, at: u64, buf: &mut KVVec) -> Result<(u64, u64)> { + /// One index entry, by its position in the index: `(value_off, value_len, flags)`. + /// + /// The class mask comes back with the address because it is in the same stride and costs + /// nothing extra to read — and because a flag scan is exactly a walk that reads it and nothing + /// else. A v3 entry has no mask and answers zero, the same way its log entries do. + fn index_entry(&mut self, at: u64, buf: &mut KVVec) -> Result<(u64, u64, u16)> { let stride = self.header.stride(); let off = self.header.index_off + at * stride as u64; self.read_image_at(off, stride, buf)?; let entry = buf.as_slice(); - let vo = if self.header.version == VERSION_V4 { - RAW_KEY_LEN + FLAGS_LEN + let flagged = self.header.version == VERSION_V4; + let flags = if flagged { + cube_format::le_u16(entry, RAW_KEY_LEN) } else { - RAW_KEY_LEN + 0 }; + let vo = if flagged { RAW_KEY_LEN + FLAGS_LEN } else { RAW_KEY_LEN }; let mut w = [0u8; 8]; w.copy_from_slice(&entry[vo..vo + 8]); let value_off = u64::from_le_bytes(w); w.copy_from_slice(&entry[vo + 8..vo + 16]); - Ok((value_off, u64::from_le_bytes(w))) + Ok((value_off, u64::from_le_bytes(w), flags)) } /// The address of `key` inside a space's index range, or nothing if the space does not hold it. @@ -2636,7 +2726,9 @@ impl Addressed { } else if found > &key[..] { hi = mid; } else { - return self.index_entry(first + mid, &mut buf).map(Some); + return self + .index_entry(first + mid, &mut buf) + .map(|(value_off, value_len, _)| Some((value_off, value_len))); } } Ok(None) @@ -2843,7 +2935,7 @@ unsafe fn v3_enum( let mut entry = KVVec::::new(); let mut at = first + cursor; while at < first + records { - let (value_off, value_len) = match image.index_entry(at, &mut entry) { + let (value_off, value_len, _) = match image.index_entry(at, &mut entry) { Ok(pair) => pair, Err(e) => return -(e.to_errno() as i32), }; @@ -2884,7 +2976,7 @@ unsafe fn v3_enum( None => None, }; match (edit, image_entry, image_key) { - (Some(e), Some((_, value_len)), Some(key)) if e.key == key => { + (Some(e), Some((_, value_len, _)), Some(key)) if e.key == key => { let _ = value_len; edit_at += 1; at += 1; @@ -2892,14 +2984,14 @@ unsafe fn v3_enum( break; } } - (Some(e), Some((value_off, value_len)), Some(key)) if e.key < key => { + (Some(e), Some((value_off, value_len, _)), Some(key)) if e.key < key => { edit_at += 1; if !e.deleted && !batch.offer(e.key, e.value) { break; } let _ = (value_off, value_len); } - (_, Some((value_off, value_len)), Some(_)) => { + (_, Some((value_off, value_len, _)), Some(_)) => { let value = match image.value_at(value_off, value_len) { Ok(v) => v, Err(e) => return -(e.to_errno() as i32), @@ -3069,7 +3161,7 @@ unsafe fn v3_range( } } // The image's own record, which the log says nothing about. - (_, Some((value_off, value_len)), Some(key)) => { + (_, Some((value_off, value_len, _)), Some(key)) => { let value = match image.value_at(value_off, value_len) { Ok(v) => v, Err(e) => return -(e.to_errno() as i32), @@ -3102,6 +3194,176 @@ unsafe fn v3_range( } 0 } + +// ── The flag scan: classify at write, retrieve by class ────────────────────────────────── +// +// The class mask lives in the index entry (v4) and in the log entry (WAL v2), so a scan by class is +// a walk that reads the mask and tests it. Nothing else changes: the same merge with the log, the +// same order. What it buys is that a record which does not match is never offered, so the caller +// pays for its own class rather than for the store. + +/// A record matches when it shares *any* bit with the scan mask: "every error", "every Wi-Fi event". +const FLAG_MODE_ANY: u16 = 0; +/// A record matches when it carries *every* bit of the scan mask: "every Wi-Fi error". +const FLAG_MODE_ALL: u16 = 1; + +/// Whether a record's class mask answers a scan for `mask` under `mode`. +/// +/// A mask of zero matches nothing. Asking "what is this" with no class named is asking no question, +/// and a scan that returned everything on an empty mask would be a walk wearing a scan's hat — so +/// this is a refusal rather than a wildcard. `cube-store`'s `scan_by_flag` states the same rule, +/// and the gate diffs the two. +fn flag_matches(flags: u16, mask: u16, mode: u16) -> bool { + if mask == 0 { + return false; + } + if mode == FLAG_MODE_ALL { + flags & mask == mask + } else { + flags & mask != 0 + } +} + +/// `CUBE_OP_FLAG_SCAN` against a v3/v4 image: the live records of one space whose mask matches. +/// +/// This is [`v3_enum`]'s merge with one difference, and it is the operation's whole point: a record +/// is offered only when its class mask answers the scan, so the cursor counts **matches** rather +/// than records walked. That is why this re-walks from the space's first record on every batch (as +/// the region walk does) instead of starting at `first + cursor` the way the plain walk can: the +/// index position of the cursor-th match is not arithmetic, because the records before it are not +/// all matches. +/// +/// A v3 image is a store whose records were written before the mask existed. Its entries read as +/// "no class", so a scan over it matches nothing and says so by returning an empty batch — which is +/// the honest answer, not an error. +/// +/// # Safety +/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s. +unsafe fn v3_flag_scan( + mut image: Addressed, + wanted: [u8; SPACE_ID_LEN], + mask: u16, + mode: u16, + cursor: u64, + buf: *mut u8, + cap: usize, + out_len: *mut u64, + out_cursor: *mut u64, +) -> i32 { + let (first, records) = match image.space_entry(&wanted) { + Ok(Some(entry)) => entry, + Ok(None) => (0, 0), + Err(e) => return -(e.to_errno() as i32), + }; + // Own the log: the edits borrow it, and walking the image needs `&mut image`. + let mut log = KVVec::::new(); + if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() { + return -12; // -ENOMEM + } + let mut edits = KVVec::>::new(); + if log_edits(log.as_slice(), &wanted, &mut edits).is_err() { + return -12; + } + + // SAFETY: the shim guarantees `cap` writable bytes at `buf`. + let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) }; + let mut batch = Batch { + out, + written: 0, + returned: 0, + // `seen` counts matches, and the cursor skips the matches already returned; starting it at + // `cursor` would skip the first `cursor` matches of a fresh walk *and* still count from + // there, re-serving the first batch forever. It starts at 0 and `offer_flagged` is the only + // thing that counts. + seen: 0, + cursor, + too_big: 0, + }; + + let mut entry = KVVec::::new(); + let mut at = first; + let mut edit_at = 0usize; + loop { + // The entries of one key arrive in log order, so the last of a key's group is the newest + // word on it — and the newest word carries the mask that counts. + while edit_at + 1 < edits.len() && edits[edit_at + 1].key == edits[edit_at].key { + edit_at += 1; + } + let edit = edits.as_slice().get(edit_at).copied(); + let image_entry = if at < first + records { + match image.index_entry(at, &mut entry) { + Ok(triple) => Some(triple), + Err(e) => return -(e.to_errno() as i32), + } + } else { + None + }; + let image_key: Option<&[u8; RAW_KEY_LEN]> = match image_entry { + Some(_) => entry.as_slice()[..RAW_KEY_LEN].try_into().ok(), + None => None, + }; + + match (edit, image_entry, image_key) { + // The log's word on a coordinate the image holds: it wins, mask and all. + (Some(e), Some(_), Some(key)) if e.key == key => { + edit_at += 1; + at += 1; + if !e.deleted + && flag_matches(e.flags, mask, mode) + && !batch.offer_flagged(e.key, e.flags, e.value) + { + break; + } + } + // A record only the log holds, in its key's place. + (Some(e), Some(_), Some(key)) if e.key < key => { + edit_at += 1; + if !e.deleted + && flag_matches(e.flags, mask, mode) + && !batch.offer_flagged(e.key, e.flags, e.value) + { + break; + } + } + // The image's own record, which the log says nothing about. + (_, Some((value_off, value_len, flags)), Some(key)) => { + let value = match image.value_at(value_off, value_len) { + Ok(v) => v, + Err(e) => return -(e.to_errno() as i32), + }; + at += 1; + if flag_matches(flags, mask, mode) && !batch.offer_flagged(key, flags, value.as_slice()) + { + break; + } + } + (Some(e), None, _) => { + edit_at += 1; + if !e.deleted + && flag_matches(e.flags, mask, mode) + && !batch.offer_flagged(e.key, e.flags, e.value) + { + break; + } + } + (None, None, _) => break, + _ => break, + } + } + + // SAFETY: both out-pointers are writable under this function's contract. + unsafe { + if batch.too_big > 0 && batch.written == 0 { + *out_len = batch.too_big as u64; + *out_cursor = cursor; + return -34; // -ERANGE + } + *out_len = batch.written as u64; + *out_cursor = cursor + batch.returned; + } + 0 +} + /// /// 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 @@ -3175,6 +3437,26 @@ fn pack_record(key: &[u8], value: &[u8], out: &mut [u8], at: usize) -> Option Option { + let need = RAW_KEY_LEN + FLAGS_LEN + 4 + value.len(); + if at + need > out.len() { + return None; + } + out[at..at + RAW_KEY_LEN].copy_from_slice(&key[..RAW_KEY_LEN]); + out[at + RAW_KEY_LEN..at + RAW_KEY_LEN + FLAGS_LEN].copy_from_slice(&flags.to_le_bytes()); + out[at + RAW_KEY_LEN + FLAGS_LEN..at + RAW_KEY_LEN + FLAGS_LEN + 4] + .copy_from_slice(&(value.len() as u32).to_le_bytes()); + out[at + RAW_KEY_LEN + FLAGS_LEN + 4..at + need].copy_from_slice(value); + Some(need) +} + /// `CUBE_OP_ENUM`: walk the records of one space into the caller's buffer, `cursor` records in. /// /// The cursor is a COUNT OF RECORDS ALREADY RETURNED, not a position in the image. It is opaque to @@ -3368,6 +3650,117 @@ pub unsafe extern "C" fn cubelinux_kernel_range( 0 } +/// `CUBE_OP_FLAG_SCAN`: the live records of one space whose class mask matches, in key order. +/// +/// Its cursor counts **matches already returned**, exactly as the region walk's counts records in +/// the box — for the same reason, and with the same consequence: a batch holds as many whole +/// records as fit, most batches come back short, and reading a short batch as the end truncates the +/// answer. A finished scan answers with no records and the cursor unchanged. +/// +/// The answer's frame is `key(24) | flags(2, LE) | value_len(u32, LE) | value` — the walk's frame +/// with the class mask in it, because a scan's answer has to say what class each record answered +/// with and no other op returns a mask. +/// +/// # Safety +/// `space` must point to 32 readable bytes; `buf` must hold `cap` writable bytes; `out_len` and +/// `out_cursor` must each point to a writable `u64`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cubelinux_kernel_flag_scan( + space: *const u8, + mask: u16, + mode: u16, + cursor: u64, + buf: *mut u8, + cap: usize, + out_len: *mut u64, + out_cursor: *mut u64, +) -> i32 { + let mut wanted = [0u8; SPACE_ID_LEN]; + // SAFETY: the caller guarantees 32 readable bytes at `space`. + unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) }; + + // A mode this build does not know is refused rather than defaulted: a caller that asked for + // "all" and silently got "any" would get a superset, and a superset of a security question is + // the worst way to be wrong. + if mode != FLAG_MODE_ANY && mode != FLAG_MODE_ALL { + return -22; // -EINVAL + } + + match Addressed::open() { + Ok(Some(image)) => { + return unsafe { + v3_flag_scan(image, wanted, mask, mode, cursor, buf, cap, out_len, out_cursor) + } + } + Ok(None) => {} + Err(e) => return -(e.to_errno() as i32), + } + + let view = match read_view() { + Ok(v) => v, + Err(e) => return -(e.to_errno() as i32), + }; + + // A v1/v2 image holds packed records, which have no mask field at all — they read as "no class" + // and so can never match a scan. What *can* match is the log: its entries are the flagged ones, + // and they carry the mask the writer stamped. So the answer here is the log's live word on each + // coordinate it holds for the space, in key order — a walk's merge with the image's half of it + // known in advance to be empty of matches. + // + // This is not a corner: it is the state of every store between the write that classified + // something and the fold that moves the mask into the index — which is the ordinary state of a + // kernel that has just recorded an event. Answering it with "nothing" would make the substrate + // work only after a checkpoint, which is the opposite of "classify at write time". + let mut log = KVVec::::new(); + if log.extend_from_slice(view.log(), GFP_KERNEL).is_err() { + return -12; // -ENOMEM + } + let mut edits = KVVec::>::new(); + if log_edits(log.as_slice(), &wanted, &mut edits).is_err() { + return -12; + } + + // SAFETY: the shim guarantees `cap` writable bytes at `buf`. + let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) }; + let mut batch = Batch { + out, + written: 0, + returned: 0, + seen: 0, + cursor, + too_big: 0, + }; + + let mut edit_at = 0usize; + while edit_at < edits.len() { + // The entries of one key arrive in log order, so the last of a key's group is the newest + // word on it — and only the newest can match, because it is what a read would return. + while edit_at + 1 < edits.len() && edits[edit_at + 1].key == edits[edit_at].key { + edit_at += 1; + } + let e = edits.as_slice()[edit_at]; + edit_at += 1; + if !e.deleted + && flag_matches(e.flags, mask, mode) + && !batch.offer_flagged(e.key, e.flags, e.value) + { + break; + } + } + + // SAFETY: both out-pointers are writable under this function's contract. + unsafe { + if batch.too_big > 0 && batch.written == 0 { + *out_len = batch.too_big as u64; + *out_cursor = cursor; + return -34; // -ERANGE + } + *out_len = batch.written as u64; + *out_cursor = cursor + batch.returned; + } + 0 +} + /// `CUBE_OP_SPACES`: the `cursor`-th distinct space that holds a record, or -ENOENT at the end. /// /// An index rather than a count of records: a caller that wants a space's records walks it with diff --git a/include/uapi/linux/cube.h b/include/uapi/linux/cube.h index 181df64bb..48f17e7db 100644 --- a/include/uapi/linux/cube.h +++ b/include/uapi/linux/cube.h @@ -43,6 +43,7 @@ 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 @@ -111,4 +112,49 @@ 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. + */ +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 */ + __u16 mask; /* in: the class mask to match; 0 matches nothing */ + __u16 mode; /* in: CUBE_FLAG_ANY or CUBE_FLAG_ALL */ + __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 */ + #endif /* _UAPI_LINUX_CUBE_H */