bytecodealliance / bytecodealliance/wasmtime
Fuel charges for const-expr initialization are dropped when the module has no `start` function
- Dominant language
- Rust
- Stars
- 18.6k
- Forks
- 1.8k
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 126
Description
### Test Case
A module whose only "complicated" global initializer is a three-op extended const-expr. `ConstExpr::const_eval` only folds a single `*.const` op, so `i32.add` is deferred to the synthesized module-startup function (`FuncKey::ModuleStartup`):
```wat
(module
(global i32 (i32.add (i32.const 1) (i32.const 2)))
(export "g2" (global 0)))
```
The module declares no `start` function.
### Steps to Reproduce
```rust
// Cargo.toml: [dependencies] wasmtime = "48"
use wasmtime::*;
const MODULE: &str = r#"(module
(global i32 (i32.add (i32.const 1) (i32.const 2)))
(export "g2" (global 0)))"#;
const MODULE_WITH_START: &str = r#"(module
(func $s)
(start $s)
(global i32 (i32.add (i32.const 1) (i32.const 2)))
(export "g2" (global 0)))"#;
fn measure(wat: &str) -> Result {
let mut config = Config::new();
config.consume_fuel(true);
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 v = instance
.get_global(&mut store, "g2")
.unwrap()
.get(&mut store)
.i32();
assert_eq!(v, Some(3), "initializer did not run");
Ok(u64::MAX - store.get_fuel()?)
}
fn main() -> Result<()> {
println!("without start: consumed = {}", measure(MODULE)?);
println!("with start: consumed = {}", measure(MODULE_WITH_START)?);
Ok(())
}
```
Output:
```text
without start: consumed = 1
with start: consumed = 6
```
Reproduced on `main` (`ffb04089ea`) and `release-48.0.0`, Linux x86_64.
### Expected Results
The const-expr runs during instantiation, so its work must be metered. Its three ops each cost `1` with the default `OperatorCost`, plus the startup function's flat entry cost of `1`, so `without start` should consume `4`. The `with start` control adds one call-site charge in the startup function and one entry charge in `$s`, hence `6`.
### Actual Results
`without start` consumes `1` — only the flat entry cost of the startup function. The three const-expr op charges are accumulated into the translator's `fuel_consumed` buffer but never flushed into `fuel_var`, so they are discarded when the function exits. The `with start` control consumes `6`, proving the work is real but only gets metered because `module_start` happens to flush the buffer before the call.
### Versions and Environment
Wasmtime version or commit: `main` at `ffb04089ea` (also `release-48.0.0`)
Operating system: Linux
Architecture: x86_64
### Extra Info
With `consume_fuel(true)`, instantiation-time const-expr work is silently un-metered when there is no `start` function: `translate_const_expr` buffers each op's charge into `fuel_consumed`, the module-startup function never flushes that buffer into `fuel_var`, and `fuel_function_exit` saves `fuel_var` without flushing the leftover. A `start` function happens to flush it via `module_start`, which is why the control shows `6`.
**Impact:** a metering/DoS accounting bypass — large `array.new_fixed`, many element-segment expressions, or long initializer chains can run at essentially zero fuel during instantiation. Affects all init paths routed through `translate_const_expr` (complicated globals, table fills, active/passive element segments, non-static memory-segment offsets).
**Suggested fix:** flush leftover `fuel_consumed` before saving `fuel_var`, e.g. call `fuel_increment_var` at the end of module-startup translation or inside `fuel_function_exit`.
Contributor guide
Assessment
This issue has not been assessed yet.