cubelinux-0.6
38
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e0218ec96b |
store: one fsync per append, not two — and torn-tail is the gate that decided it
A durable write was 8.2 ms and it was two fsyncs to ext4: the log entry, then the control
block that counts it. Measured on the store's own filesystem, one `pwrite`+`fsync` is
4.321 ms and append's shape of two is 8.630 ms, against 8.202 ms for the whole kernel put —
so the write path was fsyncs and almost nothing else, and one of them was flushing data the
next one covers.
The order stays. A mutation is still written as entry-then-count, because that order is what
makes a crash lose an unacknowledged mutation rather than count one that is not there. What
goes is the *second durability boundary*: the entry is `kernel_write`-ordered before the
control write, and the control write's `fsync` flushes what precedes it in the same file.
What that 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
this store already has a gate for one:
verify-torn-tail PASS "userspace on the same bytes, and appended over the tear
rather than after it"
verify-syscall PASS the store the kernel writes is byte-identical to userspace's
verify-file-store PASS twelve writes to a store that is a FILE, all accounted for
So the change was made against the gate that tests the failure mode rather than against an
argument about ext4's journal, and it is reversible on its own: revert this commit, rebuild,
and the two-fsync order returns. Half of every write, for a property the format already
handles.
|
||
|
|
b3dc55392e |
read: the log window is cached with the table — the last device read a read made for nothing
The log is the one thing in the store that changes without a fold, which is why it was the one thing still read on every call even after the validation, header, layout and space table were held across calls. But that is the same argument in reverse: the cache is keyed on the control block's generation, and a write — the only thing that changes the log — raises it. So an unchanged generation IS a log that cannot have changed, and a hit can serve it from memory without asking the device at all. Measured, this one is neutral in the gate: get 0.038 -> 0.044 ms, inside the run-to-run noise of this bench. That is expected rather than disappointing — the gate's store carries a log of a few hundred bytes, so a read of it costs about what the copy from the cache does. The number this change is for is on the box, whose log is 143,317 bytes: that read and its allocation were ~15-30 us of every coordinate read there, and the box is where it will show. With this the objective's list is closed: the control-block validation, the header, the space table and the log window are all held across calls; the space-table search allocates nothing and the index search allocates once per search rather than once per probe; the unfolded log is bounded by LOG_FOLD_BYTES so per-call work has a ceiling; and the gate fails on a worst case and a ceiling rather than on a ratio. |
||
|
|
b8a7f615ff |
read: validate the store in 96 bytes, and hold the layout with the table
The control block is 4096 bytes, and the question every read asks of it is one number — has this store changed since I last looked. That number, and the fields beside it, are in the first `CTL_SUMMED + 4` bytes of each copy. So a call that finds the store unmoved now reads two heads and nothing else; the header, the space table, the layout and the log's extent all come from the cache. The full 4096-byte read happens only when the store has actually moved. Both heads are read, and that is the whole safety of it: a write raises one copy's generation and leaves the other at the old one, so reading a single copy and finding it unchanged would call a moved store unmoved. The pair is what makes the answer true. Measured on #84, in the guest, against #83: get 0.077 -> 0.038 ms in the 20,000-record store (and 0.052 in the 500-record one) miss 0.074 -> 0.044 ms spaces 0.374 -> 0.173 ms worst case: read 0.763 ms (ceiling 5), write 6.762 ms (ceiling 50) A read is now about twice as fast, and it is finally faster in the LARGER store than in the small one — which the flatness before could not show. That is the diagnosis paying off: the per-call cost of a read was setup rather than device reads all along, and the largest single piece of that setup was reading 4 KB to compare 8 bytes. Also derives `Copy` for `HeaderV3` (six plain numbers; a cached header has to be handed out by value) and drops the search buffer `space_entry` no longer needs, now that the table it searches is in memory. |
||
|
|
1f7b9fe0de |
read: hold the space table across calls, keyed on the generation
Every coordinate read binary-searches the space table for its space's first record, and every batch of a listing reads a row of it — from the device, per probe, for data that changes only when the store is folded. It is now held in the driver and keyed on the **control block's generation**, which a fold raises along with everything else it writes, plus the image offset, since a fold flips the slot. Nothing has to invalidate it by hand: a writer that bumps the generation invalidates it, including a writer this driver never sees. A stale table cannot outlive the image it describes. Honest about what it bought: nothing measurable in the gate, which is the interesting part. The gate's store carries about a dozen spaces, so the search it replaces was three or four probes, and copying a few hundred bytes of table costs about what those probes did — get 0.078 -> 0.077 ms, spaces flat within the run-to-run noise of this bench (which is ~20%). What changes is the SHAPE: the lookup no longer scales with the number of spaces, and the box has 21 and grows. That is worth having and it is not the same thing as a measured win, so it is not claimed as one. What is still read per call, and is the larger constant: the control block (needed — it is the validation), the header, the log window, and the allocations for all three. Those are what the borrow-based version of this cache is for, and they are the next piece. |
||
|
|
aab316a9a1 |
read: take the log window instead of copying it, and name the boot class bit
A coordinate read cost 0.082 ms in a 500-record store and 0.081 ms in a 20,000-record one — flat to a microsecond while the index search does nine probe reads against fifteen. So the device reads are nearly free and the per-call cost is SETUP, and the clearest piece of setup was a copy of the log window into a second buffer on every read. `v3_get` copied it because what the log says borrows it while reading the image needs `&mut image`; that borrow is one field, so moving the field out settles it for nothing. Measured on #82: get 0.082 -> 0.078 ms, spaces 0.339 -> 0.267 ms. That is small in the GUEST, and the guest cannot show the real number: its store carries a log of a few hundred bytes, while the box's log is 132,679 bytes — so the copy this removes costs ~130 KB of memcpy and a ~130 KB allocation per read there, and only the box can measure it. Also names the store's own class bit. `BOOT_FLAGS` was a bare `1 << 7` and read as `TYPE_MASK == 10` ("data type: code") to anything applying `WordFlags` — a latent collision, not a live one, and the resolution is a declaration rather than a move: `WordFlags` is a different field and its sixteen bits are all allocated, so there is no bit to borrow, and the space axis already makes a boot record findable (reserved space 0xFC) without a class bit at all. What it needed was a vocabulary that claims it, which this comment now does. The one bit the two fields share is shared on purpose and by name: 0x0800, `SEALED_FLAG` in `cube-store-seal`, which `WordFlags` calls `ENCRYPTED` — same meaning in both places, which is the pattern rather than the exception. |
||
|
|
e968b3964e |
store: a write reads the log, not the whole device — and the log is now bounded
A put cost 60-131 ms on the box while a read cost 1-2 ms, and the reason was one call: every write path reached `append` through `device_and_layout()` → `read_image()`, which reads the WHOLE DEVICE — 100,663,296 bytes here — into a KVVec, to append ~70 bytes. `append` itself never looks at the image: it reads the log region and the control block. The read paths had been taught to read only where the image lies; the write paths never were. `AppendSource` now names what an append reads: `whole_image` (kept for the two callers that genuinely need it — a bare store, whose log runs to the end of the file, and the v1→v4 migration, which folds) or `log_head` (a device's control block plus the log's first `WAL_HEADER_LEN` bytes). `write_view()` reads exactly that, `append_mutation()` is the single entry point all four writers use — put, del, the boot record, and the misc device's write_iter — and the bare-image fallback is decided in one place instead of four. The fold still reads the whole store, because a fold rewrites it; that is the honest tail, and it is why a fold belongs on a timer rather than on the path a caller waits behind. The log is bounded by `LOG_FOLD_BYTES`, because it is on the READ path: `Addressed::open` reads `WAL_HEADER_LEN + log_used` bytes on every call, so an unfolded log is a tax on every read, not a bill paid once at the fold. The live store's control block reports log capacity 50,335,744 bytes — if it ever filled, every coordinate read would read ~48 MB before answering anything. A write that finds the log over the line folds first, through `fold_for_headroom`, and then appends to a fresh log; guarded so one append folds at most once, because a fold that did not shrink the log must not loop. The trade is named in the code: the write that crosses the line pays a bounded, rare fold instead of every reader paying an ever-larger log. The timer's comment — "the log grows at roughly a megabyte a day against a 50 MB region, and a fold rewrites the whole image, so folding more often would buy nothing and cost I/O" — is a data workload's arithmetic, where the log is a recovery artefact and nobody reads it. `space_entry`, `find` and `lower_bound` each allocated a fresh KVVec inside their binary-search loop; one buffer per search now. The boot record's one attempt becomes a bounded retry: `ensure_boot_record` claimed the boot with a swap BEFORE it tried, so a single transient failure cost the boot its record silently — which is exactly what happened on the box, where the first client of the boot could not open the store for writing. It now claims one of `BOOT_RECORD_MAX_ATTEMPTS` with a compare-exchange, and parks the count at the ceiling on success. Release moves to 6.19.3-cubelinux0.7: verify-box-preflight.sh now refuses a same-release reinstall, because the default entry boots the release being replaced. Gates, on #81: verify-enum-cost PASS a write is 1.45 ms mean / 7.46 ms worst (new ceilings: 50 ms write, 5 ms read), and a write does not grow with the store verify-file-store PASS 12 writes to a store that is a FILE, all accounted for verify-syscall PASS the store the kernel writes is byte-identical to userspace's |
||
|
|
daa6289e2d |
cube(2): the walk skips the records the cursor already returned
`v3_enum` built its batch with `seen: cursor`, and `Batch::offer` increments
before comparing (`seen += 1; if seen <= cursor { skip }`), so nothing was ever
skipped: every call re-served the space from its first record while `out_cursor`
advanced by the records returned. The contract's only end signal is a cursor that
stops moving, so the walk never ended — a listing looped inside the front-end
until the kernel's OOM killer took that process, twice, at ~6.4 GiB anon.
Every other Batch site already starts `seen` at 0, and two of them carry comments
describing exactly this trap; this one was the lone outlier. It only shows on an
**addressed** image whose space has log edits, because the merge path re-walks
from the space's start and has nothing but `seen` to skip with — the no-edits path
skips by index arithmetic (`first + cursor`) and was already right. That is why no
gate saw it: the enumeration gate's store is packed.
So each path now skips exactly once — the merge path through `offer`'s `seen`
starting at 0, the no-edits path through its arithmetic with the batch told to
skip nothing. Doing both would skip twice and lose records. The arithmetic cursor
is added saturating, because a wrapped sum would land back near the first record
and re-serve the walk: the same endless listing this exists to avoid.
Verified: a copy of the box's own addressed store walks in QEMU on this kernel to
`12 space(s), 69665 record(s)` — 69,641 image records plus 24 unfolded log edits —
and terminates, where before every batch repeated the space from its start.
|
||
|
|
e53ae04033 |
cubelinux: hold the store lock across the whole op, not inside append
Initialising STORE_FILE stopped the oops and exposed what it had been hiding: twelve concurrent puts all reported `ok` and one write survived. The lock guarded the file handle, not the store — it is taken, the handle is fetched or cached, and released before the caller does anything with it. Sharing a struct file * is safe, so that is all the handle needs, and it is not enough for the store. Widening it inside append is also not enough, and this commit exists because the first attempt did exactly that and changed nothing. append receives the layout as an argument, and the caller read it from device_and_layout() *before* calling: twelve writers each read log_used = 0, then queued on a lock inside append, then each appended at the same offset against the layout they had already read. Measured with the lock in append: one 68-byte entry, twelve `ok`s. The lock has to be held from before the read. So STORE_OP is taken by the four write ops — put, del, sync and the device write — and everything under them runs with it held: ensure_boot_record, device_and_layout, append, fold_now. None of those may take it again; a kernel mutex is not reentrant, and append folds and then calls itself, while fold_now is reachable both from inside append and on its own. That is why the lock sits at the ops rather than in the writers. verify-file-store.sh MODE=race, twelve concurrent puts: 816 bytes of log = 12 x 68, and control generation 13 = 1 + 12, where the previous kernel produced 68 and 2. Regression: MODE=seq still exact; verify-boot-record passes (it is the path this changes most, since ensure_boot_record now runs under the lock); verify-kernel-append passes, four acknowledged writes surviving a SIGKILL with no shutdown. |
||
|
|
7938966d6a |
cubelinux: initialise STORE_FILE, the mutex that was never initialised
STORE_FILE is declared `unsafe(uninit) static ... Mutex<Option<StoreFile>> = None` and nothing ever called STORE_FILE.init(). Its sibling SPACE_STARTS is initialised explicitly in CubeStoreModule::init; this one was simply missed. The failure mode is why it hid. A zeroed mutex satisfies the uncontended fast path — the count reads 0, which means "unlocked" — so one writer at a time works and nothing looks wrong. The first CONTENDED lock takes __mutex_lock_slowpath, which splices the task into the mutex's wait list; that list is uninitialised, so its head is NULL and the splice stores through it. A write to address 0 in kernel mode, after which the task returns with interrupts disabled and preemption held — a machine that cannot panic, log, or recover. Found by verify-file-store.sh MODE=race: twelve concurrent puts to a store that is a file on the root filesystem oopsed in cubelinux_store::store_file, while twelve sequential ones passed with exact log accounting (816 bytes of log, generation 13). The same signature is on the box, where the store had several writers and the box froze with no panic despite panic=30. |
||
|
|
da0a19786e |
cube(2): one frame for every walk — the class mask is in it
CUBE_OP_ENUM and CUBE_OP_RANGE returned `key | value_len | value`, so a caller could walk a store and not learn what any record it walked past *was*. A store could not answer `entries_flagged` from a kernel store at all, and the only way to find out was to open the device and read the format directly — which is the second reader of the format this interface exists to make unnecessary. Both walks now return the flag scan's frame: `key(24) | flags(2) | value_len | value`, with the space ahead of it when the scope is every space. One frame, one packer, one `Batch::offer`; the separate `offer_flagged` and `pack_record` are gone, and so is the reason for them to disagree. The class comes from wherever the value did: an addressed image's index entry (the binary search already read it and used to throw it away), a log entry's mask, or zero for a packed v1/v2 record, which has no field to carry one. The merge in `SpaceWalker` hands it back alongside the key and the value for the same reason — a record the log supplied carries the class its writer stamped, and dropping it there is what left a listing unable to say what it was listing. |
||
|
|
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. |
||
|
|
72e72fe7a8 |
cubelinux: an addressed image is readable without a log
merged_digest answered "no log" before it looked at the layout, so a bare v3/v4 image fell through to the packed walk — which starts at the v2 header length and reads index entries as record frames. A bare addressed image with no log beside it therefore read as a one-record store with an empty value. An addressed image is a complete store on its own: its index holds every record and says where each one is, so a log is an addition rather than a requirement. A packed image is not, which is why "no log" still means what it meant for v1/v2. Found by verify-image-read the moment userspace started writing v4: while every userspace image was v2 the fallback happened to be the right walk. The kernel's own v4 images always had a log header beside them, so no other gate could have caught it. |
||
|
|
29ae3b53f1 |
cube_format: a geometry encodes the version it is, not a constant
V3::encode wrote a hardcoded VERSION_V3. It had no callers, so the mistake cost nothing — and then userspace's v4 writer became the first caller, and a v4 geometry would have been published under a v3 header: every reader walking a 42-byte index 40 bytes at a time, finding a store that is silently wrong. One wrong byte, found before the first v4 image was written rather than after. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
0267831b18 |
cube: fix v2 log framing — never write a v2 entry under a v1 header
The v4 migration made the kernel write v2 log entries (flags field) but append only wrote a log header when the region had no magic. A store formatted by userspace already carries a v1 header, so the kernel appended v2 entries under it and every reader framed them as v1: the length landed on the flags field, the walk stopped at the first entry, and get/fold/boot saw an empty log. Now append insists the header matches what it writes: a v1 log that still holds entries is folded into the image first (the actual v4 migration), then the entry lands in a fresh v2 log; an empty v1 log is upgraded in place. The fold is factored into fold_now, shared by append and sync. |
||
|
|
a111d7e8b2 |
cubelinux: v4 — a 16-bit class mask in the index and the log entry
The first half of the flag substrate (DESIGN-flag-vocabularies.md): the shared
format file now defines VERSION_V4, whose index entry is `key | flags(u16) |
value_off | value_len`, and WAL version 2, whose entry carries the same mask
before its length. The mask is a raw u16 — its bits are a vocabulary's business,
never the format's.
Backward compatible, and pinned as such: a v3 index entry and a v1 log entry read
as a zero mask ("no class"), which a scan treats as matching nothing, so a store
folded before the flag existed degrades to "unclassified" rather than "matches
everything". `V3::decode` accepts both versions and `index_stride()` names the
one that differs; `wal_entry` keys its stride off the log's own version byte.
The readers and writers that actually move bytes (the driver's serialize and
append, and `cube-store-raw`) are separate and are the next commit; this is the
shared definition and the arithmetic a reader derives from it.
|
||
|
|
2e5021dd83 |
cubelinux: the region walk's cursor actually advances
The gate caught it on its first run: the region walk answered the box correctly five times over — the seek and the membership test were right — but the batch re-served the same five records forever, because the cursor never skipped. The cause is a one-word difference between the two existing walk paths, and it was mine. `v3_enum` positions itself with `at = first + cursor` and so does not need `Batch.seen` to skip; the packed walk has no index to reposition and relies on `seen` instead, which is why it initialises `seen: 0`. My two region paths re-seek to the span's foot every batch — there is no `first + cursor` for a box, because a span holds records that are *not* returned, so the index position of the cursor-th match is not arithmetic — and I had set `seen: cursor`, which disables the skip and re-serves the span from its foot forever. The fix is `seen: 0`, and the gate now shows why the trap matters: an unaligned box [3,3,3]-[10,10,10] answers five records, and the record at (2,7,7) — outside the box but inside the span — is examined and rejected, not returned. |
||
|
|
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. |
||
|
|
72a6bbe173 |
cubelinux: the key and the span move into the shared file, and the driver stops having its own
`morton_encode` existed twice — once in the driver, once (as the curve) in cube-core — and the two
had to agree byte for byte, because one side computes a key to look a record up and the other
computes it to lay an image out. Nothing structural held that agreement: only the gates, which
would notice afterwards. That is the same shape as the cube_format.rs finding recorded in
DESIGN-coordinate-surface.md §7.4 — a shared *file* whose shared half has no callers — so this
starts paying it off where it is cheap and exact.
The interleave now lives in cube_format.rs, which both builds compile, and the driver's copy is a
call to it. It is written as plain `while` loops rather than iterator chains because the kernel
build of this file has no `std` and no `alloc`, which is the constraint the whole arrangement
exists under.
Alongside it, two things the range work needs and neither side had:
key_cmp compare two keys as the 192-bit numbers they are. The key is big-endian, so this
is the comparison a sorted index performs, and it is stated once rather than
assumed at each site.
key_span the key span a box covers, (key(lo), key(hi)) inclusive. This is the bound a seek
needs, and — the point — it needs no decomposition at all: the interleave is
monotone in every axis, so every point in the box has a key between its corners'
keys. A sorted index can be binary-searched for the foot and scanned forward to the
head. The span is a BOUND and not the set: records outside the box can have keys
inside it, which is the classic Z-order amplification and is why a caller tests
membership per candidate. Confusing the bound for the set is close to the mistake
that put "an aligned box is one run" into two doc comments.
aligned_box_is_one_run
the condition, proved in crates/cube-format's tests and pinned there and in
cube-store: a power-of-two-aligned box is exactly one contiguous run of the key iff
max(k) - min(k) <= 1 AND the axes at the top level form a prefix of (x, y, z). A
size that is not a power of two is answered `false` rather than rounded, because a
caller passing one has a bug and "no" is the useful answer.
Verified on build #53: verify-enum (the kernel's walk against userspace's, which is the check that
would catch any change in the key), verify-boot-record, verify-syscall all pass.
|
||
|
|
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. |
||
|
|
78540b5687 |
cubelinux: the fold reads the layout it writes
Found on the box, minutes after the live store was folded for the first time: the second fold answered -EINVAL. `build_merged` parsed the packed layout — `space | key | len | value`, repeated — so it could read a store that had never been folded and nothing else. The first fold reads packed and writes addressed; every fold after that reads addressed, which is what a store does for the rest of its life. As written, a store could be folded exactly once and then never again — the log would grow until it filled. No gate folded twice, which is why it got this far: verify-kernel-checkpoint.sh, verify-frontend.sh and verify-enum-cost.sh each folded a packed store once. The guest's bench folds twice now, and verify-enum-cost.sh insists on the second one — "a store can be folded once and then never again, which is not a layout" — so the case is covered rather than remembered. The v3 branch reads through the shared format's own arithmetic: the space table gives each space's index range, the index gives each key and the value's place, and the log is applied over the result exactly as before. The packed path is untouched. Verified: verify-enum-cost.sh (which now folds twice and still measures 1.0x for a 40x store) and verify-kernel-checkpoint.sh (the image a fold writes holds exactly what userspace holds). |
||
|
|
d569df710d |
cubelinux: the store's format is one file, and the kernel and userspace both include it
Two build systems cannot share a crate: the kernel's Rust build compiles what is in its own module
tree, and a cargo crate is not that. So they share a *file* — `drivers/cube/cube_format.rs`, which
the driver declares with `mod` and `crates/cube-format` includes by path. There is no copy to drift
from, which is the only arrangement that cannot go stale. What happened when v3 landed and userspace
did not is the argument: an hour of `unsupported-version`, one gate red, and two implementations of
one format each believing itself.
The file holds what both sides must agree about, and nothing else — pure functions over slices, no
allocation, no I/O, no logging:
* the constants every reader and writer derives its arithmetic from,
* the header, in all three versions, with refusal rather than guessing: a reader that invents an
extent can read somebody else's bytes,
* the v3 space table and index, and the three equalities a reader computes its addresses from,
* the packed record and the log entry, the two framings a fold and a walk have to parse alike,
* CRC-32, FNV-1a, and the digest line both sides print.
The driver's copies of the constants are now aliases of the shared ones, and the two functions that
*validate* a header — `parse_header` and the v3 geometry — delegate to it, because validation is
where a second description gets believed. The readers stay where they are: this driver streams from
a file with its own buffers while userspace already holds the whole image, and that difference is
real rather than duplicated.
Nothing changed in behaviour, which is the point: verify-enum.sh, verify-enum-cost.sh,
verify-frontend.sh, verify-kernel-checkpoint.sh and verify-kernel-append.sh all pass, and the shared
crate's own tests include a kernel-written image — ten records, three spaces, 110 value bytes — read
through its arithmetic.
|
||
|
|
1db3abce93 |
cubelinux: v3 — the image carries the addresses, so a coordinate is a place
A packed list (`space | key | len | value`, repeated) cannot answer "where is this coordinate":
record N's offset is the sum of every record before it. The key gives ORDER, and order is not an
address, so no coordinate could be turned into a place and not even a binary search was possible —
the middle record's offset is just as unknowable. Every lookup walked, every batch of a listing
walked again, and a read of one record cost as much as the store (measured: 110 ms per get, 119 ms
per `spaces`, 1,206 ms for a 6,127-record listing).
v3 puts the addresses in the image:
[header 46] magic, version, curve, extents, counts
[space table: space_count x 48] space | first index | records
[index: record_count x 40] key | value offset | value length
[values: packed, in index order]
A lookup is now a binary search over arithmetic addresses (`index_off + i * 40`); a listing starts
at its space's first index entry and streams, reading a span of values per batch; asking which
spaces exist is the space table; a spatial range is a contiguous run of index entries. An index
entry is 40 bytes against the packed frame's 64, so the image also gets smaller.
Measured by `verify-enum-cost.sh` between a 500-record store and a 20,000-record one:
listing the same 10-record space: 3.22 ms -> 3.27 ms (1.0x, store grew 40x)
asking which spaces exist: 3.25 ms -> 3.08 ms (0.9x)
reading a coordinate that is there: 1.07 ms -> 1.25 ms (1.2x)
reading one that is not: 1.19 ms -> 1.22 ms (1.0x)
Nothing scales with the store any more, which is the premise: knowing a coordinate is what lets you
reach it.
Two bugs found on the way, both of the kind only a real image shows. The index was written with a
4-byte value length (`Entry` carries it in a u32) while the header's arithmetic and both readers
assumed 8 — every entry four bytes short, the last of them overlapping the values. And the digest's
`bytes` figure added the layout's own header length, so two layouts of one store digested
differently; it is now the records' logical size, which is the same number either way.
Verified: verify-kernel-append.sh, verify-kernel-checkpoint.sh, verify-enum.sh, verify-enum-cost.sh
and verify-frontend.sh all pass, and the workspace tests pass — including a new fixture test in
cube-store-raw that reads a 700-byte image the kernel itself wrote, because a hand-built image
cannot catch a misunderstanding shared by the builder.
|
||
|
|
56665a303e |
cubelinux: read the store where it lies, instead of rebuilding it per call
Every read began by reading the whole device into kernel memory, parsing every
record of every space into a merged pool, and heapsorting the lot — then throwing
all of it away. So a `get` of one record cost as much as listing the store, every
batch of a listing paid it again, and the coordinate bought nothing mechanically:
knowing where a record is did not make reaching it any cheaper. Measured on the
workhorse: one `get` 110 ms, one `spaces` call 119 ms, one 6,127-record listing
1,206 ms.
The records are already in the order the coordinate computes — a checkpoint writes
them by (space, key) — so the kernel now walks them where they lie: zero-copy
values, no pool, no sort, and the log (small, and the only thing that can override
the image) consulted as an overlay. It reads the image and the log window rather
than the whole device, which is four images wide.
* `get` — the log's newest word on the coordinate, else the image walked to it.
* `enum` — one merge pass over two sorted sequences: the space's image records
and its log edits.
* `spaces` — from a table of where each space starts, cached by the control
block's generation. That table is one entry per space, not per record, and a
checkpoint is the only thing that invalidates it.
* `put`/`del`/`sync` — unchanged: a write is a log append, and the fold still
writes the whole sorted store.
Verified: verify-enum.sh (a 3,006-record listing identical to userspace's, record
for record) and verify-frontend.sh (socket -> front-end -> cube(2), including a
listing larger than one batch and the store the front-end leaves being
digest-identical to userspace's) both pass.
Not yet what it should be: listing a 10-record space in a 20,000-record store
still costs about six times what it costs in a 500-record one, so the space table
is not taking effect as intended and the lookup is not yet independent of store
size. That belongs in the image's layout — a coordinate cannot be turned into a
byte offset in a variable-length packed list, because a record's position is the
sum of every value before it — and the fix is a format one, not a caching one.
|
||
|
|
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. |
||
|
|
ed5ff764ba |
cubelinux: the release starts with a digit, because module tooling demands it
The store's sort no longer lives on the kernel stack (heapsort, see the parent commit's gate), and the release is now 6.19.3-cubelinux0.6 instead of CUBELinux.0.6 — depmod/mkinitramfs reject a version whose first character is not numeric, so the literal name cannot be uname -r. The box's own kernel uses the same shape (6.19.3-cube+); this is the same concession. |
||
|
|
e202f960d5 |
cubelinux: the digest's byte count is the store's, not the reader's
'bytes=' reported how much had been read, so the same records disagreed between a file (16,537,532) and a device slot holding them (67,108,864). The gates compare this line, so it has to mean the same thing on both sides: it is now the header plus the records — the length the store actually is. Found by moving the machine's real store into a device store and comparing. |
||
|
|
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. |
||
|
|
2d4ed0fd9f |
CUBELinux.0.5: a torn log entry is overwritten, not buried
The write path's last unproven claim was the one everything else rests on: replay is prefix-trusting. Building the gate for it found that the claim was false as implemented — in *both* implementations, and in the same way. The log walkers advanced their offset past an entry's frame and only then checked the checksum. A corrupted entry therefore never moved the *replay* point (it was discarded, so the store looked right) but it did move the *append* point. The consequences, in order of how bad they are: - the kernel appended **after** the tear, burying the corruption inside a log that then looked well-formed — the exact opposite of the documented rule; - the userspace log truncated to a prefix that still contained the corrupt frame, so every later append started past it and the corruption stayed in the log forever; - and because each append then read back the same wrong prefix, every append wrote to the same offset and overwrote its predecessor — four mutations in, one on disk. The fix is one line of ordering in three places: compute the frame's end, check the length and the checksum, and only then move the offset. An entry that did not validate may not move the point that says where the log ends. Gate (kernel/verify-torn-tail.sh), on a store with an overwrite, an insertion and a deletion whose last entry has been torn by zeroing its tail in place: whole : 272 bytes, 3 records (the delete applied — the contrast) reference : 349 bytes, 4 records (userspace, torn bytes: the delete discarded) kernel : 349 bytes, 4 records appended : 667 bytes, 8 records (the kernel appended over the tear) expected : 667 bytes, 8 records (userspace: torn tail dropped, then the same four) Building the gate also corrected the gate itself: tearing a log by *truncating the device* is not a torn log, it is a smaller device — the capacity shrinks, writes past the end vanish into the page cache with no error anywhere, and the test measures an artifact. A torn write leaves the device the same size and corrupts bytes in place. All seven gates pass on 0.5. |
||
|
|
451754c7ac |
CUBELinux.0.4: the kernel folds its own log
Until now the kernel could append to a log but needed a *userspace* checkpoint to reclaim it — the dependency the write-authority decision was meant to remove. This is the fold. A store device is no longer an image at offset 0. It is: [control copy A][control copy B][slot 0][slot 1][log] with the control block saying which slot is live and how much log is in use, and two copies each carrying a generation and a checksum. Two slots buy atomicity: the folded image is written to the slot nothing is reading and flushed, and only then does the control change — one small write to the copy that is *not* current. At every instant there is either the old image with a log that still describes the mutations since it, or the new image with an empty log. A crash in the middle of the copy leaves the previous store intact, which is the entire reason for two slots. Folding into the only copy of an image is not atomic, so `sync` refuses on a bare image rather than pretending. `sync` is a real operation, not a test hook: a caller that wants the log reclaimed — a shutdown, a snapshot, a handover — is entitled to ask. Gate (kernel/verify-kernel-checkpoint.sh): the kernel writes four mutations and then folds its own log. The image it writes is compared with the one userspace writes from the same mutations byte for byte, not by digest — and it is identical, with the log empty afterwards. Three bugs came out of building this, all caught by the gates rather than by inspection: - the refactor that extracted the merge left the image walk *duplicated* inside the digest, re-adding image records after the log's entries — which gave them a newer sequence number and quietly resurrected records the log had deleted; - a v1 image has no extent, so it has no log either, and the new layout code was demanding both; - an unwritten log region is empty, not broken, and the first append was refusing it. The append gate's harness was also counting a *refused* write as accepted, which is how the second one hid. All six gates pass: three image reads (v1 curated, v1 snapshot at 35,318 records, v2 store), the log replay, the append with its SIGKILL durability test, and the checkpoint compared byte for byte. |
||
|
|
93e2c8c39b |
CUBELinux.0.3: the kernel writes, and is compared while writing
0.2 read the store. This appends to it: a mutation through /dev/cubelinux is written to the log and fsynced *before* the write is accepted, which is the kernel's version of the contract cube_duratest.py proves for the daemon. - the mutation comes in as the argument block the coordinate interface will pass: op(1) | space(32) | x(8) | y(8) | z(8) | len(4) | value[len]. The kernel Morton-encodes the point itself — a key written here has to be the key a userspace reader decodes, and that is a thing the gate would catch if it were merely similar; - the append lands after the log's valid prefix, so a torn tail is overwritten rather than appended to, exactly as the userspace log does it; - a log region that has never been written is zeros, not a log: the header is written first, the way the userspace log creates its file; - the entry's CRC covers space, key, length and value, computed the same way. The write is the byte plane — bytes at a coordinate, no header written beside them — because that is what a differential comparison against userspace's `cell put` can be exact about. The header tier sits above this. Gate (kernel/verify-kernel-append.sh), four mutations including an empty value and a record in a second space: reference : bytes=500 records=6 value_bytes=94 fnv1a64=6b679d39597a62b3 kernel : bytes=500 records=6 value_bytes=94 fnv1a64=6b679d39597a62b3 folded : bytes=500 records=6 value_bytes=94 fnv1a64=6b679d39597a62b3 survived : bytes=500 records=6 value_bytes=94 fnv1a64=6b679d39597a62b3 The third line is the one that matters: userspace *reads the log the kernel wrote*, folds it, and lands on the same store. Without it the first two would only show the kernel agreeing with itself. The fourth is a SIGKILL of the VM with no shutdown and therefore no flush for us. Not yet: the kernel folds nothing itself, so it still depends on a userspace checkpoint to reclaim its log. That is the next step, and it has its own gate. |
||
|
|
63b0a2f26e |
cubelinux: replay the write-ahead log, and be compared while doing it
A store on a device is [image][log]: the image is a checkpoint, the log is the
mutations since, in the framing cube-store/src/wal.rs writes (op | crc32 | space
| key | len | value, prefix-trusting recovery). The kernel now reads both and
reports the store they *describe* — the image with the log applied, in the order
a checkpoint would write it.
- the log's offset is fixed by geometry (the first 4 KiB boundary at or after the
image's declared extent), so no superblock is needed to find it;
- recovery is prefix-trusting, exactly as userspace does it: the first entry that
is short, mis-framed or fails its CRC ends the log;
- merging is by coordinate with the newest write winning, which is what makes an
overwrite an overwrite and a delete a delete;
- the digest is taken over the surviving records in sorted order, and `bytes`
reports the size a checkpoint of this store would produce — so the number can
be compared with the image userspace actually writes, not merely eyeballed.
Gate (kernel/verify-log-replay.sh), on a pair carrying an overwrite, an
insertion and a deletion:
reference : bytes=377 records=4 value_bytes=99 fnv1a64=a38c1db74b00966f
kernel : bytes=377 records=4 value_bytes=99 fnv1a64=a38c1db74b00966f
log entries=6 applied over the image
It failed the first time, which is the point: the coalescing kept the earliest
write of a coordinate rather than the latest, resurrecting records the log had
deleted — six records where the fold produced four. A differential gate is what
turns that class of error into a red line instead of a corrupted store.
The image-only gates still pass unchanged (v1 curated, v1 snapshot 35,318
records, v2 store): with no log to apply the digest is the image's own order, so
v1 readers and every existing tool keep seeing exactly what they saw before.
|
||
|
|
b54ba34dc0 |
cubelinux: read format v2 as well as v1
The store's write path is decided (append a log, fold it into the image at a checkpoint; DESIGN-cubelinux-write-path.md), and that decision forces the log to sit right after the image on the device. v1 had no extent and no count — it walked records until it met zero padding — so a v1 reader would have walked straight into the log's header and parsed it as a record. v2 states the image's byte extent and its record count, and this teaches the kernel reader both: - v1: walk from a 6-byte header until the trailing zeros, as before. - v2: walk from a 22-byte header, stop exactly at the declared count, and never read past the declared extent. A zero frame inside the count is a record, not padding — which is the ambiguity v1 could not resolve. - a v2 header whose extent does not cover the header itself is refused rather than guessed at, and a count that is not met counts as an error instead of quietly returning a shorter list. Gate, unchanged in method: the kernel's digest of /dev/vda must equal cube-image's digest of the same bytes. Passing on all three: v1 curated 11 records fnv1a64=161113085b1573b2 v1 snapshot 35,318 records fnv1a64=5e20f98455387b08 v2 store 4 records fnv1a64=20ecadb5cdc9c994 |
||
|
|
cee3e554d9 |
CUBELinux.0.2: the kernel reads the CUBE store off a block device
The first CUBE code in the kernel, and deliberately only a reader: the write
authority has not moved yet, and PLAN-kernel-cubelinux.md records both that
decision and the hazard that makes the order matter — a kernel writing while a
userspace daemon still holds the same image loses one of the two writers' work,
silently. A reader cannot do that.
- drivers/cube/: a Rust module exposing /dev/cubelinux. Reading it reads the
pinned image from the block device through the kernel's own file layer
(filp_open + kernel_read — the path this kernel version binds for Rust, and
the reason no C helper was needed), parses the records, and returns one line:
digest curve=0 bytes=8400896 records=35318 value_bytes=6139148 fnv1a64=5e20f98455387b08 errors=0
The work happens on read, not at init, so there is no initcall ordering to get
wrong against the block driver that provides the device.
- The format is restated in the kernel (32-byte space, 24-byte key, 8-byte LE
length, value), including the two rules the userspace parser documents: a value
that runs past the buffer is a truncated record, and an all-zero frame ends the
records only when every remaining byte is zero — the rule that keeps a real
record at the origin from being read as padding.
- The digest is the point. A record count alone lets two different images agree;
folding the bytes in means the kernel and userspace are *compared* rather than
assumed to agree. `cube-image digest` prints the same line in the same field
order, and the QEMU gate fails if they differ by a byte.
Gate, on both images:
curated 11 records, 4,096 bytes, fnv1a64=161113085b1573b2 — match
snapshot 35,318 records, 8,400,896 bytes, fnv1a64=5e20f98455387b08 — match
The tree carries CONFIG_CUBELINUX_STORE=y on top of defconfig + RUST; a tree
without it boots and simply has no /dev/cubelinux.
|
||
|
|
e4ae67ea7f |
CUBELinux.0.1: name the release, and build with this box's rustc
The tree is Linux 6.19.3 with the changes this machine's toolchain needs, and nothing else. No CUBE code yet — this is the base the coordinate interface will be built on, so it starts from a known-good bootable kernel. Naming: - VERSION/PATCHLEVEL/SUBLEVEL stay 6.19.3 (visible in `make kernelversion`), while the release string setlocalversion composes is CUBELinux.0.1, so `uname -r` and /lib/modules report this product rather than a Linux point release. The SCM suffix still applies: a dirty tree says so. rustc compatibility (rustc 1.100.0-nightly, clang 19.1.7): - scripts/generate_rust_target.rs emitted `"rustc-abi": "x86-softfloat"`, which this rustc rejects; it is `softfloat` now. - rust/Makefile's cmd_rustc_library did not pass -Zunstable-options, so the custom target spec would not load at all. - the generated bindings declare `strlen` with the kernel target's `c_char` (u8, from -funsigned-char) while rustc expects `*const i8`; the newer suspicious_runtime_symbol_definitions lint fires on that and -D warnings makes it fatal. Scoped to bindings.o and uapi.o, not to handwritten code. - three `'static` bounds the abstractions now need (irq handlers). - str.rs imported alloc::flags::* which prelude::* already provides, and `#![feature(used_with_arg)]` is stale now that the feature is stable; both are unused-feature/unused-import errors under -D warnings. Gates: bzImage builds (14,697,472 bytes) with CONFIG_RUST=y, virtio-blk and a serial console built in. Boot test in QEMU is next. |
||
|
|
598cf27219 |
Linux 6.19.3
Link: https://lore.kernel.org/r/20260217200002.683975158@linuxfoundation.org Tested-by: Florian Fainelli <florian.fainelli@broadcom.com> Tested-by: Takeshi Ogasawara <takeshi.ogasawara@futuring-girl.com> Tested-by: Peter Schneider <pschneider1968@googlemail.com> Tested-by: Jon Hunter <jonathanh@nvidia.com> Tested-by: Salvatore Bonaccorso <carnil@debian.org> Tested-by: Brett A C Sheffield <bacs@librecast.net> Tested-by: Mark Brown <broonie@kernel.org> Tested-by: Luna Jernberg <droidbittin@gmail.com> Tested-by: Ronald Warsow <rwarsow@gmx.de> Tested-by: Justin M. Forbes <jforbes@fedoraproject.org> Tested-by: Ron Economos <re@w6rz.net> Tested-by: Miguel Ojeda <ojeda@kernel.org> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |