Critical patch-now ✔︎ verified CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow GPU
DescriptionHeap buffer overflow GPU
ComponentChromium
Sub-componentGPU
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Diff matches: early multisampled-read-FB rejection in all three copy paths. Caveat: fix/test live in the validating command decoder (test gates on enable_validating_command_decoder), so default-config reachability depends on that decoder being active.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker517531647
Fix commit396621520f68 (chromium/src) +250/-7
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-06-04 (public git)
Disclosed2026-06-11
Patch gap7 days — fix public in source before the advisory

Background

Validating command decoder
The GPU-process component (GLES2DecoderImpl) that services client GL commands, tracks its own copy of GL object state, and translates commands into native driver calls.
`LevelInfo` metadata
The decoder’s internal, per-texture-level record of dimensions and format, which must stay in sync with the actual driver-side texture allocation for later bounds checks to be correct.
Multisampled read framebuffer
A framebuffer whose read attachment stores more than one sample per pixel; the OpenGL ES spec forbids CopyTex*Image from such a framebuffer and requires a GL_INVALID_OPERATION error.
Implicit-resolve attachment
A multisampled texture/renderbuffer created via glFramebufferTexture2DMultisampleEXT or glRenderbufferStorageMultisampleEXT that resolves on the fly and is safe to copy from, in contrast to a true multisampled renderbuffer (glRenderbufferStorageMultisampleCHROMIUM).

Root Cause Analysis

Before the patch, the copy-to-texture entry points reached by the diff’s three hunks (named DoCopyTexImage2D, DoCopyTexSubImage2D, and DoCopyTexSubImage3D in the commit message; the enclosing names are not present in the @@ headers, so this mapping is (inferred) from the message) performed no early rejection of a multisampled read framebuffer. Per the commit message, in the clipped branch of DoCopyTexImage2D the decoder first redefined the driver-side texture to the smaller clipped size with glTexImage2D (which succeeded), then issued a glCopyTexSubImage2D that the driver rejected because the read framebuffer was multisampled. Because the native copy failed, the decoder skipped updating its LevelInfo metadata, so the decoder believed the texture was still the original large size while the driver had already shrunk it — a state desync that the commit message states enabled subsequent out-of-bounds writes.

The fix adds IsBoundReadFramebufferMultisampledRenderbuffer() and calls it at the top of all three copy paths, returning GL_INVALID_OPERATION and returning before any driver-side glTexImage2D redefinition can occur, so the decoder and driver never diverge. The new Framebuffer::GetReadBufferIsMultisampledRenderbuffer() narrows the check to true multisampled renderbuffer attachments (samples() > 1), deliberately allowing implicit-resolve texture attachments to keep working.

Key insight
The decoder treated a native copy failure as a no-op for its own bookkeeping, but the failure came after it had already resized the driver-side texture, leaving LevelInfo describing a larger buffer than actually exists; the fix rejects the illegal multisampled-source copy up front so the driver-side redefinition never happens and the two states cannot drift apart.

Attack Path

  1. Create a true multisampled read framebuffer From web content via WebGL, allocate a renderbuffer with glRenderbufferStorageMultisampleCHROMIUM (samples > 1), attach it as GL_COLOR_ATTACHMENT0, and bind that framebuffer for reading, mirroring SetUpMultisampleRenderbufferFBO in the shipped regression test.
  2. Trigger the clipped copy branch Call glCopyTexImage2D (routed to DoCopyTexImage2D (inferred)) with a copy region that forces the clipped path, causing the decoder to redefine the driver-side destination texture to the smaller clipped size via glTexImage2D, which succeeds on the driver.
  3. Force the native copy to fail after resize The follow-up glCopyTexSubImage2D is rejected by the driver with GL_INVALID_OPERATION because the source is multisampled; the decoder therefore skips updating its LevelInfo, leaving it recording the original large dimensions while the driver holds the smaller allocation (state desync per the commit message).
  4. Drive an out-of-bounds write off the stale size A later operation that relies on the decoder’s LevelInfo bounds writes past the shrunken driver-side allocation, producing the heap buffer overflow in the GPU process. (inferred) — the specific downstream sink, offset, and object are not shown in the diff.

Impact Assessment

An attacker running WebGL/GLES commands through the validating command decoder can desynchronize the decoder’s texture-size bookkeeping from the driver’s actual allocation, yielding a heap buffer overflow in the GPU process, as classified by the advisory (“Heap buffer overflow GPU”). Preconditions are a true multisampled renderbuffer bound as the read framebuffer and use of the clipped CopyTex*Image path; implicit-resolve texture attachments are explicitly unaffected. The advisory states no exploited-in-the-wild status; any escalation from the GPU-process heap overflow to memory corruption control, sandbox compromise, or code execution is (inferred) and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
GL3Test
gpu/command_buffer/tests/gl_unittest.cc
modified
TEST_F
gpu/command_buffer/tests/gl_unittest.cc
modified
GL3MultisampleCopyTexImageTest
gpu/command_buffer/tests/gl_unittest.cc
modified
if
gpu/command_buffer/tests/gl_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/framebuffer_manager.cc
  • gpu/command_buffer/service/framebuffer_manager.h
  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/tests/gl_unittest.cc

Audit Directions

  • Fail-after-mutate ordering
    Look for other decoder paths that mutate driver-side object state (e.g., glTexImage2D redefinition) before a native call that may fail, where the failure leaves the decoder’s tracked metadata stale relative to the driver.
  • Missing spec-mandated source validation
    Audit remaining CopyTex*, blit, and read-pixels paths for absent checks on multisampled or otherwise illegal read framebuffers that the spec requires be rejected with GL_INVALID_OPERATION.
  • Metadata-vs-driver invariants
    Search for places where LevelInfo/dimension bookkeeping is updated only on success, and confirm that on the error path neither side can end up describing a different allocation than the other.
From 396621520f68411c48fc1535bb48f553e77a645b Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <[email protected]>
Date: Thu, 04 Jun 2026 16:28:01 -0700
Subject: [PATCH] [gpu] Reject CopyTexImage from multisampled framebuffers

Validate that the read framebuffer is not multisampled in
DoCopyTexImage2D, DoCopyTexSubImage2D, and DoCopyTexSubImage3D.

Attempting to copy from a multisampled framebuffer is prohibited by the
OpenGL ES spec and generates a GL_INVALID_OPERATION error. Previously,
in the clipped branch of DoCopyTexImage2D, the validating decoder would
redefine the driver-side texture size using glTexImage2D (which
succeeds), but then fail the subsequent copy with glCopyTexSubImage2D.
The native error caused the decoder to skip updating its internal
LevelInfo metadata. This resulted in a state desync where the decoder
believed the texture was the original large size, but the driver had
resized it to the smaller clipped size, enabling subsequent out-of-
bounds writes.

Rejecting multisampled read framebuffers early prevents the driver-side
texture redefinition from occurring, keeping the decoder and driver
states in sync.

Bug: 517531647
Test: `gl_tests --use-cmd-decoder=validating` with GN arg enable_validating_command_decoder = true
Change-Id: Icf2772aacb013459de4a54082745fdec6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7898041
Reviewed-by: Kenneth Russell <[email protected]>
Auto-Submit: Sunny Sachanandani <[email protected]>
Commit-Queue: Sunny Sachanandani <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1642016}
---

diff --git a/gpu/command_buffer/service/framebuffer_manager.cc b/gpu/command_buffer/service/framebuffer_manager.cc
index dd15e290..4eaf5fc 100644
--- a/gpu/command_buffer/service/framebuffer_manager.cc
+++ b/gpu/command_buffer/service/framebuffer_manager.cc
@@ -699,6 +699,13 @@
              : false;
 }
 
