Medium watch CVSS 6.5 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Skia
DescriptionOut of bounds read in Skia
ComponentSkia
Sub-componentSkp
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker502103414
Fix commit3d08a21145b0 (skia) +13/-0
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle Threat Intelligence
Reported2026-04-13 (per vendor advisory)
Fix landed2026-04-13 (public git)
Disclosed2026-04-15
Patch gap2 days — fix public in source before the advisory

Background

`SkCanvas::Lattice`
a nine-patch-style description used by drawImageLattice, consisting of fXDivs/fYDivs arrays (sized fXCount/fYCount) plus optional per-cell fRectTypes and fColors arrays.
`SkReadBuffer`
Skia’s deserializer for recorded/serialized drawing data; readInt() pulls an attacker-controllable 32-bit value and skipT<T>(n) returns a pointer into the buffer while advancing it by n elements of T.
Flag/color array sizing invariant
when per-cell rect types and colors are present, the serializer writes exactly (fXCount + 1) * (fYCount + 1) entries, so flagCount must equal that product on read-back.
`skipT<T>(count)`
reserves count elements without validating count against the remaining buffer in a way that is safe for negative or inconsistent values supplied by the caller’s own logic.

Root Cause Analysis

SkCanvasPriv::ReadLattice read fXCount, fYCount, and flagCount straight from the untrusted SkReadBuffer via readInt() and used them to carve arrays (skipT<int32_t>, skipT<SkCanvas::Lattice::RectType>, skipT<SkColor>) with no bounds or consistency checks. The serialization invariant is that flagCount equals (fXCount + 1) * (fYCount + 1) whenever rect types and colors exist, but the reader never re-verified it, so a malformed stream could declare counts that disagree with the actual number of serialized fRectTypes/fColors entries, or supply negative counts. A downstream consumer of the Lattice iterates (fXCount + 1) * (fYCount + 1) cells over fRectTypes/fColors, so an under-sized flagCount (or a negative fXCount/fYCount) leads to reads past the deserialized arrays — an out-of-bounds read.

The fix tightens the reader by rejecting negative fXCount and fYCount early (returning false before the corresponding skipT), and, when flagCount is non-zero, rejecting it unless flagCount >= 0 and (uint64_t)flagCount == ((uint64_t)fXCount + 1) * ((uint64_t)fYCount + 1), with the product computed in 64-bit to avoid overflow. Each failed check returns false before the offending skipT, so no array is ever carved from inconsistent counts.

Key insight
The reader trusted serialized lattice counts without re-establishing the flagCount == (xCount+1)*(yCount+1) invariant or rejecting negatives; the fix re-validates both before any array is sliced, computing the product in 64-bit so a consumer can no longer over-read fRectTypes/fColors.

Attack Path

  1. Craft a serialized lattice Produce a serialized SkPicture/paint-op stream containing a drawImageLattice record with inconsistent lattice metadata: e.g. a flagCount smaller than (fXCount + 1) * (fYCount + 1), or a negative fXCount/fYCount.
  2. Reach `ReadLattice` Get that data deserialized (e.g. picture/paint-op playback), where SkCanvasPriv::ReadLattice consumes the fields; pre-patch it accepts the bad counts and carves fRectTypes/fColors of flagCount entries anyway.
  3. Over-read during consumption The lattice drawing consumer walks (fXCount + 1) * (fYCount + 1) cells, indexing fRectTypes/fColors beyond the flagCount entries actually present, producing an out-of-bounds read.
  4. Information exposure The over-read returns adjacent heap/buffer contents into rendering, leaking memory; the diff does not demonstrate a write primitive or escalation (inferred if asserted).

Impact Assessment

An attacker who can supply serialized Skia drawing data gains an out-of-bounds read inside the process performing deserialization/rasterization (in Chrome, Skia consumes renderer-supplied serialized paint ops in the GPU process). The advisory rates this Medium and describes it solely as an out-of-bounds read in Skia; it reports no in-the-wild exploitation, and any use of the leaked data beyond information disclosure is (inferred), not shown by the patch.

Changed Functions

FunctionChangeNotes
if
src/core/SkCanvasPriv.cpp
modified

