[JSC] Wasm: offset the jitless marshalling frames in bytes, not by an array index - #617
[JSC] Wasm: offset the jitless marshalling frames in bytes, not by an array index#617robobun wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review. WalkthroughChangesThe change updates WebAssembly frame access to interpret locations as signed byte offsets. A no-JIT test validates JavaScript import calls, return values, and mixed-type stack argument marshalling. WebAssembly marshalling
Merge Risk: ⚪ Minimal · up to The fix preserves wasm frame addresses while preventing invalid unsigned offset handling, and added no-JIT import coverage passes. No merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Warning Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use Comment |
Preview Builds
|
… array index With the JIT off, a call between wasm and JS is marshalled by the operations in WasmOperations.cpp. Each one has an `access` lambda that returns a frame slot. The lambda took the offset as an `int` and turned it into an array index: `&reinterpret_cast<V*>(arr)[i / sizeof(V)]`. `sizeof(V)` is a `size_t`, so the division converts the offset to an unsigned value first. `WasmToJSCallableFunctionSlot` is -0x8, so the index becomes 0x1FFFFFFFFFFFFFFF. The pointer arithmetic wraps to the right address in a normal build, but it is out of bounds, and an instrumented build folds the AddressSanitizer shadow check for it into a constant address. The first wasm call to a JS import then reads that address and segfaults in operationWasmToJSExitMarshalArguments. The JS-to-wasm entry wrapper passes negative offsets too: it writes the callee's stack arguments below the frame pointer. Take the offset as a `ptrdiff_t` and offset the pointer in bytes. Every call site already passes a byte offset that is a multiple of `sizeof(V)`, so the address does not change. JSTests/wasm/stress/jitless-call-js-import.js covers a jitless call to a JS import, with and without stack arguments.
c41fd31 to
9b1532c
Compare
Problem
AddressSanitizer: SEGV on unknown address,#0 operationWasmToJSExitMarshalArguments WasmOperations.cpp:320. The faulting instruction is the shadow checkmovabs $0x200000007fff7fff,%rax; cmpb $0x0,(%rax), which runs before the real slot load.accesslambda in that function:&reinterpret_cast<V*>(arr)[i / sizeof(V)].iis anintandsizeof(V)is asize_t, so the division is unsigned.WasmToJSCallableFunctionSlotis-0x8(WasmCallingConvention.h:48), and the index becomes0x1FFFFFFFFFFFFFFF. The pointer wraps back toarr - 8, so a normal build reads the right slot, but the access is out of bounds and clang folds its shadow address into a constant.JSTests/wasmfiles die on that read in a--useJIT=0run of an ASan build.Fix
accesslambdas take aptrdiff_tbyte offset and offset the pointer in bytes. There is no division, so a negative offset stays negative.sizeof(V), so each address is unchanged. Two of the lambdas also take negative offsets fromcalleeSPOffsetFromFP: the JS-to-wasm entry wrapper writes the callee's stack arguments below the frame pointer.JSTests/wasm/stress/jitless-call-js-import.js(new) segfaults before the change and passes after. A--useJIT=0run ofwasm/function-tests,wasm/js-api,wasm/noJIT,wasm/references,wasm/gcandwasm/function-references(237 files, the new test among them) fixes 43 and regresses none.Background
WasmToJSCallableFunctionSlotis the frame slot where the wasm-to-JS wrapper stores the callable function. It is at-0x8, below the frame pointer (InPlaceInterpreter.asm:1040).(address >> 3) + 0x7fff8000before each load.0x200000007fff7fffis that map applied to0xFFFFFFFFFFFFFFF8, the out-of-bounds address before it wraps.Notes
Repro, 42 bytes of wasm:
(import "e" "f" (func)) (func (export "g") call 0).jsc --useJIT=0 repro.json the ASan build: SEGV before the change,okafter.The fold is compiler dependent. clang 17 emits the correct shadow check for the old lambda. clang 21.1.8 folds it. Without
-fsanitize=address, clang 21.1.8 compiles the old lambda tomov -0x8(%rdi),%rax, so a release build is correct. That is why only sanitizer builds see this.The 43 newly passing files cover the import call in both directions:
function-tests/many-arguments-to-function.jsandmany-args-tail-call-sp-restored.jsfor stack arguments,function-tests/i64-conversion.jsandgc/wasm-js-marshal-i31-return.jsfor the value conversions,js-api/export-arity.jsfor the argument count slot. 16 other files exit non-zero in the same way before and after the change, because running them bare skips the options the harness passes (js-api/memory64-js-api.jsand similar).The new test runs each block
wasmTestLoopCounttimes, which the shell sets to 100 with the JIT off. It takes 0.13 s on the ASan build.Upstream
WebKit/WebKitmain has the same four lambdas, unchanged, so the bug is there too. Itsaccesscall sites are identical to ours.