cube(2): a refusal with two minus signs is not a refusal — and the walk it hung

The walk with no store behind it served ~200,000 invented records a minute and never ended.
Half of that was fixed and proven in fc057820f: the driver asks whether the store can be read
before either walk op answers anything, and it refuses. The client was still handed

    spaces returned 0, len=0, cursor=1

— success, no space, a cursor one further on — so it copied the space it was handed out of a
buffer the kernel never wrote, and with the cursor moving by itself the rule that ends every
other walk here, *no progress is the only end signal*, had nothing to fire on.

The remaining half was not in the C arm, and both candidate explanations recorded there are
wrong: the `ret < 0` test IS on the path, and nothing overwrote the answer. One boot at
loglevel=7 says what crosses the boundary instead:

    cubelinux: the store is not readable; refusing to answer     (x138 in 40 s)
    walk: spaces returned 0, len=0, cursor=1                     (the client, told success)

The guard fires and the client is told success, so the value is not negative. On that path the
driver's only return is `-(e.to_errno() as i32)`, and `kernel::error::Error` IS the kernel error
code: `from_errno(-2) == ENOENT`, `to_errno()` is documented as "the kernel error code", and the
API's own conversion of a `Result` to a C result — `kernel::from_result` — writes
`T::from(e.to_errno() as i16)` with no negation, as every other driver in this tree does. The
second minus sign made every refusal in this file a *positive* number, and a positive return is
what the C arm's `ret < 0`, the client's own wrapper, and the walk arms' `ret == 0` all read as
success.

38 sites in cubelinux_store.rs were shaped `-(e.to_errno() as i32)` / `as isize`. All 38 now
return `e.to_errno()`, which is what makes them refusals. Nothing else about them changed.

Why this became a *loop* in the space walk and nowhere else: CUBE_OP_SPACES is the one arm that
advances the cursor itself. Every other walk arm leaves the cursor where the caller put it, so a
bogus return there ends the walk on the client's no-progress rule — which is why one defect was
invisible at every other verb for as long as it existed. That arm now refuses a return that is
neither 0 ("here is a space") nor negative (a refusal), so a defect of this shape cannot be read
as a space again.

One more correction in the same class, found while proving the above. A store that cannot be
*opened* answered -ENOENT, and -ENOENT is this interface's own end-of-walk signal — "no such
space; the walk is finished" — so a walk over a store on a disk whose driver had not loaded read
exactly like a walk over an empty store, which is what the first benchmark boot was.
`store_file()` now answers ENODEV when the store device is not there. A store that is not there
is not an empty store.

Proven against the reproduction, in the guest, on the bench initramfs:

    cube_store=/dev/null        -> "walk: spaces failed: Invalid argument", 0 records, 5 s, boot finishes
    cube_store=/nowhere/x.img   -> "walk: spaces failed: No such device",    0 records, 5 s, boot finishes
    before:                       364,994 invented record lines in the 90 s the instrument allowed

The three checks are `kernel/verify-no-store.sh`, a gate on the wall now, and the same three
inside the benchmark rehearsal — which is where this defect was found, and where they were
warnings while it was open.

The diagnostic prints this was hunted with come off in the same commit: the `cube_store=
resolved to` line in cube_syscall.c, and the per-call "not readable" warning in both walk ops.
The refusal is the return value, and the walk's own transcript is where a reader learns what
happened; a message per call is a diagnostic, not the interface. The guard itself stays, and
where to find it is written down at its definition rather than implied.

