CVE-2026-87602
Overview
Background
- ANGLE D3D11 backend
- ANGLE translates OpenGL ES calls onto Direct3D 11, where each texture mip level is backed by a CPU-side
Image11staging resource and a GPU-sideTextureStorage11(mTexStorage) subresource. - `isImageComplete(index)`
- a
TextureD3Dpredicate that reports whether the image at a givengl::ImageIndexhas dimensions consistent with the texture’s mip chain (i.e. the level is a valid, non-stale member of the storage). - Robust resource initialization
- the OpenGL ES / Chrome feature that zero-fills any not-yet-written texel region before it can be read, implemented for D3D11 in
TextureD3D::initializeContents. - `shouldUseSetData` / `mTexStorage->setData`
- a fast upload path that pushes pixel data straight into the D3D11 storage subresource via the device context instead of round-tripping through the
Image11staging texture.
Root Cause Analysis
When robust initialization ran on a texture level, the pre-patch TextureD3D::initializeContents (inferred name; the hunk header shows only its GLenum binding, const gl::ImageIndex &imageIndex parameters) sized a zero-filled buffer from the ImageD3D’s own getWidth()/getHeight()/getDepth() and then, if shouldUseSetData(image) returned true, handed that buffer to mTexStorage->setData(...) — and unconditionally called commitRegion on the full image area otherwise. The invariant this violated is that a mip level can be incomplete/stale: its Image11 dimensions need not match the storage subresource it is committed into (e.g. level 1 defined as 128x1 while level 0 is later redefined to make the chain expect a different size). Pushing an image-sized source region into a mismatched storage subresource through the D3D11 device context caused it to read past the source and produce the out-of-bounds read / garbage readback.
The fix tightens the path: shouldUseSetData now returns false up front when !isImageComplete(index), commitRegion/initializeContents only touch mTexStorage when mTexStorage && isImageComplete(index), and the actual zeroing is delegated to the new Image11::initializeContents, which maps and clears only the image’s own staging bytes (clamping bytesToClear to the mapped RowPitch). Because incomplete levels never reach the device-context commit and are cleared purely within their own bounds, the OOB read is eliminated while completed levels are still committed correctly.
isImageComplete(index) and moves the clear into Image11::initializeContents, which operates only within the image’s own mapped bounds.Attack Path
- Create a mismatched level Define mip level 1 (e.g. 128x1 RGBA8) with null data so it is a stale, standalone level not consistent with the rest of the mip chain.
- Redefine the base to break completeness
Define level 0 via
glCopyTexImage2D/glTexImage2Dat dimensions that make level 1 incomplete relative to the expected mip chain, soisImageComplete(index)is false for that level. - Trigger robust init on the stale level
Issue
glTexSubImage2Don level 1, which drivesensureSubImageInitialized→TextureD3D::initializeContents, where the pre-patch code takes theshouldUseSetData/commitRegionpath against the mismatched storage. - Out-of-bounds read in the device context The image-sized zero/source region is copied into the mismatched storage subresource via the D3D11 device context, reading beyond the intended bounds and returning garbage on subsequent readback (crash/OOB read as reported by the advisory).
Impact Assessment
isImageComplete. Any escalation to information disclosure of adjacent memory or a sandbox escape is (inferred) and not demonstrated by the patch, which only shows the mismatched device-context copy.Changed Functions
| Function | Change | Notes |
|---|---|---|
TextureSizeLimitTestsrc/tests/gl_tests/TextureTest.cpp |
modified |
Files Changed
src/tests/gl_tests/TextureTest.cppsrc/libANGLE/renderer/d3d/d3d11/Image11.cppsrc/libANGLE/renderer/d3d/d3d11/Image11.hsrc/libANGLE/renderer/d3d/TextureD3D.cppsrc/libANGLE/renderer/d3d/ImageD3D.hsrc/libANGLE/renderer/d3d/TextureD3D.h
Audit Directions
- Dimension source mismatchAudit any D3D11 copy/
setData/UpdateSubresource/CopySubresourceRegionpath that sizes a source region from theImagewhile writing into anmTexStoragesubresource, and confirm both sides agree before the device-context call. - Completeness assumptions on init pathsSearch other
TextureD3D/TextureStorageoperations (mipmap generation,updateStorage,ensureRenderTarget) that assume a level is complete without anisImageComplete(index)/isValidIndex(index)guard before touching storage. - Map flag correctnessReview other
map(D3D11_MAP_WRITE, ...)uses that perform partial or manual initialization, since a write-only map can let the driver discard unwritten bytes and re-expose the uninitialized-read class this patch closed.
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);
+ GLuint outputBlockWidth = dxgiFormatInfo.blockWidth;
+ GLuint outputBlockHeight = dxgiFormatInfo.blockHeight;
+ GLuint outputPixelSize = dxgiFormatInfo.pixelBytes;
+
+ GLuint numBlocksWide = (mWidth + outputBlockWidth - 1) / outputBlockWidth;
+ GLuint blockHeight = (mHeight + outputBlockHeight - 1) / outputBlockHeight;
+ size_t rowBytes = numBlocksWide * outputPixelSize;
+
+ for (GLsizei z = 0; z < mDepth; ++z)
+ {
+ uint8_t *sliceData = ANGLE_UNSAFE_BUFFERS(static_cast<uint8_t *>(mappedImage.pData) +
+ z * mappedImage.DepthPitch);
+ for (size_t y = 0; y < blockHeight; ++y)
+ {
+ uint8_t *rowData = ANGLE_UNSAFE_BUFFERS(sliceData + y * mappedImage.RowPitch);
+ size_t bytesToClear =
+ std::min<size_t>(rowBytes, static_cast<size_t>(mappedImage.RowPitch));
+ angle::Span<uint8_t> clearSpan =
+ ANGLE_UNSAFE_BUFFERS(angle::Span<uint8_t>(rowData, bytesToClear));
+ std::fill(clearSpan.begin(), clearSpan.end(), static_cast<uint8_t>(0));
+ }
+ }
+ }
+
+ unmap();
+ mDirty = true;
+ return angle::Result::Continue;
+}
+
angle::Result Image11::copyFromTexStorage(const gl::Context *context,
const gl::ImageIndex &imageIndex,
TextureStorage *source)
diff --git a/src/libANGLE/renderer/d3d/d3d11/Image11.h b/src/libANGLE/renderer/d3d/d3d11/Image11.h
index cde20d5..b784620 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Image11.h
+++ b/src/libANGLE/renderer/d3d/d3d11/Image11.h
@@ -76,6 +76,7 @@
angle::Result loadCompressedData(const gl::Context *context,
const gl::Box &area,
const void *input) override;
+ angle::Result initializeContents(const gl::Context *context) override;
angle::Result copyFromTexStorage(const gl::Context *context,
const gl::ImageIndex &imageIndex,
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:
Regression Test / PoC
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: