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

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Skia
DescriptionHeap buffer overflow in Skia
ComponentSkia
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker417599694
Fix commit40dbd0b8cb41 (chromium/src) +19/-4
CWECWE-122, CWE-787 (Heap buffer overflow, Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedHuinian Yang (@vmth6) of Amber Security Lab, OPPO Mobile Telecommunications Corp. Ltd.
Reported2025-05-14 (per vendor advisory)
Fix landed2026-01-21 (public git)
Disclosed2026-03-10
Patch gap48 days — fix public in source before the advisory

Background

`skia/ext/image_operations.cc`
Chromium’s Skia extension that resamples SkBitmaps to a new size using a ResizeFilter and the BGRAConvolve2D convolver.
`rowBytes()`
the byte stride between rows of an SkBitmap, returned as size_t; the convolver is passed it as static_cast<int>(source.rowBytes()).
`BGRAConvolve2D`
the 2-D convolution routine that reads source rows and writes the destination bitmap; it takes the row stride as an int.

Root Cause Analysis

The resize routine in skia/ext/image_operations.cc checked that the source and destination dimensions were non-zero, that source.addr() was non-null, and that the color type was kN32_SkColorType. The diff does not name the function. It then built a ResizeFilter and passed static_cast<int>(source.rowBytes()) to BGRAConvolve2D. Nothing checked that the stride, or the total source byte size (height * rowBytes), fit in an int. A very large source bitmap could therefore truncate the stride when cast, or overflow int-sized offset arithmetic inside the convolver (inferred, since the diff does not show the convolver’s internals). That would lead to out-of-bounds heap access, which matches the advisory’s “Heap buffer overflow”.

The patch adds two early return SkBitmap() guards before the ResizeFilter is built and before BGRAConvolve2D is called. One rejects rowBytes() > INT_MAX. The other computes height * rowBytes in int64_t and rejects totals above INT_MAX. Per the commit message, these checks are defensive and “speculatively” address an overflow seen in a crash stack trace, so the diff does not show the exact faulting computation. The other changes in the hunk only add braces around existing if statements.

Key insight
The resizer passed size_t stride and total-size values into an int-based convolver without range checks. The fix rejects any source whose stride or height * rowBytes exceeds INT_MAX before the convolver runs.

Attack Path

  1. Large source image a bitmap whose rowBytes() or height() * rowBytes() exceeds INT_MAX (over about 2 GiB of pixel data) reaches the image-resize routine as kN32_SkColorType with valid pixels.
  2. Checks pass the pre-patch code only rejected empty dimensions, null addr() and wrong color types, so it built the ResizeFilter and allocated the destination bitmap.
  3. Truncated stride into convolver BGRAConvolve2D received static_cast<int>(source.rowBytes()) and processed the source with int-sized arithmetic.
  4. Out-of-bounds heap access overflowed offsets would make the convolver read the source outside its buffer or write the destination incorrectly (inferred; the diff shows only the added guards).

Impact Assessment

The advisory reports a heap buffer overflow in Skia. Memory corruption in whichever process performs the resize is the likely result, and any escalation beyond a crash is (inferred). The diff does not show which callers reach this routine, or whether they run in the renderer, GPU or browser process. It also does not show whether ordinary web content can supply a source large enough to pass the INT_MAX threshold, so reachability from a web page is not established. The main precondition is a source bitmap over 2 GiB in stride or total size. The vendor rates it medium severity.

Files Changed

  • skia/ext/image_operations.cc

Audit Directions

  • `size_t`→`int` narrowing at convolver boundaries
    search for static_cast<int>(...rowBytes()) and similar narrowing of strides or sizes passed to SIMD or convolution helpers (skia/ext/convolver*.cc, image decoders, ImageOperations callers), and check each one has an explicit range check first.
  • Total-size products
    audit height * stride and width * bytesPerPixel computations done in 32-bit types across image scaling, decoding and SkBitmap copying paths. Make sure they are either widened to int64_t or checked with base::CheckedNumeric.
  • Consistency of size limits
    compare the maximum bitmap sizes that allocators (tryAllocPixels) accept with the limits that downstream processing code assumes. A buffer the allocator accepts but the convolver cannot address safely is this bug pattern.
From 40dbd0b8cb414e2ebd654421e25fd20263c306ba Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Wed, 21 Jan 2026 13:41:03 -0800
Subject: [PATCH] Add defensive checks for overflow in image resizing

The attached bug has a stacktrace that appears to be the result
of an integer overflow that happened in a full build of chromium,
not just a fuzzer.

This adds some checks that speculatively will protect against
that overflow from getting into the convolver.

Bug: b:417599694
Change-Id: I765317b1a6819c9bc68a73d4b83133ee85cdc036
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7506469
Reviewed-by: Florin Malita <fmalita@chromium.org>
Auto-Submit: Kaylee Lubick <kjlubick@chromium.org>
Commit-Queue: Florin Malita <fmalita@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1572546}
---

