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