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

nordisk/heimdall

crates/heimdall-wasm/tests/as_limit.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! A guest runs under an address-space limit (lane HEIM-04, Holger
//! HOL-HEIMDALL-04-DECISION). Holger's scan sidecar bounds each job with
//! `RLIMIT_AS` = the job's memory + 64 MiB. wasmtime's default reserves 4 GiB
//! plus guard pages of virtual space for every linear memory on a 64-bit host,
//! so the split guest's job trapped "mmap failed to reserve 0x104000000 bytes"
//! under a 1 GiB job. The host reserves no more than a memory uses (plus a
//! small growth room): what the job's memory limit says is what it gets.
//! Heimdall is a defensive sandbox; this test limits only its own child.
//!
//! The parent re-runs this test binary as a child with
//! `HEIMDALL_AS_LIMIT_CHILD=1`; the child sets its own `RLIMIT_AS` to its
//! current virtual size plus 1 GiB (below the 4 GiB a default reservation
//! takes; glibc's malloc arenas for the host's threads take their share), then
//! builds a host and loads and runs a guest twice, an out-of-bounds load (it
//! traps) and a guest that grows its memory to 200 MiB (it moves and keeps its
//! bytes).

mod common;
use common::*;
use heimdall_wasm::{RunLimits, Verdict};

const CHILD: &str = "HEIMDALL_AS_LIMIT_CHILD";

/// This process's virtual size in bytes (`VmSize` in `/proc/self/status`).
fn vm_size() -> u64 {
    let s = std::fs::read_to_string("/proc/self/status").unwrap();
    let line = s.lines().find(|l| l.starts_with("VmSize:")).unwrap();
    let kib: u64 = line.split_whitespace().nth(1).unwrap().parse().unwrap();
    kib << 10
}

#[test]
fn a_guest_runs_under_an_address_space_limit() {
    if std::env::var_os(CHILD).is_some() {
        let kit = Kit::new("as-limit");
        let limit = vm_size() + (1 << 30);
        let r = libc::rlimit {
            rlim_cur: limit,
            rlim_max: limit,
        };
        // SAFETY: a plain setrlimit on this process with a valid struct.
        assert_eq!(unsafe { libc::setrlimit(libc::RLIMIT_AS, &r) }, 0);
        let g = kit.guest("good", &good_wat());
        for _ in 0..2 {
            let o = g.run(b"hello bundle", &none(), &RunLimits::default());
            assert_eq!(
                o.verdict,
                Verdict::Accepted {
                    note: "accepted".into()
                },
                "{o:?}"
            );
            assert_eq!(o.files[0].bytes, b"hello bundle");
        }
        // Bounds are checked in code now, not by guard pages: an access past the
        // memory still traps.
        let oob = kit.guest("oob", &Wat::new("(i32.load (i32.const 0xfffffff0))").text());
        assert_trapped(
            &oob.run(b"", &none(), &RunLimits::default()),
            "out of bounds",
        );
        // A memory that outgrows its 64 MiB room moves and keeps its bytes:
        // 200 MiB, a word stored at its last page before and read back after.
        let grow = kit.guest(
            "grow",
            &Wat::new(
                r#"(i32.store (i32.const 2048) (i32.const 0x5a5a5a5a))
    (if (i32.eq (memory.grow (i32.const 3200)) (i32.const -1)) (then unreachable))
    (i32.store (i32.const 209780000) (i32.const 7))
    (if (i32.ne (i32.load (i32.const 2048)) (i32.const 0x5a5a5a5a)) (then unreachable))
    (if (i32.ne (i32.load (i32.const 209780000)) (i32.const 7)) (then unreachable))
    (call $ok (i32.const 32) (i32.const 8))"#,
            )
            .extra(r#"(data (i32.const 32) "accepted")"#)
            .text(),
        );
        let o = grow.run(b"", &none(), &RunLimits::default());
        assert_eq!(
            o.verdict,
            Verdict::Accepted {
                note: "accepted".into()
            },
            "{o:?}"
        );
        return;
    }
    let out = std::process::Command::new(std::env::current_exe().unwrap())
        .args([
            "--exact",
            "a_guest_runs_under_an_address_space_limit",
            "--nocapture",
            "--test-threads=1",
        ])
        .env(CHILD, "1")
        .output()
        .expect("the child test runs");
    let text = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(out.status.success(), "child failed:\n{text}");
    assert!(
        text.contains("1 passed"),
        "the child did not run the test:\n{text}"
    );
}