diff --git a/skia/ext/image_operations.cc b/skia/ext/image_operations.cc
index b833dbd..53ffb218 100644
--- a/skia/ext/image_operations.cc
+++ b/skia/ext/image_operations.cc
@@ -343,9 +343,10 @@
 
   // If the size of source or destination is 0, i.e. 0x0, 0xN or Nx0, just
   // return empty.
-  if (source.width() < 1 || source.height() < 1 ||
-      dest_width < 1 || dest_height < 1)
+  if (source.width() < 1 || source.height() < 1 || dest_width < 1 ||
+      dest_height < 1) {
     return SkBitmap();
+  }
 
   SkIRect dest = {0, 0, dest_width, dest_height};
   DCHECK(dest.contains(dest_subset))
@@ -356,8 +357,21 @@
   DCHECK((ImageOperations::RESIZE_FIRST_ALGORITHM_METHOD <= method) &&
          (method <= ImageOperations::RESIZE_LAST_ALGORITHM_METHOD));
 
-  if (!source.addr() || source.colorType() != kN32_SkColorType)
+  if (!source.addr() || source.colorType() != kN32_SkColorType) {
     return SkBitmap();
+  }
+
+  // Avoid overflows in the convolver
+  if (source.rowBytes() >
+      static_cast<size_t>(std::numeric_limits<int>::max())) {
+    return SkBitmap();
+  }
+
+  if (static_cast<int64_t>(source.height()) *
+          static_cast<int64_t>(source.rowBytes()) >
+      static_cast<int64_t>(std::numeric_limits<int>::max())) {
+    return SkBitmap();
+  }
 
   ResizeFilter filter(method, source.width(), source.height(),
                       dest_width, dest_height, dest_subset);
@@ -372,8 +386,9 @@
   SkBitmap result;
   result.setInfo(
       source.info().makeWH(dest_subset.width(), dest_subset.height()));
