Skip to content

Add bounds check in VMContinuationStack::initialize for args buffer size#13662

Merged
alexcrichton merged 5 commits into
bytecodealliance:mainfrom
SebTardif:fix-stack-switching-initialize-bounds-check
Jun 16, 2026
Merged

Add bounds check in VMContinuationStack::initialize for args buffer size#13662
alexcrichton merged 5 commits into
bytecodealliance:mainfrom
SebTardif:fix-stack-switching-initialize-bounds-check

Conversation

@SebTardif

@SebTardif SebTardif commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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 a 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>()

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

  1. Replace the unchecked multiplication with checked_mul + checked_add, returning an error on arithmetic overflow.
  2. 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.
  3. 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).

Test

A regression test (stack_switching_cont_new_high_arity_rejected) creates a function type with 800 i32 params on an 8 KiB continuation stack, verifying that cont.new returns 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).

@SebTardif SebTardif requested a review from a team as a code owner June 16, 2026 00:43
@SebTardif SebTardif requested review from pchickey and removed request for a team June 16, 2026 00:43
@SebTardif SebTardif force-pushed the fix-stack-switching-initialize-bounds-check branch from 2b8f887 to c329b8f Compare June 16, 2026 01:00
VMContinuationStack::initialize writes control data and an args buffer
at offsets below the top of the stack, but did not verify that the total
size fits within the allocated stack. A WebAssembly module with a
continuation function type with a very large number of parameters or
return values could cause writes past the guard page into adjacent
memory mappings.

This commit adds:

- Checked arithmetic (checked_mul + checked_add) for the args buffer
  size calculation to prevent overflow.
- A bounds check ensuring the total control data size (0x40 bytes of
  fixed header + args buffer) does not exceed the stack allocation.
- Propagation of the new Result return type through the stack.rs
  wrapper and the cont_new caller.

The dummy implementation (for unsupported platforms) is updated to
match the new signature.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Adds a test that creates a continuation function type with 4,500
parameters on a 64 KiB continuation stack, verifying that cont.new
returns an error instead of writing past the stack allocation.

The test skips gracefully on platforms where stack switching is not
supported (e.g., macOS aarch64, Pulley).

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@SebTardif SebTardif force-pushed the fix-stack-switching-initialize-bounds-check branch from c329b8f to 876e35c Compare June 16, 2026 01:34
@github-actions github-actions Bot added the wasmtime:api Related to the API of the `wasmtime` crate itself label Jun 16, 2026
let args_data_size =
usize::try_from(args_capacity).unwrap() * std::mem::size_of::<ValRaw>();
let args_data_size = usize::try_from(args_capacity)
.unwrap()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function now returns Result could this use ? instead of .unwrap()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, replaced all three .unwrap() calls in initialize with ?.

Comment on lines +263 to +267
// Ensure the control data (fixed header + args buffer) fits
// within the stack allocation. Without this check, a
// high-arity function type could write past the guard page
// into adjacent memory.
let total_control_size = 0x40 + args_data_size;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this avoid the dance here of adding/subtracting/adding 0x40 by setting the original computation to total_control_size, then calculating args_data_size by subtracing 0x40 from that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Now computes total_control_size directly from the checked arithmetic, then derives args_data_size = total_control_size - 0x40. Also replaced tos.sub(0x40 + args_data_size) with tos.sub(total_control_size) in the to_store array for consistency.

Comment thread tests/all/tags.rs
@alexcrichton alexcrichton requested review from alexcrichton and removed request for pchickey June 16, 2026 17:50
- Compute total_control_size directly instead of the add/subtract/add
  dance with 0x40
- Replace .unwrap() with ? on usize::try_from calls since initialize
  now returns Result
- Add assert!(!cfg!(target_arch = "x86_64")) to the test skip guard
  so x86_64 never silently skips the test

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

@alexcrichton alexcrichton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Happy to merge once CI is green

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@alexcrichton alexcrichton enabled auto-merge June 16, 2026 20:11
@alexcrichton alexcrichton added this pull request to the merge queue Jun 16, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 16, 2026
Comment thread tests/all/tags.rs Outdated

let Ok(engine) = Engine::new(&config) else {
// Stack switching is not supported on all platforms; skip gracefully.
assert!(!cfg!(target_arch = "x86_64"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, this'll need adjusting to:

assert!(!(cfg!(target_arch = "x86_64") && cfg!(unix)));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, updated to check both conditions.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@alexcrichton alexcrichton added this pull request to the merge queue Jun 16, 2026
Merged via the queue into bytecodealliance:main with commit 2d2718f Jun 16, 2026
51 checks passed
SebTardif added a commit to SebTardif/wasmtime that referenced this pull request Jun 22, 2026
Fix the bounds check in VMContinuationStack::initialize to compare
against usable stack space rather than total allocation size.

Prior to bytecodealliance#13662, initialize() had no bounds check at all, so a
high-arity function type (e.g. 600 params on an 8192-byte stack)
would unconditionally write control data into the guard page and
segfault. PR bytecodealliance#13662 added a bounds check, but compared against
self.len which for Mmap allocations includes the guard page. This
meant the 600-param case still slipped through: 9664 <= 12288
passed the check, but the write still landed in the non-writable
guard page.

This fix subtracts the guard page size for Mmap allocations so
the check reflects the actual usable stack space. Custom
allocations are unaffected since their len does not include a
guard page.

Adds a regression test matching the exact scenario from bytecodealliance#13703
(600 params on an 8192-byte stack).

Closes bytecodealliance#13703

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
pull Bot pushed a commit to eduardomourar/wasmtime that referenced this pull request Jun 23, 2026
…iance#13704)

Fix the bounds check in VMContinuationStack::initialize to compare
against usable stack space rather than total allocation size.

Prior to bytecodealliance#13662, initialize() had no bounds check at all, so a
high-arity function type (e.g. 600 params on an 8192-byte stack)
would unconditionally write control data into the guard page and
segfault. PR bytecodealliance#13662 added a bounds check, but compared against
self.len which for Mmap allocations includes the guard page. This
meant the 600-param case still slipped through: 9664 <= 12288
passed the check, but the write still landed in the non-writable
guard page.

This fix subtracts the guard page size for Mmap allocations so
the check reflects the actual usable stack space. Custom
allocations are unaffected since their len does not include a
guard page.

Adds a regression test matching the exact scenario from bytecodealliance#13703
(600 params on an 8192-byte stack).

Closes bytecodealliance#13703

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wasmtime:api Related to the API of the `wasmtime` crate itself

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants