// SPDX-License-Identifier: AGPL-3.0-only
//! OpenZL-compressed members (feature `openzl`). Heimdall is a defensive
//! sandbox: the guest decodes znippy's OpenZL frames itself, and only after the
//! frame header declares exactly the size the index declares. Real
//! Holger-packed bundles are accepted with the report Holger's own verifier
//! gives; tampered, truncated, lying and bomb frames are refused, and on every
//! hostile frame the guest and Holger agree on accept versus refuse.
#![cfg(feature = "openzl")]
mod common;
use common::zwriter::{openzl_frame, RawArchive};
use common::*;
use heimdall_guest_verify_bundle::znippy::Archive;
use holger_errcode as ec;
const PAYLOAD: &str = "payload/firmware.bin";
/// A compressible payload big enough for several OpenZL chunks.
fn firmware(len: usize) -> Vec<u8> {
(0..len)
.map(|i| b"ACS880 firmware block "[i % 22] ^ ((i / 4096) as u8 & 3))
.collect()
}
fn big_fixture(len: usize) -> Fixture {
build(Spec {
payloads: vec![(
"firmware.bin".into(),
firmware(len),
"application/octet-stream".into(),
)],
..Spec::default()
})
}
fn report(o: &Outcome) -> serde_json::Value {
assert!(o.is_accepted(), "refused: {:?}", o.reason());
serde_json::from_slice(&o.output).unwrap()
}
/// Every reported field equals what Holger's verifier says about the same file.
fn assert_same_report_as_holger(fx: &Fixture, archive: &[u8]) {
let h = fx.holger(archive).expect("Holger accepts its own bundle");
let r = report(&verify_bundle(archive, &fx.params()));
assert_eq!(r["bundle_id"], h.bundle_id);
assert_eq!(r["sequence"], h.sequence);
assert_eq!(r["supplier"], h.supplier);
assert_eq!(r["product"], h.product);
assert_eq!(r["release_version"], h.release_version);
assert_eq!(r["candidate_digest"], h.candidate_digest);
assert_eq!(
r["release_signer_fingerprint"],
h.release_signer_fingerprint
);
assert_eq!(r["release_signer_subject"], h.release_signer_subject);
assert_eq!(r["non_production_evidence"], h.non_production_evidence);
let mut ga: Vec<(String, String)> = r["approvals"]
.as_array()
.unwrap()
.iter()
.map(|a| {
(
a["role"].as_str().unwrap().into(),
a["fingerprint"].as_str().unwrap().into(),
)
})
.collect();
let mut ha: Vec<(String, String)> = h
.approvals
.iter()
.map(|a| (a.role.clone(), a.fingerprint.clone()))
.collect();
ga.sort();
ha.sort();
assert_eq!(ga, ha);
}
/// The guest and Holger agree: both accept (and the guest emits the original
/// bytes) or both refuse (and the guest says why with a stable code).
fn assert_agrees_with_holger(fx: &Fixture, archive: &[u8], what: &str) -> Outcome {
let o = verify_bundle(archive, &fx.params());
let h = fx.holger(archive);
assert_eq!(
o.is_accepted(),
h.is_ok(),
"{what}: guest {:?} vs Holger {:?}",
o.reason(),
h.as_ref().err()
);
if o.is_accepted() {
for e in &o.emitted[1..] {
assert_eq!(&e.bytes, member_for(&fx.members, &e.name), "{what}");
}
} else {
code_of(&o);
}
o
}
#[test]
fn a_bundle_holger_packs_with_openzl_members_is_accepted_with_holgers_report() {
let fx = big_fixture(300_000);
let archive = pack_holger(&fx.members);
assert!(
holger_compressed_rows(&archive) > 0,
"Holger's packer compressed nothing; the test would prove nothing"
);
assert_same_report_as_holger(&fx, &archive);
}
#[test]
fn an_openzl_bundle_emits_exactly_the_original_member_bytes() {
let fx = big_fixture(1_000_000);
let o = verify_bundle(&pack_holger(&fx.members), &fx.params());
assert!(o.is_accepted(), "{:?}", o.reason());
for e in &o.emitted[1..] {
assert_eq!(&e.bytes, member_for(&fx.members, &e.name), "{}", e.name);
}
}
#[test]
fn every_member_in_multi_chunk_openzl_frames_is_accepted_like_holger() {
let fx = big_fixture(100_000);
let a = RawArchive::from_members_openzl(&fx.members, 8192);
assert!(a.rows.iter().filter(|r| r.compressed).count() > 10);
assert_same_report_as_holger(&fx, &a.encode());
}
#[test]
fn a_frame_in_openzls_newest_format_decodes_too() {
let fx = good();
let mut a = RawArchive::from_members_openzl(&fx.members, 4096);
let plain = member_for(&fx.members, "payload+2ffirmware.bin").to_vec();
let frame = latest_format_frame(&plain);
let r = a.row_mut(PAYLOAD).clone();
let off = a.blobs.len() as u64;
a.blobs.extend_from_slice(&frame);
let row = a.row_mut(PAYLOAD);
row.blob_offset = off;
row.blob_size = frame.len() as u64;
assert_eq!(row.uncompressed_size, r.uncompressed_size);
assert!(verify_bundle(&a.encode(), &fx.params()).is_accepted());
}
#[test]
fn a_bit_flip_anywhere_in_an_openzl_frame_never_makes_the_guest_and_holger_disagree() {
let fx = big_fixture(20_000);
let clean = RawArchive::from_members_openzl(&fx.members, 1 << 20);
let len = clean.clone().blob_mut(PAYLOAD).len();
let mut refused = 0;
for i in 0..len {
let mut a = clean.clone();
a.blob_mut(PAYLOAD)[i] ^= 0x01 << (i % 8);
let o = assert_agrees_with_holger(&fx, &a.encode(), &format!("flip at {i}"));
if !o.is_accepted() {
refused += 1;
assert_eq!(code_of(&o), ec::UPDATE_CONTAINER.code, "{:?}", o.reason());
}
}
assert!(
refused * 10 >= len * 9,
"only {refused} of {len} flips refused"
);
}
#[test]
fn a_truncated_openzl_frame_is_refused_like_holger() {
let fx = big_fixture(20_000);
let clean = RawArchive::from_members_openzl(&fx.members, 1 << 20);
let len = clean.clone().row_mut(PAYLOAD).blob_size;
for keep in [0, 1, 4, 8, 16, 32, len / 2, len - 1] {
let mut a = clean.clone();
a.row_mut(PAYLOAD).blob_size = keep;
let o = assert_agrees_with_holger(&fx, &a.encode(), &format!("keep {keep}"));
assert_eq!(code_of(&o), ec::UPDATE_CONTAINER.code, "{:?}", o.reason());
}
}
#[test]
fn a_frame_declaring_more_or_fewer_bytes_than_the_index_is_refused_before_decoding() {
// The reader alone (the pipeline already stops a member-size lie at the
// release manifest's size, holger-update-7, before reading it).
let fx = good();
let clean = RawArchive::from_members_openzl(&fx.members, 1 << 20);
let real = clean.clone().row_mut(PAYLOAD).uncompressed_size;
for lie in [1, real - 1, real + 1, real * 2] {
let mut a = clean.clone();
a.row_mut(PAYLOAD).uncompressed_size = lie;
let bytes = a.encode();
let z = Archive::parse(&bytes).unwrap();
let e = z.read(PAYLOAD, u64::MAX).unwrap_err();
assert_eq!(e.code().as_string(), ec::UPDATE_CONTAINER.code, "{e}");
assert!(e.to_string().contains("but the index declares"), "{e}");
let o = verify_bundle(&bytes, &fx.params());
assert_refused(&o, ec::UPDATE_DIGEST_MISMATCH.code, "size digest mismatch");
}
}
#[test]
fn a_bomb_frame_behind_a_small_declared_size_is_refused_without_allocating_it() {
// 256 MiB of zeros in a frame of a few KiB; the index says 11 bytes.
let fx = good();
let bomb = openzl_frame(&vec![0u8; 256 << 20]);
assert!(bomb.len() < 1 << 20, "{}", bomb.len());
let mut a = RawArchive::from_members_openzl(&fx.members, 1 << 20);
let off = a.blobs.len() as u64;
a.blobs.extend_from_slice(&bomb);
let row = a.row_mut(PAYLOAD);
row.blob_offset = off;
row.blob_size = bomb.len() as u64;
let o = verify_bundle(&a.encode(), &fx.params());
assert_refused(
&o,
ec::UPDATE_CONTAINER.code,
"declares 268435456 bytes but the index declares",
);
}
#[test]
fn an_honest_bomb_is_refused_by_the_expansion_ratio_before_anything_is_decoded() {
let fx = good();
let bomb = openzl_frame(&vec![0u8; 256 << 20]);
let mut a = RawArchive::from_members_openzl(&fx.members, 1 << 20);
let off = a.blobs.len() as u64;
a.blobs.extend_from_slice(&bomb);
let row = a.row_mut(PAYLOAD);
row.blob_offset = off;
row.blob_size = bomb.len() as u64;
row.uncompressed_size = 256 << 20;
let o = verify_bundle(&a.encode(), &fx.params());
assert_refused(&o, ec::UPDATE_LIMIT_EXCEEDED.code, "decompression bomb");
}
#[test]
fn stored_bytes_flagged_compressed_are_refused_as_not_an_openzl_frame() {
let fx = good();
let mut a = RawArchive::from_members(&fx.members, 4096);
a.row_mut(PAYLOAD).compressed = true;
let o = verify_bundle(&a.encode(), &fx.params());
assert_refused(&o, ec::UPDATE_CONTAINER.code, "not an OpenZL frame");
}
#[test]
fn garbage_frames_of_every_small_length_are_refused_never_panic() {
let fx = good();
let clean = RawArchive::from_members_openzl(&fx.members, 1 << 20);
let mut seed = 0x9e37_79b9_7f4a_7c15u64;
for n in 0..200usize {
let mut a = clean.clone();
let mut g: Vec<u8> = (0..n)
.map(|_| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed as u8
})
.collect();
// Half of them keep a real frame's first bytes, to get past the magic.
if n % 2 == 0 {
let real = clean.clone().blob_mut(PAYLOAD).to_vec();
let k = n.min(real.len()).min(12);
g[..k].copy_from_slice(&real[..k]);
}
let off = a.blobs.len() as u64;
a.blobs.extend_from_slice(&g);
let row = a.row_mut(PAYLOAD);
row.blob_offset = off;
row.blob_size = n as u64;
let o = verify_bundle(&a.encode(), &fx.params());
assert!(!o.is_accepted(), "garbage of {n} bytes accepted");
assert_eq!(code_of(&o), ec::UPDATE_CONTAINER.code, "{:?}", o.reason());
}
}
/// How many chunk rows of a Holger-packed archive are compressed (znippy's own
/// reader is the judge).
fn holger_compressed_rows(archive: &[u8]) -> usize {
let a = Archive::parse(archive).unwrap();
a.names()
.filter_map(|n| a.entry(n))
.flat_map(|e| e.chunks.iter())
.filter(|c| c.compressed)
.count()
}
/// A frame in the newest format OpenZL 0.3.0 writes (27), not znippy's pin.
fn latest_format_frame(input: &[u8]) -> Vec<u8> {
use openzl_sys_rs::*;
let latest = unsafe { ZL_getDefaultEncodingVersion() } as i32;
let mut cctx = ZlCCtx::new().unwrap();
cctx.set_parameter(ZL_CParam_ZL_CParam_formatVersion, latest)
.unwrap();
cctx.set_parameter(ZL_CParam_ZL_CParam_compressionLevel, 3)
.unwrap();
let mut frame = vec![0u8; zl_compress_bound(input.len())];
let n = cctx.compress(&mut frame, input).unwrap();
frame.truncate(n);
let fmt = unsafe { ZL_getFormatVersionFromFrame(frame.as_ptr().cast(), frame.len()) };
assert_eq!(zl_report_value(fmt), latest as usize);
frame
}
/// The fuzz find (oden, 11 s): a 36-byte frame whose "constant" codec asks
/// OpenZL for a ~2^64-byte stream (DI_constant_typed -> STREAM_reserve) — an
/// allocation that does not come from the header size the reader checks.
/// malloc returns NULL, OpenZL reports an error, the member is refused.
const CONSTANT_STREAM_ALLOC: &[u8] =
include_bytes!("../fuzz/seeds/openzl_frame/regress-constant-stream-alloc");
#[test]
fn the_fuzz_found_constant_stream_allocation_is_an_openzl_error_not_a_crash() {
// The header declares 0 bytes, so the size check passes; the allocation
// OpenZL then asks for fails (NULL) and it reports an error.
assert_eq!(
heimdall_guest_verify_bundle::zl::frame_size(CONSTANT_STREAM_ALLOC),
Ok(0)
);
let e = heimdall_guest_verify_bundle::zl::decode(CONSTANT_STREAM_ALLOC, &mut []).unwrap_err();
assert!(e.0.starts_with("Allocation"), "{e:?}");
heimdall_guest_verify_bundle::fuzz_api::openzl_frame(CONSTANT_STREAM_ALLOC);
}
#[test]
fn the_fuzz_found_frame_as_a_zero_length_tail_chunk_is_refused_like_holger() {
let fx = good();
let mut a = RawArchive::from_members(&fx.members, 1 << 20);
a.append_compressed_tail(PAYLOAD, CONSTANT_STREAM_ALLOC);
let o = assert_agrees_with_holger(&fx, &a.encode(), "constant-stream frame");
assert_refused(&o, ec::UPDATE_CONTAINER.code, "does not decode");
}