Overview

Low
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Sub-componentTesting
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — factors set 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: Fix commit (Fixed: 511263221) adds IsContentAccessible() to the HeapConstant branch. Citations :18242/:18244/:18245-18246 are accurate. Step 2 wrongly says TryGetStringConstant itself reads string contents off-thread.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker511263221
Fix commit5873b818a1c0 (v8/v8) +2/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-08 (per vendor advisory)
Fix landed2026-05-11 (public git)
Disclosed2026-06-30
Patch gap50 days — fix public in source before the advisory

Background

Maglev
V8’s mid-tier optimizing compiler, which builds its graph on a background thread that reads heap objects concurrently with the main thread.
`TryGetStringConstant`
MaglevGraphBuilder helper (src/maglev/maglev-graph-builder.cc:18242) that extracts a Handle<String> from a constant graph node for constant-folding of string operations.
Content-accessible string
a string whose character data can be safely read from a background thread (e.g. internalized/thin), as opposed to one the main thread may mutate in place.

Root Cause Analysis

Re-analyzed on 2026-10-03 against the actual fix commit 5873b818a1c0. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (67412ec04b79).

MaglevGraphBuilder::TryGetStringConstant (src/maglev/maglev-graph-builder.cc:18242) is called while building the Maglev graph on a background compiler thread. Pre-fix, when the input was a HeapConstant, it accepted any string: if (constant->object().IsString()) (src/maglev/maglev-graph-builder.cc:18244) and returned a Handle<String> to the raw object (:18245-18246). The missing invariant is that only strings whose contents are safe to read concurrently may be materialized and read on a background thread; an ordinary (non-internalized) string can be changed in place by the main thread (for example by externalization) while the compiler thread is reading it, a data race the advisory records as a use-after-free. The reporter frames this as BuildStringConcat/WriteToFlat racing MakeExternal; the shown diff does not touch those functions, so that specific chain is (inferred), but it agrees with the code that the defect is the unguarded string access.

The fix tightens the guard to constant->object().IsString() && constant->object().AsString().IsContentAccessible() (added diff line), so Maglev only turns a string into a compile-time constant when its contents are safe to read off-thread, eliminating the race on everything else.

Key insight
Maglev materialized any string constant and read it on a background thread without checking that the string’s contents were concurrency-safe; the fix adds an IsContentAccessible() check so only safe-to-read strings are used.

Attack Path

  1. Set up a racy string constant JavaScript arranges for a non-internalized string to be referenced as a constant in code that tiers up to Maglev. (inferred — trigger from reporter, not the diff)
  2. Compile on the background thread Maglev calls TryGetStringConstant (maglev-graph-builder.cc:18242), which pre-fix returns the raw string handle regardless of type (:18244-18246) without reading its contents; a caller then reads those contents off-thread (per the reporter, BuildStringConcat/WriteToFlat). (inferred — the read is in callers, not in the shown diff)
  3. Mutate concurrently on the main thread The main thread changes the same string’s representation in place (e.g. externalization), racing the background read and leaving it dereferencing stale/overwritten layout. (inferred)
  4. Memory corruption The racing background read observes an inconsistent string object, leading to the memory-safety error the advisory classifies as use-after-free. (inferred)

Impact Assessment

In the renderer process, an attacker who controls JavaScript could race the Maglev background compiler against main-thread string mutation to trigger a use-after-free during optimized compilation. The advisory rates this low severity with no in-the-wild exploitation; any escalation to code execution within the V8 sandbox is (inferred).

Files Changed

  • src/maglev/maglev-graph-builder.cc

