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).
This commit is contained in:
surface-camera-build
2026-09-21 03:59:55 -04:00
parent d569df710d
commit 78540b5687
2 changed files with 34 additions and 33 deletions
-33
View File
@@ -22,63 +22,44 @@
// gates stay readable. // gates stay readable.
/// Every image starts with these four bytes. /// Every image starts with these four bytes.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const MAGIC: &[u8; 4] = b"CUBE"; pub const MAGIC: &[u8; 4] = b"CUBE";
/// The original format: magic, version, curve, then records until zero padding. /// The original format: magic, version, curve, then records until zero padding.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const VERSION_V1: u8 = 1; pub const VERSION_V1: u8 = 1;
/// The packed format: the same, plus the image's byte extent and record count. /// The packed format: the same, plus the image's byte extent and record count.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const VERSION_V2: u8 = 2; pub const VERSION_V2: u8 = 2;
/// The addressed format: the same, plus a space table, a fixed-size index, and packed values. /// The addressed format: the same, plus a space table, a fixed-size index, and packed values.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const VERSION_V3: u8 = 3; pub const VERSION_V3: u8 = 3;
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const HEADER_LEN_V1: usize = 6; pub const HEADER_LEN_V1: usize = 6;
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const HEADER_LEN_V2: usize = 6 + 8 + 8; pub const HEADER_LEN_V2: usize = 6 + 8 + 8;
/// magic(4) version(1) curve(1) image_bytes(8) record_count(8) space_count(8) index_off(8) /// magic(4) version(1) curve(1) image_bytes(8) record_count(8) space_count(8) index_off(8)
/// values_off(8). /// values_off(8).
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const HEADER_LEN_V3: usize = 4 + 1 + 1 + 8 + 8 + 8 + 8 + 8; pub const HEADER_LEN_V3: usize = 4 + 1 + 1 + 8 + 8 + 8 + 8 + 8;
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const SPACE_ID_LEN: usize = 32; pub const SPACE_ID_LEN: usize = 32;
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const RAW_KEY_LEN: usize = 24; pub const RAW_KEY_LEN: usize = 24;
/// A packed record's fixed part: `space | key | value_len(u64)`, with the value behind it. /// A packed record's fixed part: `space | key | value_len(u64)`, with the value behind it.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8; pub const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8;
/// A v3 index entry: `key | value_off(u64) | value_len(u64)`. /// A v3 index entry: `key | value_off(u64) | value_len(u64)`.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const INDEX_ENTRY: usize = RAW_KEY_LEN + 8 + 8; pub const INDEX_ENTRY: usize = RAW_KEY_LEN + 8 + 8;
/// A v3 space-table row: `space | first index(u64) | records(u64)`. /// A v3 space-table row: `space | first index(u64) | records(u64)`.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const SPACE_ENTRY: usize = SPACE_ID_LEN + 8 + 8; pub const SPACE_ENTRY: usize = SPACE_ID_LEN + 8 + 8;
/// The log's framing, which the fold and the readers both parse. /// The log's framing, which the fold and the readers both parse.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const WAL_MAGIC: &[u8; 4] = b"CUBW"; pub const WAL_MAGIC: &[u8; 4] = b"CUBW";
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const WAL_VERSION: u8 = 1; pub const WAL_VERSION: u8 = 1;
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const WAL_HEADER_LEN: usize = 6; pub const WAL_HEADER_LEN: usize = 6;
/// `op(1) | crc(4) | space(32) | key(24) | len(4)`, with the value behind it. /// `op(1) | crc(4) | space(32) | key(24) | len(4)`, with the value behind it.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const ENTRY_FIXED: usize = 1 + 4 + SPACE_ID_LEN + RAW_KEY_LEN + 4; pub const ENTRY_FIXED: usize = 1 + 4 + SPACE_ID_LEN + RAW_KEY_LEN + 4;
/// `CUBE_OP_PUT`: an entry that stores a value. /// `CUBE_OP_PUT`: an entry that stores a value.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const WAL_OP_WRITE: u8 = 1; pub const WAL_OP_WRITE: u8 = 1;
/// `CUBE_OP_DEL`: an entry that removes one. /// `CUBE_OP_DEL`: an entry that removes one.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const WAL_OP_DELETE: u8 = 2; pub const WAL_OP_DELETE: u8 = 2;
/// What an image's header says, in any version. /// What an image's header says, in any version.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub struct Header { pub struct Header {
pub version: u8, pub version: u8,
pub curve: u8, pub curve: u8,
@@ -112,7 +93,6 @@ impl Header {
/// Why an image was refused. A reader that guesses at an extent can read somebody else's bytes, so /// Why an image was refused. A reader that guesses at an extent can read somebody else's bytes, so
/// these are all refusals rather than defaults. /// these are all refusals rather than defaults.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub enum Bad { pub enum Bad {
/// Not a CUBE image at all. /// Not a CUBE image at all.
Magic, Magic,
@@ -125,7 +105,6 @@ pub enum Bad {
} }
/// Read and validate an image header. /// Read and validate an image header.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub fn parse_header(bytes: &[u8]) -> Result<Header, Bad> { pub fn parse_header(bytes: &[u8]) -> Result<Header, Bad> {
if bytes.len() < 5 || &bytes[0..4] != MAGIC { if bytes.len() < 5 || &bytes[0..4] != MAGIC {
return Err(Bad::Magic); return Err(Bad::Magic);
@@ -173,7 +152,6 @@ pub fn parse_header(bytes: &[u8]) -> Result<Header, Bad> {
/// The v3 tables' geometry: where the index is, where the values start, and how many of each. /// The v3 tables' geometry: where the index is, where the values start, and how many of each.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub struct V3 { pub struct V3 {
pub record_count: u64, pub record_count: u64,
pub space_count: u64, pub space_count: u64,
@@ -282,7 +260,6 @@ impl V3 {
/// A v3 index entry: the key, and where its value lies. /// A v3 index entry: the key, and where its value lies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub struct IndexEntry<'a> { pub struct IndexEntry<'a> {
pub key: &'a [u8; RAW_KEY_LEN], pub key: &'a [u8; RAW_KEY_LEN],
pub value_off: u64, pub value_off: u64,
@@ -291,7 +268,6 @@ pub struct IndexEntry<'a> {
/// One packed record, and where the next one starts. /// One packed record, and where the next one starts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub struct PackedRecord<'a> { pub struct PackedRecord<'a> {
pub space: &'a [u8; SPACE_ID_LEN], pub space: &'a [u8; SPACE_ID_LEN],
pub key: &'a [u8; RAW_KEY_LEN], pub key: &'a [u8; RAW_KEY_LEN],
@@ -304,7 +280,6 @@ pub struct PackedRecord<'a> {
/// ///
/// This is the v1/v2 layout's only parsing rule, and it is shared for the same reason the constants /// This is the v1/v2 layout's only parsing rule, and it is shared for the same reason the constants
/// are: a reader that disagrees about a frame's length reads every following record wrong. /// are: a reader that disagrees about a frame's length reads every following record wrong.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub fn packed_record(bytes: &[u8], off: usize, end: usize) -> Option<PackedRecord<'_>> { pub fn packed_record(bytes: &[u8], off: usize, end: usize) -> Option<PackedRecord<'_>> {
if off + RECORD_FIXED > end || end > bytes.len() { if off + RECORD_FIXED > end || end > bytes.len() {
return None; return None;
@@ -325,7 +300,6 @@ pub fn packed_record(bytes: &[u8], off: usize, end: usize) -> Option<PackedRecor
/// One log entry, and where the next one starts. /// One log entry, and where the next one starts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub struct WalEntry<'a> { pub struct WalEntry<'a> {
pub space: &'a [u8; SPACE_ID_LEN], pub space: &'a [u8; SPACE_ID_LEN],
pub key: &'a [u8; RAW_KEY_LEN], pub key: &'a [u8; RAW_KEY_LEN],
@@ -338,7 +312,6 @@ pub struct WalEntry<'a> {
/// ///
/// The checksum covers space, key, length and value, so a torn tail is stopped at rather than /// The checksum covers space, key, length and value, so a torn tail is stopped at rather than
/// applied — the rule the fold and every reader share. /// applied — the rule the fold and every reader share.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub fn wal_entry(log: &[u8], off: usize) -> Option<WalEntry<'_>> { pub fn wal_entry(log: &[u8], off: usize) -> Option<WalEntry<'_>> {
if off + ENTRY_FIXED > log.len() { if off + ENTRY_FIXED > log.len() {
return None; return None;
@@ -367,7 +340,6 @@ pub fn wal_entry(log: &[u8], off: usize) -> Option<WalEntry<'_>> {
/// A little-endian `u64` at `at`, or zero if it does not fit — callers bound-check first, and the /// A little-endian `u64` at `at`, or zero if it does not fit — callers bound-check first, and the
/// alternative is a panic in a reader that exists to refuse rather than guess. /// alternative is a panic in a reader that exists to refuse rather than guess.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub fn le_u64(bytes: &[u8], at: usize) -> u64 { pub fn le_u64(bytes: &[u8], at: usize) -> u64 {
let mut w = [0u8; 8]; let mut w = [0u8; 8];
if at + 8 <= bytes.len() { if at + 8 <= bytes.len() {
@@ -377,7 +349,6 @@ pub fn le_u64(bytes: &[u8], at: usize) -> u64 {
} }
/// A little-endian `u32` at `at`, with the same rule. /// A little-endian `u32` at `at`, with the same rule.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub fn le_u32(bytes: &[u8], at: usize) -> u32 { pub fn le_u32(bytes: &[u8], at: usize) -> u32 {
let mut w = [0u8; 4]; let mut w = [0u8; 4];
if at + 4 <= bytes.len() { if at + 4 <= bytes.len() {
@@ -391,7 +362,6 @@ pub fn le_u32(bytes: &[u8], at: usize) -> u32 {
/// A corruption check, not a security check: it catches a torn write or a flipped bit, and says /// A corruption check, not a security check: it catches a torn write or a flipped bit, and says
/// nothing about whether anyone tampered with the bytes — that is `cube-crypt`'s job, and it belongs /// nothing about whether anyone tampered with the bytes — that is `cube-crypt`'s job, and it belongs
/// on the record rather than on the framing. /// on the record rather than on the framing.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub fn crc32(bytes: &[u8]) -> u32 { pub fn crc32(bytes: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32; let mut crc = 0xFFFF_FFFFu32;
for byte in bytes { for byte in bytes {
@@ -405,11 +375,9 @@ pub fn crc32(bytes: &[u8]) -> u32 {
} }
/// The store's digest hash: FNV-1a, 64-bit. /// The store's digest hash: FNV-1a, 64-bit.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; pub const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
/// Fold more bytes into an FNV-1a hash. /// Fold more bytes into an FNV-1a hash.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 { pub fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 {
for byte in bytes { for byte in bytes {
h ^= *byte as u64; h ^= *byte as u64;
@@ -423,7 +391,6 @@ pub fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 {
/// `bytes` is the records' logical size — the same number whatever layout holds them — because this /// `bytes` is the records' logical size — the same number whatever layout holds them — because this
/// line is what a kernel-written store and a userspace-written one are compared by, and a figure /// line is what a kernel-written store and a userspace-written one are compared by, and a figure
/// that changed with the layout would make one store look like two. /// that changed with the layout would make one store look like two.
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
pub struct Digest { pub struct Digest {
pub version: u8, pub version: u8,
pub curve: u8, pub curve: u8,
+34
View File
@@ -50,6 +50,7 @@ use core::fmt::{self, Write};
// image. The constants below are aliases of the shared ones, and the two functions that *validate* // image. The constants below are aliases of the shared ones, and the two functions that *validate*
// a header delegate to it — validation is where a second description would be read as truth. // a header delegate to it — validation is where a second description would be read as truth.
#[path = "cube_format.rs"] #[path = "cube_format.rs"]
#[allow(dead_code)] // the two builds use different subsets of one API
mod cube_format; mod cube_format;
@@ -827,6 +828,39 @@ fn log_window<'a>(device: &'a [u8], layout: &Layout) -> &'a [u8] {
fn build_merged(image: &[u8], log: &[u8], header: &Header) -> Result<(Merged, u64), &'static str> { fn build_merged(image: &[u8], log: &[u8], header: &Header) -> Result<(Merged, u64), &'static str> {
let mut merged = Merged::new().map_err(|_| "out-of-memory")?; let mut merged = Merged::new().map_err(|_| "out-of-memory")?;
// A v3 image holds its records behind a space table and an index rather than packed one after
// another, so the store it describes is read through that arithmetic. This is the path a fold
// takes once a store HAS been folded — the first fold reads a packed image and writes an
// addressed one, and every fold after that reads an addressed one — so a reader that only knew
// the packed layout could fold a store exactly once. Found on the box, where that is not a
// hypothesis: the live store's first fold succeeded and its second answered -EINVAL.
if header.version == VERSION_V3 {
let geometry = cube_format::V3::decode(image).map_err(|_| "bad-tables")?;
let mut row = 0u64;
while row < geometry.space_count {
let (space, first, records) = geometry.space_row(image, row).ok_or("truncated-table")?;
let mut i = 0u64;
while i < records {
let entry = geometry
.index_entry(image, first + i)
.ok_or("truncated-index")?;
let value = geometry
.value(image, &entry)
.ok_or("truncated-image")?;
merged
.add(space, entry.key, value, false)
.map_err(|_| "out-of-memory")?;
i += 1;
}
row += 1;
}
let applied = match apply_log(log, &mut merged)? {
Some(n) => n,
None => 0,
};
return Ok((merged, applied));
}
let extent = header.image_bytes.ok_or("v1-has-no-extent")? as usize; let extent = header.image_bytes.ok_or("v1-has-no-extent")? as usize;
let extent = core::cmp::min(extent, image.len()); let extent = core::cmp::min(extent, image.len());
let mut off = if header.version == VERSION_V1 { let mut off = if header.version == VERSION_V1 {