High watch CVSS 3.4 chrome Uninitialized Memory 🔧 Commit mapped

Overview

High
Severity
3.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in GPU
DescriptionUninitialized resource in GPU
ComponentGPU
Sub-componentGPU
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker559737160
Fix commit05b44a448820 (chromium/src) +290/-4
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-09-10 (per vendor advisory)
Fix landed2026-09-18 (public git)
Disclosed2026-09-29
Patch gap11 days — fix public in source before the advisory

Background

Emulated backbuffer
in the validating GLES2DecoderImpl, client framebuffer 0 is tracked as null but is backed in the GL driver by a non-zero service ID from GetBackbufferServiceId() (used for offscreen contexts and surfaces with backing FBOs), not raw driver FBO 0.
`ScopedBufferReattacher`
a scoped helper used under certain GPU driver-bug workarounds (reattach_fbo_depth_stencil_on_reallocation, reattach_texture_to_fbo_after_layer_increase) that temporarily detaches and later reattaches framebuffer attachments around an operation.
Lazy backbuffer clear (`backbuffer_needs_clear_bits_`)
pending “needs clear” bits that cause the decoder to zero-initialize the backbuffer on next use (e.g., via CheckFramebufferValid(nullptr)), then disarm the bits.
Binding restoration paths
DoBindFramebuffer, RestoreCurrentFramebufferBindings, and RestoreFramebufferBindings all map a null tracked binding to GetBackbufferServiceId(); ScopedBufferReattacher did not.

Root Cause Analysis

ScopedBufferReattacher::~ScopedBufferReattacher() restored a null tracked read/draw framebuffer binding by binding raw driver FBO 0 (GLuint service_id = old_read_fbo_ ? old_read_fbo_->service_id() : 0;) instead of GetBackbufferServiceId(). When the helper was triggered while client FBO 0 (the emulated backbuffer) was bound, its destructor left the driver bound to raw FBO 0 while the state tracker still believed the emulated backbuffer was current, desynchronizing driver state from tracked state. A subsequent operation such as CheckFramebufferValid(nullptr) would then issue the pending lazy backbuffer clears against driver FBO 0 rather than the actual backbuffer, while unconditionally disarming backbuffer_needs_clear_bits_ — so the real backbuffer was never cleared but was marked as if it had been. Separately, Initialize() bound detached FBO service IDs to remove attachments but returned without restoring the prior bindings, so for commands like DoCopyTexImage2D (which constructs the reattacher before CheckBoundReadFramebufferValid() and glCopyTexImage2D()) the detached FBO stayed bound during command execution.

The fix extracts ScopedBufferReattacher::RestoreBindings(), which maps null tracked read/draw FBOs to GetBackbufferServiceId(), and calls it at the end of both Initialize() (when attachments were detached) and the destructor, keeping the driver binding in sync with the tracked state so lazy clears land on the correct framebuffer.

Key insight
The single mistake was restoring a null tracked framebuffer binding to raw FBO 0 instead of the emulated backbuffer’s GetBackbufferServiceId(), which let the lazy backbuffer security clear be consumed (and disarmed) against the wrong framebuffer, leaving the backbuffer uninitialized; the shared RestoreBindings() helper fixes both the destructor and the premature return in Initialize().

Attack Path

  1. Enable a workaround path Run on a context where reattach_fbo_depth_stencil_on_reallocation or reattach_texture_to_fbo_after_layer_increase is active and an emulated backbuffer (non-zero backing FBO) is in use, with client FBO 0 bound.
  2. Trigger `ScopedBufferReattacher` Issue a command that reallocates/reattaches (e.g., a texture reallocation, or CopyTexImage2D on a cube-map face) so the reattacher runs while both read/draw are tracked-null.
  3. Desync the driver binding On Initialize() return or destruction, the driver is left bound to raw FBO 0 while the tracker believes the emulated backbuffer is bound.
  4. Consume the lazy clear against the wrong FBO A following validity check (CheckBoundReadFramebufferValid/CheckFramebufferValid(nullptr)) performs the pending backbuffer clear on FBO 0 and disarms backbuffer_needs_clear_bits_, so the emulated backbuffer is never zero-initialized yet is marked clean.
  5. Read uninitialized backbuffer contents A subsequent read/copy/draw of the backbuffer (inferred: e.g., readback or CopyTexImage2D sampling the backbuffer) exposes stale/undefined GPU memory.

