CVE-2026-84635
Overview
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 whenslot.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
JSObjectset 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.
[[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
- 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. - Reach the TypedArray via the generic path
The store flows through
ordinarySetWithOwnDescriptor/attemptToInterceptPutByIndexOnHoleForPrototype, which (pre-patch) had no TypedArray handling. - Mis-dispatch the write The write is applied with wrong receiver semantics / routed through generic descriptor handling for a TypedArray.
- Inconsistent state
The engine reaches a state its invariants forbid for TypedArray
[[Set]]. - Process termination The inconsistency trips an assertion / release check, terminating the process (DoS). No memory-corruption primitive is shown by the diff.
Impact Assessment
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.jsJSTests/test262/expectations.yamlSource/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.hSource/JavaScriptCore/runtime/JSObject.cpp
Audit Directions
- Other `[[Set]]`/`defineOwnProperty` overrides ignoring the receiverAudit 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 objectsReview every caller of
attemptToInterceptPutByIndexOnHole*for missing exotic-object cases that would fall through to generic logic. - Assertions reachable from receiver mismatchLook for
RELEASE_ASSERT/ASSERTin the set/put paths that encode “receiver == this” assumptions now that receivers can legitimately differ.
Patch
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. '
Regression Test / PoC
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. '