Repository navigation
feat(cranelift): emulate the rwasm stack limits in compiled code - #13
Merged
Merged
Conversation
dmitry123
added this pull request to stack #14
September 23, 2026 10:31
Member
Author
|
Applied the same fix to
Local results (aarch64-apple-darwin),
|
d1r1
approved these changes
Sep 23, 2026
hedwig0x
approved these changes
Sep 23, 2026
hedwig0x
left a comment
There was a problem hiding this comment.
Reviewed stack-limit emulation and the cache/precompile compatibility fix. Local wasmtime-rwasm test suite passes (173/173).
The rwasm VM stops an execution at `N_MAX_RECURSION_DEPTH` frames or when the frames on the call chain exceed its value-stack window. Wasmtime only ran out of native stack, so a recursion 1024 deep or two nested frames of 5000 slots ran here and trapped `StackOverflow` on rwasm. `Config::rwasm_stack_limits` now makes Cranelift keep both counters in `VMStoreContext` (`rwasm_call_depth`, `rwasm_stack_slots`, exposed through `Store::rwasm_stack_counters`) and trap exactly where the rwasm VM does: - a function prologue checks its frame base plus its parameters and the height the rwasm translator recorded in the module's `rwasm.frames` custom section, like rwasm's `StackCheck`; - a call site traps when the call stack is full, publishes the callee's depth and frame base and restores its own after the call; for `call_indirect` and `call_ref` the depth check sits between the null check and the signature check, as on rwasm; tail calls keep both counters like `ReturnCallInternal`; - with `code_snippets`, the `i64` operators rwasm runs in a hidden snippet frame (`rwasm_snippet_frames`) check that frame's depth and height before the operator runs. Host functions are checked by the embedder, whose import trampoline has a frame of its own on rwasm; it reads the counters from the `Caller`.
…call Measured on a call-only recursion (fib(30), 1.6M calls) the emulation cost about 30% with two stores before every call and two more after it. The readers of the counters, a callee's prologue and the host trampolines, all run right after the call site that wrote them, and a caller keeps its own depth and base in registers, so the restore after the call is dropped. The two counters are packed into one `u64` in `VMStoreContext` (`rwasm_stack`: call depth in the high half, frame base in the low half): a call site publishes both with one add and one store, and the depth check is one compare of the whole word against the limit shifted into the high half. Neither half can carry into the other, as both stay below their limits. The window check compares the base against the room the frame leaves instead of widening and adding. For `call_indirect` the check sits after the load that traps on a null entry and before the signature compare, so the trap order stays rwasm's without an extra null check. Cost is now 3-5% on the call-only recursion, direct or indirect, and within noise on arithmetic loops. `Store::rwasm_stack_counters` and the `RwasmStackCounters` API are unchanged.
…k limits `Config::rwasm_stack_limits` changes the generated code (the call-site depth checks, the prologue frame checks and the snippet frames) but, like the bulk fuel setting in #12, was part of neither `HashedEngineCompileEnv` nor a precompiled artifact's metadata. An artifact compiled without the stack checks could therefore be served from the cache to, or deserialized into, an engine that enforces them. `RwasmStackLimits` is now `Hash` and serializable, hashed into the compatibility hash next to `rwasm_bulk_fuel`, recorded in `Metadata` and checked on load. The tests mirror the bulk fuel ones over five settings that vary each field, using a module that carries `rwasm.frames`; all three fail without the fix.
dmitry123
force-pushed
the
feat/rwasm-stack-limits
branch
from
September 23, 2026 13:38
9648c93 to
5583bd7
Compare
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
The rwasm VM stops an execution at
N_MAX_RECURSION_DEPTHframes or when the frames on the call chain exceed its value-stack window. Wasmtime only ran out of native stack, so a recursion 1024 deep or two nested frames of 5000 slots ran here and trappedStackOverflowon rwasm. This PR makes the Cranelift backend trap exactly where the rwasm VM does, so both engines agree at every boundary.Stacked on #12 (bulk fuel,
45.0.0-rwasm.3); the rwasm counterpart is fluentlabs-xyz/rwasm#213, which pins45.0.0-rwasm.3and cannot build until this is released.How it works
Config::rwasm_stack_limits(Some(RwasmStackLimits { max_call_depth, max_stack_slots, code_snippets }))makes compiled code keep two counters inVMStoreContext, packed into oneu64afterexecution_version(rwasm_stack: call depth in the high half, frame base in the low half; the offset test covers the new layout):StackOverflowwhenbase + params + height > max_stack_slots, like rwasm'sStackCheck.heightis the function's entry in the module'srwasm.framescustom section (RWASM_FRAMES_SECTION, one little-endianu32per function, imports first), written by the rwasm translator, which alone knows the temporaries its lowering pushes. A module without the section, or with the wrong length, fails to compile on such an engine.call,call_indirect,call_ref): trap whendepth >= max_call_depth(one compare of the packed word), then publish the callee's counters with one add and one store: depth plus one, and the callee's frame base (this function's parameters, locals and the operand slots below the arguments, computed from the validator's operand stack). Nothing is restored after the call: every reader runs right after the call site that wrote the counters, and the caller keeps its own in a register. Forcall_indirectthe check sits after the load that traps on a null entry and before the signature compare, forcall_refafter an explicit null check, the order rwasm traps in. Tail calls keep both counters, likeReturnCallInternal.code_snippets, thei64operators rwasm runs in a hiddenCallInternalframe (rwasm_snippet_frames: one frame, two for div/rem which call the sharedUDivMod64core) check that frame's depth and height before the operator runs. The table mirrors rwasm'sMSH_*constants; rwasm pins it to its snippet definitions.Store::rwasm_stack_counters/Caller::rwasm_stack_counters, and resets them withStore::set_rwasm_stack_countersbefore each call into the guest.Without
rwasm_stack_limitsnothing changes: the counters stay unused and no code is emitted.Cost
Measured on Apple Silicon at
OptLevel::Speed, best of 5, against the same build with the emulation switched off:call(1.6M calls)call_indirecti64.mul+i64.div_uA call-only recursion pays 3-5% for the depth compare, the add and the store per call plus the load and compare per prologue; arithmetic loops are within noise. The first version, with two stores before and two after every call, cost about 30% on the same recursion.
Verification
cargo test -p wasmtime-rwasm --features cranelift --lib: green (151 passed,field_offsetsincluded)[patch.crates-io]: green (see fix(wasmtime): emulate the rwasm stack limits on the Wasmtime backend rwasm#213)