CVE-2026-106215
Overview
Files Changed
src/tests/gl_tests/TextureTest.cppsrc/libANGLE/renderer/d3d/d3d11/TextureStorage11.hsrc/libANGLE/renderer/d3d/TextureStorage.hsrc/tests/angle_end2end_tests_expectations.txtsrc/libANGLE/renderer/d3d/TextureD3D.cppsrc/libANGLE/renderer/d3d/TextureD3D.h
Patch
From 7c4ce32df88b814f7aff3a8ad008c66bf53a06e0 Mon Sep 17 00:00:00 2001 From: Ran Wang <[email protected]> Date: Thu, 17 Sep 2026 16:25:42 -0400 Subject: [PATCH] D3D11: Release storage on zero-dimension base level transition Back up existing mip levels to staging and release storage when base level transitions to zero dimensions or mismatches storage, preventing uninitialized GPU memory reads. Test: angle_end2end_tests --gtest_filter="*BaseLevelChangeWithZero*:*BaseLevelZeroDimension*:*RedefineImageAfterZeroHeightBaseLevelTransition*" Bug: b/561066220 Change-Id: I0722d601ecd564d44dd545796d8d775b38ee2bef Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8426123 Reviewed-by: Shahbaz Youssefi <[email protected]> Commit-Queue: Ran Wang <[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 ab1a494..01e299c 100644 --- a/src/libANGLE/renderer/d3d/TextureD3D.cpp +++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp @@ -340,6 +340,9 @@ return false; } + ASSERT(storageWidth0 > 0 && storageHeight0 > 0 && storageDepth0 > 0 && + storageFormat != GL_NONE); + if (index.getLevelIndex() < 0 || index.getLevelIndex() >= static_cast<int>(storageLevels)) { return false; @@ -360,6 +363,51 @@ image->getDepth() == storageDepth && image->getInternalFormat() == storageFormat; } +angle::Result TextureD3D::releaseTexStorageIfMismatched(const gl::Context *context, + GLint level, + GLenum internalformat, + const gl::Extents &size, + bool forceReleaseStorage) +{ + return releaseTexStorageIfMismatched(context, 0 /*faceIndex*/, level, internalformat, size, + forceReleaseStorage); +} + +angle::Result TextureD3D::releaseTexStorageIfMismatched(const gl::Context *context, + size_t faceIndex, + GLint level, + GLenum internalformat, + const gl::Extents &size, + bool forceReleaseStorage) +{ + if (!mTexStorage) + { + return angle::Result::Continue; + } + + auto shouldReleaseStorage = [&]() { + const int storageLevels = mTexStorage->getLevelCount(); + return (level >= storageLevels && storageLevels != 0) || + size.width != mTexStorage->getLevelWidth(level) || + size.height != mTexStorage->getLevelHeight(level) || + size.depth != mTexStorage->getLevelDepth(level) || + internalformat != mTexStorage->getFormat(); + }; + + if (forceReleaseStorage || shouldReleaseStorage()) + { + markAllImagesDirty(); + + gl::CubeFaceArray<gl::TexLevelMask> copyImageMasks; + copyImageMasks.fill(gl::TexLevelMask().set()); + copyImageMasks[faceIndex].set(level, false); + + ANGLE_TRY(releaseTexStorage(context, copyImageMasks)); + } + + return angle::Result::Continue; +} + angle::Result TextureD3D::setImageImpl(const gl::Context *context, const gl::ImageIndex &index, GLenum type, @@ -835,28 +883,41 @@ angle::Result TextureD3D::setBaseLevel(const gl::Context *context, GLuint baseLevel) { - const int oldStorageWidth = std::max(1, getLevelZeroWidth()); - const int oldStorageHeight = std::max(1, getLevelZeroHeight()); - const int oldStorageDepth = std::max(1, getBaseLevelStorageDepth()); - const int oldStorageFormat = getBaseLevelInternalFormat(); - mBaseLevel = baseLevel; + mBaseLevel = baseLevel; + + // If mTexStorage has not been created yet, there is no existing storage to back up or release; + // when storage is later initialized, it will use the updated mBaseLevel. + // For immutable textures (created via glTexStorage*D), the storage already covers all mip + // levels with the correct dimensions, so it should never be released on base level change. + if (!mTexStorage || isImmutable()) + { + return angle::Result::Continue; + } + + const int oldStorageWidth = mTexStorage->getLevelWidth(0); + const int oldStorageHeight = mTexStorage->getLevelHeight(0); + const int oldStorageDepth = mTexStorage->getLevelDepth(0); + const GLenum oldStorageFormat = mTexStorage->getFormat(); // When the base level changes, the texture storage might not be valid anymore, since it could // have been created based on the dimensions of the previous specified level range. - // For immutable textures (created via glTexStorage*D), the storage already covers all mip - // levels with the correct dimensions, so it should never be released on base level change. // getLevelZeroWidth() can compute incorrect dimensions for NPOT textures because // (width >> N) << N != width when width is not a power of two, which would cause a spurious // dimension mismatch and lead to recreating the storage with wrong dimensions. - const int newStorageWidth = std::max(1, getLevelZeroWidth()); - const int newStorageHeight = std::max(1, getLevelZeroHeight()); - const int newStorageDepth = std::max(1, getBaseLevelStorageDepth()); - const int newStorageFormat = getBaseLevelInternalFormat(); - if (mTexStorage && !isImmutable() && - (newStorageWidth != oldStorageWidth || newStorageHeight != oldStorageHeight || - newStorageDepth != oldStorageDepth || newStorageFormat != oldStorageFormat)) + const int newStorageWidth = getLevelZeroWidth(); + const int newStorageHeight = getLevelZeroHeight(); + const int newStorageDepth = getBaseLevelStorageDepth(); + const GLenum newStorageFormat = getBaseLevelInternalFormat(); + if (newStorageWidth <= 0 || newStorageHeight <= 0 || newStorageDepth <= 0 || + newStorageWidth != oldStorageWidth || newStorageHeight != oldStorageHeight || + newStorageDepth != oldStorageDepth || newStorageFormat != oldStorageFormat) { - // Iterate over all images and backup contents from texture storage before it is released. + // Even if the new base level has zero/invalid dimensions (making the texture incomplete), + // changing GL_TEXTURE_BASE_LEVEL does not delete or invalidate pixel data in other mip + // levels (e.g. level 0). Because we must release mTexStorage now, we first back up any + // mip levels that match oldStorage into their respective ImageD3D staging buffers so their + // contents are preserved if the base level is later restored. + // // We need to copy for both: // 1. Render targets: GPU rendering modified the storage directly. // 2. Fast-path uploads: TextureStorage11::setData wrote pixel data directly to the GPU @@ -877,9 +938,9 @@ ANGLE_TRY(mTexStorage->findRenderTarget(context, index, &renderTarget)); } - const bool dimensionsMatch = isImageSubresourceMatchingStorage( - index, image, oldStorageWidth, oldStorageHeight, oldStorageDepth, - static_cast<GLenum>(oldStorageFormat), storageLevels); + const bool dimensionsMatch = + isImageSubresourceMatchingStorage(index, image, oldStorageWidth, oldStorageHeight, + oldStorageDepth, oldStorageFormat, storageLevels); const bool hasNoStagingCopy = image && !image->isDirty() && @@ -945,7 +1006,10 @@ if (anyCopyMask) { - const GLenum storageFormat = getBaseLevelInternalFormat(); + const int storageWidth0 = mTexStorage->getLevelWidth(0); + const int storageHeight0 = mTexStorage->getLevelHeight(0); + const int storageDepth0 = mTexStorage->getLevelDepth(0); + const GLenum storageFormat = mTexStorage->getFormat(); const size_t storageLevels = mTexStorage->getLevelCount(); gl::ImageIndexIterator iterator = imageIterator(); @@ -964,9 +1028,9 @@ copyImage = copyStorageToImagesMask[0][index.getLevelIndex()]; } - const bool dimensionsMatch = isImageSubresourceMatchingStorage( - index, image, getLevelZeroWidth(), getLevelZeroHeight(), getBaseLevelStorageDepth(), - storageFormat, storageLevels); + const bool dimensionsMatch = + isImageSubresourceMatchingStorage(index, image, storageWidth0, storageHeight0, + storageDepth0, storageFormat, storageLevels); if (dimensionsMatch && copyImage) { @@ -1814,36 +1878,17 @@ { ASSERT(size.depth == 1); - // If there currently is a corresponding storage texture image, it has these parameters - const int storageWidth = std::max(1, getLevelZeroWidth() >> level); - const int storageHeight = std::max(1, getLevelZeroHeight() >> level); - const GLenum storageFormat = getBaseLevelInternalFormat(); - - if (mTexStorage) + if (mTexStorage && level != 0 && mEGLImageTarget) { - const size_t storageLevels = mTexStorage->getLevelCount(); - // If the storage was from an EGL image, copy it back into local images to preserve it // while orphaning - if (level != 0 && mEGLImageTarget) - { - ANGLE_TRY(mImageArray[0]->copyFromTexStorage(context, gl::ImageIndex::Make2D(0), - mTexStorage)); - } - - if ((level >= storageLevels && storageLevels != 0) || size.width != storageWidth || - size.height != storageHeight || internalformat != storageFormat || - mEGLImageTarget) // Discard mismatched storage - { - gl::TexLevelMask copyImageMask; - copyImageMask.set(); - copyImageMask.set(level, false); - - ANGLE_TRY(releaseTexStorage(context, copyImageMask)); - markAllImagesDirty(); - } + ANGLE_TRY( + mImageArray[0]->copyFromTexStorage(context, gl::ImageIndex::Make2D(0), mTexStorage)); } + ANGLE_TRY(releaseTexStorageIfMismatched(context, static_cast<GLint>(level), internalformat, + size, mEGLImageTarget /*forceReleaseStorage*/)); + mImageArray[level]->redefine(gl::TextureType::_2D, internalformat, size, forceRelease); mDirtyImages = mDirtyImages || mImageArray[level]->isDirty(); @@ -2539,28 +2584,8 @@ const gl::Extents &size, bool forceRelease) { - // If there currently is a corresponding storage texture image, it has these parameters - const int storageWidth = std::max(1, getLevelZeroWidth() >> level); - const int storageHeight = std::max(1, getLevelZeroHeight() >> level); - const GLenum storageFormat = getBaseLevelInternalFormat(); - - if (mTexStorage) - { - const int storageLevels = mTexStorage->getLevelCount(); - - if ((level >= storageLevels && storageLevels != 0) || size.width != storageWidth || - size.height != storageHeight || - internalformat != storageFormat) // Discard mismatched storage - { - markAllImagesDirty(); - - gl::CubeFaceArray<gl::TexLevelMask> copyImageMasks; - copyImageMasks.fill(gl::TexLevelMask().set()); - copyImageMasks[faceIndex].set(level, false); - - ANGLE_TRY(releaseTexStorage(context, copyImageMasks)); - } - } + ANGLE_TRY(releaseTexStorageIfMismatched(context, faceIndex, level, internalformat, size, + false /*forceReleaseStorage*/)); mImageArray[faceIndex][level]->redefine(gl::TextureType::CubeMap, internalformat, size, forceRelease); @@ -3201,29 +3226,8 @@ const gl::Extents &size, bool forceRelease) { - // If there currently is a corresponding storage texture image, it has these parameters - const int storageWidth = std::max(1, getLevelZeroWidth() >> level); - const int storageHeight = std::max(1, getLevelZeroHeight() >> level); - const int storageDepth = std::max(1, getLevelZeroDepth() >> level); - const GLenum storageFormat = getBaseLevelInternalFormat(); - - if (mTexStorage) - { - const int storageLevels = mTexStorage->getLevelCount(); - - if ((level >= storageLevels && storageLevels != 0) || size.width != storageWidth || - size.height != storageHeight || size.depth != storageDepth || - internalformat != storageFormat) // Discard mismatched storage - { - markAllImagesDirty(); - - gl::TexLevelMask copyImageMask; - copyImageMask.set(); - copyImageMask.set(level, false); - - ANGLE_TRY(releaseTexStorage(context, copyImageMask)); - } - } + ANGLE_TRY(releaseTexStorageIfMismatched(context, level, internalformat, size, + false /*forceReleaseStorage*/)); mImageArray[level]->redefine(gl::TextureType::_3D, internalformat, size, forceRelease); mDirtyImages = mDirtyImages || mImageArray[level]->isDirty(); @@ -3965,12 +3969,6 @@ const gl::Extents &size, bool forceRelease) { - // If there currently is a corresponding storage texture image, it has these parameters - const int storageWidth = std::max(1, getLevelZeroWidth() >> level); - const int storageHeight = std::max(1, getLevelZeroHeight() >> level); - const GLenum storageFormat = getBaseLevelInternalFormat(); - const int storageDepth = getBaseLevelStorageDepth(); - // Only reallocate the layers if the size doesn't match if (size.depth != ANGLE_UNSAFE_TODO(mLayerCounts[level])) { @@ -3993,23 +3991,8 @@ } } - if (mTexStorage) - { - const int storageLevels = mTexStorage->getLevelCount(); - - if ((level >= storageLevels && storageLevels != 0) || size.width != storageWidth || - size.height != storageHeight || size.depth != storageDepth || - internalformat != storageFormat) // Discard mismatched storage - { - markAllImagesDirty(); - - gl::TexLevelMask copyImageMask; - copyImageMask.set(); - copyImageMask.set(level, false); - - ANGLE_TRY(releaseTexStorage(context, copyImageMask)); - } - } + ANGLE_TRY(releaseTexStorageIfMismatched(context, level, internalformat, size, + false /*forceReleaseStorage*/)); if (size.depth > 0) { diff --git a/src/libANGLE/renderer/d3d/TextureD3D.h b/src/libANGLE/renderer/d3d/TextureD3D.h index ecef28e..2e05754 100644 --- a/src/libANGLE/renderer/d3d/TextureD3D.h +++ b/src/libANGLE/renderer/d3d/TextureD3D.h @@ -210,6 +210,17 @@ angle::Result releaseTexStorage( const gl::Context *context, const gl::CubeFaceArray<gl::TexLevelMask> ©StorageToImagesMask); + angle::Result releaseTexStorageIfMismatched(const gl::Context *context, + GLint level, + GLenum internalformat, + const gl::Extents &size, + bool forceReleaseStorage); + angle::Result releaseTexStorageIfMismatched(const gl::Context *context, + size_t faceIndex, + GLint level, + GLenum internalformat, + const gl::Extents &size, + bool forceReleaseStorage); GLuint getBaseLevel() const { return mBaseLevel; } diff --git a/src/libANGLE/renderer/d3d/TextureStorage.h b/src/libANGLE/renderer/d3d/TextureStorage.h index dee3d4c..4ebc37b 100644 --- a/src/libANGLE/renderer/d3d/TextureStorage.h +++ b/src/libANGLE/renderer/d3d/TextureStorage.h @@ -51,6 +51,10 @@ virtual bool isManaged() const = 0; virtual bool supportsNativeMipmapFunction() const = 0; virtual int getLevelCount() const = 0; + virtual int getLevelWidth(int mipLevel) const = 0; + virtual int getLevelHeight(int mipLevel) const = 0; + virtual int getLevelDepth(int mipLevel) const = 0; + virtual GLenum getFormat() const = 0; virtual bool isMultiplanar(const gl::Context *context) = 0; diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h index b98e7f2..e09e9f1 100644 --- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h +++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h @@ -83,6 +83,10 @@ bool isManaged() const override; bool supportsNativeMipmapFunction() const override; int getLevelCount() const override; + int getLevelWidth(int mipLevel) const override; + int getLevelHeight(int mipLevel) const override; + int getLevelDepth(int mipLevel) const override; + GLenum getFormat() const override { return mFormatInfo.internalFormat; } bool isUnorderedAccess() const override { return mBindFlags & D3D11_BIND_UNORDERED_ACCESS; } bool isMultiplanar(const gl::Context *context) override; bool requiresTypelessTextureFormat() const; @@ -128,9 +132,6 @@ UINT miscFlags, GLenum internalFormat, const std::string &label); - int getLevelWidth(int mipLevel) const; - int getLevelHeight(int mipLevel) const; - int getLevelDepth(int mipLevel) const; virtual angle::Result getSwizzleTexture(const gl::Context *context, const TextureHelper11 **outTexture) = 0; @@ -685,6 +686,7 @@ const gl::ImageIndex &index, Image11 *incomingImage) override; void onLabelUpdate() override; + int getLevelDepth(int mipLevel) const override { return mTextureDepth; } struct LevelLayerRangeKey { @@ -837,6 +839,7 @@ angle::Result copyToStorage(const gl::Context *context, TextureStorage *destStorage) override; void onLabelUpdate() override; + int getLevelDepth(int mipLevel) const override { return mTextureDepth; } protected: ... (file diff truncated)
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 63a4bed..673e9a9 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -215,6 +215,8 @@
534082926 WIN INTEL VULKAN : TransformFeedbackTest.RenderOnceChangeXfbBufferRenderAgain/* = SKIP
534082926 WIN INTEL VULKAN : TransformFeedbackTest.BufferRebinding/* = SKIP
534082926 WIN INTEL VULKAN : TransformFeedbackTest.MultiplePaused/* = SKIP
+534082926 WIN INTEL VULKAN : TransformFeedbackTest.RecordTwiceWithBindingOffsetChange/* = SKIP
+534082926 WIN INTEL VULKAN : TransformFeedbackTestIOBlocks.Interleaved/* = SKIP
40096690 WIN INTEL VULKAN : TextureBufferTestES31.UseAsXFBThenAsTextureBuffer/* = SKIP
// Failures on Intel Comet Lake S UHD Graphics 630 (8086:9bc5-31.0.101.2111)
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 61d4e8d..de3c61e 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -1412,6 +1412,12 @@
}
};
+class Texture3DTestES3RobustInit : public Texture3DTestES3
+{
+ protected:
+ Texture3DTestES3RobustInit() : Texture3DTestES3() { setRobustResourceInit(true); }
+};
+
class ShadowSamplerPlusSampler3DTestES3 : public TexCoordDrawTest
{
protected:
@@ -9812,6 +9818,221 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
}
+// Test that changing GL_TEXTURE_BASE_LEVEL to a mip level with zero height properly releases
+// storage so that undeclared slices/dimensions are not sampleable and do not leak stale data.
+TEST_P(Texture3DTestES3, BaseLevelChangeWithZeroDimensionReleasesStorage)
+{
+ constexpr GLsizei kLevel0Width = 4;
+ constexpr GLsizei kLevel0Height = 4;
+ constexpr GLsizei kLevel0Depth = 2;
+ constexpr size_t kLevel0Pixels = kLevel0Width * kLevel0Height * kLevel0Depth;
+
+ constexpr GLsizei kLevel1Width = 2;
+ constexpr GLsizei kLevelHeightZero = 0;
+ constexpr GLsizei kLevel1Depth = 1;
+
+ glBindTexture(GL_TEXTURE_3D, mTexture3D);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Level 0: 4x4x2 red
+ std::vector<GLColor> level0Data(kLevel0Pixels, GLColor::red);
+
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0);
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, kLevel0Width, kLevel0Height, kLevel0Depth, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, level0Data.data());
+ glTexImage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kLevel1Width, kLevelHeightZero, kLevel1Depth, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ glUseProgram(mProgram);
+ glUniform1i(mTexture3DUniformLocation, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ // Clear the framebuffer to green so the subsequent check proves the second drawQuad re-sampled
+ // the texture rather than leaving the old framebuffer contents.
+ glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+ // Change base level to 1, which has 0 height.
+ // The texture storage must be released.
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 1);
+
+ // Change base level back to 0. Storage is reallocated and level 0 data is preserved.
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0);
+
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+}
+
+// Test that changing GL_TEXTURE_BASE_LEVEL to a mip level with zero depth properly releases
+// storage and re-preserves data upon restore.
+TEST_P(Texture3DTestES3, BaseLevelChangeWithZeroDepthReleasesStorage)
+{
+ constexpr GLsizei kLevel0Width = 4;
+ constexpr GLsizei kLevel0Height = 4;
+ constexpr GLsizei kLevel0Depth = 2;
+ constexpr size_t kLevel0Pixels = kLevel0Width * kLevel0Height * kLevel0Depth;
+
+ constexpr GLsizei kLevel1Width = 2;
+ constexpr GLsizei kLevel1Height = 2;
+ constexpr GLsizei kLevelDepthZero = 0;
+
+ glBindTexture(GL_TEXTURE_3D, mTexture3D);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ std::vector<GLColor> level0Data(kLevel0Pixels, GLColor::blue);
+
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0);
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, kLevel0Width, kLevel0Height, kLevel0Depth, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, level0Data.data());
+ glTexImage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kLevel1Width, kLevel1Height, kLevelDepthZero, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ glUseProgram(mProgram);
+ glUniform1i(mTexture3DUniformLocation, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+ // Clear the framebuffer to green so the subsequent check proves the second drawQuad re-sampled
+ // the texture rather than leaving the old framebuffer contents.
+ glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+ // Change base level to 1, which has 0 depth.
+ // The texture storage must be released.
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 1);
+
+ // Change base level back to 0. Storage is reallocated and level 0 data is preserved.
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0);
+
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+}
+
+// Test that transitioning base level to a level with zero dimension releases storage, and when
+// storage is recreated for a level whose contents were discarded, robust resource init ensures it
+// is zero-initialized rather than reading uninitialized GPU memory.
+TEST_P(Texture3DTestES3RobustInit, BaseLevelZeroDimensionClearsStorageOnRestore)
+{
+ constexpr GLsizei kLevel0Width = 4;
+ constexpr GLsizei kLevel0Height = 1;
+ constexpr GLsizei kLevel0Depth = 4;
+
+ constexpr GLsizei kLevel1Width = 2;
+ constexpr GLsizei kLevelHeightZero = 0;
+ constexpr GLsizei kLevel1Depth = 2;
+
+ constexpr GLsizei kRedefinedLevel0Height = 2;
+ constexpr GLsizei kRedefinedLevel0Depth = 2;
+
+ glBindTexture(GL_TEXTURE_3D, mTexture3D);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Define level 0 with null data and clear it via FBO to red.
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, kLevel0Width, kLevel0Height, kLevel0Depth, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, nullptr);
+ glTexImage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kLevel1Width, kLevelHeightZero, kLevel1Depth, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, mTexture3D, 0, 0);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ // Change base level to 1 (zero height), releasing storage.
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 1);
+
+ // Redefine level 0 with new dimensions and null data so that previous storage is discarded.
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, kLevel0Width, kRedefinedLevel0Height,
+ kRedefinedLevel0Depth, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ // Switch back to level 0.
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0);
+
+ // Attach level 0 to FBO and read back. Robust init must clear it to transparent black.
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, mTexture3D, 0, 0);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+}
+
+// Test the scenario from b/561066220 where base level is transitioned to a zero-height level and
+// another level is redefined, ensuring robust initialization clears newly allocated storage or
+// preserves initialized data upon restore.
+TEST_P(Texture3DTestES3RobustInit, RedefineImageAfterZeroHeightBaseLevelTransition)
+{
+ constexpr GLsizei kLevel0Width = 16;
+ constexpr GLsizei kLevelHeight = 1;
+ constexpr GLsizei kLevel0Depth = 8;
+
+ constexpr GLsizei kLevel1Width = 8;
+ constexpr GLsizei kLevel1Depth = 4;
+
+ constexpr GLsizei kLevel2Width = 4;
+ constexpr GLsizei kLevel2Depth = 2;
+ constexpr GLsizei kRedefinedLevel2Width = 3;
+
+ constexpr GLsizei kLevel3Width = 2;
+ constexpr GLsizei kLevelHeightZero = 0;
+ constexpr GLsizei kLevel3Depth = 1;
+
+ glBindTexture(GL_TEXTURE_3D, mTexture3D);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, kLevel0Width, kLevelHeight, kLevel0Depth, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, nullptr);
+ glTexImage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kLevel1Width, kLevelHeight, kLevel1Depth, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, nullptr);
+ glTexImage3D(GL_TEXTURE_3D, 2, GL_RGBA8, kLevel2Width, kLevelHeight, kLevel2Depth, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, nullptr);
+ // Level 3 has height 0.
+ glTexImage3D(GL_TEXTURE_3D, 3, GL_RGBA8, kLevel3Width, kLevelHeightZero, kLevel3Depth, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, mTexture3D, 0, 0);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ // Switch base level to 3 (height 0).
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 3);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 3);
+
+ // Redefine level 2 with mismatched dimensions.
+ glTexImage3D(GL_TEXTURE_3D, 2, GL_RGBA8, kRedefinedLevel2Width, kLevelHeight, kLevel2Depth, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ // Switch back to level 0.
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0);
+
+ // Attach level 0 to FBO and read back. Because level 0 was never redefined or invalidated,
+ // its cleared red contents must be deterministically preserved across the base-level
+ // transitions and level 2 redefinition.
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, mTexture3D, 0, 0);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+}
+
class Texture3DIncreaseDepthTestES3 : public Texture3DTestES3
{
protected:
@@ -25397,6 +25618,10 @@
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture3DTestES3);
ANGLE_INSTANTIATE_TEST_ES3(Texture3DTestES3);
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture3DTestES3RobustInit);
+ANGLE_INSTANTIATE_TEST_ES3_AND(Texture3DTestES3RobustInit,
+ ES3_VULKAN().enable(Feature::AllocateNonZeroMemory));
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture3DIncreaseDepthTestES3);
ANGLE_INSTANTIATE_TEST_ES3_AND(
Texture3DIncreaseDepthTestES3,
Original Bug Report
ANGLE D3D11: a Texture3D zero-dimension base level discards the preserved mip, handing another site's recycled GPU texture to the next sampler
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS
A hostile website can recover a complete RGBA8 texture that a different site uploaded and deleted in its own WebGL2 context. No permission, user gesture beyond a normal page visit, extension, flag, or site-isolation bypass is involved; both pages are ordinary web content in one normal-sandbox Chrome session.
This is the Texture3D sibling of the 2D-array defect fixed five days ago. 5aaa642831a241872a8a21b43842316cc6916db3, “D3D: Release 2D array storage on base level layer count change” (2026-09-08, b/553857574), repaired exactly this failure mode for TEXTURE_2D_ARRAY and left TEXTURE_3D untouched. That fix describes its impact as “stale slice retention and memory leaks”. The Texture3D form is not a leak: it hands one site another site’s texture, whole and pixel-exact. It is still present at ANGLE tip — see Upstream status below.
Distinct from 550626693 (ANGLE/D3D11 cross-origin GPU-memory image disclosure, reported by the same reporter). That issue is the StructuredBuffer SRV size cache, in Buffer11. This one is texture storage release on a base-level change, in TextureD3D. Different subsystem, different root, no shared code path.
The defect is in ANGLE’s D3D11 texture-storage release path, in src/libANGLE/renderer/d3d/TextureD3D.cpp.
TextureD3D::setBaseLevelreconstructs the level-zero dimensions from the currently selected base mip. When it compares storage identity it clamps a raw zero dimension to one. A selected mip with zero depth therefore still “matches”, and the old depth-one allocation is retained. A selected mip with zero height and nonzero depth likewise retains an old height-one allocation.- Redefining a different mip then forces
releaseTexStorage. Its nonempty mask requests preservation of level zero. - The post-transition
isImageCompletecheck rejects every requested image, because the selected base still holds the raw zero dimension. The native level-zero data is therefore never copied intoImage11. TextureStorage11_3D::onDestroydrops the cached render-target authority, while the frontend initialization state still reads initialized.- Recreating the target by sampling skips robust clearing and reads back a recycled allocation, which on the tested recycler is a different site’s freed texture.
The zero-depth and zero-height triggers are independent, which shows this is a general zero-dimension base-transition defect rather than a depth-only edge case.
Security effect. In the strict cross-site harness an attacker page at http://127.0.0.1 recovered a victim texture created and deleted at http://localhost:
| Trigger | Exact victim pixels | Bytes |
|---|---|---|
| Zero-depth 3D base | 16384 / 16384 | 65,536 |
| Zero-height 3D base | 8192 / 8192 | 32,768 |
The loopback server generated fresh random per-session patterns, sent them only to the victim, and was the only party that compared the attacker’s raw output. Neither the attacker nor the controller ever received a pattern or a pattern hash. Matched no-victim rows returned all zero, and complete-base control rows preserved every attacker pixel. Same-context scaling recovered one whole 4,194,304-byte allocation.
Suggested severity: Medium. CVSS 3.1: 6.1 — AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:N/A:N
Upstream status — both roots verified present at ANGLE tip
Checked against origin/main at 9d4f4abe5c5 (2026-09-13), i.e. after every commit listed below:
TextureD3D::setBaseLevelstill compares storage identity throughstd::max(1, getLevelZeroHeight())on both the old and the new dimensions, so a raw zero height still “matches” the retained allocation.TextureD3D::releaseTexStoragestill gates the preservation copy onisImageComplete(index)evaluated after the transition, then callsonDestroy()andSafeDelete(mTexStorage)unconditionally. There is no branch that marks the subresource uninitialized when the copy is skipped, which is what leaves frontend state claiming initialized over a recycled allocation.
Five commits have touched TextureD3D.cpp / TextureStorage11.cpp since the ANGLE revision shipped in Chrome 153, and none of them addresses this:
| Commit | Subject |
|---|---|
4d2818ad499 |
Windows: Use ComPtr for COM resource ownership |
c2b3395143b |
D3D11: fixing robust init for self copy same mip lvl |
5aaa642831a |
D3D: Release 2D array storage on base level layer count change (b/553857574) |
c477cf31f7a |
D3D: Reject out-of-storage mip levels in completeness check (b/536664909) |
a871bdb0593 |
D3D11: Fix use-after-free when redefining 2D array mip levels |
c477cf31f7a moved the completeness logic into a shared TextureD3D::isLevelComplete that returns false when any dimension is <= 0. That centralises the rejection this report depends on; it does not preserve or invalidate the affected subresource, so the disclosure survives it.
NOT CLAIMED
Cross-process, cross-profile or cross-OS-user reuse; arbitrary non-marker memory contents; the 4 MiB figure cross-site (it is same-context only); disclosure on Intel D3D11 or on NVIDIA Vulkan, both of which were clean; universal allocator reliability; memory corruption; arbitrary-address reads; sandbox escape; or code execution.
VERSION
Chrome Version: Chrome for Testing Stable 153.0.8010.36, CDP-reported as Chrome/153.0.8010.36. Also reproduced same-context on Canary 155.0.8054.0.
Operating System: Microsoft Windows 11 x64, build 26200.
GPU: NVIDIA GeForce RTX 5060 Laptop GPU, driver 32.0.15.9144.
Renderer: ANGLE (NVIDIA, NVIDIA GeForce RTX 5060 Laptop GPU (0x00002D19) Direct3D11 vs_5_0 ps_5_0, D3D11).
ANGLE: 2.1.28650, revision fca5efdfeff5daf430bc4458375c8d6b7e457400. The same path is present at current public ANGLE 927129a81b043176070e7089df897251d2da1d86.
The positive runs used --use-angle=d3d11 --force-high-performance-gpu, which select a backend and an adapter only; normal sandboxes and site isolation remained enabled. No security boundary or experimental feature changed.
Backend differential. NVIDIA D3D11 discloses. The same executable’s NVIDIA Vulkan control preserved attacker data, and Intel D3D11 returned zero. This is consistent with a D3D11 texture-recycler-dependent effect.
REPRODUCTION CASE
Attached files
| File | Role |
|---|---|
leak-server.py |
Loopback server on three origins. Draws the synthetic images, gates delivery by Host, and is the only party that compares. |
victim.html |
Victim page, served only on http://localhost. |
attacker.html |
Attacker page, served only on http://127.0.0.1. |
run-two-window-poc.ps1 |
Runner. Starts the server, launches one browser per round with two windows, optionally screen-records. |
f033-two-window-leak.mp4 |
Screen recording of the run whose results are tabled below. |
run-leak-d3d11.json |
Machine-readable results: CDP-observed browser identity, full argv, origins, and every round’s verdict. |
Put the four PoC files in one directory. Requires Python 3 with Pillow; ffmpeg on PATH only for -Record.
powershell -ExecutionPolicy Bypass -File .\run-two-window-poc.ps1 -Rounds 6 -Chrome "C:\path\to\chrome.exe"
The runner needs nothing else: it locates Chrome via -Chrome, then the usual install paths, then PATH, and writes its evidence next to itself.
What it does
The victim and the attacker are two side-by-side top-level browser windows, not frames: no shared script, no opener, no postMessage. Each is sequenced only by polling a cue endpoint on its own origin. They are two windows of one browser instance, because the shared GPU process’s D3D11 texture recycler is the boundary under test — two separate browser instances would have two GPU processes and nothing to show. Site isolation still puts the two origins in separate renderer processes.
Each round generates eight fresh synthetic images from a new random seed, serves them over a single Host-gated endpoint on the victim origin only, and compares server-side against every image of every round. The attacker page never receives an image, a hash, or any derived value; it POSTs back the raw RGBA it sampled out of the GPU, and only asks what it was after that submission.
Observed result
One freshly launched browser per round, on Stable 153.0.8010.36 / NVIDIA D3D11:
| Round | Mode | Verdict | Exact pixels | Bytes |
|---|---|---|---|---|
| 0 | no-victim-control | ATTACKER_OWN_FILL_PRESERVED | 0 / 8192 | 0 |
| 1–5 | leak | WHOLE_VICTIM_IMAGE_RECOVERED (each) | 8192 / 8192 | 32,768 each |
| 6 | leak | ALL_ZERO | 0 / 8192 | 0 |
| 7 | complete-base-control | ATTACKER_OWN_FILL_PRESERVED | 0 / 8192 | 0 |
5 of 6 leak rounds recovered a whole victim image — 163,840 bytes, each from its own round’s freshly randomised images, so this is repeatability on new data rather than one lucky allocation. Across the two-window runs on this host, 13 of 15 leak rounds recovered a whole image.
Recovery is not deterministic: the recycler may return a zeroed block instead, and such a round is inconclusive rather than a refutation. The negative claim rests on the controls, which were clean in every run. no-victim-control runs the identical attacker sequence with the victim window never cued. complete-base-control differs from a leak round only in giving the selected base a legal height of 1 instead of 0, which isolates the zero dimension as the cause.
Zero-depth trigger
The same defect is reachable through a zero depth base instead of a zero height. Measured separately on the same build, that variant recovered 16384 / 16384 exact victim pixels (65,536 bytes) cross-site, with matched no-victim and complete-base controls clean. The attached PoC exercises the zero-height form; the zero-depth form differs only in which dimension of the selected base level is left at zero.
FIX SUGGESTION
- Do not equate a logical zero dimension with one when deciding whether Texture3D storage can survive a base transition. The clamp in
TextureD3D::setBaseLevel’s identity comparison is the root. - During
releaseTexStorage, preserve every requested initialized old-storage subresource without filtering it through post-transition completeness. If it cannot be preserved, mark it uninitialized so robust initialization clears it before any later use — never leave frontend state claiming initialized over a recycled allocation. - Add D3D11 regression tests for zero-width, zero-height and zero-depth base transitions across 3D, cube and 2D-array textures, including descriptor-matched cross-context reuse, no-victim, complete-base, and a non-D3D control.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: N/A. This is a data disclosure; no process crashed. No memory corruption and no AddressSanitizer report. All accepted rows completed with zero GL errors and no context loss.
Crash State: N/A
Client ID: N/A
CREDIT INFORMATION
Reporter credit: JonathanBouman
RELATIONSHIP TO EXISTING BUGS
These are the security-impacting Texture3D forms of the generic current-completeness preservation defect, which on its own produced initialized data loss but recovered no foreign bytes.
If this is closed as a duplicate, please close it against a bug that actually covers TEXTURE_3D. b/553857574 does not: its fix is confined to the 2D-array layer count, and the zero-height Texture3D trigger still returned a whole victim volume on current Canary. The Upstream status section above records the tip verification and the five commits that have touched this file since Chrome 153’s ANGLE pin without addressing it. b/536664909 is adjacent — c477cf31f7a restructured the completeness check under it — but that change centralises the rejection this report relies on rather than repairing the preservation path.
The zero-size cube and 2D-array zero-height variants share this same root and are not separate reports.
Separately, 550626693 from the same reporter is not related beyond the shared “ANGLE D3D11 cross-origin GPU memory” phrasing: that one is the Buffer11 StructuredBuffer SRV size cache, this one is TextureD3D storage release.