CVE-2026-28857
Overview
Background
- Growable SharedArrayBuffer
- A SharedArrayBuffer created with maxByteLength that another agent can grow (never shrink) concurrently.
- TypedArray immutable methods
- toReversed/toSorted/with return a new typed array copy of the receiver’s contents.
- typedSpan()
- A view (pointer+size) over the typed array’s current backing storage, which can change if the SAB grows.
- TOCTOU
- A time-of-check/time-of-use race: length is read at one time and used to copy at another, during which the backing can change.
Root Cause Analysis
The immutable TypedArray prototype methods toReversed, toSorted and with (genericTypedArrayViewProtoFunc*) computed a length and then, in a separate step, obtained the source data span to copy into the freshly-allocated result. In toReversed/toSorted the code read size_t length = thisObject->length();, allocated the result of that length, then re-fetched from = thisObject->typedSpan() and copied length elements (with only an ASSERT that from.size()==length). When the typed array is backed by a growable SharedArrayBuffer, another agent can grow the SAB (SABs can grow but never shrink) concurrently between the length read and the copy, so the re-fetched span no longer matches the captured length, and the backing storage may have been reallocated. Copying length elements from a span whose size/pointer changed is an out-of-bounds read (and the ASSERT is not present in release builds).
The fix snapshots the span once — auto originalSpan = thisObject->typedSpan(); size_t length = originalSpan.size(); — and copies from that exact snapshot, so the length and the source pointer are mutually consistent; with similarly snapshots maybeUpdatedSpan and compares its size to the earlier length.
The restored invariant is that the element count and the source buffer used for the copy come from a single atomic view of the array, immune to a concurrent grow.
Attack Path
- Share a growable SAB Create a growable SharedArrayBuffer (maxByteLength set) and a TypedArray view over it, and share the SAB with a worker agent.
- Grow in parallel The worker repeatedly calls sab.grow(…) in a tight loop, changing the view’s length/backing during main-thread operations.
- Call the immutable methods On the main thread repeatedly call ta.toReversed()/toSorted()/with(…), which read length then re-fetch the span to copy.
- Out-of-bounds copy A grow lands between the length read and the copy, so the method copies using a stale length against a changed span — an OOB read / memory corruption in WebContent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
genericTypedArrayViewProtoFuncToReversedSource/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h |
modified | Snapshots typedSpan() once and derives length from it, copying from that snapshot instead of re-fetching the span after allocation. |
genericTypedArrayViewProtoFuncToSortedSource/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h |
modified | Same span-snapshot fix before copyElements + sort. |
genericTypedArrayViewProtoFuncWithSource/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h |
modified | Uses a snapshotted maybeUpdatedSpan for the length-change check and the copy, covering the BigInt shrink/grow branches consistently. |
Files Changed
JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.jsSource/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
Audit Directions
- Other prototype methods re-fetching spansgrep JSGenericTypedArrayView*Functions for
thisObject->length()followed later by a freshtypedSpan()/data pointer used in a copy; require a single snapshot. - SAB grow races generallyAudit typed-array/DataView fast paths that cache length across an allocation or user callback for growable-SAB TOCTOU (getByteLength, set, slice, subarray).
- ASSERT-only size checksFind
ASSERT(x.size() == length)around copyElements where the size can change at runtime, since release builds elide the check.