Medium CVSS 6.5 webkit Race 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC Runtime
Bug ClassRace
Tracker309861
Fix commitc8525868de35 (WebKit/WebKit) +4/-3
CWECWE-119, CWE-120 (Buffer bounds error, Buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedTristan Madani (@TristanInSec) from Talence Security, Nathaniel Oh (@calysteon)
Disclosed2026-05-11

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.)

Key insight
Length and backing span must be sampled from a single atomic snapshot when the buffer can grow concurrently; reading 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

  1. Set up a growable SharedArrayBuffer Create a growable SharedArrayBuffer and a typed-array view over it, shared with a Web Worker (parallel agent).
  2. Start a parallel grow loop In the worker, repeatedly grow the SharedArrayBuffer so its size changes asynchronously relative to the main thread.
  3. 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.
  4. Win the TOCTOU race Land a worker grow between the length() read and the typedSpan() read so the length used for allocation/iteration does not match the span actually captured.
  5. 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

The primitive is a TOCTOU-induced out-of-bounds access on a temporary sort buffer whose size no longer matches the operated-on span; the attacker must win a timing race against a parallel grow, making it probabilistic rather than deterministic. The commit establishes an unexpected process crash (LogicError, medium severity) and does not demonstrate a stable, controllable OOB read/write toward RCE. It is confined to the sandboxed WebContent process running JavaScript. Practical escalation would require reliably winning the race and shaping adjacent memory, neither of which the diff shows.

Changed Functions

FunctionChangeNotes
genericTypedArrayViewProtoFuncSortImpl
Source/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 test
JSTests/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.js
  • Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h

Audit Directions

  • Other prototype methods in the same header
    In JSGenericTypedArrayViewPrototypeFunctions.h grep for ->length() followed later by typedSpan()/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 callbacks
    Search for typed-array operations that call into user JS (comparators, coercions, iterators) and then reuse a pre-callback length or span; grep for isGrowableShared, typedSpan, typedVector, and post-callback re-reads.
  • General TOCTOU on shared/resizable buffers
    Across 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.

Original Bug Report

The reporter's bug is still restricted on the tracker.