From ed5ff764ba292b136b5c2fccc0c66a1bea54f1f7 Mon Sep 17 00:00:00 2001 From: surface-camera-build Date: Sat, 19 Sep 2026 06:07:47 -0400 Subject: [PATCH] cubelinux: the release starts with a digit, because module tooling demands it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 7 ++++- drivers/cube/cubelinux_store.rs | 54 ++++++++++++++++++++++++++++++++- include/uapi/linux/cube.h | 24 +++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fd6555e0d..b419d9892 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,12 @@ NAME = CUBELinux # release. The base version stays in VERSION/PATCHLEVEL/SUBLEVEL above (visible in # `make kernelversion`), while `uname -r` and /lib/modules report the CUBELinux # release, with any -dirty or SCM suffix still appended by setlocalversion. -CUBELINUX_VERSION = CUBELinux.0.6 +# +# The release must start with a digit: `depmod`, `mkinitramfs` and the rest of the module tooling +# reject a version whose first character is not numeric, so a release spelled `CUBELinux.0.6` is +# one the machine cannot load modules for. The box's own kernel works around this the same way +# (`6.19.3-cube+`), so CUBELinux does too: the Linux base, then `-cubelinux`, then our version. +CUBELINUX_VERSION = 6.19.3-cubelinux0.6 # *DOCUMENTATION* # To see a list of typical targets execute "make help" diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index 786bb48e6..c6f663826 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -598,8 +598,37 @@ impl Merged { /// Put the entries in the order a checkpoint would write them: by space, then by key, /// with the later write of a coordinate last — so a group's final entry decides it. + /// + /// This is a heapsort, and the reason is a panic rather than a preference. + /// `slice::sort_unstable` allocates about 3.5 KiB of *kernel stack* per recursion level in + /// this kernel's Rust, so a store with a couple of hundred records overflowed the 16 KiB + /// kernel stack and took the machine down — `BUG: TASK stack guard page was hit`, with the + /// instruction pointer inside + /// `core::slice::sort::unstable::quicksort::` and `sub rsp, 0xdd8` in + /// the function's first bytes. A heapsort has a constant frame and allocates nothing, so the + /// size of a store cannot decide whether reading it is safe, and the sort cannot fail for want + /// of memory in the middle of a read. + /// + /// It is not stable, which costs nothing here: `seq` is part of the ordering key, so the order + /// is total and two entries never compare equal. fn sort_entries(&mut self) { - self.entries.as_mut_slice().sort_unstable(); + let entries = self.entries.as_mut_slice(); + let len = entries.len(); + if len < 2 { + return; + } + // Build a max-heap, then repeatedly move the maximum to the end. + let mut start = len / 2; + while start > 0 { + start -= 1; + sift_down(entries, start, len); + } + let mut end = len; + while end > 1 { + end -= 1; + entries.swap(0, end); + sift_down(entries, 0, end); + } } fn value(&self, e: &Entry) -> &[u8] { @@ -608,6 +637,29 @@ impl Merged { } } +/// Restore the heap property below `root` over `entries[..end]`. +/// +/// Iterative on purpose: the whole point of the heapsort above is that nothing here grows with the +/// size of the store. +fn sift_down(entries: &mut [Entry], mut root: usize, end: usize) { + loop { + let left = 2 * root + 1; + if left >= end { + return; + } + let mut largest = left; + if left + 1 < end && entries[left] < entries[left + 1] { + largest = left + 1; + } + if entries[root] < entries[largest] { + entries.swap(root, largest); + root = largest; + } else { + return; + } + } +} + /// Read the log that follows the image, if there is one, and apply its entries. /// /// The log is a delta on the image, so its entries override image records for the same diff --git a/include/uapi/linux/cube.h b/include/uapi/linux/cube.h index a7673902e..e7e2cd04a 100644 --- a/include/uapi/linux/cube.h +++ b/include/uapi/linux/cube.h @@ -40,5 +40,29 @@ struct cube_args { #define CUBE_OP_GET 2 /* read them back */ #define CUBE_OP_DEL 3 /* remove the record */ #define CUBE_OP_SYNC 4 /* fold the log into the image */ +#define CUBE_OP_ENUM 5 /* walk the records of a space, in batches */ +#define CUBE_OP_SPACES 6 /* walk the spaces that hold records */ + +/* + * The walk's argument block: its own block rather than a wider `cube_args`, because it needs a + * cursor and a buffer, and the coordinate would otherwise be both an input and an output. + * + * Records are packed as `key(24) | value_len(u32, little-endian) | value`, in the store's own + * order — space first, then key — which is the order a checkpoint writes them and the order the + * userspace store returns them, so a kernel listing and a userspace listing can be compared + * directly. The space is not repeated per record: the caller named it. + */ +struct cube_enum_args { + __u32 size; /* sizeof(struct cube_enum_args) as the caller built it */ + __u32 op; /* CUBE_OP_ENUM or CUBE_OP_SPACES */ + __u8 space[32]; /* in: the space to walk; out: the space found (CUBE_OP_SPACES) */ + __u64 cursor; /* in: 0 to start, or what the last call returned; + * out: what to pass next + */ + __u64 value; /* user pointer: where to put the records */ + __u64 len; /* in: the buffer's capacity; + * out: bytes written, or on -ERANGE what would be needed + */ +}; #endif /* _UAPI_LINUX_CUBE_H */