CVE-2026-28902
Overview
Background
- Growable SharedArrayBuffer
- A SharedArrayBuffer that can be grown at runtime and is shared across agents, so its byte length can change concurrently from another thread.
- TOCTOU (time-of-check/time-of-use)
- A race where a value read at one point (length) becomes stale by the time a dependent operation (span access) uses it, because it changed in between.
- typedSpan()
- Returns a view (pointer+size) over the typed array’s current backing storage, i.e. an authoritative snapshot of both the data pointer and element count.
- Comparator-driven sort
- TypedArray.prototype.sort with a JS comparator, which snapshots the array into a temporary buffer, sorts, and writes back — creating a window during which script/other agents can run or grow the buffer.
Root Cause Analysis
genericTypedArrayViewProtoFuncSortImpl in Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h implements TypedArray.prototype.sort. Sorting a typed array with a user comparator requires snapshotting the view’s backing storage into a temporary vector sized from the array length, then sorting and writing back. In the vulnerable code the function first read size_t length = thisObject->length();, used that to early-out and to size the temporary buffer (CheckedSize { length } * 2U), and only afterwards obtained auto originalSpan = thisObject->typedSpan();. When the typed array is backed by a growable SharedArrayBuffer, another agent (a Web Worker) can grow the buffer concurrently between the length() read and the typedSpan() read. That opens a time-of-check/time-of-use window: length reflects one size while originalSpan reflects a different, potentially larger extent (or the two are otherwise inconsistent), so subsequent copying and indexing driven by length no longer match the actual span, yielding out-of-bounds access relative to the snapshot.
The patch closes the window by acquiring the span first and deriving length from it: auto originalSpan = thisObject->typedSpan(); size_t length = originalSpan.size();, then doing the < 2 early-out and buffer sizing from that single consistent snapshot. This restores the invariant that the length used for allocation, iteration, and write-back is exactly the length of the span actually being operated on, eliminating the race where a parallel grow desynchronizes the two. The regression test’s added line ta.sort((a, b) => a - b); exercises exactly the comparator-driven sort path under parallel growth of a growable SharedArrayBuffer. (Inference: that the concrete faulting operation is an OOB read/write during the snapshot copy or write-back is deduced from the reordering and the test name parallel-grow-during-prototype-methods; the exact copy loop that consumes length vs. originalSpan beyond the shown lines is elsewhere in the same function.)
length() before typedSpan() created a window a parallel SharedArrayBuffer grow could exploit, and the fix simply derives length from the span taken first.Attack Path
- Set up a growable SharedArrayBuffer Create a growable SharedArrayBuffer and a typed-array view over it, shared with a Web Worker (parallel agent).
- Start a parallel grow loop In the worker, repeatedly grow the SharedArrayBuffer so its size changes asynchronously relative to the main thread.
- Invoke sort with a comparator
On the main thread call
ta.sort((a,b)=>a-b), entering genericTypedArrayViewProtoFuncSortImpl which reads length then, separately, the span. - Win the TOCTOU race
Land a worker grow between the
length()read and thetypedSpan()read so the length used for allocation/iteration does not match the span actually captured. - Trigger OOB and crash The mismatched length drives copying/write-back past the snapshot’s valid range, producing an out-of-bounds access and an unexpected process crash; a controllable primitive is not demonstrated by the diff.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
genericTypedArrayViewProtoFuncSortImplSource/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h |
modified | Reordered so `typedSpan()` is taken first and `length` is derived from `originalSpan.size()`, eliminating a TOCTOU window between the length read and the span read when a growable SharedArrayBuffer is grown in parallel. |
parallel-grow-during-prototype-methods.js regression testJSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js |
modified | Adds `ta.sort((a, b) => a - b);` to the parallel-grow stress loop to cover the comparator-driven sort path. |
Files Changed
JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.jsSource/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
Audit Directions
- Other prototype methods in the same headerIn JSGenericTypedArrayViewPrototypeFunctions.h grep for
->length()followed later bytypedSpan()/typedVector()in the same function (e.g. sort/toSorted/with/toReversed/copyWithin/fill/slice/set) and reorder any that read length before the span. - Growable-buffer resynchronization after user callbacksSearch for typed-array operations that call into user JS (comparators, coercions, iterators) and then reuse a pre-callback
lengthor span; grep forisGrowableShared,typedSpan,typedVector, and post-callback re-reads. - General TOCTOU on shared/resizable buffersAcross runtime and DFG/FTL intrinsics grep for
SharedArrayBuffer,growable,resizable, and length caching around loops to find any check-then-use where the byte length can change concurrently.