Stream: git-wasmtime

Topic: wasmtime / issue #14075 Cost of stack probes not accounte...


view this post on Zulip Wasmtime GitHub notifications bot (Aug 03 2026 at 21:16):

alexcrichton opened issue #14075:

Originally surfaced in https://github.com/bytecodealliance/wasmtime/pull/14042 the problem, or at least my own summary of it, is that Cranelift-generated stack probes are an unaccounted for cost when it comes to fuel consumption. Cranelift's stack probes are used for large stack frames and the probe is either unrolled or codegen'd as a loop depending on the size of the stack frame. This cost is not account for in fuel consumption insofar as it's possible for a malicious guest to craft modules where the time/resources taken to execute are constant in fuel consumption but variable in wall-clock time and stack consumption.

One example from https://github.com/bytecodealliance/wasmtime/pull/14042 is a large function with thousands of locals which dynamically ends up doing nothing (e.g. early returns). In this situation if all these locals are live via a computation at the end, and if optimizations are disabled, then lots of locals are spilled to the stack. This causes a stack frame to be generated which is proportional to the number of locals but this function consumes a constant amount of fuel regardless of the number of locals.

Another example of increasing stack space, however, is having a very large operand stack in wasm. For example wasm can repeatedly call a function to return a value, and then after generating N values add them all up. This is another example of a variable-sized wasm function incurring a variable-sized amount of runtime (due to differing stack frame sizes) while consuming a constant amount of fuel at runtime.

The overall problem seems to be that the cost of the probe itself is unaccounted for meaning guests, in theory, given a fixed budget of fuel can consume an arbitrary amount of time. I'm not personally sure of the best fix for this since the size of the stack frame isn't known until the end of compilation. One nice property of fuel as well is that it's deterministic across targets within a Wasmtime version right now, but if the literal stack frame size were taken into account this would start to diverge across compiler options/targets/etc. On the other hand there are indicators of stack frame size, such as locals or operand-stack-depth, but I wouldn't be confident in saying these are the only variable sources of stack-frame-sizes.

Hence, an issue!

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

michael-weigelt commented on issue #14075:

I don't know the exact design goals of Wasmtime's fuel, but I can share an almost-use case: ICP needs to suspend untrusted user code 1) deterministically and 2) at wall-time intervals that should be within some time, say half a second. In other words, we need a deterministic proxy for CPU time.
Since we run many embedders representing different user programs on the same host, limiting each programs' compute properly is a liveness issue for the platform, so it's very important.
ICP does not use Wasmtime's fuel, because we need more flexibility than it had at the time, in particular, we need to charge different amounts of fuel per operation, based on benchmarks, to represent true CPU time better.
Given our use case and the stakes, we approached the problem in this issue in the most "conservative" way:

Now IMO, this could point the way to an approach for Wasmtime's fuel as well:

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

michael-weigelt edited a comment on issue #14075:

I don't know the exact design goals of Wasmtime's fuel, but I can share an almost-use case: ICP needs to suspend untrusted user code 1) deterministically and 2) at wall-time intervals that should be within some time, say half a second. In other words, we need a deterministic proxy for CPU time.
Since we run many embedders representing different user programs on the same host, limiting each program's compute properly is a liveness issue for the platform, so it's very important.
ICP does not use Wasmtime's fuel, because we need more flexibility than it had at the time, in particular, we need to charge different amounts of fuel per operation, based on benchmarks, to represent true CPU time better.
Given our use case and the stakes, we approached the problem in this issue in the most "conservative" way:

Now IMO, this could point the way to an approach for Wasmtime's fuel as well:

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

michael-weigelt edited a comment on issue #14075:

