Chrome · ANGLE
CVE-2026-87602
OOB in ANGLE
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/libANGLE/renderer/d3d/ImageD3D.hsrc/libANGLE/renderer/d3d/TextureD3D.cppsrc/libANGLE/renderer/d3d/TextureD3D.hsrc/libANGLE/renderer/d3d/d3d11/Image11.cpp
Patch
From 651089f2f55b3ea2ce8bcc51328dbf567343a398 Mon Sep 17 00:00:00 2001 From: wangra <[email protected]> Date: Thu, 06 Aug 2026 15:59:38 -0400 Subject: [PATCH] D3D11: Fix crash and garbage readbacks on incomplete levels Skip commits on incomplete mip levels to prevent device context out-of-bounds reads. Delegate initialization to Image11 on incomplete or non-renderable levels to avoid redundant clears on full uploads. Test: angle_end2end_tests --gtest_filter="*MismatchedStaleLevelTexSubImage*" Bug: b/541546782 Change-Id: I2252c50b6a13925a86fbb720710c0919a7e96600 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8214667 Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Ran Wang <[email protected]> --- diff --git a/src/libANGLE/renderer/d3d/ImageD3D.h b/src/libANGLE/renderer/d3d/ImageD3D.h index ac58ca5..f7ef13b 100644 --- a/src/libANGLE/renderer/d3d/ImageD3D.h +++ b/src/libANGLE/renderer/d3d/ImageD3D.h @@ -63,6 +63,8 @@ const gl::Box &area, const void *input) = 0; + virtual angle::Result initializeContents(const gl::Context *context) = 0; + virtual angle::Result setManagedSurface2D(const gl::Context *context, TextureStorage *storage, int level); diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp index 07f4bfc..53dd6a8 100644 --- a/src/libANGLE/renderer/d3d/TextureD3D.cpp +++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp @@ -281,8 +281,13 @@ return angle::Result::Continue; } -bool TextureD3D::shouldUseSetData(const ImageD3D *image) const +bool TextureD3D::shouldUseSetData(const gl::ImageIndex &index, const ImageD3D *image) const { + if (!isImageComplete(index)) + { + return false; + } + if (!mRenderer->getFeatures().setDataFasterThanImageUpload.enabled) { return false; @@ -338,7 +343,7 @@ if (pixelData != nullptr) { - if (shouldUseSetData(image)) + if (shouldUseSetData(index, image)) { ANGLE_TRY( mTexStorage->setData(context, index, image, nullptr, type, unpack, pixelData)); @@ -376,7 +381,7 @@ ImageD3D *image = getImage(index); ASSERT(image); - if (shouldUseSetData(image) && !mTexStorage->isMultiplanar(context)) + if (shouldUseSetData(index, image) && !mTexStorage->isMultiplanar(context)) { return mTexStorage->setData(context, index, image, &area, type, unpack, pixelData); } @@ -725,7 +730,7 @@ const gl::ImageIndex &index, const gl::Box ®ion) { - if (mTexStorage) + if (mTexStorage && isImageComplete(index)) { ASSERT(isValidIndex(index)); ImageD3D *image = getImage(index); @@ -882,8 +887,7 @@ GLenum binding, const gl::ImageIndex &imageIndex) { - ContextD3D *contextD3D = GetImplAs<ContextD3D>(context); - gl::ImageIndex index = imageIndex; + gl::ImageIndex index = imageIndex; // Special case for D3D11 3D textures. We can't create render targets for individual layers of a // 3D texture, so force the clear to the entire mip. There shouldn't ever be a case where we @@ -951,55 +955,18 @@ ASSERT(image != nullptr); - // Slow path: non-renderable texture or the texture levels aren't set up. - const auto &formatInfo = gl::GetSizedInternalFormatInfo(image->getInternalFormat()); + // Slow path: non-renderable texture, incomplete level, or texture storage doesn't exist. + ANGLE_TRY(image->initializeContents(context)); - GLuint imageBytes = 0; - if (formatInfo.compressed) - { - ANGLE_CHECK_GL_MATH( - contextD3D, formatInfo.computeCompressedImageSize( - gl::Extents(image->getWidth(), image->getHeight(), image->getDepth()), - &imageBytes)); - } - else - { - ANGLE_CHECK_GL_MATH(contextD3D, formatInfo.computeRowPitch( - formatInfo.type, image->getWidth(), 1, 0, &imageBytes)); - - angle::CheckedNumeric<GLuint> checkedImageBytes(imageBytes); - checkedImageBytes *= image->getHeight(); - checkedImageBytes *= image->getDepth(); - ANGLE_CHECK_GL_MATH(contextD3D, checkedImageBytes.AssignIfValid(&imageBytes)); - } - - gl::PixelUnpackState zeroDataUnpackState; - zeroDataUnpackState.alignment = 1; - - const angle::MemoryBuffer *zeroBuffer = nullptr; - ANGLE_CHECK_GL_ALLOC(contextD3D, context->getZeroFilledBuffer(imageBytes, &zeroBuffer)); - - if (shouldUseSetData(image)) - { - ANGLE_TRY(mTexStorage->setData(context, index, image, nullptr, formatInfo.type, - zeroDataUnpackState, zeroBuffer->data())); - } - else + if (mTexStorage && isImageComplete(index)) { gl::Box fullImageArea(0, 0, 0, image->getWidth(), image->getHeight(), image->getDepth()); - ANGLE_TRY(image->loadData(context, fullImageArea, zeroDataUnpackState, formatInfo.type, - zeroBuffer->data(), false)); - - // Force an update to the tex storage so we avoid problems with subImage and dirty regions. - if (mTexStorage) - { - ANGLE_TRY(commitRegion(context, index, fullImageArea)); - image->markClean(); - } - else - { - mDirtyImages = true; - } + ANGLE_TRY(commitRegion(context, index, fullImageArea)); + image->markClean(); + } + else + { + mDirtyImages = true; } return angle::Result::Continue; } diff --git a/src/libANGLE/renderer/d3d/TextureD3D.h b/src/libANGLE/renderer/d3d/TextureD3D.h index 499a9fe..0bbef39 100644 --- a/src/libANGLE/renderer/d3d/TextureD3D.h +++ b/src/libANGLE/renderer/d3d/TextureD3D.h @@ -227,7 +227,7 @@ virtual angle::Result updateStorage(const gl::Context *context) = 0; - bool shouldUseSetData(const ImageD3D *image) const; + bool shouldUseSetData(const gl::ImageIndex &index, const ImageD3D *image) const; angle::Result generateMipmapUsingImages(const gl::Context *context, const GLuint maxLevel); diff --git a/src/libANGLE/renderer/d3d/d3d11/Image11.cpp b/src/libANGLE/renderer/d3d/d3d11/Image11.cpp index 66fd0a3..c9b1c48 100644 --- a/src/libANGLE/renderer/d3d/d3d11/Image11.cpp +++ b/src/libANGLE/renderer/d3d/d3d11/Image11.cpp @@ -299,7 +299,9 @@ LoadImageFunction loadFunction = d3dFormatInfo.getLoadFunctions()(type).loadFunction; D3D11_MAPPED_SUBRESOURCE mappedImage; - ANGLE_TRY(map(context, D3D11_MAP_WRITE, &mappedImage)); + // Map as read-write to prevent the driver from discarding the manual zero-initialization on + // unwritten pixels. + ANGLE_TRY(map(context, D3D11_MAP_READ_WRITE, &mappedImage)); uint8_t *offsetMappedData = (ANGLE_UNSAFE_TODO(static_cast<uint8_t *>(mappedImage.pData) + @@ -359,6 +361,52 @@ return angle::Result::Continue; } +angle::Result Image11::initializeContents(const gl::Context *context) +{ + const d3d11::Format &formatInfo = + d3d11::Format::Get(mInternalFormat, mRenderer->getRenderer11DeviceCaps()); + + D3D11_MAPPED_SUBRESOURCE mappedImage; + ANGLE_TRY(map(context, D3D11_MAP_WRITE, &mappedImage)); + + if (formatInfo.dataInitializerFunction != nullptr) + { + formatInfo.dataInitializerFunction(mWidth, mHeight, mDepth, + static_cast<uint8_t *>(mappedImage.pData), + mappedImage.RowPitch, mappedImage.DepthPitch); + } + else + { + const d3d11::DXGIFormatSize &dxgiFormatInfo = d3d11::GetDXGIFormatSizeInfo(mDXGIFormat);
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 d2cd82d..70ae2ba 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -22640,6 +22640,103 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
}
+// Test that robust initialization of a mismatched stale texture level during texSubImage2D
+// succeeds and does not cause a crash/OOB read.
+TEST_P(Texture2DTestES3RobustInit, MismatchedStaleLevelTexSubImage)
+{
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+
+ // Mip level 1 is 128 x 1 RGBA8 with null pixels.
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 128, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ // Set up framebuffer to copy from (for copyTexImage2D).
+ // We need a 512 x 128 source.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ GLRenderbuffer rbo;
+ glBindRenderbuffer(GL_RENDERBUFFER, rbo);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 512, 128);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
+ EXPECT_GL_NO_ERROR();
+
+ // Define level 0 using copyTexImage2D.
+ glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 512, 128, 0);
+ EXPECT_GL_NO_ERROR();
+
+ // Partially update level 1 with texSubImage2D.
+ // This triggers ensureSubImageInitialized on level 1, which should not crash.
+ const GLColor updateData = GLColor::blue;
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &updateData);
+ EXPECT_GL_NO_ERROR();
+
+ // Redefine level 0 to 256 x 2. This makes level 1 (128 x 1) mip-compatible with level 0.
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 256, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ EXPECT_GL_NO_ERROR();
+
+ // Bind level 1 to the framebuffer and read pixels.
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1);
+ EXPECT_GL_NO_ERROR();
+
+ // Level 1 was only partially initialized. Verify the uploaded data. Additionally, verify
+ // the rest of the level is robust-initialized transparentBlack.
+ EXPECT_PIXEL_COLOR_EQ(1, 0, GLColor::transparentBlack);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+}
+
+// Test that robust initialization of a mismatched stale texture level during texSubImage2D
+// succeeds, does not cause a crash/OOB read, and preserves full level sub-image updates.
+TEST_P(Texture2DTestES3RobustInit, MismatchedStaleLevelTexSubImageFull)
+{
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+
+ // Mip level 1 is 128 x 1 RGBA8 with null pixels.
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 128, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ // Set up framebuffer to copy from (for copyTexImage2D).
+ // We need a 512 x 128 source.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ GLRenderbuffer rbo;
+ glBindRenderbuffer(GL_RENDERBUFFER, rbo);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 512, 128);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
+ EXPECT_GL_NO_ERROR();
+
+ // Define level 0 using copyTexImage2D.
+ glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 512, 128, 0);
+ EXPECT_GL_NO_ERROR();
+
+ // Fully update level 1 with texSubImage2D.
+ std::vector<GLColor> updateData(128, GLColor::blue);
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 128, 1, GL_RGBA, GL_UNSIGNED_BYTE, updateData.data());
+ EXPECT_GL_NO_ERROR();
+
+ // Redefine level 0 to 256 x 2. This makes level 1 (128 x 1) mip-compatible with level 0.
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 256, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ EXPECT_GL_NO_ERROR();
+
+ // Bind level 1 to the framebuffer and read pixels.
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1);
+ EXPECT_GL_NO_ERROR();
+
+ // Check that level 1 has the updated blue pixel since it was a full update.
+ EXPECT_PIXEL_RECT_EQ(0, 0, 128, 1, GLColor::blue);
+}
+
class TextureSizeLimitTest : public ANGLETest<>
{
protected:
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