+bool Framebuffer::GetReadBufferIsMultisampledRenderbuffer() const {
+  const Attachment* attachment = GetReadBufferAttachment();
+  return attachment ? attachment->IsRenderbufferAttachment() &&
+                          attachment->samples() > 1
+                    : false;
+}
+
 GLsizei Framebuffer::GetSamples() const {
   // Assume the framebuffer is complete, so return any attachment's samples.
   auto iter = attachments_.begin();
diff --git a/gpu/command_buffer/service/framebuffer_manager.h b/gpu/command_buffer/service/framebuffer_manager.h
index 847481f..cbc4319 100644
--- a/gpu/command_buffer/service/framebuffer_manager.h
+++ b/gpu/command_buffer/service/framebuffer_manager.h
@@ -175,6 +175,7 @@
   // returns 0.
   GLenum GetReadBufferTextureType() const;
   bool GetReadBufferIsMultisampledTexture() const;
+  bool GetReadBufferIsMultisampledRenderbuffer() const;
 
   // Verify all the rules in OpenGL ES 2.0.25 4.4.5 are followed.
   // Returns GL_FRAMEBUFFER_COMPLETE if there are no reasons we know we can't
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 8ff6bfd..4252a6e 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -913,6 +913,7 @@
   // If the color image is a renderbuffer, returns 0 for type.
   GLenum GetBoundReadFramebufferTextureType();
   GLenum GetBoundReadFramebufferInternalFormat();
+  bool IsBoundReadFramebufferMultisampledRenderbuffer();
 
   // Get the i-th draw buffer's internal format/type from the bound framebuffer.
   // If no framebuffer is bound, or no image is attached, or the DrawBuffers
@@ -4196,6 +4197,18 @@
   }
 }
 
+bool GLES2DecoderImpl::IsBoundReadFramebufferMultisampledRenderbuffer() {
+  Framebuffer* read_framebuffer = GetBoundReadFramebuffer();
+  // We check if the read framebuffer is a multisampled renderbuffer.
+  // We explicitly do NOT block multisampled texture attachments (with target
+  // GL_TEXTURE_2D, etc.) because they are implicit resolve textures (allocated
+  // via FramebufferTexture2DMultisampleEXT) and are safe to copy from (they are
+  // resolved on the fly). True multisampled textures
+  // (GL_TEXTURE_2D_MULTISAMPLE) are not supported by the validating decoder.
+  return read_framebuffer &&
+         read_framebuffer->GetReadBufferIsMultisampledRenderbuffer();
+}
+
 GLenum GLES2DecoderImpl::GetBoundColorDrawBufferType(GLint drawbuffer_i) {
   DCHECK(drawbuffer_i >= 0 &&
          drawbuffer_i < static_cast<GLint>(group_->max_draw_buffers()));
@@ -13320,6 +13333,12 @@
     return;
   }
 
