FIX: targets/wasm: coerce memory offsets to unsigned before typed array and DataView access#5407
Conversation
| array >>>= 0; | ||
| len >>>= 0; |
There was a problem hiding this comment.
I think it's more better to perform the coercion as soon as possible, and not in helper functions. So I suggest removing these lines and add coercion to all top-level functions instead.
See comment on copyBytesToJS for my motivation.
| ptr >>>= 0; | ||
| len >>>= 0; |
| ret_addr >>>= 0; | ||
| src_addr >>>= 0; | ||
| src_len >>>= 0; | ||
| let num_bytes_copied_addr = ret_addr; | ||
| let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable | ||
|
|
||
| const dst = unboxValue(dst_ref); | ||
| const src = loadSlice(src_addr, src_len); |
There was a problem hiding this comment.
This is an example of why coercing should be performed first: the coercion done by loadSlice is both redundant and insufficient because you're using the unsigned values elsewhere.
…-level import functions
|
Hey @eliasnaur, thanks for the feedback! I've updated the patch, removed the coercions from loadSlice and loadString and moved them to the top of each import function instead. While going through all the top level functions I also noticed a few that were missed in the original patch: Let me know what you think! |
eliasnaur
left a comment
There was a problem hiding this comment.
LGTM, but let an actual expert take another look.
Hi TinyGo team, hope you are all doing well.
While stress testing a fairly large TinyGo WASM project that does sustained cross-module payload transfers, I started running into intermittent crashes once the Go heap became large enough for some memory offsets to cross the signed 32-bit boundary.
After tracing the issue through the JS bridge, it looks like a few pointer and length values are still being treated as signed i32 values inside
wasm_exec.js.WASM linear memory addresses are conceptually unsigned 32-bit values, but the bridge receives them as i32 parameters. In JavaScript, values with bit 31 set become negative numbers. When those values are forwarded directly into APIs like
Uint8ArrayorDataView, browsers reject them with errors like:For example, an offset such as
0x80000010arrives in JS as-2147483632, and the typed array constructor throws immediately.In asyncify-instrumented builds, this later propagates into:
because the exception surfaces during resumed execution.
The issue was a bit tricky to reproduce consistently since it only starts happening once the heap grows large enough, but under sustained memory pressure or large allocation workloads it became very reliable in my application.
This patch adds
>>>= 0coercions before affected values are passed into typed array orDataViewconstructors. The coercion simply reinterprets the bit pattern as an unsigned 32-bit integer without changing the underlying value.Patched sites:
loadSlice(array,len)loadString(ptr,len)syscall/js.valueLoadString(slice_ptr,slice_len)syscall/js.copyBytesToGo(ret_addr,dest_addr,dest_len)syscall/js.copyBytesToJS(ret_addr,src_addr,src_len)random_get(bufPtr,bufLen)fd_write(iovs_ptr,iovs_len,nwritten_ptr)One interesting detail is that
stringValalready contained avalue_ptr >>>= 0coercion, so there was already some awareness of this issue in one part of the bridge. This patch extends the same handling consistently to the remaining affected paths.The changes are intentionally very small and localized. There should be no behavioral difference for applications whose heaps stay below the signed 32-bit boundary.
I was able to reproduce the crash consistently before the patch and could no longer reproduce it afterwards under the same stress conditions.
My guess is that this may affect other large or long-running TinyGo WASM applications as well, especially ones dealing with large payloads or sustained heap growth.
Thanks for taking a look.