Skip to content

Commit 1a31914

Browse files
authored
Allow function resumption after running out of fuel (#1498)
* rename resumable call types * apply missing renaming * move common state into the new ResumableCallCommon type * deduplicate more potentially shared logic * update resumable call integration tests * add ResumableCall::OutOfFuel variant * re-export [Typed]ResumableCallOutOfFuel variants * move ResumableHostTrapError into resumable module * add ResumableOutOfFuelError type * add ResumableOutOfFuelError to Error * add ResumableOutOfFuelError::into_error method * extend FuelError with required_fuel info * fix broken doc links * integrate new OufOfFuel resumption into the engine * reformat code a bit * clean-up and refactor wast runner invoke * move prepare_results lower in file * merge impl blocks * add ResumableCallHostTrap::into_host_error method * extend Error::into_resumable method * make Wasmi Wast runner use resumable calls if fuel metering is enabled * apply rustfmt * remove unused parameter * apply rustfmt * update instruction pointer upon failure * no longer refuel with 6000 additional fuel on top * only update ip if there is at least one call frame left * preserve fuel when instantiating a module in Wast runner * only update ip if the error is out-of-fuel * apply clippy suggestion
1 parent 7777122 commit 1a31914

File tree

19 files changed

+820
-281
lines changed

19 files changed

+820
-281
lines changed

crates/core/src/fuel.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ pub enum FuelError {
179179
/// Returned by some [`Fuel`] methods when fuel metering is disabled.
180180
FuelMeteringDisabled,
181181
/// Raised when trying to consume more fuel than is available.
182-
OutOfFuel,
182+
OutOfFuel { required_fuel: u64 },
183183
}
184184

185185
impl Error for FuelError {}
@@ -188,7 +188,7 @@ impl fmt::Display for FuelError {
188188
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189189
match self {
190190
Self::FuelMeteringDisabled => write!(f, "fuel metering is disabled"),
191-
Self::OutOfFuel => write!(f, "all fuel consumed"),
191+
Self::OutOfFuel { required_fuel } => write!(f, "ouf of fuel. required={required_fuel}"),
192192
}
193193
}
194194
}
@@ -210,8 +210,8 @@ impl FuelError {
210210
///
211211
/// This method exists to indicate that this execution path is cold.
212212
#[cold]
213-
pub fn out_of_fuel() -> Self {
214-
Self::OutOfFuel
213+
pub fn out_of_fuel(required_fuel: u64) -> Self {
214+
Self::OutOfFuel { required_fuel }
215215
}
216216
}
217217

@@ -295,7 +295,7 @@ impl Fuel {
295295
self.remaining = self
296296
.remaining
297297
.checked_sub(delta)
298-
.ok_or(FuelError::OutOfFuel)?;
298+
.ok_or(FuelError::out_of_fuel(delta))?;
299299
Ok(self.remaining)
300300
}
301301

crates/core/src/limiter.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub enum LimiterError {
1515
/// Returned if a [`ResourceLimiter`] denies allocation or growth.
1616
ResourceLimiterDeniedAllocation,
1717
/// Encountered when an operation ran out of fuel.
18-
OutOfFuel,
18+
OutOfFuel { required_fuel: u64 },
1919
}
2020

2121
impl Error for LimiterError {}
@@ -26,7 +26,9 @@ impl Display for LimiterError {
2626
LimiterError::OutOfSystemMemory => "out of system memory",
2727
LimiterError::OutOfBoundsGrowth => "out of bounds growth",
2828
LimiterError::ResourceLimiterDeniedAllocation => "resource limiter denied allocation",
29-
LimiterError::OutOfFuel => "out of fuel",
29+
LimiterError::OutOfFuel { required_fuel } => {
30+
return write!(f, "not enough fuel. required={required_fuel}")
31+
}
3032
};
3133
write!(f, "{message}")
3234
}
@@ -38,7 +40,7 @@ impl From<MemoryError> for LimiterError {
3840
MemoryError::OutOfSystemMemory => Self::OutOfSystemMemory,
3941
MemoryError::OutOfBoundsGrowth => Self::OutOfBoundsGrowth,
4042
MemoryError::ResourceLimiterDeniedAllocation => Self::ResourceLimiterDeniedAllocation,
41-
MemoryError::OutOfFuel => Self::OutOfFuel,
43+
MemoryError::OutOfFuel { required_fuel } => Self::OutOfFuel { required_fuel },
4244
error => panic!("unexpected `MemoryError`: {error}"),
4345
}
4446
}
@@ -53,7 +55,7 @@ impl From<TableError> for LimiterError {
5355
| TableError::FillOutOfBounds
5456
| TableError::InitOutOfBounds => Self::OutOfBoundsGrowth,
5557
TableError::ResourceLimiterDeniedAllocation => Self::ResourceLimiterDeniedAllocation,
56-
TableError::OutOfFuel => Self::OutOfFuel,
58+
TableError::OutOfFuel { required_fuel } => Self::OutOfFuel { required_fuel },
5759
error => panic!("unexpected `TableError`: {error}"),
5860
}
5961
}

