ilyas-mallah opened PR #14617 from ilyas-mallah:transcode-bounds-checks to bytecodealliance:main:
The host string transcoders in
vm/component/libcalls.rsbuild raw slices from the pointers they get, and only the adapter module bounds-checks those pointers. This path has had three advisories this year (GHSA-394w-hwhg-8vgm, GHSA-hx6p-xpx3-jvvv, GHSA-jxhv-7h78-9775), each a gap in the adapter's checks that the host trusted.This adds a second check in the transcoder trampoline, like #13027 and #14484 did elsewhere: before the libcall it loads each memory's
current_lengthand traps unless both buffers are in bounds and aligned, using the adapter's own trap codes. The cost is a couple of loads and compares per transcode call.A new disas test shows the checks, and the component-model wast tests pass, including
big-strings.wastandmemory64.wast, also on Pulley. With FACT'svalidate_guest_pointerdisabled locally, the 6component_model::stringstests still trap as expected. Without this change,ptr_overflowandrealloc_oobcrash the host with SIGSEGV.Open questions:
- Should the check maybe be in the Rust libcall instead, passing memory indices instead of native pointers?
- Should the pointers also go through
select_spectre_guard, likechecked_native_addr?
ilyas-mallah requested alexcrichton for a review on PR #14617.
ilyas-mallah requested wasmtime-core-reviewers for a review on PR #14617.
ilyas-mallah requested wasmtime-compiler-reviewers for a review on PR #14617.
:memo: alexcrichton submitted PR review:
Thanks for this! Out of curiosity, would you be interested in helping to try something with a slightly different tact instead? Bounds-checks are notoriously tricky and hard to get right, so instead of that one possibility would be to use a normal wasm load/store to determine if the string is valid. For example if before calling transcoding the adapter could perform a wasm load of the last byte/16-bit codepoint in the string, and if that succeeds then everything is guaranteed to be in-bounds. That'd keep the translation and handling on the wasm-side as well which I think would be nice to keep the Cranelift side smaller
ilyas-mallah commented on PR #14617:
Sure, happy to try that. I'll rework it so the adapter does a wasm load of the last byte (or the last 16-bit unit for UTF-16) of each buffer before calling the transcoder, and drop the Cranelift side. For zero-length strings I'd skip the load, since the host only builds an empty slice there. Does that match what you had in mind?
alexcrichton commented on PR #14617:
Yeah that's what I was thinking too, zero-length is just skipped
ilyas-mallah updated PR #14617.
ilyas-mallah edited PR #14617:
The host string transcoders in
vm/component/libcalls.rsbuild raw slices from the pointers they get, and only the adapter's earlier checks keep those pointers in bounds. This path had three advisories this year (GHSA-394w-hwhg-8vgm, GHSA-hx6p-xpx3-jvvv, GHSA-jxhv-7h78-9775), each a gap in those checks.This adds a second check in the adapter right before every transcoder call: a plain wasm load of the last byte or 16-bit unit of each buffer the host will access, in that buffer's memory. Empty buffers skip the load. Before it, the adapter traps on a length over the maximum string size or an address that wraps, and 16-bit buffers keep the alignment check, now a helper shared with
validate_guest_pointer.If one of these loads fails, the trap is a regular out-of-bounds trap instead of
StringOutOfBounds. That only happens when an earlier check has a bug.Tested with a new disas test and the component-model wast tests on Cranelift, Winch and Pulley. With
validate_guest_pointerdisabled locally, the out-of-bounds cases incomponent_model::stringsstill trap, and without the new loads as well they crash the test process.
ilyas-mallah commented on PR #14617:
Yeah that's what I was thinking too, zero-length is just skipped
Just reworked and pushed it as a new commit that replaces the Cranelift version entirely. The full PR diff is easier to read than the commit on its own. Two things that might be worth a look: the 16-bit alignment check moved out of
validate_guest_pointerinto a helper so both can use it, and a failed load is now a plain out-of-bounds trap instead ofStringOutOfBounds. I also swapped the disas test for one showing the loads, can drop it if it's not useful.
Last updated: Oct 11 2026 at 02:20 UTC