CVE-2026-17712
Overview
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 intofStream. - `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) andSkAutoSharedMutexShared(read) are used by the fix to serialize all access tofStream.
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.
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
- Populate the shared cache
Cause the same
SkTypeface_Macto be created and stored in the process-widegTFCache, so a single instance (and itsfStream) is reachable from multiple threads. - Trigger concurrent stream access
On one thread invoke a code path reaching
SkTypeface_Mac::onOpenStream(which begins writingfStream) while another thread invokesonOpenExistingStreamor the clone path that readsfStream. - Win the race on `fStream`
Land the reader’s
fStream ? fStream->duplicate() : nullptrcheck whileonOpenStreamis mid-assignment of thestd::unique_ptr, so the reader observes a torn or partially initialized pointer.(inferred) - Corrupt-pointer use
Dereferencing or duplicating the raced
fStreamyields 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
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
| Function | Change | Notes |
|---|---|---|
ifsrc/ports/SkTypeface_mac_ct.cpp |
modified |
Files Changed
src/ports/SkTypeface_mac_ct.cppsrc/ports/SkTypeface_mac_ct.h
Audit Directions
- Lazy `mutable` members guarded by `SkOnce`Audit other
mutablefields initialized once but read by multiple accessor methods;SkOnceserializes the initializer but not unsynchronized concurrent readers of the resulting field. - Objects shared via global/process-wide cachesWherever 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 fieldLook for members where only the write path or only some read paths are guarded; the fix here required locking
fStreamat every read and write site (onOpenStream,onOpenExistingStream, and the clone path) for theSkSharedMutexto be effective.
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.