Skip to content

Commit c6b2cd5

Browse files
committed
Fix lazy translation when running into out of fuel (#1648)
* extend fuel_metering.rs integration tests * add FuncEntity::set_uncompiled method This allows to go from Compiling state back to Uncompiled state. * rename bindings and no longer use mem::take * rename param: entity -> uncompiled * differentiate failed lazy compilation from out-of-fuel
1 parent c329218 commit c6b2cd5

File tree

2 files changed

+79
-29
lines changed

2 files changed

+79
-29
lines changed

crates/wasmi/src/engine/code_map.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{
1515
module::{FuncIdx, ModuleHeader},
1616
Config,
1717
Error,
18+
TrapCode,
1819
};
1920
use alloc::boxed::Box;
2021
use core::{
@@ -366,11 +367,11 @@ impl CodeMap {
366367
&'a self,
367368
fuel: Option<&mut Fuel>,
368369
func: EngineFunc,
369-
mut entity: UncompiledFuncEntity,
370+
mut uncompiled: UncompiledFuncEntity,
370371
) -> Result<CompiledFuncRef<'a>, Error> {
371372
// Note: it is important that compilation happens without locking the `CodeMap`
372373
// since compilation can take a prolonged time.
373-
let compiled_func = entity.compile(fuel, &self.features);
374+
let compiled_func = uncompiled.compile(fuel, &self.features);
374375
let mut funcs = self.funcs.lock();
375376
let Some(entity) = funcs.get_mut(func) else {
376377
panic!("encountered invalid internal function: {func:?}")
@@ -380,6 +381,10 @@ impl CodeMap {
380381
let cref = entity.set_compiled(compiled_func);
381382
Ok(self.adjust_cref_lifetime(cref))
382383
}
384+
Err(error) if error.as_trap_code() == Some(TrapCode::OutOfFuel) => {
385+
entity.set_uncompiled(uncompiled);
386+
Err(error)
387+
}
383388
Err(error) => {
384389
entity.set_failed_to_compile();
385390
Err(error)
@@ -502,6 +507,28 @@ impl FuncEntity {
502507
}
503508
}
504509

510+
/// Sets the state back to [`UncompiledFuncEntity`] if possible.
511+
///
512+
/// # Panics
513+
///
514+
/// If the current state was not [`FuncEntity::Compiling`].
515+
#[inline]
516+
pub fn set_uncompiled(&mut self, uncompiled: UncompiledFuncEntity) {
517+
match mem::replace(self, FuncEntity::Uncompiled(uncompiled)) {
518+
Self::Compiling => {}
519+
unexpected => {
520+
// Safety: we just asserted that `self` must be an uncompiled function
521+
// since otherwise we would have returned `None` above.
522+
// Since this is a performance critical path we need to leave out this check.
523+
unsafe {
524+
unreachable_unchecked!(
525+
"can only set `Compiling` back to `UncompiledFuncEntity` but found: {unexpected:?}"
526+
)
527+
}
528+
}
529+
}
530+
}
531+
505532
/// Sets the [`FuncEntity`] as [`CompiledFuncEntity`].
506533
///
507534
/// Returns a [`CompiledFuncRef`] to the [`CompiledFuncEntity`].
@@ -617,10 +644,10 @@ impl UncompiledFuncEntity {
617644
VALIDATE_FUEL_PER_BYTE + COMPILE_FUEL_PER_BYTE;
618645

619646
let func_idx = self.func_index;
620-
let bytes = mem::take(&mut self.bytes);
647+
let wasm_bytes = self.bytes.as_slice();
621648
let needs_validation = self.validation.is_some();
622649
let compilation_fuel = |_costs: &FuelCostsProvider| {
623-
let len_bytes = bytes.as_slice().len() as u64;
650+
let len_bytes = wasm_bytes.len() as u64;
624651
let fuel_per_byte = match needs_validation {
625652
false => COMPILE_FUEL_PER_BYTE,
626653
true => VALIDATE_AND_COMPILE_FUEL_PER_BYTE,
@@ -655,7 +682,7 @@ impl UncompiledFuncEntity {
655682
};
656683
let validator = func_to_validate.into_validator(allocs.1);
657684
let translator = ValidatingFuncTranslator::new(validator, translator)?;
658-
let allocs = FuncTranslationDriver::new(0, &bytes[..], translator)?.translate(
685+
let allocs = FuncTranslationDriver::new(0, wasm_bytes, translator)?.translate(
659686
|compiled_func| {
660687
result.write(compiled_func);
661688
},
@@ -665,7 +692,7 @@ impl UncompiledFuncEntity {
665692
None => {
666693
let allocs = engine.get_translation_allocs();
667694
let translator = FuncTranslator::new(func_idx, module, allocs)?;
668-
let allocs = FuncTranslationDriver::new(0, &bytes[..], translator)?.translate(
695+
let allocs = FuncTranslationDriver::new(0, wasm_bytes, translator)?.translate(
669696
|compiled_func| {
670697
result.write(compiled_func);
671698
},
Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
//! Tests to check if wasmi's fuel metering works as intended.
22
33
use std::fmt::Debug;
4-
use wasmi::{Config, Engine, Error, Func, Linker, Module, Store, TrapCode};
4+
use wasmi::{CompilationMode, Config, Engine, Error, Func, Linker, Module, Store, TrapCode};
55

66
/// Setup [`Engine`] and [`Store`] for fuel metering.
7-
fn test_setup() -> (Store<()>, Linker<()>) {
7+
fn test_setup(mode: CompilationMode) -> (Store<()>, Linker<()>) {
88
let mut config = Config::default();
99
config.consume_fuel(true);
10-
config.compilation_mode(wasmi::CompilationMode::Eager);
10+
config.compilation_mode(mode);
1111
let engine = Engine::new(&config);
1212
let store = Store::new(&engine, ());
1313
let linker = Linker::new(&engine);
@@ -24,8 +24,8 @@ fn create_module(store: &Store<()>, bytes: &[u8]) -> Module {
2424
}
2525

2626
/// Setup [`Store`] and [`Instance`] for fuel metering.
27-
fn default_test_setup(wasm: &[u8]) -> (Store<()>, Func) {
28-
let (mut store, linker) = test_setup();
27+
fn default_test_setup(mode: CompilationMode, wasm: &[u8]) -> (Store<()>, Func) {
28+
let (mut store, linker) = test_setup(mode);
2929
let module = create_module(&store, wasm);
3030
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
3131
let func = instance.get_func(&store, "test").unwrap();
@@ -38,37 +38,45 @@ fn default_test_setup(wasm: &[u8]) -> (Store<()>, Func) {
3838
///
3939
/// We just check if the call succeeded, not if the results are correct.
4040
/// That is to be determined by another kind of test.
41+
#[track_caller]
4142
fn assert_success<T>(call_result: Result<T, Error>)
4243
where
4344
T: Debug,
4445
{
45-
assert!(call_result.is_ok());
46+
if let Err(error) = call_result {
47+
panic!("expected `Ok` but got: {error}")
48+
}
4649
}
4750

4851
/// Asserts that the call trapped with [`TrapCode::OutOfFuel`].
52+
#[track_caller]
4953
fn assert_out_of_fuel<T>(call_result: Result<T, Error>)
5054
where
5155
T: Debug,
5256
{
53-
assert!(matches!(
54-
call_result.unwrap_err().as_trap_code(),
55-
Some(TrapCode::OutOfFuel)
56-
));
57+
let Err(error) = call_result else {
58+
panic!("expected `out of fuel` error but got `Ok`")
59+
};
60+
assert_eq!(
61+
error.as_trap_code(),
62+
Some(TrapCode::OutOfFuel),
63+
"expected `out of fuel` but got: {error}"
64+
);
5765
}
5866

59-
#[test]
60-
fn metered_i32_add() {
61-
let wasm = r#"
62-
(module
63-
(func (export "test") (param $a i32) (param $b i32) (result i32)
64-
(i32.add
65-
(local.get $a)
66-
(local.get $b)
67-
)
67+
const WASM_INPUT: &str = r#"
68+
(module
69+
(func (export "test") (param $a i32) (param $b i32) (result i32)
70+
(i32.add
71+
(local.get $a)
72+
(local.get $b)
6873
)
6974
)
70-
"#;
71-
let (mut store, func) = default_test_setup(wasm.as_bytes());
75+
)
76+
"#;
77+
78+
fn run_test(mode: CompilationMode, final_fuel: u64) {
79+
let (mut store, func) = default_test_setup(mode, WASM_INPUT.as_bytes());
7280
let func = func.typed::<(i32, i32), i32>(&store).unwrap();
7381
// No fuel -> no success.
7482
assert_out_of_fuel(func.call(&mut store, (1, 2)));
@@ -78,7 +86,22 @@ fn metered_i32_add() {
7886
assert_out_of_fuel(func.call(&mut store, (1, 2)));
7987
assert_eq!(store.get_fuel().ok(), Some(1));
8088
// Now add enough fuel, so execution should succeed.
81-
store.set_fuel(10).unwrap();
89+
store.set_fuel(100).unwrap();
8290
assert_success(func.call(&mut store, (1, 2)));
83-
assert_eq!(store.get_fuel().ok(), Some(7));
91+
assert_eq!(store.get_fuel().ok(), Some(final_fuel));
92+
}
93+
94+
#[test]
95+
fn metered_i32_add_eager() {
96+
run_test(CompilationMode::Eager, 97)
97+
}
98+
99+
#[test]
100+
fn metered_i32_add_lazy_translation() {
101+
run_test(CompilationMode::LazyTranslation, 48)
102+
}
103+
104+
#[test]
105+
fn metered_i32_add_lazy() {
106+
run_test(CompilationMode::Lazy, 34)
84107
}

0 commit comments

Comments
 (0)