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

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

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

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

Also settles what an append writes into which log, since the entry's frame
has to match the header a reader frames it by: a log that already holds v1
entries keeps taking v1 entries (a device folds first — that is the v4
migration — and a bare image keeps the log it has), an empty log is framed
v2 on a device and left alone on a bare image, and a log with no header is
framed v2 on a device and v1 on a bare image. The bare layout is the legacy
one: it has no index for a mask to be folded into, so a mask written there
could only be scanned and never checkpointed — half a feature, bought by
making every existing reader of that layout grow a version it cannot use.
This commit is contained in:
surface-camera-build
2026-09-22 00:08:19 -04:00
parent 0267831b18
commit 41e437c07a
3 changed files with 549 additions and 46 deletions
+65 -1
View File
@@ -56,6 +56,15 @@ int cubelinux_kernel_range(const __u8 *space,
__u64 cursor, void *buf, size_t cap,
__u64 *out_len, __u64 *out_cursor);
/*
* The flag scan (CUBE_OP_FLAG_SCAN). The mask and its mode travel as plain numbers: which bits mean
* what is a vocabulary's business, and the kernel never interprets one it compares masks, which is
* what lets a new vocabulary attach without a format change.
*/
int cubelinux_kernel_flag_scan(const __u8 *space, __u16 mask, __u16 mode,
__u64 cursor, void *buf, size_t cap,
__u64 *out_len, __u64 *out_cursor);
/* The store device path, resolved from the `cube_store=` boot parameter at boot. */
const char *cubelinux_store_device(void);
@@ -361,7 +370,60 @@ static long cube_range_op(void __user *uargs)
}
/*
* One syscall, three argument blocks. They share a prefix `size`, then `op` so the size the
* The flag scan CUBE_OP_FLAG_SCAN which travels in `struct cube_flag_scan_args`.
*
* The walks' shape again, because it is the walks' contract: a batch fills the caller's buffer and
* returns how much was used plus the cursor to pass next; a record that does not fit ends the
* batch; a record that cannot fit in any buffer the caller offered comes back as -ERANGE with `len`
* saying what it would need.
*
* The one thing a caller must know beyond the walk's rules: the cursor counts the records that
* **matched**, not the records examined, because the records the mask rejected are not answers.
*/
static long cube_flag_scan_op(void __user *uargs)
{
struct cube_flag_scan_args f;
void *buf = NULL;
long ret = 0;
u64 out_len = 0, out_cursor = 0;
if (copy_from_user(&f, uargs, sizeof(f)))
return -EFAULT;
if (f.size != sizeof(struct cube_flag_scan_args) || f.op != CUBE_OP_FLAG_SCAN)
return -EINVAL;
if (f.mode != CUBE_FLAG_ANY && f.mode != CUBE_FLAG_ALL)
return -EINVAL;
if (f.len > CUBE_MAX_WALK)
return -E2BIG;
if (f.len > 0) {
buf = kvmalloc(f.len, GFP_KERNEL);
if (!buf)
return -ENOMEM;
}
ret = cubelinux_kernel_flag_scan(f.space, f.mask, f.mode, f.cursor,
buf, f.len, &out_len, &out_cursor);
if (ret == 0) {
if (out_len > 0 && copy_to_user((void __user *)f.value, buf, out_len))
ret = -EFAULT;
f.len = out_len;
f.cursor = out_cursor;
if (copy_to_user(uargs, &f, sizeof(f)))
ret = -EFAULT;
} else if (ret == -ERANGE) {
/* Nothing was written; `len` now says how much one record needs. */
f.len = out_len;
if (copy_to_user(uargs, &f, sizeof(f)))
ret = -EFAULT;
}
kvfree(buf);
return ret;
}
/*
* One syscall, four argument blocks. They share a prefix `size`, then `op` so the size the
* caller declares is what says which one arrived. That is the whole point of putting `size`
* first: an interface that cannot grow has to be replaced, and this one grows by being given a
* new block with a new size.
@@ -378,5 +440,7 @@ SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs)
return cube_enum_op(op, uargs);
if (size == sizeof(struct cube_range_args))
return cube_range_op(uargs);
if (size == sizeof(struct cube_flag_scan_args))
return cube_flag_scan_op(uargs);
return -EINVAL;
}
+438 -45
View File
@@ -1167,24 +1167,34 @@ struct Mutation {
value: KVVec<u8>,
}
/// 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<KVVec<u8>, AllocError> {
let total = ENTRY_FIXED_V2 + value.len();
let mut entry = KVVec::<u8>::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::<u8>::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<Option<Control>, 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 && &region[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 && &region[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 || &region[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 || &region[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<Effect<'a>> {
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<LogEntries<'_>, &'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<u8>) -> 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<u8>) -> 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::<u8>::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::<u8>::new();
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
let mut edits = KVVec::<LogEdit<'_>>::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::<u8>::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<us
Some(need)
}
/// Pack one record for a flag scan: `key(24) | flags(2, LE) | value_len(u32, LE) | value`.
///
/// The mask travels with the record, unlike the walk's frame, because a scan's answer has to say
/// *what class* each record answered with. A record can carry bits the scan did not ask for, and
/// no other op returns a mask — so dropping it here would make those bits unreachable, which is
/// the opposite of what a classification substrate is for. The field order echoes the v4 index
/// entry (`key | flags | …`), so the wire shape is the index's shape.
fn pack_flagged(key: &[u8], flags: u16, value: &[u8], out: &mut [u8], at: usize) -> Option<usize> {
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::<u8>::new();
if log.extend_from_slice(view.log(), GFP_KERNEL).is_err() {
return -12; // -ENOMEM
}
let mut edits = KVVec::<LogEdit<'_>>::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
+46
View File
@@ -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 */