Audit Directions

  • Off-thread heap reads
    Flag background-thread compiler code that reads object payloads (strings, elements) without a content-accessibility or snapshot guard against concurrent main-thread mutation.
  • Cross-tier parity
    When one tier (TurboFan) applies a safety predicate like StringCanSafelyBeRead/IsContentAccessible before constant-folding, verify the other tiers (Maglev) apply the equivalent guard on the same path.
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index e56cd01..bdba638 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -18241,7 +18241,8 @@
 
 MaybeHandle<String> MaglevGraphBuilder::TryGetStringConstant(ValueNode* value) {
   if (HeapConstant* constant = value->TryCast<HeapConstant>()) {
-    if (constant->object().IsString()) {
+    if (constant->object().IsString() &&
+        constant->object().AsString().IsContentAccessible()) {
       return handle(Cast<String>(*constant->object().object()),
                     local_isolate());
     }
Loading diff…

Original Bug Report

reported by vm...@google.com

Race condition in Maglev's BuildStringConcat leads to out-of-bounds write

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Maglev’s BuildStringConcat fails to verify if a string constant is safe to read on a background thread. This creates a data race with in-place string externalization on the main thread, potentially leading to an out-of-bounds write within the V8 sandbox.

Affected files:

  • v8/src/maglev/maglev-graph-builder.cc
  • v8/src/objects/string.cc

Estimated timestamp from git blame: 2026-01-13

Summary

A race condition exists in V8’s Maglev compiler during the constant-folding of string concatenations. The MaglevGraphBuilder::BuildStringConcat function retrieves string constants from the graph and attempts to concatenate them on a background compiler thread. However, unlike TurboFan, it fails to verify if the string’s content is safely accessible from a background thread (i.e., whether it is an internalized or thin string).

Concurrently, the main thread can externalize these same non-internalized strings (e.g., via Blink’s ToBlinkString calling v8::String::MakeExternal). Because String::MakeExternal does not acquire the internalized_string_access mutex for non-internalized strings, it performs an in-place layout change without synchronization against the Maglev background thread reading the string.

Technical Details

In v8/src/maglev/maglev-graph-builder.cc, BuildStringConcat calls TryGetStringConstant, which returns a Handle<String> from a Constant node. Maglev passes the raw handle directly to compiler::utils::ConcatenateStrings, which eventually invokes String::WriteToFlat on the background thread.

While the background thread is reading the string, the main thread may execute String::MakeExternal on the same object. For non-internalized strings, MakeExternal avoids taking the internalized_string_access lock and proceeds to:

  1. Update the object’s Map to an external string Map.
  2. Initialize external pointer fields (overwriting existing internal fields).

If the string is a ConsString, the new 32-bit ExternalPointerHandle allocated for the resource_ field overwrites the first_ child pointer, as both are located at offset 12.

When the background thread executing WriteToFlat reads first = cons_string->first(), it reads this ExternalPointerHandle. Because the handle is shifted by 8 (kExternalPointerIndexShift), its lowest bit is 0, so V8 interprets it as a compressed Smi. Pointer decompression adds the V8 cage base to this value, resolving it to a pointer within the V8 sandbox.

If an attacker inflates the ExternalPointerTable (by allocating many external strings), they can control the offset of this handle and point it to an attacker-controlled object (e.g., an ArrayBuffer) within the sandbox, creating a fake String.

When WriteToFlat accesses first->length(), it reads the length from the attacker-controlled fake string. By providing a massive length, the attacker causes WriteToFlat to miscalculate second_length as a negative number. This triggers a logic path where WriteToFlat replaces its internal length bound with the massive length from the fake string, and loops to copy characters from the fake string.

This results in a massive CopyChars operation into a small sink buffer, causing an out-of-bounds write that can corrupt memory within the V8 sandbox.

Potential Reproduction Steps

Note: These are suggested steps based on static analysis, as this agent cannot execute code.

  1. Create a non-internalized ConsString in old-space, large enough for in-place externalization (>= 24 bytes).
  2. Store this string in a constant-tracked data field of a globally accessible object.
  3. Define and warm up a JavaScript function that performs string concatenation using this constant field, triggering Maglev tier-up.
  4. Concurrently, repeatedly allocate ExternalStrings to inflate the ExternalPointerTable so that the next handle points into an attacker-controlled ArrayBuffer containing a fake String payload (with a huge length).
  5. Pass the non-internalized ConsString to a DOM sink (e.g., node.textContent = S) on the main thread to trigger v8::String::MakeExternal.
  6. Maglev’s background thread races with MakeExternal, reads the overwritten first_ pointer, resolves it to the fake String, and performs an out-of-bounds write during WriteToFlat.

Suggested Fix

In MaglevGraphBuilder::TryGetStringConstant, restrict the returned string constants to those that are safe to read on a background thread. Similar to TurboFan’s StringCanSafelyBeRead, only return handles for strings where IsContentAccessible() or SupportedStringKind() is true (i.e., internalized or thin strings).

MaybeHandle<String> MaglevGraphBuilder::TryGetStringConstant(ValueNode* value) {
  if (Constant* constant = value->TryCast<Constant>()) {
    if (constant->object().IsString() && constant->object().IsContentAccessible()) {
      return handle(Cast<String>(*constant->object().object()), local_isolate());
    }
  } ...
}

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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