Stream: git-wasmtime

Topic: wasmtime / issue #14203 Fuel charges for const-expr initi...


view this post on Zulip Wasmtime GitHub notifications bot (Aug 25 2026 at 13:15):

AndSDev opened issue #14203:

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):

(module
  (global i32 (i32.add (i32.const 1) (i32.const 2)))
  (export "g2" (global 0)))

The module declares no start function.

Steps to Reproduce

// 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<u64> {
    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:

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.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 25 2026 at 13:15):

AndSDev added the bug label to Issue #14203.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 25 2026 at 18:06):

alexcrichton added the wasmtime:fuel label to Issue #14203.


Last updated: Aug 30 2026 at 09:07 UTC