Low chrome OOB 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker541546782
Fix commit651089f2f55b (angle/angle) +169/-54
CISA KEVNot listed
CreditedHyeongeun Ji of JeroScope
Disclosed2026-09-08

Files Changed

  • src/libANGLE/renderer/d3d/ImageD3D.h
  • src/libANGLE/renderer/d3d/TextureD3D.cpp
  • src/libANGLE/renderer/d3d/TextureD3D.h
  • src/libANGLE/renderer/d3d/d3d11/Image11.cpp
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 &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);
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.