Implement limiting WebAssembly execution with fuel (#2611)
* Consume fuel during function execution This commit adds codegen infrastructure necessary to instrument wasm code to consume fuel as it executes. Currently nothing is really done with the fuel, but that'll come in later commits. The focus of this commit is to implement the codegen infrastructure necessary to consume fuel and account for fuel consumed correctly. * Periodically check remaining fuel in wasm JIT code This commit enables wasm code to periodically check to see if fuel has run out. When fuel runs out an intrinsic is called which can do what it needs to do in the result of fuel running out. For now a trap is thrown to have at least some semantics in synchronous stores, but another planned use for this feature is for asynchronous stores to periodically yield back to the host based on fuel running out. Checks for remaining fuel happen in the same locations as interrupt checks, which is to say the start of the function as well as loop headers. * Improve codegen by caching `*const VMInterrupts` The location of the shared interrupt value and fuel value is through a double-indirection on the vmctx (load through the vmctx and then load through that pointer). The second pointer in this chain, however, never changes, so we can alter codegen to account for this and remove some extraneous load instructions and hopefully reduce some register pressure even maybe. * Add tests fuel can abort infinite loops * More fuzzing with fuel Use fuel to time out modules in addition to time, using fuzz input to figure out which. * Update docs on trapping instructions * Fix doc links * Fix a fuzz test * Change setting fuel to adding fuel * Fix a doc link * Squelch some rustdoc warnings
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3138,6 +3138,7 @@ dependencies = [
|
|||||||
"wasmtime-wasi-crypto",
|
"wasmtime-wasi-crypto",
|
||||||
"wasmtime-wasi-nn",
|
"wasmtime-wasi-nn",
|
||||||
"wasmtime-wast",
|
"wasmtime-wast",
|
||||||
|
"wast 32.0.0",
|
||||||
"wat",
|
"wat",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3149,6 +3150,7 @@ dependencies = [
|
|||||||
"cranelift-entity",
|
"cranelift-entity",
|
||||||
"cranelift-frontend",
|
"cranelift-frontend",
|
||||||
"cranelift-wasm",
|
"cranelift-wasm",
|
||||||
|
"wasmparser",
|
||||||
"wasmtime-environ",
|
"wasmtime-environ",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ test-programs = { path = "crates/test-programs" }
|
|||||||
wasmtime-fuzzing = { path = "crates/fuzzing" }
|
wasmtime-fuzzing = { path = "crates/fuzzing" }
|
||||||
wasmtime-runtime = { path = "crates/runtime" }
|
wasmtime-runtime = { path = "crates/runtime" }
|
||||||
tracing-subscriber = "0.2.0"
|
tracing-subscriber = "0.2.0"
|
||||||
|
wast = "32.0.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.19"
|
anyhow = "1.0.19"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ static WASM_MAGIC: &[u8] = &[0x00, 0x61, 0x73, 0x6D];
|
|||||||
/// Harvest candidates for superoptimization from a Wasm or Clif file.
|
/// Harvest candidates for superoptimization from a Wasm or Clif file.
|
||||||
///
|
///
|
||||||
/// Candidates are emitted in Souper's text format:
|
/// Candidates are emitted in Souper's text format:
|
||||||
/// https://github.com/google/souper
|
/// <https://github.com/google/souper>
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
/// Specify an input file to be used. Use '-' for stdin.
|
/// Specify an input file to be used. Use '-' for stdin.
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
|
|||||||
.extend_from_slice(builder.block_params(loop_body));
|
.extend_from_slice(builder.block_params(loop_body));
|
||||||
|
|
||||||
builder.switch_to_block(loop_body);
|
builder.switch_to_block(loop_body);
|
||||||
environ.translate_loop_header(builder.cursor())?;
|
environ.translate_loop_header(builder)?;
|
||||||
}
|
}
|
||||||
Operator::If { ty } => {
|
Operator::If { ty } => {
|
||||||
let val = state.pop1();
|
let val = state.pop1();
|
||||||
|
|||||||
@@ -295,6 +295,12 @@ pub trait FuncEnvironment: TargetEnvironment {
|
|||||||
ReturnMode::NormalReturns
|
ReturnMode::NormalReturns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called after the locals for a function have been parsed, and the number
|
||||||
|
/// of variables defined by this function is provided.
|
||||||
|
fn after_locals(&mut self, num_locals_defined: usize) {
|
||||||
|
drop(num_locals_defined);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set up the necessary preamble definitions in `func` to access the global variable
|
/// Set up the necessary preamble definitions in `func` to access the global variable
|
||||||
/// identified by `index`.
|
/// identified by `index`.
|
||||||
///
|
///
|
||||||
@@ -637,7 +643,7 @@ pub trait FuncEnvironment: TargetEnvironment {
|
|||||||
///
|
///
|
||||||
/// This can be used to insert explicit interrupt or safepoint checking at
|
/// This can be used to insert explicit interrupt or safepoint checking at
|
||||||
/// the beginnings of loops.
|
/// the beginnings of loops.
|
||||||
fn translate_loop_header(&mut self, _pos: FuncCursor) -> WasmResult<()> {
|
fn translate_loop_header(&mut self, _builder: &mut FunctionBuilder) -> WasmResult<()> {
|
||||||
// By default, don't emit anything.
|
// By default, don't emit anything.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ fn parse_local_decls<FE: FuncEnvironment + ?Sized>(
|
|||||||
declare_locals(builder, count, ty, &mut next_local, environ)?;
|
declare_locals(builder, count, ty, &mut next_local, environ)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
environ.after_locals(next_local);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ cranelift-wasm = { path = "../../cranelift/wasm", version = "0.69.0" }
|
|||||||
cranelift-codegen = { path = "../../cranelift/codegen", version = "0.69.0" }
|
cranelift-codegen = { path = "../../cranelift/codegen", version = "0.69.0" }
|
||||||
cranelift-frontend = { path = "../../cranelift/frontend", version = "0.69.0" }
|
cranelift-frontend = { path = "../../cranelift/frontend", version = "0.69.0" }
|
||||||
cranelift-entity = { path = "../../cranelift/entity", version = "0.69.0" }
|
cranelift-entity = { path = "../../cranelift/entity", version = "0.69.0" }
|
||||||
|
wasmparser = "0.73.0"
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ use cranelift_codegen::ir::{AbiParam, ArgumentPurpose, Function, InstBuilder, Si
|
|||||||
use cranelift_codegen::isa::{self, TargetFrontendConfig};
|
use cranelift_codegen::isa::{self, TargetFrontendConfig};
|
||||||
use cranelift_entity::{EntityRef, PrimaryMap};
|
use cranelift_entity::{EntityRef, PrimaryMap};
|
||||||
use cranelift_frontend::FunctionBuilder;
|
use cranelift_frontend::FunctionBuilder;
|
||||||
|
use cranelift_frontend::Variable;
|
||||||
use cranelift_wasm::{
|
use cranelift_wasm::{
|
||||||
self, FuncIndex, GlobalIndex, GlobalVariable, MemoryIndex, SignatureIndex, TableIndex,
|
self, FuncIndex, FuncTranslationState, GlobalIndex, GlobalVariable, MemoryIndex,
|
||||||
TargetEnvironment, TypeIndex, WasmError, WasmResult, WasmType,
|
SignatureIndex, TableIndex, TargetEnvironment, TypeIndex, WasmError, WasmResult, WasmType,
|
||||||
};
|
};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
use std::mem;
|
||||||
|
use wasmparser::Operator;
|
||||||
use wasmtime_environ::{
|
use wasmtime_environ::{
|
||||||
BuiltinFunctionIndex, MemoryPlan, MemoryStyle, Module, TableStyle, Tunables, VMOffsets,
|
BuiltinFunctionIndex, MemoryPlan, MemoryStyle, Module, TableStyle, Tunables, VMOffsets,
|
||||||
INTERRUPTED, WASM_PAGE_SIZE,
|
INTERRUPTED, WASM_PAGE_SIZE,
|
||||||
@@ -125,6 +128,20 @@ pub struct FuncEnvironment<'module_environment> {
|
|||||||
pub(crate) offsets: VMOffsets,
|
pub(crate) offsets: VMOffsets,
|
||||||
|
|
||||||
tunables: &'module_environment Tunables,
|
tunables: &'module_environment Tunables,
|
||||||
|
|
||||||
|
/// A function-local variable which stores the cached value of the amount of
|
||||||
|
/// fuel remaining to execute. If used this is modified frequently so it's
|
||||||
|
/// stored locally as a variable instead of always referenced from the field
|
||||||
|
/// in `*const VMInterrupts`
|
||||||
|
fuel_var: cranelift_frontend::Variable,
|
||||||
|
|
||||||
|
/// A function-local variable which caches the value of `*const
|
||||||
|
/// VMInterrupts` for this function's vmctx argument. This pointer is stored
|
||||||
|
/// in the vmctx itself, but never changes for the lifetime of the function,
|
||||||
|
/// so if we load it up front we can continue to use it throughout.
|
||||||
|
vminterrupts_ptr: cranelift_frontend::Variable,
|
||||||
|
|
||||||
|
fuel_consumed: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'module_environment> FuncEnvironment<'module_environment> {
|
impl<'module_environment> FuncEnvironment<'module_environment> {
|
||||||
@@ -151,6 +168,12 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
|
|||||||
builtin_function_signatures,
|
builtin_function_signatures,
|
||||||
offsets: VMOffsets::new(target_config.pointer_bytes(), module),
|
offsets: VMOffsets::new(target_config.pointer_bytes(), module),
|
||||||
tunables,
|
tunables,
|
||||||
|
fuel_var: Variable::new(0),
|
||||||
|
vminterrupts_ptr: Variable::new(0),
|
||||||
|
|
||||||
|
// Start with at least one fuel being consumed because even empty
|
||||||
|
// functions should consume at least some fuel.
|
||||||
|
fuel_consumed: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +441,241 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
|
|||||||
(global, 0)
|
(global, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn declare_vminterrupts_ptr(&mut self, builder: &mut FunctionBuilder<'_>) {
|
||||||
|
// We load the `*const VMInterrupts` value stored within vmctx at the
|
||||||
|
// head of the function and reuse the same value across the entire
|
||||||
|
// function. This is possible since we know that the pointer never
|
||||||
|
// changes for the lifetime of the function.
|
||||||
|
let pointer_type = self.pointer_type();
|
||||||
|
builder.declare_var(self.vminterrupts_ptr, pointer_type);
|
||||||
|
let vmctx = self.vmctx(builder.func);
|
||||||
|
let base = builder.ins().global_value(pointer_type, vmctx);
|
||||||
|
let offset = i32::try_from(self.offsets.vmctx_interrupts()).unwrap();
|
||||||
|
let interrupt_ptr = builder
|
||||||
|
.ins()
|
||||||
|
.load(pointer_type, ir::MemFlags::trusted(), base, offset);
|
||||||
|
builder.def_var(self.vminterrupts_ptr, interrupt_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fuel_function_entry(&mut self, builder: &mut FunctionBuilder<'_>) {
|
||||||
|
// On function entry we load the amount of fuel into a function-local
|
||||||
|
// `self.fuel_var` to make fuel modifications fast locally. This cache
|
||||||
|
// is then periodically flushed to the Store-defined location in
|
||||||
|
// `VMInterrupts` later.
|
||||||
|
builder.declare_var(self.fuel_var, ir::types::I64);
|
||||||
|
self.fuel_load_into_var(builder);
|
||||||
|
self.fuel_check(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fuel_function_exit(&mut self, builder: &mut FunctionBuilder<'_>) {
|
||||||
|
// On exiting the function we need to be sure to save the fuel we have
|
||||||
|
// cached locally in `self.fuel_var` back into the Store-defined
|
||||||
|
// location.
|
||||||
|
self.fuel_save_from_var(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fuel_before_op(
|
||||||
|
&mut self,
|
||||||
|
op: &Operator<'_>,
|
||||||
|
builder: &mut FunctionBuilder<'_>,
|
||||||
|
reachable: bool,
|
||||||
|
) {
|
||||||
|
if !reachable {
|
||||||
|
// In unreachable code we shouldn't have any leftover fuel we
|
||||||
|
// haven't accounted for since the reason for us to become
|
||||||
|
// unreachable should have already added it to `self.fuel_var`.
|
||||||
|
debug_assert_eq!(self.fuel_consumed, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fuel_consumed += match op {
|
||||||
|
// Nop and drop generate no code, so don't consume fuel for them.
|
||||||
|
Operator::Nop | Operator::Drop => 0,
|
||||||
|
|
||||||
|
// Control flow may create branches, but is generally cheap and
|
||||||
|
// free, so don't consume fuel. Note the lack of `if` since some
|
||||||
|
// cost is incurred with the conditional check.
|
||||||
|
Operator::Block { .. }
|
||||||
|
| Operator::Loop { .. }
|
||||||
|
| Operator::Unreachable
|
||||||
|
| Operator::Return
|
||||||
|
| Operator::Else
|
||||||
|
| Operator::End => 0,
|
||||||
|
|
||||||
|
// everything else, just call it one operation.
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
match op {
|
||||||
|
// Exiting a function (via a return or unreachable) or otherwise
|
||||||
|
// entering a different function (via a call) means that we need to
|
||||||
|
// update the fuel consumption in `VMInterrupts` because we're
|
||||||
|
// about to move control out of this function itself and the fuel
|
||||||
|
// may need to be read.
|
||||||
|
//
|
||||||
|
// Before this we need to update the fuel counter from our own cost
|
||||||
|
// leading up to this function call, and then we can store
|
||||||
|
// `self.fuel_var` into `VMInterrupts`.
|
||||||
|
Operator::Unreachable
|
||||||
|
| Operator::Return
|
||||||
|
| Operator::CallIndirect { .. }
|
||||||
|
| Operator::Call { .. }
|
||||||
|
| Operator::ReturnCall { .. }
|
||||||
|
| Operator::ReturnCallIndirect { .. } => {
|
||||||
|
self.fuel_increment_var(builder);
|
||||||
|
self.fuel_save_from_var(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To ensure all code preceding a loop is only counted once we
|
||||||
|
// update the fuel variable on entry.
|
||||||
|
Operator::Loop { .. }
|
||||||
|
|
||||||
|
// Entering into an `if` block means that the edge we take isn't
|
||||||
|
// known until runtime, so we need to update our fuel consumption
|
||||||
|
// before we take the branch.
|
||||||
|
| Operator::If { .. }
|
||||||
|
|
||||||
|
// Control-flow instructions mean that we're moving to the end/exit
|
||||||
|
// of a block somewhere else. That means we need to update the fuel
|
||||||
|
// counter since we're effectively terminating our basic block.
|
||||||
|
| Operator::Br { .. }
|
||||||
|
| Operator::BrIf { .. }
|
||||||
|
| Operator::BrTable { .. }
|
||||||
|
|
||||||
|
// Exiting a scope means that we need to update the fuel
|
||||||
|
// consumption because there are multiple ways to exit a scope and
|
||||||
|
// this is the only time we have to account for instructions
|
||||||
|
// executed so far.
|
||||||
|
| Operator::End
|
||||||
|
|
||||||
|
// This is similar to `end`, except that it's only the terminator
|
||||||
|
// for an `if` block. The same reasoning applies though in that we
|
||||||
|
// are terminating a basic block and need to update the fuel
|
||||||
|
// variable.
|
||||||
|
| Operator::Else => self.fuel_increment_var(builder),
|
||||||
|
|
||||||
|
// This is a normal instruction where the fuel is buffered to later
|
||||||
|
// get added to `self.fuel_var`.
|
||||||
|
//
|
||||||
|
// Note that we generally ignore instructions which may trap and
|
||||||
|
// therefore result in exiting a block early. Current usage of fuel
|
||||||
|
// means that it's not too important to account for a precise amount
|
||||||
|
// of fuel consumed but rather "close to the actual amount" is good
|
||||||
|
// enough. For 100% precise counting, however, we'd probably need to
|
||||||
|
// not only increment but also save the fuel amount more often
|
||||||
|
// around trapping instructions. (see the `unreachable` instruction
|
||||||
|
// case above)
|
||||||
|
//
|
||||||
|
// Note that `Block` is specifically omitted from incrementing the
|
||||||
|
// fuel variable. Control flow entering a `block` is unconditional
|
||||||
|
// which means it's effectively executing straight-line code. We'll
|
||||||
|
// update the counter when exiting a block, but we shouldn't need to
|
||||||
|
// do so upon entering a block.
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fuel_after_op(&mut self, op: &Operator<'_>, builder: &mut FunctionBuilder<'_>) {
|
||||||
|
// After a function call we need to reload our fuel value since the
|
||||||
|
// function may have changed it.
|
||||||
|
match op {
|
||||||
|
Operator::Call { .. } | Operator::CallIndirect { .. } => {
|
||||||
|
self.fuel_load_into_var(builder);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds `self.fuel_consumed` to the `fuel_var`, zero-ing out the amount of
|
||||||
|
/// fuel consumed at that point.
|
||||||
|
fn fuel_increment_var(&mut self, builder: &mut FunctionBuilder<'_>) {
|
||||||
|
let consumption = mem::replace(&mut self.fuel_consumed, 0);
|
||||||
|
if consumption == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fuel = builder.use_var(self.fuel_var);
|
||||||
|
let fuel = builder.ins().iadd_imm(fuel, consumption);
|
||||||
|
builder.def_var(self.fuel_var, fuel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the fuel consumption value from `VMInterrupts` into `self.fuel_var`
|
||||||
|
fn fuel_load_into_var(&mut self, builder: &mut FunctionBuilder<'_>) {
|
||||||
|
let (addr, offset) = self.fuel_addr_offset(builder);
|
||||||
|
let fuel = builder
|
||||||
|
.ins()
|
||||||
|
.load(ir::types::I64, ir::MemFlags::trusted(), addr, offset);
|
||||||
|
builder.def_var(self.fuel_var, fuel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores the fuel consumption value from `self.fuel_var` into
|
||||||
|
/// `VMInterrupts`.
|
||||||
|
fn fuel_save_from_var(&mut self, builder: &mut FunctionBuilder<'_>) {
|
||||||
|
let (addr, offset) = self.fuel_addr_offset(builder);
|
||||||
|
let fuel_consumed = builder.use_var(self.fuel_var);
|
||||||
|
builder
|
||||||
|
.ins()
|
||||||
|
.store(ir::MemFlags::trusted(), fuel_consumed, addr, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `(address, offset)` of the fuel consumption within
|
||||||
|
/// `VMInterrupts`, used to perform loads/stores later.
|
||||||
|
fn fuel_addr_offset(
|
||||||
|
&mut self,
|
||||||
|
builder: &mut FunctionBuilder<'_>,
|
||||||
|
) -> (ir::Value, ir::immediates::Offset32) {
|
||||||
|
(
|
||||||
|
builder.use_var(self.vminterrupts_ptr),
|
||||||
|
i32::from(self.offsets.vminterrupts_fuel_consumed()).into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the amount of remaining, and if we've run out of fuel we call
|
||||||
|
/// the out-of-fuel function.
|
||||||
|
fn fuel_check(&mut self, builder: &mut FunctionBuilder) {
|
||||||
|
self.fuel_increment_var(builder);
|
||||||
|
let out_of_gas_block = builder.create_block();
|
||||||
|
let continuation_block = builder.create_block();
|
||||||
|
|
||||||
|
// Note that our fuel is encoded as adding positive values to a
|
||||||
|
// negative number. Whenever the negative number goes positive that
|
||||||
|
// means we ran out of fuel.
|
||||||
|
//
|
||||||
|
// Compare to see if our fuel is positive, and if so we ran out of gas.
|
||||||
|
// Otherwise we can continue on like usual.
|
||||||
|
let zero = builder.ins().iconst(ir::types::I64, 0);
|
||||||
|
let fuel = builder.use_var(self.fuel_var);
|
||||||
|
let cmp = builder.ins().ifcmp(fuel, zero);
|
||||||
|
builder
|
||||||
|
.ins()
|
||||||
|
.brif(IntCC::SignedGreaterThanOrEqual, cmp, out_of_gas_block, &[]);
|
||||||
|
builder.ins().jump(continuation_block, &[]);
|
||||||
|
builder.seal_block(out_of_gas_block);
|
||||||
|
|
||||||
|
// If we ran out of gas then we call our out-of-gas intrinsic and it
|
||||||
|
// figures out what to do. Note that this may raise a trap, or do
|
||||||
|
// something like yield to an async runtime. In either case we don't
|
||||||
|
// assume what happens and handle the case the intrinsic returns.
|
||||||
|
//
|
||||||
|
// Note that we save/reload fuel around this since the out-of-gas
|
||||||
|
// intrinsic may alter how much fuel is in the system.
|
||||||
|
builder.switch_to_block(out_of_gas_block);
|
||||||
|
self.fuel_save_from_var(builder);
|
||||||
|
let out_of_gas_sig = self.builtin_function_signatures.out_of_gas(builder.func);
|
||||||
|
let (vmctx, out_of_gas) = self.translate_load_builtin_function_address(
|
||||||
|
&mut builder.cursor(),
|
||||||
|
BuiltinFunctionIndex::out_of_gas(),
|
||||||
|
);
|
||||||
|
builder
|
||||||
|
.ins()
|
||||||
|
.call_indirect(out_of_gas_sig, out_of_gas, &[vmctx]);
|
||||||
|
self.fuel_load_into_var(builder);
|
||||||
|
builder.ins().jump(continuation_block, &[]);
|
||||||
|
builder.seal_block(continuation_block);
|
||||||
|
|
||||||
|
builder.switch_to_block(continuation_block);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'module_environment> TargetEnvironment for FuncEnvironment<'module_environment> {
|
impl<'module_environment> TargetEnvironment for FuncEnvironment<'module_environment> {
|
||||||
@@ -437,6 +695,11 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
|
|||||||
index >= 2
|
index >= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn after_locals(&mut self, num_locals: usize) {
|
||||||
|
self.vminterrupts_ptr = Variable::new(num_locals);
|
||||||
|
self.fuel_var = Variable::new(num_locals + 1);
|
||||||
|
}
|
||||||
|
|
||||||
fn make_table(&mut self, func: &mut ir::Function, index: TableIndex) -> WasmResult<ir::Table> {
|
fn make_table(&mut self, func: &mut ir::Function, index: TableIndex) -> WasmResult<ir::Table> {
|
||||||
let pointer_type = self.pointer_type();
|
let pointer_type = self.pointer_type();
|
||||||
|
|
||||||
@@ -1482,36 +1745,90 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
|
|||||||
Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
|
Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_loop_header(&mut self, mut pos: FuncCursor) -> WasmResult<()> {
|
fn translate_loop_header(&mut self, builder: &mut FunctionBuilder) -> WasmResult<()> {
|
||||||
if !self.tunables.interruptable {
|
// If enabled check the interrupt flag to prevent long or infinite
|
||||||
return Ok(());
|
// loops.
|
||||||
}
|
|
||||||
|
|
||||||
// Start out each loop with a check to the interupt flag to allow
|
|
||||||
// interruption of long or infinite loops.
|
|
||||||
//
|
//
|
||||||
// For more information about this see comments in
|
// For more information about this see comments in
|
||||||
// `crates/environ/src/cranelift.rs`
|
// `crates/environ/src/cranelift.rs`
|
||||||
let vmctx = self.vmctx(&mut pos.func);
|
if self.tunables.interruptable {
|
||||||
let pointer_type = self.pointer_type();
|
let pointer_type = self.pointer_type();
|
||||||
let base = pos.ins().global_value(pointer_type, vmctx);
|
let interrupt_ptr = builder.use_var(self.vminterrupts_ptr);
|
||||||
let offset = i32::try_from(self.offsets.vmctx_interrupts()).unwrap();
|
let interrupt = builder.ins().load(
|
||||||
let interrupt_ptr = pos
|
pointer_type,
|
||||||
.ins()
|
ir::MemFlags::trusted(),
|
||||||
.load(pointer_type, ir::MemFlags::trusted(), base, offset);
|
interrupt_ptr,
|
||||||
let interrupt = pos.ins().load(
|
i32::from(self.offsets.vminterrupts_stack_limit()),
|
||||||
pointer_type,
|
);
|
||||||
ir::MemFlags::trusted(),
|
// Note that the cast to `isize` happens first to allow sign-extension,
|
||||||
interrupt_ptr,
|
// if necessary, to `i64`.
|
||||||
i32::from(self.offsets.vminterrupts_stack_limit()),
|
let interrupted_sentinel = builder
|
||||||
);
|
.ins()
|
||||||
// Note that the cast to `isize` happens first to allow sign-extension,
|
.iconst(pointer_type, INTERRUPTED as isize as i64);
|
||||||
// if necessary, to `i64`.
|
let cmp = builder
|
||||||
let interrupted_sentinel = pos.ins().iconst(pointer_type, INTERRUPTED as isize as i64);
|
.ins()
|
||||||
let cmp = pos
|
.icmp(IntCC::Equal, interrupt, interrupted_sentinel);
|
||||||
.ins()
|
builder.ins().trapnz(cmp, ir::TrapCode::Interrupt);
|
||||||
.icmp(IntCC::Equal, interrupt, interrupted_sentinel);
|
}
|
||||||
pos.ins().trapnz(cmp, ir::TrapCode::Interrupt);
|
|
||||||
|
// Additionally if enabled check how much fuel we have remaining to see
|
||||||
|
// if we've run out by this point.
|
||||||
|
if self.tunables.consume_fuel {
|
||||||
|
self.fuel_check(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn before_translate_operator(
|
||||||
|
&mut self,
|
||||||
|
op: &Operator,
|
||||||
|
builder: &mut FunctionBuilder,
|
||||||
|
state: &FuncTranslationState,
|
||||||
|
) -> WasmResult<()> {
|
||||||
|
if self.tunables.consume_fuel {
|
||||||
|
self.fuel_before_op(op, builder, state.reachable());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_translate_operator(
|
||||||
|
&mut self,
|
||||||
|
op: &Operator,
|
||||||
|
builder: &mut FunctionBuilder,
|
||||||
|
state: &FuncTranslationState,
|
||||||
|
) -> WasmResult<()> {
|
||||||
|
if self.tunables.consume_fuel && state.reachable() {
|
||||||
|
self.fuel_after_op(op, builder);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn before_translate_function(
|
||||||
|
&mut self,
|
||||||
|
builder: &mut FunctionBuilder,
|
||||||
|
_state: &FuncTranslationState,
|
||||||
|
) -> WasmResult<()> {
|
||||||
|
// If the `vminterrupts_ptr` variable will get used then we initialize
|
||||||
|
// it here.
|
||||||
|
if self.tunables.consume_fuel || self.tunables.interruptable {
|
||||||
|
self.declare_vminterrupts_ptr(builder);
|
||||||
|
}
|
||||||
|
// Additionally we initialize `fuel_var` if it will get used.
|
||||||
|
if self.tunables.consume_fuel {
|
||||||
|
self.fuel_function_entry(builder);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_translate_function(
|
||||||
|
&mut self,
|
||||||
|
builder: &mut FunctionBuilder,
|
||||||
|
state: &FuncTranslationState,
|
||||||
|
) -> WasmResult<()> {
|
||||||
|
if self.tunables.consume_fuel && state.reachable() {
|
||||||
|
self.fuel_function_exit(builder);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ macro_rules! foreach_builtin_function {
|
|||||||
memory_atomic_wait64(vmctx, i32, i32, i64, i64) -> (i32);
|
memory_atomic_wait64(vmctx, i32, i32, i64, i64) -> (i32);
|
||||||
/// Returns an index for wasm's `memory.atomic.wait64` for imported memories.
|
/// Returns an index for wasm's `memory.atomic.wait64` for imported memories.
|
||||||
imported_memory_atomic_wait64(vmctx, i32, i32, i64, i64) -> (i32);
|
imported_memory_atomic_wait64(vmctx, i32, i32, i64, i64) -> (i32);
|
||||||
|
/// Invoked when fuel has run out while executing a function.
|
||||||
|
out_of_gas(vmctx) -> ();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ pub struct Tunables {
|
|||||||
/// calls and interrupts are implemented through the `VMInterrupts`
|
/// calls and interrupts are implemented through the `VMInterrupts`
|
||||||
/// structure, or `InterruptHandle` in the `wasmtime` crate.
|
/// structure, or `InterruptHandle` in the `wasmtime` crate.
|
||||||
pub interruptable: bool,
|
pub interruptable: bool,
|
||||||
|
|
||||||
|
/// Whether or not fuel is enabled for generated code, meaning that fuel
|
||||||
|
/// will be consumed every time a wasm instruction is executed.
|
||||||
|
pub consume_fuel: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Tunables {
|
impl Default for Tunables {
|
||||||
@@ -57,6 +61,7 @@ impl Default for Tunables {
|
|||||||
generate_native_debuginfo: false,
|
generate_native_debuginfo: false,
|
||||||
parse_wasm_debuginfo: true,
|
parse_wasm_debuginfo: true,
|
||||||
interruptable: false,
|
interruptable: false,
|
||||||
|
consume_fuel: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,6 +258,11 @@ impl VMOffsets {
|
|||||||
pub fn vminterrupts_stack_limit(&self) -> u8 {
|
pub fn vminterrupts_stack_limit(&self) -> u8 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the offset of the `fuel_consumed` field of `VMInterrupts`
|
||||||
|
pub fn vminterrupts_fuel_consumed(&self) -> u8 {
|
||||||
|
self.pointer_size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offsets for `VMCallerCheckedAnyfunc`.
|
/// Offsets for `VMCallerCheckedAnyfunc`.
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ pub struct Config {
|
|||||||
debug_info: bool,
|
debug_info: bool,
|
||||||
canonicalize_nans: bool,
|
canonicalize_nans: bool,
|
||||||
interruptable: bool,
|
interruptable: bool,
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub consume_fuel: bool,
|
||||||
|
|
||||||
// Note that we use 32-bit values here to avoid blowing the 64-bit address
|
// Note that we use 32-bit values here to avoid blowing the 64-bit address
|
||||||
// space by requesting ungodly-large sizes/guards.
|
// space by requesting ungodly-large sizes/guards.
|
||||||
@@ -82,7 +84,8 @@ impl Config {
|
|||||||
.dynamic_memory_guard_size(self.dynamic_memory_guard_size.unwrap_or(0).into())
|
.dynamic_memory_guard_size(self.dynamic_memory_guard_size.unwrap_or(0).into())
|
||||||
.cranelift_nan_canonicalization(self.canonicalize_nans)
|
.cranelift_nan_canonicalization(self.canonicalize_nans)
|
||||||
.cranelift_opt_level(self.opt_level.to_wasmtime())
|
.cranelift_opt_level(self.opt_level.to_wasmtime())
|
||||||
.interruptable(self.interruptable);
|
.interruptable(self.interruptable)
|
||||||
|
.consume_fuel(self.consume_fuel);
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,20 @@ fn log_wasm(wasm: &[u8]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Methods of timing out execution of a WebAssembly module
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Timeout {
|
||||||
|
/// No timeout is used, it should be guaranteed via some other means that
|
||||||
|
/// the input does not infinite loop.
|
||||||
|
None,
|
||||||
|
/// A time-based timeout is used with a sleeping thread sending a signal
|
||||||
|
/// after the specified duration.
|
||||||
|
Time(Duration),
|
||||||
|
/// Fuel-based timeouts are used where the specified fuel is all that the
|
||||||
|
/// provided wasm module is allowed to consume.
|
||||||
|
Fuel(u64),
|
||||||
|
}
|
||||||
|
|
||||||
/// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected
|
/// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected
|
||||||
/// panic or segfault or anything else that can be detected "passively".
|
/// panic or segfault or anything else that can be detected "passively".
|
||||||
///
|
///
|
||||||
@@ -51,7 +65,7 @@ pub fn instantiate(wasm: &[u8], known_valid: bool, strategy: Strategy) {
|
|||||||
// pre-module-linking modules due to imports
|
// pre-module-linking modules due to imports
|
||||||
let mut cfg = crate::fuzz_default_config(strategy).unwrap();
|
let mut cfg = crate::fuzz_default_config(strategy).unwrap();
|
||||||
cfg.wasm_module_linking(false);
|
cfg.wasm_module_linking(false);
|
||||||
instantiate_with_config(wasm, known_valid, cfg, None);
|
instantiate_with_config(wasm, known_valid, cfg, Timeout::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected
|
/// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected
|
||||||
@@ -64,28 +78,38 @@ pub fn instantiate_with_config(
|
|||||||
wasm: &[u8],
|
wasm: &[u8],
|
||||||
known_valid: bool,
|
known_valid: bool,
|
||||||
mut config: Config,
|
mut config: Config,
|
||||||
timeout: Option<Duration>,
|
timeout: Timeout,
|
||||||
) {
|
) {
|
||||||
crate::init_fuzzing();
|
crate::init_fuzzing();
|
||||||
|
|
||||||
config.interruptable(timeout.is_some());
|
config.interruptable(match &timeout {
|
||||||
|
Timeout::Time(_) => true,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
config.consume_fuel(match &timeout {
|
||||||
|
Timeout::Fuel(_) => true,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
let engine = Engine::new(&config);
|
let engine = Engine::new(&config);
|
||||||
let store = Store::new(&engine);
|
let store = Store::new(&engine);
|
||||||
|
|
||||||
// If a timeout is requested then we spawn a helper thread to wait for the
|
|
||||||
// requested time and then send us a signal to get interrupted. We also
|
|
||||||
// arrange for the thread's sleep to get interrupted if we return early (or
|
|
||||||
// the wasm returns within the time limit), which allows the thread to get
|
|
||||||
// torn down.
|
|
||||||
//
|
|
||||||
// This prevents us from creating a huge number of sleeping threads if this
|
|
||||||
// function is executed in a loop, like it does on nightly fuzzing
|
|
||||||
// infrastructure.
|
|
||||||
|
|
||||||
let mut timeout_state = SignalOnDrop::default();
|
let mut timeout_state = SignalOnDrop::default();
|
||||||
if let Some(timeout) = timeout {
|
match timeout {
|
||||||
let handle = store.interrupt_handle().unwrap();
|
Timeout::Fuel(fuel) => store.add_fuel(fuel),
|
||||||
timeout_state.spawn_timeout(timeout, move || handle.interrupt());
|
// If a timeout is requested then we spawn a helper thread to wait for
|
||||||
|
// the requested time and then send us a signal to get interrupted. We
|
||||||
|
// also arrange for the thread's sleep to get interrupted if we return
|
||||||
|
// early (or the wasm returns within the time limit), which allows the
|
||||||
|
// thread to get torn down.
|
||||||
|
//
|
||||||
|
// This prevents us from creating a huge number of sleeping threads if
|
||||||
|
// this function is executed in a loop, like it does on nightly fuzzing
|
||||||
|
// infrastructure.
|
||||||
|
Timeout::Time(timeout) => {
|
||||||
|
let handle = store.interrupt_handle().unwrap();
|
||||||
|
timeout_state.spawn_timeout(timeout, move || handle.interrupt());
|
||||||
|
}
|
||||||
|
Timeout::None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
log_wasm(wasm);
|
log_wasm(wasm);
|
||||||
@@ -98,11 +122,14 @@ pub fn instantiate_with_config(
|
|||||||
|
|
||||||
match Instance::new(&store, &module, &imports) {
|
match Instance::new(&store, &module, &imports) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
// Allow traps which can happen normally with `unreachable`
|
// Allow traps which can happen normally with `unreachable` or a timeout
|
||||||
Err(e) if e.downcast_ref::<Trap>().is_some() => {}
|
Err(e) if e.downcast_ref::<Trap>().is_some() => {}
|
||||||
// Allow resource exhaustion since this is something that our wasm-smith
|
// Allow resource exhaustion since this is something that our wasm-smith
|
||||||
// generator doesn't guarantee is forbidden.
|
// generator doesn't guarantee is forbidden.
|
||||||
Err(e) if e.to_string().contains("resource limit exceeded") => {}
|
Err(e) if e.to_string().contains("resource limit exceeded") => {}
|
||||||
|
// Also allow errors related to fuel consumption
|
||||||
|
Err(e) if e.to_string().contains("all fuel consumed") => {}
|
||||||
|
// Everything else should be a bug in the fuzzer
|
||||||
Err(e) => panic!("failed to instantiate {}", e),
|
Err(e) => panic!("failed to instantiate {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,13 +410,16 @@ pub fn make_api_calls(api: crate::generators::api::ApiCalls) {
|
|||||||
/// Executes the wast `test` spectest with the `config` specified.
|
/// Executes the wast `test` spectest with the `config` specified.
|
||||||
///
|
///
|
||||||
/// Ensures that spec tests pass regardless of the `Config`.
|
/// Ensures that spec tests pass regardless of the `Config`.
|
||||||
pub fn spectest(config: crate::generators::Config, test: crate::generators::SpecTest) {
|
pub fn spectest(fuzz_config: crate::generators::Config, test: crate::generators::SpecTest) {
|
||||||
crate::init_fuzzing();
|
crate::init_fuzzing();
|
||||||
log::debug!("running {:?} with {:?}", test.file, config);
|
log::debug!("running {:?} with {:?}", test.file, fuzz_config);
|
||||||
let mut config = config.to_wasmtime();
|
let mut config = fuzz_config.to_wasmtime();
|
||||||
config.wasm_reference_types(false);
|
config.wasm_reference_types(false);
|
||||||
config.wasm_bulk_memory(false);
|
config.wasm_bulk_memory(false);
|
||||||
let store = Store::new(&Engine::new(&config));
|
let store = Store::new(&Engine::new(&config));
|
||||||
|
if fuzz_config.consume_fuel {
|
||||||
|
store.add_fuel(u64::max_value());
|
||||||
|
}
|
||||||
let mut wast_context = WastContext::new(store);
|
let mut wast_context = WastContext::new(store);
|
||||||
wast_context.register_spectest().unwrap();
|
wast_context.register_spectest().unwrap();
|
||||||
wast_context
|
wast_context
|
||||||
@@ -398,16 +428,22 @@ pub fn spectest(config: crate::generators::Config, test: crate::generators::Spec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a series of `table.get` and `table.set` operations.
|
/// Execute a series of `table.get` and `table.set` operations.
|
||||||
pub fn table_ops(config: crate::generators::Config, ops: crate::generators::table_ops::TableOps) {
|
pub fn table_ops(
|
||||||
|
fuzz_config: crate::generators::Config,
|
||||||
|
ops: crate::generators::table_ops::TableOps,
|
||||||
|
) {
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
|
|
||||||
let num_dropped = Rc::new(Cell::new(0));
|
let num_dropped = Rc::new(Cell::new(0));
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut config = config.to_wasmtime();
|
let mut config = fuzz_config.to_wasmtime();
|
||||||
config.wasm_reference_types(true);
|
config.wasm_reference_types(true);
|
||||||
let engine = Engine::new(&config);
|
let engine = Engine::new(&config);
|
||||||
let store = Store::new(&engine);
|
let store = Store::new(&engine);
|
||||||
|
if fuzz_config.consume_fuel {
|
||||||
|
store.add_fuel(u64::max_value());
|
||||||
|
}
|
||||||
|
|
||||||
let wasm = ops.to_wasm_binary();
|
let wasm = ops.to_wasm_binary();
|
||||||
log_wasm(&wasm);
|
log_wasm(&wasm);
|
||||||
@@ -520,6 +556,9 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &crate::generators::Con
|
|||||||
wasmtime_config.cranelift_nan_canonicalization(true);
|
wasmtime_config.cranelift_nan_canonicalization(true);
|
||||||
let wasmtime_engine = Engine::new(&wasmtime_config);
|
let wasmtime_engine = Engine::new(&wasmtime_config);
|
||||||
let wasmtime_store = Store::new(&wasmtime_engine);
|
let wasmtime_store = Store::new(&wasmtime_engine);
|
||||||
|
if config.consume_fuel {
|
||||||
|
wasmtime_store.add_fuel(u64::max_value());
|
||||||
|
}
|
||||||
let wasmtime_module =
|
let wasmtime_module =
|
||||||
Module::new(&wasmtime_engine, &wasm).expect("Wasmtime can compile module");
|
Module::new(&wasmtime_engine, &wasm).expect("Wasmtime can compile module");
|
||||||
let wasmtime_instance = Instance::new(&wasmtime_store, &wasmtime_module, &[])
|
let wasmtime_instance = Instance::new(&wasmtime_store, &wasmtime_module, &[])
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Support for jitdump files which can be used by perf for profiling jitted code.
|
//! Support for jitdump files which can be used by perf for profiling jitted code.
|
||||||
//! Spec definitions for the output format is as described here:
|
//! Spec definitions for the output format is as described here:
|
||||||
//! https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/perf/Documentation/jitdump-specification.txt
|
//! <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/perf/Documentation/jitdump-specification.txt>
|
||||||
//!
|
//!
|
||||||
//! Usage Example:
|
//! Usage Example:
|
||||||
//! Record
|
//! Record
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
//!
|
//!
|
||||||
//! For more general information on deferred reference counting, see *An
|
//! For more general information on deferred reference counting, see *An
|
||||||
//! Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
|
//! Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
|
||||||
//! https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf
|
//! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf>
|
||||||
|
|
||||||
use std::alloc::Layout;
|
use std::alloc::Layout;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|||||||
@@ -581,3 +581,8 @@ pub unsafe extern "C" fn wasmtime_imported_memory_atomic_wait64(
|
|||||||
"wasm atomics (fn wasmtime_imported_memory_atomic_wait64) unsupported",
|
"wasm atomics (fn wasmtime_imported_memory_atomic_wait64) unsupported",
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hook for when an instance runs out of fuel.
|
||||||
|
pub unsafe extern "C" fn wasmtime_out_of_gas(_vmctx: *mut VMContext) {
|
||||||
|
crate::traphandlers::out_of_gas()
|
||||||
|
}
|
||||||
|
|||||||
@@ -410,6 +410,13 @@ pub fn with_last_info<R>(func: impl FnOnce(Option<&dyn Any>) -> R) -> R {
|
|||||||
tls::with(|state| func(state.map(|s| s.trap_info.as_any())))
|
tls::with(|state| func(state.map(|s| s.trap_info.as_any())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Invokes the contextually-defined context's out-of-gas function.
|
||||||
|
///
|
||||||
|
/// (basically delegates to `wasmtime::Store::out_of_gas`)
|
||||||
|
pub fn out_of_gas() {
|
||||||
|
tls::with(|state| state.unwrap().trap_info.out_of_gas())
|
||||||
|
}
|
||||||
|
|
||||||
/// Temporary state stored on the stack which is registered in the `tls` module
|
/// Temporary state stored on the stack which is registered in the `tls` module
|
||||||
/// below for calls into wasm.
|
/// below for calls into wasm.
|
||||||
pub struct CallThreadState<'a> {
|
pub struct CallThreadState<'a> {
|
||||||
@@ -442,6 +449,12 @@ pub unsafe trait TrapInfo {
|
|||||||
/// Returns the maximum size, in bytes, the wasm native stack is allowed to
|
/// Returns the maximum size, in bytes, the wasm native stack is allowed to
|
||||||
/// grow to.
|
/// grow to.
|
||||||
fn max_wasm_stack(&self) -> usize;
|
fn max_wasm_stack(&self) -> usize;
|
||||||
|
|
||||||
|
/// Callback invoked whenever WebAssembly has entirely consumed the fuel
|
||||||
|
/// that it was allotted.
|
||||||
|
///
|
||||||
|
/// This function may return, and it may also `raise_lib_trap`.
|
||||||
|
fn out_of_gas(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UnwindReason {
|
enum UnwindReason {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
use crate::externref::VMExternRef;
|
use crate::externref::VMExternRef;
|
||||||
use crate::instance::Instance;
|
use crate::instance::Instance;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use std::cell::UnsafeCell;
|
||||||
use std::ptr::NonNull;
|
use std::ptr::NonNull;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
|
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
|
||||||
use std::u32;
|
use std::u32;
|
||||||
@@ -612,6 +613,7 @@ impl VMBuiltinFunctionsArray {
|
|||||||
wasmtime_memory_atomic_wait64 as usize;
|
wasmtime_memory_atomic_wait64 as usize;
|
||||||
ptrs[BuiltinFunctionIndex::imported_memory_atomic_wait64().index() as usize] =
|
ptrs[BuiltinFunctionIndex::imported_memory_atomic_wait64().index() as usize] =
|
||||||
wasmtime_imported_memory_atomic_wait64 as usize;
|
wasmtime_imported_memory_atomic_wait64 as usize;
|
||||||
|
ptrs[BuiltinFunctionIndex::out_of_gas().index() as usize] = wasmtime_out_of_gas as usize;
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
for i in 0..ptrs.len() {
|
for i in 0..ptrs.len() {
|
||||||
@@ -658,8 +660,7 @@ impl VMInvokeArgument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Structure used to control interrupting wasm code, currently with only one
|
/// Structure used to control interrupting wasm code.
|
||||||
/// atomic flag internally used.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct VMInterrupts {
|
pub struct VMInterrupts {
|
||||||
@@ -668,6 +669,14 @@ pub struct VMInterrupts {
|
|||||||
/// This is used to control both stack overflow as well as interrupting wasm
|
/// This is used to control both stack overflow as well as interrupting wasm
|
||||||
/// modules. For more information see `crates/environ/src/cranelift.rs`.
|
/// modules. For more information see `crates/environ/src/cranelift.rs`.
|
||||||
pub stack_limit: AtomicUsize,
|
pub stack_limit: AtomicUsize,
|
||||||
|
|
||||||
|
/// Indicator of how much fuel has been consumed and is remaining to
|
||||||
|
/// WebAssembly.
|
||||||
|
///
|
||||||
|
/// This field is typically negative and increments towards positive. Upon
|
||||||
|
/// turning positive a wasm trap will be generated. This field is only
|
||||||
|
/// modified if wasm is configured to consume fuel.
|
||||||
|
pub fuel_consumed: UnsafeCell<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VMInterrupts {
|
impl VMInterrupts {
|
||||||
@@ -682,6 +691,7 @@ impl Default for VMInterrupts {
|
|||||||
fn default() -> VMInterrupts {
|
fn default() -> VMInterrupts {
|
||||||
VMInterrupts {
|
VMInterrupts {
|
||||||
stack_limit: AtomicUsize::new(usize::max_value()),
|
stack_limit: AtomicUsize::new(usize::max_value()),
|
||||||
|
fuel_consumed: UnsafeCell::new(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,28 @@ impl Config {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configures whether execution of WebAssembly will "consume fuel" to
|
||||||
|
/// either halt or yield execution as desired.
|
||||||
|
///
|
||||||
|
/// This option is similar in purpose to [`Config::interruptable`] where
|
||||||
|
/// you can prevent infinitely-executing WebAssembly code. The difference
|
||||||
|
/// is that this option allows deterministic execution of WebAssembly code
|
||||||
|
/// by instrumenting generated code consume fuel as it executes. When fuel
|
||||||
|
/// runs out the behavior is defined by configuration within a [`Store`],
|
||||||
|
/// and by default a trap is raised.
|
||||||
|
///
|
||||||
|
/// Note that a [`Store`] starts with no fuel, so if you enable this option
|
||||||
|
/// you'll have to be sure to pour some fuel into [`Store`] before
|
||||||
|
/// executing some code.
|
||||||
|
///
|
||||||
|
/// By default this option is `false`.
|
||||||
|
///
|
||||||
|
/// [`Store`]: crate::Store
|
||||||
|
pub fn consume_fuel(&mut self, enable: bool) -> &mut Self {
|
||||||
|
self.tunables.consume_fuel = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Configures the maximum amount of native stack space available to
|
/// Configures the maximum amount of native stack space available to
|
||||||
/// executing WebAssembly code.
|
/// executing WebAssembly code.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use anyhow::{bail, Result};
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::rc::{Rc, Weak};
|
use std::rc::{Rc, Weak};
|
||||||
@@ -72,6 +73,10 @@ pub(crate) struct StoreInner {
|
|||||||
instance_count: Cell<usize>,
|
instance_count: Cell<usize>,
|
||||||
memory_count: Cell<usize>,
|
memory_count: Cell<usize>,
|
||||||
table_count: Cell<usize>,
|
table_count: Cell<usize>,
|
||||||
|
|
||||||
|
/// An adjustment to add to the fuel consumed value in `interrupts` above
|
||||||
|
/// to get the true amount of fuel consumed.
|
||||||
|
fuel_adj: Cell<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HostInfoKey(VMExternRef);
|
struct HostInfoKey(VMExternRef);
|
||||||
@@ -117,6 +122,7 @@ impl Store {
|
|||||||
instance_count: Default::default(),
|
instance_count: Default::default(),
|
||||||
memory_count: Default::default(),
|
memory_count: Default::default(),
|
||||||
table_count: Default::default(),
|
table_count: Default::default(),
|
||||||
|
fuel_adj: Cell::new(0),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,6 +433,64 @@ impl Store {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the amount of fuel consumed by this store's execution so far.
|
||||||
|
///
|
||||||
|
/// If fuel consumption is not enabled via
|
||||||
|
/// [`Config::consume_fuel`](crate::Config::consume_fuel) then this
|
||||||
|
/// function will return `None`. Also note that fuel, if enabled, must be
|
||||||
|
/// originally configured via [`Store::add_fuel`].
|
||||||
|
pub fn fuel_consumed(&self) -> Option<u64> {
|
||||||
|
if !self.engine().config().tunables.consume_fuel {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let consumed = unsafe { *self.inner.interrupts.fuel_consumed.get() };
|
||||||
|
Some(u64::try_from(self.inner.fuel_adj.get() + consumed).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds fuel to this [`Store`] for wasm to consume while executing.
|
||||||
|
///
|
||||||
|
/// For this method to work fuel consumption must be enabled via
|
||||||
|
/// [`Config::consume_fuel`](crate::Config::consume_fuel). By default a
|
||||||
|
/// [`Store`] starts with 0 fuel for wasm to execute with (meaning it will
|
||||||
|
/// immediately trap). This function must be called for the store to have
|
||||||
|
/// some fuel to allow WebAssembly to execute.
|
||||||
|
///
|
||||||
|
/// Note that at this time when fuel is entirely consumed it will cause
|
||||||
|
/// wasm to trap. More usages of fuel are planned for the future.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function will panic if the store's [`Config`](crate::Config) did
|
||||||
|
/// not have fuel consumption enabled.
|
||||||
|
pub fn add_fuel(&self, fuel: u64) {
|
||||||
|
assert!(self.engine().config().tunables.consume_fuel);
|
||||||
|
|
||||||
|
// Fuel is stored as an i64, so we need to cast it. If the provided fuel
|
||||||
|
// value overflows that just assume that i64::max will suffice. Wasm
|
||||||
|
// execution isn't fast enough to burn through i64::max fuel in any
|
||||||
|
// reasonable amount of time anyway.
|
||||||
|
let fuel = i64::try_from(fuel).unwrap_or(i64::max_value());
|
||||||
|
let adj = self.inner.fuel_adj.get();
|
||||||
|
let consumed_ptr = unsafe { &mut *self.inner.interrupts.fuel_consumed.get() };
|
||||||
|
|
||||||
|
match (consumed_ptr.checked_sub(fuel), adj.checked_add(fuel)) {
|
||||||
|
// If we succesfully did arithmetic without overflowing then we can
|
||||||
|
// just update our fields.
|
||||||
|
(Some(consumed), Some(adj)) => {
|
||||||
|
self.inner.fuel_adj.set(adj);
|
||||||
|
*consumed_ptr = consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise something overflowed. Make sure that we preserve the
|
||||||
|
// amount of fuel that's already consumed, but otherwise assume that
|
||||||
|
// we were given infinite fuel.
|
||||||
|
_ => {
|
||||||
|
self.inner.fuel_adj.set(i64::max_value());
|
||||||
|
*consumed_ptr = (*consumed_ptr + adj) - i64::max_value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl TrapInfo for Store {
|
unsafe impl TrapInfo for Store {
|
||||||
@@ -448,6 +512,23 @@ unsafe impl TrapInfo for Store {
|
|||||||
fn max_wasm_stack(&self) -> usize {
|
fn max_wasm_stack(&self) -> usize {
|
||||||
self.engine().config().max_wasm_stack
|
self.engine().config().max_wasm_stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn out_of_gas(&self) {
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OutOfGas;
|
||||||
|
|
||||||
|
impl fmt::Display for OutOfGas {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("all fuel consumed by WebAssembly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for OutOfGas {}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
wasmtime_runtime::raise_lib_trap(wasmtime_runtime::Trap::User(Box::new(OutOfGas)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Store {
|
impl Default for Store {
|
||||||
@@ -481,6 +562,13 @@ pub struct InterruptHandle {
|
|||||||
interrupts: Arc<VMInterrupts>,
|
interrupts: Arc<VMInterrupts>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The `VMInterrupts` type is a pod-type with no destructor, and we only access
|
||||||
|
// `interrupts` from other threads, so add in these trait impls which are
|
||||||
|
// otherwise not available due to the `fuel_consumed` variable in
|
||||||
|
// `VMInterrupts`.
|
||||||
|
unsafe impl Send for InterruptHandle {}
|
||||||
|
unsafe impl Sync for InterruptHandle {}
|
||||||
|
|
||||||
impl InterruptHandle {
|
impl InterruptHandle {
|
||||||
/// Flags that execution within this handle's original [`Store`] should be
|
/// Flags that execution within this handle's original [`Store`] should be
|
||||||
/// interrupted.
|
/// interrupted.
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ use libfuzzer_sys::fuzz_target;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use wasm_smith::MaybeInvalidModule;
|
use wasm_smith::MaybeInvalidModule;
|
||||||
use wasmtime::Strategy;
|
use wasmtime::Strategy;
|
||||||
use wasmtime_fuzzing::oracles;
|
use wasmtime_fuzzing::oracles::{self, Timeout};
|
||||||
|
|
||||||
fuzz_target!(|module: MaybeInvalidModule| {
|
fuzz_target!(|pair: (bool, MaybeInvalidModule)| {
|
||||||
|
let (timeout_with_time, module) = pair;
|
||||||
oracles::instantiate_with_config(
|
oracles::instantiate_with_config(
|
||||||
&module.to_bytes(),
|
&module.to_bytes(),
|
||||||
false,
|
false,
|
||||||
wasmtime_fuzzing::fuzz_default_config(Strategy::Auto).unwrap(),
|
wasmtime_fuzzing::fuzz_default_config(Strategy::Auto).unwrap(),
|
||||||
Some(Duration::from_secs(20)),
|
if timeout_with_time {
|
||||||
|
Timeout::Time(Duration::from_secs(20))
|
||||||
|
} else {
|
||||||
|
Timeout::Fuel(100_000)
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,21 @@ use libfuzzer_sys::fuzz_target;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use wasm_smith::{Config, ConfiguredModule, SwarmConfig};
|
use wasm_smith::{Config, ConfiguredModule, SwarmConfig};
|
||||||
use wasmtime::Strategy;
|
use wasmtime::Strategy;
|
||||||
use wasmtime_fuzzing::oracles;
|
use wasmtime_fuzzing::oracles::{self, Timeout};
|
||||||
|
|
||||||
fuzz_target!(|module: ConfiguredModule<SwarmConfig>| {
|
fuzz_target!(|pair: (bool, ConfiguredModule<SwarmConfig>)| {
|
||||||
|
let (timeout_with_time, module) = pair;
|
||||||
let mut cfg = wasmtime_fuzzing::fuzz_default_config(Strategy::Auto).unwrap();
|
let mut cfg = wasmtime_fuzzing::fuzz_default_config(Strategy::Auto).unwrap();
|
||||||
cfg.wasm_multi_memory(true);
|
cfg.wasm_multi_memory(true);
|
||||||
cfg.wasm_module_linking(module.config().module_linking_enabled());
|
cfg.wasm_module_linking(module.config().module_linking_enabled());
|
||||||
oracles::instantiate_with_config(&module.to_bytes(), true, cfg, Some(Duration::from_secs(20)));
|
oracles::instantiate_with_config(
|
||||||
|
&module.to_bytes(),
|
||||||
|
true,
|
||||||
|
cfg,
|
||||||
|
if timeout_with_time {
|
||||||
|
Timeout::Time(Duration::from_secs(20))
|
||||||
|
} else {
|
||||||
|
Timeout::Fuel(100_000)
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
124
tests/all/fuel.rs
Normal file
124
tests/all/fuel.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use wasmtime::*;
|
||||||
|
use wast::parser::{self, Parse, ParseBuffer, Parser};
|
||||||
|
|
||||||
|
mod kw {
|
||||||
|
wast::custom_keyword!(assert_fuel);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FuelWast<'a> {
|
||||||
|
assertions: Vec<(wast::Span, u64, wast::Module<'a>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Parse<'a> for FuelWast<'a> {
|
||||||
|
fn parse(parser: Parser<'a>) -> parser::Result<Self> {
|
||||||
|
let mut assertions = Vec::new();
|
||||||
|
while !parser.is_empty() {
|
||||||
|
assertions.push(parser.parens(|p| {
|
||||||
|
let span = p.parse::<kw::assert_fuel>()?.0;
|
||||||
|
Ok((span, p.parse()?, p.parens(|p| p.parse())?))
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
Ok(FuelWast { assertions })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run() -> Result<()> {
|
||||||
|
let test = std::fs::read_to_string("tests/all/fuel.wast")?;
|
||||||
|
let buf = ParseBuffer::new(&test)?;
|
||||||
|
let mut wast = parser::parse::<FuelWast<'_>>(&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);
|
||||||
|
let module = Module::new(&engine, wasm).unwrap();
|
||||||
|
let store = Store::new(&engine);
|
||||||
|
store.add_fuel(u64::max_value());
|
||||||
|
drop(Instance::new(&store, &module, &[]));
|
||||||
|
store.fuel_consumed().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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);
|
||||||
|
let module = Module::new(&engine, wat).unwrap();
|
||||||
|
let store = Store::new(&engine);
|
||||||
|
store.add_fuel(10_000);
|
||||||
|
let error = Instance::new(&store, &module, &[]).err().unwrap();
|
||||||
|
assert!(
|
||||||
|
error.to_string().contains("all fuel consumed"),
|
||||||
|
"bad error: {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
208
tests/all/fuel.wast
Normal file
208
tests/all/fuel.wast
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
(assert_fuel 0 (module))
|
||||||
|
|
||||||
|
(assert_fuel 1
|
||||||
|
(module
|
||||||
|
(func $f)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 2
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
drop
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 1
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
block
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 1
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
unreachable
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 7
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
unreachable
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 1
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
return
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
i32.const 0
|
||||||
|
unreachable
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 3
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
if
|
||||||
|
call $f
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 4
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 1
|
||||||
|
if
|
||||||
|
i32.const 0
|
||||||
|
drop
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 4
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 1
|
||||||
|
if
|
||||||
|
i32.const 0
|
||||||
|
drop
|
||||||
|
else
|
||||||
|
call $f
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 4
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
if
|
||||||
|
call $f
|
||||||
|
else
|
||||||
|
i32.const 0
|
||||||
|
drop
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 3
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
block
|
||||||
|
i32.const 1
|
||||||
|
br_if 0
|
||||||
|
i32.const 0
|
||||||
|
drop
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
(assert_fuel 4
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
block
|
||||||
|
i32.const 0
|
||||||
|
br_if 0
|
||||||
|
i32.const 0
|
||||||
|
drop
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
;; count code before unreachable
|
||||||
|
(assert_fuel 2
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
unreachable
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
;; count code before return
|
||||||
|
(assert_fuel 2
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
return
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
;; cross-function fuel works
|
||||||
|
(assert_fuel 3
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
call $other
|
||||||
|
)
|
||||||
|
(func $other)
|
||||||
|
(start $f)))
|
||||||
|
(assert_fuel 5
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
call $other
|
||||||
|
i32.const 0
|
||||||
|
drop
|
||||||
|
)
|
||||||
|
(func $other (param i32))
|
||||||
|
(start $f)))
|
||||||
|
(assert_fuel 4
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
call $other
|
||||||
|
drop
|
||||||
|
)
|
||||||
|
(func $other (result i32)
|
||||||
|
i32.const 0
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
(assert_fuel 4
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
i32.const 0
|
||||||
|
call_indirect
|
||||||
|
)
|
||||||
|
(func $other)
|
||||||
|
(table funcref (elem $other))
|
||||||
|
(start $f)))
|
||||||
|
|
||||||
|
;; loops!
|
||||||
|
(assert_fuel 1
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
loop
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
|
(assert_fuel 53 ;; 5 loop instructions, 10 iterations, 2 header instrs, 1 func
|
||||||
|
(module
|
||||||
|
(func $f
|
||||||
|
(local i32)
|
||||||
|
i32.const 10
|
||||||
|
local.set 0
|
||||||
|
|
||||||
|
loop
|
||||||
|
local.get 0
|
||||||
|
i32.const 1
|
||||||
|
i32.sub
|
||||||
|
local.tee 0
|
||||||
|
br_if 0
|
||||||
|
end
|
||||||
|
)
|
||||||
|
(start $f)))
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
//! `include_bytes!("./fuzzing/some-descriptive-name.wasm")`.
|
//! `include_bytes!("./fuzzing/some-descriptive-name.wasm")`.
|
||||||
|
|
||||||
use wasmtime::{Config, Strategy};
|
use wasmtime::{Config, Strategy};
|
||||||
use wasmtime_fuzzing::oracles;
|
use wasmtime_fuzzing::oracles::{self, Timeout};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn instantiate_empty_module() {
|
fn instantiate_empty_module() {
|
||||||
@@ -26,5 +26,5 @@ fn instantiate_module_that_compiled_to_x64_has_register_32() {
|
|||||||
let mut config = Config::new();
|
let mut config = Config::new();
|
||||||
config.debug_info(true);
|
config.debug_info(true);
|
||||||
let data = wat::parse_str(include_str!("./fuzzing/issue694.wat")).unwrap();
|
let data = wat::parse_str(include_str!("./fuzzing/issue694.wat")).unwrap();
|
||||||
oracles::instantiate_with_config(&data, true, config, None);
|
oracles::instantiate_with_config(&data, true, config, Timeout::None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod cli_tests;
|
|||||||
mod custom_signal_handler;
|
mod custom_signal_handler;
|
||||||
mod debug;
|
mod debug;
|
||||||
mod externals;
|
mod externals;
|
||||||
|
mod fuel;
|
||||||
mod func;
|
mod func;
|
||||||
mod fuzzing;
|
mod fuzzing;
|
||||||
mod globals;
|
mod globals;
|
||||||
|
|||||||
Reference in New Issue
Block a user