Low watch CVSS 4.7 chrome OOB 🔧 Commit mapped

Overview

Low
Severity
4.7
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Sub-componentD3D11
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker541546782
Fix commit651089f2f55b (angle/angle) +169/-54
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedHyeongeun Ji of JeroScope
Reported2026-08-01 (per vendor advisory)
Fix landed2026-08-14 (public git)
Disclosed2026-09-08
Patch gap25 days — fix public in source before the advisory

Background

ANGLE D3D11 backend
ANGLE translates OpenGL ES calls onto Direct3D 11, where each texture mip level is backed by a CPU-side Image11 staging resource and a GPU-side TextureStorage11 (mTexStorage) subresource.
`isImageComplete(index)`
a TextureD3D predicate that reports whether the image at a given gl::ImageIndex has 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 Image11 staging 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.

Key insight
The core mistake was zero-initializing and committing a stale/incomplete mip level through the D3D11 device context using the image’s dimensions against a mismatched storage subresource; the fix gates every storage-touching path on isImageComplete(index) and moves the clear into Image11::initializeContents, which operates only within the image’s own mapped bounds.

Attack Path

  1. 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.
  2. Redefine the base to break completeness Define level 0 via glCopyTexImage2D/glTexImage2D at dimensions that make level 1 incomplete relative to the expected mip chain, so isImageComplete(index) is false for that level.
  3. Trigger robust init on the stale level Issue glTexSubImage2D on level 1, which drives ensureSubImageInitialized → TextureD3D::initializeContents, where the pre-patch code takes the shouldUseSetData/commitRegion path against the mismatched storage.
  4. 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

An attacker able to run WebGL/WebGL2 in the renderer (any web page) can drive ANGLE’s D3D11 backend into an out-of-bounds read, yielding uninitialized/garbage texture readbacks and potential process crashes; the disclosed severity is limited to an out-of-bounds read in ANGLE. The preconditions are the Windows/D3D11 ANGLE backend with robust resource initialization active and a texture crafted so a stale mip level fails 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

FunctionChangeNotes
TextureSizeLimitTest
src/tests/gl_tests/TextureTest.cpp
modified

Files Changed

  • src/tests/gl_tests/TextureTest.cpp
  • src/libANGLE/renderer/d3d/d3d11/Image11.cpp
  • src/libANGLE/renderer/d3d/d3d11/Image11.h
  • src/libANGLE/renderer/d3d/TextureD3D.cpp
  • src/libANGLE/renderer/d3d/ImageD3D.h
  • src/libANGLE/renderer/d3d/TextureD3D.h

Audit Directions

  • Dimension source mismatch
    Audit any D3D11 copy/setData/UpdateSubresource/CopySubresourceRegion path that sizes a source region from the Image while writing into an mTexStorage subresource, and confirm both sides agree before the device-context call.
  • Completeness assumptions on init paths
    Search other TextureD3D/TextureStorage operations (mipmap generation, updateStorage, ensureRenderTarget) that assume a level is complete without an isImageComplete(index) / isValidIndex(index) guard before touching storage.
  • Map flag correctness
    Review 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.
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 &region)
 {
-    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:
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.