guests/verify-bundle/src/wasm.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The wasm exports, bound to heimdall-wasm's WIT
//! (`crates/heimdall-wasm/wit/heimdall.wit`). Heimdall is a defensive sandbox.
//! One module exports exactly one world, chosen by feature:
//!
//! * default: world `job` — the single-instance guest, OpenZL linked in.
//! `run(input, params) -> result<output, refusal>` plus `emit-file`.
//! * `split` (built with `--no-default-features`): world `verifier` — the
//! same `run`, but every compressed chunk goes to the host's
//! `decode-chunks` import; no C in this module.
//! * `decoder`: world `decoder` — `decode-chunks` only, OpenZL + BLAKE3,
//! imports nothing. `evil-decoder` adds test-only lies (see [`evil`]).
//!
//! `input` is the raw `.holger.znippy`; `params` are the job's string pairs.
//! Outputs are emitted only on Accepted. The refusal `reason` starts with the
//! stable code in brackets (`[holger-update-<n>]` or `[heimdall-bundle-<n>]`),
//! because the WIT refusal record carries one string.
#![allow(unsafe_code)] // the generated canonical-ABI glue only
#[cfg(all(feature = "split", feature = "decoder"))]
compile_error!("features `split` and `decoder` build different worlds; pick one");
/// The note of an accepted run, after its files were emitted.
#[cfg(not(feature = "decoder"))]
fn emit_accepted(o: &crate::Outcome, emit: impl Fn(&str, &[u8])) -> String {
for e in &o.emitted {
emit(&e.name, &e.bytes);
}
format!(
"accepted: {} file(s) emitted, report in {}",
o.emitted.len(),
crate::REPORT_NAME
)
}
#[cfg(not(any(feature = "split", feature = "decoder")))]
mod job {
wit_bindgen::generate!({ path: "../../crates/heimdall-wasm/wit/heimdall.wit", world: "job" });
struct VerifyBundle;
impl Guest for VerifyBundle {
fn run(input: Vec<u8>, params: Vec<Param>) -> Result<Output, Refusal> {
let o = crate::verify_bundle_pairs(
&input,
params.iter().map(|p| (p.key.as_str(), p.value.as_str())),
);
match o.verdict {
crate::Verdict::Accepted => Ok(Output {
note: super::emit_accepted(&o, |n, b| emit_file(n, b)),
}),
crate::Verdict::Refused { reason, .. } => Err(Refusal { reason }),
}
}
}
export!(VerifyBundle);
}
#[cfg(all(feature = "split", not(feature = "decoder")))]
mod verifier {
wit_bindgen::generate!({
path: "../../crates/heimdall-wasm/wit/heimdall.wit",
world: "verifier",
// The frames are lent to the host straight out of the input (no copy).
ownership: Borrowing { duplicate_if_necessary: false },
});
use crate::decode;
/// The host's decoder instances, behind the `decode-chunks` import.
struct HostDecoder;
impl decode::ChunkDecoder for HostDecoder {
fn decode_chunks(&mut self, chunks: &[decode::ChunkRef<'_>]) -> Vec<decode::ChunkResult> {
let refs: Vec<ChunkRef<'_>> = chunks
.iter()
.map(|c| ChunkRef {
frame: c.frame,
size: c.size,
want_bytes: c.want_bytes,
})
.collect();
decode_chunks(&refs)
.into_iter()
.map(|r| {
r.map(|d| decode::Decoded {
digest: d.digest,
bytes: d.bytes,
})
})
.collect()
}
/// The host checks every answer's length and BLAKE3 natively before
/// it returns it (crates/heimdall-wasm, a lying decoder is refused
/// there with heimdall-wasm-13).
fn digests_confirmed(&self) -> bool {
true
}
/// Native SHA-256 + BLAKE3 by the host, in parallel over members.
fn member_digests(&mut self, members: &[&[u8]]) -> Option<Vec<decode::MemberDigest>> {
Some(
digest_members(members)
.into_iter()
.map(|d| decode::MemberDigest {
sha256: d.sha256,
blake3: d.blake3,
})
.collect(),
)
}
}
struct VerifyBundle;
impl Guest for VerifyBundle {
fn run(input: Vec<u8>, params: Vec<Param>) -> Result<Output, Refusal> {
let o = crate::verify_bundle_pairs_split(
&input,
params.iter().map(|p| (p.key.as_str(), p.value.as_str())),
&mut HostDecoder,
);
match o.verdict {
crate::Verdict::Accepted => Ok(Output {
note: super::emit_accepted(&o, |n, b| emit_file(n, b)),
}),
crate::Verdict::Refused { reason, .. } => Err(Refusal { reason }),
}
}
}
export!(VerifyBundle);
}
#[cfg(feature = "decoder")]
mod decoder {
wit_bindgen::generate!({ path: "../../crates/heimdall-wasm/wit/heimdall.wit", world: "decoder" });
use crate::decode;
struct Decoder;
impl Guest for Decoder {
fn decode_chunks(chunks: Vec<ChunkRef>) -> Vec<ChunkResult> {
let out: Vec<ChunkResult> = chunks
.iter()
.map(|c| {
decode::decode_one(&decode::ChunkRef {
frame: &c.frame,
size: c.size,
want_bytes: c.want_bytes,
})
.map(|d| Decoded {
digest: d.digest,
bytes: d.bytes,
})
})
.collect();
#[cfg(feature = "evil-decoder")]
let out = {
let mut out = out;
super::evil::apply(&mut out, |d| (&mut d.digest, &mut d.bytes));
out
};
out
}
}
export!(Decoder);
}
/// TEST-ONLY lies (feature `evil-decoder`): the decoder the verifier and the
/// host must catch. The mode is the 8 bytes after [`MARKER`] in the module's
/// data, patched by the tests after the build (one build, many modes).
#[cfg(feature = "evil-decoder")]
mod evil {
pub const MARKER: &[u8; 19] = b"HEIMDALL-EVIL-MODE=";
#[used]
static MODE: [u8; 27] = *b"HEIMDALL-EVIL-MODE=honest__";
fn mode() -> [u8; 8] {
let mut m = [0u8; 8];
for (i, b) in m.iter_mut().enumerate() {
// SAFETY: an in-bounds read of a static; volatile so the patched
// bytes are read at run time, never folded at build time.
*b = unsafe { core::ptr::read_volatile(&MODE[MARKER.len() + i]) };
}
m
}
#[inline(never)]
fn spin(n: u64) -> u64 {
let mut x = n;
loop {
x = core::hint::black_box(x.wrapping_mul(6364136223846793005).wrapping_add(1));
if x == 0 {
return x;
}
}
}
pub fn apply<T>(
out: &mut Vec<Result<T, String>>,
parts: impl Fn(&mut T) -> (&mut Vec<u8>, &mut Vec<u8>),
) {
let mode = mode();
let first_ok = out.iter().position(|r| r.is_ok());
match &mode {
b"flipbyte" => {
// Wrong bytes, the digest of the RIGHT bytes.
if let Some(Ok(d)) = first_ok.map(|i| &mut out[i]) {
let (_, bytes) = parts(d);
if let Some(b) = bytes.first_mut() {
*b ^= 0x5a;
}
}
}
b"baddigst" => {
// The right bytes, a wrong digest.
if let Some(Ok(d)) = first_ok.map(|i| &mut out[i]) {
let (digest, _) = parts(d);
digest[0] ^= 1;
}
}
b"consistn" => {
// Wrong bytes AND their own true digest: only the index
// checksum (and the signed manifest) can tell.
if let Some(Ok(d)) = first_ok.map(|i| &mut out[i]) {
let (digest, bytes) = parts(d);
if let Some(b) = bytes.first_mut() {
*b ^= 0x5a;
}
*digest = blake3::hash(bytes).as_bytes().to_vec();
}
}
b"shortone" => {
// One byte short, with that short slice's true digest.
if let Some(Ok(d)) = first_ok.map(|i| &mut out[i]) {
let (digest, bytes) = parts(d);
bytes.pop();
*digest = blake3::hash(bytes).as_bytes().to_vec();
}
}
b"dropone_" => {
out.pop();
}
b"trap____" => core::arch::wasm32::unreachable(),
b"spin____" => {
let n = spin(out.len() as u64);
out.truncate(n as usize);
}
b"hog_____" => {
// Grow memory until the host's limit stops it.
let mut keep: Vec<Vec<u8>> = Vec::new();
loop {
keep.push(vec![1u8; 16 << 20]);
core::hint::black_box(&keep);
}
}
_ => {} // honest
}
}
}