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 <[email protected]> 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 <[email protected]> Reviewed-by: Yuxin Hu <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- 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