bytecodealliance / bytecodealliance/wasmtime

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

Open
#14,205 1 comment 0 reactions 0 assignees View on GitHub
wasmtime:fuel
Dominant language
Rust
Stars
18.6k
Forks
1.8k
Avg merge
1d 18h
Merged PRs (30d)
126

Description

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

```wat
(module
(func $f (result i32) i32.const 42)
(table 1 funcref)
(elem $passive func $f))
```

```rust
// 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.

Contributor guide

Open the contributing guide

Research direction

Start in crates/cranelift/src/func_environ.rs, especially fuel_function_entry, fuel_function_exit, fuel_check, and the handling of FuncKey::ModuleStartup. Trace how Config or Tunables reaches compilation, then add coverage for a passive elem module with set_fuel(0), verifying the current option traps and the exemption allows Instance::new to succeed. Confirm that an explicit (start ...) call remains fuel-metered.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend, compilers
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.