Commit Graph
10 Commits
Author SHA1 Message Date
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 1081b5f1e2 cube(2): CUBE_OP_GET answers with the record's class
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.
2026-09-22 01:34:23 -04:00
surface-camera-build 9586e114e7 cube(2): CUBE_OP_PUT takes a class mask
The mask is the writer's and is stamped once, at the moment the record's
class is known for certain; every later reader is spared re-deriving it. It
means nothing to this side — which bits are which class is a vocabulary's
business, and a kernel that interpreted one would be inventing a vocabulary.

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.
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.

The field is appended, so sizeof(cube_args) grows from 80 to 88 — still
distinct from the other three argument blocks, which is what the size-first
dispatch depends on.
2026-09-22 00:37:09 -04:00
surface-camera-build 71552fc157 cube(2): CUBE_OP_FLAG_SCAN takes a scope — one space, or every space
"Every error anywhere" and "every error here" are different questions, and
a space is a hard partition, so the scope is a field rather than a widening.
It is not a reserved space id because there is no such id to reserve: every
32-byte value is a legitimate space, root 0x00 and edge 0xFF…FF among them,
so a sentinel would be a space somebody could name. The field occupies what
was padding, which keeps sizeof unchanged — and that matters, because size is
what says which argument block arrived and cube_args is exactly eight bytes
wider.

An every-space answer carries each frame's space, and that is not decoration:
a walk's frame omits the space on the grounds that the caller named it, and
this caller named none. A coordinate is meaningless without its space, so an
answer that left it out would be unusable rather than merely terse. The space
leads because the (space, key) pair it forms is the order records are stored
in and returned in — so an every-space scan answers in exactly the order a
checkpoint writes.

The walk visits the space table's order and puts a space only the log writes
into in its place in that same order, which is the one thing a plain walk of
the table would miss entirely. A scope or mode this build does not know is
refused rather than defaulted: silently answering a narrower question than the
one asked is as quiet a way to be wrong as answering a wider one.
2026-09-22 00:31:56 -04:00
surface-camera-build 41e437c07a 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.
2026-09-22 00:08:19 -04:00
surface-camera-build 88ce1bf2bf cubelinux: CUBE_OP_RANGE — a region walk that seeks on the box's key span
The seventh operation: `cube(2)` gains CUBE_OP_RANGE, a bounded walk of one
space's records that lie in a box. Its argument block is its own (`cube_range_args`,
versioned by `size` like the walk's), and the box travels as its six numbers for
the reason a coordinate does — the key it has to become is the driver's business.

The operation rests on the span that the shared file already owns. `key_span(lo, hi)`
bounds every key in the box because the interleave is monotone on each axis, so a
v3 image is **sought**: the fixed-stride index is binary-searched for the span's foot
(`Addressed::lower_bound`) and read forward to its head, merging the log's edits
exactly as a walk does. A packed v1/v2 image has no index to search, so its space is
walked with the same span used only to stop early — and the contract is the same
either way, so a caller is not told which path it got.

The trap that shaped the code, and the reason it is written the way it is: **the span
is a bound, not the set.** Keys of points outside the box fall inside it (Z-order
amplification), so every candidate is decoded and tested against the box before it
is returned — which is what `morton_decode`, the interleave's inverse, is for, now in
the shared file with the same kind of hand-pinned tests the interleave has. And the
cursor counts the records *in the box*, not the records of the space, because those
are the records the walk returns.

The over-coverage — records examined versus records returned — is the number this
operation is meant to publish, and it is not wired to the caller yet: the cost gate
measures it by comparing against the userspace store, which counts the same thing.
2026-09-21 19:44:48 -04:00
surface-camera-build 087c6b0e60 cubelinux: the kernel records its own boot
The second half of "the OS stores itself". The store was already the kernel's; what was missing
was the kernel *saying* something of its own rather than a client doing it. At its first write of
a boot the driver now appends one record describing the boot it is having — its own version
banner, the wall-clock time, and the store device it resolved — to a reserved space, through the
same append path every other mutation uses.

Three things had to be decided, and each had a wrong answer that looked right:

WHERE IT HOOKS. "At store init" does not exist and must not be invented: there is no init-time
open, deliberately, so there is no ordering to get wrong against the block driver that provides
the device. An __initcall appending a record would reintroduce exactly that ordering problem.
The moment is the FIRST WRITE, which needs no ordering at all and is the semantically right one —
the kernel records itself when it becomes the writer. A boot in which the kernel only reads
writes no record, which is honest rather than a gap.

WHICH SPACE. 0xFD was the first choice, following the convention that a reserved space is one
repeated byte. 0xFD is the OS KEYSTORE — the space the kill switch exists to destroy — and
writing boot records into it would have been a serious bug. Only the userspace name table catches
this (format_space in crates/cube-command) because the kernel keeps no table of space names, so
the table is now written down where the constant is: 0x00 root, 0xFF edges, 0xFE portal,
0xFD keystore, 0xFC boot. The record lives at (0,0,0) in 0xFC: one record, the current boot.

