Chrome · ANGLE
CVE-2026-87621
OOB in ANGLE
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/libANGLE/renderer/d3d/TextureD3D.cpp
Patch
From c477cf31f7a97abee4ce05a16b68ac6f1a852707 Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Tue, 18 Aug 2026 15:46:46 -0400
Subject: [PATCH] D3D: Reject out-of-storage mip levels in completeness check
Check creationLevels in TextureD3D isLevelComplete to prevent
out-of-storage mip levels from being treated as complete, which routes
copyTexture to the staging fallback and avoids invalid GPU subresource
access.
Test: angle_end2end_tests --gtest_filter="*DestLevelPastStorageMips*"
Bug: b/536664909
Change-Id: I8e4bbbbd4cfd234df06b9fcc81c7eb689006da2e
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8263283
Commit-Queue: Ran Wang <wangra@google.com>
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index ffc3a84..4b82538 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -209,7 +209,7 @@
GLint TextureD3D::getLevelZeroDepth() const
{
- return getBaseLevelDepth();
+ return 1;
}
GLint TextureD3D::getBaseLevelWidth() const
@@ -508,6 +508,45 @@
}
}
+bool TextureD3D::isValidLevel(int level) const
+{
+ return mTexStorage && level >= 0 && level < mTexStorage->getLevelCount();
+}
+
+bool TextureD3D::isLevelComplete(int level) const
+{
+ if (isImmutable())
+ {
+ return isValidLevel(level);
+ }
+
+ GLsizei width = getLevelZeroWidth();
+ GLsizei height = getLevelZeroHeight();
+ GLsizei depth = getLevelZeroDepth();
+
+ if (width <= 0 || height <= 0 || depth <= 0)
+ {
+ return false;
+ }
+
+ if (level >= creationLevels(width, height, depth))
+ {
+ return false;
+ }
+
+ if (level == static_cast<int>(getBaseLevel()))
+ {
+ return true;
+ }
+
+ return isNonBaseLevelComplete(level);
+}
+
+bool TextureD3D::isNonBaseLevelComplete(int level) const
+{
+ return false;
+}
+
TextureStorage *TextureD3D::getStorage()
{
ASSERT(mTexStorage);
@@ -1558,32 +1597,8 @@
return mTexStorage->getRenderTarget(context, index, outRT);
}
-bool TextureD3D_2D::isValidLevel(int level) const
+bool TextureD3D_2D::isNonBaseLevelComplete(int level) const
{
- return (mTexStorage ? (level >= 0 && level < mTexStorage->getLevelCount()) : false);
-}
-
-bool TextureD3D_2D::isLevelComplete(int level) const
-{
- if (isImmutable())
- {
- return true;
- }
-
- GLsizei width = getLevelZeroWidth();
- GLsizei height = getLevelZeroHeight();
-
- if (width <= 0 || height <= 0)
- {
- return false;
- }
-
- // The base image level is complete if the width and height are positive
- if (level == static_cast<int>(getBaseLevel()))
- {
- return true;
- }
-
ASSERT(level >= 0 && level <= static_cast<int>(mImageArray.size()) &&
mImageArray[level] != nullptr);
ImageD3D *image = mImageArray[level].get();
@@ -1593,12 +1608,12 @@
return false;
}
- if (image->getWidth() != std::max(1, width >> level))
+ if (image->getWidth() != std::max(1, getLevelZeroWidth() >> level))
{
return false;
}
- if (image->getHeight() != std::max(1, height >> level))
+ if (image->getHeight() != std::max(1, getLevelZeroHeight() >> level))
{
return false;
}
@@ -2004,7 +2019,7 @@
ASSERT(size.width == size.height);
- if (size.width > 0 && isValidFaceLevel(faceIndex, index.getLevelIndex()))
+ if (size.width > 0 && isValidLevel(index.getLevelIndex()))
{
ANGLE_TRY(updateStorageFaceLevel(context, faceIndex, index.getLevelIndex()));
ANGLE_TRY(mRenderer->copyImageCube(context, source, clippedArea, internalFormat,
@@ -2043,7 +2058,7 @@
else
{
ANGLE_TRY(ensureRenderTarget(context));
- if (isValidFaceLevel(faceIndex, index.getLevelIndex()))
+ if (isValidLevel(index.getLevelIndex()))
{
ANGLE_TRY(updateStorageFaceLevel(context, faceIndex, index.getLevelIndex()));
ANGLE_TRY(mRenderer->copyImageCube(
@@ -2084,7 +2099,7 @@
{
ANGLE_TRY(ensureRenderTarget(context));
- ASSERT(isValidFaceLevel(faceIndex, index.getLevelIndex()));
+ ASSERT(isValidLevel(index.getLevelIndex()));
ANGLE_TRY(updateStorageFaceLevel(context, faceIndex, index.getLevelIndex()));
ANGLE_TRY(mRenderer->copyTexture(context, source, sourceLevel.get(), gl::TextureTarget::_2D,
@@ -2132,7 +2147,7 @@
if (!isSRGB(index.getLevelIndex(), faceIndex) && canCreateRenderTargetForImage(index))
{
ANGLE_TRY(ensureRenderTarget(context));
- ASSERT(isValidFaceLevel(faceIndex, index.getLevelIndex()));
+ ASSERT(isValidLevel(index.getLevelIndex()));
ANGLE_TRY(updateStorageFaceLevel(context, faceIndex, index.getLevelIndex()));
const gl::InternalFormat &internalFormatInfo =
@@ -2387,11 +2402,6 @@
return angle::Result::Continue;
}
-bool TextureD3D_Cube::isValidFaceLevel(int faceIndex, int level) const
-{
- return (mTexStorage ? (level >= 0 && level < mTexStorage->getLevelCount()) : 0);
-}
-
bool TextureD3D_Cube::isFaceLevelComplete(int faceIndex, int level) const
{
if (getBaseLevel() >= gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS)
@@ -2404,12 +2414,11 @@
if (isImmutable())
{
- return true;
+ return isValidLevel(level);
}
int levelZeroSize = getLevelZeroWidth();
-
- if (levelZeroSize <= 0)
+ if (levelZeroSize <= 0 || level >= creationLevels(levelZeroSize, levelZeroSize, 1))
{
return false;
}
@@ -3067,35 +3076,11 @@
return angle::Result::Continue;
}
-bool TextureD3D_3D::isValidLevel(int level) const
-{
- return (mTexStorage ? (level >= 0 && level < mTexStorage->getLevelCount()) : 0);
-}
-
-bool TextureD3D_3D::isLevelComplete(int level) const
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 5bdd2f5..d45ae91 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -41,6 +41,12 @@
532617619 OPENGL : MipmapRobustInitTestES3.GenerateMipmapRobustInitOptimization*/* = SKIP
532617619 GLES : MipmapRobustInitTestES3.GenerateMipmapRobustInitOptimization*/* = SKIP
+// Redefining base level on native OpenGL/GLES drivers drops existing higher mip levels.
+480069042 OPENGL : CopyTextureTest.CubeMapDestLevelPastStorageMips/* = SKIP
+480069042 OPENGL : CopyTextureTest.Texture2DDestLevelPastStorageMips/* = SKIP
+480069042 GLES : CopyTextureTest.CubeMapDestLevelPastStorageMips/* = SKIP
+480069042 GLES : CopyTextureTest.Texture2DDestLevelPastStorageMips/* = SKIP
+
381742474 : ShaderStorageBufferTest31.ExceedMaxShaderStorageBlockSize/* = SKIP
// Vulkan color attachment incorrectly configured when GL_RASTERIZER_DISCARD is enabled and no glClear is issued.
@@ -2605,6 +2611,8 @@
438268609 WGPU : CopyTextureTest.InternalFormat/* = SKIP
// WGPU does not currently resize textures
438268609 WGPU : CopyTextureTest.CopyOutsideMipmap/* = SKIP
+438268609 WGPU : CopyTextureTest.CubeMapDestLevelPastStorageMips/* = SKIP
+438268609 WGPU : CopyTextureTest.Texture2DDestLevelPastStorageMips/* = SKIP
// Unimplemented format conversions for texture copies
438268609 WGPU : CopyTextureVariationsTest.Copy*Texture/ES2_WebGPU__AToRGB* = SKIP
438268609 WGPU : CopyTextureVariationsTest.Copy*Texture/ES2_WebGPU__LToRGB* = SKIP
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index d8293b4..a4bf8a7 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -1376,6 +1376,110 @@
EXPECT_GL_NO_ERROR();
}
+// Test copying to a cube map mip level beyond the current storage mip chain, then redefining base
+// level to promote the staged image.
+TEST_P(CopyTextureTest, CubeMapDestLevelPastStorageMips)
+{
+ if (!checkExtensions())
+ {
+ return;
+ }
+
+ ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 &&
+ !IsGLExtensionEnabled("GL_OES_fbo_render_mipmap"));
+
+ GLTexture src;
+ glBindTexture(GL_TEXTURE_2D, src);
+ GLColor srcPix = GLColor::red;
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &srcPix);
+ ASSERT_GL_NO_ERROR();
+
+ GLTexture cube;
+ glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
+ std::vector<GLubyte> base(1 * 1 * 4, 0x11);
+ for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
+ ++face)
+ {
+ glTexImage2D(face, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, base.data());
+ }
+ ASSERT_GL_NO_ERROR();
+
+ // Copy into level 1 of NEGATIVE_Z face (1x1), which lies beyond the 1-level mip chain of 1x1
+ // base.
+ glCopyTextureCHROMIUM(src, 0, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, cube,
+ /*destLevel=*/1, GL_RGBA, GL_UNSIGNED_BYTE,
+ /*unpackFlipY=*/GL_FALSE, /*premultiply=*/GL_FALSE,
+ /*unmultiply=*/GL_FALSE);
+ EXPECT_GL_NO_ERROR();
+
+ // Also define level 1 for the remaining 5 faces so the cube map is mipmap complete.
+ for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face < GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
+ ++face)
+ {
+ glTexImage2D(face, 1, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, base.data());
+ }
+ EXPECT_GL_NO_ERROR();
+
+ // Redefine base level to 2x2 so level 1 (1x1) becomes part of the complete mip chain.
+ std::vector<GLubyte> bigBase(2 * 2 * 4, 0x22);
+ for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
+ ++face)
+ {
+ glTexImage2D(face, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, bigBase.data());
+ }
+ EXPECT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
+ cube, 1);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+}
+
+// Test copying to a 2D texture mip level beyond the current storage mip chain, then redefining base
+// level to promote the staged image.
+TEST_P(CopyTextureTest, Texture2DDestLevelPastStorageMips)
+{
+ if (!checkExtensions())
+ {
+ return;
+ }
+
+ ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 &&
+ !IsGLExtensionEnabled("GL_OES_fbo_render_mipmap"));
+
+ GLTexture src;
+ glBindTexture(GL_TEXTURE_2D, src);
+ GLColor srcPix = GLColor::blue;
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &srcPix);
+ ASSERT_GL_NO_ERROR();
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ std::vector<GLubyte> base(1 * 1 * 4, 0x11);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, base.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Copy into level 1 (1x1), which lies beyond the 1-level mip chain of 1x1 base.
+ glCopyTextureCHROMIUM(src, 0, GL_TEXTURE_2D, tex,
+ /*destLevel=*/1, GL_RGBA, GL_UNSIGNED_BYTE,
+ /*unpackFlipY=*/GL_FALSE, /*premultiply=*/GL_FALSE,
+ /*unmultiply=*/GL_FALSE);
+ EXPECT_GL_NO_ERROR();
+
+ // Redefine base level to 2x2 so level 1 (1x1) becomes part of the complete mip chain.
+ std::vector<GLubyte> bigBase(2 * 2 * 4, 0x22);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, bigBase.data());
+ EXPECT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+}
+
// Test BGRA to RGBA cube map copy
TEST_P(CopyTextureTest, CubeMapTargetBGRA)
{
@@ -1581,9 +1685,6 @@
return;
}
- // http://anglebug.com/42263316
- ANGLE_SKIP_TEST_IF(IsD3D());
-
// http://anglebug.com/42263799
ANGLE_SKIP_TEST_IF(IsWindows() && IsNVIDIA() && IsOpenGL());
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page