Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/heimdall

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
        }
    }
}