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:
@@ -22,63 +22,44 @@
|
||||
// gates stay readable.
|
||||
|
||||
/// 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";
|
||||
|
||||
/// 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;
|
||||
/// 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;
|
||||
/// 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;
|
||||
|
||||
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
|
||||
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;
|
||||
/// magic(4) version(1) curve(1) image_bytes(8) record_count(8) space_count(8) index_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;
|
||||
|
||||
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
|
||||
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;
|
||||
/// 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;
|
||||
/// 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;
|
||||
/// 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;
|
||||
|
||||
/// 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";
|
||||
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
|
||||
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;
|
||||
/// `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;
|
||||
|
||||
/// `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;
|
||||
/// `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;
|
||||
|
||||
/// What an image's header says, in any version.
|
||||
#[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 version: 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
|
||||
/// these are all refusals rather than defaults.
|
||||
#[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 {
|
||||
/// Not a CUBE image at all.
|
||||
Magic,
|
||||
@@ -125,7 +105,6 @@ pub enum Bad {
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
if bytes.len() < 5 || &bytes[0..4] != 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.
|
||||
#[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 record_count: u64,
|
||||
pub space_count: u64,
|
||||
@@ -282,7 +260,6 @@ impl V3 {
|
||||
|
||||
/// A v3 index entry: the key, and where its value lies.
|
||||
#[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 key: &'a [u8; RAW_KEY_LEN],
|
||||
pub value_off: u64,
|
||||
@@ -291,7 +268,6 @@ pub struct IndexEntry<'a> {
|
||||
|
||||
/// One packed record, and where the next one starts.
|
||||
#[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 space: &'a [u8; SPACE_ID_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
|
||||
/// 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<'_>> {
|
||||
if off + RECORD_FIXED > end || end > bytes.len() {
|
||||
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.
|
||||
#[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 space: &'a [u8; SPACE_ID_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
|
||||
/// 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<'_>> {
|
||||
if off + ENTRY_FIXED > log.len() {
|
||||
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
|
||||
/// 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 {
|
||||
let mut w = [0u8; 8];
|
||||
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.
|
||||
#[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 {
|
||||
let mut w = [0u8; 4];
|
||||
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
|
||||
/// 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.
|
||||
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
|
||||
pub fn crc32(bytes: &[u8]) -> u32 {
|
||||
let mut crc = 0xFFFF_FFFFu32;
|
||||
for byte in bytes {
|
||||
@@ -405,11 +375,9 @@ pub fn crc32(bytes: &[u8]) -> u32 {
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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 {
|
||||
for byte in bytes {
|
||||
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
|
||||
/// 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.
|
||||
#[allow(dead_code)] // one build uses it and the other does not: neither subset is dead
|
||||
pub struct Digest {
|
||||
pub version: u8,
|
||||
pub curve: u8,
|
||||
|
||||
@@ -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*
|
||||
// a header delegate to it — validation is where a second description would be read as truth.
|
||||
#[path = "cube_format.rs"]
|
||||
#[allow(dead_code)] // the two builds use different subsets of one API
|
||||
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> {
|
||||
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 = core::cmp::min(extent, image.len());
|
||||
let mut off = if header.version == VERSION_V1 {
|
||||
|
||||
Reference in New Issue
Block a user