From 69450aabd0306ce5cf4e893d47671cd5960d4f9b Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 27 Jan 2021 08:49:53 -0800 Subject: [PATCH 01/11] 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. --- Cargo.lock | 2 + Cargo.toml | 1 + cranelift/wasm/src/environ/spec.rs | 6 + cranelift/wasm/src/func_translator.rs | 2 + crates/cranelift/Cargo.toml | 1 + crates/cranelift/src/func_environ.rs | 240 +++++++++++++++++++++++++- crates/environ/src/tunables.rs | 5 + crates/environ/src/vmoffsets.rs | 5 + crates/runtime/src/vmcontext.rs | 5 + crates/wasmtime/src/config.rs | 20 +++ crates/wasmtime/src/store.rs | 16 ++ tests/all/fuel.rs | 45 +++++ tests/all/fuel.wast | 208 ++++++++++++++++++++++ tests/all/main.rs | 1 + 14 files changed, 555 insertions(+), 2 deletions(-) create mode 100644 tests/all/fuel.rs create mode 100644 tests/all/fuel.wast diff --git a/Cargo.lock b/Cargo.lock index ead0ab8413fb..352bcee78177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index c1d0cc8ed73a..00cd71f52628 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/cranelift/wasm/src/environ/spec.rs b/cranelift/wasm/src/environ/spec.rs index 84978dc0d842..eda505669e18 100644 --- a/cranelift/wasm/src/environ/spec.rs +++ b/cranelift/wasm/src/environ/spec.rs @@ -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`. /// diff --git a/cranelift/wasm/src/func_translator.rs b/cranelift/wasm/src/func_translator.rs index 7a8f9c19d2b5..97e5354c6ed7 100644 --- a/cranelift/wasm/src/func_translator.rs +++ b/cranelift/wasm/src/func_translator.rs @@ -170,6 +170,8 @@ fn parse_local_decls( declare_locals(builder, count, ty, &mut next_local, environ)?; } + environ.after_locals(next_local); + Ok(()) } diff --git a/crates/cranelift/Cargo.toml b/crates/cranelift/Cargo.toml index 99ccc5a497d3..9b4e0972bb82 100644 --- a/crates/cranelift/Cargo.toml +++ b/crates/cranelift/Cargo.toml @@ -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" diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 6b94824f9386..f277541dbb58 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -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,10 @@ pub struct FuncEnvironment<'module_environment> { pub(crate) offsets: VMOffsets, tunables: &'module_environment Tunables, + + fuel_var: cranelift_frontend::Variable, + + fuel_consumed: i64, } impl<'module_environment> FuncEnvironment<'module_environment> { @@ -151,6 +158,11 @@ impl<'module_environment> FuncEnvironment<'module_environment> { builtin_function_signatures, offsets: VMOffsets::new(target_config.pointer_bytes(), module), tunables, + fuel_var: 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 +430,180 @@ impl<'module_environment> FuncEnvironment<'module_environment> { (global, 0) } } + + 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); + } + + 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 `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 consumption = builder.ins().iconst(ir::types::I64, consumption); + let fuel = builder.ins().iadd(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) { + // The address/offset of the fuel consumption is the second field of + // `VMInterrupts`, so first load the pointer to the `VMInterrupts` and + // then get an offset from that. + let vmctx = self.vmctx(builder.func); + let pointer_type = self.pointer_type(); + 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); + ( + interrupt_ptr, + i32::from(self.offsets.vminterrupts_fuel_consumed()).into(), + ) + } } impl<'module_environment> TargetEnvironment for FuncEnvironment<'module_environment> { @@ -437,6 +623,10 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m index >= 2 } + fn after_locals(&mut self, num_locals: usize) { + self.fuel_var = Variable::new(num_locals); + } + fn make_table(&mut self, func: &mut ir::Function, index: TableIndex) -> WasmResult { let pointer_type = self.pointer_type(); @@ -1514,4 +1704,50 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m pos.ins().trapnz(cmp, ir::TrapCode::Interrupt); 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 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(()) + } } diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index 939861504da2..bd86fef237f9 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -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, } } } diff --git a/crates/environ/src/vmoffsets.rs b/crates/environ/src/vmoffsets.rs index 8673a38d3cc2..7f74f46754c1 100644 --- a/crates/environ/src/vmoffsets.rs +++ b/crates/environ/src/vmoffsets.rs @@ -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`. diff --git a/crates/runtime/src/vmcontext.rs b/crates/runtime/src/vmcontext.rs index 3eebdabd4e4d..1d109f5f943c 100644 --- a/crates/runtime/src/vmcontext.rs +++ b/crates/runtime/src/vmcontext.rs @@ -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; @@ -668,6 +669,9 @@ 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, + + /// TODO + pub fuel_consumed: UnsafeCell, } impl VMInterrupts { @@ -682,6 +686,7 @@ impl Default for VMInterrupts { fn default() -> VMInterrupts { VMInterrupts { stack_limit: AtomicUsize::new(usize::max_value()), + fuel_consumed: UnsafeCell::new(0), } } } diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 3edbfebb2b58..7688e6c8600e 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -137,6 +137,26 @@ 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`. + 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. /// diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index cc86011fb250..57598e8e91af 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -427,6 +427,15 @@ impl Store { ); } } + + /// TODO + pub fn fuel_consumed(&self) -> Option { + if self.engine().config().tunables.consume_fuel { + Some(unsafe { *self.inner.interrupts.fuel_consumed.get() }) + } else { + None + } + } } unsafe impl TrapInfo for Store { @@ -481,6 +490,13 @@ pub struct InterruptHandle { interrupts: Arc, } +// 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. diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs new file mode 100644 index 000000000000..49a1809e3943 --- /dev/null +++ b/tests/all/fuel.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use wasmtime::*; +use wast::parser::{self, Parse, ParseBuffer, Parser}; + +mod kw { + wast::custom_keyword!(assert_fuel); +} + +struct FuelWast<'a> { + assertions: Vec<(u64, wast::Module<'a>)>, +} + +impl<'a> Parse<'a> for FuelWast<'a> { + fn parse(parser: Parser<'a>) -> parser::Result { + let mut assertions = Vec::new(); + while !parser.is_empty() { + assertions.push(parser.parens(|p| { + p.parse::()?; + Ok((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::>(&buf)?; + for (fuel, module) in wast.assertions.iter_mut() { + assert_fuel(*fuel, &module.encode()?); + } + Ok(()) +} + +fn assert_fuel(fuel: u64, wasm: &[u8]) { + 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); + drop(Instance::new(&store, &module, &[])); + assert_eq!(store.fuel_consumed(), Some(fuel)); +} diff --git a/tests/all/fuel.wast b/tests/all/fuel.wast new file mode 100644 index 000000000000..bb3450b6ccc1 --- /dev/null +++ b/tests/all/fuel.wast @@ -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))) diff --git a/tests/all/main.rs b/tests/all/main.rs index 1f6cb8045917..c20b2dddce03 100644 --- a/tests/all/main.rs +++ b/tests/all/main.rs @@ -2,6 +2,7 @@ mod cli_tests; mod custom_signal_handler; mod debug; mod externals; +mod fuel; mod func; mod fuzzing; mod globals; From 184467df94055687c9dd7e6f6d0b4283431e5bcd Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 27 Jan 2021 10:35:44 -0800 Subject: [PATCH 02/11] 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. --- cranelift/wasm/src/code_translator.rs | 2 +- cranelift/wasm/src/environ/spec.rs | 2 +- crates/cranelift/src/func_environ.rs | 109 +++++++++++++++++++------- crates/environ/src/builtin.rs | 2 + crates/fuzzing/src/generators.rs | 5 +- crates/fuzzing/src/oracles.rs | 22 ++++-- crates/runtime/src/libcalls.rs | 5 ++ crates/runtime/src/traphandlers.rs | 13 +++ crates/runtime/src/vmcontext.rs | 13 ++- crates/wasmtime/src/store.rs | 70 +++++++++++++++-- tests/all/fuel.rs | 27 +++++-- 11 files changed, 219 insertions(+), 51 deletions(-) diff --git a/cranelift/wasm/src/code_translator.rs b/cranelift/wasm/src/code_translator.rs index 266e002ae48e..a20975b25bc9 100644 --- a/cranelift/wasm/src/code_translator.rs +++ b/cranelift/wasm/src/code_translator.rs @@ -261,7 +261,7 @@ pub fn translate_operator( .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(); diff --git a/cranelift/wasm/src/environ/spec.rs b/cranelift/wasm/src/environ/spec.rs index eda505669e18..0bd3c48745f9 100644 --- a/cranelift/wasm/src/environ/spec.rs +++ b/cranelift/wasm/src/environ/spec.rs @@ -643,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(()) } diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index f277541dbb58..f7a5bebd1222 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -438,6 +438,7 @@ impl<'module_environment> FuncEnvironment<'module_environment> { // `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<'_>) { @@ -604,6 +605,52 @@ impl<'module_environment> FuncEnvironment<'module_environment> { 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> { @@ -1672,36 +1719,44 @@ 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); - 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( - pointer_type, - ir::MemFlags::trusted(), - interrupt_ptr, - i32::from(self.offsets.vminterrupts_stack_limit()), - ); - // 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 - .ins() - .icmp(IntCC::Equal, interrupt, interrupted_sentinel); - pos.ins().trapnz(cmp, ir::TrapCode::Interrupt); + if self.tunables.interruptable { + let vmctx = self.vmctx(builder.func); + let pointer_type = self.pointer_type(); + 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); + let interrupt = builder.ins().load( + pointer_type, + ir::MemFlags::trusted(), + interrupt_ptr, + i32::from(self.offsets.vminterrupts_stack_limit()), + ); + // Note that the cast to `isize` happens first to allow sign-extension, + // if necessary, to `i64`. + let interrupted_sentinel = builder + .ins() + .iconst(pointer_type, INTERRUPTED as isize as i64); + let cmp = builder + .ins() + .icmp(IntCC::Equal, interrupt, interrupted_sentinel); + 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(()) } diff --git a/crates/environ/src/builtin.rs b/crates/environ/src/builtin.rs index f2bb330217d0..1bbc06424166 100644 --- a/crates/environ/src/builtin.rs +++ b/crates/environ/src/builtin.rs @@ -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) -> (); } }; } diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index a35bd186a90a..b44af8daf517 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -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; } } diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 093ffbacd8bc..b474a8a1dbea 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -383,13 +383,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.set_fuel_remaining(i64::max_value() as u64); + } let mut wast_context = WastContext::new(store); wast_context.register_spectest().unwrap(); wast_context @@ -398,16 +401,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.set_fuel_remaining(i64::max_value() as u64); + } let wasm = ops.to_wasm_binary(); log_wasm(&wasm); @@ -520,6 +529,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.set_fuel_remaining(i64::max_value() as u64); + } let wasmtime_module = Module::new(&wasmtime_engine, &wasm).expect("Wasmtime can compile module"); let wasmtime_instance = Instance::new(&wasmtime_store, &wasmtime_module, &[]) diff --git a/crates/runtime/src/libcalls.rs b/crates/runtime/src/libcalls.rs index 61e8ca4a2165..026738a8a1cb 100644 --- a/crates/runtime/src/libcalls.rs +++ b/crates/runtime/src/libcalls.rs @@ -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() +} diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index e554e54c3395..feaf2c4a9af4 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -410,6 +410,13 @@ pub fn with_last_info(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 { diff --git a/crates/runtime/src/vmcontext.rs b/crates/runtime/src/vmcontext.rs index 1d109f5f943c..c20a42b5ed4a 100644 --- a/crates/runtime/src/vmcontext.rs +++ b/crates/runtime/src/vmcontext.rs @@ -613,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() { @@ -659,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 { @@ -670,8 +670,13 @@ pub struct VMInterrupts { /// modules. For more information see `crates/environ/src/cranelift.rs`. pub stack_limit: AtomicUsize, - /// TODO - pub fuel_consumed: UnsafeCell, + /// 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, } impl VMInterrupts { diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 57598e8e91af..2707e6fefe8c 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -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, memory_count: Cell, table_count: Cell, + + /// An adjustment to add to the fuel consumed value in `interrupts` above + /// to get the true amount of fuel consumed. + fuel_adj: Cell, } 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), }), } } @@ -428,12 +434,49 @@ impl Store { } } - /// TODO + /// Returns the amount of fuel consumed by this store's execution so far. + /// + /// If fuel consumption is not enabled via [`Config::consume_fuel`] then + /// this function will return `None`. Also note that fuel, if enabled, must + /// be originally configured via [`Store::set_fuel_remaining`]. + /// + /// Finally, this will only return the amount of fuel consumed since + /// [`Store::set_fuel_remaining`] was last called, so this does not return + /// the total amount of fuel consumed for the entire lifetime of this + /// [`Store`]. pub fn fuel_consumed(&self) -> Option { - if self.engine().config().tunables.consume_fuel { - Some(unsafe { *self.inner.interrupts.fuel_consumed.get() }) - } else { - None + 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()) + } + + /// Updates the amount of fuel remaining in this [`Store`] available for + /// wasm to consume while executing. + /// + /// For this method to work fuel consumption must be enabled via + /// [`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`] did not have fuel + /// consumption enabled. + /// + /// This funtion will also panic if `remaining` is larger than + /// `i64::max_value()`. + pub fn set_fuel_remaining(&self, remaining: u64) { + assert!(self.engine().config().tunables.consume_fuel); + let remaining = i64::try_from(remaining).unwrap(); + self.inner.fuel_adj.set(remaining); + unsafe { + *self.inner.interrupts.fuel_consumed.get() = -remaining; } } } @@ -457,6 +500,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 { diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index 49a1809e3943..2b9679f459ab 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -7,7 +7,7 @@ mod kw { } struct FuelWast<'a> { - assertions: Vec<(u64, wast::Module<'a>)>, + assertions: Vec<(wast::Span, u64, wast::Module<'a>)>, } impl<'a> Parse<'a> for FuelWast<'a> { @@ -15,8 +15,8 @@ impl<'a> Parse<'a> for FuelWast<'a> { let mut assertions = Vec::new(); while !parser.is_empty() { assertions.push(parser.parens(|p| { - p.parse::()?; - Ok((p.parse()?, p.parens(|p| p.parse())?)) + let span = p.parse::()?.0; + Ok((span, p.parse()?, p.parens(|p| p.parse())?)) })?); } Ok(FuelWast { assertions }) @@ -28,18 +28,31 @@ fn run() -> Result<()> { let test = std::fs::read_to_string("tests/all/fuel.wast")?; let buf = ParseBuffer::new(&test)?; let mut wast = parser::parse::>(&buf)?; - for (fuel, module) in wast.assertions.iter_mut() { - assert_fuel(*fuel, &module.encode()?); + 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 assert_fuel(fuel: u64, wasm: &[u8]) { +fn fuel_consumed(wasm: &[u8]) -> u64 { + const MAX: u64 = 10_000; 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.set_fuel_remaining(MAX); drop(Instance::new(&store, &module, &[])); - assert_eq!(store.fuel_consumed(), Some(fuel)); + store.fuel_consumed().unwrap() } From 20142f46900ed8277051f0311ebb06731dec0b3e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 27 Jan 2021 11:10:10 -0800 Subject: [PATCH 03/11] 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. --- crates/cranelift/src/func_environ.rs | 59 ++++++++++++++++++---------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index f7a5bebd1222..8d2c63b15b56 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -129,8 +129,18 @@ pub struct FuncEnvironment<'module_environment> { 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, } @@ -159,6 +169,7 @@ impl<'module_environment> FuncEnvironment<'module_environment> { 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. @@ -431,6 +442,22 @@ impl<'module_environment> FuncEnvironment<'module_environment> { } } + 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 @@ -560,8 +587,7 @@ impl<'module_environment> FuncEnvironment<'module_environment> { } let fuel = builder.use_var(self.fuel_var); - let consumption = builder.ins().iconst(ir::types::I64, consumption); - let fuel = builder.ins().iadd(fuel, consumption); + let fuel = builder.ins().iadd_imm(fuel, consumption); builder.def_var(self.fuel_var, fuel); } @@ -590,18 +616,8 @@ impl<'module_environment> FuncEnvironment<'module_environment> { &mut self, builder: &mut FunctionBuilder<'_>, ) -> (ir::Value, ir::immediates::Offset32) { - // The address/offset of the fuel consumption is the second field of - // `VMInterrupts`, so first load the pointer to the `VMInterrupts` and - // then get an offset from that. - let vmctx = self.vmctx(builder.func); - let pointer_type = self.pointer_type(); - 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); ( - interrupt_ptr, + builder.use_var(self.vminterrupts_ptr), i32::from(self.offsets.vminterrupts_fuel_consumed()).into(), ) } @@ -671,7 +687,8 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m } fn after_locals(&mut self, num_locals: usize) { - self.fuel_var = Variable::new(num_locals); + 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 { @@ -1726,14 +1743,8 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m // For more information about this see comments in // `crates/environ/src/cranelift.rs` if self.tunables.interruptable { - let vmctx = self.vmctx(builder.func); let pointer_type = self.pointer_type(); - 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); + let interrupt_ptr = builder.use_var(self.vminterrupts_ptr); let interrupt = builder.ins().load( pointer_type, ir::MemFlags::trusted(), @@ -1789,6 +1800,12 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m 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); } From e07963a5aa75cfd72ddf68e29bf8f75ddaecd8e2 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 27 Jan 2021 14:20:31 -0800 Subject: [PATCH 04/11] Add tests fuel can abort infinite loops --- tests/all/fuel.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index 2b9679f459ab..2b6eb9f0cbd3 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -56,3 +56,70 @@ fn fuel_consumed(wasm: &[u8]) -> u64 { 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.set_fuel_remaining(10_000); + let error = Instance::new(&store, &module, &[]).err().unwrap(); + assert!( + error.to_string().contains("all fuel consumed"), + "bad error: {}", + error + ); + } +} From 72a2b790b6ef2cb5fb8a7094c9946230782bed75 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 27 Jan 2021 14:43:35 -0800 Subject: [PATCH 05/11] More fuzzing with fuel Use fuel to time out modules in addition to time, using fuzz input to figure out which. --- crates/fuzzing/src/oracles.rs | 61 +++++++++++++------ .../fuzz_targets/instantiate-maybe-invalid.rs | 11 +++- fuzz/fuzz_targets/instantiate-swarm.rs | 16 ++++- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index b474a8a1dbea..1a03c8f1f009 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -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,28 +78,38 @@ pub fn instantiate_with_config( wasm: &[u8], known_valid: bool, mut config: Config, - timeout: Option, + 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 { - let handle = store.interrupt_handle().unwrap(); - timeout_state.spawn_timeout(timeout, move || handle.interrupt()); + match timeout { + Timeout::Fuel(fuel) => store.set_fuel_remaining(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); @@ -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::().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), } } diff --git a/fuzz/fuzz_targets/instantiate-maybe-invalid.rs b/fuzz/fuzz_targets/instantiate-maybe-invalid.rs index 219986a25a60..46a749f5e8a4 100644 --- a/fuzz/fuzz_targets/instantiate-maybe-invalid.rs +++ b/fuzz/fuzz_targets/instantiate-maybe-invalid.rs @@ -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) + }, ); }); diff --git a/fuzz/fuzz_targets/instantiate-swarm.rs b/fuzz/fuzz_targets/instantiate-swarm.rs index d9c5bdb6450c..b534cc49709f 100644 --- a/fuzz/fuzz_targets/instantiate-swarm.rs +++ b/fuzz/fuzz_targets/instantiate-swarm.rs @@ -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| { +fuzz_target!(|pair: (bool, ConfiguredModule)| { + 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) + }, + ); }); From 66b2cb0e454d1391011e032c75ca86a47587a737 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 28 Jan 2021 07:19:42 -0800 Subject: [PATCH 06/11] Update docs on trapping instructions --- crates/cranelift/src/func_environ.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 8d2c63b15b56..a9481aca93ee 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -558,6 +558,15 @@ impl<'module_environment> FuncEnvironment<'module_environment> { // 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 From cb9e8c14058546beee823072dea83f097e255270 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 28 Jan 2021 07:22:06 -0800 Subject: [PATCH 07/11] Fix doc links --- crates/wasmtime/src/config.rs | 2 ++ crates/wasmtime/src/store.rs | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 7688e6c8600e..a662599dc8e2 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -152,6 +152,8 @@ impl Config { /// 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 diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 2707e6fefe8c..0544088141b1 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -436,9 +436,10 @@ 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`] then - /// this function will return `None`. Also note that fuel, if enabled, must - /// be originally configured via [`Store::set_fuel_remaining`]. + /// 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::set_fuel_remaining`]. /// /// Finally, this will only return the amount of fuel consumed since /// [`Store::set_fuel_remaining`] was last called, so this does not return @@ -456,18 +457,18 @@ impl Store { /// wasm to consume while executing. /// /// For this method to work fuel consumption must be enabled via - /// [`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. + /// [`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`] did not have fuel - /// consumption enabled. + /// This function will panic if the store's [`Config`](crate::Config) did + /// not have fuel consumption enabled. /// /// This funtion will also panic if `remaining` is larger than /// `i64::max_value()`. From f0787784ce4afd4012c938b9efaa44d3beafa02a Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 28 Jan 2021 10:52:00 -0800 Subject: [PATCH 08/11] Fix a fuzz test --- tests/all/fuzzing.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/all/fuzzing.rs b/tests/all/fuzzing.rs index 3e46af2d724d..e5c1906886ed 100644 --- a/tests/all/fuzzing.rs +++ b/tests/all/fuzzing.rs @@ -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); } From b892ff99b1d0e71c241b43da8a00045ff11cb919 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 28 Jan 2021 13:46:11 -0800 Subject: [PATCH 09/11] Change setting fuel to adding fuel --- crates/fuzzing/src/oracles.rs | 8 +++---- crates/wasmtime/src/store.rs | 41 ++++++++++++++++++++++------------- tests/all/fuel.rs | 5 ++--- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 1a03c8f1f009..2fc4532de41c 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -95,7 +95,7 @@ pub fn instantiate_with_config( let mut timeout_state = SignalOnDrop::default(); match timeout { - Timeout::Fuel(fuel) => store.set_fuel_remaining(fuel), + 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 @@ -418,7 +418,7 @@ pub fn spectest(fuzz_config: crate::generators::Config, test: crate::generators: config.wasm_bulk_memory(false); let store = Store::new(&Engine::new(&config)); if fuzz_config.consume_fuel { - store.set_fuel_remaining(i64::max_value() as u64); + store.add_fuel(u64::max_value()); } let mut wast_context = WastContext::new(store); wast_context.register_spectest().unwrap(); @@ -442,7 +442,7 @@ pub fn table_ops( let engine = Engine::new(&config); let store = Store::new(&engine); if fuzz_config.consume_fuel { - store.set_fuel_remaining(i64::max_value() as u64); + store.add_fuel(u64::max_value()); } let wasm = ops.to_wasm_binary(); @@ -557,7 +557,7 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &crate::generators::Con let wasmtime_engine = Engine::new(&wasmtime_config); let wasmtime_store = Store::new(&wasmtime_engine); if config.consume_fuel { - wasmtime_store.set_fuel_remaining(i64::max_value() as u64); + wasmtime_store.add_fuel(u64::max_value()); } let wasmtime_module = Module::new(&wasmtime_engine, &wasm).expect("Wasmtime can compile module"); diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 0544088141b1..49fa864ceeec 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -440,11 +440,6 @@ impl Store { /// [`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::set_fuel_remaining`]. - /// - /// Finally, this will only return the amount of fuel consumed since - /// [`Store::set_fuel_remaining`] was last called, so this does not return - /// the total amount of fuel consumed for the entire lifetime of this - /// [`Store`]. pub fn fuel_consumed(&self) -> Option { if !self.engine().config().tunables.consume_fuel { return None; @@ -453,8 +448,7 @@ impl Store { Some(u64::try_from(self.inner.fuel_adj.get() + consumed).unwrap()) } - /// Updates the amount of fuel remaining in this [`Store`] available for - /// wasm to consume while executing. + /// 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 @@ -469,15 +463,32 @@ impl Store { /// /// This function will panic if the store's [`Config`](crate::Config) did /// not have fuel consumption enabled. - /// - /// This funtion will also panic if `remaining` is larger than - /// `i64::max_value()`. - pub fn set_fuel_remaining(&self, remaining: u64) { + pub fn add_fuel(&self, fuel: u64) { assert!(self.engine().config().tunables.consume_fuel); - let remaining = i64::try_from(remaining).unwrap(); - self.inner.fuel_adj.set(remaining); - unsafe { - *self.inner.interrupts.fuel_consumed.get() = -remaining; + + // 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(); + } } } } diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index 2b6eb9f0cbd3..b1b73bc6a805 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -46,13 +46,12 @@ fn run() -> Result<()> { } fn fuel_consumed(wasm: &[u8]) -> u64 { - const MAX: u64 = 10_000; 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.set_fuel_remaining(MAX); + store.add_fuel(u64::max_value()); drop(Instance::new(&store, &module, &[])); store.fuel_consumed().unwrap() } @@ -114,7 +113,7 @@ fn iloop() { let engine = Engine::new(&config); let module = Module::new(&engine, wat).unwrap(); let store = Store::new(&engine); - store.set_fuel_remaining(10_000); + store.add_fuel(10_000); let error = Instance::new(&store, &module, &[]).err().unwrap(); assert!( error.to_string().contains("all fuel consumed"), From a8a2ad647e25702a262e170cf00cb6e945b47533 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 28 Jan 2021 14:08:55 -0800 Subject: [PATCH 10/11] Fix a doc link --- crates/wasmtime/src/store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 49fa864ceeec..10564d79dd43 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -439,7 +439,7 @@ impl Store { /// 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::set_fuel_remaining`]. + /// originally configured via [`Store::add_fuel`]. pub fn fuel_consumed(&self) -> Option { if !self.engine().config().tunables.consume_fuel { return None; From c4d7f385d69f0d36d0e16a9fa9fb9f84e5e810ce Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 28 Jan 2021 14:09:48 -0800 Subject: [PATCH 11/11] Squelch some rustdoc warnings --- cranelift/src/souper_harvest.rs | 2 +- crates/profiling/src/jitdump_linux.rs | 2 +- crates/runtime/src/externref.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cranelift/src/souper_harvest.rs b/cranelift/src/souper_harvest.rs index 5bfa1fe62913..e92a50230249 100644 --- a/cranelift/src/souper_harvest.rs +++ b/cranelift/src/souper_harvest.rs @@ -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 +/// #[derive(StructOpt)] pub struct Options { /// Specify an input file to be used. Use '-' for stdin. diff --git a/crates/profiling/src/jitdump_linux.rs b/crates/profiling/src/jitdump_linux.rs index ef6684c95fa2..0f3a4bb00d48 100644 --- a/crates/profiling/src/jitdump_linux.rs +++ b/crates/profiling/src/jitdump_linux.rs @@ -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 +//! //! //! Usage Example: //! Record diff --git a/crates/runtime/src/externref.rs b/crates/runtime/src/externref.rs index dd0a4f147513..b5a8ef30bb9d 100644 --- a/crates/runtime/src/externref.rs +++ b/crates/runtime/src/externref.rs @@ -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 +//! use std::alloc::Layout; use std::any::Any;