SebTardif opened PR #13662 from SebTardif:fix-stack-switching-initialize-bounds-check to bytecodealliance:main:
\`VMContinuationStack::initialize\` writes control data and an args buffer at offsets below the top of the continuation stack, but does not verify that the total size fits within the allocated stack region.
The problem
A Wasm module can define a continuation function type with an extremely large number of parameters or return values. When \`cont.new\` is executed, the call chain reaches \`initialize()\` (\`stack/unix.rs:226\`), which computes:
\`\`\`
args_data_size = max(param_count, return_count) * size_of::<ValRaw>()
\`\`\`With the default 2 MiB continuation stack (\`async_stack_size\`), the fixed header occupies 0x40 (64) bytes. A function type with 131,069 or more parameters produces an \`args_data_size\` of 2,097,104 bytes, exceeding the stack allocation. The write loop at line 298 then stores values at offsets like \`0x28 + args_data_size\`, placing data past the guard page into adjacent memory mappings.
Additionally, the original \`args_data_size\` computation uses unchecked multiplication (\`capacity * 16\`), which, while safe on 64-bit (the inputs are \`u32\`), is not self-documenting about overflow safety.
The fix
- Replace the unchecked multiplication with \`checked_mul\` + \`checked_add\`, returning an error on arithmetic overflow.
- Add a bounds check (\`ensure!(total_control_size <= self.len)\`) before any writes, so a high-arity function type produces a clean trap instead of memory corruption.
- Change the return type of \`initialize\` from \`()\` to \`Result<()>\` and propagate the error through the \`stack.rs\` wrapper and the \`cont_new\` caller (which already returns \`Result\`).
Bug origin
Introduced in #10388 ("Stack switching: Infrastructure and runtime support", Frank Emrich, 2025-06-04).
SebTardif requested pchickey for a review on PR #13662.
SebTardif requested wasmtime-core-reviewers for a review on PR #13662.
SebTardif updated PR #13662.
SebTardif updated PR #13662.
SebTardif updated PR #13662.
SebTardif edited PR #13662:
VMContinuationStack::initializewrites control data and an args buffer at offsets below the top of the continuation stack, but does not verify that the total size fits within the allocated stack region.The problem
A Wasm module can define a continuation function type with a large number of parameters or return values. When
cont.newis executed, the call chain reachesinitialize()(stack/unix.rs:226), which computes:args_data_size = max(param_count, return_count) * size_of::<ValRaw>
()The fixed header occupies 0x40 (64) bytes. If the total control data size (header + args buffer) exceeds the stack allocation, the write loop stores values past the stack region into adjacent memory. With the default 2 MiB stack, this requires 131,069+ params; with a smaller
async_stack_size, fewer params suffice (e.g., 800 params on an 8 KiB stack).Without this fix, the out-of-bounds write hits the guard page and crashes with SIGSEGV. With the fix, a clean error is returned instead.
The fix
- Replace the unchecked multiplication with
checked_mul+checked_add, returning an error on arithmetic overflow.- Add a bounds check (
ensure!(total_control_size <= self.len)) before any writes, so a high-arity function type produces a clean trap instead of a crash.- Change the return type of
initializefrom()toResult<()>and propagate the error through thestack.rswrapper and thecont_newcaller (which already returnsResult).Test
A regression test (
stack_switching_cont_new_high_arity_rejected) creates a function type with 800i32params on an 8 KiB continuation stack, verifying thatcont.newreturns an error instead of crashing. The test skips gracefully on platforms where stack switching is not supported.Bug origin
Introduced in #10388 ("Stack switching: Infrastructure and runtime support", Frank Emrich, 2025-06-04).
github-actions[bot] added the label wasmtime:api on PR #13662.
alexcrichton edited PR #13662:
VMContinuationStack::initializewrites control data and an args buffer at offsets below the top of the continuation stack, but does not verify that the total size fits within the allocated stack region.The problem
A Wasm module can define a continuation function type with a large number of parameters or return values. When
cont.newis executed, the call chain reachesinitialize()(stack/unix.rs:226), which computes:
args_data_size = max(param_count, return_count) * size_of::<ValRaw>()The fixed header occupies 0x40 (64) bytes. If the total control data size (header + args buffer) exceeds the stack allocation, the write loop stores values past the stack region into adjacent memory. With the default 2 MiB stack, this requires 131,069+ params; with a smaller
async_stack_size, fewer params suffice (e.g., 800 params on an 8 KiB stack).Without this fix, the out-of-bounds write hits the guard page and crashes with SIGSEGV. With the fix, a clean error is returned instead.
The fix
- Replace the unchecked multiplication with
checked_mul+checked_add, returning an error on arithmetic overflow.- Add a bounds check (
ensure!(total_control_size <= self.len)) before any writes, so a high-arity function type produces a clean trap instead of a crash.- Change the return type of
initializefrom()toResult<()>and propagate the error through thestack.rswrapper and thecont_newcaller (which already returnsResult).Test
A regression test (
stack_switching_cont_new_high_arity_rejected) creates a function type with 800i32params on an 8 KiB continuation stack, verifying thatcont.newreturns an error instead of crashing. The test skips gracefully on platforms where stack switching is not supported.Bug origin
Introduced in #10388 ("Stack switching: Infrastructure and runtime support", Frank Emrich, 2025-06-04).
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
Since this function now returns
Resultcould this use?instead of.unwrap()?
:speech_balloon: alexcrichton created PR review comment:
Could this avoid the dance here of adding/subtracting/adding 0x40 by setting the original computation to
total_control_size, then calculatingargs_data_sizeby subtracing 0x40 from that?
alexcrichton requested alexcrichton for a review on PR #13662.
alexcrichton unassigned pchickey from PR #13662 Add bounds check in VMContinuationStack::initialize for args buffer size.
:speech_balloon: alexcrichton created PR review comment:
I realize that this is preexisting, but could you add an
assert!(!cfg(target_arch = "x86_64"))here? That way we can at least verify this test passes on one architecture instead of accidentally always falling through to here silently
SebTardif updated PR #13662.
:memo: SebTardif submitted PR review.
:memo: SebTardif submitted PR review.
:memo: SebTardif submitted PR review.
:speech_balloon: SebTardif created PR review comment:
Added.
:speech_balloon: SebTardif created PR review comment:
Done, replaced all three
.unwrap()calls ininitializewith?.
:speech_balloon: SebTardif created PR review comment:
Good call. Now computes
total_control_sizedirectly from the checked arithmetic, then derivesargs_data_size = total_control_size - 0x40. Also replacedtos.sub(0x40 + args_data_size)withtos.sub(total_control_size)in the to_store array for consistency.
:thumbs_up: alexcrichton submitted PR review:
Thanks! Happy to merge once CI is green
SebTardif updated PR #13662.
alexcrichton has enabled auto merge for PR #13662.
alexcrichton added PR #13662 Add bounds check in VMContinuationStack::initialize for args buffer size to the merge queue.
github-merge-queue[bot] removed PR #13662 Add bounds check in VMContinuationStack::initialize for args buffer size from the merge queue.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
Ah, sorry, this'll need adjusting to:
assert!(!(cfg!(target_arch = "x86_64") && cfg!(unix)));
SebTardif updated PR #13662.
:memo: SebTardif submitted PR review.
:speech_balloon: SebTardif created PR review comment:
Done, updated to check both conditions.
alexcrichton added PR #13662 Add bounds check in VMContinuationStack::initialize for args buffer size to the merge queue.
:check: alexcrichton merged PR #13662.
alexcrichton removed PR #13662 Add bounds check in VMContinuationStack::initialize for args buffer size from the merge queue.
Last updated: Jul 29 2026 at 05:03 UTC