use anyhow::Result; use wasmtime::*; use wast::parser::{self, Parse, ParseBuffer, Parser}; use wast::token::Span; mod kw { wast::custom_keyword!(assert_fuel); } struct FuelWast<'a> { assertions: Vec<(Span, u64, wast::core::Module<'a>)>, } impl<'a> Parse<'a> for FuelWast<'a> { fn parse(parser: Parser<'a>) -> parser::Result { let mut assertions = Vec::new(); while !parser.is_empty() { assertions.push(parser.parens(|p| { let span = p.parse::()?.0; Ok((span, p.parse()?, p.parens(|p| p.parse())?)) })?); } Ok(FuelWast { assertions }) } } #[test] #[cfg_attr(miri, ignore)] fn run() -> Result<()> { let test = std::fs::read_to_string("tests/all/fuel.wast")?; let buf = ParseBuffer::new(&test)?; let mut wast = parser::parse::>(&buf)?; for (span, fuel, module) in wast.assertions.iter_mut() { let consumed = fuel_consumed(&module.encode()?); if consumed == *fuel { continue; } let (line, col) = span.linecol_in(&test); panic!( "tests/all/fuel.wast:{}:{} - expected {} fuel, found {}", line + 1, col + 1, fuel, consumed ); } Ok(()) } fn fuel_consumed(wasm: &[u8]) -> u64 { let mut config = Config::new(); config.consume_fuel(true); let engine = Engine::new(&config).unwrap(); let module = Module::new(&engine, wasm).unwrap(); let mut store = Store::new(&engine, ()); store.add_fuel(u64::max_value()).unwrap(); drop(Instance::new(&mut store, &module, &[])); store.fuel_consumed().unwrap() } #[test] #[cfg_attr(miri, ignore)] fn iloop() { iloop_aborts( r#" (module (start 0) (func loop br 0 end) ) "#, ); iloop_aborts( r#" (module (start 0) (func loop i32.const 1 br_if 0 end) ) "#, ); iloop_aborts( r#" (module (start 0) (func loop i32.const 0 br_table 0 end) ) "#, ); iloop_aborts( r#" (module (start 0) (func $f0 call $f1 call $f1) (func $f1 call $f2 call $f2) (func $f2 call $f3 call $f3) (func $f3 call $f4 call $f4) (func $f4 call $f5 call $f5) (func $f5 call $f6 call $f6) (func $f6 call $f7 call $f7) (func $f7 call $f8 call $f8) (func $f8 call $f9 call $f9) (func $f9 call $f10 call $f10) (func $f10 call $f11 call $f11) (func $f11 call $f12 call $f12) (func $f12 call $f13 call $f13) (func $f13 call $f14 call $f14) (func $f14 call $f15 call $f15) (func $f15 call $f16 call $f16) (func $f16) ) "#, ); fn iloop_aborts(wat: &str) { let mut config = Config::new(); config.consume_fuel(true); let engine = Engine::new(&config).unwrap(); let module = Module::new(&engine, wat).unwrap(); let mut store = Store::new(&engine, ()); store.add_fuel(10_000).unwrap(); let error = Instance::new(&mut store, &module, &[]).err().unwrap(); assert_eq!(error.downcast::().unwrap(), Trap::OutOfFuel); } } #[test] fn manual_fuel() { let mut config = Config::new(); config.consume_fuel(true); let engine = Engine::new(&config).unwrap(); let mut store = Store::new(&engine, ()); store.add_fuel(10_000).unwrap(); assert_eq!(store.fuel_consumed(), Some(0)); assert_eq!(store.fuel_remaining(), Some(10_000)); assert_eq!(store.consume_fuel(1).unwrap(), 9_999); assert_eq!(store.fuel_consumed(), Some(1)); assert_eq!(store.fuel_remaining(), Some(9_999)); assert!(store.consume_fuel(10_000).is_err()); assert_eq!(store.consume_fuel(999).unwrap(), 9_000); assert!(store.consume_fuel(10_000).is_err()); assert_eq!(store.consume_fuel(8998).unwrap(), 2); assert!(store.consume_fuel(3).is_err()); assert_eq!(store.consume_fuel(1).unwrap(), 1); assert_eq!(store.consume_fuel(1).unwrap(), 0); assert_eq!(store.consume_fuel(0).unwrap(), 0); assert_eq!(store.fuel_remaining(), Some(0)); } #[test] #[cfg_attr(miri, ignore)] fn host_function_consumes_all() { const FUEL: u64 = 10_000; let mut config = Config::new(); config.consume_fuel(true); let engine = Engine::new(&config).unwrap(); let module = Module::new( &engine, r#" (module (import "" "" (func)) (func (export "") call 0 call $other) (func $other)) "#, ) .unwrap(); let mut store = Store::new(&engine, ()); store.add_fuel(FUEL).unwrap(); let func = Func::wrap(&mut store, |mut caller: Caller<'_, ()>| { let consumed = caller.fuel_consumed().unwrap(); assert_eq!(caller.consume_fuel((FUEL - consumed) - 1).unwrap(), 1); }); let instance = Instance::new(&mut store, &module, &[func.into()]).unwrap(); let export = instance.get_typed_func::<(), ()>(&mut store, "").unwrap(); let trap = export.call(&mut store, ()).unwrap_err(); assert_eq!(trap.downcast::().unwrap(), Trap::OutOfFuel); } #[test] fn manual_edge_cases() { let mut config = Config::new(); config.consume_fuel(true); let engine = Engine::new(&config).unwrap(); let mut store = Store::new(&engine, ()); store.add_fuel(u64::MAX).unwrap(); assert_eq!(store.fuel_consumed(), Some(0)); assert!(store.consume_fuel(u64::MAX).is_err()); assert!(store.consume_fuel(i64::MAX as u64 + 1).is_err()); assert_eq!(store.consume_fuel(i64::MAX as u64).unwrap(), 0); } #[test] #[cfg_attr(miri, ignore)] fn unconditionally_trapping_memory_accesses_save_fuel_before_trapping() { let mut config = Config::new(); config.consume_fuel(true); config.static_memory_maximum_size(0x1_0000); let engine = Engine::new(&config).unwrap(); let module = Module::new( &engine, r#" (module (memory 1 1) (func (export "f") (param i32) (result i32) local.get 0 local.get 0 i32.add ;; This offset is larger than our memory max size and therefore ;; will unconditionally trap. i32.load8_s offset=0xffffffff)) "#, ) .unwrap(); let mut store = Store::new(&engine, ()); let init_fuel = 1_000; store.add_fuel(init_fuel).unwrap(); assert_eq!(init_fuel, store.fuel_remaining().unwrap()); let instance = Instance::new(&mut store, &module, &[]).unwrap(); let f = instance .get_typed_func::(&mut store, "f") .unwrap(); let trap = f.call(&mut store, 0).unwrap_err(); assert_eq!(trap.downcast::().unwrap(), Trap::MemoryOutOfBounds); // The `i32.add` consumed some fuel before the unconditionally trapping // memory access. let consumed_fuel = store.fuel_consumed().unwrap(); assert!(consumed_fuel > 0); assert_eq!(init_fuel, consumed_fuel + store.fuel_remaining().unwrap()); }