CVE-2026-11087
Overview
Files Changed
extensions/CHROMIUM_copy_texture.txtsrc/libANGLE/ErrorStrings.hsrc/libANGLE/validationES2.cppsrc/tests/gl_tests/CopyTextureTest.cpp
Patch
From 2729ae1261b26b20019307c966f28a5df99d8313 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Tue, 14 Apr 2026 15:44:23 -0400
Subject: [PATCH] Disallow copies with the same source and dest mip
The validating command decoder disallows when sourceId == destId which
is more strict. ANGLE has the CopyTextureTestES3.CopyBetweenMips test to
verify that copies work within the same texture.
Bug: chromium:500140149
Change-Id: I3de9665fa812696a6af90c4ac2b58419b64674af
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7759465
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
diff --git a/extensions/CHROMIUM_copy_texture.txt b/extensions/CHROMIUM_copy_texture.txt
index f5f9e28..3de5cc8 100644
--- a/extensions/CHROMIUM_copy_texture.txt
+++ b/extensions/CHROMIUM_copy_texture.txt
@@ -125,6 +125,9 @@
INVALID_VALUE is generated if <sourceId> or <destId> are not valid texture
objects.
+ INVALID_OPERATION is generated if <sourceId> and <destId> refer to the same
+ texture objects and <sourceLevel> is the same as <destLevel>.
+
INVALID_ENUM is generated if <destTarget> is not one of the valid targets
described above.
diff --git a/src/libANGLE/ErrorStrings.h b/src/libANGLE/ErrorStrings.h
index 5fc0d6e..57f3546 100644
--- a/src/libANGLE/ErrorStrings.h
+++ b/src/libANGLE/ErrorStrings.h
@@ -389,6 +389,7 @@
inline constexpr const char *kInvalidShadingModel = "Invalid shading model.";
inline constexpr const char *kInvalidShadingRate = "Invalid shading rate.";
inline constexpr const char *kInvalidSourceTexture = "Source texture is not a valid texture object.";
+inline constexpr const char *kInvalidSourceTextureSameAsDestTexture = "Source texture and destination texture are the same.";
inline constexpr const char *kInvalidSourceTextureInternalFormat = "Source texture internal format is invalid.";
inline constexpr const char *kInvalidSourceTextureLevel = "Invalid source texture level.";
inline constexpr const char *kInvalidSourceTextureSize = "Invalid source texture height or width.";
diff --git a/src/libANGLE/validationES2.cpp b/src/libANGLE/validationES2.cpp
index ec176ba..3fa34ac 100644
--- a/src/libANGLE/validationES2.cpp
+++ b/src/libANGLE/validationES2.cpp
@@ -3375,6 +3375,12 @@
return false;
}
+ if (source == dest && sourceLevel == destLevel)
+ {
+ ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidSourceTextureSameAsDestTexture);
+ return false;
+ }
+
return true;
}
@@ -3517,6 +3523,12 @@
return false;
}
+ if (source == dest && sourceLevel == destLevel)
+ {
+ ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidSourceTextureSameAsDestTexture);
+ return false;
+ }
+
return true;
}
@@ -3571,6 +3583,12 @@
return false;
}
+ if (source == dest)
+ {
+ ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidSourceTextureSameAsDestTexture);
+ return false;
+ }
+
return true;
}
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index 0a7c97d..07d490c 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -941,6 +941,10 @@
false, false);
EXPECT_GL_ERROR(GL_INVALID_VALUE);
+ glCopyTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[0], 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, false, false, false);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
glCopyTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[1], 0, GL_RGBA,
GL_UNSIGNED_BYTE, false, false, false);
EXPECT_GL_NO_ERROR();
@@ -1038,6 +1042,10 @@
false);
EXPECT_GL_ERROR(GL_INVALID_VALUE);
+ glCopySubTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[0], 0, 1, 1, 0, 0, 1, 1,
+ false, false, false);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
glCopySubTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[1], 0, 1, 1, 0, 0, 1, 1,
false, false, false);
EXPECT_GL_NO_ERROR();
Regression Test / PoC
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index 0a7c97d..07d490c 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -941,6 +941,10 @@
false, false);
EXPECT_GL_ERROR(GL_INVALID_VALUE);
+ glCopyTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[0], 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, false, false, false);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
glCopyTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[1], 0, GL_RGBA,
GL_UNSIGNED_BYTE, false, false, false);
EXPECT_GL_NO_ERROR();
@@ -1038,6 +1042,10 @@
false);
EXPECT_GL_ERROR(GL_INVALID_VALUE);
+ glCopySubTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[0], 0, 1, 1, 0, 0, 1, 1,
+ false, false, false);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
glCopySubTextureCHROMIUM(mTextures[0], 0, GL_TEXTURE_2D, mTextures[1], 0, 1, 1, 0, 0, 1, 1,
false, false, false);
EXPECT_GL_NO_ERROR();
Original Bug Report
GPU memory leak in ANGLE GL backend via CopyTextureCHROMIUM self-copy
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 without the security team.
Overview: A potential vulnerability in ANGLE’s GL backend may allow a compromised renderer to disclose uninitialized GPU memory. By performing a self-copy using glCopyTextureCHROMIUM, the backend orphans the texture storage, replacing initialized memory with uninitialized driver memory. This bypasses robust resource initialization checks and allows for a cross-origin information leak.
Affected files:
third_party/angle/src/libANGLE/renderer/gl/TextureGL.cppthird_party/angle/src/libANGLE/validationES2.cppgpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
Estimated timestamp from git blame: 2025-09-04
Summary
A missing source and destination identity check in glCopyTextureCHROMIUM allows a compromised renderer to trick the ANGLE GL backend into disclosing uninitialized GPU memory. When a texture is copied to itself, the backend orphans the destination (and inadvertently the source, since they are the same), replacing it with uninitialized driver memory. The frontend then marks the texture as initialized, entirely bypassing robust resource initialization safeguards.
Vulnerability Details
The sequence of events leading to the potential leak is as follows:
- Missing Validation: Neither the passthrough command decoder (
gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc) nor ANGLE’s frontend validation (third_party/angle/src/libANGLE/validationES2.cpp) checks if the source and destination textures are the same forglCopyTextureCHROMIUM. (The legacy validating decoder previously rejected this). - Frontend Initialization Check: In
Texture::copyTexture(third_party/angle/src/libANGLE/Texture.cpp),source->ensureInitialized()is called. If the attacker previously initialized the texture, this returns immediately, and the frontend considers the source texture safe to read. - Backend Orphaning: The frontend delegates to the GL backend’s
TextureGL::copyTexture(third_party/angle/src/libANGLE/renderer/gl/TextureGL.cpp). This function unconditionally callsreserveTexImageToBeFilled(), which issues a nativeglTexImage2Dcommand with anullptrdata pointer. - Storage Replacement: The
nullptrargument orphans the existing texture storage and allocates new, uninitialized memory from the GPU driver. Since the source and destination point to the exact same native texture, the initialized source data is destroyed and replaced by this uninitialized memory. - State Corruption: The backend attempts to copy the uninitialized memory onto itself. Regardless of whether this native operation fails or copies garbage, execution returns to the frontend, which mistakenly updates the texture’s state to
InitState::Initialized(mState.setImageDesc(..., InitState::Initialized)). - Information Leak: The texture now contains uninitialized GPU memory (which may contain cross-origin data or compositor frames) but is incorrectly locked as initialized. Subsequent reads by the renderer via
glReadPixelsor shaders will successfully exfiltrate this memory without triggering robust clears.
Suggested Reproduction Steps
Note: These are potential steps as our tooling agent does not yet have the ability to run code to verify with a working proof of concept.
- From a compromised renderer, create a WebGL context using the passthrough decoder and ANGLE-on-GL backend.
- Create a texture
Tand explicitly initialize level 0 (e.g., viatexImage2Dwith dummy data) so the frontend marks it asInitState::Initialized. - Issue a raw
CopyTextureCHROMIUMcommand-buffer command setting bothsourceIdanddestIdtoT, and both levels to0. - Read back the texture data using
glReadPixels. The resulting data will contain uninitialized native GPU memory instead of the initialized dummy data.
Suggested Fix
Add a validation check in ValidateCopyTextureCHROMIUM (third_party/angle/src/libANGLE/validationES2.cpp) to reject calls where the source and destination texture objects and levels are identical, returning GL_INVALID_OPERATION. Additionally, consider adding a similar check in the passthrough command decoder (GLES2DecoderPassthroughImpl::DoCopyTextureCHROMIUM) to match the behavior of the legacy validating decoder.
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.