WASM Shadow Stack
This page explains exactly when the direct WASM backend uses the shadow stack, how frame layout is computed, and how out-slot returns interact with it.
What It Is
RukaLang's direct WASM backend uses a per-call shadow stack frame for aggregate values that should not be heap-allocated for temporary use.
The compile-time decisions are encoded in:
The runtime ABI symbols used to reserve/release the frame are:
Exactly When It Is Used
A local is assigned shadow-stack storage when all of the following are true:
- The local is not a function parameter.
- Either:
- the local is a value local whose type is one of
Tuple,Struct, orSlice, or - the local is a slice place local (
RefRo<Slice<_>>orRefMut<Slice<_>>).
- the local is a value local whose type is one of
This selection logic is implemented by
should_shadow_stack_local and
is_shadow_stack_aggregate_ty.
Frame Layout and Prologue
Frame construction happens in lower_function.
For each selected local:
- Payload bytes are computed via
aggregate_payload_bytes. - Slot size is
align_up(ARRAY_DATA_OFFSET + payload_bytes, 8). - Offsets are assigned in declaration order, each starting at 8-byte alignment.
After all slots are sized:
- If frame size is zero, no runtime shadow-stack calls are emitted.
- If frame size is non-zero, function entry emits one reserve call to
__ruka_rt::shadow_stack_reserve(frame_bytes). - The returned base pointer is kept in a scratch local.
- Each shadow local is initialized to
frame_base + local_offset.
How Instructions Use Shadow-Stack Locals
Aggregate-producing instructions check whether dst is shadow-backed:
lower_aggregate_instrskips heap allocation for tuple/struct/slice destinations and requires those destinations to be shadow-backed.- The instruction then writes aggregate fields directly through the local pointer.
For call destinations:
lower_call_family_instrchecks whether the destination local is shadow-backed and requires out-slot destinations to be shadow-backed.
Out-Slot Returns and Caller/Callee Behavior
Return-type decision:
function_returns_via_out_slotcurrently returns true for tuple/struct/slice return types.- Borrowed/reference returns are rejected before signature planning.
Signature shaping:
signature_typesinserts ani32out-slot pointer parameter at index0when return-via-out-slot is required.- In that case, the WASM result list is empty.
Call-site behavior:
- Caller passes destination pointer as arg
0. - If destination local is shadow-backed, that pointer is reused.
- If destination local is not shadow-backed, lowering fails instead of heap-allocating implicit out-slot storage.
Return behavior:
lower_terminatorhandlesReturn.- For out-slot returns, it copies return bytes from the local storage pointer to the out-slot pointer.
- For non-out-slot returns, it pushes the value as a normal WASM result.
Release and Lifetime Rules
At every emitted Return path in
lower_terminator:
- If the function reserved a non-zero frame, the backend emits one call to
__ruka_rt::shadow_stack_release(frame_bytes)beforereturn.
This gives function-scoped shadow-stack lifetimes:
- Reserve once in function entry.
- Reuse slots for all selected locals in that function.
- Release once on each return path.
Runtime Side Notes
The runtime reserve/release behavior itself is implemented in the wasm32-only runtime module source:
That module currently:
- lazily allocates one backing region,
- bumps a shadow-stack pointer on reserve,
- checks overflow/underflow with assertions,
- and rewinds the pointer on release.