-  if (!result.tryAllocPixels(allocator) || !result.readyToDraw())
+  if (!result.tryAllocPixels(allocator) || !result.readyToDraw()) {
     return SkBitmap();
+  }
 
   BGRAConvolve2D(source_subset, static_cast<int>(source.rowBytes()),
                  !source.isOpaque(), filter.x_filter(), filter.y_filter(),
Loading diff…

Original Bug Report

reported by vm...@gmail.com

OOB read in skia::BGRAConvolve2D

Steps to reproduce the problem

  1. ./image_operations_resize_fuzzer__better ./out_better1crash-a849b610d97bc4bf8be154b2d226a4353b8fd93e

Problem Description

In skia::ImageOperations::Resize of VizCompositorTh process, source_byte_row_stride in skia::BGRAConvolve2D can larger than source_byte_row_stride, causing heap-buffer-overflow in skia::Convolve4RowsHorizontally_SSE2[1] and skia::ConvolveHorizontally_SSE2[2]. ImageOperations::Resize is a regular operation in renderer process, and through calling of viz/service/display/software_renderer.cc:CopyOutputRequest, this can compromise VizCompositorTh process.

void BGRAConvolve2D(const unsigned char* source_data,
                    int source_byte_row_stride,
                    bool source_has_alpha,
                    const ConvolutionFilter1D& filter_x,
                    const ConvolutionFilter1D& filter_y,
                    int output_byte_row_stride,
                    unsigned char* output,
                    bool use_simd_if_possible) {
  ConvolveProcs simd;
  simd.extra_horizontal_reads = 0;
  simd.convolve_vertically = NULL;
  simd.convolve_4rows_horizontally = NULL;
  simd.convolve_horizontally = NULL;
  if (use_simd_if_possible) {
    SetupSIMD(&simd);
  }

//...

  for (int out_y = 0; out_y < num_output_rows; out_y++) {
    filter_values = filter_y.FilterForValue(out_y,
                                            &filter_offset, &filter_length);

    // Generate output rows until we have enough to run the current filter.
    while (next_x_row < filter_offset + filter_length) {
      if (simd.convolve_4rows_horizontally &&
          next_x_row + 3 < last_filter_offset + last_filter_length -
          avoid_simd_rows) {
        const unsigned char* src[4];
        unsigned char* out_row[4];
        for (int i = 0; i < 4; ++i) {
          src[i] = &source_data[(next_x_row + i) * source_byte_row_stride];                  //<--- [1]
          out_row[i] = row_buffer.AdvanceRow();
        }
        simd.convolve_4rows_horizontally(src, filter_x, out_row);
        next_x_row += 4;
      } else {
        // Check if we need to avoid SSE2 for this row.
        if (simd.convolve_horizontally &&
            next_x_row < last_filter_offset + last_filter_length -
            avoid_simd_rows) {
          simd.convolve_horizontally(                                                       //<--- [2]
              &source_data[next_x_row * source_byte_row_stride],
              filter_x, row_buffer.AdvanceRow(), source_has_alpha);
        } else {
─── Breakpoints ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[1] break at 0x00007ffff313874f in ../../skia/ext/convolver.cc:475 for ../../skia/ext/convolver.cc:475 hit 1 time
─── Expressions ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
─── History ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
─── Memory ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
─── Registers ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    rax 0x00007bffe71e7110    rbx 0x00007fffffffcee0       rcx 0x000000000000025d    rdx 0x4000000000000000    rsi 0x003ffff989c3977e
    rdi 0x000055555619ae00    rbp 0x00007fffffffd180       rsp 0x00007fffffffcee0     r8 0x0000010000000000     r9 0x00007fffffffff01
    r10 0x0000000000001f01    r11 0x0000000000000001       r12 0x0000555555624ca0    r13 0x00007fffffffdc50    r14 0x00007c3fe81e08f0
    r15 0x00007bffe72e1a50    rip 0x00007ffff313874f    eflags [ CF PF AF SF IF ]     cs 0x00000033             ss 0x0000002b        
     ds 0x00000000             es 0x00000000                fs 0x00000000             gs 0x00000000        
─── Source ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Cannot display "convolver.cc"
─── Stack ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[0] from 0x00007ffff313874f in skia::BGRAConvolve2D(unsigned char const*, int, bool, skia::ConvolutionFilter1D const&, skia::ConvolutionFilter1D const&, int, unsigned char*, bool)+1727 at ../../skia/ext/convolver.cc:475
[1] from 0x00007ffff316c95b in skia::ImageOperations::Resize(SkPixmap const&, skia::ImageOperations::ResizeMethod, int, int, SkIRect const&, SkBitmap::Allocator*)+2731 at ../../skia/ext/image_operations.cc:378
[2] from 0x000055555570c11d in LLVMFuzzerTestOneInput(uint8_t const*, size_t)+1933 at ../../skia/ext/image_operations_resize_fuzzer__better.cc:101
[3] from 0x000055555578d5db in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long)+1147 at ../../third_party/libFuzzer/src/FuzzerLoop.cpp:619
[4] from 0x000055555573bb0c in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long)+460 ```

# Additional Comments
```patch
diff --git a/skia/ext/convolver.cc b/skia/ext/convolver.cc
index b77fcdb165309..59519fa1ab970 100644
--- a/skia/ext/convolver.cc
+++ b/skia/ext/convolver.cc
@@ -439,6 +439,7 @@ void BGRAConvolve2D(const unsigned char* source_data,
   // Loop over every possible output row, processing just enough horizontal
   // convolutions to run each subsequent vertical convolution.
   SkASSERT(output_byte_row_stride >= filter_x.num_values() * 4);
+  SkASSERT(source_byte_row_stride < output_byte_row_stride);
   int num_output_rows = filter_y.num_values();
Running: ./out_better1crash-a849b610d97bc4bf8be154b2d226a4353b8fd93e
[0514/161817.940809:FATAL:skia/ext/convolver.cc:442] check(source_byte_row_stride < output_byte_row_stride)
#0 0x5628ff98e806 <unknown>
#1 0x7f5e0acbcd70 <unknown>
#2 0x7f5e0ab9dd62 <unknown>

Summary

OOB read in skia::BGRAConvolve2D

Custom Questions

Type of crash:

VizCompositorTh

Crash state:

#6526	REDUCE cov: 37 ft: 28 corp: 19/480b lim: 43 exec/s: 0 rss: 121Mb L: 39/39 MS: 2 ChangeASCIIInt-EraseBytes-
#7319	REDUCE cov: 37 ft: 28 corp: 19/479b lim: 48 exec/s: 0 rss: 121Mb L: 38/38 MS: 3 InsertByte-ChangeByte-EraseBytes-
=================================================================
==3824374==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b787d1071b0 at pc 0x560bb534fd07 bp 0x7ffc8448c700 sp 0x7ffc8448c6f8
READ of size 16 at 0x7b787d1071b0 thread T0
    #0 0x560bb534fd06 in skia::Convolve4RowsHorizontally_SSE2(unsigned char const**, skia::ConvolutionFilter1D const&, unsigned char**) skia/ext/convolver_SSE2.cc:212:7
    #1 0x560bb534b8e6 in skia::BGRAConvolve2D(unsigned char const*, int, bool, skia::ConvolutionFilter1D const&, skia::ConvolutionFilter1D const&, int, unsigned char*, bool) skia/ext/convolver.cc:478:9
    #2 0x560bb5343739 in skia::ImageOperations::Resize(SkPixmap const&, skia::ImageOperations::ResizeMethod, int, int, SkIRect const&, SkBitmap::Allocator*) skia/ext/image_operations.cc:378:3
    #3 0x560bb391933a in LLVMFuzzerTestOneInput skia/ext/image_operations_resize_fuzzer__better.cc:101:7
    #4 0x560bb395866c in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) third_party/libFuzzer/src/FuzzerLoop.cpp:619:13
    #5 0x560bb3957538 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) third_party/libFuzzer/src/FuzzerLoop.cpp:516:7
    #6 0x560bb395ab67 in fuzzer::Fuzzer::MutateAndTestOne() third_party/libFuzzer/src/FuzzerLoop.cpp:765:19
    #7 0x560bb395c9ba in fuzzer::Fuzzer::Loop(std::__Cr::vector<fuzzer::SizedFile, std::__Cr::allocator<fuzzer::SizedFile>>&) third_party/libFuzzer/src/FuzzerLoop.cpp:910:5
    #8 0x560bb39333a4 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) third_party/libFuzzer/src/FuzzerDriver.cpp:915:6
    #9 0x560bb39198b5 in main third_party/libFuzzer/src/FuzzerMain.cpp:20:10
    #10 0x7f387de14082 in __libc_start_main /build/glibc-FcRMwW/glibc-2.31/csu/../csu/libc-start.c:308:16

0x7b787d1071b0 is located 242 bytes after 46-byte region [0x7b787d107090,0x7b787d1070be)
allocated by thread T0 here:
    #0 0x560bb3917a0d in operator new[](unsigned long) /b/s/w/ir/cache/builder/src/third_party/llvm/compiler-rt/lib/asan/asan_new_delete.cpp:89:3
    #1 0x560bb3958418 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) third_party/libFuzzer/src/FuzzerLoop.cpp:601:23
    #2 0x560bb3957538 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) third_party/libFuzzer/src/FuzzerLoop.cpp:516:7
    #3 0x560bb395ab67 in fuzzer::Fuzzer::MutateAndTestOne() third_party/libFuzzer/src/FuzzerLoop.cpp:765:19
    #4 0x560bb395c9ba in fuzzer::Fuzzer::Loop(std::__Cr::vector<fuzzer::SizedFile, std::__Cr::allocator<fuzzer::SizedFile>>&) third_party/libFuzzer/src/FuzzerLoop.cpp:910:5
    #5 0x560bb39333a4 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) third_party/libFuzzer/src/FuzzerDriver.cpp:915:6
    #6 0x560bb39198b5 in main third_party/libFuzzer/src/FuzzerMain.cpp:20:10
    #7 0x7f387de14082 in __libc_start_main /build/glibc-FcRMwW/glibc-2.31/csu/../csu/libc-start.c:308:16

SUMMARY: AddressSanitizer: heap-buffer-overflow skia/ext/convolver_SSE2.cc:212:7 in skia::Convolve4RowsHorizontally_SSE2(unsigned char const**, skia::ConvolutionFilter1D const&, unsigned char**)
Shadow bytes around the buggy address:
  0x7b787d106f00: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fd
  0x7b787d106f80: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
  0x7b787d107000: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fa
  0x7b787d107080: fa fa 00 00 00 00 00 06 fa fa fa fa fa fa fa fa
  0x7b787d107100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x7b787d107180: fa fa fa fa fa fa[fa]fa fa fa fa fa fa fa fa fa
  0x7b787d107200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7b787d107280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7b787d107300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7b787d107380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7b787d107400: 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
==3824374==ABORTING
MS: 1 PersAutoDict- DE: "\034\000\000\000\000\000\000\000"-; base unit: 3acaf8e9917854385c881681874a2b4fa25c52dd
0xa,0xa,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x0,0xa,0x0,0x4,0x0,0x1c,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xb6,0x0,0x0,0x0,0x7a,0x98,0x0,0x0,0xd3,0x41,0x0,0x0,0xff,0xff,0x0,0x34,0x2d,0x30,0x47,0x98,0xf7,0x61,
\012\012GGGGGGG\000\012\000\004\000\034\000\000\000\000\000\000\000\000\000\266\000\000\000z\230\000\000\323A\000\000\377\377\0004-0G\230\367a
artifact_prefix='./out_better1'; Test unit written to ./out_better1crash-a849b610d97bc4bf8be154b2d226a4353b8fd93e
Base64: CgpHR0dHR0dHAAoABAAcAAAAAAAAAAAAtgAAAHqYAADTQQAA//8ANC0wR5j3YQ==

Reporter credit:

/

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A

View on issue tracker