WHAT IT SAYS. boot=<epoch seconds> device=<resolved path> kernel=<the version banner>, banner
last and unquoted so everything after the final = is the kernel's own words rather than a field
this code parsed. Raw epoch seconds rather than a date: rendering a calendar date in the kernel
is date arithmetic, and a caller with a clock can do it without a kernel bug being the reason a
timestamp is wrong. The banner comes from linux_banner and the time from ktime_get_real_ts64.

WHY IT IS OFF BY DEFAULT. Not caution, but an invariant. The gates' method is that the store the
kernel produces is comparable, byte for byte, with the store userspace produces from the same
mutations; a record the kernel injects that the caller never asked for would turn those
comparisons into non-comparisons. So it is cube_boot_record=1 on the kernel command line, parsed
in C beside cube_store= for the reason that parameter is in C (this kernel's Rust cannot express
a string parameter), and kernel/verify-boot-record.sh is the gate that turns it on.

A failure to record is logged and never propagated: the record is worth having and is not a
precondition for the caller's write. It is attempted once per boot rather than retried per write,
because a store that will not take it will not take it later, and one warning is information
where a stream of them is noise.
2026-09-21 18:05:10 -04:00
surface-camera-build b9a7b8d8f3 cubelinux: cube(2) walks the store — CUBE_OP_ENUM and CUBE_OP_SPACES
Enumeration was the one operation the interface did not have, and the one a
capability interface owes an explanation for: a listing is the opposite of
"knowing a coordinate is the authorisation to use it". So the walk is bounded
and explicit. A caller names the space, holds a cursor, and gets as many whole
records as fit in the buffer it offered, packed `key(24) | value_len(u32) | value`
in the store's own order. SPACES walks the distinct spaces that hold a record,
one per call; range is that walk with the region as a filter, applied by the
caller rather than by a second operation in the kernel.

Its own argument block, versioned by its own size: SYSCALL_DEFINE2 peeks `size`
and routes — 80 bytes is the coordinate block, 64 is this one. An interface that
cannot grow has to be replaced, and this one grows by being given a new block.

The end of a walk is the cursor alone. A batch holds as many whole records as
fit, so it is FULL only when a record lands on the boundary and most batches
come back short; a caller that reads "the buffer was not filled" as "the space
is exhausted" truncates its listing to the first batch and cannot tell. ENUM
answers a finished walk with no records and the cursor unmoved; SPACES answers
it with -ENOENT, because the space after the last one is not there. That rule is
in the uapi header because it is a contract detail, not an implementation one.

The kernel caches no merged index: each call walks the records in order, asks
the log — small, and the only thing that can override one — what the winner is,
and skips what the cursor has already covered. O(records) a call, tens of
milliseconds for the live store, which is worth more than a cache every write
would have to invalidate.
2026-09-20 23:15:56 -04:00
CUBELinux build 3ee59dafff CUBELinux.0.6: cube(2) — the coordinate interface
The write path was proven but unreachable: its operations lived behind a device
node. This is the interface the decision chose (DESIGN-cube-interface.md) — one
syscall number, an opcode, and a versioned argument block, with operations that
are the verbs the command language already defines: put, get, del, sync.

  long cube(unsigned int op, struct cube_args __user *args)

`size` comes first and is checked, because syscall numbers are permanent and an
interface that cannot grow would have to be replaced. A coordinate is the space and
its three axes; nothing here resolves a name and nothing enumerates.

Split deliberately: the entry point, the user copies and the argument validation are
in C (cube_syscall.c) because `SYSCALL_DEFINE*` is a C macro this kernel has no Rust
equivalent for; everything that touches the store's bytes is in Rust, which passes
the coordinate to the format code as its parts so that Morton encoding stays in the
one module that must get it exactly right. A read that does not fit returns the size
it needs rather than truncating — a short read would be worse than an error.

Number 548: the x86_64 table says numbers 548 and above are available for
non-x32 use.

Gate (kernel/verify-syscall.sh): a static client in the initramfs does four writes
(including an empty value and a second space), reads one back *through the same
interface*, and folds with `sync`. Three different failures are separated — the
calls failing (a broken ABI), a read not returning what a write stored (a wrong key
encoding or index), and the folded image differing (a wrong format, order or merge).

  put 7,0,0 ok (21 bytes)
  put 8,0,0 ok (0 bytes)
  put 9,0,0 ok (28 bytes)
  put 1,2,3 ok (13 bytes)
  get 7,0,0 21 bytes: the kernel wrote this
  sync ok

and the image left on the device is byte-identical to the one userspace writes from
the same mutations, with the log empty. The interface's store is the same store.

All nine gates pass on 0.6.
2026-09-18 22:18:18 -04:00