Files Changed

  • src/core/SkCanvasPriv.cpp

Audit Directions

  • Deserialized count fields feeding array slicing
    Audit every readInt()/skipT<T>(n) pair in Skia readers for missing non-negative and against-remaining-size checks before the slice is taken.
  • Cross-field invariants lost on read-back
    Where a serializer derives one field from others (here flagCount from xCount/yCount), confirm the reader re-asserts that relationship rather than trusting the stored value.
  • Integer overflow in size products
    Verify size computations like (a + 1) * (b + 1) are done in a wide unsigned type, since narrow signed multiplication of attacker counts can wrap and bypass a naive equality check.
From 3d08a21145b080f644d6b6a621ae33a452d27a35 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Mon, 13 Apr 2026 14:30:46 -0400
Subject: [PATCH] [skp] Validate image lattice counts

Bug: b/502103414
Change-Id: Id13b2dfd30830c238d19914f3231f76383cee80b
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1208956
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Auto-Submit: Michael Ludwig <michaelludwig@google.com>
---

diff --git a/src/core/SkCanvasPriv.cpp b/src/core/SkCanvasPriv.cpp
index 5eaa7dd..bfd4d4e 100644
--- a/src/core/SkCanvasPriv.cpp
+++ b/src/core/SkCanvasPriv.cpp
@@ -25,6 +25,7 @@
 #include "src/core/SkWriteBuffer.h"
 #include "src/core/SkWriter32.h"
 
+#include <limits>
 #include <utility>
 #include <cstdint>
 