Impact Assessment

An attacker able to drive WebGL/GLES commands under the affected driver workarounds can leave the emulated backbuffer uninitialized while it is marked cleared, then read back stale GPU memory, an information-disclosure primitive in the GPU process. The advisory classifies this as an uninitialized resource in GPU and reports no exploited-in-the-wild status; the specific readback sink is (inferred) from the decoder’s clear/validity mechanics rather than shown directly in the diff.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gles2_cmd_decoder.cc
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc

Audit Directions

  • Null-framebuffer to service-ID mapping
    Search for any code that treats a null tracked framebuffer as driver FBO 0 rather than GetBackbufferServiceId(); only the canonical restore paths handled this correctly before the fix.
  • Scoped helpers that leave driver state unrestored
    Audit RAII/scoped GL helpers whose Initialize/constructor changes bindings but whose early returns or destructors may not restore them, especially around lazy-clear and validity-check sinks.
  • Unconditional disarming of lazy-init bits
    Review paths that consume and clear *_needs_clear_bits_ (or similar lazy-initialization flags) to confirm the clear is applied to the same object the bits are then disarmed for.
From 05b44a44882003b14cf261a6d7ddb2c6a8b49259 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <[email protected]>
Date: Fri, 18 Sep 2026 16:23:51 -0700
Subject: [PATCH] gpu: Fix ScopedBufferReattacher backbuffer restoration

In the validating GLES2 decoder (GLES2DecoderImpl), client framebuffer
0 is tracked as null in bound_read_framebuffer / bound_draw_framebuffer,
but is backed in the GL driver by the emulated backbuffer service ID
returned by GetBackbufferServiceId() (non-zero for offscreen contexts
and surfaces with backing FBOs), not raw driver FBO 0.

All canonical framebuffer binding restoration paths (DoBindFramebuffer,
RestoreCurrentFramebufferBindings, RestoreFramebufferBindings) map null
tracked bindings to GetBackbufferServiceId(). However, the driver
workaround helper ScopedBufferReattacher had two issues:

1. Its destructor mapped null tracked framebuffer bindings to raw driver
   FBO 0 instead of GetBackbufferServiceId(). When triggered by an
   unbound FBO while client FBO 0 was bound (under workarounds
   reattach_fbo_depth_stencil_on_reallocation or
   reattach_texture_to_fbo_after_layer_increase), ~ScopedBufferReattacher
   left the driver bound to raw FBO 0 while the state tracker believed
   the emulated backbuffer was bound. Subsequent operations such as
   CheckFramebufferValid(nullptr) would then issue pending lazy
   backbuffer clears (backbuffer_needs_clear_bits_) against driver FBO 0
   instead of the backbuffer while unconditionally disarming the clear
   bits.
2. Initialize() bound detached FBO service IDs in the driver to detach
   textures/renderbuffers without restoring the previous FBO bindings
   before returning. For commands like DoCopyTexImage2D where
   ScopedBufferReattacher is constructed before
   CheckBoundReadFramebufferValid() and glCopyTexImage2D(), this left
   the detached FBO bound in the driver during command execution.

Fix both issues by extracting ScopedBufferReattacher::RestoreBindings(),
which maps null tracked read/draw FBOs to GetBackbufferServiceId(), and
calling it at the end of both Initialize() (when attachments are
detached) and ~ScopedBufferReattacher(). Add unit tests verifying that
both shared and separate read/draw emulated backbuffer bindings are
properly restored across Initialize(), CopyTexImage2D lazy clears, and
destruction.

Bug: 559737160
Change-Id: Id887b92bd2699ab2ae870db55d2e8a55059e3fa1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8422647
Commit-Queue: Zhenyao Mo <[email protected]>
Reviewed-by: Kenneth Russell <[email protected]>
Auto-Submit: Zhenyao Mo <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1701196}
---
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 2ddb9f5a..bd7ee010 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -2570,6 +2570,7 @@
   };
 
   void Initialize();
+  void RestoreBindings();
 
   raw_ptr<GLES2DecoderImpl> decoder_;
   raw_ptr<TextureRef> texture_ref_ = nullptr;
@@ -2675,6 +2676,10 @@
           GL_FRAMEBUFFER, attachment_point, GL_RENDERBUFFER, 0);
     }
   }
+
+  if (!saved_attachments_.empty()) {
+    RestoreBindings();
+  }
 }
 
 ScopedBufferReattacher::~ScopedBufferReattacher() {
@@ -2711,13 +2716,25 @@
     }
   }
 
-  // Restore bindings.
+  RestoreBindings();
+}
+
+void ScopedBufferReattacher::RestoreBindings() {
+  // Restore bindings. A null tracked framebuffer means the client has the
+  // default framebuffer bound, which on this decoder is the emulated
+  // backbuffer, not driver FBO 0 (see RestoreCurrentFramebufferBindings and
+  // RestoreFramebufferBindings). Restoring raw FBO 0 here desyncs the driver
+  // binding from the tracked state and lets the pending backbuffer clear
+  // (backbuffer_needs_clear_bits_) be consumed against the wrong framebuffer.
   if (old_read_fbo_ == old_draw_fbo_) {
-    GLuint service_id = old_read_fbo_ ? old_read_fbo_->service_id() : 0;
+    GLuint service_id = old_read_fbo_ ? old_read_fbo_->service_id()
+                                      : decoder_->GetBackbufferServiceId();
     decoder_->api()->glBindFramebufferEXTFn(GL_FRAMEBUFFER, service_id);
   } else {
-    GLuint read_id = old_read_fbo_ ? old_read_fbo_->service_id() : 0;
-    GLuint draw_id = old_draw_fbo_ ? old_draw_fbo_->service_id() : 0;
+    GLuint read_id = old_read_fbo_ ? old_read_fbo_->service_id()
+                                   : decoder_->GetBackbufferServiceId();
+    GLuint draw_id = old_draw_fbo_ ? old_draw_fbo_->service_id()
+                                   : decoder_->GetBackbufferServiceId();
     decoder_->api()->glBindFramebufferEXTFn(GL_READ_FRAMEBUFFER, read_id);
     decoder_->api()->glBindFramebufferEXTFn(GL_DRAW_FRAMEBUFFER, draw_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 e3efc70..a8fee822 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -4369,6 +4369,275 @@
   }
 }
 
+TEST_P(GLES3DecoderManualInitTest,
+       ScopedBufferReattacherRestoresEmulatedBackbuffer) {
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  GpuDriverBugWorkarounds workarounds;
+  workarounds.reattach_fbo_depth_stencil_on_reallocation = true;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLuint kEmulatedBackbufferServiceId = 7;
+  scoped_refptr<GLSurfaceMock> other_surface(new GLSurfaceMock);
+  EXPECT_CALL(*other_surface.get(), GetBackingFramebufferObject())
+      .WillRepeatedly(Return(kEmulatedBackbufferServiceId));
+  EXPECT_CALL(*other_surface.get(), GetSize())
+      .WillRepeatedly(Return(gfx::Size(kBackBufferWidth, kBackBufferHeight)));
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  decoder_->SetSurface(other_surface);
+
+  // Attach depth texture to an FBO, then bind client FBO 0 (emulated
+  // backbuffer).
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+  DoTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 4, 4, 0,
+               GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, 0, 0);
+  DoBindFramebuffer(GL_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+  DoFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
+                         client_texture_id_, kServiceTextureId, 0, GL_NO_ERROR);
+  DoBindFramebuffer(GL_FRAMEBUFFER, 0, kEmulatedBackbufferServiceId);
+
+  // Case 1: Both read and draw FBOs are tracked-null (client FBO 0).
+  // Reallocating the depth texture must restore kEmulatedBackbufferServiceId
+  // (not raw FBO 0) in both Initialize() and ~ScopedBufferReattacher().
+  // This is testing the implementation of cmds::TexImage2D.
+  {
+    ::testing::InSequence sequence;
+    // Initialize() detaches from kServiceFramebufferId and restores backbuffer.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, 0, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    // TexImage2D executes.
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 8, 8,
+                                 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, _))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+
+    // ~ScopedBufferReattacher() reattaches and restores backbuffer.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, kServiceTextureId, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    cmds::TexImage2D cmd;
+    cmd.Init(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 8, 8, GL_DEPTH_COMPONENT,
+             GL_UNSIGNED_SHORT, 0, 0);
+    EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+    EXPECT_EQ(GL_NO_ERROR, GetGLError());
+  }
+
+  // Case 2: Separate read and draw bindings (read = client FBO 0, draw = user
+  // FBO). Must restore GL_READ_FRAMEBUFFER to kEmulatedBackbufferServiceId and
+  // GL_DRAW_FRAMEBUFFER to kServiceFramebufferId.
+  // This is testing the implementation of cmds::TexImage2D.
+  DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+  {
+    ::testing::InSequence sequence;
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, 0, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                         kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 4, 4,
+                                 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, _))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, kServiceTextureId, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                         kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    cmds::TexImage2D cmd;
+    cmd.Init(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 4, 4, GL_DEPTH_COMPONENT,
+             GL_UNSIGNED_SHORT, 0, 0);
+    EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+    EXPECT_EQ(GL_NO_ERROR, GetGLError());
+  }
+}
+
+TEST_P(GLES3DecoderManualInitTest,
+       ScopedBufferReattacherCopyTexImage2DPreservesBackbufferBinding) {
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  init.has_alpha = true;
+  GpuDriverBugWorkarounds workarounds;
+  workarounds.reattach_texture_to_fbo_after_layer_increase = true;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLuint kEmulatedBackbufferServiceId = 7;
+  scoped_refptr<GLSurfaceMock> other_surface(new GLSurfaceMock);
+  EXPECT_CALL(*other_surface.get(), GetBackingFramebufferObject())
+      .WillRepeatedly(Return(kEmulatedBackbufferServiceId));
+  EXPECT_CALL(*other_surface.get(), GetSize())
+      .WillRepeatedly(Return(gfx::Size(kBackBufferWidth, kBackBufferHeight)));
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  decoder_->SetSurface(other_surface);
+
+  // Attach a cube map texture face to an unbound FBO.
+  DoBindTexture(GL_TEXTURE_CUBE_MAP, client_texture_id_, kServiceTextureId);
+  DoTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, 4, 4, 0, GL_RGBA,
+               GL_UNSIGNED_BYTE, 0, 0);
+  DoBindFramebuffer(GL_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+  DoFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                         GL_TEXTURE_CUBE_MAP_POSITIVE_X, client_texture_id_,
+                         kServiceTextureId, 0, GL_NO_ERROR);
+  DoBindFramebuffer(GL_FRAMEBUFFER, 0, kEmulatedBackbufferServiceId);
+
+  // Invalidate client FBO 0 so that backbuffer_needs_clear_bits_ is armed.
+  const GLsizei count = 1;
+  GLenum attachments[] = {GL_COLOR_EXT};
+  EXPECT_CALL(*gl_, InvalidateFramebuffer(GL_FRAMEBUFFER, count, _)).Times(0);
+  auto& invalidate_cmd =
+      *GetImmediateAs<cmds::InvalidateFramebufferImmediate>();
+  invalidate_cmd.Init(GL_FRAMEBUFFER, count, attachments);
+  EXPECT_EQ(error::kNoError,
+            ExecuteImmediateCmd(invalidate_cmd, sizeof(attachments)));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  // Calling CopyTexImage2D on the cube map face triggers ScopedBufferReattacher
+  // in Initialize(). It must restore kEmulatedBackbufferServiceId before
+  // CheckBoundReadFramebufferValid runs the lazy backbuffer security clear.
+  {
+    ::testing::InSequence sequence;
+    // 1. Initialize() detaches from kServiceFramebufferId and restores
+    // kEmulatedBackbufferServiceId.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                        GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    // 2. CheckBoundReadFramebufferValid performs the lazy backbuffer clear
+    // (while kEmulatedBackbufferServiceId is bound).
+    EXPECT_CALL(*gl_, ClearColor(0, 0, 0, 1.0f)).Times(1).RetiresOnSaturation();
+    SetupExpectationsForColorMask(true, true, true, true);
+    EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+    SetupExpectationsForStencilMask(GLES2Decoder::kDefaultStencilMask,
+                                    GLES2Decoder::kDefaultStencilMask);
+    EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+    SetupExpectationsForDepthMask(true);
+    SetupExpectationsForEnableDisable(GL_SCISSOR_TEST, false);
+    EXPECT_CALL(*gl_, Clear(GL_COLOR_BUFFER_BIT))
+        .Times(1)
+        .RetiresOnSaturation();
+    SetupExpectationsForRestoreClearState(0.0f, 0.0f, 0.0f, 0.0f, 0, 1.0f,
+                                          false, 0, 0, kBackBufferWidth,
+                                          kBackBufferHeight);
+
+    // 3. CopyTexImage2D executes against the backbuffer.
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, CopyTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0,
+                                     GL_RGBA8, 0, 0, 4, 4, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+
+    // 4. ~ScopedBufferReattacher() reattaches and restores
+    // kEmulatedBackbufferServiceId.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, FramebufferTexture2DEXT(
+                          GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                          GL_TEXTURE_CUBE_MAP_POSITIVE_X, kServiceTextureId, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    cmds::CopyTexImage2D cmd;
+    cmd.Init(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, 0, 0, 4, 4);
+    EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+    EXPECT_EQ(GL_NO_ERROR, GetGLError());
+  }
+}
+
 // TODO(gman): PixelStorei
 
 }  // namespace gles2
