High CVSS 7.5 chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Skia
DescriptionInteger overflow in Skia
ComponentSkia
Bug ClassInteger Overflow
Tracker499152771
Fix commit8ac48d9b53de (skia) +3/-3
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-03 (per vendor advisory)
Fix landed2026-04-27 (public git)
Disclosed2026-05-27
Patch gap30 days — fix public in source before the advisory

Background

Skia text/GPU SubRun
Skia stores GPU glyph runs (“SubRuns”) in arena-allocated memory managed by BagOfBytes/SubRunAllocator; GlyphVector holds the per-glyph SkPackedGlyphID data.
`BagOfBytes::allocateBytesFor<Size,Alignment>(int n)`
Arena allocator that computes n * Size as the byte count; the size is carried in a signed int, so a large n can overflow.
`GlyphBytes`
The per-glyph element type whose sizeof (8 on 64-bit) is the multiplier in the allocation; this differs from the sizeof(uint32_t) used in the old validation.
`BagOfBytes::WillCountFit<T>(count)`
The new helper the patch uses to verify that count elements of type T can 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.

Key insight
The pre-check validated 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

  1. Compromise renderer Attacker gains code execution in a renderer process (inferred; needed to craft raw SKP payloads and Mojo messages).
  2. Craft SKP Build an SkPicture with a glyph-run op carrying a very large glyphCount (e.g. 0x10000000) sized so glyphCount * sizeof(GlyphBytes) overflows int but glyphCount * sizeof(uint32_t) stays within buffer.available() (inferred from the removed checks).
  3. Deliver Send the picture to the Paint Preview compositor utility process, whose deserialization path reaches GlyphVector::MakeFromBuffer.
  4. Overflow the count The old glyphCount <= INT_MAX/sizeof(uint32_t) check passes; the arena allocation for GlyphBytes overflows, returning an undersized chunk.
  5. Heap write The glyph-populating loop writes glyphCount entries linearly past the undersized allocation — a large out-of-bounds heap write (inferred loop, per advisory; not in this diff).
  6. 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

The overflow is reached during SKP deserialization in a sandboxed utility process (Paint Preview compositor per the advisory), from a compromised/hostile renderer. The diff substantiates an integer overflow in the allocation-count validation; the memory-safety sink (the undersized allocation and the linear OOB write) is described by the advisory but is not in this diff, so the heap-write primitive and any RCE escalation are (inferred). What the patch provably prevents is passing an overflow-inducing glyphCount into the arena allocator.

Files Changed

  • src/text/gpu/GlyphVector.cpp

Audit Directions

  • Element-size mismatches
    Audit every *::MakeFromBuffer/deserializer that validates a count against sizeof(uint32_t) (serialized stride) but allocates with a larger runtime element type.
  • Signed `int` size math in `BagOfBytes`
    Review allocateBytesFor/allocateBytes for remaining places where n * Size is stored in int without WillCountFit/checked arithmetic.
  • Paint Preview vs OOP-R size limits
    Check other large-payload Skia paths reachable through Paint Preview (which permits gigabyte payloads) that bypass OOP-R’s 16 MB PaintOp cap.
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;
     }
Loading diff…

Original Bug Report

reported by vm...@google.com

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.h
  • third_party/skia/src/text/gpu/GlyphVector.cpp
  • third_party/skia/src/text/gpu/SubRunContainer.cpp
  • components/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).

  1. The check SkASSERT_RELEASE(size <= kMaxByteSize) passes because a negative number is mathematically less than the positive limit.
  2. The method calls allocateBytes(-2147483648, 8).
  3. Inside allocateBytes, the condition if (fCapacity < size) (e.g., 4096 < -2147483648) evaluates to false, bypassing the need to allocate more memory.
  4. 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:

  1. Compromise Renderer: An attacker compromises a renderer process.
  2. Craft Payload: The attacker crafts a malicious SKP containing a DrawSlugOp with a glyphCount of 0x10000000. The SKP is padded to exactly 1 GB to satisfy availability checks.
  3. Deliver Payload: The SKP is sent via Mojo using mojo_base.mojom.BigBuffer to the PaintPreviewCompositor utility process. (Note: This is unreachable via OOP-R because OOP-R restricts serialized PaintOps to 16 MB, but Paint Preview allows gigabyte-scale payloads).
  4. Trigger Overflow: GlyphVector::MakeFromBuffer (third_party/skia/src/text/gpu/GlyphVector.cpp:61) reads the glyphCount. The value passes the glyphCount <= INT_MAX / sizeof(uint32_t) check and the buffer.available() check. It then requests an allocation for the glyphs, triggering the integer overflow in SubRunAllocator as described above.
  5. Heap Corruption: GlyphVector enters its deserialization loop:
    for (int i = 0; i < glyphCount; i++) {
        *reinterpret_cast<SkPackedGlyphID*>(glyphs[i].data()) = SkPackedGlyphID(buffer.readUInt());
    }
    
    This loop writes 4 bytes and skips 4 bytes over the course of 0x10000000 iterations, writing attacker-controlled 20-bit values linearly across the heap.
  6. Exploitation: The linear write traverses PartitionAlloc. While it will inevitably hit a PROT_NONE guard 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.

View on issue tracker