CVE-2026-79130
Overview
Files Changed
src/libANGLE/Texture.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/FramebufferTest.cppsrc/tests/gl_tests/TextureTest.cpp
Patch
From 5211acfc35dfc49a52defd510620449da4ce3e66 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Tue, 14 Jul 2026 10:49:45 -0400 Subject: [PATCH] Verify all cube faces when checking for enabled levels Initial tests and investigations credit Ran. Bug: chromium:523717796 Change-Id: I25893273e81f5ff354d9c35f23a393914348b641 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8091778 Reviewed-by: Ran Wang <[email protected]> Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp index cc714ea..cbeb708 100644 --- a/src/libANGLE/Texture.cpp +++ b/src/libANGLE/Texture.cpp @@ -623,7 +623,7 @@ GLuint maxLevel = getMipmapMaxLevel(); ASSERT(maxLevel >= baseLevel); - // Note: for cube textures, we only check the first face. + // For cube textures, expect other faces to match the first face. TextureTarget target = TextureTypeToTarget(mType, 0); const Format &expectedFormat = mImageDescs[GetImageDescIndex(target, baseLevel)].format; @@ -640,6 +640,33 @@ { break; } + + // The cube map face checks are done for non-base levels only, because texture completeness + // rules already require base-level to be complete, and this function is used to know if any + // _additional_ levels are compatible with base. + if (enabledLevel != baseLevel && mType == gl::TextureType::CubeMap) + { + bool otherFacesValid = true; + angle::EnumIterator<TextureTarget> face = kCubeMapTextureTargetMin; + for (++face; face != kAfterCubeMapTextureTargetMax; ++face) + { + size_t otherFaceDescIndex = GetImageDescIndex(*face, enabledLevel); + const Extents &otherFaceLevelSize = mImageDescs[otherFaceDescIndex].size; + const Format &otherFaceLevelFormat = mImageDescs[otherFaceDescIndex].format; + + if (otherFaceLevelSize != levelSize || + !Format::SameSized(otherFaceLevelFormat, levelFormat)) + { + otherFacesValid = false; + break; + } + } + if (!otherFacesValid) + { + break; + } + } + if (expectedSize.valid()) { Extents newSize = expectedSize.value(); diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt index 72e29e4..8c29c0f 100644 --- a/src/tests/angle_end2end_tests_expectations.txt +++ b/src/tests/angle_end2end_tests_expectations.txt @@ -22,6 +22,11 @@ 377614665 VULKAN : GLSLValidationTest.StructSamplerVsComma/* = SKIP 410584007 VULKAN : ImageTestES31.UseSourceTextureAsStorageImage/* = SKIP 514710696 : PbufferTest.BindTexImageAndGenerateMipmap/* = SKIP +// Fails anywhere RGBA4 is emulated with RGBA8 +534823410 VULKAN : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFaceZero/ES3_Vulkan_SwiftShader_ForceRenderableFallbackFormat = SKIP +534823410 VULKAN : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFaceOne/ES3_Vulkan_SwiftShader_ForceRenderableFallbackFormat = SKIP +534823410 VULKAN NVIDIA : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFace*/* = SKIP +534823410 VULKAN LINUX INTEL : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFace*/* = SKIP // Fails in Android with EGL_BAD_ALLOC 374797737 : EGLSurfaceTest.CreateMultiWindowsSurfaceNoDestroy/* = SKIP // Incorrect tracking of robust init through EGL images @@ -448,6 +453,7 @@ 496604559 MAC METAL : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP 515709506 MAC METAL : DrawBaseVertexBaseInstanceTest_ES3.BaseInstanceSmallDivisorClientMemory/* = SKIP 524008572 MAC METAL : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP +534815900 MAC METAL : TextureCubeTestES3.RedefinedCubemapLevelsOnlyFaceZeroCompatible/* = SKIP // The workaround is not intended to be enabled in this configuration so // skip it as the failure is likely a driver bug. @@ -1148,6 +1154,7 @@ 480069042 PIXEL10 GLES : Texture2DTestES3.DepthTexturesWithMipmaps/* = SKIP 480069042 PIXEL10 GLES : Texture2DTestES3.DrawWithLevelZeroUndefined/* = SKIP 480069042 PIXEL10 GLES : Texture2DTestES3.DrawWithLevelsOutsideRangeUndefined/* = SKIP +480069042 PIXEL10 GLES : Texture2DTestES3.MipmapIncompleteMipmappingDisabled/* = SKIP 480069042 PIXEL10 GLES : Texture2DTestES3.TextureFormatChangesWithBaseLevel/* = SKIP 480069042 PIXEL10 GLES : Texture2DTestES3.UnpackOverlappingRowsFromUnpackBuffer/* = SKIP 480069042 PIXEL10 GLES : Texture3DTestES2.CopySubImageAlpha/* = SKIP diff --git a/src/tests/gl_tests/FramebufferTest.cpp b/src/tests/gl_tests/FramebufferTest.cpp index 9336878..56c8318 100644 --- a/src/tests/gl_tests/FramebufferTest.cpp +++ b/src/tests/gl_tests/FramebufferTest.cpp @@ -2734,7 +2734,93 @@ // Test cube map texture format fallback after one face of a non-base level is incompatibly // redefined. When the image is reformatted, the other faces of that level must be preserved. -TEST_P(FramebufferTestWithFormatFallback, R4G4B4A4_CubeTexImageRedefinedFace) +// The redefined face is face 0. +TEST_P(FramebufferTestWithFormatFallback, R4G4B4A4_CubeTexImageRedefinedFaceZero) +{ + constexpr GLenum kInternalFormat = GL_RGBA4; + constexpr GLenum kType = GL_UNSIGNED_SHORT_4_4_4_4; + const GLColor kColors[6] = {GLColor::red, GLColor::green, GLColor::blue, + GLColor::cyan, GLColor::yellow, GLColor::magenta}; + + // Create a two-level cube map and upload distinct colors to every face. + GLTexture cube; + glBindTexture(GL_TEXTURE_CUBE_MAP, cube); + for (GLenum face = 0; face < 6; ++face) + { + const GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; + const GLushort u16Color = convertGLColorToUShort(kInternalFormat, kColors[face]); + std::vector<GLushort> pixels(kTexWidth * kTexHeight, u16Color); + glTexImage2D(target, 0, kInternalFormat, kTexWidth, kTexHeight, 0, GL_RGBA, kType, + pixels.data()); + glTexImage2D(target, 1, kInternalFormat, kTexWidth / 2, kTexHeight / 2, 0, GL_RGBA, kType, + pixels.data()); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 1); + ASSERT_GL_NO_ERROR(); + + // Sample from the cube map so the backing image is allocated and committed. + constexpr char kFS[] = R"(precision highp float; + uniform samplerCube texCube; + void main() + { + gl_FragColor = textureCube(texCube, vec3(0, 0, 1)); + })"; + ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); + ASSERT_GL_NO_ERROR(); + + // Incompatibly redefine one face of level 1 with a different size. + { + const GLushort u16Color = convertGLColorToUShort(kInternalFormat, GLColor::white); + std::vector<GLushort> pixels(kTexWidth * kTexHeight, u16Color); + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 1, kInternalFormat, kTexWidth, kTexHeight, 0, + GL_RGBA, kType, pixels.data()); + } + + // Attach a face of level 0 to a framebuffer and read it back. This is the point at which the + // image may be reformatted. + GLFramebuffer fbo; + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X, + cube, 0); + EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + EXPECT_PIXEL_COLOR_EQ(kTexWidth / 4, kTexHeight / 4, kColors[0]); + + // Restore the redefined face to a compatible size, then verify every face of level 1. The + // other five faces must still hold the data that was originally uploaded. + { + const GLushort u16Color = convertGLColorToUShort(kInternalFormat, GLColor::white); + std::vector<GLushort> pixels((kTexWidth / 2) * (kTexHeight / 2), u16Color); + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 1, kInternalFormat, kTexWidth / 2, + kTexHeight / 2, 0, GL_RGBA, kType, pixels.data()); + } + for (GLenum face = 0; face < 6; ++face) + { + const GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, cube, 1); + EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + const GLColor expected = + target == GL_TEXTURE_CUBE_MAP_POSITIVE_X ? GLColor::white : kColors[face]; + EXPECT_PIXEL_COLOR_EQ(kTexWidth / 4, kTexHeight / 4, expected) << "face " << face; + } + + // Verify level 0 is also intact. + for (GLenum face = 0; face < 6; ++face) + { + const GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, cube, 0); + EXPECT_PIXEL_COLOR_EQ(kTexWidth / 4, kTexHeight / 4, kColors[face]) << "face " << face; + } + ASSERT_GL_NO_ERROR(); +} + +// Test cube map texture format fallback after one face of a non-base level is incompatibly +// redefined. When the image is reformatted, the other faces of that level must be preserved. +// The redefined face is not face 0. +TEST_P(FramebufferTestWithFormatFallback, R4G4B4A4_CubeTexImageRedefinedFaceOne) { constexpr GLenum kInternalFormat = GL_RGBA4; constexpr GLenum kType = GL_UNSIGNED_SHORT_4_4_4_4; diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 2aad897..7de389f 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -15203,6 +15203,90 @@ EXPECT_PIXEL_NEAR(0, 0, 128, 0, 0, 255, 2); }
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 72e29e4..8c29c0f 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -22,6 +22,11 @@
377614665 VULKAN : GLSLValidationTest.StructSamplerVsComma/* = SKIP
410584007 VULKAN : ImageTestES31.UseSourceTextureAsStorageImage/* = SKIP
514710696 : PbufferTest.BindTexImageAndGenerateMipmap/* = SKIP
+// Fails anywhere RGBA4 is emulated with RGBA8
+534823410 VULKAN : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFaceZero/ES3_Vulkan_SwiftShader_ForceRenderableFallbackFormat = SKIP
+534823410 VULKAN : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFaceOne/ES3_Vulkan_SwiftShader_ForceRenderableFallbackFormat = SKIP
+534823410 VULKAN NVIDIA : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFace*/* = SKIP
+534823410 VULKAN LINUX INTEL : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFace*/* = SKIP
// Fails in Android with EGL_BAD_ALLOC
374797737 : EGLSurfaceTest.CreateMultiWindowsSurfaceNoDestroy/* = SKIP
// Incorrect tracking of robust init through EGL images
@@ -448,6 +453,7 @@
496604559 MAC METAL : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
515709506 MAC METAL : DrawBaseVertexBaseInstanceTest_ES3.BaseInstanceSmallDivisorClientMemory/* = SKIP
524008572 MAC METAL : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP
+534815900 MAC METAL : TextureCubeTestES3.RedefinedCubemapLevelsOnlyFaceZeroCompatible/* = SKIP
// The workaround is not intended to be enabled in this configuration so
// skip it as the failure is likely a driver bug.
@@ -1148,6 +1154,7 @@
480069042 PIXEL10 GLES : Texture2DTestES3.DepthTexturesWithMipmaps/* = SKIP
480069042 PIXEL10 GLES : Texture2DTestES3.DrawWithLevelZeroUndefined/* = SKIP
480069042 PIXEL10 GLES : Texture2DTestES3.DrawWithLevelsOutsideRangeUndefined/* = SKIP
+480069042 PIXEL10 GLES : Texture2DTestES3.MipmapIncompleteMipmappingDisabled/* = SKIP
480069042 PIXEL10 GLES : Texture2DTestES3.TextureFormatChangesWithBaseLevel/* = SKIP
480069042 PIXEL10 GLES : Texture2DTestES3.UnpackOverlappingRowsFromUnpackBuffer/* = SKIP
480069042 PIXEL10 GLES : Texture3DTestES2.CopySubImageAlpha/* = SKIP
diff --git a/src/tests/gl_tests/FramebufferTest.cpp b/src/tests/gl_tests/FramebufferTest.cpp
index 9336878..56c8318 100644
--- a/src/tests/gl_tests/FramebufferTest.cpp
+++ b/src/tests/gl_tests/FramebufferTest.cpp
@@ -2734,7 +2734,93 @@
// Test cube map texture format fallback after one face of a non-base level is incompatibly
// redefined. When the image is reformatted, the other faces of that level must be preserved.
-TEST_P(FramebufferTestWithFormatFallback, R4G4B4A4_CubeTexImageRedefinedFace)
+// The redefined face is face 0.
+TEST_P(FramebufferTestWithFormatFallback, R4G4B4A4_CubeTexImageRedefinedFaceZero)
+{
+ constexpr GLenum kInternalFormat = GL_RGBA4;
+ constexpr GLenum kType = GL_UNSIGNED_SHORT_4_4_4_4;
+ const GLColor kColors[6] = {GLColor::red, GLColor::green, GLColor::blue,
+ GLColor::cyan, GLColor::yellow, GLColor::magenta};
+
+ // Create a two-level cube map and upload distinct colors to every face.
+ GLTexture cube;
+ glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
+ for (GLenum face = 0; face < 6; ++face)
+ {
+ const GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
+ const GLushort u16Color = convertGLColorToUShort(kInternalFormat, kColors[face]);
+ std::vector<GLushort> pixels(kTexWidth * kTexHeight, u16Color);
+ glTexImage2D(target, 0, kInternalFormat, kTexWidth, kTexHeight, 0, GL_RGBA, kType,
+ pixels.data());
+ glTexImage2D(target, 1, kInternalFormat, kTexWidth / 2, kTexHeight / 2, 0, GL_RGBA, kType,
+ pixels.data());
+ }
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 1);
+ ASSERT_GL_NO_ERROR();
+
+ // Sample from the cube map so the backing image is allocated and committed.
+ constexpr char kFS[] = R"(precision highp float;
+ uniform samplerCube texCube;
+ void main()
+ {
+ gl_FragColor = textureCube(texCube, vec3(0, 0, 1));
+ })";
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ ASSERT_GL_NO_ERROR();
+
+ // Incompatibly redefine one face of level 1 with a different size.
+ {
+ const GLushort u16Color = convertGLColorToUShort(kInternalFormat, GLColor::white);
+ std::vector<GLushort> pixels(kTexWidth * kTexHeight, u16Color);
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 1, kInternalFormat, kTexWidth, kTexHeight, 0,
+ GL_RGBA, kType, pixels.data());
+ }
+
+ // Attach a face of level 0 to a framebuffer and read it back. This is the point at which the
+ // image may be reformatted.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X,
+ cube, 0);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_COLOR_EQ(kTexWidth / 4, kTexHeight / 4, kColors[0]);
+
+ // Restore the redefined face to a compatible size, then verify every face of level 1. The
+ // other five faces must still hold the data that was originally uploaded.
+ {
+ const GLushort u16Color = convertGLColorToUShort(kInternalFormat, GLColor::white);
+ std::vector<GLushort> pixels((kTexWidth / 2) * (kTexHeight / 2), u16Color);
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 1, kInternalFormat, kTexWidth / 2,
+ kTexHeight / 2, 0, GL_RGBA, kType, pixels.data());
+ }
+ for (GLenum face = 0; face < 6; ++face)
+ {
+ const GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, cube, 1);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ const GLColor expected =
+ target == GL_TEXTURE_CUBE_MAP_POSITIVE_X ? GLColor::white : kColors[face];
+ EXPECT_PIXEL_COLOR_EQ(kTexWidth / 4, kTexHeight / 4, expected) << "face " << face;
+ }
+
+ // Verify level 0 is also intact.
+ for (GLenum face = 0; face < 6; ++face)
+ {
+ const GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, cube, 0);
+ EXPECT_PIXEL_COLOR_EQ(kTexWidth / 4, kTexHeight / 4, kColors[face]) << "face " << face;
+ }
+ ASSERT_GL_NO_ERROR();
+}
+
+// Test cube map texture format fallback after one face of a non-base level is incompatibly
+// redefined. When the image is reformatted, the other faces of that level must be preserved.
+// The redefined face is not face 0.
+TEST_P(FramebufferTestWithFormatFallback, R4G4B4A4_CubeTexImageRedefinedFaceOne)
{
constexpr GLenum kInternalFormat = GL_RGBA4;
constexpr GLenum kType = GL_UNSIGNED_SHORT_4_4_4_4;
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 2aad897..7de389f 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -15203,6 +15203,90 @@
EXPECT_PIXEL_NEAR(0, 0, 128, 0, 0, 255, 2);
}
+// Test that disabling mipmapping and creating a texture that is complete but not mip complete
+// works.
+TEST_P(Texture2DTestES3, MipmapIncompleteMipmappingDisabled)
+{
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+
+ // Disable mipmapping
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Create two levels for the texture that are not compatible.
+ constexpr uint32_t kLevel0Size = 75;
+ constexpr uint32_t kLevel1Size = 123;
+ const std::vector<GLColor> kLevel0Data(kLevel0Size * kLevel0Size, GLColor::red);
+ const std::vector<GLColor> kLevel1Data(kLevel1Size * kLevel1Size, GLColor::green);
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Size, kLevel0Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ kLevel0Data.data());
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Size, kLevel1Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ kLevel1Data.data());
+
+ // Set levels to [0, 1], while mipmapping is still disabled.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+ // Sampling from the texture should work, sampling only from level 0.
+ ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+ drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, kLevel0Data[0]);
+ ASSERT_GL_NO_ERROR();
+}
+
+// Test that disabling mipmapping and recreating a texture that is complete but not mip complete
+// works.
+TEST_P(Texture2DTestES3, MipmapIncompleteAfterRecreateMipmappingDisabled)
+{
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+
+ // Disable mipmapping
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Create two levels for the texture.
+ constexpr uint32_t kLevel0Size = 75;
+ constexpr uint32_t kLevel1Size = kLevel0Size >> 1;
+ const std::vector<GLColor> kLevel0Data(kLevel0Size * kLevel0Size, GLColor::red);
+ const std::vector<GLColor> kLevel1Data(kLevel1Size * kLevel1Size, GLColor::green);
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Size, kLevel0Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ kLevel0Data.data());
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Size, kLevel1Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ kLevel1Data.data());
+
+ // Set levels to [0, 1], while mipmapping is still disabled.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+ // Sample from the texture so it's synced.
+ ANGLE_GL_PROGRAM(drawTexture, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+ drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, kLevel0Data[0]);
+ ASSERT_GL_NO_ERROR();
+
+ // Change Base Level, then redefine level 0; it's changing a level that is out of
+ // range of [Base, Max] levels.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+
+ // Resize level 1 to be incompatible.
+ constexpr uint32_t kLevel1Resize = 123;
+ const std::vector<GLColor> kLevel1Data2(kLevel1Resize * kLevel1Resize, GLColor::blue);
+
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Resize, kLevel1Resize, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, kLevel1Data2.data());
+
+ // Draw again, sampling should still be done from level 0 only.
+ glClear(GL_COLOR_BUFFER_BIT);
+ drawQuad(drawTexture, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, kLevel0Data[0]);
+ ASSERT_GL_NO_ERROR();
+}
+
// The test is added to cover http://anglebug.com/42260889. Cubemap completeness checks used to
// start always at level 0 instead of the base level resulting in an incomplete texture if the faces
// at level 0 are not created. The test creates a cubemap texture, specifies the images only for mip
@@ -15932,6 +16016,88 @@
ASSERT_GL_NO_ERROR();
}
+// Test that redefining a cubemap with a compatible size in level 1 but incompatible in other levels
+// works.
+// Regression test for a bug where only the first face was checked for compatibility.
+TEST_P(TextureCubeTestES3, RedefinedCubemapLevelsOnlyFaceZeroCompatible)
+{
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
+
+ // Disabling mipmapping
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ constexpr uint32_t kSize = 45;
+ const std::vector<GLColor> kLevel0Data(kSize * kSize, GLColor::red);
+ const std::vector<GLColor> kLevel1Data(kSize * kSize / 4, GLColor::green);
+
+ // Create two levels for the texture.
+ for (GLenum face = 0; face < 6; face++)
+ {
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA8, kSize, kSize, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, kLevel0Data.data());
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 1, GL_RGBA8, kSize >> 1, kSize >> 1, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, kLevel1Data.data());
+ }
+
+ // Set levels to [0, 1], while mipmapping is still disabled.
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 1);
+
+ glUseProgram(mProgram);
+ glUniform1i(mTexture2DUniformLocation, 1);
+ glUniform1i(mTextureCubeUniformLocation, 0);
+
+ // Sample from the texture so it's synced.
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, kLevel0Data[0]);
+ ASSERT_GL_NO_ERROR();
+
+ // Redefine only the first face of level 1, to an incompatible size
+ constexpr uint32_t kSize2 = 33;
+ const std::vector<GLColor> kLevel0Data2(kSize2 * kSize2, GLColor::yellow);
+ const std::vector<GLColor> kLevel1Data2(kSize * kSize / 4, GLColor::blue);
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 1, GL_RGBA8, kSize2 >> 1, kSize2 >> 1, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, kLevel1Data2.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Make base level dirty.
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 0);
+
+ // Redefine level 0 in a way that's compatible with the first face of level 1.
+ for (GLenum face = 0; face < 6; face++)
+ {
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA8, kSize2, kSize2, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, kLevel0Data2.data());
+ }
+ ASSERT_GL_NO_ERROR();
+
+ // Draw again, sampling should still be done from level 0 only.
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, kLevel0Data2[0]);
+ ASSERT_GL_NO_ERROR();
+
+ // Restore level 1's first face to its original size, verify the rest of the faces retain the
+ // originally uploaded data.
+ static_assert(kSize > kSize2,
+ "kLevel1Data2 is sized based on kSize, and is used for both kSize and kSize2 "
+ "texture levels");
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 1, GL_RGBA8, kSize >> 1, kSize >> 1, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, kLevel1Data2.data());
... (truncated)
Original Bug Report
Heap Buffer Overflow in ANGLE Vulkan via stale staged updates
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 heap buffer overflow exists in ANGLE’s Vulkan backend due to incorrect handling of redefined cubemap levels. Stale staged updates with large dimensions are applied to a new smaller image because the state tracking mask is cleared prematurely. This allows an attacker to trigger an out-of-bounds memory write in the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/renderer_utils.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A potential heap buffer overflow vulnerability exists in the ANGLE Vulkan backend’s texture management. The issue arises when a cubemap texture’s storage is respecified after one or more faces have been redefined to a different size. Specifically, the mRedefinedLevels state is cleared in TextureVk::releaseImage while stale staged updates from the previous (larger) image are still pending. When these updates are later flushed to a newly allocated (smaller) image, the absence of the mRedefinedLevels skip-mask causes the updates to be applied with their original large dimensions, leading to an out-of-bounds write.
Potential Attack Sequence
The following steps describe a potential way an attacker could trigger this vulnerability from a WebGL2 context:
- Setup: Create a cubemap texture and bind it to
GL_TEXTURE_CUBE_MAP. SetGL_TEXTURE_MAX_LEVELto 1 andGL_TEXTURE_MIN_FILTERtoGL_NEARESTto control completeness. - Initial Allocation: Initialize all 6 faces at Level 0 to a large dimension (e.g., 512x512) and Level 1 to 256x256 using
texImage2D. Force ANGLE to allocate the underlying 512x512VkImage(e.g., via a dummy draw call). - Redefine Face 0: Use
texImage2Dto redefine only Face 0 at Level 0 to a smaller size (e.g., 64x64). This sets themRedefinedLevelsbitmask for Face 0. The old 512x512VkImageis kept alive, and a 64x64 update is staged for Face 0. - Change Base Level: Set
GL_TEXTURE_BASE_LEVELto 1. This marks the texture state as dirty. - Trigger Respecification: Bind the cubemap and issue a dummy draw call.
ContextVk::syncStatecallsTextureVk::syncState, which detects the base level change and callsTextureVk::respecifyImageStorage. - Stage Stale Updates:
respecifyImageStoragecallsstageSelfAsSubresourceUpdates. This moves the current 512x512 image into aprevImageand stagesUpdateSource::Imageupdates from it. Because Face 0 is inmRedefinedLevels, it is skipped. For Faces 1-5, massive 512x512 updates are appended tomSubresourceUpdates. - Premature Mask Clearing:
respecifyImageStoragethen callsTextureVk::releaseImage. This resets state but crucially leavesmSubresourceUpdatesintact. It then clears the tracking mask:mRedefinedLevels = {}. The 512x512 updates for Faces 1-5 are now orphaned without their skip-mask. - Allocate Level 1: The draw call continues, allocating a new 256x256
VkImage(based onBASE_LEVEL=1).flushImageStagedUpdatesonly flushes Level 1, leaving the 512x512 Level 0 updates safely pending. - Revert Base Level: Set
GL_TEXTURE_BASE_LEVELback to 0. - FBO Attachment: Attach Face 0 (Level 0) to a Framebuffer Object (FBO), leave the texture bound, and trigger
glDrawArrays. - Allocate Small Image:
FramebufferVk::syncStateruns first, detects the base level revert, and reallocates the image. BecauseBASE_LEVELis 0, it uses Face 0’s dimensions (64x64) and allocates a tiny 64x64 physicalVkImage. - The Overflow:
TextureVk::syncStatesubsequently callsflushImageStagedUpdatesfor all faces starting at Level 0. BecausemRedefinedLevelswas cleared in step 7, the stale 512x512 updates for Faces 1-5 are NOT skipped.ImageHelper::flushStagedUpdatesImplcallsvkCmdCopyImage, forcing a 512x512 copy into the tiny 64x64VkImage. The Vulkan driver executes this without bounds checking, causing a massive heap buffer overflow.
Impact
This vulnerability allows for an out-of-bounds write in the GPU process. On platforms where the GPU process is unsandboxed (such as Android), this can lead to Remote Code Execution (RCE) and full system compromise from a WebGL context.
Proposed Fix
The core issue is that mRedefinedLevels is cleared while updates that rely on it to be skipped are still staged in mSubresourceUpdates.
In TextureVk::releaseImage (third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp):
onStateChange(angle::SubjectMessage::SubjectChanged);
mRedefinedLevels = {}; // This is premature if updates are still staged
The fix should ensure that either:
mRedefinedLevelsis only cleared ifmImage->hasStagedUpdatesInAllocatedLevels()is false for the relevant levels.- When
mRedefinedLevelsis cleared, any corresponding staged updates that were meant to be skipped are actively removed from theImageHelper’smSubresourceUpdateslist. - As a defense-in-depth measure,
ImageHelper::flushStagedUpdatesImplshould clampvkCmdCopyImageextents to the destination image’s actual bounds to prevent driver-level buffer overflows.
Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb
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.