Gunnar

nordisk/heimdall

crates/heimdall-wasm/tests/cpu_limits.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! Spec §11 Wasm: fuel and timeout limits hold. A guest that never returns is
//! stopped by fuel, or by the epoch wall clock when fuel is plentiful, and the
//! host refuses it by name. Heimdall is a defensive sandbox.

mod common;
use common::*;
use heimdall_wasm::RunLimits;
use std::time::{Duration, Instant};

fn spin() -> String {
    Wat::new("(loop $l (br $l)) unreachable").text()
}

#[test]
fn an_infinite_loop_is_stopped_by_fuel() {
    let kit = Kit::new("fuel");
    let g = kit.guest("spin", &spin());
    let limits = RunLimits {
        fuel: 5_000_000,
        timeout: Duration::from_secs(60),
        ..RunLimits::default()
    };
    let t = Instant::now();
    let o = g.run(b"", &none(), &limits);
    assert_trapped(&o, "out of fuel");
    assert!(
        t.elapsed() < Duration::from_secs(30),
        "fuel did not stop it early: {:?}",
        t.elapsed()
    );
    assert_eq!(o.fuel_used, 5_000_000, "the whole budget should be spent");
}

#[test]
fn an_infinite_loop_is_stopped_by_the_wall_clock_when_fuel_is_plentiful() {
    let kit = Kit::new("epoch");
    let g = kit.guest("spin", &spin());
    let limits = RunLimits {
        fuel: u64::MAX,
        timeout: Duration::from_millis(300),
        ..RunLimits::default()
    };
    let t = Instant::now();
    let o = g.run(b"", &none(), &limits);
    let took = t.elapsed();
    assert_trapped(&o, "wall-clock timeout");
    assert!(
        took >= Duration::from_millis(250),
        "stopped before the timeout: {took:?}"
    );
    assert!(
        took < Duration::from_secs(5),
        "the timeout did not hold: {took:?}"
    );
}

#[test]
fn a_loop_inside_cabi_realloc_is_stopped_while_the_input_is_handed_over() {
    let kit = Kit::new("realloc-spin");
    let g = kit.guest(
        "realloc-spin",
        &Wat::new("(call $ok (i32.const 0) (i32.const 0))")
            .realloc_prelude("(loop $s (br $s))")
            .text(),
    );
    let o = g.run(
        b"some input",
        &none(),
        &RunLimits {
            fuel: 2_000_000,
            ..RunLimits::default()
        },
    );
    assert_trapped(&o, "out of fuel");
}

#[test]
fn a_fuel_limit_of_zero_refuses_before_any_guest_code_runs() {
    let kit = Kit::new("zero-fuel");
    let g = kit.guest("good", &good_wat());
    let o = g.run(
        b"x",
        &none(),
        &RunLimits {
            fuel: 0,
            ..RunLimits::default()
        },
    );
    assert_trapped(&o, "out of fuel");
}

#[test]
fn a_guest_within_its_fuel_is_not_disturbed_by_a_spinning_neighbour() {
    let kit = Kit::new("neighbour");
    let spinner = std::sync::Arc::new(kit.guest("spin", &spin()));
    let good = kit.guest("good", &good_wat());
    let s = spinner.clone();
    let h = std::thread::spawn(move || {
        s.run(
            b"",
            &none(),
            &RunLimits {
                fuel: u64::MAX,
                timeout: Duration::from_millis(800),
                ..RunLimits::default()
            },
        )
    });
    let o = good.run(b"ok", &none(), &RunLimits::default());
    assert!(o.verdict.is_accepted(), "{}", o.verdict);
    assert_trapped(&h.join().unwrap(), "wall-clock timeout");
}

#[test]
fn fuel_used_grows_with_the_work_done() {
    let kit = Kit::new("fuel-grows");
    let g = kit.guest("good", &good_wat());
    let small = g.run(&[b'a'; 10], &none(), &RunLimits::default()).fuel_used;
    let large = g
        .run(&vec![b'a'; 100_000], &none(), &RunLimits::default())
        .fuel_used;
    assert!(
        large > small,
        "copying 100 kB cost no more fuel ({large}) than 10 B ({small})"
    );
}