Loading diff…

Regression Test / PoC

shipped with the fix
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 e3efc70..a8fee822 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -4369,6 +4369,275 @@
   }
 }
 
+TEST_P(GLES3DecoderManualInitTest,
+       ScopedBufferReattacherRestoresEmulatedBackbuffer) {
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  GpuDriverBugWorkarounds workarounds;
+  workarounds.reattach_fbo_depth_stencil_on_reallocation = true;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLuint kEmulatedBackbufferServiceId = 7;
+  scoped_refptr<GLSurfaceMock> other_surface(new GLSurfaceMock);
+  EXPECT_CALL(*other_surface.get(), GetBackingFramebufferObject())
+      .WillRepeatedly(Return(kEmulatedBackbufferServiceId));
+  EXPECT_CALL(*other_surface.get(), GetSize())
+      .WillRepeatedly(Return(gfx::Size(kBackBufferWidth, kBackBufferHeight)));
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  decoder_->SetSurface(other_surface);
+
+  // Attach depth texture to an FBO, then bind client FBO 0 (emulated
+  // backbuffer).
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+  DoTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 4, 4, 0,
+               GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, 0, 0);
+  DoBindFramebuffer(GL_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+  DoFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
+                         client_texture_id_, kServiceTextureId, 0, GL_NO_ERROR);
+  DoBindFramebuffer(GL_FRAMEBUFFER, 0, kEmulatedBackbufferServiceId);
+
+  // Case 1: Both read and draw FBOs are tracked-null (client FBO 0).
+  // Reallocating the depth texture must restore kEmulatedBackbufferServiceId
+  // (not raw FBO 0) in both Initialize() and ~ScopedBufferReattacher().
+  // This is testing the implementation of cmds::TexImage2D.
+  {
+    ::testing::InSequence sequence;
+    // Initialize() detaches from kServiceFramebufferId and restores backbuffer.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, 0, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    // TexImage2D executes.
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 8, 8,
+                                 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, _))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+
+    // ~ScopedBufferReattacher() reattaches and restores backbuffer.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, kServiceTextureId, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    cmds::TexImage2D cmd;
+    cmd.Init(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 8, 8, GL_DEPTH_COMPONENT,
+             GL_UNSIGNED_SHORT, 0, 0);
+    EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+    EXPECT_EQ(GL_NO_ERROR, GetGLError());
+  }
+
+  // Case 2: Separate read and draw bindings (read = client FBO 0, draw = user
+  // FBO). Must restore GL_READ_FRAMEBUFFER to kEmulatedBackbufferServiceId and
+  // GL_DRAW_FRAMEBUFFER to kServiceFramebufferId.
+  // This is testing the implementation of cmds::TexImage2D.
+  DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+  {
+    ::testing::InSequence sequence;
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, 0, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                         kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 4, 4,
+                                 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, _))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                        GL_TEXTURE_2D, kServiceTextureId, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                         kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    cmds::TexImage2D cmd;
+    cmd.Init(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 4, 4, GL_DEPTH_COMPONENT,
+             GL_UNSIGNED_SHORT, 0, 0);
+    EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+    EXPECT_EQ(GL_NO_ERROR, GetGLError());
+  }
+}
+
+TEST_P(GLES3DecoderManualInitTest,
+       ScopedBufferReattacherCopyTexImage2DPreservesBackbufferBinding) {
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  init.has_alpha = true;
+  GpuDriverBugWorkarounds workarounds;
+  workarounds.reattach_texture_to_fbo_after_layer_increase = true;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLuint kEmulatedBackbufferServiceId = 7;
+  scoped_refptr<GLSurfaceMock> other_surface(new GLSurfaceMock);
+  EXPECT_CALL(*other_surface.get(), GetBackingFramebufferObject())
+      .WillRepeatedly(Return(kEmulatedBackbufferServiceId));
+  EXPECT_CALL(*other_surface.get(), GetSize())
+      .WillRepeatedly(Return(gfx::Size(kBackBufferWidth, kBackBufferHeight)));
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER,
+                                       kEmulatedBackbufferServiceId))
+      .Times(1)
+      .RetiresOnSaturation();
+  decoder_->SetSurface(other_surface);
+
+  // Attach a cube map texture face to an unbound FBO.
+  DoBindTexture(GL_TEXTURE_CUBE_MAP, client_texture_id_, kServiceTextureId);
+  DoTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, 4, 4, 0, GL_RGBA,
+               GL_UNSIGNED_BYTE, 0, 0);
+  DoBindFramebuffer(GL_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+  DoFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                         GL_TEXTURE_CUBE_MAP_POSITIVE_X, client_texture_id_,
+                         kServiceTextureId, 0, GL_NO_ERROR);
+  DoBindFramebuffer(GL_FRAMEBUFFER, 0, kEmulatedBackbufferServiceId);
+
+  // Invalidate client FBO 0 so that backbuffer_needs_clear_bits_ is armed.
+  const GLsizei count = 1;
+  GLenum attachments[] = {GL_COLOR_EXT};
+  EXPECT_CALL(*gl_, InvalidateFramebuffer(GL_FRAMEBUFFER, count, _)).Times(0);
+  auto& invalidate_cmd =
+      *GetImmediateAs<cmds::InvalidateFramebufferImmediate>();
+  invalidate_cmd.Init(GL_FRAMEBUFFER, count, attachments);
+  EXPECT_EQ(error::kNoError,
+            ExecuteImmediateCmd(invalidate_cmd, sizeof(attachments)));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  // Calling CopyTexImage2D on the cube map face triggers ScopedBufferReattacher
+  // in Initialize(). It must restore kEmulatedBackbufferServiceId before
+  // CheckBoundReadFramebufferValid runs the lazy backbuffer security clear.
+  {
+    ::testing::InSequence sequence;
+    // 1. Initialize() detaches from kServiceFramebufferId and restores
+    // kEmulatedBackbufferServiceId.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_,
+                FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                        GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    // 2. CheckBoundReadFramebufferValid performs the lazy backbuffer clear
+    // (while kEmulatedBackbufferServiceId is bound).
+    EXPECT_CALL(*gl_, ClearColor(0, 0, 0, 1.0f)).Times(1).RetiresOnSaturation();
+    SetupExpectationsForColorMask(true, true, true, true);
+    EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+    SetupExpectationsForStencilMask(GLES2Decoder::kDefaultStencilMask,
+                                    GLES2Decoder::kDefaultStencilMask);
+    EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+    SetupExpectationsForDepthMask(true);
+    SetupExpectationsForEnableDisable(GL_SCISSOR_TEST, false);
+    EXPECT_CALL(*gl_, Clear(GL_COLOR_BUFFER_BIT))
+        .Times(1)
+        .RetiresOnSaturation();
+    SetupExpectationsForRestoreClearState(0.0f, 0.0f, 0.0f, 0.0f, 0, 1.0f,
+                                          false, 0, 0, kBackBufferWidth,
+                                          kBackBufferHeight);
+
+    // 3. CopyTexImage2D executes against the backbuffer.
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, CopyTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0,
+                                     GL_RGBA8, 0, 0, 4, 4, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
+
+    // 4. ~ScopedBufferReattacher() reattaches and restores
+    // kEmulatedBackbufferServiceId.
+    EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kServiceFramebufferId))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, FramebufferTexture2DEXT(
+                          GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                          GL_TEXTURE_CUBE_MAP_POSITIVE_X, kServiceTextureId, 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kEmulatedBackbufferServiceId))
+        .Times(1)
+        .RetiresOnSaturation();
+
+    cmds::CopyTexImage2D cmd;
+    cmd.Init(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, 0, 0, 4, 4);
+    EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+    EXPECT_EQ(GL_NO_ERROR, GetGLError());
+  }
+}
+
 // TODO(gman): PixelStorei
 
 }  // namespace gles2
Loading diff…