From 71552fc157810de3bc23584f5e2f65f633599b3d Mon Sep 17 00:00:00 2001 From: surface-camera-build Date: Tue, 22 Sep 2026 00:31:56 -0400 Subject: [PATCH] =?UTF-8?q?cube(2):=20CUBE=5FOP=5FFLAG=5FSCAN=20takes=20a?= =?UTF-8?q?=20scope=20=E2=80=94=20one=20space,=20or=20every=20space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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. --- drivers/cube/cube_syscall.c | 8 +- drivers/cube/cubelinux_store.rs | 516 +++++++++++++++++++++++--------- include/uapi/linux/cube.h | 27 +- 3 files changed, 406 insertions(+), 145 deletions(-) diff --git a/drivers/cube/cube_syscall.c b/drivers/cube/cube_syscall.c index b2d2444ed..335cfe413 100644 --- a/drivers/cube/cube_syscall.c +++ b/drivers/cube/cube_syscall.c @@ -62,7 +62,7 @@ int cubelinux_kernel_range(const __u8 *space, * 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, + __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. */ @@ -393,6 +393,8 @@ static long cube_flag_scan_op(void __user *uargs) 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; @@ -402,8 +404,8 @@ static long cube_flag_scan_op(void __user *uargs) return -ENOMEM; } - ret = cubelinux_kernel_flag_scan(f.space, f.mask, f.mode, f.cursor, - buf, f.len, &out_len, &out_cursor); + 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; diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index 5966ece5f..491146c46 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -2466,6 +2466,19 @@ struct Batch<'a> { cursor: u64, /// Set when a record did not fit: the size it needs, for the `-ERANGE` answer. too_big: usize, + /// Whether each frame carries its own space. + /// + /// A walk's frame leaves the space out because the caller named it — repeating 32 bytes per + /// record to say what the caller already said is a tax on every listing. A scan that names *no* + /// space cannot do that: a coordinate is meaningless without its space, so an every-space + /// answer has to say which space each record came from or the answer is unusable. Hence a frame + /// that depends on the scope, which is the scope's business and not the record's. + with_space: bool, +} + +/// The fixed part of a flag-scan frame, before the value. +fn flagged_frame(with_space: bool) -> usize { + RAW_KEY_LEN + FLAGS_LEN + 4 + if with_space { SPACE_ID_LEN } else { 0 } } impl Batch<'_> { @@ -2491,19 +2504,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 { + /// + /// `space` is carried only when `with_space` is set — see [`Batch::with_space`]. + fn offer_flagged( + &mut self, + space: &[u8; SPACE_ID_LEN], + 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) { + match pack_flagged(space, key, flags, value, self.out, self.written, self.with_space) { Some(n) => { self.written += n; self.returned += 1; true } None => { - self.too_big = RAW_KEY_LEN + FLAGS_LEN + 4 + value.len(); + self.too_big = flagged_frame(self.with_space) + value.len(); false } } @@ -2686,6 +2707,30 @@ impl Addressed { Ok(None) } + /// One space-table row by its position: the space, where its records start, and how many. + /// + /// [`space_entry`](Self::space_entry) answers "where is *this* space" by binary search, which is + /// what a caller who named a space wants. A caller who named none has to visit every space in + /// the table's own order — which is sorted, so the rows come back in the order a checkpoint + /// wrote them and an every-space walk answers in (space, key) order for free. + fn space_row(&mut self, row: u64) -> Result> { + if row >= self.header.space_count { + return Ok(None); + } + let mut buf = KVVec::::new(); + self.read_image_at(HEADER_LEN_V3 as u64 + row * SPACE_ENTRY as u64, SPACE_ENTRY, &mut buf)?; + let entry = buf.as_slice(); + let space: [u8; SPACE_ID_LEN] = match entry[..SPACE_ID_LEN].try_into() { + Ok(s) => s, + Err(_) => return Err(EINVAL), + }; + let mut w = [0u8; 8]; + w.copy_from_slice(&entry[SPACE_ID_LEN..SPACE_ID_LEN + 8]); + let first = u64::from_le_bytes(w); + w.copy_from_slice(&entry[SPACE_ID_LEN + 8..SPACE_ENTRY]); + Ok(Some((space, first, u64::from_le_bytes(w)))) + } + /// 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 @@ -2927,6 +2972,7 @@ unsafe fn v3_enum( seen: cursor, cursor, too_big: 0, + with_space: false, }; if edits.is_empty() { @@ -3100,6 +3146,7 @@ unsafe fn v3_range( seen: 0, cursor, too_big: 0, + with_space: false, }; let mut entry = KVVec::::new(); @@ -3207,6 +3254,10 @@ 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; +/// The scan's scope: one named space, or every space. Two questions, not one widened. +const FLAG_SCOPE_ONE: u32 = 0; +const FLAG_SCOPE_EVERY: u32 = 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, @@ -3224,15 +3275,177 @@ fn flag_matches(flags: u16, mask: u16, mode: u16) -> bool { } } -/// `CUBE_OP_FLAG_SCAN` against a v3/v4 image: the live records of one space whose mask matches. +/// The answer a finished batch gives: how much was written and what cursor to pass next, or +/// `-ERANGE` with the size the record that did not fit would need. +/// +/// # Safety +/// `out_len` and `out_cursor` must point to writable `u64`s. +unsafe fn finish_batch( + batch: &Batch<'_>, + cursor: u64, + out_len: *mut u64, + out_cursor: *mut u64, +) -> i32 { + // 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 +} + +/// Scan one space's live records into `batch`, in key order, offering only those 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 +/// than records walked. That is why a scan 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. /// +/// Returns `false` when `batch` is full, so a caller walking several spaces can stop — which is what +/// the every-space scope needs: one batch, several spaces, and one cursor counting matches across +/// all of them. +fn flag_scan_space( + image: &mut Addressed, + log: &[u8], + wanted: &[u8; SPACE_ID_LEN], + first: u64, + records: u64, + mask: u16, + mode: u16, + batch: &mut Batch<'_>, +) -> Result { + let mut edits = KVVec::>::new(); + log_edits(log, wanted, &mut edits)?; + + 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 { + Some(image.index_entry(at, &mut entry)?) + } 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(wanted, e.key, e.flags, e.value) + { + return Ok(false); + } + } + // 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(wanted, e.key, e.flags, e.value) + { + return Ok(false); + } + } + // The image's own record, which the log says nothing about. + (_, Some((value_off, value_len, flags)), Some(key)) => { + let value = image.value_at(value_off, value_len)?; + at += 1; + if flag_matches(flags, mask, mode) + && !batch.offer_flagged(wanted, key, flags, value.as_slice()) + { + return Ok(false); + } + } + (Some(e), None, _) => { + edit_at += 1; + if !e.deleted + && flag_matches(e.flags, mask, mode) + && !batch.offer_flagged(wanted, e.key, e.flags, e.value) + { + return Ok(false); + } + } + // One side ran out and the other has nothing left that could interleave: this space is + // done, not the batch. + _ => return Ok(true), + } + } +} + +/// Offer the log's live word on each coordinate it holds for one space. +/// +/// This is the whole answer for a store whose image has no index: a packed record carries no mask +/// field, so none of the image's records can answer a scan and only the log's can. That 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. +/// +/// Returns `false` when `batch` is full. +fn flag_scan_log_space( + log: &[u8], + space: &[u8; SPACE_ID_LEN], + mask: u16, + mode: u16, + batch: &mut Batch<'_>, +) -> Result { + let mut edits = KVVec::>::new(); + log_edits(log, space, &mut edits)?; + 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(space, e.key, e.flags, e.value) + { + return Ok(false); + } + } + Ok(true) +} + +/// A batch ready to receive a flag scan's matches. `seen` starts at 0 for the reason the region +/// walk's does: this path re-walks from the beginning on every batch, so `offer_flagged` is the only +/// thing that counts. 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. +fn flag_batch<'a>(out: &'a mut [u8], cursor: u64, with_space: bool) -> Batch<'a> { + Batch { + out, + written: 0, + returned: 0, + seen: 0, + cursor, + too_big: 0, + with_space, + } +} + +/// `CUBE_OP_FLAG_SCAN` against a v3/v4 image, scoped to one space. +/// /// 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. @@ -3260,108 +3473,111 @@ unsafe fn v3_flag_scan( 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() { + + // SAFETY: the shim guarantees `cap` writable bytes at `buf`. + let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) }; + // The caller named the space, so the frame need not repeat it per record. + let mut batch = flag_batch(out, cursor, false); + if let Err(e) = flag_scan_space( + &mut image, + log.as_slice(), + &wanted, + first, + records, + mask, + mode, + &mut batch, + ) { + return -(e.to_errno() as i32); + } + + // SAFETY: both out-pointers are writable under this function's contract. + unsafe { finish_batch(&batch, cursor, out_len, out_cursor) } +} + +/// `CUBE_OP_FLAG_SCAN` with `CUBE_SPACE_EVERY`: the same question asked of every space. +/// +/// One batch and one cursor across all of them, so a caller pages through the whole store's matches +/// as one list — which is what "every event anywhere" has to mean if it is to be usable in batches +/// at all. The spaces are visited in the space table's order, and a space the log writes into that +/// the image has never seen is visited in its place in that same order, so the answer comes back in +/// (space, key) order exactly as a checkpoint writes records. +/// +/// # Safety +/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s. +unsafe fn v3_flag_scan_every( + mut image: Addressed, + mask: u16, + mode: u16, + cursor: u64, + buf: *mut u8, + cap: usize, + out_len: *mut u64, + out_cursor: *mut u64, +) -> i32 { + let mut log = KVVec::::new(); + if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() { + return -12; // -ENOMEM + } + // The spaces the log writes into, sorted — including any the image has never seen, which are the + // ones a plain walk of the space table would miss entirely. + let mut from_log = KVVec::<[u8; SPACE_ID_LEN]>::new(); + if log_spaces(log.as_slice(), &mut from_log).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, - }; + // The caller named no space, so every frame has to say which one its record came from. + let mut batch = flag_batch(out, cursor, true); - 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), + // One closure would have to borrow `image` and `batch` mutably at once, so the walk steps by + // hand; `full` is the batch saying it has no room for another record. + let mut full = false; + let mut row = 0u64; + let mut log_at = 0usize; + while row < image.header.space_count && !full { + let (space, first, records) = match image.space_row(row) { + Ok(Some(triple)) => triple, + Ok(None) => break, + Err(e) => return -(e.to_errno() as i32), + }; + // A space only the log writes sorts before this one: visit it here, in its place. + while log_at < from_log.len() + && from_log.as_slice()[log_at] < space + && !full + { + let candidate = from_log.as_slice()[log_at]; + log_at += 1; + match flag_scan_space(&mut image, log.as_slice(), &candidate, 0, 0, mask, mode, &mut batch) { + Ok(keep_going) => full = !keep_going, 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 space the log also writes into is the same space, not a second one. + if log_at < from_log.len() && from_log.as_slice()[log_at] == space { + log_at += 1; + } + if !full { + match flag_scan_space(&mut image, log.as_slice(), &space, first, records, mask, mode, &mut batch) { + Ok(keep_going) => full = !keep_going, + Err(e) => return -(e.to_errno() as i32), } - // 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, + } + row += 1; + } + // Whatever the log writes into that sorts after every space the image has. + while log_at < from_log.len() && !full { + let candidate = from_log.as_slice()[log_at]; + log_at += 1; + match flag_scan_space(&mut image, log.as_slice(), &candidate, 0, 0, mask, mode, &mut batch) { + Ok(keep_going) => full = !keep_going, + Err(e) => return -(e.to_errno() as i32), } } // 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 + unsafe { finish_batch(&batch, cursor, out_len, out_cursor) } } /// @@ -3437,23 +3653,43 @@ fn pack_record(key: &[u8], value: &[u8], out: &mut [u8], at: usize) -> Option Option { - let need = RAW_KEY_LEN + FLAGS_LEN + 4 + value.len(); +/// +/// The space leads, and only when `with_space` says so, because it is the one field the frame +/// cannot imply when the caller named no space — and it leads rather than trails because the +/// (space, key) order it forms is the order records are stored in and returned in. +fn pack_flagged( + space: &[u8; SPACE_ID_LEN], + key: &[u8], + flags: u16, + value: &[u8], + out: &mut [u8], + at: usize, + with_space: bool, +) -> Option { + let need = flagged_frame(with_space) + value.len(); if at + need > out.len() { return None; } + let mut at = at; + if with_space { + out[at..at + SPACE_ID_LEN].copy_from_slice(&space[..SPACE_ID_LEN]); + at += SPACE_ID_LEN; + } 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); + at += RAW_KEY_LEN; + out[at..at + FLAGS_LEN].copy_from_slice(&flags.to_le_bytes()); + at += FLAGS_LEN; + out[at..at + 4].copy_from_slice(&(value.len() as u32).to_le_bytes()); + at += 4; + out[at..at + value.len()].copy_from_slice(value); Some(need) } @@ -3519,6 +3755,7 @@ pub unsafe extern "C" fn cubelinux_kernel_enum( seen: 0, cursor, too_big: 0, + with_space: false, }; while let Some((key, value)) = walker.next() { if !batch.offer(key, value) { @@ -3621,6 +3858,7 @@ pub unsafe extern "C" fn cubelinux_kernel_range( seen: 0, cursor, too_big: 0, + with_space: false, }; while let Some((key, value)) = walker.next() { match region.place(key) { @@ -3650,7 +3888,7 @@ 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. +/// `CUBE_OP_FLAG_SCAN`: the live records 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 @@ -3661,6 +3899,10 @@ pub unsafe extern "C" fn cubelinux_kernel_range( /// 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. /// +/// `every_space` chooses between two questions rather than widening one: a space is a hard +/// partition, so "this class here" and "this class anywhere" are different answers, and the narrower +/// one is what a caller gets by leaving the field zero. +/// /// # 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`. @@ -3669,6 +3911,7 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan( space: *const u8, mask: u16, mode: u16, + every_space: u32, cursor: u64, buf: *mut u8, cap: usize, @@ -3681,15 +3924,25 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan( // 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. + // the worst way to be wrong. The same for a scope it does not know — a caller that asked for + // "every space" and silently got one space would get a subset, which is the other way to be + // wrong and just as quiet. if mode != FLAG_MODE_ANY && mode != FLAG_MODE_ALL { return -22; // -EINVAL } + if every_space != FLAG_SCOPE_ONE && every_space != FLAG_SCOPE_EVERY { + return -22; + } + let every = every_space == FLAG_SCOPE_EVERY; match Addressed::open() { Ok(Some(image)) => { return unsafe { - v3_flag_scan(image, wanted, mask, mode, cursor, buf, cap, out_len, out_cursor) + if every { + v3_flag_scan_every(image, mask, mode, cursor, buf, cap, out_len, out_cursor) + } else { + v3_flag_scan(image, wanted, mask, mode, cursor, buf, cap, out_len, out_cursor) + } } } Ok(None) => {} @@ -3704,8 +3957,8 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan( // 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. + // coordinate it holds, 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 @@ -3715,50 +3968,31 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan( 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 batch = flag_batch(out, cursor, every); - 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; + if every { + // Every space the log writes into, sorted — which is every space that can hold a match, + // since none of the image's records can. + let mut from_log = KVVec::<[u8; SPACE_ID_LEN]>::new(); + if log_spaces(log.as_slice(), &mut from_log).is_err() { + return -12; } - 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; + for space in from_log.as_slice() { + match flag_scan_log_space(log.as_slice(), space, mask, mode, &mut batch) { + Ok(true) => {} + Ok(false) => break, + Err(e) => return -(e.to_errno() as i32), + } } + } else if let Err(e) = flag_scan_log_space(log.as_slice(), &wanted, mask, mode, &mut batch) { + return -(e.to_errno() as i32); } // 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 + unsafe { finish_batch(&batch, cursor, out_len, out_cursor) } } /// `CUBE_OP_SPACES`: the `cursor`-th distinct space that holds a record, or -ENOENT at the end. diff --git a/include/uapi/linux/cube.h b/include/uapi/linux/cube.h index 48f17e7db..58fa9698a 100644 --- a/include/uapi/linux/cube.h +++ b/include/uapi/linux/cube.h @@ -138,13 +138,21 @@ struct cube_range_args { * — 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 */ + __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 */ @@ -157,4 +165,21 @@ struct cube_flag_scan_args { #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 */