+  if (IsBoundReadFramebufferMultisampledRenderbuffer()) {
+    LOCAL_SET_GL_ERROR(GL_INVALID_OPERATION, func_name,
+                       "cannot copy from a multisampled framebuffer");
+    return;
+  }
+
   GLenum read_format = GetBoundReadFramebufferInternalFormat();
   GLenum read_type = GetBoundReadFramebufferTextureType();
   if (!ValidateCopyTexFormat(func_name, internal_format,
@@ -13572,6 +13591,12 @@
     return;
   }
 
+  if (IsBoundReadFramebufferMultisampledRenderbuffer()) {
+    LOCAL_SET_GL_ERROR(GL_INVALID_OPERATION, func_name,
+                       "cannot copy from a multisampled framebuffer");
+    return;
+  }
+
   GLenum read_format = GetBoundReadFramebufferInternalFormat();
   GLenum read_type = GetBoundReadFramebufferTextureType();
   if (!ValidateCopyTexFormat(func_name, internal_format,
@@ -13703,6 +13728,12 @@
     return;
   }
 
+  if (IsBoundReadFramebufferMultisampledRenderbuffer()) {
+    LOCAL_SET_GL_ERROR(GL_INVALID_OPERATION, func_name,
+                       "cannot copy from a multisampled framebuffer");
+    return;
+  }
+
   GLenum read_format = GetBoundReadFramebufferInternalFormat();
   GLenum read_type = GetBoundReadFramebufferTextureType();
   if (!ValidateCopyTexFormat(func_name, internal_format,
diff --git a/gpu/command_buffer/tests/gl_unittest.cc b/gpu/command_buffer/tests/gl_unittest.cc
index ffcf252..2a3003a 100644
--- a/gpu/command_buffer/tests/gl_unittest.cc
+++ b/gpu/command_buffer/tests/gl_unittest.cc
@@ -4,6 +4,7 @@
 
 #include <GLES2/gl2.h>
 #include <GLES2/gl2ext.h>
+#include <GLES2/gl2extchromium.h>
 #include <GLES3/gl3.h>
 #include <stdint.h>
 
@@ -131,11 +132,7 @@
       reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION)));
 }
 
-// TODO(crbug.com/513543143): Goldfish GLES emulator driver on 32-bit x86
-// Android bots has a known driver bug where it incorrectly rejects
-// glCopyTexImage2D on cubemaps with GL_INVALID_ENUM.
-#if BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER) && \
-    !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+#if BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
 class GL3Test : public GLTest {
  protected:
   void SetUp() override {
@@ -155,7 +152,12 @@
 // instead of the specific face target (e.g., GL_TEXTURE_CUBE_MAP_POSITIVE_X)
 // which would cause driver-side GL_INVALID_ENUM errors and result in a state
 // desynchronization between the decoder and the GPU driver.
-TEST_F(GL3Test, CopyTexImage2DWorkaroundStateDesync) {
+//
+// TODO(crbug.com/513543143): Goldfish GLES emulator driver on 32-bit x86
+// Android bots has a known driver bug where it incorrectly rejects
+// glCopyTexImage2D on cubemaps with GL_INVALID_ENUM.
+#if !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+TEST_F(GL3Test, CopyTexImage2DCubeMapStateDesync) {
   GLuint tex = 0;
   glGenTextures(1, &tex);
   glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
@@ -202,6 +204,208 @@
   glDeleteFramebuffers(1, &fbo);
   glDeleteTextures(1, &tex);
 }
-#endif
+#endif  // !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+
+class GL3MultisampleCopyTexImageTest : public GL3Test {
+ protected:
+  void SetUp() override { GL3Test::SetUp(); }
+
+  void TearDown() override {
+    if (sample_fbo_) {
+      glDeleteFramebuffers(1, &sample_fbo_);
+    }
+    if (sample_tex_) {
+      glDeleteTextures(1, &sample_tex_);
+    }
+    if (sample_rb_) {
+      glDeleteRenderbuffers(1, &sample_rb_);
+    }
+    if (dest_tex_) {
+      glDeleteTextures(1, &dest_tex_);
+    }
+    GL3Test::TearDown();
+  }
+
+  void SetUpImplicitResolveTextureFBO() {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/tests/gl_unittest.cc b/gpu/command_buffer/tests/gl_unittest.cc
index ffcf252..2a3003a 100644
--- a/gpu/command_buffer/tests/gl_unittest.cc
+++ b/gpu/command_buffer/tests/gl_unittest.cc
@@ -4,6 +4,7 @@
 
 #include <GLES2/gl2.h>
 #include <GLES2/gl2ext.h>
+#include <GLES2/gl2extchromium.h>
 #include <GLES3/gl3.h>
 #include <stdint.h>
 
@@ -131,11 +132,7 @@
       reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION)));
 }
 
-// TODO(crbug.com/513543143): Goldfish GLES emulator driver on 32-bit x86
-// Android bots has a known driver bug where it incorrectly rejects
-// glCopyTexImage2D on cubemaps with GL_INVALID_ENUM.
-#if BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER) && \
-    !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+#if BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
 class GL3Test : public GLTest {
  protected:
   void SetUp() override {
@@ -155,7 +152,12 @@
 // instead of the specific face target (e.g., GL_TEXTURE_CUBE_MAP_POSITIVE_X)
 // which would cause driver-side GL_INVALID_ENUM errors and result in a state
 // desynchronization between the decoder and the GPU driver.
-TEST_F(GL3Test, CopyTexImage2DWorkaroundStateDesync) {
+//
+// TODO(crbug.com/513543143): Goldfish GLES emulator driver on 32-bit x86
+// Android bots has a known driver bug where it incorrectly rejects
+// glCopyTexImage2D on cubemaps with GL_INVALID_ENUM.
+#if !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+TEST_F(GL3Test, CopyTexImage2DCubeMapStateDesync) {
   GLuint tex = 0;
   glGenTextures(1, &tex);
   glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
@@ -202,6 +204,208 @@
   glDeleteFramebuffers(1, &fbo);
   glDeleteTextures(1, &tex);
 }
-#endif
+#endif  // !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+
+class GL3MultisampleCopyTexImageTest : public GL3Test {
+ protected:
+  void SetUp() override { GL3Test::SetUp(); }
+
+  void TearDown() override {
+    if (sample_fbo_) {
+      glDeleteFramebuffers(1, &sample_fbo_);
+    }
+    if (sample_tex_) {
+      glDeleteTextures(1, &sample_tex_);
+    }
+    if (sample_rb_) {
+      glDeleteRenderbuffers(1, &sample_rb_);
+    }
+    if (dest_tex_) {
+      glDeleteTextures(1, &dest_tex_);
+    }
+    GL3Test::TearDown();
+  }
+
+  void SetUpImplicitResolveTextureFBO() {
+    glGenTextures(1, &sample_tex_);
+    glBindTexture(GL_TEXTURE_2D, sample_tex_);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glGenFramebuffers(1, &sample_fbo_);
+    glBindFramebuffer(GL_FRAMEBUFFER, sample_fbo_);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                         GL_TEXTURE_2D, sample_tex_, 0, 4);
+    EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+              glCheckFramebufferStatus(GL_FRAMEBUFFER));
+    GLTestHelper::CheckGLError("SetUpImplicitResolveTextureFBO", __LINE__);
+  }
+
+  void SetUpMultisampleRenderbufferFBO() {
+    glGenRenderbuffers(1, &sample_rb_);
+    glBindRenderbuffer(GL_RENDERBUFFER, sample_rb_);
+    glRenderbufferStorageMultisampleCHROMIUM(GL_RENDERBUFFER, 4, GL_RGBA8, 16,
+                                             16);
+    glGenFramebuffers(1, &sample_fbo_);
+    glBindFramebuffer(GL_FRAMEBUFFER, sample_fbo_);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                              GL_RENDERBUFFER, sample_rb_);
+    EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+              glCheckFramebufferStatus(GL_FRAMEBUFFER));
+    GLTestHelper::CheckGLError("SetUpMultisampleRenderbufferFBO", __LINE__);
+  }
+
+  void SetUpImplicitResolveRenderbufferFBO() {
+    glGenRenderbuffers(1, &sample_rb_);
+    glBindRenderbuffer(GL_RENDERBUFFER, sample_rb_);
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 4, GL_RGBA8, 16, 16);
+    glGenFramebuffers(1, &sample_fbo_);
+    glBindFramebuffer(GL_FRAMEBUFFER, sample_fbo_);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                              GL_RENDERBUFFER, sample_rb_);
+    EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+              glCheckFramebufferStatus(GL_FRAMEBUFFER));
+    GLTestHelper::CheckGLError("SetUpImplicitResolveRenderbufferFBO", __LINE__);
+  }
+
+  void SetUpDestTexture2D() {
+    glGenTextures(1, &dest_tex_);
+    glBindTexture(GL_TEXTURE_2D, dest_tex_);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    GLTestHelper::CheckGLError("SetUpDestTexture2D", __LINE__);
+  }
+
+  void SetUpDestTexture3D() {
+    glGenTextures(1, &dest_tex_);
+    glBindTexture(GL_TEXTURE_3D, dest_tex_);
+    glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 16, 16, 16, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    GLTestHelper::CheckGLError("SetUpDestTexture3D", __LINE__);
+  }
+
+  void VerifyCopySucceeded(GLenum dest_target,
+                           int expected_width,
+                           int expected_height) {
+    EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+    VerifyTextureSize(dest_target, expected_width, expected_height);
+  }
+
+  void VerifyCopyBlocked(GLenum dest_target) {
+    EXPECT_EQ(static_cast<GLenum>(GL_INVALID_OPERATION), glGetError());
+    VerifyTextureSize(dest_target, 16, 16);
+  }
+
+  void VerifyTextureSize(GLenum target,
+                         int expected_width,
+                         int expected_height) {
+    int tracked_width = 0;
+    int tracked_height = 0;
+    bool defined = InspectTextureLevelSize(&gl_, dest_tex_, target, 0,
+                                           &tracked_width, &tracked_height);
+    EXPECT_TRUE(defined);
+    EXPECT_EQ(expected_width, tracked_width);
+    EXPECT_EQ(expected_height, tracked_height);
+  }
+
+  GLuint sample_fbo_ = 0;
+  GLuint sample_tex_ = 0;
+  GLuint sample_rb_ = 0;
+  GLuint dest_tex_ = 0;
+};
+
+// Implicit Resolve Texture
+TEST_F(GL3MultisampleCopyTexImageTest, CopyTexImage2DImplicitResolveTexture) {
+  if (!GLTestHelper::HasExtension("GL_EXT_multisample_render_to_texture")) {
+    GTEST_SKIP() << "GL_EXT_multisample_render_to_texture not supported";
+  }
+  SetUpImplicitResolveTextureFBO();
+  SetUpDestTexture2D();
+  glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 2, 2, 0);
+  VerifyCopySucceeded(GL_TEXTURE_2D, 2, 2);
+}
+TEST_F(GL3MultisampleCopyTexImageTest,
+       CopyTexSubImage2DImplicitResolveTexture) {
+  if (!GLTestHelper::HasExtension("GL_EXT_multisample_render_to_texture")) {
+    GTEST_SKIP() << "GL_EXT_multisample_render_to_texture not supported";
+  }
+  SetUpImplicitResolveTextureFBO();
+  SetUpDestTexture2D();
+  glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 2);
+  VerifyCopySucceeded(GL_TEXTURE_2D, 16, 16);
+}
+TEST_F(GL3MultisampleCopyTexImageTest,
+       CopyTexSubImage3DImplicitResolveTexture) {
+  if (!GLTestHelper::HasExtension("GL_EXT_multisample_render_to_texture")) {
+    GTEST_SKIP() << "GL_EXT_multisample_render_to_texture not supported";
+  }
+  SetUpImplicitResolveTextureFBO();
+  SetUpDestTexture3D();
+  glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 0, 0, 2, 2);
+  VerifyCopySucceeded(GL_TEXTURE_3D, 16, 16);
+}
+
+// Multisample Renderbuffer
+TEST_F(GL3MultisampleCopyTexImageTest, CopyTexImage2DMultisampleRenderbuffer) {
+  if (!GLTestHelper::HasExtension("GL_CHROMIUM_framebuffer_multisample")) {
+    GTEST_SKIP() << "GL_CHROMIUM_framebuffer_multisample not supported";
+  }
+  SetUpMultisampleRenderbufferFBO();
+  SetUpDestTexture2D();
+  glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 2, 2, 0);
+  VerifyCopyBlocked(GL_TEXTURE_2D);
+}
+TEST_F(GL3MultisampleCopyTexImageTest,
+       CopyTexSubImage2DMultisampleRenderbuffer) {
+  if (!GLTestHelper::HasExtension("GL_CHROMIUM_framebuffer_multisample")) {
+    GTEST_SKIP() << "GL_CHROMIUM_framebuffer_multisample not supported";
+  }
+  SetUpMultisampleRenderbufferFBO();
+  SetUpDestTexture2D();
+  glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 2);
+  VerifyCopyBlocked(GL_TEXTURE_2D);
+}
+TEST_F(GL3MultisampleCopyTexImageTest,
+       CopyTexSubImage3DMultisampleRenderbuffer) {
+  if (!GLTestHelper::HasExtension("GL_CHROMIUM_framebuffer_multisample")) {
+    GTEST_SKIP() << "GL_CHROMIUM_framebuffer_multisample not supported";
+  }
+  SetUpMultisampleRenderbufferFBO();
+  SetUpDestTexture3D();
+  glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 0, 0, 2, 2);
+  VerifyCopyBlocked(GL_TEXTURE_3D);
+}
+
+// Implicit Resolve Renderbuffer
+TEST_F(GL3MultisampleCopyTexImageTest,
+       CopyTexImage2DImplicitResolveRenderbuffer) {
+  if (!GLTestHelper::HasExtension("GL_EXT_multisample_render_to_texture")) {
+    GTEST_SKIP() << "GL_EXT_multisample_render_to_texture not supported";
+  }
+  SetUpImplicitResolveRenderbufferFBO();
+  SetUpDestTexture2D();
+  glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 2, 2, 0);
+  VerifyCopyBlocked(GL_TEXTURE_2D);
+}
+TEST_F(GL3MultisampleCopyTexImageTest,
+       CopyTexSubImage2DImplicitResolveRenderbuffer) {
+  if (!GLTestHelper::HasExtension("GL_EXT_multisample_render_to_texture")) {
+    GTEST_SKIP() << "GL_EXT_multisample_render_to_texture not supported";
+  }
+  SetUpImplicitResolveRenderbufferFBO();
+  SetUpDestTexture2D();
+  glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 2);
+  VerifyCopyBlocked(GL_TEXTURE_2D);
+}
+TEST_F(GL3MultisampleCopyTexImageTest,
+       CopyTexSubImage3DImplicitResolveRenderbuffer) {
+  if (!GLTestHelper::HasExtension("GL_EXT_multisample_render_to_texture")) {
+    GTEST_SKIP() << "GL_EXT_multisample_render_to_texture not supported";
+  }
+  SetUpImplicitResolveRenderbufferFBO();
+  SetUpDestTexture3D();
+  glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 0, 0, 2, 2);
+  VerifyCopyBlocked(GL_TEXTURE_3D);
+}
+#endif  // BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
 
 }  // namespace gpu
