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}"
);
}