Compare commits

...
5 Commits
Author SHA1 Message Date
surface-camera-build 8c24a7ff0d store: an acknowledged write is durable again, in the shape nothing was flushing
`verify-kernel-append` asks the question a log exists to answer: four writes are acknowledged, the
VM is killed with `SIGKILL` with no shutdown at all, and the log must still hold them. It is red on
`#95` and on `#87` — the kernel this box runs — and green on the kernel built at 18:41, and the one
commit between them is e0218ec96: the collapse to a single `fsync` per append, where the entry goes
down unsynced and the control write that counts it carries the flush for both.

That collapse is right where its sentence is true, and the sentence assumes there is a control block
to write. *A store with no control block has no count write*, so nothing on the append path was
flushed at all — and the caller was told a write it may never get back. That is not an exotic shape:
it is what a store the CLI builds looks like, and it is the shape that gate's own device has.
Measured, same device, same four writes, the host's copy of the device read while the guest is still
hung:

    no control block    the log region holds its header and zeros where the entry should be, and
                        the fold returns the base store: records=2
    a formatted device  the entry is on the device, and the fold reproduces the userspace store
                        exactly: records=6, fnv1a64=6b679d39597a62b3

and a pre-collapse kernel keeps the entry in *both* shapes, because the entry's own `fsync` is what
made it durable there.

So the fix is not the revert. `append` takes the entry's own `fsync` exactly when no later write in
the same operation will carry one — `layout.control.is_some()`, one boolean, both branches gated by
the two shapes above. The box's store is a formatted file with a control block, so it keeps the
measured 4.96 ms win; a bare store pays its entry's flush, which is what it always did before the
collapse. The count still goes second, so a crash can still lose an unacknowledged mutation rather
than count one that is not there.

The gate was wrong in the same way the first diagnosis was, and is fixed with the kernel: it ran its
crash check over one device shape, which is the shape that made the collapse look safe everywhere. It
now runs it over both — a store with no control block and a formatted device — folds each the way
userspace reads it, and compares both against the same userspace store, so "it survived" means the
same thing for each. It also stops SIGKILLing the `timeout` wrapper rather than QEMU: SIGKILL cannot
be forwarded, so every run of this gate left a 512 MiB VM spinning in `pause()` for the rest of the
wall, twice found beside a gate that measures latency.

Built as #96. The wall, whole: 26 of 26.
2026-09-23 23:16:31 -04:00
surface-camera-build 8db279a4d8 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.
2026-09-23 22:42:36 -04:00
surface-camera-build fc057820fa store: refuse a store that cannot be read — and the proof that the refusal is not the whole defect
The guard is `store_readable()`: four bytes at offset zero, asked before either walk
op answers anything. Proven against the reproduction, which is the reproduction
from the record — `cube_store=/dev/null cubelinux.enum=1`:

  cubelinux: cube_store= resolved to /dev/null                 (the token was honoured)
  cubelinux: the store is not readable; refusing to answer      (the kernel refuses)

and the client is *still* handed `spaces returned 0, len=0, cursor=1`, so it copies
an unfilled space and walks 199,000 invented records. That is the whole defect in
one transcript: the driver refuses and the syscall boundary reports success anyway.

So this commit fixes and proves half of it. What remains is `cube_syscall.c`'s
CUBE_OP_SPACES arm, where the driver's -EINVAL becomes a 0 with a cursor advanced by
one — either its `ret < 0` test is not on the path the client takes, or the answer is
overwritten before the copy-out. Nothing above or below that needs touching.

Three earlier guesses are recorded as wrong rather than deleted: the packed fallback
and the magic guard written for it are never reached (probed, proven), and
`read_exact_at` already rejects a short read. The guard here is the first one that
fires.

The device-path print in cube_syscall.c stays on purpose: it is what turned four
builds of inference into one line of fact.

Built as #93, deliberately NOT installed — installing it alone would leave the walk
still looping. The machine runs #87.
2026-09-23 22:02:22 -04:00
surface-camera-build 9fb4169e52 store: refuse a device that does not carry the packed image's magic (inert, NOT installed)
This is the guard for one of the two doors on the walk-with-no-store defect:
"neither control block decodes" is not evidence of a packed image, so the packed
reader is allowed only for a device that begins with the image header's own magic —
the question the userspace raw reader has always asked and the kernel did not.

It builds clean and it does NOT stop the reproduction: `cube_store=/dev/null
cubelinux.enum=1` still serves invented records at the same rate. So the guard is
not on the path that answers, and it is committed for that reason stated plainly
rather than installed: this machine stays on #87, which is measured, instead of
moving to a kernel whose only change is one that has not been shown to do anything.

Kept rather than reverted because the check is right on its own terms — an
addressed store begins with CUBS, a bare packed image with CUBE — and because the
next attempt should begin by asking whether this guard is even reached before
writing another one.
2026-09-23 21:15:53 -04:00
surface-camera-build 4cfdbfe63b store: a get on the first slice of a chain returns the whole value
The arrangement vocabulary's second half — the producer and the userspace
reader have existed since 0244-ish, and this is the kernel joining them for a
caller who does not know it holds a chain. Bounded by the same 65,536 slices the
userspace reader uses and by MAX_CHAIN_BYTES (CUBE_MAX_VALUE, 16 MiB: a longer
chain could not be handed over in one call, so reading on would be work whose
answer nobody can receive). A hole, a slice that claims to continue and does not,
and a second beginning are each -EIO rather than a guessed end.

A joined read reports START|END, because the flags describe the bytes handed over
rather than the slice they came from — which is also why the userspace reader
needed no change: it already stops at END_RECORD.

It does NOT join a sealed chain, and that is a fact about the format: a sealed
slice is an envelope with its own header and nonce, so N of them concatenated are
not one openable value, and joining them belongs to whoever holds the key. When
it cannot join (sealed, or a packed image with no index) it returns the slice with
the record's own flags — CONTINUATION without END_RECORD says "this is a slice,
not the whole value" — so the caller is told rather than misled.

Reading those bits at all was safe because nothing uses them, and that was
measured before the code was written: over the live store's 69,749 records,
flag-scan finds 1 record in 0x0080, 137 in 0x0800, and zero in every one of
0x0001, 0x0002, 0x0003, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0100, 0x0200.

