diff --git a/RELEASES.md b/RELEASES.md index c152459a49d0..7ddd1e42cbf3 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,13 @@ Unreleased. ### Changed +* `Config::operator_cost` now applies to operators inside constant expressions + (global initializers, element and data segment offsets, element segment + expressions) and to the synthesized call to a module's `start` function. + Previously each of those was charged 1 fuel unit regardless of the configured + cost. + [#14215](https://github.com/bytecodealliance/wasmtime/pull/14215) + -------------------------------------------------------------------------------- Release notes for previous releases of Wasmtime can be found on the respective diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index b722c22b3bac..0da899c472d8 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -6115,7 +6115,9 @@ impl FuncEnvironment<'_> { // Manuall manage fuel around the call as the `Call` opcode does for // normal wasm to ensure that it's correctly accounted for. if self.tunables.consume_fuel { - self.fuel_consumed += 1; + self.fuel_consumed += self.tunables.operator_cost.cost(&Operator::Call { + function_index: func.as_u32(), + }); self.fuel_increment_var(builder); self.fuel_save_from_var(builder); } @@ -6145,7 +6147,7 @@ impl FuncEnvironment<'_> { let mut stack = Vec::new(); for op in expr.ops() { if self.tunables.consume_fuel { - self.fuel_consumed += 1; + self.fuel_consumed += self.tunables.operator_cost.cost(&op.to_operator()); } match op { ConstOp::I32Const(i) => { diff --git a/crates/environ/src/types.rs b/crates/environ/src/types.rs index 2f7782e1ea24..c3c5b1f759ce 100644 --- a/crates/environ/src/types.rs +++ b/crates/environ/src/types.rs @@ -2145,6 +2145,67 @@ impl ConstOp { } }) } + + /// Convert a `ConstOp` back to a `wasmparser::Operator`. + /// + /// `RefNull`'s heap type does not round-trip, so only use this where the + /// immediates do not matter, such as looking up an operator's fuel cost. + pub fn to_operator(&self) -> wasmparser::Operator<'static> { + use wasmparser::{AbstractHeapType, HeapType, Ieee32, Ieee64, Operator, V128}; + match self { + ConstOp::I32Const(value) => Operator::I32Const { value: *value }, + ConstOp::I64Const(value) => Operator::I64Const { value: *value }, + ConstOp::F32Const(bits) => Operator::F32Const { + value: Ieee32::from(f32::from_bits(*bits)), + }, + ConstOp::F64Const(bits) => Operator::F64Const { + value: Ieee64::from(f64::from_bits(*bits)), + }, + ConstOp::V128Const(value) => Operator::V128Const { + value: V128::from(*value as i128), + }, + ConstOp::GlobalGet(index) => Operator::GlobalGet { + global_index: index.as_u32(), + }, + ConstOp::RefI31 => Operator::RefI31, + ConstOp::RefNull(_) => Operator::RefNull { + hty: HeapType::Abstract { + shared: false, + ty: AbstractHeapType::Any, + }, + }, + ConstOp::RefFunc(index) => Operator::RefFunc { + function_index: index.as_u32(), + }, + ConstOp::I32Add => Operator::I32Add, + ConstOp::I32Sub => Operator::I32Sub, + ConstOp::I32Mul => Operator::I32Mul, + ConstOp::I64Add => Operator::I64Add, + ConstOp::I64Sub => Operator::I64Sub, + ConstOp::I64Mul => Operator::I64Mul, + ConstOp::StructNew { struct_type_index } => Operator::StructNew { + struct_type_index: struct_type_index.as_u32(), + }, + ConstOp::StructNewDefault { struct_type_index } => Operator::StructNewDefault { + struct_type_index: struct_type_index.as_u32(), + }, + ConstOp::ArrayNew { array_type_index } => Operator::ArrayNew { + array_type_index: array_type_index.as_u32(), + }, + ConstOp::ArrayNewDefault { array_type_index } => Operator::ArrayNewDefault { + array_type_index: array_type_index.as_u32(), + }, + ConstOp::ArrayNewFixed { + array_type_index, + array_size, + } => Operator::ArrayNewFixed { + array_type_index: array_type_index.as_u32(), + array_size: *array_size, + }, + ConstOp::ExternConvertAny => Operator::ExternConvertAny, + ConstOp::AnyConvertExtern => Operator::AnyConvertExtern, + } + } } /// The type that can be used to index into [Memory] and [Table]. diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index f172503ca9de..cf9163c2cf37 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -651,6 +651,10 @@ impl Config { /// configures per-byte, per-element, and per-page costs for operators whose /// work depends on a runtime operand. /// + /// These costs apply both to operators in function bodies and to operators + /// in constant expressions evaluated at instantiation time, such as global + /// initializers and element or data segment offsets. + /// /// This is only relevant when [`Config::consume_fuel`] is enabled. pub fn operator_cost(&mut self, cost: OperatorCost) -> &mut Self { self.tunables.operator_cost = Some(OperatorCostStrategy::table(cost)); diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index 1192fdc46d8c..f6a06a21f025 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -1052,3 +1052,76 @@ fn fuel_around_table_grow() -> Result<()> { assert_eq!(trap, Trap::TableOutOfBounds); Ok(()) } + +#[wasmtime_test(wasm_features(extended_const))] +#[cfg_attr(miri, ignore)] +fn const_expr_honors_operator_cost(config: &mut Config) -> Result<()> { + const WAT: &str = r#" + (module + (global $g i32 (i32.add (i32.const 1) (i32.const 2))) + (export "g" (global $g)) + (func $start) + (start $start)) + "#; + + fn instantiation_fuel(config: &mut Config, op_cost: OperatorCost) -> Result { + config.consume_fuel(true).operator_cost(op_cost); + let engine = Engine::new(config)?; + let module = Module::new(&engine, WAT)?; + + let mut store = Store::new(&engine, ()); + store.set_fuel(10_000)?; + let instance = Instance::new(&mut store, &module, &[])?; + + let g = instance + .get_global(&mut store, "g") + .unwrap() + .get(&mut store); + assert_eq!(g.i32(), Some(3), "global initializer did not run"); + + Ok(10_000 - store.get_fuel()?) + } + + assert_eq!(instantiation_fuel(config, OperatorCost::default())?, 6); + + let custom = OperatorCost { + I32Const: 7, + I32Add: 100, + ..Default::default() + }; + assert_eq!(instantiation_fuel(config, custom)?, 117); + + Ok(()) +} + +#[wasmtime_test] +#[cfg_attr(miri, ignore)] +fn module_start_call_honors_operator_cost(config: &mut Config) -> Result<()> { + const WAT: &str = r#" + (module + (func $start) + (start $start)) + "#; + + fn instantiation_fuel(config: &mut Config, op_cost: OperatorCost) -> Result { + config.consume_fuel(true).operator_cost(op_cost); + let engine = Engine::new(config)?; + let module = Module::new(&engine, WAT)?; + + let mut store = Store::new(&engine, ()); + store.set_fuel(10_000)?; + Instance::new(&mut store, &module, &[])?; + + Ok(10_000 - store.get_fuel()?) + } + + assert_eq!(instantiation_fuel(config, OperatorCost::default())?, 3); + + let custom = OperatorCost { + Call: 50, + ..Default::default() + }; + assert_eq!(instantiation_fuel(config, custom)?, 52); + + Ok(()) +}