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.
This commit is contained in:
surface-camera-build
2026-09-22 01:02:39 -04:00
parent 29ae3b53f1
commit 72e72fe7a8
+11 -1
View File
@@ -1047,7 +1047,17 @@ fn build_merged(image: &[u8], log: &[u8], header: &Header) -> Result<(Merged, u6
/// to it. Returns `Err("no-log")` when there is none, so the caller can fall back to reading /// to it. Returns `Err("no-log")` when there is none, so the caller can fall back to reading
/// the image by itself. /// the image by itself.
fn merged_digest(image: &[u8], log: &[u8], header: &Header) -> Result<Line, &'static str> { fn merged_digest(image: &[u8], log: &[u8], header: &Header) -> Result<Line, &'static str> {
if log.len() < WAL_HEADER_LEN || &log[0..4] != WAL_MAGIC { // 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 — without a log there is nothing this path adds over the image's own order, which is why
// it answers "no log" and lets the caller walk the records it can see.
//
// Requiring a log here regardless of layout is what made a bare v3/v4 image read as a *packed*
// one: the caller fell through to a walk that starts at the v2 header length and reads index
// entries as record frames. It went unnoticed while every userspace-written image was v2, which
// is exactly the kind of bug that a migration finds and a unit test does not.
let addressed = header.version == VERSION_V3 || header.version == VERSION_V4;
if !addressed && (log.len() < WAL_HEADER_LEN || &log[0..4] != WAL_MAGIC) {
return Err("no-log"); return Err("no-log");
} }
let (mut merged, applied) = build_merged(image, log, header)?; let (mut merged, applied) = build_merged(image, log, header)?;