Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in V8
DescriptionRace condition in V8
ComponentV8
Sub-componentObjects
Bug ClassRace
Tracker517546096
Fix commit29245b9096e3 (v8/v8) +2/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Files Changed

  • src/objects/prototype-info-inl.h
From 29245b9096e370d9622878ca175411dad2f08e56 Mon Sep 17 00:00:00 2001
From: Arash Kazemi <[email protected]>
Date: Thu, 03 Sep 2026 14:14:06 +0000
Subject: [PATCH] [objects] Use acq/rel semantics for PrototypeInfo::ObjectCreateMap

When caching the Object.create map on an existing derived_maps list,
slot 0 was previously updated with a relaxed store. A concurrent
compiler thread reading via ObjectCreateMap(kAcquireLoad) loaded slot 0
with a relaxed load, allowing weakly-ordered architectures (or the
compiler) to make the Map pointer visible before its fields were
initialized.

This CL updates slot 0 to use release-store when updating existing
derived_maps and acquire-load when reading under AcquireLoadTag.

TAG=agy

Fixed: 517546096
Change-Id: I2527f8b8ebad31b2846e6dc35d5e03decf482213
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8348126
Reviewed-by: Igor Sheludko <[email protected]>
Commit-Queue: Arash Kazemi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#109668}
---
diff --git a/src/objects/prototype-info-inl.h b/src/objects/prototype-info-inl.h
index 53c48ca..eacf6bc 100644
--- a/src/objects/prototype-info-inl.h
+++ b/src/objects/prototype-info-inl.h
@@ -123,7 +123,7 @@
   // Index 0 is the map for object create
   Tagged<WeakArrayList> derived_list = Cast<WeakArrayList>(derived);
   DCHECK_GT(derived_list->length().value(), 0);
-  Tagged<MaybeObject> el = derived_list->Get(0);
+  Tagged<MaybeObject> el = derived_list->get(0, kAcquireLoad);
   DCHECK(el.IsWeakOrCleared());
   return el;
 }
@@ -141,7 +141,7 @@
     Tagged<WeakArrayList> derived = Cast<WeakArrayList>(info->derived_maps());
     DCHECK(derived->Get(0).IsCleared());
     DCHECK_GT(derived->length().value(), 0);
-    derived->Set(0, MakeWeak(*map));
+    derived->set(0, MakeWeak(*map), kReleaseStore);
   }
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

Potential Race in PrototypeInfo::SetObjectCreateMap on Weak-Memory Architectures

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential race condition in V8’s map-publishing mechanism on weak-memory architectures like ARM64 may allow a concurrent compiler thread to observe a partially initialized Map. This occurs due to a relaxed store when updating an existing derived maps list, which can bypass memory synchronization. Consequently, the JIT compiler could cache a stale instance size and generate code that allocates an undersized object, leading to a potential heap out-of-bounds write.

Affected files:

  • v8/src/objects/prototype-info-inl.h
  • v8/src/compiler/heap-refs.cc
  • v8/src/compiler/js-create-lowering.cc

Estimated timestamp from git blame: 2023-12-11

Description

A potential concurrency issue exists in V8 when publishing a newly allocated Map to an already-created WeakArrayList of derived maps on weak-memory architectures (such as ARM64).

In v8/src/objects/prototype-info-inl.h, the function PrototypeInfo::SetObjectCreateMap publishes a newly allocated Map into derived_maps:

void PrototypeInfo::SetObjectCreateMap(DirectHandle<PrototypeInfo> info,
                                       DirectHandle<Map> map,
                                       Isolate* isolate) {
  if (IsUndefined(info->derived_maps())) {
    Tagged<WeakArrayList> derived = *isolate->factory()->NewWeakArrayList(1);
    derived->Set(0, MakeWeak(*map));
    derived->set_length(1);
    info->set_derived_maps(derived, kReleaseStore);
  } else {
    Tagged<WeakArrayList> derived = Cast<WeakArrayList>(info->derived_maps());
    DCHECK(derived->Get(0).IsCleared());
    DCHECK_GT(derived->length().value(), 0);
    derived->Set(0, MakeWeak(*map)); // <--- Relaxed store!
  }
}

