CVE-2026-17949
Overview
Background
- `ClearLevel3D`
- a
GLES2DecoderImplroutine that zero-initializes a 3D texture level by uploading zeros via a pixel-unpack buffer (PBO) and repeatedglTexSubImage3Dcalls, used to guarantee textures never expose stale GPU memory. - PBO (Pixel Buffer Object)
- a GL buffer bound to
GL_PIXEL_UNPACK_BUFFERthat supplies the source data for texture uploads; its data store is allocated withglBufferData(). - `GL_OUT_OF_MEMORY` / `GL_INVALID_OPERATION`
- GL error states; the former can be raised by
glBufferData()when VRAM is exhausted, the latter byglTexSubImage3Dwhen the bound PBO lacks a valid/large-enough data store. - `LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER` / `LOCAL_PEEK_GL_ERROR`
- decoder macros that drain the driver’s pending GL errors into the command-buffer wrapper and then read them back, letting the decoder observe whether a real driver error occurred.
Root Cause Analysis
ClearLevel3D allocated a PBO with glBufferData() and then issued glTexSubImage3D calls to write zeros into the texture level, but it unconditionally returned true (reporting the level as successfully cleared) regardless of whether those GL calls actually succeeded. Under VRAM pressure glBufferData() can fail with GL_OUT_OF_MEMORY, leaving the PBO with no data store, so every subsequent glTexSubImage3D fails with GL_INVALID_OPERATION and writes nothing — yet the decoder still marked the level cleared, leaving the texture backed by uninitialized GPU memory.
The fix drains any pre-existing driver errors before the clear sequence with LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER("glClearLevel3D"), then after the upload calls LOCAL_PEEK_GL_ERROR("glClearLevel3D") and returns false if any error is present, so a failed clear no longer marks the level as cleared. Routing the check through PeekGLError also ensures OnOutOfMemoryError() fires so lose_context_when_out_of_memory_ takes effect. Because the level stays marked uncleared, the texture manager will retry or block sampling rather than expose stale memory.
ClearLevel3D returned true without checking GL errors, so an OOM-induced upload failure silently left uninitialized memory marked as cleared. The fix makes success contingent on an actual post-upload GL_NO_ERROR check.Attack Path
- Create a 3D texture level
A renderer using WebGL2/WebGPU allocates a 3D texture (or otherwise triggers lazy clearing) so that
ClearLevel3Dwill be invoked to initialize the level. - Induce VRAM exhaustion
The attacker drives the GPU process toward memory pressure so that the
glBufferData()allocating the clear PBO fails withGL_OUT_OF_MEMORY(inferred as the triggering condition per the patch comment). - Silent clear failure
With no PBO data store, each
glTexSubImage3DfailsGL_INVALID_OPERATIONand writes no zeros, but pre-patchClearLevel3Dreturnstrue, so the texture manager records the level as cleared. - Read uninitialized memory The renderer then samples/reads back the texture level, obtaining whatever stale GPU memory backs it, an information leak of uninitialized (potentially cross-context) data. (inferred that the read-back is the disclosure sink; the diff shows only the failed clear.)
Impact Assessment
(inferred).Files Changed
gpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
Audit Directions
- Success reported without error checkLook for GL/driver clear/initialize routines that return
trueafter issuing upload/allocation commands without peeking the resulting GL error, especially those feeding a “cleared/initialized” bit. - Allocation-failure-under-pressure pathsAudit code that allocates GPU buffers with
glBufferData()/similar and assumes success, since OOM leaves an unusable buffer that downstream calls silently fail against. - Lazy-clear invariantsReview all
ClearLevel*variants and their callers inTextureManagerto confirm a failed clear never marks a level cleared and never allows sampling of uninitialized storage.
Patch
From 557967a618dceaa36bf5620ff0e4135d9b2959f4 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Mon, 08 Jun 2026 14:57:40 -0700
Subject: [PATCH] gpu: Fix ClearLevel3D uninitialized memory exposure under OOM
glBufferData() may fail with GL_OUT_OF_MEMORY under VRAM pressure,
leaving the PBO without a data store and causing subsequent calls to
glTexSubImage3D to fail GL_INVALID_OPERATION without writing zeros.
Returning true in this case erroneously reports the texture level as
cleared, exposing uninitialized memory.
This CL drains driver errors before the clear sequence and checks for
any errors afterward, returning false if any error occurred so that the
level is not marked as cleared.
Bug: 517000034
Change-Id: I3ec7dc0b667dfec9e4df0d69d7875bf66cc4eedc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7902883
Auto-Submit: Zhenyao Mo <zmo@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1643473}
---
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 7b387bf..f8dddd5 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -12310,6 +12310,10 @@
TRACE_EVENT1("gpu", "GLES2DecoderImpl::ClearLevel3D", "size", size);
+ // Drain any pre-existing driver errors so the post-upload check below only
+ // reflects errors generated by this clear sequence.
+ LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER("glClearLevel3D");
+
{
ScopedPixelUnpackState reset_restore(&state_);
GLuint buffer_id = 0;
@@ -12339,6 +12343,15 @@
texture_manager()->GetTextureInfoForTarget(&state_, texture->target());
api()->glBindTextureFn(texture->target(),
bound_texture ? bound_texture->service_id() : 0);
+
+ // glBufferData() may fail with GL_OUT_OF_MEMORY under VRAM pressure, leaving
+ // the PBO without a data store and causing every glTexSubImage3D above to
+ // fail GL_INVALID_OPERATION without writing zeros. Do not report the level as
+ // cleared in that case; routing the error through PeekGLError also ensures
+ // OnOutOfMemoryError() fires so lose_context_when_out_of_memory_ applies.
+ if (LOCAL_PEEK_GL_ERROR("glClearLevel3D") != GL_NO_ERROR) {
+ return false;
+ }
return true;
}
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
index 32589d7..d828410 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
@@ -803,6 +803,9 @@
base::span<GLsizei> depth,
GLuint bound_pixel_unpack_buffer) {
InSequence seq;
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ALIGNMENT, 1))
.Times(1)
.RetiresOnSaturation();
@@ -852,6 +855,9 @@
.RetiresOnSaturation();
}
EXPECT_CALL(*gl_, BindTexture(target, _)).Times(1).RetiresOnSaturation();
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
}
void GLES2DecoderTestBase::SetupExpectationsForFramebufferClearing(
Regression Test / PoC
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
index 32589d7..d828410 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
@@ -803,6 +803,9 @@
base::span<GLsizei> depth,
GLuint bound_pixel_unpack_buffer) {
InSequence seq;
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ALIGNMENT, 1))
.Times(1)
.RetiresOnSaturation();
@@ -852,6 +855,9 @@
.RetiresOnSaturation();
}
EXPECT_CALL(*gl_, BindTexture(target, _)).Times(1).RetiresOnSaturation();
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
}
void GLES2DecoderTestBase::SetupExpectationsForFramebufferClearing(