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:
Alex Crichton
2021-01-29 08:57:17 -06:00
committed by GitHub
parent 78f312799e
commit 0e41861662
26 changed files with 936 additions and 67 deletions

2
Cargo.lock generated
View File

@@ -3138,6 +3138,7 @@ dependencies = [
"wasmtime-wasi-crypto",
"wasmtime-wasi-nn",
"wasmtime-wast",
"wast 32.0.0",
"wat",
]
@@ -3149,6 +3150,7 @@ dependencies = [
"cranelift-entity",
"cranelift-frontend",
"cranelift-wasm",
"wasmparser",
"wasmtime-environ",
]

View File

@@ -55,6 +55,7 @@ test-programs = { path = "crates/test-programs" }
wasmtime-fuzzing = { path = "crates/fuzzing" }
wasmtime-runtime = { path = "crates/runtime" }
tracing-subscriber = "0.2.0"
wast = "32.0.0"
[build-dependencies]
anyhow = "1.0.19"

View File

@@ -12,7 +12,7 @@ static WASM_MAGIC: &[u8] = &[0x00, 0x61, 0x73, 0x6D];
/// Harvest candidates for superoptimization from a Wasm or Clif file.
///
/// Candidates are emitted in Souper's text format:
/// https://github.com/google/souper
/// <https://github.com/google/souper>
#[derive(StructOpt)]
pub struct Options {
/// Specify an input file to be used. Use '-' for stdin.

View File

@@ -261,7 +261,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
.extend_from_slice(builder.block_params(loop_body));
builder.switch_to_block(loop_body);
environ.translate_loop_header(builder.cursor())?;
environ.translate_loop_header(builder)?;
}
Operator::If { ty } => {
let val = state.pop1();

View File

@@ -295,6 +295,12 @@ pub trait FuncEnvironment: TargetEnvironment {
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
/// identified by `index`.
///
@@ -637,7 +643,7 @@ pub trait FuncEnvironment: TargetEnvironment {
///
/// This can be used to insert explicit interrupt or safepoint checking at
/// 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.
Ok(())
}

View File

@@ -170,6 +170,8 @@ fn parse_local_decls<FE: FuncEnvironment + ?Sized>(
declare_locals(builder, count, ty, &mut next_local, environ)?;
}
environ.after_locals(next_local);
Ok(())
}

View File

@@ -17,3 +17,4 @@ cranelift-wasm = { path = "../../cranelift/wasm", version = "0.69.0" }
cranelift-codegen = { path = "../../cranelift/codegen", version = "0.69.0" }
cranelift-frontend = { path = "../../cranelift/frontend", version = "0.69.0" }
cranelift-entity = { path = "../../cranelift/entity", version = "0.69.0" }
wasmparser = "0.73.0"

View File

@@ -7,11 +7,14 @@ use cranelift_codegen::ir::{AbiParam, ArgumentPurpose, Function, InstBuilder, Si
use cranelift_codegen::isa::{self, TargetFrontendConfig};
use cranelift_entity::{EntityRef, PrimaryMap};
use cranelift_frontend::FunctionBuilder;
use cranelift_frontend::Variable;
use cranelift_wasm::{
self, FuncIndex, GlobalIndex, GlobalVariable, MemoryIndex, SignatureIndex, TableIndex,
TargetEnvironment, TypeIndex, WasmError, WasmResult, WasmType,
self, FuncIndex, FuncTranslationState, GlobalIndex, GlobalVariable, MemoryIndex,
SignatureIndex, TableIndex, TargetEnvironment, TypeIndex, WasmError, WasmResult, WasmType,
};
use std::convert::TryFrom;
use std::mem;
use wasmparser::Operator;
use wasmtime_environ::{
BuiltinFunctionIndex, MemoryPlan, MemoryStyle, Module, TableStyle, Tunables, VMOffsets,
INTERRUPTED, WASM_PAGE_SIZE,
@@ -125,6 +128,20 @@ pub struct FuncEnvironment<'module_environment> {
pub(crate) offsets: VMOffsets,
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> {
@@ -151,6 +168,12 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
builtin_function_signatures,
offsets: VMOffsets::new(target_config.pointer_bytes(), module),
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)
}
}
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> {
@@ -437,6 +695,11 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
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> {
let pointer_type = self.pointer_type();
@@ -1482,24 +1745,16 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
}
fn translate_loop_header(&mut self, mut pos: FuncCursor) -> WasmResult<()> {
if !self.tunables.interruptable {
return Ok(());
}
// Start out each loop with a check to the interupt flag to allow
// interruption of long or infinite loops.
fn translate_loop_header(&mut self, builder: &mut FunctionBuilder) -> WasmResult<()> {
// If enabled check the interrupt flag to prevent long or infinite
// loops.
//
// For more information about this see comments in
// `crates/environ/src/cranelift.rs`
let vmctx = self.vmctx(&mut pos.func);
if self.tunables.interruptable {
let pointer_type = self.pointer_type();
let base = pos.ins().global_value(pointer_type, vmctx);
let offset = i32::try_from(self.offsets.vmctx_interrupts()).unwrap();
let interrupt_ptr = pos
.ins()
.load(pointer_type, ir::MemFlags::trusted(), base, offset);
let interrupt = pos.ins().load(
let interrupt_ptr = builder.use_var(self.vminterrupts_ptr);
let interrupt = builder.ins().load(
pointer_type,
ir::MemFlags::trusted(),
interrupt_ptr,
@@ -1507,11 +1762,73 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
);
// Note that the cast to `isize` happens first to allow sign-extension,
// if necessary, to `i64`.
let interrupted_sentinel = pos.ins().iconst(pointer_type, INTERRUPTED as isize as i64);
let cmp = pos
let interrupted_sentinel = builder
.ins()
.iconst(pointer_type, INTERRUPTED as isize as i64);
let cmp = builder
.ins()
.icmp(IntCC::Equal, interrupt, interrupted_sentinel);
pos.ins().trapnz(cmp, ir::TrapCode::Interrupt);
builder.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(())
}
}

View File

@@ -57,6 +57,8 @@ macro_rules! foreach_builtin_function {
memory_atomic_wait64(vmctx, i32, i32, i64, i64) -> (i32);
/// Returns an index for wasm's `memory.atomic.wait64` for imported memories.
imported_memory_atomic_wait64(vmctx, i32, i32, i64, i64) -> (i32);
/// Invoked when fuel has run out while executing a function.
out_of_gas(vmctx) -> ();
}
};
}

View File

@@ -23,6 +23,10 @@ pub struct Tunables {
/// calls and interrupts are implemented through the `VMInterrupts`
/// structure, or `InterruptHandle` in the `wasmtime` crate.
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 {
@@ -57,6 +61,7 @@ impl Default for Tunables {
generate_native_debuginfo: false,
parse_wasm_debuginfo: true,
interruptable: false,
consume_fuel: false,
}
}
}

View File

@@ -258,6 +258,11 @@ impl VMOffsets {
pub fn vminterrupts_stack_limit(&self) -> u8 {
0
}
/// Return the offset of the `fuel_consumed` field of `VMInterrupts`
pub fn vminterrupts_fuel_consumed(&self) -> u8 {
self.pointer_size
}
}
/// Offsets for `VMCallerCheckedAnyfunc`.

View File

@@ -64,6 +64,8 @@ pub struct Config {
debug_info: bool,
canonicalize_nans: 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
// 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())
.cranelift_nan_canonicalization(self.canonicalize_nans)
.cranelift_opt_level(self.opt_level.to_wasmtime())
.interruptable(self.interruptable);
.interruptable(self.interruptable)
.consume_fuel(self.consume_fuel);
return cfg;
}
}

View File

@@ -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
/// 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
let mut cfg = crate::fuzz_default_config(strategy).unwrap();
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
@@ -64,29 +78,39 @@ pub fn instantiate_with_config(
wasm: &[u8],
known_valid: bool,
mut config: Config,
timeout: Option<Duration>,
timeout: Timeout,
) {
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 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();
if let Some(timeout) = timeout {
match timeout {
Timeout::Fuel(fuel) => store.add_fuel(fuel),
// 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);
let module = match Module::new(&engine, wasm) {
@@ -98,11 +122,14 @@ pub fn instantiate_with_config(
match Instance::new(&store, &module, &imports) {
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() => {}
// Allow resource exhaustion since this is something that our wasm-smith
// generator doesn't guarantee is forbidden.
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),
}
}
@@ -383,13 +410,16 @@ pub fn make_api_calls(api: crate::generators::api::ApiCalls) {
/// Executes the wast `test` spectest with the `config` specified.
///
/// 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();
log::debug!("running {:?} with {:?}", test.file, config);
let mut config = config.to_wasmtime();
log::debug!("running {:?} with {:?}", test.file, fuzz_config);
let mut config = fuzz_config.to_wasmtime();
config.wasm_reference_types(false);
config.wasm_bulk_memory(false);
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);
wast_context.register_spectest().unwrap();
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.
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 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);
let engine = Engine::new(&config);
let store = Store::new(&engine);
if fuzz_config.consume_fuel {
store.add_fuel(u64::max_value());
}
let wasm = ops.to_wasm_binary();
log_wasm(&wasm);
@@ -520,6 +556,9 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &crate::generators::Con
wasmtime_config.cranelift_nan_canonicalization(true);
let wasmtime_engine = Engine::new(&wasmtime_config);
let wasmtime_store = Store::new(&wasmtime_engine);
if config.consume_fuel {
wasmtime_store.add_fuel(u64::max_value());
}
let wasmtime_module =
Module::new(&wasmtime_engine, &wasm).expect("Wasmtime can compile module");
let wasmtime_instance = Instance::new(&wasmtime_store, &wasmtime_module, &[])

View File

@@ -1,6 +1,6 @@
//! Support for jitdump files which can be used by perf for profiling jitted code.
//! 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:
//! Record

View File

@@ -97,7 +97,7 @@
//!
//! For more general information on deferred reference counting, see *An
//! 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::any::Any;

View File

@@ -581,3 +581,8 @@ pub unsafe extern "C" fn wasmtime_imported_memory_atomic_wait64(
"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()
}

View File

@@ -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())))
}
/// 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
/// below for calls into wasm.
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
/// grow to.
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 {

View File

@@ -4,6 +4,7 @@
use crate::externref::VMExternRef;
use crate::instance::Instance;
use std::any::Any;
use std::cell::UnsafeCell;
use std::ptr::NonNull;
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::u32;
@@ -612,6 +613,7 @@ impl VMBuiltinFunctionsArray {
wasmtime_memory_atomic_wait64 as usize;
ptrs[BuiltinFunctionIndex::imported_memory_atomic_wait64().index() 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) {
for i in 0..ptrs.len() {
@@ -658,8 +660,7 @@ impl VMInvokeArgument {
}
}
/// Structure used to control interrupting wasm code, currently with only one
/// atomic flag internally used.
/// Structure used to control interrupting wasm code.
#[derive(Debug)]
#[repr(C)]
pub struct VMInterrupts {
@@ -668,6 +669,14 @@ pub struct VMInterrupts {
/// This is used to control both stack overflow as well as interrupting wasm
/// modules. For more information see `crates/environ/src/cranelift.rs`.
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 {
@@ -682,6 +691,7 @@ impl Default for VMInterrupts {
fn default() -> VMInterrupts {
VMInterrupts {
stack_limit: AtomicUsize::new(usize::max_value()),
fuel_consumed: UnsafeCell::new(0),
}
}
}

View File

@@ -137,6 +137,28 @@ impl Config {
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
/// executing WebAssembly code.
///

View File

@@ -6,6 +6,7 @@ use anyhow::{bail, Result};
use std::any::Any;
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::rc::{Rc, Weak};
@@ -72,6 +73,10 @@ pub(crate) struct StoreInner {
instance_count: Cell<usize>,
memory_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);
@@ -117,6 +122,7 @@ impl Store {
instance_count: Default::default(),
memory_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 {
@@ -448,6 +512,23 @@ unsafe impl TrapInfo for Store {
fn max_wasm_stack(&self) -> usize {
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 {
@@ -481,6 +562,13 @@ pub struct InterruptHandle {
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 {
/// Flags that execution within this handle's original [`Store`] should be
/// interrupted.

View File

@@ -4,13 +4,18 @@ use libfuzzer_sys::fuzz_target;
use std::time::Duration;
use wasm_smith::MaybeInvalidModule;
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(
&module.to_bytes(),
false,
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)
},
);
});

View File

@@ -4,11 +4,21 @@ use libfuzzer_sys::fuzz_target;
use std::time::Duration;
use wasm_smith::{Config, ConfiguredModule, SwarmConfig};
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();
cfg.wasm_multi_memory(true);
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
View 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
View 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)))

View File

@@ -6,7 +6,7 @@
//! `include_bytes!("./fuzzing/some-descriptive-name.wasm")`.
use wasmtime::{Config, Strategy};
use wasmtime_fuzzing::oracles;
use wasmtime_fuzzing::oracles::{self, Timeout};
#[test]
fn instantiate_empty_module() {
@@ -26,5 +26,5 @@ fn instantiate_module_that_compiled_to_x64_has_register_32() {
let mut config = Config::new();
config.debug_info(true);
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);
}

View File

@@ -2,6 +2,7 @@ mod cli_tests;
mod custom_signal_handler;
mod debug;
mod externals;
mod fuel;
mod func;
mod fuzzing;
mod globals;