nordisk/heimdall
crates/heimdall-wasm/tests/memory_limits.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Memory, table, instance and stack limits: a bomb is a named refusal, never
//! a host crash or an out-of-memory kill. Heimdall is a defensive sandbox.
mod common;
use common::*;
use heimdall_wasm::RunLimits;
#[test]
fn a_memory_bomb_is_stopped_at_the_memory_limit() {
let kit = Kit::new("membomb");
// Grow 1 MiB at a time until the host says no; a -1 from memory.grow would
// let it return "survived", which the test refuses.
let g = kit.guest(
"membomb",
&Wat::new(
r#"(loop $l (br_if $l (i32.ne (memory.grow (i32.const 16)) (i32.const -1))))
(call $ok (i32.const 16) (i32.const 8))"#,
)
.extra(r#"(data (i32.const 16) "survived")"#)
.text(),
);
let o = g.run(b"", &none(), &tight());
assert_trapped(&o, "memory limit");
}
#[test]
fn a_memory_declared_larger_than_the_limit_is_refused_at_instantiation() {
let kit = Kit::new("bigmem");
// 512 pages = 32 MiB, over tight()'s 16 MiB.
let g = kit.guest(
"bigmem",
&Wat::new("(call $ok (i32.const 0) (i32.const 0))")
.pages(512)
.text(),
);
assert_trapped(&g.run(b"", &none(), &tight()), "memory limit");
// The same guest under a roomier limit runs.
let roomy = RunLimits {
memory_bytes: 64 << 20,
..tight()
};
assert!(g.run(b"", &none(), &roomy).verdict.is_accepted());
}
#[test]
fn a_table_bomb_is_stopped_at_the_table_limit() {
let kit = Kit::new("tablebomb");
let g = kit.guest(
"tablebomb",
&Wat::new(
r#"(loop $l (br_if $l (i32.ne (table.grow $t (ref.null func) (i32.const 10000)) (i32.const -1))))
(call $ok (i32.const 16) (i32.const 8))"#,
)
.extra(r#"(table $t 1 funcref) (data (i32.const 16) "survived")"#)
.text(),
);
assert_trapped(&g.run(b"", &none(), &tight()), "table limit");
}
#[test]
fn a_table_declared_larger_than_the_limit_is_refused_at_instantiation() {
let kit = Kit::new("bigtable");
let g = kit.guest(
"bigtable",
&Wat::new("(call $ok (i32.const 0) (i32.const 0))")
.extra("(table $t 5000000 funcref)")
.text(),
);
assert_trapped(&g.run(b"", &none(), &tight()), "table limit");
}
#[test]
fn deep_recursion_is_stopped_at_the_stack_limit() {
let kit = Kit::new("recursion");
let g = kit.guest(
"recursion",
&Wat::new("(call $deep (i32.const 0))")
.extra("(func $deep (param i32) (result i32) (call $deep (i32.add (local.get 0) (i32.const 1))))")
.text(),
);
let o = g.run(
b"",
&none(),
&RunLimits {
fuel: u64::MAX,
..RunLimits::default()
},
);
assert_trapped(&o, "stack overflow");
}
#[test]
fn an_input_larger_than_the_guest_memory_limit_traps_instead_of_crashing() {
let kit = Kit::new("big-input");
let g = kit.guest("good", &good_wat());
let input = vec![b'a'; 20 << 20];
let o = g.run(&input, &none(), &tight());
assert_trapped(&o, "memory limit");
}
#[test]
fn an_instance_limit_below_what_the_component_needs_refuses_the_run() {
let kit = Kit::new("instances");
let g = kit.guest("good", &good_wat());
let o = g.run(
b"x",
&none(),
&RunLimits {
instances: 0,
..RunLimits::default()
},
);
assert_trapped(&o, "instance");
}
#[test]
fn a_memory_count_limit_of_zero_refuses_the_run() {
let kit = Kit::new("memories");
let g = kit.guest("good", &good_wat());
let o = g.run(
b"x",
&none(),
&RunLimits {
memories: 0,
..RunLimits::default()
},
);
assert_trapped(&o, "memor");
}
#[test]
fn the_host_survives_a_hundred_bombs_in_a_row() {
let kit = Kit::new("hundred");
let bomb = kit.guest(
"membomb",
&Wat::new(
"(loop $l (br_if $l (i32.ne (memory.grow (i32.const 16)) (i32.const -1)))) unreachable",
)
.text(),
);
for _ in 0..100 {
assert_trapped(&bomb.run(b"", &none(), &tight()), "memory limit");
}
let good = kit.guest("good", &good_wat());
assert!(good.run(b"x", &none(), &tight()).verdict.is_accepted());
}