CVE-2026-87555
Overview
Background
- Lazy clearing
- A GPU optimization where a newly allocated texture level is not zeroed immediately but instead flagged as needing a clear before it can be safely read from or rendered.
- PBO (Pixel Buffer Object)
- A GL buffer bound to
GL_PIXEL_UNPACK_BUFFERfrom which texture pixel data is sourced during uploads instead of client memory. - Driver bug workaround
- A code path (e.g.
unpack_overlapping_rows_separately_unpack_bufferorunpack_alignment_workaround_with_unpack_buffer) that rewrites a texture upload into multipleTexSubImage2Dcalls to sidestep buggy driver behavior. - `SetLevelCleared`
- A
TextureManagercall that marks a texture level as fully initialized, allowing it to passSafeToRenderFromchecks without a lazy clear.
Root Cause Analysis
In TextureManager::ValidateAndDoTexImage, when a PBO-based driver-bug workaround runs — the row-by-row path (DoTexSubImageRowByRowWorkaround) or the alignment path (DoTexSubImageWithAlignmentWorkaround) — the code unconditionally called SetLevelCleared(texture_ref, args.target, args.level, true) immediately after issuing the split TexSubImage2D uploads. The invariant that “a level may only be marked cleared once its entire contents have actually been written” was violated because the workaround could fail partway through (for example a driver returning GL_OUT_OF_MEMORY), leaving some rows of the level never written. Because the level was still flagged as cleared, the lazy-clear step was permanently bypassed and the texture reported SafeToRenderFrom() == true.
The fix wraps each SetLevelCleared call in a check of ERRORSTATE_PEEK_GL_ERROR(error_state, function_name) == GL_NO_ERROR, so the level is marked cleared only when the workaround completed without a GL error; otherwise the level stays uncleared and the normal lazy-clear path will zero it before use.
SetLevelCleared on ERRORSTATE_PEEK_GL_ERROR returning GL_NO_ERROR, preserving the uncleared state whenever the uploads fail.Attack Path
- Trigger a workaround-enabled context
On hardware where
unpack_overlapping_rows_separately_unpack_bufferorunpack_alignment_workaround_with_unpack_bufferis active, a page obtains a WebGL2 context that routes uploads through the vulnerable path. - Bind a PBO and set pixel-store state
The attacker binds a buffer to
GL_PIXEL_UNPACK_BUFFERand configuresGL_UNPACK_ROW_LENGTH/alignment soValidateAndDoTexImageselects the split-upload workaround. - Force a mid-upload GL error
By sizing the texture, buffer, or state such that one of the split
TexSubImage2Dcalls fails (e.g. driver returnsGL_OUT_OF_MEMORY), the level is left only partially written. - Bypass lazy clear
The pre-patch code still calls
SetLevelCleared(..., true), so the level is flagged cleared and never zeroed despite containing uninitialized VRAM. - Read back the stale contents
The attacker samples or reads back the texture (which now passes
SafeToRenderFrom), disclosing previously-freed GPU memory.
Impact Assessment
unpack_overlapping_rows_separately_unpack_buffer or unpack_alignment_workaround_with_unpack_buffer workaround is enabled, use of a PBO-sourced upload, and inducing a GL error mid-workaround.Changed Functions
| Function | Change | Notes |
|---|---|---|
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
- Unconditional `SetLevelCleared` after uploadsSearch for
SetLevelCleared(..., true)calls that follow texture-write helpers without an intervening GL-error check, since any failed or partial write followed by a cleared flag reopens this uninitialized-memory class. - Error handling in driver-bug workaround pathsReview each
GpuDriverBugWorkarounds-gated split-upload path (row-by-row, alignment, and similar) to confirm it propagates or peeks GL errors before updating clear/initialized state. - Lazy-clear invariant enforcementAudit places where
SafeToRenderFromand level-cleared state can diverge from actual written coverage, ensuring partially-completed uploads never leave a level marked as fully initialized.
Patch
From b4c415c65c314e40b6b9c65af05ed758e8c26a22 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> Date: Wed, 12 Aug 2026 09:14:28 -0700 Subject: [PATCH] [agy][gpu] Fix PBO workaround lazy-clear bypass Do not mark texture level as cleared if an error occurred during the PBO workarounds (row-by-row or alignment workarounds) in ValidateAndDoTexImage. Fixed: 517337579 Test: gpu_unittests Change-Id: If54f50ff98fb55435fae3ad3cf145caf5fb41d78 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8239634 Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Tzarial <[email protected]> Cr-Commit-Position: refs/heads/main@{#1678093} --- diff --git a/gpu/command_buffer/service/texture_manager.cc b/gpu/command_buffer/service/texture_manager.cc index 5c9b27bc9..c9a764c 100644 --- a/gpu/command_buffer/service/texture_manager.cc +++ b/gpu/command_buffer/service/texture_manager.cc @@ -2688,7 +2688,12 @@ DoTexSubImageRowByRowWorkaround(texture_state, state, sub_args, unpack_params); - SetLevelCleared(texture_ref, args.target, args.level, true); + // https://crbug.com/517337579: Only mark the level as cleared if the + // workaround succeeded, to prevent leaking uninitialized VRAM if + // sub-image uploads failed. + if (ERRORSTATE_PEEK_GL_ERROR(error_state, function_name) == GL_NO_ERROR) { + SetLevelCleared(texture_ref, args.target, args.level, true); + } return; } } @@ -2722,7 +2727,12 @@ : DoTexSubImageArguments::CommandType::kTexSubImage2D}; DoTexSubImageWithAlignmentWorkaround(texture_state, state, sub_args); - SetLevelCleared(texture_ref, args.target, args.level, true); + // https://crbug.com/517337579: Only mark the level as cleared if the + // workaround succeeded, to prevent leaking uninitialized VRAM if + // sub-image uploads failed. + if (ERRORSTATE_PEEK_GL_ERROR(error_state, function_name) == GL_NO_ERROR) { + SetLevelCleared(texture_ref, args.target, args.level, true); + } return; } } 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 70e377be..6b7d756 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,119 @@ texture->GetLevelSize(GL_TEXTURE_2D, level, &width, &height, nullptr)); } +TEST_P(GLES2DecoderManualInitTest, + TexImage2DUnpackOverlappingRowsWorkaroundGLError) { + gpu::GpuDriverBugWorkarounds workarounds; + workarounds.unpack_overlapping_rows_separately_unpack_buffer = true; + InitState init; + init.gl_version = "OpenGL ES 3.0"; + init.context_type = CONTEXT_TYPE_OPENGLES3; + InitDecoderWithWorkarounds(init, workarounds); + + const GLsizei kWidth = 2; + const GLsizei kHeight = 2; + const GLint kRowLength = 1; + const GLenum kFormat = GL_RGBA; + const GLenum kType = GL_UNSIGNED_BYTE; + const GLint kBufferSize = 16; + + DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId); + + cmds::PixelStorei pixel_store_cmd; + pixel_store_cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength); + EXPECT_EQ(error::kNoError, ExecuteCmd(pixel_store_cmd)); + + EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, kRowLength)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0)) + .Times(1) + .RetiresOnSaturation(); + DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId); + DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize); + + TextureManager* manager = group().texture_manager(); + TextureRef* texture_ref = manager->GetTexture(client_texture_id_); + ASSERT_TRUE(texture_ref != nullptr); + Texture* texture = texture_ref->texture(); + + EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber()); + EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber()); + EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, + kFormat, kType, nullptr)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL( + *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, 1, kFormat, kType, _)) + .Times(kHeight) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_OUT_OF_MEMORY)) + .RetiresOnSaturation(); + cmds::TexImage2D cmd; + cmd.Init(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, kFormat, kType, 0, 0); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0)); + EXPECT_FALSE(texture->SafeToRenderFrom()); + EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError()); +} + +TEST_P(GLES2DecoderManualInitTest, TexImage2DUnpackAlignmentWorkaroundGLError) { + gpu::GpuDriverBugWorkarounds workarounds; + workarounds.unpack_alignment_workaround_with_unpack_buffer = true; + InitState init; + init.gl_version = "OpenGL ES 3.0"; + init.context_type = CONTEXT_TYPE_OPENGLES3; + InitDecoderWithWorkarounds(init, workarounds); + + const GLsizei kWidth = 2; + const GLsizei kHeight = 2; + const GLenum kFormat = GL_RGB; + const GLenum kType = GL_UNSIGNED_BYTE; + // Padded row size 8 (alignment 4), unpadded row size 6; pixels_size = 14. + const GLint kBufferSize = 14; + + DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId); + + EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, 0)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0)) + .Times(1) + .RetiresOnSaturation(); + DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId); + DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize); + + TextureManager* manager = group().texture_manager(); + TextureRef* texture_ref = manager->GetTexture(client_texture_id_); + ASSERT_TRUE(texture_ref != nullptr); + Texture* texture = texture_ref->texture(); + + EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber()); + EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber()); + EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0, + kFormat, kType, nullptr)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL( + *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, _, kFormat, kType, _)) + .Times(2) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_OUT_OF_MEMORY)) + .RetiresOnSaturation(); + cmds::TexImage2D cmd; + cmd.Init(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, kFormat, kType, 0, 0); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0)); + EXPECT_FALSE(texture->SafeToRenderFrom()); + EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError()); +} + 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,
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 70e377be..6b7d756 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,119 @@
texture->GetLevelSize(GL_TEXTURE_2D, level, &width, &height, nullptr));
}
+TEST_P(GLES2DecoderManualInitTest,
+ TexImage2DUnpackOverlappingRowsWorkaroundGLError) {
+ gpu::GpuDriverBugWorkarounds workarounds;
+ workarounds.unpack_overlapping_rows_separately_unpack_buffer = true;
+ InitState init;
+ init.gl_version = "OpenGL ES 3.0";
+ init.context_type = CONTEXT_TYPE_OPENGLES3;
+ InitDecoderWithWorkarounds(init, workarounds);
+
+ const GLsizei kWidth = 2;
+ const GLsizei kHeight = 2;
+ const GLint kRowLength = 1;
+ const GLenum kFormat = GL_RGBA;
+ const GLenum kType = GL_UNSIGNED_BYTE;
+ const GLint kBufferSize = 16;
+
+ DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+ cmds::PixelStorei pixel_store_cmd;
+ pixel_store_cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(pixel_store_cmd));
+
+ EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, kRowLength))
+ .Times(1)
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0))
+ .Times(1)
+ .RetiresOnSaturation();
+ DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
+ DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize);
+
+ TextureManager* manager = group().texture_manager();
+ TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+ ASSERT_TRUE(texture_ref != nullptr);
+ Texture* texture = texture_ref->texture();
+
+ EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0,
+ kFormat, kType, nullptr))
+ .Times(1)
+ .RetiresOnSaturation();
+ EXPECT_CALL(
+ *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, 1, kFormat, kType, _))
+ .Times(kHeight)
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_OUT_OF_MEMORY))
+ .RetiresOnSaturation();
+ cmds::TexImage2D cmd;
+ cmd.Init(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, kFormat, kType, 0, 0);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0));
+ EXPECT_FALSE(texture->SafeToRenderFrom());
+ EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+}
+
+TEST_P(GLES2DecoderManualInitTest, TexImage2DUnpackAlignmentWorkaroundGLError) {
+ gpu::GpuDriverBugWorkarounds workarounds;
+ workarounds.unpack_alignment_workaround_with_unpack_buffer = true;
+ InitState init;
+ init.gl_version = "OpenGL ES 3.0";
+ init.context_type = CONTEXT_TYPE_OPENGLES3;
+ InitDecoderWithWorkarounds(init, workarounds);
+
+ const GLsizei kWidth = 2;
+ const GLsizei kHeight = 2;
+ const GLenum kFormat = GL_RGB;
+ const GLenum kType = GL_UNSIGNED_BYTE;
+ // Padded row size 8 (alignment 4), unpadded row size 6; pixels_size = 14.
+ const GLint kBufferSize = 14;
+
+ DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+ EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, 0))
+ .Times(1)
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0))
+ .Times(1)
+ .RetiresOnSaturation();
+ DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
+ DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize);
+
+ TextureManager* manager = group().texture_manager();
+ TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+ ASSERT_TRUE(texture_ref != nullptr);
+ Texture* texture = texture_ref->texture();
+
+ EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0,
+ kFormat, kType, nullptr))
+ .Times(1)
+ .RetiresOnSaturation();
+ EXPECT_CALL(
+ *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, _, kFormat, kType, _))
+ .Times(2)
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_OUT_OF_MEMORY))
+ .RetiresOnSaturation();
+ cmds::TexImage2D cmd;
+ cmd.Init(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, kFormat, kType, 0, 0);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0));
+ EXPECT_FALSE(texture->SafeToRenderFrom());
+ EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+}
+
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,
Original Bug Report
Potential cross-origin VRAM leak in TextureManager PBO workarounds
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential vulnerability in gpu/command_buffer/service/texture_manager.cc allows bypassing the validating decoder’s lazy-clear protections on affected NVIDIA Android devices. Unconditional calls to SetLevelCleared in the NVIDIA PBO workaround branches mark texture levels as cleared even if driver-level sub-uploads fail due to GPU memory pressure. This can potentially result in the disclosure of uninitialized, cross-origin GPU memory (VRAM) to an attacker’s WebGL context.
Affected files:
gpu/command_buffer/service/texture_manager.cc
Estimated timestamp from git blame: 2016-03-14
Description
There is a potential vulnerability in the GLES2 validating command decoder’s texture manager (gpu/command_buffer/service/texture_manager.cc) associated with the NVIDIA Pixel Buffer Object (PBO) workarounds. On affected configurations (such as Android devices running with NVIDIA GPU drivers), specific workaround branches in TextureManager::ValidateAndDoTexImage can unconditionally mark a texture level as fully cleared even if the driver-level sub-uploads fail. This allows an attacker to bypass the validating decoder’s lazy-clear protections and potentially read back uninitialized, cross-origin GPU memory (VRAM).
Root Cause Analysis
In gpu/command_buffer/service/texture_manager.cc, TextureManager::ValidateAndDoTexImage handles NVIDIA PBO-workaround paths for overlapping unpack rows (workaround ID 165) and alignment constraints (workaround ID 217). The implementation carries out the following operations:
- It pre-allocates uninitialized backing storage on the driver using
ReserveTexImageToBeFilled(which callsDoTexImagewithpixels = nullptrand the pixel unpack buffer temporarily unbound). Sincepixelsisnullptr,DoTexImagesets the level cleared rect to empty, correctly marking the texture level as uncleared. - It attempts to populate this uninitialized storage with the user’s data by calling the helper functions
DoTexSubImageRowByRowWorkaroundorDoTexSubImageWithAlignmentWorkaround. - Finally, it unconditionally marks the texture level as fully cleared at line 2684 and line 2718 by calling:
SetLevelCleared(texture_ref, args.target, args.level, true);
The potential vulnerability is that the helper sub-upload functions are void and do not perform any GL-error checking. If a driver-level sub-upload fails silently (for example, generating a GL_OUT_OF_MEMORY error under high GPU memory pressure), the uninitialized VRAM allocated in step 1 is not fully overwritten with user-supplied data. However, because SetLevelCleared is called unconditionally, the command decoder assumes the texture is safely initialized. Consequently, the lazy-clear checks (such as those in gles2_cmd_decoder.cc using SafeToRenderFrom()) are bypassed.
This behavior contrasts with secure reference patterns in the codebase, such as GLES2DecoderImpl::DoCopyTexSubImage2D (at gles2_cmd_decoder.cc:13768), where the cleared state commit is deferred and strictly gated on LOCAL_PEEK_GL_ERROR(...) == GL_NO_ERROR.
Potential Trigger Path
Please note that these are potential steps; our automated analysis tooling does not currently have the capability to run code or execute a proof of concept.
- An attacker sets up a page containing WebGL2 content, targeting a device using GLES2 validating decoder with an NVIDIA vendor (where workarounds 165 and 217 are active).
- The attacker’s script binds a
PIXEL_UNPACK_BUFFERand configures overlapping unpack rows viaUNPACK_ROW_LENGTH(whererow_length < width) or triggers specific alignment constraints. - The attacker allocates a texture using
texImage2DortexImage3Dto enter the workaround branches inValidateAndDoTexImage. - If the attacker has previously induced high GPU memory pressure (by allocating large graphics resources), one or more raw
glTexSubImage2D/glTexSubImage3Dcalls within the helper sub-upload loops may fail withGL_OUT_OF_MEMORY. - The driver records the error, but since the decoder does not check for it, the texture level is unconditionally marked as cleared via
SetLevelCleared. - The attacker subsequently reads back from the texture (e.g., via shader sampling or framebuffer readbacks with
gl.readPixels). Because lazy-clearing is bypassed, the script potentially obtains uninitialized VRAM that contains stale data from other origins sharing the GPU process.
Suggested Remediation
To remediate this issue, the cleared-state commit should be deferred and strictly gated on verifying that no GL errors occurred during the workaround sub-uploads, similar to the pattern used in DoCopyTexSubImage2D:
// Verify no errors occurred before marking the level as cleared
if (error_state->PeekGLError(function_name, __LINE__) == GL_NO_ERROR) {
SetLevelCleared(texture_ref, args.target, args.level, true);
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.