@@ -55,13 +56,25 @@
 
 bool SkCanvasPriv::ReadLattice(SkReadBuffer& buffer, SkCanvas::Lattice* lattice) {
     lattice->fXCount = buffer.readInt();
+    if (lattice->fXCount < 0) {
+        return false;
+    }
     lattice->fXDivs = buffer.skipT<int32_t>(lattice->fXCount);
     lattice->fYCount = buffer.readInt();
+    if (lattice->fYCount < 0) {
+        return false;
+    }
     lattice->fYDivs = buffer.skipT<int32_t>(lattice->fYCount);
     int flagCount = buffer.readInt();
     lattice->fRectTypes = nullptr;
     lattice->fColors = nullptr;
+    // flagCount was serialized as (xCount + 1) * (yCount + 1) if there were rect types and colors,
+    // so if that isn't still the case the buffer is invalid.
     if (flagCount) {
+        if (flagCount < 0 || (uint64_t) flagCount != ((uint64_t) lattice->fXCount + 1) *
+                                                     ((uint64_t) lattice->fYCount + 1)) {
+            return false;
+        }
         lattice->fRectTypes = buffer.skipT<SkCanvas::Lattice::RectType>(flagCount);
         lattice->fColors = buffer.skipT<SkColor>(flagCount);
     }
Loading diff…

Original Bug Report

reported by bs...@google.com

Skia heap oob read in SkRecordCanvas::onDrawImageLattice2

VULNERABILITY DETAILS

There is a heap oob read in SkRecordCanvas::onDrawImageLattice2 during the parsing of SKP files containing DRAW_IMAGE_LATTICE or DRAW_IMAGE_LATTICE2 operations. This vulnerability can be triggered via a malformed SKP file.

The issue stems from a mismatch in how SkCanvasPriv::ReadLattice validates the lattice data versus how SkRecordCanvas::onDrawImageLattice2 copies it.

When parsing a lattice operation, SkCanvasPriv::ReadLattice reads flagCount from the buffer and skips that many elements to set lattice->fRectTypes and lattice->fColors:

// In src/core/SkCanvasPriv.cpp
bool SkCanvasPriv::ReadLattice(SkReadBuffer& buffer, SkCanvas::Lattice* lattice) {
    lattice->fXCount = buffer.readInt();
    lattice->fXDivs = buffer.skipT<int32_t>(lattice->fXCount);
    lattice->fYCount = buffer.readInt();
    lattice->fYDivs = buffer.skipT<int32_t>(lattice->fYCount);
    int flagCount = buffer.readInt();
    lattice->fRectTypes = nullptr;
    lattice->fColors = nullptr;
    if (flagCount) {
        lattice->fRectTypes = buffer.skipT<SkCanvas::Lattice::RectType>(flagCount);
        lattice->fColors = buffer.skipT<SkColor>(flagCount);
    }
    lattice->fBounds = buffer.skipT<SkIRect>();
    return buffer.isValid();
}

However, it fails to validate that flagCount is equal to (lattice->fXCount + 1) * (lattice->fYCount + 1). An attacker can provide large values for fXCount and fYCount but a small value for flagCount (e.g., 1).

Next, in SkRecordCanvas::onDrawImageLattice2, the code assumes that the number of elements in fRectTypes and fColors is strictly defined by the grid dimensions:

// In src/core/SkRecordCanvas.cpp
void SkRecordCanvas::onDrawImageLattice2(...) {
    int flagCount = lattice.fRectTypes ? (lattice.fXCount + 1) * (lattice.fYCount + 1) : 0;
    SkASSERT(lattice.fBounds);
    this->append<SkRecords::DrawImageLattice>(this->copy(paint),
                                              sk_ref_sp(image),
                                              lattice.fXCount,
                                              this->copy(lattice.fXDivs, lattice.fXCount),
                                              lattice.fYCount,
                                              this->copy(lattice.fYDivs, lattice.fYCount),
                                              flagCount,
                                              this->copy(lattice.fRectTypes, flagCount),
                                              this->copy(lattice.fColors, flagCount),
                                              *lattice.fBounds, ...);

flagCount is recalculated here as (lattice.fXCount + 1) * (lattice.fYCount + 1). The this->copy() method then reads flagCount elements from lattice.fColors (which only has 1 element allocated in the SkReadBuffer, so an oob read is triggered).

VERSION

Skia Version: 0cb35533022b3912904831c2290680bb8eea0363

REPRODUCTION CASE

  • Check out Skia at commit 0cb35533022b3912904831c2290680bb8eea0363
  • Build ASan version of skpbench according to instructions
  • Generate a proof of concept skp file:
> python3 gen_poc.py poc.skp
  • Feed the skp file to skpbench and observe an ASan crash:
> ASAN_SYMBOLIZER_PATH=$HOME/clang/bin/llvm-symbolizer ASAN_OPTIONS=symbolize=1 ./out/asan/skpbench --src poc.skp --config gles
   accum    median       max       min   stddev  samples  sample_ms  clock  metric  config    bench
=================================================================
==39469==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51300000116c at pc 0x55cd9fb64c1e bp 0x7fffb906a0e0 sp 0x7fffb906a0d8
READ of size 1 at 0x51300000116c thread T0
error: failed to decompress '.debug_aranges', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
error: failed to decompress '.debug_info', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
error: failed to decompress '.debug_abbrev', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
error: failed to decompress '.debug_line', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
error: failed to decompress '.debug_str', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
error: failed to decompress '.debug_line_str', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
error: failed to decompress '.debug_loclists', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
error: failed to decompress '.debug_rnglists', LLVM was not built with LLVM_ENABLE_ZLIB or did not find zlib at build time
    #0 0x55cd9fb64c1d in SkCanvas::Lattice::RectType* SkRecordCanvas::copy<SkCanvas::Lattice::RectType>(SkCanvas::Lattice::RectType const*, unsigned long) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkRecordCanvas.cpp:144:25
    #1 0x55cd9fb5873a in SkRecordCanvas::onDrawImageLattice2(SkImage const*, SkCanvas::Lattice const&, SkRect const&, SkFilterMode, SkPaint const*) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkRecordCanvas.cpp:254:53
    #2 0x55cd9ebaf3fe in SkCanvas::drawImageLattice(SkImage const*, SkCanvas::Lattice const&, SkRect const&, SkFilterMode, SkPaint const*) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkCanvas.cpp:1829:15
    #3 0x55cd9f60c574 in SkPicturePlayback::handleOp(SkReadBuffer*, DrawType, unsigned int, SkCanvas*, SkM44 const&) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPicturePlayback.cpp:471:21
    #4 0x55cd9f60553e in SkPicturePlayback::draw(SkCanvas*, SkPicture::AbortCallback*, SkReadBuffer*) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPicturePlayback.cpp:96:15
    #5 0x55cd9f5cd43e in SkPicture::Forwardport(SkPictInfo const&, SkPictureData const*, SkReadBuffer*) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPicture.cpp:141:14
    #6 0x55cd9f5cda21 in SkPicture::MakeFromStreamPriv(SkStream*, SkDeserialProcs const*, SkTypefacePlayback*, int) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPicture.cpp:190:20
    #7 0x55cd9f5cd539 in SkPicture::MakeFromStream(SkStream*, SkDeserialProcs const*) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPicture.cpp:148:12
    #8 0x55cd9e5a611f in main /usr/local/google/home/bsevens/repos/skia/out/asan/../../tools/skpbench/skpbench.cpp:561:19
    #9 0x7f1672229f74  (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f74) (BuildId: f13b8a5d5cd39727792736394456f00d222c38c4)
    #10 0x7f167222a026 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a026) (BuildId: f13b8a5d5cd39727792736394456f00d222c38c4)
    #11 0x55cd9e4abf10 in _start (/usr/local/google/home/bsevens/repos/skia/out/asan/skpbench+0x4432f10)

0x51300000116c is located 0 bytes after 364-byte region [0x513000001000,0x51300000116c)
allocated by thread T0 here:
/usr/local/google/home/bsevens/clang/bin/llvm-symbolizer: error: '[stack]': No such file or directory
    #0 0x55cd9e58237d in operator new(unsigned long) /tmp/clang/llvm-project/compiler-rt/lib/asan/asan_new_delete.cpp:86:3
    #1 0x55cd9edf9c00 in SkData::PrivateNewWithCopy(void const*, unsigned long) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkData.cpp:114:21
    #2 0x55cd9edfa0db in SkData::MakeUninitialized(unsigned long) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkData.cpp:147:12
    #3 0x55cd9edfa92a in SkData::MakeFromStream(SkStream*, unsigned long) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkData.cpp:217:24
    #4 0x55cd9f5e10d5 in SkPictureData::parseStreamTag(SkStream*, unsigned int, unsigned int, SkDeserialProcs const&, SkTypefacePlayback*, int) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPictureData.cpp:315:23
    #5 0x55cd9f5e4287 in SkPictureData::parseStream(SkStream*, SkDeserialProcs const&, SkTypefacePlayback*, int) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPictureData.cpp:560:20
    #6 0x55cd9f5e3dd0 in SkPictureData::CreateFromStream(SkStream*, SkPictInfo const&, SkDeserialProcs const&, SkTypefacePlayback*, int) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPictureData.cpp:530:16
    #7 0x55cd9f5cd9ec in SkPicture::MakeFromStreamPriv(SkStream*, SkDeserialProcs const*, SkTypefacePlayback*, int) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPicture.cpp:188:21
    #8 0x55cd9f5cd539 in SkPicture::MakeFromStream(SkStream*, SkDeserialProcs const*) /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkPicture.cpp:148:12
    #9 0x55cd9e5a611f in main /usr/local/google/home/bsevens/repos/skia/out/asan/../../tools/skpbench/skpbench.cpp:561:19
    #10 0x7f1672229f74  (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f74) (BuildId: f13b8a5d5cd39727792736394456f00d222c38c4)
    #11 0x7fffb906db0f  ([stack]+0x20b0f)

SUMMARY: AddressSanitizer: heap-buffer-overflow /usr/local/google/home/bsevens/repos/skia/out/asan/../../src/core/SkRecordCanvas.cpp:144:25 in SkCanvas::Lattice::RectType* SkRecordCanvas::copy<SkCanvas::Lattice::RectType>(SkCanvas::Lattice::RectType const*, unsigned long)
Shadow bytes around the buggy address:
  0x513000000e80: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x513000000f00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x513000000f80: fd fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000001000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000001080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x513000001100: 00 00 00 00 00 00 00 00 00 00 00 00 00[04]fa fa
  0x513000001180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000001200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000001280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000001300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000001380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==39469==ABORTING

CREDIT INFORMATION

Reporter credit: Google Threat Analysis Group

View on issue tracker