Runtime: Copy only the view's window in swjs_load_typed_array - #810
Open
kateinoigakukun wants to merge 1 commit into
Open
Runtime: Copy only the view's window in swjs_load_typed_array#810kateinoigakukun wants to merge 1 commit into
kateinoigakukun wants to merge 1 commit into
Conversation
`new Uint8Array(typedArray.buffer)` views the whole backing `ArrayBuffer`, ignoring the `byteOffset` and `byteLength` of the view it was handed. A view with a non-zero offset (anything from `subarray`, or `new Uint8Array(buffer, offset, length)`) therefore arrives in the guest shifted by that offset, silently wrong rather than failing. It is also a memory-safety bug: `JSTypedArray.copyMemory(to:)` sizes the destination from the view's own `length`, so whenever the view is smaller than its backing buffer the runtime writes past the end of the destination and corrupts whatever follows it in linear memory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kateinoigakukun
force-pushed
the
fix/load-typed-array-byteoffset
branch
from
September 4, 2026 06:20
d8519f7 to
f249c50
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
swjs_load_typed_arrayviews the whole backingArrayBufferinstead of the window the view it was handed describes:typedArray.bufferis the backingArrayBuffer, which may be larger than the view and may start before it. Any view produced bysubarrayornew Uint8Array(buffer, offset, length)has a non-zerobyteOffset, and the copy ignores it.Minimal repro
The bytes come back shifted by
byteOffset. Nothing throws — the result is just quietly wrong, which is what makes this class of bug expensive to find downstream.Why this is a memory-safety issue, not only a correctness one
The guest sizes the destination from the view's element count, never from the backing buffer:
Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift—lengthisjsObject["length"], the view's element count.copyMemory(to:)preconditions onlybuffer.count >= lengthbefore callingswjs_load_typed_array.withUnsafeBytes/withUnsafeBytesAsyncallocate exactlycapacity: length.So whenever the view is smaller than its backing buffer, JavaScript writes
buffer.byteLengthbytes into a destination sized forview.byteLength. The excess lands in whatever follows the destination in linear memory. Thepreconditioncannot catch it — it is satisfied, and the overrun happens on the JS side of the boundary. There is no error, no trap, and no diagnostic; just corrupted memory after the buffer.It is reachable from ordinary API use:
JSTypedArray(unsafelyWrapping:)over any JS-side view, or aUint8Arrayhanded in from JavaScript that happens to be asubarray.The fix
Respect the view's window:
This is the generic solution for every element type the runtime supports — all
TypedArraytypes andDataViewexposebyteOffsetandbyteLength, and for a whole-buffer view (the common case, and everythingswjs_create_typed_arrayproduces) it is(buffer, 0, buffer.byteLength), identical to today's behaviour.Follow-up not included here
The runtime still trusts that the guest sized the destination correctly; it has no way to know how many bytes the caller allocated. A defensive bounds check would need the length plumbed through the
swjs_load_typed_arrayABI, which is a larger change than this fix warrants, so it is left out deliberately. Worth considering separately if the ABI is revised.Testing
Plugins/PackageToJS/Templates/runtime.mjswas regenerated withmake regenerate_swiftpm_resources, not hand-edited; its diff is exactly the change toRuntime/src/index.ts.runtime.d.tsis unchanged (no type surface change).New tests, both of which fail before the fix and pass after:
Runtime/test/load-typed-array.test.ts— drivesswjs_load_typed_arrayagainst a realWebAssembly.Memory, following the pattern inpointer-normalization.test.ts. CoversUint8ArrayandInt32Arrayviews with a non-zerobyteOffset, aDataView, the no-overrun property (guest memory past the view'sbyteLengthmust keep its sentinel), and a whole-buffer view as a control.Before the fix: 4 failed, 1 passed (the whole-buffer control). After: 5 passed.
Tests/JavaScriptKitTests/JSTypedArrayTests.swift—testTypedArrayWithByteOffsetandtestMultiByteTypedArrayWithByteOffsetassert the same two properties through the Swift API: correct bytes viawithUnsafeBytes, and thatcopyMemory(to:)leaves sentinel values past the destination untouched.With the runtime reverted to
new Uint8Array(typedArray.buffer):194 passed, 2 failed, 196 total, and the overrun assertions name the corrupted slots directly (copyMemory(to:) wrote past the destination at 8…at 31). With the fix:196 passed, 196 total.Ran locally:
npm run test:runtime,npx prettier --check Runtime/src,npm run check:bridgejs-dts,./Utilities/format.swift(no changes),swift test --package-path ./Plugins/PackageToJS,swift test --package-path ./Plugins/BridgeJS, andmake unittest BUILD_SYSTEM=nativeagainst awasm32-unknown-wasip1SDK (all suites pass, includingJSTypedArrayTests).🤖 Generated with Claude Code