crates/core/src/memory/error.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub enum MemoryError {
2121
/// The maximum size of the memory type overflows the system index type.
2222
MaximumSizeOverflow,
2323
/// Encountered if a `memory.grow` operation runs out of fuel.
24-
OutOfFuel,
24+
OutOfFuel { required_fuel: u64 },
2525
}
2626

2727
impl Error for MemoryError {}
@@ -45,7 +45,9 @@ impl Display for MemoryError {
4545
Self::MaximumSizeOverflow => {
4646
"the maximum size of the memory type overflows the system index type"
4747
}
48-
Self::OutOfFuel => "out of fuel",
48+
Self::OutOfFuel { required_fuel } => {
49+
return write!(f, "not enough fuel. required={required_fuel}")
50+
}
4951
};
5052
write!(f, "{message}")
5153
}
@@ -57,7 +59,7 @@ impl From<LimiterError> for MemoryError {
5759
LimiterError::OutOfSystemMemory => Self::OutOfSystemMemory,
5860
LimiterError::OutOfBoundsGrowth => Self::OutOfBoundsGrowth,
5961
LimiterError::ResourceLimiterDeniedAllocation => Self::ResourceLimiterDeniedAllocation,
60-
LimiterError::OutOfFuel => Self::OutOfFuel,
62+
LimiterError::OutOfFuel { required_fuel } => Self::OutOfFuel { required_fuel },
6163
}
6264
}
6365
}

crates/core/src/memory/mod.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub use self::{
2121
error::MemoryError,
2222
ty::{MemoryType, MemoryTypeBuilder},
2323
};
24-
use crate::{Fuel, ResourceLimiterRef};
24+
use crate::{Fuel, FuelError, ResourceLimiterRef};
2525

2626
#[cfg(feature = "simd")]
2727
pub use self::access::ExtendInto;
@@ -237,11 +237,10 @@ impl Memory {
237237
let additional_bytes = additional
238238
.checked_mul(bytes_per_page)
239239
.expect("additional size is within [min, max) page bounds");
240-
if fuel
241-
.consume_fuel_if(|costs| costs.fuel_for_copying_bytes(additional_bytes))
242-
.is_err()
240+
if let Err(FuelError::OutOfFuel { required_fuel }) =
241+
fuel.consume_fuel_if(|costs| costs.fuel_for_copying_bytes(additional_bytes))
243242
{
244-
return notify_limiter(limiter, MemoryError::OutOfFuel);
243+
return notify_limiter(limiter, MemoryError::OutOfFuel { required_fuel });
245244
}
246245
}
247246
// At this point all checks passed to grow the linear memory:

crates/core/src/table/error.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub enum TableError {
2929
/// Occurs when operating with a [`Table`](crate::Table) and mismatching element types.
3030
ElementTypeMismatch,
3131
/// The operation ran out of fuel before completion.
32-
OutOfFuel,
32+
OutOfFuel { required_fuel: u64 },
3333
}
3434

3535
impl Error for TableError {}
@@ -51,7 +51,9 @@ impl Display for TableError {
5151
Self::CopyOutOfBounds => "out of bounds table access: `table.copy`",
5252
Self::SetOutOfBounds => "out of bounds table access: `table.set`",
5353
Self::ElementTypeMismatch => "encountered mismatching table element type",
54-
Self::OutOfFuel => "out of fuel",
54+
Self::OutOfFuel { required_fuel } => {
55+
return write!(f, "not enough fuel: required={required_fuel}")
56+
}
5557
};
5658
write!(f, "{message}")
5759
}
@@ -63,15 +65,15 @@ impl From<LimiterError> for TableError {
6365
LimiterError::OutOfSystemMemory => Self::OutOfSystemMemory,
6466
LimiterError::OutOfBoundsGrowth => Self::GrowOutOfBounds,
6567
LimiterError::ResourceLimiterDeniedAllocation => Self::ResourceLimiterDeniedAllocation,
66-
LimiterError::OutOfFuel => Self::OutOfFuel,
68+
LimiterError::OutOfFuel { required_fuel } => Self::OutOfFuel { required_fuel },
6769
}
6870
}
6971
}
7072