When the else branch is taken, the pointer to the new Map is written directly into the existing WeakArrayList via derived->Set(0, MakeWeak(*map)). In v8/src/objects/fixed-array-inl.h, this is defined as a relaxed store:

void WeakArrayList::Set(uint32_t index, Tagged<MaybeObject> value,
                        WriteBarrierMode mode) {
  set(index, value, kRelaxedStore, mode);
}

Because the stores initializing the fields of the newly created Map (such as instance_size and bit_field3) in Factory::InitializeMap are either non-atomic or relaxed stores, ARM64’s weak memory model allows the relaxed store of the Map’s pointer into derived_maps to be reordered and become globally visible to other CPU cores before the Map’s internal fields are fully flushed to memory.

Impact and JIT Compilation Path

Concurrently, a background compiler thread can execute JSObjectRef::GetObjectCreateMap in v8/src/compiler/heap-refs.cc:

OptionalMapRef JSObjectRef::GetObjectCreateMap(JSHeapBroker* broker) const {
  ...
  Tagged<MaybeObject> maybe_object_create_map =
      Cast<PrototypeInfo>(maybe_proto_info)->ObjectCreateMap(kAcquireLoad);
  if (!maybe_object_create_map.IsWeak()) return {};

  return MapRef(broker->GetOrCreateData(
      maybe_object_create_map.GetHeapObjectAssumeWeak(), kAssumeMemoryFence));
}

Although derived_maps is loaded using kAcquireLoad inside ObjectCreateMap(kAcquireLoad), accessing the slot itself (derived_list->Get(0)) is implemented as a standard Relaxed_Load(). No memory synchronization fence exists between the main thread writing to index 0 and the compiler thread reading index 0.

Furthermore, by constructing the reference with kAssumeMemoryFence, the compiler thread explicitly bypasses IsPendingAllocation checks. Consequently, the background compiler thread can load the uninitialized/stale Map and cache an incorrect (smaller or stale) instance_size.

If the JIT compiler later optimizes a JSCreateObject node in JSCreateLowering::ReduceJSCreateObject, it will query this cached representation:

int const instance_size = instance_map.instance_size();

Using this smaller instance size, the compiler generates instructions to allocate an undersized object via the bump allocator, while stamping it with the actual Map (which expects a larger layout). Any subsequent in-object property stores targeting higher offsets in the optimized code will write past the allocation boundary, leading to an inter-object heap out-of-bounds write in the young generation.

Potential Steps to Trigger the Race

Note: These are potential steps. Our current testing environment does not support executing active exploit code to confirm runtime reachability, but the memory ordering logic suggests this path is theoretically feasible on multi-core ARM64 systems.

  1. Establish a prototype object p whose derived_maps list has already been allocated (e.g., by adding a different derived map beforehand), leaving the object-create slot at index 0 empty/cleared.
  2. Execute a function foo(x) { return Object.create(p); } multiple times to trigger concurrent JIT compilation on a background thread.
  3. While the background compiler thread is executing JSObjectRef::GetObjectCreateMap and reading index 0 of derived_maps, concurrently execute Object.create(p) for the first time on the main thread.
  4. The main thread allocates a new Map, initializes its fields, and writes it to index 0 of the existing list using a relaxed store.
  5. On ARM64, the compiler thread reads the newly published Map pointer but observes stale/uninitialized fields, caching an incorrect instance size.
  6. The resulting optimized code allocates an undersized object, leading to heap memory corruption when high-offset properties are stored.

Suggested Fix

To ensure proper synchronization, the publication of a new Map to an already-created WeakArrayList must use release-acquire semantics.

  1. Modify WeakArrayList::Set or introduce an overload that accepts a ReleaseStoreTag to write the elements using release memory ordering when publishing newly initialized heap objects.
  2. In PrototypeInfo::SetObjectCreateMap, use a release store when writing to the list slot if the container is already published:
  } else {
    Tagged<WeakArrayList> derived = Cast<WeakArrayList>(info->derived_maps());
    DCHECK(derived->Get(0).IsCleared());
    DCHECK_GT(derived->length().value(), 0);
    derived->Set(0, MakeWeak(*map), kReleaseStore); // Enforce release semantics
  }

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker