CVE-2026-78977
Overview
Background
- `ValidateAndDoTexSubImage`
- the GPU service (command-buffer) routine that validates and performs a
glTexSubImage/glTexImageupload into a texture level on behalf of a GLES client. - Cleared/`SafeToRenderFrom` state
- per-level tracking (via
SetLevelCleared/SetLevelClearedRectandTexture::SafeToRenderFrom) recording that a texture level’s pixels are fully defined, so it may be sampled/rendered without leaking uninitialized memory. - `ERRORSTATE_COPY_REAL_GL_ERRORS_TO_WRAPPER` / `ERRORSTATE_PEEK_GL_ERROR`
- macros that drain any pre-existing driver errors and then read back whether the just-issued GL call produced an error.
Root Cause Analysis
ValidateAndDoTexSubImage marked the destination level cleared before it issued the actual driver glTex(Sub)Image{2,3}D call — calling SetLevelCleared(...) (full image) or SetLevelClearedRect(...) (partial) — and never checked whether the upload succeeded. If the driver rejected the upload (e.g. returned GL_OUT_OF_MEMORY), no pixels were written, yet the level was already recorded as cleared, so Texture::SafeToRenderFrom() returned true for a level whose GPU memory was never initialized. Subsequent sampling or rendering then read uninitialized driver memory.
The fix defers committing the cleared state: it records intent in set_cleared/set_cleared_rect/cleared_rect_to_set, snapshots existing GL errors with ERRORSTATE_COPY_REAL_GL_ERRORS_TO_WRAPPER, performs the upload, and only calls SetLevelCleared/SetLevelClearedRect afterward when ERRORSTATE_PEEK_GL_ERROR(...) == GL_NO_ERROR. The return statements in the workaround branches were also converted to a uploaded flag so the post-upload error check always runs.
GL_NO_ERROR.Attack Path
- Create an uncleared texture level
Attacker-controlled GPU content allocates a texture level (e.g.
glTexImage2Dwith a null data pointer) so its backing memory is uninitialized andSafeToRenderFrom()is false. - Issue an upload that the driver will fail
A
TexSubImage2D/TexImage2Dis issued under conditions that make the driver return an error such asGL_OUT_OF_MEMORY, so no pixels are actually written (modeled by theTexSubImage2DGLErrorDoesNotMarkLevelAsClearedregression test). - Level wrongly marked cleared
Pre-patch,
ValidateAndDoTexSubImagehad already calledSetLevelCleared/SetLevelClearedRect, so the level is now considered fully (or partially) initialized despite the failed upload. - Read uninitialized memory
The attacker samples or renders from the level; because
SafeToRenderFrom()reports safe, ANGLE skips its own clear, exposing leftover uninitialized GPU memory contents.
Impact Assessment
(inferred). Preconditions are the ability to submit texture uploads and to induce a driver-side error (e.g. out-of-memory) on the upload.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/texture_manager.cc |
modified | |
TEST_Pgpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc |
modified |
Files Changed
gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.ccgpu/command_buffer/service/texture_manager.cc
Audit Directions
- Optimistic state commits before I/OSearch for code that updates “initialized/cleared/valid” bookkeeping before the operation that actually produces the data, and confirm the commit is gated on success.
- Unchecked driver call resultsAudit command-buffer paths that issue driver
glTex*,glBufferData,glCopyTex*etc. without peeking the GL error afterward, especially where success implies a security-relevant state change (SafeToRenderFrom). - Early returns bypassing post-conditionsReview functions with multiple
returnexits (workaround branches) to ensure they do not skip a mandatory post-operation validation or cleanup step.
Patch
From 5300747616b2910e05deb6fd0b936b51697eee63 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> Date: Tue, 21 Jul 2026 06:15:38 -0700 Subject: [PATCH] [agy][gpu] Defer cleared state in TexSubImage ValidateAndDoTexSubImage marked the destination level as cleared before issuing the driver glTex(Sub)Image{2,3}D call and never checked whether the upload succeeded. If the driver returned an error, the level was left marked SafeToRenderFrom() even though no pixels were written. This CL defers the commit of the cleared state until after the GL call succeeds, using ERRORSTATE_COPY_REAL_GL_ERRORS_TO_WRAPPER and PEEK_GL_ERROR. Fixed: 516864349 Test: gpu_unittests Change-Id: I90dbaf1b04a44b1340b64d5cc20831f8b7e920a7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8108701 Commit-Queue: Tzarial <[email protected]> Reviewed-by: Geoff Lang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665413} --- diff --git a/gpu/command_buffer/service/texture_manager.cc b/gpu/command_buffer/service/texture_manager.cc index 5565b575..c8a45997 100644 --- a/gpu/command_buffer/service/texture_manager.cc +++ b/gpu/command_buffer/service/texture_manager.cc @@ -2876,6 +2876,9 @@ &tex_height, &tex_depth); DCHECK(ok); bool full_image; + bool set_cleared = false; + bool set_cleared_rect = false; + gfx::Rect cleared_rect_to_set; if (args.xoffset != 0 || args.yoffset != 0 || args.zoffset != 0 || args.width != tex_width || args.height != tex_height || args.depth != tex_depth) { @@ -2890,7 +2893,8 @@ texture->GetLevelClearedRect(args.target, args.level) .size() .GetArea()); - SetLevelClearedRect(texture_ref, args.target, args.level, cleared_rect); + cleared_rect_to_set = cleared_rect; + set_cleared_rect = !texture->IsLevelCleared(args.target, args.level); } else { // Otherwise clear part of texture level that is not already cleared. if (!ClearTextureLevel(decoder, texture_ref, args.target, args.level)) { @@ -2901,11 +2905,19 @@ } full_image = false; } else { - SetLevelCleared(texture_ref, args.target, args.level, true); + set_cleared = !texture->IsLevelCleared(args.target, args.level); full_image = true; } + // Defer committing the cleared state until the driver upload succeeds. + // See https://crbug.com/516864349 + const bool update_cleared_state = set_cleared || set_cleared_rect; + if (update_cleared_state) { + ERRORSTATE_COPY_REAL_GL_ERRORS_TO_WRAPPER(error_state, function_name); + } + Buffer* buffer = state->bound_pixel_unpack_buffer.get(); + bool uploaded = false; if (texture_state->unpack_overlapping_rows_separately_unpack_buffer && buffer) { @@ -2922,31 +2934,34 @@ // work around driver bug. DoTexSubImageRowByRowWorkaround(texture_state, state, args, unpack_params); - return; + uploaded = true; } } - if (texture_state->unpack_alignment_workaround_with_unpack_buffer && buffer && + if (!uploaded && + texture_state->unpack_alignment_workaround_with_unpack_buffer && buffer && args.width && args.height && args.depth) { uint32_t buffer_size = static_cast<uint32_t>(buffer->size()); if (buffer_size - args.pixels_size - ToGLuint(args.pixels) < args.padding) { TRACE_EVENT0("gpu", "WithAlignmentWorkaround"); DoTexSubImageWithAlignmentWorkaround(texture_state, state, args); - return; + uploaded = true; } } - if (texture_state->split_level_0_pbo_full_sub_image_2d && args.level == 0 && - buffer && + if (!uploaded && texture_state->split_level_0_pbo_full_sub_image_2d && + args.level == 0 && buffer && args.command_type == DoTexSubImageArguments::CommandType::kTexSubImage2D && full_image && args.width > 0 && args.height > 0) { TRACE_EVENT0("gpu", "SplitLevel0PboFullSubImage2dWorkaround"); DoTexSubImageSplitLevel0PboWorkaround(texture_state, state, args); - return; + uploaded = true; } - if (full_image && !texture->IsImmutable()) { + if (uploaded) { + // Upload was performed by one of the workarounds above. + } else if (full_image && !texture->IsImmutable()) { TRACE_EVENT0("gpu", "FullImage"); GLenum internal_format; GLenum tex_type; @@ -2984,6 +2999,16 @@ args.type, args.pixels); } } + + if (update_cleared_state && + ERRORSTATE_PEEK_GL_ERROR(error_state, function_name) == GL_NO_ERROR) { + if (set_cleared) { + SetLevelCleared(texture_ref, args.target, args.level, true); + } else if (set_cleared_rect) { + SetLevelClearedRect(texture_ref, args.target, args.level, + cleared_rect_to_set); + } + } } void TextureManager::DoTexSubImageWithAlignmentWorkaround( diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc index 407f6b9f..aab63f97 100644 --- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc +++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc @@ -594,6 +594,62 @@ texture->GetLevelSize(GL_TEXTURE_2D, level, &width, &height, nullptr)); } +TEST_P(GLES2DecoderTest, TexSubImage2DGLErrorDoesNotMarkLevelAsCleared) { + DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId); + DoTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0, + 0); + + TextureManager* manager = group().texture_manager(); + TextureRef* texture_ref = manager->GetTexture(client_texture_id_); + ASSERT_TRUE(texture_ref != nullptr); + Texture* texture = texture_ref->texture(); + EXPECT_FALSE(texture->SafeToRenderFrom()); + + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_OUT_OF_MEMORY)) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, + GL_UNSIGNED_BYTE, shared_memory_address_.get())) + .Times(1) + .RetiresOnSaturation(); + cmds::TexSubImage2D cmd; + cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, + shared_memory_id_, kSharedMemoryOffset, GL_FALSE); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError()); + EXPECT_FALSE(texture->SafeToRenderFrom()); +} + +TEST_P(GLES2DecoderTest, TexSubImage2DGLErrorDoesNotExpandClearedRect) { + DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId); + DoTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0, + 0); + + TextureManager* manager = group().texture_manager(); + TextureRef* texture_ref = manager->GetTexture(client_texture_id_); + ASSERT_TRUE(texture_ref != nullptr); + Texture* texture = texture_ref->texture(); + EXPECT_EQ(gfx::Rect(), texture->GetLevelClearedRect(GL_TEXTURE_2D, 0)); + + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_OUT_OF_MEMORY)) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, + TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_RGBA, + GL_UNSIGNED_BYTE, shared_memory_address_.get())) + .Times(1) + .RetiresOnSaturation(); + cmds::TexSubImage2D cmd; + cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE, + shared_memory_id_, kSharedMemoryOffset, GL_FALSE); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError()); + EXPECT_EQ(gfx::Rect(), texture->GetLevelClearedRect(GL_TEXTURE_2D, 0)); + EXPECT_FALSE(texture->SafeToRenderFrom()); +} + TEST_P(GLES2DecoderTest, CopyTexImage2DGLError) { GLenum target = GL_TEXTURE_2D; GLint level = 0; @@ -2634,6 +2690,10 @@ SetupClearTextureExpectations(kServiceTextureId, kServiceTextureId, GL_TEXTURE_2D, GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0, 1, 2, 1, 0); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); EXPECT_CALL(*gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, _, 1, GL_RGBA, GL_UNSIGNED_BYTE, shared_memory_address_.get())) @@ -2694,6 +2754,10 @@ SetupClearTextureExpectations(kServiceTextureId, kServiceTextureId, GL_TEXTURE_2D, GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0, 1, 2, 1, 0); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); EXPECT_CALL(*gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, _, 1, GL_RGBA, GL_UNSIGNED_BYTE, shared_memory_address_.get())) @@ -2722,6 +2786,10 @@ DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId); DoBufferData(GL_PIXEL_UNPACK_BUFFER, 8); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); EXPECT_CALL(*gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE, 0)) .Times(1) @@ -3478,6 +3546,10 @@ GL_FLOAT, 0, 0); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kWidth, kHeight, 0, GL_RGBA, GL_FLOAT, shared_memory_address_.get())) .Times(1) @@ -3510,6 +3582,10 @@ SetupClearTextureExpectations(kServiceTextureId, kServiceTextureId, GL_TEXTURE_2D, GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, 0, kHeight - 1, kWidth, 1, 0); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); EXPECT_CALL(*gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, _, _, GL_RGBA, GL_FLOAT, shared_memory_address_.get())) .Times(2)
Regression Test / PoC
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
index 407f6b9f..aab63f97 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -594,6 +594,62 @@
texture->GetLevelSize(GL_TEXTURE_2D, level, &width, &height, nullptr));
}
+TEST_P(GLES2DecoderTest, TexSubImage2DGLErrorDoesNotMarkLevelAsCleared) {
+ DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+ DoTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0,
+ 0);
+
+ TextureManager* manager = group().texture_manager();
+ TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+ ASSERT_TRUE(texture_ref != nullptr);
+ Texture* texture = texture_ref->texture();
+ EXPECT_FALSE(texture->SafeToRenderFrom());
+
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_OUT_OF_MEMORY))
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, shared_memory_address_.get()))
+ .Times(1)
+ .RetiresOnSaturation();
+ cmds::TexSubImage2D cmd;
+ cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE,
+ shared_memory_id_, kSharedMemoryOffset, GL_FALSE);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+ EXPECT_FALSE(texture->SafeToRenderFrom());
+}
+
+TEST_P(GLES2DecoderTest, TexSubImage2DGLErrorDoesNotExpandClearedRect) {
+ DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+ DoTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0,
+ 0);
+
+ TextureManager* manager = group().texture_manager();
+ TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+ ASSERT_TRUE(texture_ref != nullptr);
+ Texture* texture = texture_ref->texture();
+ EXPECT_EQ(gfx::Rect(), texture->GetLevelClearedRect(GL_TEXTURE_2D, 0));
+
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_OUT_OF_MEMORY))
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_,
+ TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_RGBA,
+ GL_UNSIGNED_BYTE, shared_memory_address_.get()))
+ .Times(1)
+ .RetiresOnSaturation();
+ cmds::TexSubImage2D cmd;
+ cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE,
+ shared_memory_id_, kSharedMemoryOffset, GL_FALSE);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+ EXPECT_EQ(gfx::Rect(), texture->GetLevelClearedRect(GL_TEXTURE_2D, 0));
+ EXPECT_FALSE(texture->SafeToRenderFrom());
+}
+
TEST_P(GLES2DecoderTest, CopyTexImage2DGLError) {
GLenum target = GL_TEXTURE_2D;
GLint level = 0;
@@ -2634,6 +2690,10 @@
SetupClearTextureExpectations(kServiceTextureId, kServiceTextureId,
GL_TEXTURE_2D, GL_TEXTURE_2D, 0, GL_RGBA,
GL_UNSIGNED_BYTE, 0, 1, 2, 1, 0);
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
EXPECT_CALL(*gl_,
TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, _, 1, GL_RGBA,
GL_UNSIGNED_BYTE, shared_memory_address_.get()))
@@ -2694,6 +2754,10 @@
SetupClearTextureExpectations(kServiceTextureId, kServiceTextureId,
GL_TEXTURE_2D, GL_TEXTURE_2D, 0, GL_RGBA,
GL_UNSIGNED_BYTE, 0, 1, 2, 1, 0);
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
EXPECT_CALL(*gl_,
TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, _, 1, GL_RGBA,
GL_UNSIGNED_BYTE, shared_memory_address_.get()))
@@ -2722,6 +2786,10 @@
DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
DoBufferData(GL_PIXEL_UNPACK_BUFFER, 8);
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
EXPECT_CALL(*gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_RGBA,
GL_UNSIGNED_BYTE, 0))
.Times(1)
@@ -3478,6 +3546,10 @@
GL_FLOAT,
0,
0);
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kWidth, kHeight, 0,
GL_RGBA, GL_FLOAT, shared_memory_address_.get()))
.Times(1)
@@ -3510,6 +3582,10 @@
SetupClearTextureExpectations(kServiceTextureId, kServiceTextureId,
GL_TEXTURE_2D, GL_TEXTURE_2D, 0, GL_RGBA,
GL_FLOAT, 0, kHeight - 1, kWidth, 1, 0);
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
EXPECT_CALL(*gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, _, _, GL_RGBA,
GL_FLOAT, shared_memory_address_.get()))
.Times(2)