AndSDev opened issue #14204:
Operators inside constant expressions (
globalinitializers, element/data segment offsets, etc.) are each charged exactly1fuel unit regardless of theOperatorCosttable supplied viaConfig::operator_cost. Regular function code honors the table; const-expr code does not.Test Case
A module whose global initializer is a multi-op const-expr. Wasmtime does not constant-fold
i32.addat compile time, so it runs throughtranslate_const_exprat instantiation. Astartfunction is present so the charges buffered during instantiation are actually flushed to the fuel counter (see #14203).(module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start))Steps to Reproduce
//! Cargo.toml //! [dependencies] //! wasmtime = "48" use wasmtime::*; const WAT: &str = r#" (module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start)) "#; /// Instantiate `WAT` and return the fuel consumed by instantiation. fn instantiation_fuel(config: &Config) -> Result<u64> { let engine = Engine::new(config)?; let module = Module::new(&engine, WAT)?; let mut store = Store::new(&engine, ()); store.set_fuel(u64::MAX)?; 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(u64::MAX - store.get_fuel()?) } fn main() -> Result<()> { // Default table: every op costs 1. let mut default = Config::new(); default.consume_fuel(true); println!("default: {}", instantiation_fuel(&default)?); // Custom table: i32.add costs 100. let mut cost = OperatorCost::new(); cost.I32Add = 100; let mut custom = Config::new(); custom.consume_fuel(true).operator_cost(cost); println!("custom: {}", instantiation_fuel(&custom)?); Ok(()) }Expected Results
default: 6 custom: 105Breakdown for the custom table (
i32.const = 1,i32.add = 100):
Step Fuel module-init entry cost 1 i32.const 1(const-expr op)1 i32.const 2(const-expr op)1 i32.add(const-expr op)100 manual accounting for the startcall1 startfunction entry cost1 Total 105 With the default table every op costs
1, so the total is6.Actual Results
default: 6 custom: 6Each const-expr op is charged a fixed
1(see Extra Info), so theI32Add=100setting has no effect. The6for the default table is coincidentally correct only because all default costs are1.Versions and Environment
Wasmtime version or commit:
mainatffb04089ea(alsorelease-48.0.0)Operating system: Linux
Architecture: x86_64
Extra Info
Extra Info
Root cause is in
FuncEnvironment::translate_const_expr(crates/cranelift/src/func_environ.rs:6101): each const-expr op doesself.fuel_consumed += 1, ignoringtunables.operator_cost. TheOperatorCost::variableper-byte/per-element costs (e.g.array.new_fixedwith a large constant length) are likewise not applied, soConfig::operator_costis silently ineffective for all instantiation-time work (global initializers, element/data segments, segment offsets).
AndSDev added the bug label to Issue #14204.
AndSDev edited issue #14204:
Operators inside constant expressions (
globalinitializers, element/data segment offsets, etc.) are each charged exactly1fuel unit regardless of theOperatorCosttable supplied viaConfig::operator_cost. Regular function code honors the table; const-expr code does not.Test Case
A module whose global initializer is a multi-op const-expr. Wasmtime does not constant-fold
i32.addat compile time, so it runs throughtranslate_const_exprat instantiation. Astartfunction is present so the charges buffered during instantiation are actually flushed to the fuel counter (see #14203).(module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start))Steps to Reproduce
//! Cargo.toml //! [dependencies] //! wasmtime = "48" use wasmtime::*; const WAT: &str = r#" (module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start)) "#; /// Instantiate `WAT` and return the fuel consumed by instantiation. fn instantiation_fuel(config: &Config) -> Result<u64> { let engine = Engine::new(config)?; let module = Module::new(&engine, WAT)?; let mut store = Store::new(&engine, ()); store.set_fuel(u64::MAX)?; 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(u64::MAX - store.get_fuel()?) } fn main() -> Result<()> { // Default table: every op costs 1. let mut default = Config::new(); default.consume_fuel(true); println!("default: {}", instantiation_fuel(&default)?); // Custom table: i32.add costs 100. let mut cost = OperatorCost::new(); cost.I32Add = 100; let mut custom = Config::new(); custom.consume_fuel(true).operator_cost(cost); println!("custom: {}", instantiation_fuel(&custom)?); Ok(()) }Expected Results
default: 6 custom: 105Breakdown for the custom table (
i32.const = 1,i32.add = 100):
Step Fuel module-init entry cost 1 i32.const 1(const-expr op)1 i32.const 2(const-expr op)1 i32.add(const-expr op)100 manual accounting for the startcall1 startfunction entry cost1 Total 105 With the default table every op costs
1, so the total is6.Actual Results
default: 6 custom: 6Each const-expr op is charged a fixed
1(see Extra Info), so theI32Add=100setting has no effect. The6for the default table is coincidentally correct only because all default costs are1.Versions and Environment
Wasmtime version or commit:
mainatffb04089ea(alsorelease-48.0.0)Operating system: Linux
Architecture: x86_64
Extra Info
Root cause is in
FuncEnvironment::translate_const_expr(crates/cranelift/src/func_environ.rs:6101): each const-expr op doesself.fuel_consumed += 1, ignoringtunables.operator_cost. TheOperatorCost::variableper-byte/per-element costs (e.g.array.new_fixedwith a large constant length) are likewise not applied, soConfig::operator_costis silently ineffective for all instantiation-time work (global initializers, element/data segments, segment offsets).P.S. The same hardcoded-
1bug exists inFuncEnvironment::module_start(crates/cranelift/src/func_environ.rs:6082). That function synthesizes the call to the(start ...)function and manually replicates thefuel_before_opaccounting forOperator::Call:// 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; // hardcoded, ignores OperatorCost::Call self.fuel_increment_var(builder); self.fuel_save_from_var(builder); }
AndSDev edited issue #14204:
Operators inside constant expressions (
globalinitializers, element/data segment offsets, etc.) are each charged exactly1fuel unit regardless of theOperatorCosttable supplied viaConfig::operator_cost. Regular function code honors the table; const-expr code does not.The same hardcoded-
1bug exists inFuncEnvironment::module_start(crates/cranelift/src/func_environ.rs:6082).Test Case
A module whose global initializer is a multi-op const-expr. Wasmtime does not constant-fold
i32.addat compile time, so it runs throughtranslate_const_exprat instantiation. Astartfunction is present so the charges buffered during instantiation are actually flushed to the fuel counter (see #14203).(module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start))Steps to Reproduce
//! Cargo.toml //! [dependencies] //! wasmtime = "48" use wasmtime::*; const WAT: &str = r#" (module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start)) "#; /// Instantiate `WAT` and return the fuel consumed by instantiation. fn instantiation_fuel(config: &Config) -> Result<u64> { let engine = Engine::new(config)?; let module = Module::new(&engine, WAT)?; let mut store = Store::new(&engine, ()); store.set_fuel(u64::MAX)?; 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(u64::MAX - store.get_fuel()?) } fn main() -> Result<()> { // Default table: every op costs 1. let mut default = Config::new(); default.consume_fuel(true); println!("default: {}", instantiation_fuel(&default)?); // Custom table: i32.add costs 100. let mut cost = OperatorCost::new(); cost.I32Add = 100; let mut custom = Config::new(); custom.consume_fuel(true).operator_cost(cost); println!("custom: {}", instantiation_fuel(&custom)?); Ok(()) }Expected Results
default: 6 custom: 105Breakdown for the custom table (
i32.const = 1,i32.add = 100):
Step Fuel module-init entry cost 1 i32.const 1(const-expr op)1 i32.const 2(const-expr op)1 i32.add(const-expr op)100 manual accounting for the startcall1 startfunction entry cost1 Total 105 With the default table every op costs
1, so the total is6.Actual Results
default: 6 custom: 6Each const-expr op is charged a fixed
1(see Extra Info), so theI32Add=100setting has no effect. The6for the default table is coincidentally correct only because all default costs are1.Versions and Environment
Wasmtime version or commit:
mainatffb04089ea(alsorelease-48.0.0)Operating system: Linux
Architecture: x86_64
Extra Info
Root cause is in
FuncEnvironment::translate_const_expr(crates/cranelift/src/func_environ.rs:6101): each const-expr op doesself.fuel_consumed += 1, ignoringtunables.operator_cost. TheOperatorCost::variableper-byte/per-element costs (e.g.array.new_fixedwith a large constant length) are likewise not applied, soConfig::operator_costis silently ineffective for all instantiation-time work (global initializers, element/data segments, segment offsets).P.S. The same hardcoded-
1bug exists inFuncEnvironment::module_start(crates/cranelift/src/func_environ.rs:6082). That function synthesizes the call to the(start ...)function and manually replicates thefuel_before_opaccounting forOperator::Call:// 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; // hardcoded, ignores OperatorCost::Call self.fuel_increment_var(builder); self.fuel_save_from_var(builder); }
alexcrichton added the wasmtime:fuel label to Issue #14204.
arcusbuilds commented on issue #14204:
I would like to work on it!
cfallin closed issue #14204:
Operators inside constant expressions (
globalinitializers, element/data segment offsets, etc.) are each charged exactly1fuel unit regardless of theOperatorCosttable supplied viaConfig::operator_cost. Regular function code honors the table; const-expr code does not.The same hardcoded-
1bug exists inFuncEnvironment::module_start(crates/cranelift/src/func_environ.rs:6082).Test Case
A module whose global initializer is a multi-op const-expr. Wasmtime does not constant-fold
i32.addat compile time, so it runs throughtranslate_const_exprat instantiation. Astartfunction is present so the charges buffered during instantiation are actually flushed to the fuel counter (see #14203).(module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start))Steps to Reproduce
//! Cargo.toml //! [dependencies] //! wasmtime = "48" use wasmtime::*; const WAT: &str = r#" (module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) (export "g" (global $g)) (func $start) (start $start)) "#; /// Instantiate `WAT` and return the fuel consumed by instantiation. fn instantiation_fuel(config: &Config) -> Result<u64> { let engine = Engine::new(config)?; let module = Module::new(&engine, WAT)?; let mut store = Store::new(&engine, ()); store.set_fuel(u64::MAX)?; 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(u64::MAX - store.get_fuel()?) } fn main() -> Result<()> { // Default table: every op costs 1. let mut default = Config::new(); default.consume_fuel(true); println!("default: {}", instantiation_fuel(&default)?); // Custom table: i32.add costs 100. let mut cost = OperatorCost::new(); cost.I32Add = 100; let mut custom = Config::new(); custom.consume_fuel(true).operator_cost(cost); println!("custom: {}", instantiation_fuel(&custom)?); Ok(()) }Expected Results
default: 6 custom: 105Breakdown for the custom table (
i32.const = 1,i32.add = 100):
Step Fuel module-init entry cost 1 i32.const 1(const-expr op)1 i32.const 2(const-expr op)1 i32.add(const-expr op)100 manual accounting for the startcall1 startfunction entry cost1 Total 105 With the default table every op costs
1, so the total is6.Actual Results
default: 6 custom: 6Each const-expr op is charged a fixed
1(see Extra Info), so theI32Add=100setting has no effect. The6for the default table is coincidentally correct only because all default costs are1.Versions and Environment
Wasmtime version or commit:
mainatffb04089ea(alsorelease-48.0.0)Operating system: Linux
Architecture: x86_64
Extra Info
Root cause is in
FuncEnvironment::translate_const_expr(crates/cranelift/src/func_environ.rs:6101): each const-expr op doesself.fuel_consumed += 1, ignoringtunables.operator_cost. TheOperatorCost::variableper-byte/per-element costs (e.g.array.new_fixedwith a large constant length) are likewise not applied, soConfig::operator_costis silently ineffective for all instantiation-time work (global initializers, element/data segments, segment offsets).P.S. The same hardcoded-
1bug exists inFuncEnvironment::module_start(crates/cranelift/src/func_environ.rs:6082). That function synthesizes the call to the(start ...)function and manually replicates thefuel_before_opaccounting forOperator::Call:// 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; // hardcoded, ignores OperatorCost::Call self.fuel_increment_var(builder); self.fuel_save_from_var(builder); }
Last updated: Aug 30 2026 at 09:07 UTC