Built as #95, which is what the box now has installed: the machine boots it on its next reboot.
This commit is contained in:
surface-camera-build
2026-09-23 22:42:36 -04:00
parent fc057820fa
commit 8db279a4d8
2 changed files with 79 additions and 50 deletions
+16 -1
View File
@@ -95,7 +95,6 @@ static char store_device_path[256] = "/dev/vda";
static int __init cube_store_setup(char *str)
{
strscpy(store_device_path, str, sizeof(store_device_path));
pr_info("cubelinux: cube_store= resolved to %s\n", store_device_path);
return 1;
}
__setup("cube_store=", cube_store_setup);
@@ -265,8 +264,24 @@ static long cube_enum_op(unsigned int op, void __user *uargs)
__u8 found[32];
ret = cubelinux_kernel_spaces(e.cursor, found);
/*
* The driver answers with one space written and 0, or with a negative errno. A
* positive value is neither, and it must not be read as either: the buffer was not
* written, so a caller handed this answer copies a space nobody found.
*
* This arm is the only one that advances the cursor by itself. Every other walk
* leaves the cursor where the caller put it, so a bogus return there ends the walk
* on the client's no-progress rule. Here it would hand the walk a cursor that keeps
* moving over a space that is not there which is not a hypothetical: a kernel error
* code that had lost its sign did exactly that, and the walk served ~200,000 invented
* records a minute until the machine stopped making progress. The cause is fixed
* where it was (the driver's errno conversions, cubelinux_store.rs); this is the
* bound that keeps a defect of that shape from becoming a loop again.
*/
if (ret < 0)
return ret;
if (ret != 0)
return -EINVAL;
memcpy(e.space, found, sizeof(found));
/* An index here, not a count of records: hand back the one after this space. */
e.cursor = e.cursor + 1;
+63 -49
View File
@@ -466,7 +466,15 @@ fn store_file() -> Result<*mut bindings::file> {
// O_RDWR is 2. SAFETY: store_device() is a NUL-terminated C string the module parameter filled
// at boot; filp_open returns a valid `struct file *` or an error pointer, checked below.
let filp = unsafe { bindings::filp_open(store_device(), 2, 0) };
let filp = kernel::error::from_err_ptr(filp)?;
// A store that is not there is not an empty store, and this interface has to be able to say so.
// Every walk here ends on -ENOENT — "no such space; the walk is finished" — so a store that
// cannot even be opened would answer its caller with an empty listing and a success, and a
// caller has no way to tell the two apart. That is not hypothetical either: the benchmark boot
// that started this work had its store on a disk whose driver had not loaded, and the walk had
// no way to say what was wrong. "No such device" is what the store being absent is called here;
// the interface's own words for it are `cube_store=` and `/dev/cubelinux`.
let filp =
kernel::error::from_err_ptr(filp).map_err(|e| if e == ENOENT { ENODEV } else { e })?;
if filp.is_null() {
return Err(EINVAL);
}
@@ -2088,7 +2096,7 @@ pub unsafe extern "C" fn cubelinux_kernel_put(
};
match append_mutation(&mutation, 1) {
Ok(_) => 0,
Err(e) => -(e.to_errno() as i32),
Err(e) => e.to_errno() as i32,
}
}
@@ -3301,16 +3309,16 @@ unsafe fn v3_get(
let (first, records) = match image.space_entry(&sp) {
Ok(Some(entry)) => entry,
Ok(None) => return -2,
Err(e) => return -(e.to_errno() as isize),
Err(e) => return e.to_errno() as isize,
};
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),
Err(e) => return e.to_errno() as isize,
};
let value = match image.value_at(value_off, value_len) {
Ok(v) => v,
Err(e) => return -(e.to_errno() as isize),
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.
@@ -3405,7 +3413,7 @@ unsafe fn v3_get_chain(
let image = match Addressed::open() {
Ok(Some(image)) => image,
Ok(None) => return -95, // -EOPNOTSUPP
Err(e) => return -(e.to_errno() as isize),
Err(e) => return e.to_errno() as isize,
};
// The next slice is arithmetic, not a pointer: the same space at the same x and y, one `z`
// further along. Nothing is stored to find it, so nothing can point wrongly.
@@ -3448,7 +3456,7 @@ unsafe fn v3_get_chain(
let image = match Addressed::open() {
Ok(Some(image)) => image,
Ok(None) => return -95, // -EOPNOTSUPP
Err(e) => return -(e.to_errno() as isize),
Err(e) => return e.to_errno() as isize,
};
let (sp, key) = unsafe { coord_key(space, x, y, zz) };
let mut slice_flags = 0u16;
@@ -3566,7 +3574,7 @@ unsafe fn v3_enum(
let (first, records) = match image.space_entry(&wanted) {
Ok(Some(entry)) => entry,
Ok(None) => (0, 0),
Err(e) => return -(e.to_errno() as i32),
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();
@@ -3621,11 +3629,11 @@ unsafe fn v3_enum(
while at < first + records {
let (value_off, value_len, flags) = match image.index_entry(at, &mut entry) {
Ok(triple) => triple,
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
};
let value = match image.value_at(value_off, value_len) {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
};
let key: &[u8; RAW_KEY_LEN] = match entry.as_slice()[..RAW_KEY_LEN].try_into() {
Ok(k) => k,
@@ -3660,7 +3668,7 @@ unsafe fn v3_enum(
let image_entry = if at < first + records {
match image.index_entry(at, &mut entry) {
Ok(pair) => Some(pair),
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
}
} else {
None
@@ -3686,7 +3694,7 @@ unsafe fn v3_enum(
(_, 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),
Err(e) => return e.to_errno() as i32,
};
at += 1;
if !batch.offer(&wanted, key, flags, value.as_slice()) {
@@ -3749,7 +3757,7 @@ unsafe fn v3_range(
let (first, records) = match image.space_entry(&wanted) {
Ok(Some(entry)) => entry,
Ok(None) => (0, 0),
Err(e) => return -(e.to_errno() as i32),
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();
@@ -3772,7 +3780,7 @@ unsafe fn v3_range(
// The index's half.
let mut at = match image.lower_bound(&region.foot, first, records) {
Ok(p) => p,
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
};
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
@@ -3805,7 +3813,7 @@ unsafe fn v3_range(
let image_entry = if at < first + records {
match image.index_entry(at, &mut entry) {
Ok(pair) => Some(pair),
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
}
} else {
None
@@ -3860,7 +3868,7 @@ unsafe fn v3_range(
(_, 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),
Err(e) => return e.to_errno() as i32,
};
at += 1;
if region.holds(key) && !batch.offer(&wanted, key, flags, value.as_slice()) {
@@ -4118,7 +4126,7 @@ unsafe fn v3_flag_scan(
let (first, records) = match image.space_entry(&wanted) {
Ok(Some(entry)) => entry,
Ok(None) => (0, 0),
Err(e) => return -(e.to_errno() as i32),
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();
@@ -4140,7 +4148,7 @@ unsafe fn v3_flag_scan(
mode,
&mut batch,
) {
return -(e.to_errno() as i32);
return e.to_errno() as i32;
}
// SAFETY: both out-pointers are writable under this function's contract.
@@ -4192,7 +4200,7 @@ unsafe fn v3_flag_scan_every(
let (space, first, records) = match image.space_row(row) {
Ok(Some(triple)) => triple,
Ok(None) => break,
Err(e) => return -(e.to_errno() as i32),
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()
@@ -4203,7 +4211,7 @@ unsafe fn v3_flag_scan_every(
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),
Err(e) => return e.to_errno() as i32,
}
}
// A space the log also writes into is the same space, not a second one.
@@ -4213,7 +4221,7 @@ unsafe fn v3_flag_scan_every(
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),
Err(e) => return e.to_errno() as i32,
}
}
row += 1;
@@ -4224,7 +4232,7 @@ unsafe fn v3_flag_scan_every(
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),
Err(e) => return e.to_errno() as i32,
}
}
@@ -4289,11 +4297,11 @@ pub unsafe extern "C" fn cubelinux_kernel_get(
return got;
}
Ok(None) => {}
Err(e) => return -(e.to_errno() as isize),
Err(e) => return e.to_errno() as isize,
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as isize),
Err(e) => return e.to_errno() as isize,
};
match log_effect(view.log(), &sp, &key) {
@@ -4409,8 +4417,7 @@ pub unsafe extern "C" fn cubelinux_kernel_enum(
out_cursor: *mut u64,
) -> i32 {
if let Err(e) = store_readable() {
pr_warn!("cubelinux: the store is not readable; refusing to answer\n");
return -(e.to_errno() as i32);
return e.to_errno() as i32;
}
let mut wanted = [0u8; SPACE_ID_LEN];
// SAFETY: the caller guarantees 32 readable bytes at `space`.
@@ -4421,12 +4428,12 @@ pub unsafe extern "C" fn cubelinux_kernel_enum(
return unsafe { v3_enum(image, wanted, cursor, buf, cap, out_len, out_cursor) }
}
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
};
// The live records of this space: the image's, with the log's edits applied as an overlay.
@@ -4526,12 +4533,12 @@ pub unsafe extern "C" fn cubelinux_kernel_range(
return unsafe { v3_range(image, wanted, region, cursor, buf, cap, out_len, out_cursor) }
}
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
};
let mut walker = match SpaceWalker::new(&view, &wanted) {
Ok(w) => w,
@@ -4637,12 +4644,12 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan(
}
}
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
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"
@@ -4675,25 +4682,17 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan(
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),
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);
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_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
/// `CUBE_OP_ENUM` once it has learned the name. Entries are already in (space, key) order, so the
/// distinct spaces come out sorted.
///
/// # Safety
/// `space_out` must point to 32 writable bytes.
/// Whether the store can be read at all, asked before an operation answers anything.
///
/// A walk pointed at something that is not a store served invented records — a garbage space, a
@@ -4704,6 +4703,14 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan(
/// downstream answered over a device that cannot be read.
///
/// So the question is asked here, where the answer leaves no room: four bytes at offset zero.
///
/// **A refusal is only a refusal if it is negative.** This guard fired on every call and the walk
/// still received "here is a space": the callers below converted the error with `-(e.to_errno())`,
/// and [`Error::to_errno`] already returns the kernel's error code, which is negative. A doubled
/// minus sign made every refusal in this file a *positive* number, and a positive return is what
/// every layer above reads as success — the syscall's `ret < 0`, and the client's own wrapper. The
/// conversion is now `e.to_errno()`, as `kernel::from_result` does it, and the walk arm that advances
/// its own cursor refuses a positive answer as well, so this cannot be read as an answer again.
fn store_readable() -> Result<()> {
let file = store_file()?;
let mut head = KVVec::<u8>::with_capacity(8, GFP_KERNEL)?;
@@ -4716,20 +4723,27 @@ fn store_readable() -> Result<()> {
Ok(())
}
/// `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
/// `CUBE_OP_ENUM` once it has learned the name. Entries are already in (space, key) order, so the
/// distinct spaces come out sorted.
///
/// # Safety
/// `space_out` must point to 32 writable bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8) -> i32 {
if let Err(e) = store_readable() {
pr_warn!("cubelinux: the store is not readable; refusing to answer\n");
return -(e.to_errno() as i32);
return e.to_errno() as i32;
}
match Addressed::open() {
Ok(Some(image)) => return unsafe { v3_spaces(image, cursor, space_out) },
Ok(None) => {}
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
}
let view = match read_view() {
Ok(v) => v,
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
};
// Candidates, in ascending order: the spaces the image's records run through (a checkpoint
@@ -4816,7 +4830,7 @@ pub unsafe extern "C" fn cubelinux_kernel_del(space: *const u8, x: u64, y: u64,
// bounded read: a delete does not touch the records either.
match append_mutation(&mutation, 2) {
Ok(_) => 0,
Err(e) => -(e.to_errno() as i32),
Err(e) => e.to_errno() as i32,
}
}
@@ -4827,11 +4841,11 @@ pub extern "C" fn cubelinux_kernel_sync() -> i32 {
ensure_boot_record();
let (device, layout) = match device_and_layout() {
Ok(pair) => pair,
Err(e) => return -(e.to_errno() as i32),
Err(e) => return e.to_errno() as i32,
};
match fold_now(&device, &layout) {
Ok(()) => 0,
Err(e) => -(e.to_errno() as i32),
Err(e) => e.to_errno() as i32,
}
}