Repository navigation
fix(cranelift): charge fuel per region to match the rwasm VM - #11
Merged
Merged
Conversation
Wasmtime accumulated operator costs in a register and flushed them only at block exits, calls and known traps, while the rwasm VM charges every straight-line region in full when it is entered. The two engines therefore burned different fuel on any path that did not complete (traps, halts, out-of-fuel), and Wasmtime could overrun the limit on a leaf function and still return success. Fuel metering now mirrors the rwasm translator: a region opens at function entry, at every loop header, in each `if`/`else` arm, after every `end` and on the fall-through after `br_if`. The first non-zero charge emits `fuel += <cost>` with a placeholder immediate, an out-of-fuel trap and a store; the immediate is patched with the region total when the region closes. Regions made only of free operators emit nothing. Out-of-fuel is raised through the new `TRAP_OUT_OF_FUEL` trap placed before the store, so a charge that does not fit is never applied and `Store::get_fuel` reports the same remaining fuel as the rwasm store. This replaces the `out_of_gas` libcall path, which rwasm never used to refuel. Syscall fuel runs the overflow guard before charging and computes in 32-bit arithmetic that wraps like the `i32` code rwasm emits, then extends to 64 bits. The cost constants are re-exported from `rwasm-fuel-policy`; the operator table stays local only because this crate pins a different `wasmparser`. Versions bumped to 45.0.0-rwasm.2.
djadjka
previously approved these changes
Sep 9, 2026
Raising out-of-fuel as a plain trap dropped Wasmtime's refuel path, which broke `fuel_async_yield_interval` and every async test that relies on it. Region charges that exceed the injected fuel now go through a new `rwasm_out_of_fuel(vmctx, before)` libcall. It refuels from the store's reserve and yields when an async interval is configured, exactly like `out_of_gas`; when nothing is left it puts `before` back into the store and traps, so a refused charge is still never applied and `Store::get_fuel` reports what was available before the region. GC array fills charge one base unit per element written. Vanilla Wasmtime only did so by accident of where it flushed its pending cost, and the region scheme lost that, letting a 64 MiB `array.new_default` run for the price of one operator. The fuel tests that encoded the old rules now assert the rwasm ones: a region is charged as a whole before it runs, consuming exactly the limit is not out of fuel, and a refused charge leaves the remaining fuel untouched. `custom_operator_cost` is ignored because operator costs are fixed by `rwasm-fuel-policy`.
d1r1
approved these changes
Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Aligns Wasmtime's fuel metering with the rwasm VM so that both engines burn the same fuel, stop at the same point and report the same remaining fuel on every path, not only on runs that complete.
Root causes
OutOfFuel.What changed
crates/cranelift/src/func_environ.rs: region-based eager charging. A region opens at function entry, every loop header, eachif/elsearm, after everyendand on the fall-through afterbr_if, mirroring where the rwasm translator emitsConsumeFuel. The first non-zero charge emitsfuel += <cost>with a placeholder immediate, publishes the counter, and branches to a cold out-of-fuel block when the injected fuel is exceeded; the immediate is patched with the region total when the region closes. Regions made only of free operators emit nothing.rwasm_out_of_fuel(vmctx, before)libcall (crates/environ/src/builtin.rs,crates/wasmtime/src/runtime/vm/libcalls.rs): refuels from the reserve and yields whenfuel_async_yield_intervalis set, exactly likeout_of_gas; when nothing is left it restores the pre-charge counter and traps withOutOfFuel, so a refused charge is never applied andStore::get_fuelmatches rwasm's remaining fuel.func_environ/gc/enabled.rs) charge one base unit per element written, so a hugearray.new_defaultcannot run for the price of one operator (vanilla only charged this by accident of where it flushed).i32code before extending to 64 bits.crates/cranelift/src/rwasm_fuel.rs: cost constants come fromrwasm-fuel-policy; the operator table stays local because this crate pins a differentwasmparser.tests/all/fuel.rs: the tests that encoded the old rules now assert the rwasm ones (a region is charged as a whole before it runs, consuming exactly the limit is not out of fuel, a refused charge leaves the remaining fuel untouched);custom_operator_costis ignored because operator costs are fixed byrwasm-fuel-policy.45.0.0-rwasm.2.Testing
Fork suite (
cargo test --test all, unit tests ofwasmtime-rwasmand the cranelift crate): the only failures are the ones already failing onv45-patched(component-model tests,wasm_ty_roundtrip,same_import_names_still_distinct,host_always_has_some_stack, and thewasmtime-environ-rwasmunit tests not compiling), none of which touch fuel.From
fluentlabs-xyz/rwasmwith this branch patched in ([patch.crates-io]):tests/fuel_alignment.rs, 21 differential cases covering traps mid-region and in callees, disabled float opcodes, exact-limit and overrun boundaries, loops, tail-call recursion, host failures/halts/consumption,reset_fuel, and all three syscall charging modes: all pass on both engines, with identical absolute numbers.fpu+full-wasm-mode.cargo fuzz run differentialon the non-signal trap path: no findings.Follow-up
The rwasm side lands separately: unbounded
fuel_limit: Noneon the Wasmtime executor,param_indexcounted by parameter position withi64slots handled, unreachable marking after disabled float opcodes, and the dependency bump to45.0.0-rwasm.2once this is published.