Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in Skia
DescriptionRace in Skia
ComponentSkia
Bug ClassRace
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker520535595
Fix commitba3ee9b265f0 (skia) +6/-5
CWECWE-362 (Race condition)
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-06-05 (per vendor advisory)
Fix landed2026-06-12 (public git)
Disclosed2026-07-29
Patch gap47 days — fix public in source before the advisory

Background

`SkTypeface_Mac`
Skia’s macOS/CoreText-backed typeface implementation, defined in SkTypeface_mac_ct.cpp/SkTypeface_mac_ct.h, that can lazily materialize the underlying font bytes into fStream.
`fStream`
a mutable std::unique_ptr<SkStreamAsset> member that holds the typeface’s font data and is duplicated out to callers that need the raw font stream.
`SkOnce`
a one-shot initialization primitive (used here via the removed fInitStream) that runs a callback exactly once but does not serialize concurrent readers against the in-progress writer.
`SkSharedMutex`
a reader/writer lock whose RAII guards SkAutoSharedMutexExclusive (write) and SkAutoSharedMutexShared (read) are used by the fix to serialize all access to fStream.

Root Cause Analysis

The vulnerable path centered on the lazy initialization of fStream inside SkTypeface_Mac::onOpenStream, which built the stream under an SkOnce guard (fInitStream), while SkTypeface_Mac::onOpenExistingStream and the clone-producing function near line 1221 read fStream with no synchronization at all. Because the same SkTypeface_Mac instance became shared across threads once typefaces were cached in the process-wide gTFCache (introduced by change 204720), one thread could be partway through assigning fStream = std::make_unique<SkMemoryStream>(...) while another thread concurrently read the same fStream pointer, violating the invariant that a std::unique_ptr must not be read while it is being mutated.

The fix removes fInitStream and instead protects every access to fStream with a single SkSharedMutex fStreamMutex: the writer path in onOpenStream takes an exclusive lock (SkAutoSharedMutexExclusive), and both reader paths take shared locks (SkAutoSharedMutexShared), so a reader can never observe a half-written pointer. This works because the shared mutex establishes a happens-before relationship that forces readers to either wait for initialization to complete or observe the fully constructed stream.

Key insight
The single mistake was using SkOnce to serialize only the initialization of fStream while leaving concurrent readers of that same pointer entirely unsynchronized; the fix replaces the one-shot guard with a SkSharedMutex so that writer and reader accesses to fStream are mutually exclusive rather than racing.

Attack Path

  1. Populate the shared cache Cause the same SkTypeface_Mac to be created and stored in the process-wide gTFCache, so a single instance (and its fStream) is reachable from multiple threads.
  2. Trigger concurrent stream access On one thread invoke a code path reaching SkTypeface_Mac::onOpenStream (which begins writing fStream) while another thread invokes onOpenExistingStream or the clone path that reads fStream.
  3. Win the race on `fStream` Land the reader’s fStream ? fStream->duplicate() : nullptr check while onOpenStream is mid-assignment of the std::unique_ptr, so the reader observes a torn or partially initialized pointer. (inferred)
  4. Corrupt-pointer use Dereferencing or duplicating the raced fStream yields memory corruption or a use of an inconsistent pointer, the specific sink and any read/write primitive being unspecified by the diff. (inferred)

Impact Assessment

An attacker who can drive concurrent font/typeface operations on a shared cached SkTypeface_Mac gains a data race on fStream in whatever process runs Skia’s macOS font code (typically the renderer). The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8. The primary precondition is multi-threaded access to a SkTypeface_Mac shared via gTFCache, which the cross-thread caching change made reachable.

Changed Functions

FunctionChangeNotes
if
src/ports/SkTypeface_mac_ct.cpp
modified

Files Changed

  • src/ports/SkTypeface_mac_ct.cpp
  • src/ports/SkTypeface_mac_ct.h

Audit Directions

  • Lazy `mutable` members guarded by `SkOnce`
    Audit other mutable fields initialized once but read by multiple accessor methods; SkOnce serializes the initializer but not unsynchronized concurrent readers of the resulting field.
  • Objects shared via global/process-wide caches
    Wherever an instance is interned into a shared cache such as gTFCache, re-audit every member that was previously treated as single-threaded, since caching silently makes it cross-thread.
  • Asymmetric locking of a single field
    Look for members where only the write path or only some read paths are guarded; the fix here required locking fStream at every read and write site (onOpenStream, onOpenExistingStream, and the clone path) for the SkSharedMutex to be effective.
