CVE-2026-9909
Overview
Background
- Skia text/GPU SubRun
- Skia stores GPU glyph runs (“SubRuns”) in arena-allocated memory managed by
BagOfBytes/SubRunAllocator;GlyphVectorholds the per-glyphSkPackedGlyphIDdata. - `BagOfBytes::allocateBytesFor<Size,Alignment>(int n)`
- Arena allocator that computes
n * Sizeas the byte count; the size is carried in a signedint, so a largencan overflow. - `GlyphBytes`
- The per-glyph element type whose
sizeof(8 on 64-bit) is the multiplier in the allocation; this differs from thesizeof(uint32_t)used in the old validation. - `BagOfBytes::WillCountFit<T>(count)`
- The new helper the patch uses to verify that
countelements of typeTcan be allocated without overflowing the arena’s size math.
Root Cause Analysis
Before the patch, GlyphVector::MakeFromBuffer guarded glyphCount only against overflow of a different multiplication: static constexpr int kMaxCount = (int)(INT_MAX / sizeof(uint32_t)); if (!buffer.validate(glyphCount <= kMaxCount)) .... This bounds glyphCount * sizeof(uint32_t), i.e. the size of the serialized packedGlyphID stream, but it does not bound the subsequent arena allocation, which is sized by sizeof(GlyphBytes) (8 bytes), not sizeof(uint32_t) (4). A glyphCount that passes glyphCount <= INT_MAX/4 can still make glyphCount * sizeof(GlyphBytes) overflow the int size variable inside allocateBytesFor, wrapping to a negative value that slips past the SkASSERT_RELEASE(size <= kMaxByteSize) capacity check and returns an undersized arena chunk; the deserialization loop then writes linearly far past it.
The patch closes exactly this element-size mismatch. It replaces the kMaxCount/sizeof(uint32_t) check with if (!buffer.validate(BagOfBytes::WillCountFit<GlyphBytes>(glyphCount))), validating glyphCount against the allocation element type GlyphBytes so the arena multiplication cannot overflow. It keeps the separate “enough serialized bytes” check glyphCount * sizeof(uint32_t) <= buffer.available() and adds static_assert(sizeof(GlyphBytes) >= sizeof(uint32_t)) to document that the byte-stream bound is weaker than the allocation bound. Note the mapped diff touches only GlyphVector.cpp (the validation site); the advisory’s cited sink in SubRunAllocator.h/allocateBytes and the WillCountFit definition are not shown in this diff — the fix is landing the guard at the deserialization entry point. This is consistent with the advisory’s IntegerOverflow label.
glyphCount against the 4-byte serialized stride but the arena allocated with the 8-byte GlyphBytes stride; the larger multiplier could overflow the signed int size inside the allocator, so the count had to be validated against the allocation element type, which is what WillCountFit<GlyphBytes> now does.Attack Path
- Compromise renderer Attacker gains code execution in a renderer process (inferred; needed to craft raw SKP payloads and Mojo messages).
- Craft SKP
Build an
SkPicturewith a glyph-run op carrying a very largeglyphCount(e.g.0x10000000) sized soglyphCount * sizeof(GlyphBytes)overflowsintbutglyphCount * sizeof(uint32_t)stays withinbuffer.available()(inferred from the removed checks). - Deliver
Send the picture to the Paint Preview compositor utility process, whose deserialization path reaches
GlyphVector::MakeFromBuffer. - Overflow the count
The old
glyphCount <= INT_MAX/sizeof(uint32_t)check passes; the arena allocation forGlyphBytesoverflows, returning an undersized chunk. - Heap write
The glyph-populating loop writes
glyphCountentries linearly past the undersized allocation — a large out-of-bounds heap write (inferred loop, per advisory; not in this diff). - Corruption → potential RCE Adjacent heap objects are overwritten before an eventual guard-page crash; concurrent use of corrupted objects could yield code execution in the utility process (inferred/escalation).
Impact Assessment
glyphCount into the arena allocator.Files Changed
src/text/gpu/GlyphVector.cpp
Audit Directions
- Element-size mismatchesAudit every
*::MakeFromBuffer/deserializer that validates a count againstsizeof(uint32_t)(serialized stride) but allocates with a larger runtime element type. - Signed `int` size math in `BagOfBytes`Review
allocateBytesFor/allocateBytesfor remaining places wheren * Sizeis stored inintwithoutWillCountFit/checked arithmetic. - Paint Preview vs OOP-R size limitsCheck other large-payload Skia paths reachable through Paint Preview (which permits gigabyte payloads) that bypass OOP-R’s 16 MB PaintOp cap.
Patch
From 8ac48d9b53de0287c171093d751e8db180c641cf Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Mon, 27 Apr 2026 19:58:02 +0000
Subject: [PATCH] Identify overflow in SkGlyph allocation earlier
Follow-up to https://review.skia.org/1209996
Bug: b/499152771
Change-Id: I487f64d92aa7177a84c1754e275852373a3cc40e
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1219717
Commit-Queue: Kaylee Lubick <kjlubick@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
---
diff --git a/src/text/gpu/GlyphVector.cpp b/src/text/gpu/GlyphVector.cpp
index d1a41e0..7040436 100644
--- a/src/text/gpu/GlyphVector.cpp
+++ b/src/text/gpu/GlyphVector.cpp
@@ -73,14 +73,14 @@
return std::nullopt;
}
- // Make sure we can multiply without overflow in the check below.
- static constexpr int kMaxCount = (int)(INT_MAX / sizeof(uint32_t));
- if (!buffer.validate(glyphCount <= kMaxCount)) {
+ // Make sure we won't overflow the glyphCount math or allocation below.
+ if (!buffer.validate(BagOfBytes::WillCountFit<GlyphBytes>(glyphCount))) {
return std::nullopt;
}
// Check for enough bytes to populate the packedGlyphID array. If not enough something has
// gone wrong.
+ static_assert(sizeof(GlyphBytes) >= sizeof(uint32_t));
if (!buffer.validate(glyphCount * sizeof(uint32_t) <= buffer.available())) {
return std::nullopt;
}
Original Bug Report
Integer Overflow in Skia SubRunAllocator Leads to Heap OOB Write
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 without the security team.
Overview: An integer overflow in Skia’s SubRunAllocator during SKP deserialization can result in a negative allocation size, bypassing capacity checks. This returns an undersized arena pointer, allowing an attacker to perform a massive linear heap out-of-bounds write. While PartitionAlloc guard pages ensure an eventual crash, a concurrent thread could access corrupted data in the 2 MB window before the crash, potentially leading to RCE in the Paint Preview utility process.
Affected files:
third_party/skia/src/text/gpu/SubRunAllocator.hthird_party/skia/src/text/gpu/GlyphVector.cppthird_party/skia/src/text/gpu/SubRunContainer.cppcomponents/paint_preview/common/serial_utils.cc
Estimated timestamp from git blame: 2026-02-24
Summary
There is a potential integer overflow vulnerability in Skia’s SubRunAllocator within BagOfBytes::allocateBytesFor. When deserializing SkPicture (SKP) payloads in the PaintPreviewCompositor utility process, an attacker can supply a large glyphCount that causes an allocation size calculation to overflow a 32-bit signed integer to a negative value. This bypasses internal capacity checks and returns an undersized memory chunk. The subsequent initialization loop performs a massive out-of-bounds (OOB) heap write.
While PartitionAlloc’s 2 MB Super Page guard pages make a crash deterministic, an attacker can overwrite up to 2 MB of adjacent heap objects prior to the crash, which may be exploitable via a race condition.
Root Cause Analysis
The vulnerability lies in third_party/skia/src/text/gpu/SubRunAllocator.h:
template <size_t Size, size_t Alignment>
char* allocateBytesFor(int n = 1)
requires (Alignment <= kMaxAlignment && Size < kMaxByteSize && Size % Alignment == 0) {
int size = n ? n * Size : 1;
SkASSERT_RELEASE(size <= kMaxByteSize);
return this->allocateBytes(size, Alignment);
}
When n is large (e.g., 0x10000000 or 268,435,456) and Size is 8 (the size of GlyphBytes on 64-bit systems), the multiplication n * Size is performed as a 64-bit size_t due to C++ promotion rules, resulting in 0x0000000080000000.
This value is then assigned to the 32-bit signed integer size, where it overflows and is interpreted as INT_MIN (-2,147,483,648).
- The check
SkASSERT_RELEASE(size <= kMaxByteSize)passes because a negative number is mathematically less than the positive limit. - The method calls
allocateBytes(-2147483648, 8). - Inside
allocateBytes, the conditionif (fCapacity < size)(e.g.,4096 < -2147483648) evaluates to false, bypassing the need to allocate more memory. - The method updates the capacity (
fCapacity -= size), causing a massive positive overflow, and returns a pointer to the current, undersized arena block.
Potential Attack Path
These are suggested steps based on static analysis, as our tooling cannot currently run code to provide a working Proof of Concept:
- Compromise Renderer: An attacker compromises a renderer process.
- Craft Payload: The attacker crafts a malicious SKP containing a
DrawSlugOpwith aglyphCountof0x10000000. The SKP is padded to exactly 1 GB to satisfy availability checks. - Deliver Payload: The SKP is sent via Mojo using
mojo_base.mojom.BigBufferto thePaintPreviewCompositorutility process. (Note: This is unreachable via OOP-R because OOP-R restricts serializedPaintOps to 16 MB, but Paint Preview allows gigabyte-scale payloads). - Trigger Overflow:
GlyphVector::MakeFromBuffer(third_party/skia/src/text/gpu/GlyphVector.cpp:61) reads theglyphCount. The value passes theglyphCount <= INT_MAX / sizeof(uint32_t)check and thebuffer.available()check. It then requests an allocation for the glyphs, triggering the integer overflow inSubRunAllocatoras described above. - Heap Corruption:
GlyphVectorenters its deserialization loop:This loop writes 4 bytes and skips 4 bytes over the course offor (int i = 0; i < glyphCount; i++) { *reinterpret_cast<SkPackedGlyphID*>(glyphs[i].data()) = SkPackedGlyphID(buffer.readUInt()); }0x10000000iterations, writing attacker-controlled 20-bit values linearly across the heap. - Exploitation: The linear write traverses PartitionAlloc. While it will inevitably hit a
PROT_NONEguard page at a 2 MB Super Page boundary and crash, it will overwrite up to 2 MB of adjacent heap objects in the current Super Page before doing so. If the attacker grooms the heap and triggers a concurrent thread to interact with the corrupted objects (e.g., vtable pointers) before the crash, they may achieve Remote Code Execution (RCE) in the sandboxed utility process.
Suggested Fix
Refactor allocateBytesFor in SubRunAllocator.h to use Skia’s safe math utilities (or base::CheckedNumeric if permitted in this context) to prevent integer overflows during size calculations. Alternatively, explicitly cast the size to size_t and validate it against kMaxByteSize before converting it to an int for the arena logic:
size_t safe_size = (size_t)n * Size;
SkASSERT_RELEASE(safe_size <= kMaxByteSize);
int size = static_cast<int>(safe_size);
Furthermore, adding strict bounds checking on fCapacity and size in BagOfBytes::allocateBytes to reject negative sizes would serve as an effective defense-in-depth measure.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.