Gunnar

nordisk/heimdall

guests/verify-bundle/src/zl.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! The OpenZL frame decoder (feature `openzl`). Heimdall is a defensive sandbox.
//!
//! On wasm32 this is the vendored OpenZL 0.3.0 C decoder (`openzl/`, compiled
//! into the module by `build.rs`): a C bug on hostile bytes is a wasm trap or,
//! at worst, corrupts this one guest instance's own memory — never the host.
//! Natively it is `openzl-sys-rs` 0.4.1, the library Holger itself links.
//!
//! The caller ([`crate::znippy`]) bounds everything BEFORE a byte is allocated:
//! the frame header's size must equal the size the index declares for the chunk,
//! and the member's declared size already passed its cap and the archive's
//! expansion ratio. The decoder then writes into exactly that many bytes.

/// Why a frame was not decoded: OpenZL's error name and number.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZlError(pub String);

/// The decompressed size a frame's header declares.
pub fn frame_size(frame: &[u8]) -> Result<u64, ZlError> {
    imp::frame_size(frame)
}

/// Decode `frame` into `out` (exactly the declared size); returns bytes written.
pub fn decode(frame: &[u8], out: &mut [u8]) -> Result<usize, ZlError> {
    imp::decode(frame, out)
}

/// Decode `frame` into exactly `dst`: the frame header must declare
/// `dst.len()` bytes BEFORE the decoder runs (a frame that claims more is a
/// size lie or a bomb, refused without decoding), and the decoder must fill
/// all of it. The `Err` text names what failed; the archive reader puts it
/// into Holger's container refusal.
pub fn decode_exact(frame: &[u8], dst: &mut [u8]) -> Result<(), String> {
    let declared = frame_size(frame).map_err(|e| format!("not an OpenZL frame: {}", e.0))?;
    if declared != dst.len() as u64 {
        return Err(format!(
            "the OpenZL frame declares {declared} bytes but the index declares {}",
            dst.len()
        ));
    }
    let n = decode(frame, dst).map_err(|e| format!("the OpenZL frame does not decode: {}", e.0))?;
    if n != dst.len() {
        return Err(format!(
            "the OpenZL frame decoded to {n} bytes, not the declared {}",
            dst.len()
        ));
    }
    Ok(())
}

#[cfg(not(target_arch = "wasm32"))]
mod imp {
    use super::ZlError;
    use openzl_sys_rs as z;

    fn err(r: z::ZL_Report) -> ZlError {
        // SAFETY: an error report's `_code` is the error arm of the union, and
        // ZL_ErrorCode_toString returns a static NUL-terminated string.
        #[allow(unsafe_code)]
        unsafe {
            let code = r._code;
            let name = std::ffi::CStr::from_ptr(z::ZL_ErrorCode_toString(code));
            ZlError(format!("{} ({code})", name.to_string_lossy()))
        }
    }

    pub fn frame_size(frame: &[u8]) -> Result<u64, ZlError> {
        // SAFETY: pointer and length of one live slice.
        #[allow(unsafe_code)]
        let r = unsafe { z::ZL_getDecompressedSize(frame.as_ptr().cast(), frame.len()) };
        if z::zl_is_error(r) {
            return Err(err(r));
        }
        Ok(z::zl_report_value(r) as u64)
    }

    pub fn decode(frame: &[u8], out: &mut [u8]) -> Result<usize, ZlError> {
        // SAFETY: pointers and lengths of two live, disjoint slices.
        #[allow(unsafe_code)]
        let r = unsafe {
            z::ZL_decompress(
                out.as_mut_ptr().cast(),
                out.len(),
                frame.as_ptr().cast(),
                frame.len(),
            )
        };
        if z::zl_is_error(r) {
            return Err(err(r));
        }
        Ok(z::zl_report_value(r))
    }
}

