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 <wangra@google.com>
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 <geofflang@chromium.org>
Commit-Queue: Ran Wang <wangra@google.com>
---
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