Medium webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to an unexpected process termination
DescriptionA logic issue was addressed with improved state management.
ComponentJSC
Bug ClassLogic Error
Tracker310457
Fix commit9654ed7d9e3d (webkit) +29/-45
CISA KEVNot listed
CreditedSouta Sugiyama
Disclosed2026-09-14

Background

`JSGenericTypedArrayView::put`
JSC’s [[Set]] implementation for TypedArrays (Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h). It handles writes to integer-indexed and canonical-numeric-index-string properties.
Receiver / "this value altered"
In Reflect.set(target, key, value, receiver) (and prototype-chain sets), the receiver can differ from the object whose [[Set]] runs. isThisValueAltered(slot, thisObject) is true when slot.thisValue() is not the TypedArray itself.
ECMAScript spec change (tc39/ecma262#1556)
Updated integer-indexed [[Set]] so that when the receiver is not the TypedArray, the write must fall back to ordinary [[Set]] on the receiver (coercing the value, defining a property on the receiver), rather than silently writing into the TypedArray and ignoring the receiver.
`ordinarySetWithOwnDescriptor` / `attemptToInterceptPutByIndexOnHoleForPrototype`
Generic JSObject set machinery (Source/JavaScriptCore/runtime/JSObject.cpp) that walks the prototype chain when a property is not an own property.

Root Cause Analysis

Old JSGenericTypedArrayView::put ignored the receiver entirely: for any parsed integer index it called putByIndex(thisObject, ...), writing straight into the TypedArray regardless of whether slot.thisValue() was some other object. That both violated the updated spec (#1556) and, more importantly for memory safety, let the generic prototype-chain set machinery reach a TypedArray in an inconsistent way. When an ordinary object had a TypedArray on its prototype chain, a store to a hole index drove JSObject::attemptToInterceptPutByIndexOnHoleForPrototype, which — lacking any TypedArray-specific handling — fell through to generic prototype handling and could mis-dispatch the write, reaching assertions / invalid states (the advisory’s “unexpected process termination”).

The patch makes receiver handling explicit. In put, when isThisValueAltered(slot, thisObject): for a parsed integer index, if the array is detached or the index is out of bounds it returns true (spec: out-of-bounds integer-indexed writes on a TypedArray succeed-and-ignore), otherwise it defers to ordinarySetSlow(...) so the value lands on the receiver; for a canonical-numeric-index string with an altered receiver it returns true after giving the RHS its chance to throw. In ordinarySetWithOwnDescriptor, a new branch detects current != object && isTypedArrayType(current->type()) and re-dispatches through the TypedArray’s own methodTable()->put, so the TypedArray’s [[Set]] (with correct receiver semantics) runs instead of generic descriptor logic. In attemptToInterceptPutByIndexOnHoleForPrototype, a new branch handles a TypedArray found on the prototype chain: if it is out of bounds or i >= length() it reports success, otherwise returns false to let normal in-bounds handling proceed. The removed/updated test262 expectations (dozens of TypedArrayConstructors/internals/Set/... cases now passing) and the reflect-set.js change (receiver now receives the value, receiver.hasOwnProperty(0) becomes true) confirm the behavior change is receiver routing, not bounds relaxation.

Key insight
TypedArray [[Set]] ignored the Reflect.set receiver and the generic prototype-set path had no TypedArray case, so writes with an altered receiver were mis-routed into the TypedArray / generic descriptor logic, reaching an inconsistent state that aborted the process.

Attack Path

  1. Build an altered-receiver set Script calls Reflect.set(typedArray, index, value, otherReceiver), or places a TypedArray on an object’s prototype chain and stores to a matching index on the derived object.
  2. Reach the TypedArray via the generic path The store flows through ordinarySetWithOwnDescriptor / attemptToInterceptPutByIndexOnHoleForPrototype, which (pre-patch) had no TypedArray handling.
  3. Mis-dispatch the write The write is applied with wrong receiver semantics / routed through generic descriptor handling for a TypedArray.
  4. Inconsistent state The engine reaches a state its invariants forbid for TypedArray [[Set]].
  5. Process termination The inconsistency trips an assertion / release check, terminating the process (DoS). No memory-corruption primitive is shown by the diff.

Impact Assessment

Runs in the WebContent (renderer) JS engine; reachable from ordinary script via Reflect.set with a distinct receiver or a TypedArray on a prototype chain. The diff substantiates a logic/receiver-routing defect whose observable impact is a spec-conformance change plus unexpected process termination (crash/DoS), matching the advisory’s “unexpected process termination.” Bounds are still enforced on every path (detached / out-of-bounds short-circuit to success), so this diff does not substantiate an out-of-bounds or type-confusion memory-corruption primitive; any such escalation would be inferred and is not supported here.

Files Changed

  • JSTests/stress/reflect-set.js
  • JSTests/test262/expectations.yaml
  • Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h
  • Source/JavaScriptCore/runtime/JSObject.cpp

Audit Directions

  • Other `[[Set]]`/`defineOwnProperty` overrides ignoring the receiver
    Audit exotic objects (arguments, module namespace, DOM indexed getters/setters) for the same “ignore receiver, write to self” shortcut post-#1556.
  • Prototype-chain interception for exotic objects
    Review every caller of attemptToInterceptPutByIndexOnHole* for missing exotic-object cases that would fall through to generic logic.
  • Assertions reachable from receiver mismatch
    Look for RELEASE_ASSERT/ASSERT in the set/put paths that encode “receiver == this” assumptions now that receivers can legitimately differ.
diff --git a/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h b/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h
index 1484d0bada95..66a835323bfa 100644
--- a/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h
+++ b/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h
@@ -568,10 +568,19 @@ bool JSGenericTypedArrayView<Adaptor>::put(
 {
     JSGenericTypedArrayView* thisObject = jsCast<JSGenericTypedArrayView*>(cell);
 
-    if (std::optional<uint32_t> index = parseIndex(propertyName))
+    // https://tc39.es/ecma262/#sec-typedarray-set
+    if (std::optional<uint32_t> index = parseIndex(propertyName)) {
+        if (isThisValueAltered(slot, thisObject)) [[unlikely]] {
+            if (thisObject->isDetached() || !thisObject->inBounds(index.value()))
+                return true;
+            return ordinarySetSlow(globalObject, thisObject, propertyName, value, slot.thisValue(), slot.isStrictMode());
+        }
         return putByIndex(thisObject, globalObject, index.value(), value, slot.isStrictMode());
+    }
 
     if (isCanonicalNumericIndexString(propertyName.uid())) {
+        if (isThisValueAltered(slot, thisObject)) [[unlikely]]
+            return true;
         // Cases like '-0', '1.1', etc. are still obliged to give the RHS a chance to throw.
         toNativeFromValue<Adaptor>(globalObject, value);
         return true;
diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 9836ce52491a..a20b6aa1ac63 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -683,6 +683,11 @@ bool ordinarySetWithOwnDescriptor(JSGlobalObject* globalObject, JSObject* object
             RELEASE_AND_RETURN(scope, proxy->ProxyObject::put(proxy, globalObject, propertyName, value, slot));
         }
 
+        if (current != object && isTypedArrayType(current->type())) {
+            PutPropertySlot slot(receiver, shouldThrow);
+            RELEASE_AND_RETURN(scope, current->methodTable()->put(current, globalObject, propertyName, value, slot));
+        }
+
         // 9.1.9.1-2 Let ownDesc be ? O.[[GetOwnProperty]](P).
         bool ownDescriptorFound;
         if (current == object)
@@ -3159,7 +3164,16 @@ bool JSObject::attemptToInterceptPutByIndexOnHoleForPrototype(JSGlobalObject* gl
             putResult = proxy->putByIndexCommon(globalObject, thisValue, i, value, shouldThrow);
             return true;
         }
-        
+
+        if (isTypedArrayType(current->type())) {
+            auto* typedArray = jsCast<JSArrayBufferView*>(current);
+            if (typedArray->isOutOfBounds() || i >= typedArray->length()) {
+                putResult = true;
+                return true;
+            }
+            return false;
+        }
+
         JSValue prototypeValue = current->getPrototype(globalObject);
         RETURN_IF_EXCEPTION(scope, false);
         if (prototypeValue.isNull())
diff --git a/JSTests/stress/reflect-set.js b/JSTests/stress/reflect-set.js
index 448646366c3f..6d934ee2e767 100644
--- a/JSTests/stress/reflect-set.js
+++ b/JSTests/stress/reflect-set.js
@@ -687,11 +687,11 @@ var symbol = Symbol();
 
         var object = new TypedArray(64);
         var receiver = {};
-        // The receiver is ignored when the property name is an indexed one.
-        // shouldBe(Reflect.set(object, 0, 42, receiver), true);
+        // https://github.com/tc39/ecma262/pull/1556
         shouldBe(Reflect.set(object, 0, 42, receiver), true);
-        shouldBe(Reflect.get(object, 0), 42);
-        shouldBe(receiver.hasOwnProperty(0), false);
+        shouldBe(Reflect.get(object, 0), 0);
+        shouldBe(receiver.hasOwnProperty(0), true);
+        shouldBe(receiver[0], 42);
 
         var object = new TypedArray(1);
         transferArrayBuffer(object.buffer);
diff --git a/JSTests/test262/expectations.yaml b/JSTests/test262/expectations.yaml
index 7a7748d6ca9f..46744c185ad0 100644
--- a/JSTests/test262/expectations.yaml
+++ b/JSTests/test262/expectations.yaml
@@ -481,42 +481,6 @@ test/built-ins/TypedArrayConstructors/ctors/object-arg/iterated-array-changed-by
 test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/throw-type-error-before-custom-proto-access.js:
   default: 'Test262Error: Expected a TypeError but got a Test262Error (Testing with Float64Array and makePassthrough.)'
   strict mode: 'Test262Error: Expected a TypeError but got a Test262Error (Testing with Float64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-canonical-invalid-index-prototype-chain-set.js:
-  default: 'Test262Error: 1 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 1 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-canonical-invalid-index-reflect-set.js:
-  default: 'Test262Error: value should not be coerced Expected SameValue(«32», «0») to be true'
-  strict mode: 'Test262Error: value should not be coerced Expected SameValue(«32», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-valid-index-prototype-chain-set.js:
-  default: 'Test262Error: 0 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 0 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-valid-index-reflect-set.js:
-  default: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2n», «0n») to be true (Testing with BigInt64Array and makePassthrough.)'
-  strict mode: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2n», «0n») to be true (Testing with BigInt64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-canonical-invalid-index-prototype-chain-set.js:
-  default: 'Test262Error: 1 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 1 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-canonical-invalid-index-reflect-set.js:
-  default: 'Test262Error: value should not be coerced Expected SameValue(«160», «0») to be true'
-  strict mode: 'Test262Error: value should not be coerced Expected SameValue(«160», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-in-bounds-receiver-is-not-typed-array.js:
-  default: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-  strict mode: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds-receiver-is-not-object.js:
-  default: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-  strict mode: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds-receiver-is-not-typed-array.js:
-  default: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-  strict mode: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds-receiver-is-proto.js:
-  default: 'Test262Error: [[Set]] succeeeds'
-  strict mode: 'Test262Error: [[Set]] succeeeds'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-valid-index-prototype-chain-set.js:
-  default: 'Test262Error: 0 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 0 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-valid-index-reflect-set.js:
-  default: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2.3», «0») to be true (Testing with Float64Array and makePassthrough.)'
-  strict mode: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2.3», «0») to be true (Testing with Float64Array and makePassthrough.)'
 test/built-ins/Uint8Array/prototype/setFromBase64/trailing-garbage-empty.js:
   default: 'SyntaxError: Uint8Array.prototype.setFromBase64 requires a valid base64 string'
   strict mode: 'SyntaxError: Uint8Array.prototype.setFromBase64 requires a valid base64 string'
@@ -974,9 +938,6 @@ test/staging/sm/TypedArray/constructor-buffer-sequence.js:
 test/staging/sm/TypedArray/iterator-next-with-detached.js:
   default: 'TypeError: Underlying ArrayBuffer has been detached from the view or out-of-bounds'
   strict mode: 'TypeError: Underlying ArrayBuffer has been detached from the view or out-of-bounds'
-test/staging/sm/TypedArray/set-with-receiver.js:
-  default: 'Test262Error: Expected SameValue(«47», «0») to be true'
-  strict mode: 'Test262Error: Expected SameValue(«47», «0») to be true'
 test/staging/sm/TypedArray/slice-memcpy.js:
   default: 'Test262Error: Actual [1, 2, 1, 2, 3, 4] and expected [1, 2, 1, 2, 1, 2] should have the same contents. '
   strict mode: 'Test262Error: Actual [1, 2, 1, 2, 3, 4] and expected [1, 2, 1, 2, 1, 2] should have the same contents. '
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/stress/reflect-set.js b/JSTests/stress/reflect-set.js
index 448646366c3f..6d934ee2e767 100644
--- a/JSTests/stress/reflect-set.js
+++ b/JSTests/stress/reflect-set.js
@@ -687,11 +687,11 @@ var symbol = Symbol();
 
         var object = new TypedArray(64);
         var receiver = {};
-        // The receiver is ignored when the property name is an indexed one.
-        // shouldBe(Reflect.set(object, 0, 42, receiver), true);
+        // https://github.com/tc39/ecma262/pull/1556
         shouldBe(Reflect.set(object, 0, 42, receiver), true);
-        shouldBe(Reflect.get(object, 0), 42);
-        shouldBe(receiver.hasOwnProperty(0), false);
+        shouldBe(Reflect.get(object, 0), 0);
+        shouldBe(receiver.hasOwnProperty(0), true);
+        shouldBe(receiver[0], 42);
 
         var object = new TypedArray(1);
         transferArrayBuffer(object.buffer);
diff --git a/JSTests/test262/expectations.yaml b/JSTests/test262/expectations.yaml
index 7a7748d6ca9f..46744c185ad0 100644
--- a/JSTests/test262/expectations.yaml
+++ b/JSTests/test262/expectations.yaml
@@ -481,42 +481,6 @@ test/built-ins/TypedArrayConstructors/ctors/object-arg/iterated-array-changed-by
 test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/throw-type-error-before-custom-proto-access.js:
   default: 'Test262Error: Expected a TypeError but got a Test262Error (Testing with Float64Array and makePassthrough.)'
   strict mode: 'Test262Error: Expected a TypeError but got a Test262Error (Testing with Float64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-canonical-invalid-index-prototype-chain-set.js:
-  default: 'Test262Error: 1 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 1 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-canonical-invalid-index-reflect-set.js:
-  default: 'Test262Error: value should not be coerced Expected SameValue(«32», «0») to be true'
-  strict mode: 'Test262Error: value should not be coerced Expected SameValue(«32», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-valid-index-prototype-chain-set.js:
-  default: 'Test262Error: 0 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 0 setter should be unreachable! (Testing with BigInt64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-valid-index-reflect-set.js:
-  default: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2n», «0n») to be true (Testing with BigInt64Array and makePassthrough.)'
-  strict mode: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2n», «0n») to be true (Testing with BigInt64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-canonical-invalid-index-prototype-chain-set.js:
-  default: 'Test262Error: 1 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 1 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-canonical-invalid-index-reflect-set.js:
-  default: 'Test262Error: value should not be coerced Expected SameValue(«160», «0») to be true'
-  strict mode: 'Test262Error: value should not be coerced Expected SameValue(«160», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-in-bounds-receiver-is-not-typed-array.js:
-  default: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-  strict mode: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds-receiver-is-not-object.js:
-  default: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-  strict mode: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds-receiver-is-not-typed-array.js:
-  default: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-  strict mode: 'Test262Error: valueOf is not called Expected SameValue(«1», «0») to be true'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds-receiver-is-proto.js:
-  default: 'Test262Error: [[Set]] succeeeds'
-  strict mode: 'Test262Error: [[Set]] succeeeds'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-valid-index-prototype-chain-set.js:
-  default: 'Test262Error: 0 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-  strict mode: 'Test262Error: 0 setter should be unreachable! (Testing with Float64Array and makePassthrough.)'
-test/built-ins/TypedArrayConstructors/internals/Set/key-is-valid-index-reflect-set.js:
-  default: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2.3», «0») to be true (Testing with Float64Array and makePassthrough.)'
-  strict mode: 'Test262Error: target[0] should remain unchanged (receiver: empty object) Expected SameValue(«2.3», «0») to be true (Testing with Float64Array and makePassthrough.)'
 test/built-ins/Uint8Array/prototype/setFromBase64/trailing-garbage-empty.js:
   default: 'SyntaxError: Uint8Array.prototype.setFromBase64 requires a valid base64 string'
   strict mode: 'SyntaxError: Uint8Array.prototype.setFromBase64 requires a valid base64 string'
@@ -974,9 +938,6 @@ test/staging/sm/TypedArray/constructor-buffer-sequence.js:
 test/staging/sm/TypedArray/iterator-next-with-detached.js:
   default: 'TypeError: Underlying ArrayBuffer has been detached from the view or out-of-bounds'
   strict mode: 'TypeError: Underlying ArrayBuffer has been detached from the view or out-of-bounds'
-test/staging/sm/TypedArray/set-with-receiver.js:
-  default: 'Test262Error: Expected SameValue(«47», «0») to be true'
-  strict mode: 'Test262Error: Expected SameValue(«47», «0») to be true'
 test/staging/sm/TypedArray/slice-memcpy.js:
   default: 'Test262Error: Actual [1, 2, 1, 2, 3, 4] and expected [1, 2, 1, 2, 1, 2] should have the same contents. '
   strict mode: 'Test262Error: Actual [1, 2, 1, 2, 3, 4] and expected [1, 2, 1, 2, 1, 2] should have the same contents. '
Loading diff…