CVE-2026-17712
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/ports/SkTypeface_mac_ct.cpp |
modified |
Files Changed
src/ports/SkTypeface_mac_ct.cppsrc/ports/SkTypeface_mac_ct.h
Patch
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;
Original Bug Report
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.cppthird_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;
- Writer Thread (
onOpenStream): InsideonOpenStream(third_party/skia/src/ports/SkTypeface_mac_ct.cpp),fStreamis lazily initialized inside anSkOncelambda 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();
}
- Reader Threads (
onOpenExistingStreamandonMakeClone): Two other const methods readfStreamdirectly without executing or checkingfInitStream:
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.
- A webpage renders text using a variable system font on the main thread, resulting in its registration within the process-global
gTFCacheinSkTypeface_Mac::Make. - A concurrent worker thread (e.g., executing
OffscreenCanvasrendering) requests the same font and callsmakeClone()to adjust variations (such as optical sizingopsz), which invokesonMakeCloneand readsfStreamraw. - Concurrently, the main thread performs an action that triggers font serialization or printing (e.g.,
window.print()or via the Local Font Access API), invokingonOpenStreamto lazily construct and storefStreamunder theSkOnceguard. - If the worker thread’s read of
fStreaminterleaves 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.