#[cfg(target_arch = "wasm32")]
#[allow(unsafe_code)]
mod imp {
    //! The scalar-only ABI of openzl/shim/hzl.c, and the C heap = Rust's heap.
    use super::ZlError;
    use std::alloc::{alloc, alloc_zeroed, dealloc, realloc as re, Layout};
    use std::ffi::{c_char, c_int, c_void, CStr};

    extern "C" {
        fn hzl_frame_size(src: *const u8, len: usize, out: *mut u64) -> c_int;
        fn hzl_decode(
            dst: *mut u8,
            cap: usize,
            src: *const u8,
            len: usize,
            written: *mut usize,
        ) -> c_int;
        fn hzl_error_name(code: c_int) -> *const c_char;
    }

    fn err(code: c_int) -> ZlError {
        // SAFETY: OpenZL returns a static NUL-terminated string for any code.
        let name = unsafe { CStr::from_ptr(hzl_error_name(code)) };
        ZlError(format!("{} ({code})", name.to_string_lossy()))
    }

    pub fn frame_size(frame: &[u8]) -> Result<u64, ZlError> {
        let mut n = 0u64;
        // SAFETY: a live slice and a live out-pointer.
        match unsafe { hzl_frame_size(frame.as_ptr(), frame.len(), &mut n) } {
            0 => Ok(n),
            c => Err(err(c)),
        }
    }

    pub fn decode(frame: &[u8], out: &mut [u8]) -> Result<usize, ZlError> {
        let mut w = 0usize;
        // SAFETY: two live, disjoint slices and a live out-pointer.
        match unsafe {
            hzl_decode(
                out.as_mut_ptr(),
                out.len(),
                frame.as_ptr(),
                frame.len(),
                &mut w,
            )
        } {
            0 if w <= out.len() => Ok(w),
            0 => Err(ZlError(
                "decoder reported more bytes than the buffer".into(),
            )),
            c => Err(err(c)),
        }
    }

    // ---- malloc/calloc/realloc/free for the C decoder, on Rust's allocator.
    // Every block carries a 16-byte header holding its size; 16-byte aligned
    // like a C malloc. Allocation failure returns NULL (OpenZL then reports an
    // error); it never aborts the guest by itself.
    const HDR: usize = 16;

    fn layout(size: usize) -> Option<Layout> {
        Layout::from_size_align(size.checked_add(HDR)?, HDR).ok()
    }

    unsafe fn finish(base: *mut u8, size: usize) -> *mut c_void {
        if base.is_null() {
            return std::ptr::null_mut();
        }
        base.cast::<usize>().write(size);
        base.add(HDR).cast()
    }

    #[no_mangle]
    pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void {
        match layout(size) {
            Some(l) => finish(alloc(l), size),
            None => std::ptr::null_mut(),
        }
    }

    #[no_mangle]
    pub unsafe extern "C" fn calloc(n: usize, size: usize) -> *mut c_void {
        match n.checked_mul(size).and_then(|t| layout(t).map(|l| (t, l))) {
            Some((t, l)) => finish(alloc_zeroed(l), t),
            None => std::ptr::null_mut(),
        }
    }

    #[no_mangle]
    pub unsafe extern "C" fn free(p: *mut c_void) {
        if p.is_null() {
            return;
        }
        let base = p.cast::<u8>().sub(HDR);
        let size = base.cast::<usize>().read();
        dealloc(base, Layout::from_size_align_unchecked(size + HDR, HDR));
    }

    #[no_mangle]
    pub unsafe extern "C" fn realloc(p: *mut c_void, size: usize) -> *mut c_void {
        if p.is_null() {
            return malloc(size);
        }
        let Some(new) = layout(size) else {
            return std::ptr::null_mut();
        };
        let base = p.cast::<u8>().sub(HDR);
        let old = base.cast::<usize>().read();
        let nb = re(
            base,
            Layout::from_size_align_unchecked(old + HDR, HDR),
            new.size(),
        );
        finish(nb, size)
    }
}