Loading diff…

Original Bug Report

reported by [email protected]

Potential state desync in DoCopyTexImage2D via multisampled framebuffer leads to OOB write

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 state desynchronization vulnerability exists in the GLES2 validating command decoder’s DoCopyTexImage2D implementation. When copying from a multisampled framebuffer, a native GL error during the sub-image copy prevents the decoder from updating its internal texture state, while the underlying driver-side texture is successfully redefined to a smaller size. This desynchronization can allow subsequent sub-image writes to bypass validation and trigger an out-of-bounds write in the GPU process.

Affected files:

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

Estimated timestamp from git blame: 2018-01-08

Description

A potential state desynchronization vulnerability has been identified in GLES2DecoderImpl::DoCopyTexImage2D within the GLES2 validating command decoder (gpu/command_buffer/service/gles2_cmd_decoder.cc).

When a client issues a glCopyTexImage2D command that requires coordinate clipping (such as when the requested copy coordinates or dimensions extend beyond the read framebuffer boundaries), the validating decoder enters a clipped execution branch. In this branch, it performs two native GPU driver calls:

  1. It first calls api()->glTexImage2DFn(...) to redefine the driver-side texture storage to the smaller, clipped dimensions.
  2. It then calls api()->glCopyTexSubImage2DFn(...) to copy the pixel region.