From ba3ee9b265f06ed1f16e4d7402d29ad7ac583e7a Mon Sep 17 00:00:00 2001
From: alexisdavidc <alexisdavidc@google.com>
Date: Fri, 12 Jun 2026 13:28:34 -0400
Subject: [PATCH] Resolved a Data Race on fStream in SkTypeface_Mac

There was a data race in SkTypeface_Mac where `onOpenStream`
and `onOpenExistingStream` would race to read/write `fStream`. While `onOpenStream` would begin initializing `fStream` on one thread, a separate thread could be calling `onOpenExistingStream` to try to read `fStream` before it was done initializing.

The issue was resolved by applying a mutex on `fStream`.

The cl introducing this bug (https://skia-review.git.corp.google.com/c/skia/+/204720) was focused on caching the typefaces received with a global process wide `gTFCache` to save on performance and memory. The issue arose in that since the SkTypeface_Mac could be accessed across threads, it became thread unsafe.

A test was added to this CL but removed as it was too large and took too long. It helps us keep it in the patch history for reference.

Bug: b/520535595
Change-Id: Id28aeed3d67a5a5246d22681f2c7ab0e6c133558
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1264296
Commit-Queue: Alexis Cruz-Ayala <alexisdavidc@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
---

diff --git a/src/ports/SkTypeface_mac_ct.cpp b/src/ports/SkTypeface_mac_ct.cpp
index 889d876..c1b40e0 100644
--- a/src/ports/SkTypeface_mac_ct.cpp
+++ b/src/ports/SkTypeface_mac_ct.cpp
@@ -605,9 +605,9 @@
 std::unique_ptr<SkStreamAsset> SkTypeface_Mac::onOpenStream(int* ttcIndex) const {
     *ttcIndex = 0;
 
-    fInitStream([this]{
+    SkAutoSharedMutexExclusive sm(fStreamMutex);
     if (fStream) {
-        return;
+        return fStream->duplicate();
     }
 
     SK_SFNT_ULONG fontType = get_font_type_tag(fFontRef.get());
@@ -705,12 +705,12 @@
         ++entry;
     }
     fStream = std::make_unique<SkMemoryStream>(std::move(streamData));
-    });
     return fStream->duplicate();
 }
 
 std::unique_ptr<SkStreamAsset> SkTypeface_Mac::onOpenExistingStream(int* ttcIndex) const {
     *ttcIndex = 0;
+    SkAutoSharedMutexShared sm(fStreamMutex);
     return fStream ? fStream->duplicate() : nullptr;
 }
 
@@ -1221,7 +1221,7 @@
     if (!ctVariant) {
         return nullptr;
     }
-
+    SkAutoSharedMutexShared sm(fStreamMutex);
     return SkTypeface_Mac::Make(std::move(ctVariant), ctVariation.opsz,
                                 fStream ? fStream->duplicate() : nullptr);
 }
diff --git a/src/ports/SkTypeface_mac_ct.h b/src/ports/SkTypeface_mac_ct.h
index 885a726..eea2083 100644
--- a/src/ports/SkTypeface_mac_ct.h
+++ b/src/ports/SkTypeface_mac_ct.h
@@ -19,6 +19,7 @@
 #include "include/core/SkStream.h"
 #include "include/core/SkTypeface.h"
 #include "include/private/SkOnce.h"
+#include "src/base/SkSharedMutex.h"
 #include "src/utils/mac/SkUniqueCFRef.h"
 
 #ifdef SK_BUILD_FOR_MAC
@@ -129,8 +130,8 @@
 private:
     mutable std::unique_ptr<SkStreamAsset> fStream;
     mutable SkUniqueCFRef<CFArrayRef> fVariationAxes;
+    mutable SkSharedMutex fStreamMutex;
     bool fIsFromStream;
-    mutable SkOnce fInitStream;
     mutable SkOnce fInitVariationAxes;
 
     using INHERITED = SkTypeface;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential data race and publication race on mutable fStream in SkTypeface_Mac

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 publication race exists in SkTypeface_Mac on macOS and iOS, where the mutable member fStream is lazily initialized within an SkOnce block but read raw in onOpenExistingStream and onMakeClone without synchronization. Because SkTypeface instances are shared process-wide via a global cache, concurrent reads and writes can occur across threads. On weak memory architectures like ARM64, this can lead to virtual method dispatch on a partially-initialized object, potentially causing memory corruption.

