[11] [JSC] Use span's length in genericTypedArrayViewProtoFuncSortImpl
Rated High because the diff fixes a TOCTOU on a GSAB-backed TypedArray's length: a parallel grow desynchronizes
length()fromtypedSpan().size(), and the sort body uses both — an OOB during element copy.
genericTypedArrayViewProtoFuncSortImpl performed two independent length reads — thisObject->length() and thisObject->typedSpan(). With a Growable SharedArrayBuffer backing, another agent's grow() between the reads could make them disagree.
Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
Patch Details
The order is swapped: originalSpan is constructed first and length is derived from originalSpan.size(). Single snapshot, single source of truth.
TOCTOU on a shared-memory length: deriving two values from two independent reads of the same race-prone quantity instead of a single snapshot.
Background
A Growable SharedArrayBuffer (constructed with maxByteLength) can be grown via SharedArrayBuffer.prototype.grow. A length-tracking TypedArray view over a GSAB returns a length derived from the current SAB byte length; SABs are shared across agents (main thread and Workers), so each length re-derivation reads a value that can change underneath the implementation. std::span here is a {pointer, size} pair captured at call time. %TypedArray%.prototype.sort accepts a user comparator and is implemented in C++ by copying into a work buffer, sorting, then copying back.
Analysis
length reflected the pre-grow size while originalSpan reflected post-grow. The sort body uses both: the work vector is sized from length, while the span is the source/destination of element copies and sort callbacks. The invariant length == originalSpan.size() no longer held.
Exploit shape: create a GSAB with maxByteLength significantly larger than initial, and a length-tracking Int32Array over it. Post the SAB to a Worker that spins on sab.grow(). On the main thread, repeatedly call ta.sort(compareFn). The body then operates with mismatched bounds — an OOB read or write during element copy or write-back, bounded by the size delta between pre- and post-grow length. The regression test confirms sort was the missing sibling to already-covered with/toReversed/toSorted.
This vulnerability weakens memory-type safety inside the WebContent process: an attacker controlling both the main thread and a worker could induce a bounded OOB primitive adjacent to the sort work vector or the GSAB-backed storage.
Audit directions
- Two independent length reads on a length-tracking TypedArray over a GSAB. Audit every method in
JSGenericTypedArrayViewPrototypeFunctions.hforlength()+typedSpan()separately, or any combination oflength(),byteLength(),typedVector()called more than once. Methods not yet in the regression test:copyWithin,fill,set,slice,subarray,indexOf,lastIndexOf,includes,reverse,join. - Validation before a JS callback boundary that returns to bounds-trusting code. The comparator callback is a re-entrancy point. Trace any TypedArray method that takes a JS-callable argument and verify the post-callback path consults the original snapshot.
- Boundary between JSC and JIT inlining. Grep
Source/JavaScriptCore/dfg/andftl/for thunks that inline TypedArray accessors — historically their length-derivation paths haven't been updated in lockstep. - Convert the regression test from denylist to allowlist. Iterate
Object.getOwnPropertyNames(TypedArray.prototype)programmatically to catch the next missing case automatically.