Skip to content

Exempt module instantiation from fuel (v36-compatible set_fuel(0)) #14205

Description

@AndSDev

Feature

Add a Config/Tunables option that exempts the synthesized ModuleStartup function from fuel metering, restoring the ≤ v36 behavior where Instance::new with store.set_fuel(0) succeeds for modules that have no (start ...) function. Since #13487 ("Move most module initialization to compiled code"), module initialization (globals, segments, tables, etc.) runs as compiled Wasm inside ModuleStartup (crates/cranelift/src/func_environ.rs) and is intentionally fuel-metered, so Instance::new now consumes fuel even for modules with no (start ...) and a small fuel budget makes instantiation itself trap with Trap::OutOfFuel. Fuel is meant to bound Wasm function execution, not to forbid instantiation.

Example

A module that requires a startup function but has no (start ...) — here a passive element segment, which is initialized once during instantiation:

(module
  (func $f (result i32) i32.const 42)
  (table 1 funcref)
  (elem $passive func $f))
// Cargo.toml: [dependencies] wasmtime = "48"
use wasmtime::*;

const MODULE: &str = r#"
    (module
      (func $f (result i32) i32.const 42)
      (table 1 funcref)
      (elem $passive func $f))
"#;

fn main() -> Result<()> {
    let mut config = Config::new();
    config.consume_fuel(true);
    let engine = Engine::new(&config)?;
    let module = Module::new(&engine, MODULE)?;

    // No `(start ...)`, but the passive element segment forces Wasmtime to
    // synthesize a `ModuleStartup` function, and that function is fuel-metered.
    for fuel in [0, 1, 2] {
        let mut store = Store::new(&engine, ());
        store.set_fuel(fuel)?;

        match Instance::new(&mut store, &module, &[]) {
            Ok(_) => {
                let consumed = fuel - store.get_fuel()?;
                println!("fuel={fuel}: instantiation succeeded, consumed {consumed} unit(s)");
            }
            Err(e) => println!("fuel={fuel}: instantiation failed: {e}"),
        }
    }
    Ok(())
}

Output (reproduced on main at f1412a598f, Wasmtime 48.0.0, Linux x86_64):

fuel=0: instantiation failed: wasm trap: all fuel consumed by WebAssembly
fuel=1: instantiation failed: wasm trap: all fuel consumed by WebAssembly
fuel=2: instantiation succeeded, consumed 1 unit(s)

fuel=0 traps inside Instance::new even though the module has no (start ...), and fuel=1 traps as well because the startup function's flat entry charge of 1 is mandatory; only fuel=2 succeeds, showing that instantiation consumes exactly 1 unit for this module. In ≤ v36, set_fuel(0) + Instance::new succeeded here, with fuel spent only when running an exported function. A module that needs no startup function still instantiates fine at fuel=0 today.

Why a cost parameter alone is insufficient

With store.set_fuel(0) the fuel counter is 0. fuel_check (func_environ.rs:626) traps once the counter becomes >= 0, and fuel_function_entry runs that check plus the mandatory initial fuel_consumed: 1 (func_environ.rs:296) at the entry of every compiled function, including ModuleStartup. So even setting every startup cost to 0 still leaves 0 + 1 >= 0 → trap. Only skipping the fuel entry/exit handling for ModuleStartup restores the v36 behavior.

Benefit

  • v36 compatibility: instantiation with set_fuel(0) works again. Today it traps for any module whose initialization cannot be constant-folded or precomputed (measured: a passive elem, a complicated global, and an active externref table — none with (start ...) — each consume 1 unit of fuel on Instance::new, and trap when the budget is exhausted). Fuel should bound Wasm function execution, not forbid instantiation.

Implementation

  • Add Config::consume_fuel_during_module_initialization(bool) (or an equivalent Tunables field), defaulting to true for current behavior. When false, compile FuncKey::ModuleStartup with fuel accounting disabled for that function: skip fuel_function_entry/fuel_function_exit so it neither charges the flat entry cost (fuel_consumed: 1) nor runs the entry >= 0 check that makes set_fuel(0) trap.
  • The (start ...) call itself remains ordinary Wasm (it consumed fuel in v36 as well); scope the exemption to the synthesized initialization body.
  • Tests: set_fuel(0) + Instance::new on a module that needs a startup function (e.g. a passive elem); option on → traps, option off → succeeds.

Alternatives

  • Configurable startup cost: add a dedicated cost knob for the startup function, e.g. OperatorCost::module_startup (a flat per-instance charge, defaulting to 1 to preserve current metering), replacing the hardcoded fuel_consumed: 1 entry charge that ModuleStartup currently pays. Setting it to 0 restores the v36 set_fuel(0) behavior, and values > 0 keep metering while letting embedders charge a custom amount for instantiation-time work — strictly more flexible than the boolean flag. The required crutch: zeroing the cost is not enough on its own, because fuel_check (func_environ.rs:626) traps once the counter becomes >= 0 and fuel_function_entry runs that check unconditionally at ModuleStartup entry. So the 0 case must also skip fuel_function_entry/fuel_function_exit for FuncKey::ModuleStartup — the same surgical skip the boolean flag needs, but keyed on cost == 0 rather than a separate option.

Metadata

Metadata

Assignees

No one assigned

    Labels

    wasmtime:fuelWasmtime issues related to fuel consumption

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions