CVE-2026-17808
Overview
Background
- Lazy clear
- Chrome’s GPU service defers zero-initializing framebuffer attachments until they are first read, so uncleared attachments must be cleared on-demand before any read exposes their contents.
- Draw vs. read framebuffer
- GLES3 splits the framebuffer binding into
GL_DRAW_FRAMEBUFFER(write target) andGL_READ_FRAMEBUFFER(read source), which may point at different FBOs simultaneously. - `glDrawBuffersARB`
- a GL call that selects which color attachments a
glClear(and other draw operations) will affect; it acts on the currently boundGL_DRAW_FRAMEBUFFER. - `readPixels`/`copyTexImage`
- WebGL read operations that copy pixels out of the read framebuffer, where uninitialized attachment memory would be disclosed.
Root Cause Analysis
When clearing uncleared color attachments, GLES2DecoderImpl::ClearUnclearedAttachments() calls PrepareDrawBuffersForClearingUninitializedAttachments(), which issues glDrawBuffersARB to enable the uncleared attachments as clear targets. Because glDrawBuffersARB operates on GL_DRAW_FRAMEBUFFER, the draw-buffer setup only affects whichever FBO is currently bound as the draw target. On the read-triggered clear path, the read target is GL_READ_FRAMEBUFFER and the framebuffer being cleared is the read FBO, but the currently bound draw FBO (draw_framebuffer) can be a different framebuffer.
As a result, glDrawBuffersARB configured the draw buffers of the wrong (draw) FBO, and the subsequent glClear — which also targets the draw framebuffer — cleared the wrong attachments (or none of the intended ones), leaving the read FBO’s attachments uninitialized.
The fix rebinds GL_DRAW_FRAMEBUFFER to the framebuffer being cleared (framebuffer->service_id()) before preparing draw buffers, when target == GL_READ_FRAMEBUFFER and draw_framebuffer != framebuffer.
glDrawBuffersARB and glClear implicitly act on GL_DRAW_FRAMEBUFFER, so lazily clearing a framebuffer reached via GL_READ_FRAMEBUFFER silently mis-targets whatever draw FBO happens to be bound unless the draw binding is first pointed at the framebuffer actually being cleared.Attack Path
- Set up divergent bindings
In WebGL2, bind one FBO with an uncleared color attachment to
GL_READ_FRAMEBUFFERand a different FBO toGL_DRAW_FRAMEBUFFER. - Trigger read-path lazy clear
Issue a read against the read FBO (e.g.
readPixelsorcopyTexImage), invoking the on-read lazy-clear path. - Mis-targeted clear
PrepareDrawBuffersForClearingUninitializedAttachments()and the followingglClearoperate on the wrong draw FBO, so the read FBO’s uncleared attachment is never zeroed. - Disclose uninitialized memory The read then returns the attachment’s prior GPU memory contents, leaking data from other contexts or prior allocations.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/gles2_cmd_decoder.cc |
modified | |
TEST_Pgpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc |
modified | |
ifgpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc |
modified |
Files Changed
gpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
Audit Directions
- Implicit draw-target dependenceAudit every lazy-clear/auto-clear path that uses
glDrawBuffersARB,glClear, blits, orglDrawArrays-style ops against a framebuffer reached viaGL_READ_FRAMEBUFFER, confirming the correct FBO is bound as the draw target first. - Split read/draw framebuffer handlingReview code that assumes a single framebuffer binding but runs under GLES3 where
draw_framebufferandread_framebuffercan diverge, looking for operations applied to the wrong binding. - Clear/restore symmetryVerify that any temporary rebinding done for clearing (
reset_draw_buffers, the newrebound_draw_for_clearrestore logic) correctly restores the original draw framebuffer and draw-buffer state on all exit paths.
Patch
From 7daeec48402d93417c03d59a9f8e4fcd84188b68 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Mon, 08 Jun 2026 12:44:42 -0700
Subject: [PATCH] Rebind draw framebuffer before lazy clear.
Fix a code path where the lazy clear could target the wrong
framebuffer.
Integrate unit test from the bug report.
Co-authored with jetski-cli.
Fixed: 516778390
Change-Id: I207c71cb0d97aac93cb989e6b7c610c2db1924dc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7903549
Reviewed-by: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Auto-Submit: Kenneth Russell <kbr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1643408}
---
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 4252a6e..7b387bf 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -7094,6 +7094,7 @@
GLbitfield clear_bits = 0;
bool reset_draw_buffers = false;
+ bool rebound_draw_for_clear = cleared_int_renderbuffers;
if (framebuffer->HasUnclearedColorAttachments()) {
// We should always use alpha == 0 here, because 1) some draw buffers may
// have alpha and some may not; 2) we won't have the same situation as the
@@ -7103,6 +7104,14 @@
clear_bits |= GL_COLOR_BUFFER_BIT;
if (SupportsDrawBuffers()) {
+ // Ensure |framebuffer| is bound as DRAW before preparing draw buffers.
+ // Otherwise glDrawBuffersARB mutates the wrong FBO's state, causing
+ // the glClear to skip clearing uncleared attachments.
+ if (!rebound_draw_for_clear && target == GL_READ_FRAMEBUFFER &&
+ draw_framebuffer != framebuffer) {
+ BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer->service_id());
+ rebound_draw_for_clear = true;
+ }
reset_draw_buffers =
framebuffer->PrepareDrawBuffersForClearingUninitializedAttachments();
}
@@ -7122,8 +7131,8 @@
}
if (clear_bits) {
- if (!cleared_int_renderbuffers &&
- target == GL_READ_FRAMEBUFFER && draw_framebuffer != framebuffer) {
+ if (!rebound_draw_for_clear && target == GL_READ_FRAMEBUFFER &&
+ draw_framebuffer != framebuffer) {
// TODO(zmo): There is no guarantee that an FBO that is complete on the
// READ attachment will be complete as a DRAW attachment.
BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer->service_id());
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
index b39cc0e..134e4404 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -2858,6 +2858,120 @@
EXPECT_EQ(GL_NO_ERROR, GetGLError());
}
+// Regression test: when lazily clearing a READ FBO (with a different DRAW
+// FBO), the decoder must rebind the lazy-clear target as DRAW before
+// calling PrepareDrawBuffersForClearingUninitializedAttachments().
+// Otherwise the wrong FBO's state is mutated, leading to skipped clears
+// and potential memory disclosure.
+TEST_P(GLES3DecoderTest, LazyClearReadFBORebindsDrawBeforePrepareDrawBuffers) {
+ // Setup: fboA with uncleared RGBA8 renderbuffer and drawBuffers=NONE
+ DoBindRenderbuffer(GL_RENDERBUFFER, client_renderbuffer_id_,
+ kServiceRenderbufferId);
+ DoRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1, GL_NO_ERROR);
+
+ DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, client_framebuffer_id_,
+ kServiceFramebufferId);
+ DoFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_RENDERBUFFER, client_renderbuffer_id_,
+ kServiceRenderbufferId, GL_NO_ERROR);
+ {
+ // Set drawBuffers to NONE on fboA.
+ const GLenum bufs[] = {GL_NONE};
+ EXPECT_CALL(*gl_, DrawBuffersARB(1, _)).Times(1).RetiresOnSaturation();
+ auto& db = *GetImmediateAs<cmds::DrawBuffersEXTImmediate>();
+ db.Init(1, bufs);
+ EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(db, sizeof(bufs)));
+ }
+
+ // Bind fboB as DRAW (draw_framebuffer != fboA).
+ EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+ .WillOnce(SetArgPointee<1>(kNewServiceId))
+ .RetiresOnSaturation();
+ GLuint fbo_b_client = client_framebuffer_id_ + 1;
+ GenHelper<cmds::GenFramebuffersImmediate>(fbo_b_client);
+ DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo_b_client, kNewServiceId);
+
+ // Bind fboA as READ.
+ DoBindFramebuffer(GL_READ_FRAMEBUFFER, client_framebuffer_id_,
+ kServiceFramebufferId);
+
+ // Trigger ReadPixels, which should lazy clear fboA.
+ struct Tracker {
+ GLuint bound_draw_fbo;
+ GLuint fbo_at_prepare_drawbuffers;
+ GLuint fbo_at_clear;
+ int drawbuffers_calls;
+ };
+ auto t = std::make_shared<Tracker>();
+ t->bound_draw_fbo = kNewServiceId;
+ t->fbo_at_prepare_drawbuffers = 0xDEADBEEF;
+ t->fbo_at_clear = 0xDEADBEEF;
+ t->drawbuffers_calls = 0;
+
+ EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_READ_FRAMEBUFFER))
+ .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+ .RetiresOnSaturation();
+
+ EXPECT_CALL(*gl_, BindFramebufferEXT(_, _))
+ .WillRepeatedly([t](GLenum target, GLuint id) {
+ if (target == GL_DRAW_FRAMEBUFFER || target == GL_FRAMEBUFFER) {
+ t->bound_draw_fbo = id;
+ }
+ });
+
+ EXPECT_CALL(*gl_, DrawBuffersARB(_, _))
+ .WillRepeatedly([t](GLsizei, const GLenum*) {
+ // First call is PrepareDrawBuffers, second is RestoreDrawBuffers.
+ if (t->drawbuffers_calls++ == 0) {
+ t->fbo_at_prepare_drawbuffers = t->bound_draw_fbo;
+ }
+ });
+
+ EXPECT_CALL(*gl_, Clear(GL_COLOR_BUFFER_BIT))
+ .WillOnce([t](GLbitfield) { t->fbo_at_clear = t->bound_draw_fbo; })
+ .RetiresOnSaturation();
+
+ // Absorb the rest of the lazy-clear / restore-state noise.
+ EXPECT_CALL(*gl_, ClearColor(_, _, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, ColorMask(_, _, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, ClearStencil(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, ClearDepth(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, StencilMaskSeparate(_, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, DepthMask(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, Disable(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, Enable(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, Scissor(_, _, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, WindowRectanglesEXT(_, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, GetError()).WillRepeatedly(Return(GL_NO_ERROR));
+ EXPECT_CALL(*gl_, ReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, _))
+ .Times(1)
+ .RetiresOnSaturation();
+
+ auto* result = GetSharedMemoryAs<cmds::ReadPixels::Result*>();
+ cmds::ReadPixels rp;
+ rp.Init(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, shared_memory_id_,
+ kSharedMemoryOffset + sizeof(*result), shared_memory_id_,
+ kSharedMemoryOffset, false);
+ result->success = 0;
+ EXPECT_EQ(error::kNoError, ExecuteCmd(rp));
+
+ // Assertions
+ // PrepareDrawBuffers should have been called.
+ EXPECT_GE(t->drawbuffers_calls, 1)
+ << "PrepareDrawBuffers should have issued glDrawBuffersARB.";
+
+ // glClear must target fboA.
+ EXPECT_EQ(static_cast<GLuint>(kServiceFramebufferId), t->fbo_at_clear)
+ << "glClear targetted wrong FBO.";
+
+ // Verify fboA was bound as DRAW when PrepareDrawBuffers was called.
+ EXPECT_EQ(static_cast<GLuint>(kServiceFramebufferId),
+ t->fbo_at_prepare_drawbuffers)
+ << "PrepareDrawBuffers called with wrong DRAW framebuffer bound: "
+ << t->fbo_at_prepare_drawbuffers << " instead of "
+ << kServiceFramebufferId;
+}
+
TEST_P(GLES2DecoderWithShaderTest, CopyTexImageWithInCompleteFBOFails) {
GLenum target = GL_TEXTURE_2D;
GLint level = 0;
Regression Test / PoC
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
index b39cc0e..134e4404 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -2858,6 +2858,120 @@
EXPECT_EQ(GL_NO_ERROR, GetGLError());
}
+// Regression test: when lazily clearing a READ FBO (with a different DRAW
+// FBO), the decoder must rebind the lazy-clear target as DRAW before
+// calling PrepareDrawBuffersForClearingUninitializedAttachments().
+// Otherwise the wrong FBO's state is mutated, leading to skipped clears
+// and potential memory disclosure.
+TEST_P(GLES3DecoderTest, LazyClearReadFBORebindsDrawBeforePrepareDrawBuffers) {
+ // Setup: fboA with uncleared RGBA8 renderbuffer and drawBuffers=NONE
+ DoBindRenderbuffer(GL_RENDERBUFFER, client_renderbuffer_id_,
+ kServiceRenderbufferId);
+ DoRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1, GL_NO_ERROR);
+
+ DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, client_framebuffer_id_,
+ kServiceFramebufferId);
+ DoFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_RENDERBUFFER, client_renderbuffer_id_,
+ kServiceRenderbufferId, GL_NO_ERROR);
+ {
+ // Set drawBuffers to NONE on fboA.
+ const GLenum bufs[] = {GL_NONE};
+ EXPECT_CALL(*gl_, DrawBuffersARB(1, _)).Times(1).RetiresOnSaturation();
+ auto& db = *GetImmediateAs<cmds::DrawBuffersEXTImmediate>();
+ db.Init(1, bufs);
+ EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(db, sizeof(bufs)));
+ }
+
+ // Bind fboB as DRAW (draw_framebuffer != fboA).
+ EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+ .WillOnce(SetArgPointee<1>(kNewServiceId))
+ .RetiresOnSaturation();
+ GLuint fbo_b_client = client_framebuffer_id_ + 1;
+ GenHelper<cmds::GenFramebuffersImmediate>(fbo_b_client);
+ DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo_b_client, kNewServiceId);
+
+ // Bind fboA as READ.
+ DoBindFramebuffer(GL_READ_FRAMEBUFFER, client_framebuffer_id_,
+ kServiceFramebufferId);
+
+ // Trigger ReadPixels, which should lazy clear fboA.
+ struct Tracker {
+ GLuint bound_draw_fbo;
+ GLuint fbo_at_prepare_drawbuffers;
+ GLuint fbo_at_clear;
+ int drawbuffers_calls;
+ };
+ auto t = std::make_shared<Tracker>();
+ t->bound_draw_fbo = kNewServiceId;
+ t->fbo_at_prepare_drawbuffers = 0xDEADBEEF;
+ t->fbo_at_clear = 0xDEADBEEF;
+ t->drawbuffers_calls = 0;
+
+ EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_READ_FRAMEBUFFER))
+ .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+ .RetiresOnSaturation();
+
+ EXPECT_CALL(*gl_, BindFramebufferEXT(_, _))
+ .WillRepeatedly([t](GLenum target, GLuint id) {
+ if (target == GL_DRAW_FRAMEBUFFER || target == GL_FRAMEBUFFER) {
+ t->bound_draw_fbo = id;
+ }
+ });
+
+ EXPECT_CALL(*gl_, DrawBuffersARB(_, _))
+ .WillRepeatedly([t](GLsizei, const GLenum*) {
+ // First call is PrepareDrawBuffers, second is RestoreDrawBuffers.
+ if (t->drawbuffers_calls++ == 0) {
+ t->fbo_at_prepare_drawbuffers = t->bound_draw_fbo;
+ }
+ });
+
+ EXPECT_CALL(*gl_, Clear(GL_COLOR_BUFFER_BIT))
+ .WillOnce([t](GLbitfield) { t->fbo_at_clear = t->bound_draw_fbo; })
+ .RetiresOnSaturation();
+
+ // Absorb the rest of the lazy-clear / restore-state noise.
+ EXPECT_CALL(*gl_, ClearColor(_, _, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, ColorMask(_, _, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, ClearStencil(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, ClearDepth(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, StencilMaskSeparate(_, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, DepthMask(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, Disable(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, Enable(_)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, Scissor(_, _, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, WindowRectanglesEXT(_, _, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, GetError()).WillRepeatedly(Return(GL_NO_ERROR));
+ EXPECT_CALL(*gl_, ReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, _))
+ .Times(1)
+ .RetiresOnSaturation();
+
+ auto* result = GetSharedMemoryAs<cmds::ReadPixels::Result*>();
+ cmds::ReadPixels rp;
+ rp.Init(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, shared_memory_id_,
+ kSharedMemoryOffset + sizeof(*result), shared_memory_id_,
+ kSharedMemoryOffset, false);
+ result->success = 0;
+ EXPECT_EQ(error::kNoError, ExecuteCmd(rp));
+
+ // Assertions
+ // PrepareDrawBuffers should have been called.
+ EXPECT_GE(t->drawbuffers_calls, 1)
+ << "PrepareDrawBuffers should have issued glDrawBuffersARB.";
+
+ // glClear must target fboA.
+ EXPECT_EQ(static_cast<GLuint>(kServiceFramebufferId), t->fbo_at_clear)
+ << "glClear targetted wrong FBO.";
+
+ // Verify fboA was bound as DRAW when PrepareDrawBuffers was called.
+ EXPECT_EQ(static_cast<GLuint>(kServiceFramebufferId),
+ t->fbo_at_prepare_drawbuffers)
+ << "PrepareDrawBuffers called with wrong DRAW framebuffer bound: "
+ << t->fbo_at_prepare_drawbuffers << " instead of "
+ << kServiceFramebufferId;
+}
+
TEST_P(GLES2DecoderWithShaderTest, CopyTexImageWithInCompleteFBOFails) {
GLenum target = GL_TEXTURE_2D;
GLint level = 0;
Original Bug Report
Potential bypass of lazy clear in validating command decoder via misplaced draw framebuffer bind
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 logic vulnerability exists in the GLES2 validating command decoder where the active draw framebuffer is rebound after preparing draw buffers for clearing. This discrepancy can allow a WebGL context to bypass lazy initialization of color attachments and read back uninitialized GPU memory contents.
Affected files:
gpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/service/framebuffer_manager.cc
Estimated timestamp from git blame: 2016-07-15
Description
In gpu/command_buffer/service/gles2_cmd_decoder.cc, the function GLES2DecoderImpl::ClearUnclearedAttachments handles the lazy clearing of framebuffers containing uninitialized attachments. When a framebuffer (fboA) is bound as GL_READ_FRAMEBUFFER and a different framebuffer (fboB) is bound as GL_DRAW_FRAMEBUFFER, the decoder must temporarily rebind fboA to the GL_DRAW_FRAMEBUFFER target to execute the glClear operation.
However, this rebinding is performed after the call to PrepareDrawBuffersForClearingUninitializedAttachments(). Consequently, the underlying driver-level call to glDrawBuffersARB is executed while fboB is still bound to the driver’s GL_DRAW_FRAMEBUFFER target, instead of fboA. This results in two issues:
- The draw buffer state of
fboBis incorrectly modified/corrupted. - When
fboAis subsequently bound andglClearis executed, its active draw buffer state is still the client-defined state (which may be set toGL_NONEby the attacker). As a result, theglClearcall executes as a silent no-op.
Despite the clear operation failing to overwrite the uninitialized backing memory of the attachment, the decoder unconditionally marks the attachments of fboA as initialized via MarkAttachmentsAsCleared(). A subsequent read operation (such as glReadPixels) will then return uninitialized GPU memory contents back to the client.
Potential Trigger Path (Theoretical Analysis)
Note: Our tooling does not currently have the capability to run code, and the following steps represent a potential attack vector based on static code analysis.
- A WebGL2 page creates a renderbuffer (
rb) with an uninitialized backing store (e.g., usinggl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, 256, 256)). - The page attaches
rbto a framebufferfboAatGL_COLOR_ATTACHMENT0. - The page configures
fboAto disable drawing to the color attachment by callinggl.drawBuffers([gl.NONE]). - The page binds a separate framebuffer
fboBtoGL_DRAW_FRAMEBUFFER, and bindsfboAtoGL_READ_FRAMEBUFFER. - The page triggers a read operation (e.g.,
gl.readPixels) fromfboA, forcing a lazy clear check. - During
ClearUnclearedAttachments, the driver-sideGL_DRAW_FRAMEBUFFERis not rebound tofboAearly because the renderbuffer format is not an integer format. PrepareDrawBuffersForClearingUninitializedAttachmentsruns, invokingglDrawBuffersARBwhich applies to the active draw FBO (fboB).fboAis bound toGL_DRAW_FRAMEBUFFER, andglClearis executed. Because its driver-side draw buffer is stillGL_NONE, no clear occurs onrb.rbis marked as cleared, and the uninitialized contents ofrbare read and returned to the attacker’s buffer.
Impact
An attacker in a sandboxed WebGL context could exploit this vulnerability to read uninitialized GPU memory contents. In a shared GPU process environment, this could allow disclosure of sensitive cross-origin graphics data (such as canvas contents, textures, or video frames from other processes/tabs).
Suggested Remediation
To resolve this issue, the driver-side rebinding of the draw framebuffer should occur before evaluating or preparing the draw buffers for clearing.
Specifically, in GLES2DecoderImpl::ClearUnclearedAttachments, the rebinding logic should be consolidated and moved to the beginning of the function if target == GL_READ_FRAMEBUFFER and draw_framebuffer != framebuffer:
// Suggested Fix in gpu/command_buffer/service/gles2_cmd_decoder.cc
void GLES2DecoderImpl::ClearUnclearedAttachments(
GLenum target, Framebuffer* framebuffer) {
...
Framebuffer* draw_framebuffer = GetBoundDrawFramebuffer();
bool rebound_draw_framebuffer = false;
if (target == GL_READ_FRAMEBUFFER && draw_framebuffer != framebuffer) {
BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer->service_id());
rebound_draw_framebuffer = true;
}
...
This ensures that any subsequent draw buffer configuration or clear operation is executed against the correct target framebuffer.
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.