Under the OpenGL ES 3.0/3.1 specifications, calling glCopyTexSubImage2D is prohibited if the read framebuffer is multisampled (generating a GL_INVALID_OPERATION error). However, the preceding glTexImage2D allocation does not read from the framebuffer and thus succeeds without error. This results in a scenario where the driver-side texture has been resized to the smaller dimensions, but the subsequent copy operation fails.

At the end of DoCopyTexImage2D, the decoder checks for native GL errors via LOCAL_PEEK_GL_ERROR. Because the second call generated GL_INVALID_OPERATION, the error check fails (error != GL_NO_ERROR), causing the decoder to skip the call to texture_manager()->SetLevelInfo(...). Consequently, the decoder’s internal LevelInfo metadata retains the texture’s original, larger dimensions, while the underlying GPU driver’s texture level has successfully been redefined to the smaller size.

This state desynchronization allows subsequent sub-image writes (via glTexSubImage2D) to bypass the validating decoder’s bounds checks (which validate against the stale, larger LevelInfo metadata) and write past the actual, smaller driver-allocated texture bounds, resulting in an out-of-bounds heap write in the GPU process.

Note: Our tooling agent currently does not have the ability to run code or verify this with a live Proof of Concept (PoC). The vulnerability and steps below are based entirely on static code analysis of the active Chromium codebase.


