From 1081b5f1e2aadbae84578c6a55ff7d9ab46026e6 Mon Sep 17 00:00:00 2001 From: surface-camera-build Date: Tue, 22 Sep 2026 01:34:23 -0400 Subject: [PATCH] cube(2): CUBE_OP_GET answers with the record's class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last hole in the substrate: a caller could write a class through the syscall but not read one back. `CUBE_OP_GET` now fills `args.flags` from the same index entry the address came from, so learning what a record *is* costs nothing beyond a read that was going to happen. One field for both directions, because it is one thing — the class of this record. A write states it, a read learns it, and neither is a special case of the other. `find` hands the mask back with the address for the same reason: it is in the same stride the binary search already read, so wanting both does not mean searching twice. A read that finds nothing leaves 0 rather than a stale class for the caller to believe. The size of `cube_args` does not change, which matters because `size` is what says which argument block arrived. --- drivers/cube/cube_syscall.c | 5 +-- drivers/cube/cubelinux_store.rs | 58 +++++++++++++++++++++++++-------- include/uapi/linux/cube.h | 25 +++++++------- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/drivers/cube/cube_syscall.c b/drivers/cube/cube_syscall.c index 1601bfc6c..20c9b6398 100644 --- a/drivers/cube/cube_syscall.c +++ b/drivers/cube/cube_syscall.c @@ -32,7 +32,7 @@ int cubelinux_kernel_put(const __u8 *space, __u64 x, __u64 y, __u64 z, const void *value, size_t len, __u16 flags); ssize_t cubelinux_kernel_get(const __u8 *space, __u64 x, __u64 y, __u64 z, - void *buf, size_t len); + void *buf, size_t len, __u16 *out_flags); int cubelinux_kernel_del(const __u8 *space, __u64 x, __u64 y, __u64 z); int cubelinux_kernel_sync(void); @@ -196,7 +196,8 @@ static long cube_args_op(unsigned int op, void __user *uargs) ssize_t got; got = cubelinux_kernel_get(args.coord.space, args.coord.x, - args.coord.y, args.coord.z, buf, args.len); + args.coord.y, args.coord.z, buf, args.len, + &args.flags); if (got < 0) { ret = got; break; diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index b5fea2867..0678b50a4 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -2105,7 +2105,8 @@ impl<'a> Records<'a> { /// What the log says about one coordinate: written, or removed. enum Effect<'a> { - Write(&'a [u8]), + /// The bytes it wrote, and the class mask it wrote them under. + Write(&'a [u8], u16), Delete, } @@ -2116,12 +2117,12 @@ 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, _flags, 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 } else { - Effect::Write(&log[value_at..value_at + value_len]) + Effect::Write(&log[value_at..value_at + value_len], flags) }); } } @@ -2771,8 +2772,17 @@ impl Addressed { 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. - fn find(&mut self, key: &[u8; RAW_KEY_LEN], first: u64, records: u64) -> Result> { + /// The address of `key` inside a space's index range, or nothing if the space does not hold it: + /// `(value_off, value_len, flags)`. + /// + /// The mask comes back with the address because it is in the same stride the search already + /// read, so a read that wants both does not search twice for one entry. + fn find( + &mut self, + key: &[u8; RAW_KEY_LEN], + first: u64, + records: u64, + ) -> Result> { let mut buf = KVVec::::new(); let mut lo = 0u64; let mut hi = records; @@ -2787,9 +2797,7 @@ impl Addressed { } else if found > &key[..] { hi = mid; } else { - return self - .index_entry(first + mid, &mut buf) - .map(|(value_off, value_len, _)| Some((value_off, value_len))); + return self.index_entry(first + mid, &mut buf).map(Some); } } Ok(None) @@ -2842,7 +2850,14 @@ impl Addressed { /// /// # Safety /// `buf` must hold `len` writable bytes. -unsafe fn v3_get(mut image: Addressed, sp: [u8; SPACE_ID_LEN], key: [u8; RAW_KEY_LEN], buf: *mut u8, len: usize) -> isize { +unsafe fn v3_get( + mut image: Addressed, + sp: [u8; SPACE_ID_LEN], + key: [u8; RAW_KEY_LEN], + buf: *mut u8, + len: usize, + out_flags: *mut u16, +) -> isize { // The log is owned for the duration: what it says borrows it, and reading the image needs // `&mut image`. It is what a checkpoint has not folded, so it is small. let mut log = KVVec::::new(); @@ -2851,7 +2866,11 @@ unsafe fn v3_get(mut image: Addressed, sp: [u8; SPACE_ID_LEN], key: [u8; RAW_KEY } match log_effect(log.as_slice(), &sp, &key) { Some(Effect::Delete) => return -2, // -ENOENT - Some(Effect::Write(value)) => return copy_out(value, buf, len), + Some(Effect::Write(value, flags)) => { + // SAFETY: the caller guarantees a writable u16. + unsafe { *out_flags = flags }; + return copy_out(value, buf, len); + } None => {} } let (first, records) = match image.space_entry(&sp) { @@ -2859,7 +2878,7 @@ unsafe fn v3_get(mut image: Addressed, sp: [u8; SPACE_ID_LEN], key: [u8; RAW_KEY Ok(None) => return -2, Err(e) => return -(e.to_errno() as isize), }; - let (value_off, value_len) = match image.find(&key, first, records) { + let (value_off, value_len, flags) = match image.find(&key, first, records) { Ok(Some(entry)) => entry, Ok(None) => return -2, Err(e) => return -(e.to_errno() as isize), @@ -2868,6 +2887,10 @@ unsafe fn v3_get(mut image: Addressed, sp: [u8; SPACE_ID_LEN], key: [u8; RAW_KEY Ok(v) => v, Err(e) => return -(e.to_errno() as isize), }; + // The record's own class, from the index entry the address came from. A v3 entry has no mask + // and answers zero, which is what "no class" means everywhere here. + // SAFETY: the caller guarantees a writable u16. + unsafe { *out_flags = flags }; copy_out(value.as_slice(), buf, len) } @@ -3615,12 +3638,17 @@ pub unsafe extern "C" fn cubelinux_kernel_get( z: u64, buf: *mut u8, len: usize, + out_flags: *mut u16, ) -> isize { let (sp, key) = unsafe { coord_key(space, x, y, z) }; + // A read that finds nothing must not leave a class behind for the caller to believe: the answer + // is "no record", and a stale mask beside it would be worse than none. + // SAFETY: the caller guarantees a writable u16. + unsafe { *out_flags = 0 }; // A v3 image is addressed, so a read is a binary search and a few bytes. v1 and v2 images are // packed lists, which the walking reader below handles. match Addressed::open() { - Ok(Some(image)) => return unsafe { v3_get(image, sp, key, buf, len) }, + Ok(Some(image)) => return unsafe { v3_get(image, sp, key, buf, len, out_flags) }, Ok(None) => {} Err(e) => return -(e.to_errno() as isize), } @@ -3631,7 +3659,11 @@ pub unsafe extern "C" fn cubelinux_kernel_get( match log_effect(view.log(), &sp, &key) { Some(Effect::Delete) => return -2, // -ENOENT - Some(Effect::Write(value)) => return copy_out(value, buf, len), + Some(Effect::Write(value, flags)) => { + // SAFETY: the caller guarantees a writable u16. + unsafe { *out_flags = flags }; + return copy_out(value, buf, len); + } None => {} } diff --git a/include/uapi/linux/cube.h b/include/uapi/linux/cube.h index 44d3cb44c..f2a14a25b 100644 --- a/include/uapi/linux/cube.h +++ b/include/uapi/linux/cube.h @@ -34,18 +34,21 @@ struct cube_args { * out: on -ERANGE, the bytes that would be needed; * on success for CUBE_OP_GET, the bytes read. */ - __u16 flags; /* in: CUBE_OP_PUT only — the class mask to stamp on the - * record. The mask is written where the record is written, - * which is the moment its class is known for certain; a - * reader never has to re-derive it. 0 is "no class", which - * is what every record written before the field existed - * reads as, so a caller that does not classify is not - * writing a special value. + __u16 flags; /* in: CUBE_OP_PUT — the class mask to stamp on the record; + * out: CUBE_OP_GET — the mask the record carries. * - * A store whose image is the legacy packed layout has no - * field to put a mask in, and drops it: that layout cannot - * carry a class and saying otherwise would be a lie about - * the bytes on disk. + * One field for both directions because it is one thing: the + * class of this record. A write states it, a read learns it, + * and neither is a special case of the other. 0 is "no class", + * which is what every record written before the field existed + * reads as — so not classifying is not writing a special value, + * and a read that found nothing leaves 0 rather than a stale + * class for the caller to believe. + * + * A store whose image is the legacy packed layout has no field + * to put a mask in, and drops it: that layout cannot carry a + * class, and saying otherwise would be a lie about the bytes on + * disk. A read from such a store answers 0 for the same reason. */ __u16 reserved; /* must be 0 */ };