Gated by kernel/verify-chain.sh: PASS, seven checks.
2026-09-23 19:59:57 -04:00
2 changed files with 327 additions and 47 deletions
+16
View File
@@ -264,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;
+311 -47
View File
@@ -220,6 +220,11 @@ const LOG_ALIGN: usize = 4096;
/// Control block magic, distinct from the image's `CUBE` and the log's `CUBW`.
const CTL_MAGIC: [u8; 4] = *b"CUBS";
const CTL_VERSION: u8 = 1;
/// The image header's own magic, which a bare packed image begins with.
///
/// It is read for one decision: a device whose control block does not decode is a packed image only
/// if it says so. Without that question, anything at all was walked as though it were a store.
const IMAGE_MAGIC: &[u8; 4] = b"CUBE";
/// One control copy, and where the two live.
const CTL_COPY_LEN: usize = 2048;
const CTL_COPY_A: usize = 0;
@@ -461,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);
}
@@ -1328,6 +1341,13 @@ fn encode_entry(
/// control write's `fsync` flushes what precedes it in the same file. So the entry's own fsync is a
/// second flush of data the next one covers.
///
/// **Only where that next write exists.** A store with no control block has no count to write, so
/// nothing after the entry is flushed and this call would be the last word on a mutation that is
/// only in the page cache — an acknowledgement of a write the machine may never get back. `append`
/// therefore calls this only when `layout.control` is `Some`, and takes the entry's own `fsync`
/// otherwise; the two cases are one boolean apart and both are gated
/// (`kernel/verify-kernel-append.sh`, over an unformatted device and a formatted one).
///
/// What this gives up, stated rather than implied: the two writes are no longer independently
/// durable, so a crash *inside* the control write's commit can in principle leave the control block
/// counting bytes whose entry did not fully reach the media. That is a torn tail, and
@@ -1678,6 +1698,18 @@ fn append(
// valid file or an error pointer, which is checked. O_RDWR is 2.
let file = store_file()?;
// Whether this append gets its durability from its own flush or from the control block's.
//
// The collapse to one `fsync` per append rests on a sentence: the entry goes down unsynced and
// the control write that counts it — written next, to the same file — carries the flush for
// both. That is true where there is a control block to write, and is measured to be true
// (`kernel/verify-kernel-append.sh` over a formatted device: the entry is on the device after a
// SIGKILL). It is *not* true of a store with no control block: the count write never happens,
// so nothing on this path is flushed at all and the caller is told a write it may never get
// back. That shape is not exotic — it is what a store the CLI builds looks like, and it is the
// shape that gate's own device has, where four acknowledged writes did not survive a kill.
// So the entry carries its own flush exactly when no later write in this operation will.
let synced_by_the_count = layout.control.is_some();
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 header that does not already
@@ -1692,14 +1724,16 @@ fn append(
result = write_and_sync(file, layout.log_off as u64, &hdr);
}
if result.is_ok() {
// The entry, without its own flush: the control block below is written next and synced, and
// that sync flushes this. One durability boundary per append instead of two — measured, one
// `pwrite`+`fsync` on this filesystem is 4.3 ms, so this is about half of every write.
result = write_at(
file,
(region_at + WAL_HEADER_LEN + used) as u64,
entry.as_slice(),
);
// One durability boundary per append where the control block can carry it — measured, one
// `pwrite`+`fsync` on this filesystem is 4.3 ms, so this is about half of every write. Where
// it cannot, this write is the boundary, and a write that is acknowledged without having
// been flushed is not a write.
let at = (region_at + WAL_HEADER_LEN + used) as u64;
result = if synced_by_the_count {
write_at(file, at, entry.as_slice())
} else {
write_and_sync(file, at, entry.as_slice())
};
}
// The count second, so replay can never be told about an entry that is not there.
@@ -2083,7 +2117,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,
}
}
@@ -3014,9 +3048,26 @@ impl Addressed {
(Some(x), Some(y)) => core::cmp::max(x, y),
(Some(x), None) => x,
(None, Some(y)) => y,
// Neither copy decodes: there is no control block, so this is a bare image and the
// walking reader handles it.
(None, None) => return Ok(None),
// Neither copy decodes. That is *not* by itself evidence of a packed image: it is
// evidence that this device does not begin with a control block, and on a device that is
// not a store at all there is nothing to walk. The userspace raw reader has always asked
// that question — its message for this is `not a CUBE store (bad magic)` — and the kernel
// did not, which is the whole of this defect: with nothing behind the read, the walking
// reader served *invented* records, a garbage space with a zero-length value and a cursor
// that kept moving, for as long as the machine was left on. A boot pointed at a store it
// could not read hung instead of saying so.
//
// So the fallback is taken only for something that says it is a packed image: a bare
// packed image begins with the image header's magic. A device with anything else at
// offset 0 is refused here, loudly, which is what a missing or wrong store deserves.
(None, None) => {
let mut head = KVVec::<u8>::with_capacity(8, GFP_KERNEL)?;
read_exact_at(file, 0, IMAGE_MAGIC.len(), &mut head, &mut cache.scratch)?;
if head.as_slice() != IMAGE_MAGIC {
return Err(EINVAL);
}
return Ok(None);
}
} as u64;
let mut opened = Addressed {
@@ -3279,16 +3330,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.
@@ -3297,6 +3348,157 @@ unsafe fn v3_get(
copy_out(value.as_slice(), buf, len)
}
// ── the arrangement vocabulary: one value, several records ─────────────────────────────────────
//
// Three bits say where a record sits in a chain of records that together hold one value:
// `START_RECORD` on the first slice, `CONTINUATION` on every slice that is followed by another,
// `END_RECORD` on the last — so a single-slice value carries `START|END`, which is what a
// self-contained record already is. `cube_core::WordFlags` allocates them,
// `DESIGN-flag-vocabularies.md` §7 is the design, and `crates/cube-store/src/chain.rs` is the
// producer and the reference reader.
//
// **The bits were allocated and unread, so "is anything already using them?" decided whether
// reading them here is safe — and it was measured rather than assumed.** Over this machine's live
// store, `cube-image flag-scan` finds 1 record in class 0x0080 and 137 in 0x0800, and nothing at all
// in 0x0001, 0x0002, 0x0003, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0100 or 0x0200. So no read
// that works today can change meaning because of the code below.
const START_RECORD: u16 = 1 << 2;
const END_RECORD: u16 = 1 << 3;
const CONTINUATION: u16 = 1 << 5;
/// `cube-store-seal`'s flag: this record's value is an *envelope*, not plaintext.
///
/// It is what stops the kernel joining a chain, and the reason is in the format rather than in
/// policy. A sealed slice carries its own header and its own nonce, so N of them concatenated are
/// not one value anybody can open; joining those belongs to whoever holds the key, one envelope at
/// a time, which is what `chain.rs` does.
const SEALED_FLAG: u16 = 0x0800;
/// The most slices one chain may be joined from — the userspace reader's own ceiling, because two
/// readers of one format that disagreed about the bound would disagree about what is valid.
const MAX_CHAIN_SLICES: usize = 1 << 16;
/// The most bytes a joined chain may be: `CUBE_MAX_VALUE`, the largest value a caller can ask for in
/// one call. A longer chain could not be handed over in one piece, so reading on to find its end
/// would be work whose answer nobody can receive.
const MAX_CHAIN_BYTES: usize = 16 * 1024 * 1024;
/// Whether these flags say the record *begins* a chain that continues past it.
fn chain_starts(flags: u16) -> bool {
flags & START_RECORD != 0 && flags & END_RECORD == 0
}
/// Read the chain starting at `(x, y, z)` and hand the caller the joined value.
///
/// The first slice has already been read — this is only called once a read found a chain — so it is
/// not read again to find out, and its length and flags come in as arguments.
///
/// The buffer rule is a single record's rule applied to the whole value: if the joined bytes do not
/// fit, nothing is copied and the length is returned, so the caller sizes up and asks again. The log
/// is consulted per slice, so a chain that is written but not yet folded joins exactly like one that
/// has been folded.
///
/// # Safety
/// `space` must point to 32 readable bytes; `buf` to `len` writable bytes; `out_flags` must be
/// writable.
unsafe fn v3_get_chain(
space: *const u8,
x: u64,
y: u64,
z: u64,
first_len: usize,
first_flags: u16,
buf: *mut u8,
len: usize,
out_flags: *mut u16,
) -> isize {
// Pass one: how long is the value, and does the chain hold together at all? The sizes come from
// the index — and the log — so this pass reads no value bytes, which is what keeps the
// size-then-read pattern as cheap for a chain as it is for a record.
let mut total = first_len;
let mut flags = first_flags;
let mut zz = z;
let mut slices = 1usize;
while flags & END_RECORD == 0 {
if flags & CONTINUATION == 0 {
// It neither continues nor ends. Where the value stops is not knowable, and inventing an
// end would hand the caller bytes nobody wrote.
return -5; // -EIO
}
if slices >= MAX_CHAIN_SLICES {
// A chain with no end is a read with no end: the ceiling is the answer.
return -5; // -EIO
}
zz += 1;
slices += 1;
let image = match Addressed::open() {
Ok(Some(image)) => image,
Ok(None) => return -95, // -EOPNOTSUPP
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.
let (sp, key) = unsafe { coord_key(space, x, y, zz) };
let mut slice_flags = 0u16;
let n = unsafe { v3_get(image, sp, key, core::ptr::null_mut(), 0, &mut slice_flags) };
if n == -2 {
// A hole. A chain with a missing slice is not a shorter value, it is a corrupt one, and
// returning the prefix would hand the caller something that looks smaller rather than
// broken.
return -5; // -EIO
}
if n < 0 {
return n;
}
if slice_flags & (CONTINUATION | END_RECORD) == 0 || slice_flags & START_RECORD != 0 {
// A record that belongs to no chain, or a second beginning: either way the chain this
// read started from is not the chain it claimed to be.
return -5; // -EIO
}
total = match total.checked_add(n as usize) {
Some(t) if t <= MAX_CHAIN_BYTES => t,
_ => return -7, // -E2BIG
};
flags = slice_flags;
}
// The flags describe the bytes the caller is handed, not the slice they came from: this is now a
// whole self-contained value, so it answers `START|END`. That is also what lets the userspace
// reader work unchanged against a joining store — it stops at `END_RECORD`, which is what it was
// already written to do.
unsafe { *out_flags = (first_flags & !CONTINUATION) | END_RECORD };
if total > len {
return total as isize;
}
// Pass two: copy, in z order. Both passes walk the same coordinates, so each slice is copied into
// the place the first pass measured for it.
let mut off = 0usize;
let mut zz = z;
for _ in 0..slices {
let image = match Addressed::open() {
Ok(Some(image)) => image,
Ok(None) => return -95, // -EOPNOTSUPP
Err(e) => return e.to_errno() as isize,
};
let (sp, key) = unsafe { coord_key(space, x, y, zz) };
let mut slice_flags = 0u16;
let n = unsafe {
v3_get(image, sp, key, unsafe { buf.add(off) }, len - off, &mut slice_flags)
};
if n < 0 {
return n;
}
off += n as usize;
zz += 1;
}
// `off` and `total` come from the same coordinates read twice, so they must agree. A
// disagreement means the store moved under the read, and a length nobody measured is the wrong
// thing to return quietly.
if off != total {
return -5; // -EIO
}
total as isize
}
/// `CUBE_OP_SPACES` against a v3 image: the space table, and whatever only the log writes.
///
/// # Safety
@@ -3393,7 +3595,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();
@@ -3448,11 +3650,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,
@@ -3487,7 +3689,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
@@ -3513,7 +3715,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()) {
@@ -3576,7 +3778,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();
@@ -3599,7 +3801,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`.
@@ -3632,7 +3834,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
@@ -3687,7 +3889,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()) {
@@ -3945,7 +4147,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();
@@ -3967,7 +4169,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.
@@ -4019,7 +4221,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()
@@ -4030,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,
}
}
// A space the log also writes into is the same space, not a second one.
@@ -4040,7 +4242,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;
@@ -4051,7 +4253,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,
}
}
@@ -4068,6 +4270,11 @@ unsafe fn v3_flag_scan_every(
/// only if it says nothing about the coordinate does the image answer. Nothing is copied out of
/// either: the value the caller gets is a slice of bytes already in memory.
///
/// A value written as a *chain* — several records at consecutive `z`, marked with the arrangement
/// bits — comes back **joined**: a `get` on the first slice returns every slice's bytes in `z` order,
/// because the kernel is the half that can walk a chain without the caller reading each slice itself.
/// What joins and what deliberately does not is stated at the arrangement vocabulary below.
///
/// # Safety
/// `space` must point to 32 readable bytes; `buf` to `len` writable bytes.
#[unsafe(no_mangle)]
@@ -4088,13 +4295,34 @@ pub unsafe extern "C" fn cubelinux_kernel_get(
// 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, out_flags) },
Ok(Some(image)) => {
let mut flags = 0u16;
let got = unsafe { v3_get(image, sp, key, buf, len, &mut flags) };
// A record that begins a chain and does not end it holds one slice of a larger value, and
// the kernel is the half that can join it: a caller would otherwise read every slice
// itself just to learn how long the value is.
//
// Two cases do not join here, and both are said in the answer rather than approximated. A
// *sealed* slice is an envelope with its own header and nonce, so joining those is not
// joining a value — that belongs to whoever holds the key. A packed image has no index to
// address slices by. In both, the caller gets the slice with the record's own flags, and
// `CONTINUATION` without `END_RECORD` says plainly "this is a slice, not the whole
// value": told, not misled.
if got >= 0 && chain_starts(flags) && (flags & SEALED_FLAG) == 0 {
return unsafe {
v3_get_chain(space, x, y, z, got as usize, flags, buf, len, out_flags)
};
}
// SAFETY: the caller guarantees a writable u16.
unsafe { *out_flags = flags };
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) {
@@ -4209,6 +4437,9 @@ pub unsafe extern "C" fn cubelinux_kernel_enum(
out_len: *mut u64,
out_cursor: *mut u64,
) -> i32 {
if let Err(e) = store_readable() {
return e.to_errno() as 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) };
@@ -4218,12 +4449,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.
@@ -4323,12 +4554,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,
@@ -4434,12 +4665,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"
@@ -4472,17 +4703,47 @@ 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) }
}
/// 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
/// zero-length value, a cursor that advanced on its own, forever — and a boot that hung instead of
/// saying what was wrong. Probing every branch of `Addressed::open()` showed why the earlier guesses
/// were wrong: it is entered, the file opens, and execution never gets past the control-block read, so
/// neither the packed fallback nor the guard written for it was ever on that path. Whatever answered
/// 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)?;
// The scratch is what `read_exact_at` reads *into*, and it asks for `min(left, scratch.len())`
// bytes — so a scratch with capacity but no length asks for zero bytes, gets zero, and the
// helper correctly reports a short read for every store, good or bad. Length, not capacity.
let mut scratch = KVVec::<u8>::with_capacity(4096, GFP_KERNEL)?;
scratch.resize(4096, 0, GFP_KERNEL)?;
read_exact_at(file, 0, IMAGE_MAGIC.len(), &mut head, &mut scratch)?;
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
@@ -4493,14 +4754,17 @@ pub unsafe extern "C" fn cubelinux_kernel_flag_scan(
/// `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() {
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
@@ -4587,7 +4851,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,
}
}
@@ -4598,11 +4862,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,
}
}