Potential Steps to Reproduce / Exploit

An attacker in control of a compromised renderer process would potentially execute the following steps:

  1. Create and bind a multisampled framebuffer to GL_READ_FRAMEBUFFER (e.g., using glRenderbufferStorageMultisample).
  2. Create a texture and define its initial level 0 to have large dimensions (e.g., 1000x1000) using glTexImage2D. The validating decoder registers these dimensions in LevelInfo.
  3. Call glCopyTexImage2D targeting the newly created texture at level 0, specifying smaller dimensions (e.g., 100x100) and copy coordinates that extend outside the boundaries of the multisampled read framebuffer. This forces the decoder into the clipped fallback path.
  4. During DoCopyTexImage2D execution:
    • The native driver successfully processes glTexImage2D, resizing the actual texture storage to 100x100.
    • The native driver fails the subsequent glCopyTexSubImage2D with GL_INVALID_OPERATION due to the multisampled framebuffer.
    • The decoder detects the error and skips SetLevelInfo, leaving its internal metadata for the texture at 1000x1000.
  5. Issue a glTexSubImage2D command to write a 500x500 pixel payload to the texture.
  6. The validating decoder’s TextureManager::ValidateTexSubImage checks the bounds (500x500) against the stale metadata (1000x1000) and allows the command to pass.
  7. The native GPU driver attempts to write the 500x500 payload into the 100x100 buffer, resulting in an out-of-bounds heap write in the GPU process.

Proposed Fix

To prevent this state desynchronization, the validating command decoder should explicitly validate that the read framebuffer is not multisampled before executing any driver-side modifications in DoCopyTexImage2D and DoCopyTexSubImage2D:

In gpu/command_buffer/service/gles2_cmd_decoder.cc:

if (GetBoundFramebufferSamples(GL_READ_FRAMEBUFFER) > 0) {
  LOCAL_SET_GL_ERROR(
      GL_INVALID_OPERATION, func_name,
      "cannot copy from a multisampled framebuffer");
  return;
}

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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.

View on issue tracker