CVE-2026-12010
Overview
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*Imagefrom such a framebuffer and requires aGL_INVALID_OPERATIONerror. - Implicit-resolve attachment
- A multisampled texture/renderbuffer created via
glFramebufferTexture2DMultisampleEXTorglRenderbufferStorageMultisampleEXTthat 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.
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
- Create a true multisampled read framebuffer
From web content via WebGL, allocate a renderbuffer with
glRenderbufferStorageMultisampleCHROMIUM(samples > 1), attach it asGL_COLOR_ATTACHMENT0, and bind that framebuffer for reading, mirroringSetUpMultisampleRenderbufferFBOin the shipped regression test. - Trigger the clipped copy branch
Call
glCopyTexImage2D(routed toDoCopyTexImage2D(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 viaglTexImage2D, which succeeds on the driver. - Force the native copy to fail after resize
The follow-up
glCopyTexSubImage2Dis rejected by the driver withGL_INVALID_OPERATIONbecause the source is multisampled; the decoder therefore skips updating itsLevelInfo, leaving it recording the original large dimensions while the driver holds the smaller allocation (state desync per the commit message). - Drive an out-of-bounds write off the stale size
A later operation that relies on the decoder’s
LevelInfobounds 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
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
| Function | Change | Notes |
|---|---|---|
GL3Testgpu/command_buffer/tests/gl_unittest.cc |
modified | |
TEST_Fgpu/command_buffer/tests/gl_unittest.cc |
modified | |
GL3MultisampleCopyTexImageTestgpu/command_buffer/tests/gl_unittest.cc |
modified | |
ifgpu/command_buffer/tests/gl_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/framebuffer_manager.ccgpu/command_buffer/service/framebuffer_manager.hgpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/tests/gl_unittest.cc
Audit Directions
- Fail-after-mutate orderingLook for other decoder paths that mutate driver-side object state (e.g.,
glTexImage2Dredefinition) 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 validationAudit remaining
CopyTex*, blit, and read-pixels paths for absent checks on multisampled or otherwise illegal read framebuffers that the spec requires be rejected withGL_INVALID_OPERATION. - Metadata-vs-driver invariantsSearch 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.
Patch
From 396621520f68411c48fc1535bb48f553e77a645b Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
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 <kbr@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
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() {
Regression Test / PoC
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
Original Bug Report
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.ccgpu/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:
- It first calls
api()->glTexImage2DFn(...)to redefine the driver-side texture storage to the smaller, clipped dimensions. - 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:
- Create and bind a multisampled framebuffer to
GL_READ_FRAMEBUFFER(e.g., usingglRenderbufferStorageMultisample). - Create a texture and define its initial level 0 to have large dimensions (e.g., 1000x1000) using
glTexImage2D. The validating decoder registers these dimensions inLevelInfo. - Call
glCopyTexImage2Dtargeting 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. - During
DoCopyTexImage2Dexecution:- The native driver successfully processes
glTexImage2D, resizing the actual texture storage to 100x100. - The native driver fails the subsequent
glCopyTexSubImage2DwithGL_INVALID_OPERATIONdue to the multisampled framebuffer. - The decoder detects the error and skips
SetLevelInfo, leaving its internal metadata for the texture at 1000x1000.
- The native driver successfully processes
- Issue a
glTexSubImage2Dcommand to write a 500x500 pixel payload to the texture. - The validating decoder’s
TextureManager::ValidateTexSubImagechecks the bounds (500x500) against the stale metadata (1000x1000) and allows the command to pass. - 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.