CVE-2026-14406
Overview
Background
- `CodeEntry`
- a V8 CPU-profiler object describing a piece of code (name, resource, line info) that is read from multiple threads while the sampling profiler runs.
- `RareData`
- a lazily-allocated sub-object of
CodeEntryholding infrequently-used fields, pointed to by therare_data_std::unique_ptr. - Store-store reordering
- a weak-memory-model behavior (e.g. on arm64) where two stores by one thread can become visible to other threads in a different order than the program writes them.
- `std::atomic_thread_fence(std::memory_order_release)`
- a memory barrier that prevents prior memory writes from being reordered after it, so they are all visible before any subsequent store the reader observes.
Root Cause Analysis
CodeEntry::EnsureRareData() lazily created the sub-object with rare_data_.reset(new RareData()), a single statement that both constructs/initializes the RareData fields and publishes the pointer into the rare_data_ std::unique_ptr. On a platform with store-store reordering, the store that makes the rare_data_ pointer non-null could become visible to other threads before the writes that initialize the RareData members completed. Because CodeEntry is accessed concurrently by the profiler, another thread checking if (!rare_data_) could then observe a non-null pointer and dereference a RareData whose fields are still uninitialized.
The fix separates the two steps — construct into a local RareData* rare_data = new RareData(), issue std::atomic_thread_fence(std::memory_order_release), then rare_data_.reset(rare_data) — so the barrier guarantees the field initializations are globally visible before the pointer publication, restoring the invariant that a visible rare_data_ always points to a fully-initialized object.
rare_data_ pointer in the same unordered step as initializing the object it points to; the release fence added between construction and reset ensures the initialization is visible before the pointer is, closing the data race without penalizing readers (as an std::atomic field would).Attack Path
- Start profiling
Cause the CPU profiler to run so
CodeEntryobjects are created and accessed concurrently by the profiler thread and other threads. (inferred) - Trigger lazy allocation
Reach a code path that calls
CodeEntry::EnsureRareData()on aCodeEntryfor the first time, publishing a freshRareDatavia theunique_ptr. - Race the publication
On an arm64-class device, a second thread observes the reordered non-null
rare_data_before theRareDatafield initializations are visible, per the commit description. - Read uninitialized RareData
That thread dereferences the partially-constructed
RareData, reading uninitialized/garbage field values, which manifests as the advisory’s out-of-bounds read in V8. (inferred)
Impact Assessment
RareData, producing an out-of-bounds read within the V8/renderer process as the advisory states (“Out of bounds read in V8”). Preconditions are the profiler being active and a store-store-reordering platform; the advisory does not report in-the-wild exploitation, and any escalation beyond the OOB read is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/profiler/profile-generator.cc |
modified |
Files Changed
src/profiler/profile-generator.cc
Audit Directions
- Publish-before-init pointersSearch for lazy-init patterns (
ptr.reset(new T()), raw pointer assignment, orunique_ptr/plain-pointer publication) on objects read by multiple threads without an intervening release barrier or atomic store. - Non-atomic shared fields on weak memoryReview profiler and other concurrently-read structures for member pointers checked with a plain null test and dereferenced, on platforms with store-store reordering.
- `EnsureX`/lazy accessorsAudit similar
Ensure*/GetOrCreate*helpers that construct-and-publish in one step, confirming initialization ordering is enforced for any cross-thread reader.
Patch
From 6d33b8b329ef903dde6a296ebd973a41ea298de6 Mon Sep 17 00:00:00 2001
From: Benedikt Meurer <bmeurer@chromium.org>
Date: Fri, 15 May 2026 13:06:18 +0200
Subject: [PATCH] [profiler] Prevent data race on `CodeEntry::rare_data_`.
This adds an explicit memory barrier to `CodeEntry::EnsureRareData()`
for platforms with store-store reordering (e.g. arm64), where it's
otherwise possible to that the store to `rare_data_` becomes visible to
other threads before the field initialization completes.
Fixed: 513435594
Change-Id: I78a45c35bed6a3a2459c1ec994393709027c5f22
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7849376
Reviewed-by: Yang Guo <yangguo@chromium.org>
Commit-Queue: Yang Guo <yangguo@chromium.org>
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107368}
---
diff --git a/src/profiler/profile-generator.cc b/src/profiler/profile-generator.cc
index b62c067..bb7aea1 100644
--- a/src/profiler/profile-generator.cc
+++ b/src/profiler/profile-generator.cc
@@ -5,6 +5,7 @@
#include "src/profiler/profile-generator.h"
#include <algorithm>
+#include <atomic>
#include <vector>
#include "include/v8-profiler.h"
@@ -270,7 +271,15 @@
CodeEntry::RareData* CodeEntry::EnsureRareData() {
if (!rare_data_) {
- rare_data_.reset(new RareData());
+ // On platforms with store-store reordering, we need to ensure that the
+ // field initializations in RareData complete before the pointer (in the
+ // std::unique_ptr) becomes visible. Using an std::atomic for rare_data_
+ // would also work, but would penalize every read, also on platforms with
+ // no store-store reordering (like x64).
+ // See http://crbug.com/513435594 for additional details.
+ RareData* rare_data = new RareData();
+ std::atomic_thread_fence(std::memory_order_release);
+ rare_data_.reset(rare_data);
}
return rare_data_.get();
}
Original Bug Report
Potential data race in V8 CPU Profiler allows arbitrary memory read via CDP
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 data race in V8’s CPU profiler occurs when CodeEntry::rare_data_ is accessed concurrently by the profiler and main threads without synchronization. On weakly-ordered architectures like ARM64, this can lead to an arbitrary native memory read primitive accessible via the Chrome DevTools Protocol (CDP).
Affected files:
v8/src/profiler/profile-generator.hv8/src/profiler/profile-generator.ccv8/src/profiler/cpu-profiler.ccv8/src/inspector/v8-profiler-agent-impl.ccv8/src/api/api.ccv8/src/profiler/cpu-profiler-inl.h
Estimated timestamp from git blame: 2018-05-18
Summary
A potential data race in the V8 CPU profiler allows for a wild pointer read, which could be exploited to leak renderer process memory via the Chrome DevTools Protocol (CDP). The issue stems from unsynchronized access to CodeEntry::rare_data_ when a profile is being serialized on the main thread while the profiler background thread is concurrently updating deoptimization or bailout metadata.
Technical Details
The CodeEntry class (defined in v8/src/profiler/profile-generator.h) uses a std::unique_ptr<RareData> rare_data_ to store optional metadata such as bailout reasons and deoptimization information. This pointer is accessed across threads without atomic protections or mutexes.
-
Concurrent Writer: The
SamplingEventsProcessorthread processes code events (e.g.,CodeDisableOptEvent). It callsCodeEntry::set_bailout_reason(), which invokesEnsureRareData(). This method allocates a newRareDataobject and assigns it to therare_data_unique pointer:CodeEntry::RareData* CodeEntry::EnsureRareData() { if (!rare_data_) { rare_data_.reset(new RareData()); } return rare_data_.get(); } -
Concurrent Reader: When multiple profiling sessions are active on a single
v8::CpuProfiler, callingStopProfilingon one session does not stop the background profiler thread if other sessions remain active. While the background thread continues processing events, the main thread begins serializing the stopped profile. This eventually callsCodeEntry::bailout_reason()via the V8 API (v8::CpuProfileNode::GetBailoutReason):const char* bailout_reason() const { return rare_data_ ? rare_data_->bailout_reason_ : kEmptyBailoutReason; } -
The Race: On weakly-ordered architectures (such as ARM64), the memory store to
rare_data_and the initialization of its fields (likebailout_reason_) can be reordered. Consequently, the main thread may observe a non-nullrare_data_pointer before its internalbailout_reason_pointer has been correctly initialized. If the underlying memory was previously controlled by an attacker (e.g., via heap spraying), the reader may dereference a stale or malicious pointer.
Potential Impact
An attacker with access to the Profiler CDP domain (e.g., via a chrome.debugger extension or a compromised renderer) could potentially perform an arbitrary-address null-terminated read of the renderer process memory. The leaked data is returned to the CDP client as a string in the consoleProfileFinished event or the Profiler.stop response. This could be used to leak sensitive information or bypass ASLR.
Suggested Potential Exploitation Steps
- Attach a CDP client to the target renderer.
- Spray the native heap with objects of the same size as
RareDatacontaining a target memory address at the offset ofbailout_reason_. - Start multiple concurrent CPU profiling sessions.
- Trigger a code event that causes an optimization bailout (e.g., executing code that violates a previous optimization assumption).
- Immediately stop one of the profiling sessions.
- The race condition may cause the serialization process to read the attacker-controlled pointer as a
const char*, leaking memory from that address into the CDP response.
Suggested Fix
Access to CodeEntry::rare_data_ should be synchronized. This can be achieved by making rare_data_ an std::atomic<RareData*> with proper memory ordering (Acquire/Release) or by protecting the CodeEntry metadata with a mutex. Given that CodeEntry objects are heavily used during profiling, a lock-free approach with atomics is likely preferable for performance.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.