CVE-2026-106373
Overview
Files Changed
third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc
Original Bug Report
Potential UAF in font_fallback_win.cc via Data Race in GetFontFamilyForScript
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 the Windows-specific font fallback logic allows Web Workers to concurrently mutate a process-wide shared static cache. This can lead to a Use-After-Free (UAF) of a StringImpl object in the BufferPartition. The UAF could potentially be escalated to a double-free and arbitrary code execution in the renderer process.
Affected files:
third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Root Cause
There is a potential data race in third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc within GetFontFamilyForScript (and similar functions like GetColorEmojiFont).
The function utilizes a DEFINE_THREAD_SAFE_STATIC_LOCAL cache containing std::optional<AtomicString> objects:
struct AtomicFamilies {
AtomicFamilies() : families(ScriptToFontMap::kSize) {}
Vector<std::optional<AtomicString>> families;
};
DEFINE_THREAD_SAFE_STATIC_LOCAL(AtomicFamilies, families, ());
std::optional<AtomicString>& family = families.families[script];
if (family) {
return *family;
}
// ...
family.emplace((*script_font_map)[script].FirstAvailableFont(font_manager));
return *family;
While the static initialization of the AtomicFamilies container is thread-safe, concurrent access to its internal elements is not synchronized. If this function is invoked simultaneously from multiple Web Worker threads (e.g., via OffscreenCanvasRenderingContext2D::fillText requesting fallback fonts), a data race occurs.
Vulnerability Mechanism
- Thread A and Thread B concurrently reach the
if (family)check for an empty cache slot and bypass it. - Thread A resolves the font, calls
family.emplace(...)to store the newAtomicString, and returns aconst AtomicString&reference. - The reference bubbles up to
GetFallbackFamilyNameFromHardcodedChoices(infont_cache_skia_win.cc), where it is copied:const AtomicString fallback_family = GetFallbackFamily(...). This copy requires reading theStringImpl*and callingAddRef(). - The Race: Concurrently, Thread B executes its own
family.emplace(...). In C++,std::optional::emplacedestroys the existing object before constructing the new one. This calls the destructor for Thread A’sAtomicString, decrementing theStringImplrefcount to 0 and freeing it viaoperator delete. - Thread A resumes its copy operation and executes
AddRef()on the danglingStringImpl*pointer, resulting in a Use-After-Free (UAF).
Exploitation Path
Because scoped_refptr utilizes RAW_PTR_EXCLUSION, this UAF is not mitigated by MiraclePtr. The StringImpl is allocated within the WTF::BufferPartition.
An attacker could potentially exploit this by:
- Spawning multiple Web Workers to trigger the race condition reliably.
- Concurrently spraying the
BufferPartitionusing JS APIs (e.g.,fetch()with raw strings to createEncodedFormDatavectors, orTextEncoder.encode) to overlap the freedStringImplmemory with an attacker-controlled buffer. - Spoofing the
ref_count_field (offset 0) inside the sprayed buffer so that Thread A’s delayedAddRef()increments it. - When Thread A finishes using the copied string, it calls
Release(), dropping the spoofedref_count_to 0. This triggers a second call tooperator deleteon the memory address. - This results in a double-free of the attacker’s sprayed buffer within the
BufferPartition, a primitive that can typically be escalated to arbitrary read/write and Remote Code Execution (RCE) in the renderer process.
(Note: These are potential steps based on code analysis; our tooling agent does not run code to provide a working PoC.)
Suggested Fix
Cross-thread access to AtomicString instances is inherently unsafe as they rely on thread-local AtomicStringTable operations and unprotected refcounts. The code acknowledges this in a comment: // Try the AtomicString cache first. AtomicString must be per thread... but fails to enforce it.
The shared caches (families, emoji_font, math_font) should either:
- Be protected by a
base::Lockto serialize access and avoid races duringemplace, and avoid returning rawAtomicStringreferences that can be copied without synchronization. - Be migrated to use
WTF::ThreadSpecificso that each Web Worker thread maintains its own independent cache, eliminating the cross-thread access entirely.
Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb
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.