CVE-2026-7949
Overview
Background
- Strike data / glyph cache
- serialized font “strike” (rendered-glyph) data that the GPU/renderer sends to Skia for deserialization via
SkChromeRemoteGlyphCache. - Volatile shared memory
- a buffer backed by memory another (less-trusted) process can mutate at any time, so its contents can change between a validation read and a later use.
- TOCTOU (time-of-check to time-of-use)
- a class of bug where data validated at one instant is used later, after an attacker has changed it in between.
- `SkReadBuffer`
- Skia’s deserialization reader that parses structured data (lengths, offsets, objects) out of a raw byte buffer.
Root Cause Analysis
Per the hunk, the deserialization constructed an SkReadBuffer buffer{const_cast<const void*>(memory), memorySize} directly over memory, which the commit message states is volatile shared memory and untrusted. The violated invariant is that deserialization must operate on a stable snapshot: because SkReadBuffer read length/offset fields and then consumed bytes from the same live buffer, a concurrent writer could mutate the buffer after a size/field was validated but before it was used, defeating internal bounds checks and causing an out-of-bounds read during parsing.
The fix makes a private, non-volatile copy — sk_sp<SkData> safeMemory = SkData::MakeWithCopy(const_cast<const void*>(memory), memorySize) — and points the reader at the copy (SkReadBuffer buffer{safeMemory->data(), safeMemory->size()}), so every check and use sees identical, attacker-immutable bytes, closing the TOCTOU window. The enclosing function name is not shown in the @@ -632,9 +632,12 @@ hunk header; the commit subject indicates it is the strike-deserialization routine (readStrikeData) (inferred).
SkData::MakeWithCopy) that cannot change underneath the reader.Attack Path
- Submit strike data
A compromised renderer (or other producer) supplies serialized strike data in shared memory to the strike-deserialization path that builds an
SkReadBufferover it. - Race the buffer during parse
While
SkReadBufferreads a length/offset, validates it, and then consumes bytes, the attacker concurrently mutates the still-shared buffer so the validated value no longer matches the data actually read. - Out-of-bounds read The parser uses a now-stale, larger-than-valid length/offset to read past the intended bounds of the buffer during deserialization, the out-of-bounds read the advisory describes.
Impact Assessment
Files Changed
src/text/gpu/SkChromeRemoteGlyphCache.cpp
Audit Directions
- `SkReadBuffer`/reader over shared memoryAudit all deserialization that constructs a reader directly over caller-owned or shared-memory buffers; require a defensive copy before parsing untrusted, mutable input.
- Volatile-input TOCTOULook for any validate-then-use sequence on memory a less-trusted process can write concurrently, and ensure a stable snapshot is taken first (cf. the cited
cc/paint/paint_op_reader.ccpattern). - Cross-process serialization boundariesReview other GPU/renderer-to-Skia serialization entry points for the same assumption that input bytes are immutable during parsing.
Patch
From f04d09d1075926a5e5ef0a52171c9c12043fab03 Mon Sep 17 00:00:00 2001
From: Florin Malita <fmalita@google.com>
Date: Fri, 27 Mar 2026 12:07:04 -0400
Subject: [PATCH] Use a local data copy for strike deserialization
The readStrikeData() input is volatile (shared memory) and untrusted.
To avoid time-of-check to time-of-use issues during deserialization,
always make a copy when transitioning to internal/non-volatile APIs.
This is similar to the other defensive copies used in Chromium's
cc/paint_op deserialization, e.g. [1].
[1] https://source.chromium.org/chromium/chromium/src/+/main:cc/paint/paint_op_reader.cc;drc=9c91b2494d4bf0a2d33b5985f7d1af79e72146f2;l=329
Bug: 496206134
Change-Id: I775d24b10ee7348b159016171ce044737f5bcbe0
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1197136
Commit-Queue: Florin Malita <fmalita@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
---
diff --git a/src/text/gpu/SkChromeRemoteGlyphCache.cpp b/src/text/gpu/SkChromeRemoteGlyphCache.cpp
index b0a37db..caf2869 100644
--- a/src/text/gpu/SkChromeRemoteGlyphCache.cpp
+++ b/src/text/gpu/SkChromeRemoteGlyphCache.cpp
@@ -632,9 +632,12 @@
SkASSERT(memorySize != 0);
SkASSERT(memory != nullptr);
+ // Use a local copy to defend against volatile memory TOCTOU issues during deserialization.
+ sk_sp<SkData> safeMemory = SkData::MakeWithCopy(const_cast<const void*>(memory), memorySize);
+
// We do not need to set any SkDeserialProcs here because SkStrikeServerImpl::writeStrikeData
// did not encode any SkImages.
- SkReadBuffer buffer{const_cast<const void*>(memory), memorySize};
+ SkReadBuffer buffer{safeMemory->data(), safeMemory->size()};
// Limit the kinds of effects that appear in a glyph's drawable (crbug.com/1442140):
buffer.setAllowSkSL(false);
Original Bug Report
TOCTOU in Skia SkPathData::Make leads to Heap OOB Read in GPU process
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A Time-of-Check Time-of-Use (TOCTOU) vulnerability exists in Skia’s path deserialization when reading font data from shared memory in the GPU process. A compromised renderer can mutate path verbs between validation and copying, creating a malformed SkPath. Iterating this path during rasterization causes a heap out-of-bounds read, potentially leaking sensitive cross-origin data.
Affected files:
third_party/skia/src/core/SkPathData.cppthird_party/skia/src/core/SkPath_serial.cppthird_party/skia/src/core/SkReadBuffer.cppthird_party/skia/src/text/gpu/SkChromeRemoteGlyphCache.cppgpu/command_buffer/service/raster_decoder.cc
Estimated timestamp from git blame: 2025-12-23
Description
There is a potential Time-of-Check Time-of-Use (TOCTOU) vulnerability in Skia’s path deserialization logic when handling remote glyph cache data in the GPU process. This can lead to a heap out-of-bounds (OOB) read, allowing a compromised renderer to leak cross-origin GPU memory.
When a renderer sends font data to the GPU process via RasterDecoderImpl::DoRasterCHROMIUM, the data is passed via a shared memory buffer. The buffer is passed down as a volatile span to ServiceFontManager::Deserialize. However, Skia’s SkStrikeClient::readStrikeData explicitly casts away the volatile qualifier and wraps the shared memory directly in an SkReadBuffer:
// third_party/skia/src/text/gpu/SkChromeRemoteGlyphCache.cpp
SkReadBuffer buffer{const_cast<const void*>(memory), memorySize};
When deserializing an SkPath with the kVerbsAreStoredForward_Version format, SkPath::ReadFromMemory skips allocating temporary storage and instead obtains direct pointers into this shared memory for the points and verbs arrays.
These shared memory spans are passed to SkPathData::Make:
// third_party/skia/src/core/SkPathData.cpp
sk_sp<SkPathData> SkPathData::Make(SkSpan<const SkPoint> pts, SkSpan<const SkPathVerb> vbs, ...) {
if (!valid_path_data(pts, vbs, conics)) { // [1] Time of Check
return nullptr;
}
return MakeNoCheck(pts, vbs, conics, {}, {}); // [2] Time of Use
}
- Time of Check:
valid_path_dataiterates over thevbsspan (reading directly from shared memory) to calculate the expected number of points, verifying it matches the size of theptsarray. - Time of Use: If validation passes,
MakeNoCheckallocates a newSkPathDataobject sized according to the original array lengths. It then callsSkSpanPriv::Copyto copy the verbs and points from the shared memory into the new allocation.
A compromised renderer can continuously mutate the verbs in shared memory. If the renderer changes a verb from kLine (requires 1 point) to kCubic (requires 3 points) after valid_path_data reads it but before MakeNoCheck copies it, the resulting SkPathData object will contain a kCubic verb but only have memory allocated for a kLine.
When the GPU process later iterates over this path (e.g., via SkPathEdgeIter or SkPath::RawIter) to draw it, the iterator will read past the bounds of the allocated fPoints array, treating adjacent heap memory as float coordinates. This geometry can then be read back via a canvas to leak sensitive data.
Suggested Steps to Trigger
(Note: These are potential steps as our setup does not have the ability to run code or verify a live exploit)
- A compromised renderer process allocates a shared memory buffer for font glyph serialization.
- The renderer crafts a serialized
SkPathin this buffer usingkVerbsAreStoredForward_Version, populating it with $N$ points and $N$ verbs (all set tokLine). - The renderer issues a
RasterDecoderImpl::DoRasterCHROMIUMcommand to the GPU process, pointing to this shared memory. - While the GPU process begins deserialization, a racing thread in the renderer continuously overwrites the
kLine(0x01) verbs in the shared memory withkCubic(0x04) verbs. - If the race is won, the GPU process allocates an
SkPathDataobject with space for $N$ points but copies in $N$kCubicverbs. - The renderer requests the GPU process to draw the glyph and reads back the resulting pixels. The out-of-bounds heap memory affects the drawn geometry, allowing the attacker to infer the leaked memory contents.
Suggested Fix
To prevent TOCTOU vulnerabilities, untrusted data from shared memory should be copied into secure, local memory before any validation occurs.
In SkPath::ReadFromMemory (or SkPathData::Make), the verbs and points arrays should be copied out of the SkReadBuffer (shared memory) into local buffers before calling valid_path_data. Alternatively, MakeNoCheck could be modified to perform the validation after copying the data, or a new Make variant could be introduced that safely copies and validates in a single pass.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.