Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in Skia
DescriptionRace in Skia
ComponentSkia
Bug ClassRace
Tracker520535595
Fix commitba3ee9b265f0 (skia) +6/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

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
From ba3ee9b265f06ed1f16e4d7402d29ad7ac583e7a Mon Sep 17 00:00:00 2001
From: alexisdavidc <[email protected]>
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 <[email protected]>
Reviewed-by: Kaylee Lubick <[email protected]>
---

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 [email protected]

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