CVE-2026-106201
Overview
Files Changed
src/objects/prototype-info-inl.h
Patch
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); } }
Original Bug Report
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.hv8/src/compiler/heap-refs.ccv8/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.
- Establish a prototype object
pwhosederived_mapslist has already been allocated (e.g., by adding a different derived map beforehand), leaving the object-create slot at index 0 empty/cleared. - Execute a function
foo(x) { return Object.create(p); }multiple times to trigger concurrent JIT compilation on a background thread. - While the background compiler thread is executing
JSObjectRef::GetObjectCreateMapand reading index 0 ofderived_maps, concurrently executeObject.create(p)for the first time on the main thread. - The main thread allocates a new
Map, initializes its fields, and writes it to index 0 of the existing list using a relaxed store. - On ARM64, the compiler thread reads the newly published
Mappointer but observes stale/uninitialized fields, caching an incorrect instance size. - 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.
- Modify
WeakArrayList::Setor introduce an overload that accepts aReleaseStoreTagto write the elements using release memory ordering when publishing newly initialized heap objects. - 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.