Chrome · ANGLE › Metal
CVE-2026-118329
Input Validation in ANGLE (Metal)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/tests/gl_tests/TextureTest.cppsrc/libANGLE/renderer/metal/TextureMtl.mm
Patch
From 974b79ad146a3dca3d9a54e13f38519a8269964e Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
Date: Fri, 09 Oct 2026 02:33:55 +0800
Subject: [PATCH] Metal: Fix 2D array layer count check in TextureMtl::redefineImage
For MTLTextureType2DArray textures, MTLTexture.depth is always 1 while
the layer count is stored in arrayLength. Substitute arrayLength into
the depth component via GetTextureGLSize when comparing GL image
extents in TextureMtl::redefineImage so layer-count-only redefinitions
properly release stale storage and per-level views.
Bug: chromium:568900535
Change-Id: I9d0b6a36941d739fab15ab1fa37767946bc0441b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8542591
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
---
diff --git a/src/libANGLE/renderer/metal/TextureMtl.mm b/src/libANGLE/renderer/metal/TextureMtl.mm
index ab28138..e60b91e 100644
--- a/src/libANGLE/renderer/metal/TextureMtl.mm
+++ b/src/libANGLE/renderer/metal/TextureMtl.mm
@@ -713,6 +713,19 @@
return levelTextureViews[level];
}
+// Returns the texture extents in GL terms, substituting arrayLength into depth for array textures
+// since MTLTexture stores layer count in arrayLength with depth == 1.
+gl::Extents GetTextureGLSize(const mtl::TextureRef &texture,
+ const mtl::MipmapNativeLevel &nativeLevel)
+{
+ gl::Extents size = texture->size(nativeLevel);
+ if (texture->textureType() == MTLTextureType2DArray)
+ {
+ size.depth = static_cast<int>(texture->arrayLength());
+ }
+ return size;
+}
+
} // namespace
// TextureMtl::NativeTextureWrapper implementation.
@@ -799,6 +812,10 @@
uint32_t depth(GLuint glLevel) const { return mNativeTexture->depth(getNativeLevel(glLevel)); }
gl::Extents size(GLuint glLevel) const { return mNativeTexture->size(getNativeLevel(glLevel)); }
+ gl::Extents getGLSize(GLuint glLevel) const
+ {
+ return GetTextureGLSize(mNativeTexture, getNativeLevel(glLevel));
+ }
// Get width, height, depth, size at base level.
uint32_t widthAt0() const { return width(mBaseGLLevel); }
@@ -2094,7 +2111,7 @@
// must release it.
ASSERT(mNativeTextureStorage->textureType() == mtl::GetTextureType(index.getType()));
if (mNativeTextureStorage->getFormat() != mtlFormat ||
- size != mNativeTextureStorage->size(glLevel))
+ size != mNativeTextureStorage->getGLSize(glLevel))
{
// Keep other images
deallocateNativeStorage(/*keepImages=*/true);
@@ -2118,7 +2135,7 @@
imageDef.image->textureType() ==
mtl::GetTextureType(GetTextureImageType(index.getType())) &&
imageDef.formatID == mNativeTextureStorage->getFormat().intendedFormatId &&
- imageDef.image->sizeAt0() == size)
+ GetTextureGLSize(imageDef.image, mtl::kZeroNativeMipLevel) == size)
{
// Keep it! (No-op)
}
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 1bb7cc7..0177499 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -9796,6 +9796,52 @@
EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::green);
}
+// Test that redefining level 0 of a multi-layer 2D array texture to 1 layer (with unchanged
+// width, height, and format) followed by glGenerateMipmap preserves the newly uploaded level 0
+// data and generates mipmaps from it.
+TEST_P(Texture2DArrayTestES3, RedefineBaseLevelLayerCountToOneThenGenerateMipmap)
+{
+ constexpr GLsizei kSize = 8;
+ constexpr GLsizei kInitialLayers = 4;
+
+ glBindTexture(GL_TEXTURE_2D_ARRAY, m2DArrayTexture);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ // Define level 0 with 4 layers filled with red and draw to materialize native storage.
+ const std::vector<GLColor> pixelsRed(kSize * kSize * kInitialLayers, GLColor::red);
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, kInitialLayers, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, pixelsRed.data());
+ ASSERT_GL_NO_ERROR();
+
+ glUseProgram(mProgram);
+ glUniform1i(mTextureArraySliceUniformLocation, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ ASSERT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ // Redefine level 0 with 1 layer (unchanged width, height, and format) filled with green.
+ const std::vector<GLColor> pixelsGreen(kSize * kSize * 1, GLColor::green);
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ pixelsGreen.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Generate mipmaps; this recreates storage for the full mip chain of the 1-layer array.
+ glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify level 0 and level 1 contain green.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m2DArrayTexture, 0, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, GLColor::green);
+
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m2DArrayTexture, 1, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kSize / 2, kSize / 2, GLColor::green);
+}
+
// Create a 2D array texture, use it, then redefine one level without changing dimensions.
TEST_P(Texture2DArrayTestES3, RedefineLevelData)
{
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 1bb7cc7..0177499 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -9796,6 +9796,52 @@
EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::green);
}
+// Test that redefining level 0 of a multi-layer 2D array texture to 1 layer (with unchanged
+// width, height, and format) followed by glGenerateMipmap preserves the newly uploaded level 0
+// data and generates mipmaps from it.
+TEST_P(Texture2DArrayTestES3, RedefineBaseLevelLayerCountToOneThenGenerateMipmap)
+{
+ constexpr GLsizei kSize = 8;
+ constexpr GLsizei kInitialLayers = 4;
+
+ glBindTexture(GL_TEXTURE_2D_ARRAY, m2DArrayTexture);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ // Define level 0 with 4 layers filled with red and draw to materialize native storage.
+ const std::vector<GLColor> pixelsRed(kSize * kSize * kInitialLayers, GLColor::red);
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, kInitialLayers, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, pixelsRed.data());
+ ASSERT_GL_NO_ERROR();
+
+ glUseProgram(mProgram);
+ glUniform1i(mTextureArraySliceUniformLocation, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ ASSERT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ // Redefine level 0 with 1 layer (unchanged width, height, and format) filled with green.
+ const std::vector<GLColor> pixelsGreen(kSize * kSize * 1, GLColor::green);
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ pixelsGreen.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Generate mipmaps; this recreates storage for the full mip chain of the 1-layer array.
+ glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify level 0 and level 1 contain green.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m2DArrayTexture, 0, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, GLColor::green);
+
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m2DArrayTexture, 1, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kSize / 2, kSize / 2, GLColor::green);
+}
+
// Create a 2D array texture, use it, then redefine one level without changing dimensions.
TEST_P(Texture2DArrayTestES3, RedefineLevelData)
{
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