Affected files:

  • third_party/skia/src/ports/SkTypeface_mac_ct.cpp
  • third_party/skia/src/ports/SkTypeface_mac_ct.h

Estimated timestamp from git blame: 2019-12-13

Description

A potential thread-safety data race and publication race exists in SkTypeface_Mac (used on macOS and iOS) due to unsynchronized concurrent access to the mutable member fStream.

Root Cause Analysis

In third_party/skia/src/ports/SkTypeface_mac_ct.h, fStream and its initialization tracker fInitStream are declared as follows:

mutable std::unique_ptr<SkStreamAsset> fStream;
mutable SkOnce fInitStream;
  1. Writer Thread (onOpenStream): Inside onOpenStream (third_party/skia/src/ports/SkTypeface_mac_ct.cpp), fStream is lazily initialized inside an SkOnce lambda block:
std::unique_ptr<SkStreamAsset> SkTypeface_Mac::onOpenStream(int* ttcIndex) const {
    *ttcIndex = 0;
    fInitStream([this]{
        if (fStream) { return; }
        ...
        fStream = std::make_unique<SkMemoryStream>(std::move(streamData)); // Plain non-atomic store
    });
    return fStream->duplicate();
}
  1. Reader Threads (onOpenExistingStream and onMakeClone): Two other const methods read fStream directly without executing or checking fInitStream:
std::unique_ptr<SkStreamAsset> SkTypeface_Mac::onOpenExistingStream(int* ttcIndex) const {
    *ttcIndex = 0;
    return fStream ? fStream->duplicate() : nullptr; // Raw load, bypassing fInitStream
}

And inside onMakeClone:

return SkTypeface_Mac::Make(std::move(ctVariant), ctVariation.opsz,
                            fStream ? fStream->duplicate() : nullptr); // Raw load, bypassing fInitStream

Because SkTypeface is designed and documented as thread-safe (include/core/SkTypeface.h:52) and shared process-wide via a global cache (gTFCache in SkTypeface_Mac::Make), different threads (such as the main thread and a worker thread) can hold references to the exact same typeface instance.

Concurrency and Publication Risk on ARM64

On weakly ordered memory architectures like ARM64 (Apple Silicon), the raw store of the newly created SkMemoryStream pointer into fStream inside the SkOnce block can be reordered relative to the initialization of the stream object’s internal fields (such as its virtual table pointer vptr and data buffers).

Since the reader threads (onOpenExistingStream and onMakeClone) load fStream raw without any synchronization barrier or checking the SkOnce state, a concurrent thread can observe a non-null but partially initialized/stale SkMemoryStream object. When the reader thread subsequently attempts to call the inline duplicate() helper—which internally performs a virtual method call to onDuplicate()—it will dereference a stale or uninitialized vptr, resulting in an indirect jump to an arbitrary address and potential Remote Code Execution (RCE) in the sandboxed renderer process.

Potential Trigger Steps

Note: These are potential steps based on static analysis; our tooling agent does not currently have the capability to run code or verify a live Proof of Concept.

  1. A webpage renders text using a variable system font on the main thread, resulting in its registration within the process-global gTFCache in SkTypeface_Mac::Make.
  2. A concurrent worker thread (e.g., executing OffscreenCanvas rendering) requests the same font and calls makeClone() to adjust variations (such as optical sizing opsz), which invokes onMakeClone and reads fStream raw.
  3. Concurrently, the main thread performs an action that triggers font serialization or printing (e.g., window.print() or via the Local Font Access API), invoking onOpenStream to lazily construct and store fStream under the SkOnce guard.
  4. If the worker thread’s read of fStream interleaves during the write window on ARM64, it accesses the partially published object, leading to a crash or control flow hijack via virtual dispatch.

Suggested Fix

To guarantee proper happens-before ordering and synchronization, ensure that fInitStream is always invoked prior to reading fStream in both onOpenExistingStream and onMakeClone:

std::unique_ptr<SkStreamAsset> SkTypeface_Mac::onOpenExistingStream(int* ttcIndex) const {
    *ttcIndex = 0;
    fInitStream([]{}); // Ensure stream initialization has completed and barriers are retired
    return fStream ? fStream->duplicate() : nullptr;
}

Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf


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