7173
impl From<FuelError> for TableError {
7274
fn from(error: FuelError) -> Self {
7375
match error {
74-
FuelError::OutOfFuel => Self::OutOfFuel,
76+
FuelError::OutOfFuel { required_fuel } => Self::OutOfFuel { required_fuel },
7577
FuelError::FuelMeteringDisabled => panic!("fuel metering is disabled"),
7678
}
7779
}

crates/core/src/table/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ impl Table {
170170
if let Some(fuel) = fuel {
171171
match fuel.consume_fuel(|costs| costs.fuel_for_copying_values(delta)) {
172172
Ok(_) | Err(FuelError::FuelMeteringDisabled) => {}
173-
Err(FuelError::OutOfFuel) => return notify_limiter(limiter, TableError::OutOfFuel),
173+
Err(FuelError::OutOfFuel { required_fuel }) => {
174+
return notify_limiter(limiter, TableError::OutOfFuel { required_fuel })
175+
}
174176
}
175177
}
176178
if self.elements.try_reserve(delta_size).is_err() {

crates/wasmi/src/engine/code_map.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
use super::{FuncTranslationDriver, FuncTranslator, TranslationError, ValidatingFuncTranslator};
99
use crate::{
1010
collections::arena::{Arena, ArenaIndex},
11-
core::{Fuel, FuelCostsProvider, FuelError, TrapCode, UntypedVal},
12-
engine::utils::unreachable_unchecked,
11+
core::{Fuel, FuelCostsProvider, FuelError, UntypedVal},
12+
engine::{utils::unreachable_unchecked, ResumableOutOfFuelError},
1313
ir::{index::InternalFunc, Instruction},
1414
module::{FuncIdx, ModuleHeader},
1515
Config,
@@ -628,7 +628,9 @@ impl UncompiledFuncEntity {
628628
};
629629
if let Some(fuel) = fuel {
630630
match fuel.consume_fuel(compilation_fuel) {
631-
Err(FuelError::OutOfFuel) => return Err(Error::from(TrapCode::OutOfFuel)),
631+
Err(FuelError::OutOfFuel { required_fuel }) => {
632+
return Err(Error::from(ResumableOutOfFuelError::new(required_fuel)))
633+
}
632634
Ok(_) | Err(FuelError::FuelMeteringDisabled) => {}
633635
}
634636
}

crates/wasmi/src/engine/executor/instrs.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub use self::call::{dispatch_host_func, ResumableHostError};
1+
pub use self::call::dispatch_host_func;
22
use super::{cache::CachedInstance, InstructionPtr, Stack};
33
use crate::{
44
core::{hint, wasm, ReadAs, TrapCode, UntypedVal, WriteAs},
@@ -70,7 +70,18 @@ pub fn execute_instrs<'engine>(
7070
) -> Result<(), Error> {
7171
let instance = stack.calls.instance_expect();
7272
let cache = CachedInstance::new(store.inner_mut(), instance);
73-
Executor::new(stack, code_map, cache).execute(store)
73+
let mut executor = Executor::new(stack, code_map, cache);
74+
if let Err(error) = executor.execute(store) {
75+
if error.is_out_of_fuel() {
76+
if let Some(frame) = executor.stack.calls.peek_mut() {
77+
// Note: we need to update the instruction pointer to make it possible to
78+
// resume execution at the current instruction after running out of fuel.
79+
frame.update_instr_ptr(executor.ip);
80+
}
81+
}
82+
return Err(error);
83+
}
84+
Ok(())
7485
}
7586

7687
/// An execution context for executing a Wasmi function frame.
@@ -118,7 +129,7 @@ impl<'engine> Executor<'engine> {
118129

119130
/// Executes the function frame until it returns or traps.
120131
#[inline(always)]
121-
fn execute(mut self, store: &mut PrunedStore) -> Result<(), Error> {
132+
fn execute(&mut self, store: &mut PrunedStore) -> Result<(), Error> {
122133
use Instruction as Instr;
123134
loop {
124135
match *self.ip.get() {

crates/wasmi/src/engine/executor/instrs/call.rs

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
utils::unreachable_unchecked,
88
EngineFunc,
99
FuncInOut,
10+
ResumableHostTrapError,
1011
},
1112
func::{FuncEntity, HostFuncEntity},
1213
ir::{index, Instruction, Reg, RegSpan},
@@ -16,7 +17,7 @@ use crate::{
1617
FuncRef,
1718
Instance,
1819
};
19-
use core::{array, fmt};
20+
use core::array;
2021

2122
/// Dispatches and executes the host function.
2223
///
@@ -63,52 +64,6 @@ pub enum CallKind {
6364
Tail,
6465
}
6566

66-
/// Error returned from a called host function in a resumable state.
67-
#[derive(Debug)]
68-
pub struct ResumableHostError {
69-
/// The error returned by the called host function.
70-
host_error: Error,
71-
/// The host function that returned the error.
72-
host_func: Func,
73-
/// The result registers of the caller of the host function.
74-
caller_results: RegSpan,
75-
}
76-
77-
impl core::error::Error for ResumableHostError {}
78-
79-
impl fmt::Display for ResumableHostError {
80-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81-
self.host_error.fmt(f)
82-
}
83-
}
84-
85-
impl ResumableHostError {
86-
/// Creates a new [`ResumableHostError`].
87-
#[cold]
88-
pub(crate) fn new(host_error: Error, host_func: Func, caller_results: RegSpan) -> Self {
89-
Self {
90-
host_error,
91-
host_func,
92-
caller_results,
93-
}
94-
}
95-
96-
/// Consumes `self` to return the underlying [`Error`].
97-
pub(crate) fn into_error(self) -> Error {
98-
self.host_error
99-
}
100-
101-
/// Returns the [`Func`] of the [`ResumableHostError`].
102-
pub(crate) fn host_func(&self) -> &Func {
103-
&self.host_func
104-
}
105-
106-
/// Returns the caller results [`RegSpan`] of the [`ResumableHostError`].
107-
pub(crate) fn caller_results(&self) -> &RegSpan {
108-
&self.caller_results
109-
}
110-
}
111-
11267
trait CallContext {
11368
const KIND: CallKind;
11469
const HAS_PARAMS: bool;
@@ -497,11 +452,11 @@ impl Executor<'_> {
497452
/// # Note
498453
///
499454
/// This uses the value stack to store parameters and results of the host function call.
500-
/// Returns an [`ErrorKind::ResumableHost`] variant if the host function returned an error
455+
/// Returns an [`ErrorKind::ResumableHostTrap`] variant if the host function returned an error
501456
/// and there are still call frames on the call stack making it possible to resume the
502457
/// execution at a later point in time.
503458
///
504-
/// [`ErrorKind::ResumableHost`]: crate::error::ErrorKind::ResumableHost
459+
/// [`ErrorKind::ResumableHostTrap`]: crate::error::ErrorKind::ResumableHostTrap
505460
fn execute_host_func<C: CallContext>(
506461
&mut self,
507462
store: &mut PrunedStore,
@@ -535,7 +490,7 @@ impl Executor<'_> {
535490
self.dispatch_host_func(store, host_func, &instance)
536491
.map_err(|error| match self.stack.calls.is_empty() {
537492
true => error,
538-
false => ResumableHostError::new(error, *func, results).into(),
493+
false => ResumableHostTrapError::new(error, *func, results).into(),
539494
})?;
540495
self.cache.update(store.inner_mut(), &instance);
541496
let results = results.iter(len_results);

crates/wasmi/src/engine/executor/instrs/memory.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::{Executor, InstructionPtr};
22
use crate::{
33
core::{MemoryError, ResourceLimiterRef, TrapCode},
4-
engine::utils::unreachable_unchecked,
4+
engine::{utils::unreachable_unchecked, ResumableOutOfFuelError},
55
ir::{
66
index::{Data, Memory},
77
Const16,
@@ -120,14 +120,15 @@ impl Executor<'_> {
120120
unsafe { self.cache.update_memory(store) };
121121
return_value
122122
}
123-
Err(
124-
MemoryError::OutOfBoundsGrowth
125-
| MemoryError::OutOfFuel
126-
| MemoryError::OutOfSystemMemory,
127-
) => match memory.ty().is_64() {
128-
true => u64::MAX,
129-
false => u64::from(u32::MAX),
130-
},
123+
Err(MemoryError::OutOfBoundsGrowth | MemoryError::OutOfSystemMemory) => {
124+
match memory.ty().is_64() {
125+
true => u64::MAX,
126+
false => u64::from(u32::MAX),
127+
}
128+
}
129+
Err(MemoryError::OutOfFuel { required_fuel }) => {
130+
return Err(Error::from(ResumableOutOfFuelError::new(required_fuel)))
131+
}
131132
Err(MemoryError::ResourceLimiterDeniedAllocation) => {
132133
return Err(Error::from(TrapCode::GrowthOperationLimited))
133134
}

0 commit comments

Comments
 (0)