Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Fonts
DescriptionUse after free in Fonts
ComponentFonts
Sub-componentBlink>Fonts
Bug ClassUAF
Tracker523699645
Fix commitffaf825e8c2e (chromium/src) +27/-23
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Files Changed

  • third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc

Original Bug Report

reported by [email protected]

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

  1. Thread A and Thread B concurrently reach the if (family) check for an empty cache slot and bypass it.
  2. Thread A resolves the font, calls family.emplace(...) to store the new AtomicString, and returns a const AtomicString& reference.
  3. The reference bubbles up to GetFallbackFamilyNameFromHardcodedChoices (in font_cache_skia_win.cc), where it is copied: const AtomicString fallback_family = GetFallbackFamily(...). This copy requires reading the StringImpl* and calling AddRef().
  4. The Race: Concurrently, Thread B executes its own family.emplace(...). In C++, std::optional::emplace destroys the existing object before constructing the new one. This calls the destructor for Thread A’s AtomicString, decrementing the StringImpl refcount to 0 and freeing it via operator delete.
  5. Thread A resumes its copy operation and executes AddRef() on the dangling StringImpl* 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:

  1. Spawning multiple Web Workers to trigger the race condition reliably.
  2. Concurrently spraying the BufferPartition using JS APIs (e.g., fetch() with raw strings to create EncodedFormData vectors, or TextEncoder.encode) to overlap the freed StringImpl memory with an attacker-controlled buffer.
  3. Spoofing the ref_count_ field (offset 0) inside the sprayed buffer so that Thread A’s delayed AddRef() increments it.
  4. When Thread A finishes using the copied string, it calls Release(), dropping the spoofed ref_count_ to 0. This triggers a second call to operator delete on the memory address.
  5. 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:

  1. Be protected by a base::Lock to serialize access and avoid races during emplace, and avoid returning raw AtomicString references that can be copied without synchronization.
  2. Be migrated to use WTF::ThreadSpecific so 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.

View on issue tracker