I don't know the exact design goals of Wasmtime's fuel, but I can share an almost-use case: ICP needs to suspend untrusted user code 1) deterministically and 2) at wall-time intervals that should be within some time, say half a second. In other words, we need a deterministic proxy for CPU time.
Since we run many embedders representing different user programs on the same host, limiting each program's compute properly is a liveness issue for the platform, so it's very important.
ICP does not use Wasmtime's fuel, because we need more flexibility than it had at the time, in particular, we need to charge different amounts of fuel per operation, based on benchmarks, to represent true CPU time better.
Given our use case and the stakes, we approached the problem in this issue in the most "conservative" way:

because anything else leaves us open to attacks which slow down the platform.
It's clear that this overcharges in some situations, but for ICP, this is the trade-off we need.

Now IMO, this could point the way to an approach for Wasmtime's fuel as well:

should give fuel users the means to choose their own operation point in this trade-off between overcharging and safety. It feels a bit blunt, but it's better than forcing the user to either extreme (either can be a deal-breaker for some application).
And perhaps a similar approach could be found for the call depth issue, though I haven't thought about that yet.

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

michael-weigelt edited a comment on issue #14075:

I don't know the exact design goals of Wasmtime's fuel, but I can share an almost-use case: ICP needs to suspend untrusted user code 1) deterministically and 2) at wall-time intervals that should be within some time, say half a second. In other words, we need a deterministic proxy for CPU time.
Since we run many embedders representing different user programs on the same host, limiting each program's compute properly is a liveness issue for the platform, so it's very important.
ICP does not use Wasmtime's fuel, because we need more flexibility than it had at the time, in particular, we need to charge different amounts of fuel per operation, based on benchmarks, to represent true CPU time better.
Given our use case and the stakes, we approached the problem in this issue in the most "conservative" way:

because anything else leaves us open to attacks which slow down the platform.
It's clear that this overcharges in some situations, but for ICP, this is the trade-off we need.

Now IMO, this could point the way to an approach for Wasmtime's fuel as well:

should give fuel users the means to choose their own operation point in this trade-off between overcharging and safety. It feels a bit blunt, but it's better than forcing the user to either extreme (either can be a deal-breaker for some application).
And perhaps a similar approach could be found for the call depth issue, though I haven't thought about that yet.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 10 2026 at 22:59):

cfallin commented on issue #14075:

This is a very interesting question! So far all of our thinking about fuel has been relative to an abstract machine execution model at the CLIF level, where storage is free. It is indeed true that passing through regalloc produces a mapping to loads and stores to a stack frame of some arbitrary size, and there is a size-proportional cost in the setup wrt probes (not to mention lower-level microarchitectural effects wrt cache misses when very large, sparse stack frames are pushed).

The tricky bit is that given that regalloc is an NP-hard problem and the "realistic" answer is so much better (via binpacking) than the truly worst-case answer, I worry we'd lose practical correspondence of fuel to runtime cost for most programs. The worst-case frame size is bounded by the number of SSA values, and that is bounded by the size of the function. Note that the worst-case program in #14042 had to actually use the locals so that our SSA construction would turn them into materialized computation in the IR; so locals per se are literally free at runtime, even from a stackframe-size point of view. It is only defs of values that cost.

And the tricky bit is that the maximum size of the live-set ("size of the largest clique in the interference graph") is so much smaller than the number of total SSA values in most functions that we'd charge an absurdly high price for most functions. And there is also no clean delineating line -- the "load factor" (so to speak) of how many registers + spillslots is required is a function of a pile of heuristics that try to do efficient binpacking, but may fail in edge-cases.

If the requirement is that fuel accurately measures all runtime costs, though, I think there's no way around it: we need to charge for SSA values. That is something we can patch in to the IR, after wasm-to-CLIF translation but before the backend optimizes + regallocs the function body. I do believe that that would capture the worst-case frame size: in RA2 anyway, abstract-spillslots are assigned to every SSA value, and in the worst case if no coalescing occurs, and the spillslot allocator fails to share slots between any two abstract-spillslots, we're still bounded by SSA value count.

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

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


Last updated: Aug 30 2026 at 09:07 UTC