Skip to content

targets/wasm_exec.js: coerce pointer/length params to unsigned in helpers#5425

Closed
iamrajiv wants to merge 1 commit into
tinygo-org:devfrom
iamrajiv:fix-wasm-exec-unsigned-coercion
Closed

targets/wasm_exec.js: coerce pointer/length params to unsigned in helpers#5425
iamrajiv wants to merge 1 commit into
tinygo-org:devfrom
iamrajiv:fix-wasm-exec-unsigned-coercion

Conversation

@iamrajiv
Copy link
Copy Markdown

Description

Summary

Coerce pointer and length parameters in four wasm_exec.js helpers (loadValue, loadSlice, loadSliceOfValues, loadString) to unsigned 32-bit using >>>= 0, matching the pattern already used by every other import handler in the same file.

Fixes #5095

Background

I was working on a related fix in upstream Go's syscall/js package — golang/go#79148 (CL 778940, Code-Review +2, awaiting submit) — and got curious how TinyGo handles the same code paths.

While reading targets/wasm_exec.js, I noticed an inconsistency: most functions defensively coerce pointer/length params to unsigned with >>>= 0, but four internal helpers don't. Searching for an existing report I found #5095, which describes exactly this — RangeError: Start offset -X is outside the bounds of the buffer when WebAssembly memory pointers exceed 2^31.

The bug

When WASM calls a JS import with an i32 parameter holding a uint32 value whose high bit is set (a memory pointer in the upper half of WebAssembly memory), JavaScript receives it as a negative Number. Using that signed value directly as a byte offset in new DataView(buffer, offset, len) or new Uint8Array(buffer, offset, len) throws.

Reproduction (no large allocation required — the boundary behavior is isolated):

const memory = new WebAssembly.Memory({ initial: 1024 }); // 64 MB
const buffer = memory.buffer;
const ptr = 0xfc000000 | 0; // what JS sees if WASM uses the upper half of memory

console.log("ptr in JS:", ptr);
console.log("ptr >>> 0:", ptr >>> 0);

try {
  new DataView(buffer, ptr, 10);
} catch (e) {
  console.log("DataView:", e.message);
}
try {
  new DataView(buffer, ptr >>> 0, 10);
} catch (e) {
  console.log("fixed:   ", e.message);
}

Output:

ptr in JS: -67108864
ptr >>> 0: 4227858432
DataView: Start offset -67108864 is outside the bounds of the buffer
fixed:    Start offset 4227858432 is outside the bounds of the buffer

Same Start offset -X is outside the bounds pattern as reported in #5095.

Why these four helpers

Every other function in targets/wasm_exec.js already does the coercion at the top of the body. Examples:

  • fd_write, random_get, runtime.getRandomData
  • syscall/js.stringVal, valueGet, valueSet, valueDelete, valueCall, valueInvoke
  • syscall/js.copyBytesToGo, syscall/js.copyBytesToJS

Only these four were missed:

Helper File:line
loadValue(addr) targets/wasm_exec.js:162
loadSlice(array, len, cap) targets/wasm_exec.js:223
loadSliceOfValues(array, len, cap) targets/wasm_exec.js:227
loadString(ptr, len) targets/wasm_exec.js:235

This PR applies the same >>>= 0 defensive coercion to make them safe regardless of how they're called.

…pers

Most import handlers in wasm_exec.js defensively coerce pointer and
length parameters with `>>>= 0` so that values with the high bit set
(common in larger WebAssembly memories where pointers exceed 2^31) are
treated as unsigned rather than negative signed integers. The four
internal helpers (loadValue, loadSlice, loadSliceOfValues, loadString)
were the only places missing this coercion.

This caused `RangeError: Start offset -X is outside the bounds of the
buffer` from `new DataView(...)` (loadString) and `new Uint8Array(...)`
(loadSlice) when handling large data, as reported in tinygo-org#5095.

Apply the same `>>>= 0` coercion to the four helpers so they are safe
regardless of how they are called.

Fixes tinygo-org#5095
@eliasnaur
Copy link
Copy Markdown
Contributor

Some internal helpers had their coercion removed in #5407. Rationale

@iamrajiv
Copy link
Copy Markdown
Author

@eliasnaur thanks for the pointer i see now #5407 already added the coercion at every boundary handler which is the cleaner approach closing this sorry for the noise and thanks for taking the time to explain the rationale good signal for me on how this codebase prefers to handle these conversions

@iamrajiv iamrajiv closed this May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wasm_exec.js unsigned int problem

2 participants