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 <[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); + 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: