Gunnar

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