High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in ANGLE
DescriptionType Confusion in ANGLE
ComponentANGLE
Bug ClassType Confusion
Tracker506374676
Fix commita469cc1501fd (angle/angle) +58/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/TextureTest.cpp
From a469cc1501fda3ca0e8fb4638e0e92e69b390d43 Mon Sep 17 00:00:00 2001
From: Tzarial <[email protected]>
Date: Thu, 30 Apr 2026 19:45:54 +0000
Subject: [PATCH] Fix type confusion in Buffer11::PackStorage staging cache

When `Buffer11::PackStorage::packPixels` reused a cached staging
texture, it previously only validated that the format and dimensions
(width, height, depth) matched the source texture. Since
`createStagingTexture` forces a depth of 1 for 3D textures, a 3D
staging texture of size WxHx1 would incorrectly match a 2D source
(or 2D array layer) of the same WxH.

This type confusion resulted in a 3D staging texture being used for a
2D source. Furthermore, because `mStagingTexture.is3D()` was checked
instead of `srcTexture->is3D()`, `srcBox.front` was incorrectly set
to the layer index. This caused an invalid `CopySubresourceRegion`
call with a depth-OOB `pSrcBox` on a 2D source and a 3D/2D resource
dimension mismatch, leading to undefined behavior in the D3D11 driver
(potentially GPU memory corruption or OOB reads).

This CL fixes the issue by:
1. Adding a `getTextureType()` check to the cache invalidation
   predicate to ensure the staging texture type strictly matches the
   source type.
2. Checking `srcTexture->is3D()` instead of `mStagingTexture.is3D()`
   to determine if the Z-offset (layer) should be applied to `srcBox`.

Bug: b/506374676
Change-Id: I6e451cc2f10cc038d291bd88585ed2ba8ce1f3f6
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7809144
Reviewed-by: Geoff Lang <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
index 1cbfd32..6dff1d3 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
@@ -1636,8 +1636,10 @@
 
     gl::Extents srcTextureSize(params.area.width, params.area.height, 1);
     if (!mStagingTexture.get() || mStagingTexture.getFormat() != srcTexture->getFormat() ||
-        mStagingTexture.getExtents() != srcTextureSize)
+        mStagingTexture.getExtents() != srcTextureSize ||
+        mStagingTexture.getTextureType() != srcTexture->getTextureType())
     {
+        mStagingTexture.reset();
         ANGLE_TRY(mRenderer->createStagingTexture(context, srcTexture->getTextureType(),
                                                   srcTexture->getFormatSet(), srcTextureSize,
                                                   StagingAccess::READ, &mStagingTexture));
@@ -1652,7 +1654,7 @@
 
     // Select the correct layer from a 3D attachment
     srcBox.front = 0;
-    if (mStagingTexture.is3D())
+    if (srcTexture->is3D())
     {
         srcBox.front = static_cast<UINT>(readAttachment.layer());
     }
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 399b0fd..87e2666 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -361,6 +361,7 @@
 42264556 MAC AMD OPENGL : IndexBufferOffsetTestES3.UseAsUBOThenUpdateThenUInt8Index/* = SKIP
 42264556 MAC AMD OPENGL : IndexBufferOffsetTestES3.UseAsUBOThenUpdateThenUInt8IndexSmallUpdates/* = SKIP
 42264591 MAC NVIDIA METAL : BlitFramebufferTest.OOBWrite/* = SKIP
+509308437 MAC OPENGL : Texture2DTestES3.PackPixels3DAnd2DArrayTypeConfusion/* = SKIP
 42264655 MAC OPENGL : GLSLTestLoops.*ContinueInSwitch/* = SKIP
 42264671 MAC OPENGL : BlitFramebufferTest.BlitDepthStencilPixelByPixel/* = SKIP
 42264755 MAC OPENGL : BufferDataTestES3.DrawWithNotCallingBufferData/* = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index c562a86..b1cb37b 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -18028,6 +18028,59 @@
     glDrawElementsInstanced(GL_TRIANGLES, 4, GL_UNSIGNED_SHORT, 0, 1);
 }
 
+// Tests that packing pixels into the same PBO from a 3D texture and then a 2D array texture
+// works.  Regression test for a bug in the D3D11 backend with the staging texture cache.
+TEST_P(Texture2DTestES3, PackPixels3DAnd2DArrayTypeConfusion)
+{
+    // PIXEL_PACK_BUFFER
+    GLBuffer pbo;
+    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+    glBufferData(GL_PIXEL_PACK_BUFFER, 64 * 64 * 4, nullptr, GL_STREAM_READ);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    // 1. Create a 3D texture and read from it
+    GLTexture tex3d;
+    glBindTexture(GL_TEXTURE_3D, tex3d);
+    glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 64, 64, 4);
+
+    // Fill to ensure FBO is complete
+    std::vector<GLubyte> emptyData(64 * 64 * 4 * 4, 0);
+    glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 64, 64, 4, GL_RGBA, GL_UNSIGNED_BYTE,
+                    emptyData.data());
+
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex3d, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Prime the staging cache with a 3D texture
+    glReadPixels(0, 0, 64, 64, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+    EXPECT_GL_NO_ERROR();
+
+    // 2. Create a 2D array texture and read from it
+    GLTexture tex2a;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, tex2a);
+    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 64, 64, 4);
+
+    // Fill layer 2 with specific data
+    std::vector<GLubyte> expectData(64 * 64 * 4, 128);
+    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, 64, 64, 1, GL_RGBA, GL_UNSIGNED_BYTE,
+                    expectData.data());
+
+    // Read back the same area into the same PBO again. D3D11 backend previously hit UB here.
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex2a, 0, 2);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    glReadPixels(0, 0, 64, 64, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+    EXPECT_GL_NO_ERROR();
+
+    // Verify the data was read correctly
+    void *mapPointer = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, 64 * 64 * 4, GL_MAP_READ_BIT);
+    ASSERT_NE(nullptr, mapPointer);
+    EXPECT_EQ(0, memcmp(mapPointer, expectData.data(), 64 * 64 * 4));
+    glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+}
+
 // Checks that drawing incomplete zero texture buffer does not crash.
 TEST_P(TextureBufferTestES31, DrawIncompleteZeroTexture)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 399b0fd..87e2666 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -361,6 +361,7 @@
 42264556 MAC AMD OPENGL : IndexBufferOffsetTestES3.UseAsUBOThenUpdateThenUInt8Index/* = SKIP
 42264556 MAC AMD OPENGL : IndexBufferOffsetTestES3.UseAsUBOThenUpdateThenUInt8IndexSmallUpdates/* = SKIP
 42264591 MAC NVIDIA METAL : BlitFramebufferTest.OOBWrite/* = SKIP
+509308437 MAC OPENGL : Texture2DTestES3.PackPixels3DAnd2DArrayTypeConfusion/* = SKIP
 42264655 MAC OPENGL : GLSLTestLoops.*ContinueInSwitch/* = SKIP
 42264671 MAC OPENGL : BlitFramebufferTest.BlitDepthStencilPixelByPixel/* = SKIP
 42264755 MAC OPENGL : BufferDataTestES3.DrawWithNotCallingBufferData/* = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index c562a86..b1cb37b 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -18028,6 +18028,59 @@
     glDrawElementsInstanced(GL_TRIANGLES, 4, GL_UNSIGNED_SHORT, 0, 1);
 }
 
+// Tests that packing pixels into the same PBO from a 3D texture and then a 2D array texture
+// works.  Regression test for a bug in the D3D11 backend with the staging texture cache.
+TEST_P(Texture2DTestES3, PackPixels3DAnd2DArrayTypeConfusion)
+{
+    // PIXEL_PACK_BUFFER
+    GLBuffer pbo;
+    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+    glBufferData(GL_PIXEL_PACK_BUFFER, 64 * 64 * 4, nullptr, GL_STREAM_READ);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    // 1. Create a 3D texture and read from it
+    GLTexture tex3d;
+    glBindTexture(GL_TEXTURE_3D, tex3d);
+    glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 64, 64, 4);
+
+    // Fill to ensure FBO is complete
+    std::vector<GLubyte> emptyData(64 * 64 * 4 * 4, 0);
+    glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 64, 64, 4, GL_RGBA, GL_UNSIGNED_BYTE,
+                    emptyData.data());
+
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex3d, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Prime the staging cache with a 3D texture
+    glReadPixels(0, 0, 64, 64, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+    EXPECT_GL_NO_ERROR();
+
+    // 2. Create a 2D array texture and read from it
+    GLTexture tex2a;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, tex2a);
+    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 64, 64, 4);
+
+    // Fill layer 2 with specific data
+    std::vector<GLubyte> expectData(64 * 64 * 4, 128);
+    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, 64, 64, 1, GL_RGBA, GL_UNSIGNED_BYTE,
+                    expectData.data());
+
+    // Read back the same area into the same PBO again. D3D11 backend previously hit UB here.
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex2a, 0, 2);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    glReadPixels(0, 0, 64, 64, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+    EXPECT_GL_NO_ERROR();
+
+    // Verify the data was read correctly
+    void *mapPointer = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, 64 * 64 * 4, GL_MAP_READ_BIT);
+    ASSERT_NE(nullptr, mapPointer);
+    EXPECT_EQ(0, memcmp(mapPointer, expectData.data(), 64 * 64 * 4));
+    glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+}
+
 // Checks that drawing incomplete zero texture buffer does not crash.
 TEST_P(TextureBufferTestES31, DrawIncompleteZeroTexture)
 {
Loading diff…

Original Bug Report

reported by [email protected]

Potential Type Confusion and OOB Read in ANGLE D3D11 Buffer11::PackStorage

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: ANGLE’s D3D11 backend caches a staging texture for pixel packing operations but fails to validate the ResourceType when reusing it. A cached 3D staging texture can be incorrectly reused for a 2D Array texture, leading to an invalid CopySubresourceRegion call with out-of-bounds Z-coordinates, potentially resulting in GPU memory corruption or information leaks.

Affected files:

  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp

Estimated timestamp from git blame: 2026-01-14

Summary

A potential type confusion vulnerability exists in the ANGLE renderer’s D3D11 backend, specifically within the Buffer11::PackStorage::packPixels function. The vulnerability arises from an insufficient validation check when reusing a cached staging texture. This failure allows a 3D staging texture to be reused for 2D or 2D-array source textures, leading to a mismatch between resource types and the use of out-of-bounds (OOB) box parameters in D3D11 driver calls. This can result in driver-dependent memory corruption or out-of-bounds reads within the GPU process.

Root Cause Analysis

In third_party/angle/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp, the PackStorage::packPixels function caches a staging texture (mStagingTexture) used for reading pixels into a Pixel Buffer Object (PBO). When a readPixels operation occurs, the code checks if the cached staging texture can be reused:

gl::Extents srcTextureSize(params.area.width, params.area.height, 1);
if (!mStagingTexture.get() || mStagingTexture.getFormat() != srcTexture->getFormat() ||
    mStagingTexture.getExtents() != srcTextureSize)
{
    ANGLE_TRY(mRenderer->createStagingTexture(context, srcTexture->getTextureType(),
                                              srcTexture->getFormatSet(), srcTextureSize,
                                              StagingAccess::READ, &mStagingTexture));
}

This cache check verifies the format and the dimensions (width, height, and depth). However, it fails to verify that the ResourceType (e.g., Texture2D vs Texture3D) matches.

Crucially, Renderer11::createStagingTexture for 3D textures (in Renderer11.cpp) hard-codes the depth of the staging texture to 1. Consequently, a 3D staging texture will have extents {W, H, 1}, which identically matches the extents of a 2D texture (or 2D-array layer) of the same width and height, causing a cache hit.

If a 3D staging texture is cached and then a 2D-array texture is used for a subsequent pack operation, the code proceeds to calculate the source box:

srcBox.front = 0;
if (mStagingTexture.is3D())
{
    srcBox.front = static_cast<UINT>(readAttachment.layer());
}
srcBox.back = srcBox.front + 1;

Because mStagingTexture is a stale 3D texture, is3D() returns true even if the current source is a 2D-array. If the operation involves a 2D-array attachment at layer $N \ge 1$, srcBox.front is set to $N$.

The subsequent call to CopySubresourceRegion uses this srcBox against a source subresource that only has a depth of 1 (valid Z-coordinates $[0, 1)$), resulting in a depth-OOB access and a resource dimension mismatch (3D destination vs 2D source). For a 2D array texture, the layer is encoded in the D3D11 subresource index, so the Z coordinate for the box should be 0.

Potential Impact

This vulnerability is reachable from any WebGL2 context on Windows (where D3D11 is the default ANGLE backend). By triggering these invalid driver calls with attacker-controlled parameters (layer index, width, height, and format), an attacker can cause driver-dependent undefined behavior in the GPU process. Practical impacts include out-of-bounds reads of GPU memory (potentially leaking cross-origin graphics data) or memory corruption. The GPU process is sandboxed on Windows, but it is shared across different origins.

Suggested Steps to Reproduce

Note: Our tooling agent doesn’t yet have the ability to run code, so these steps are suggested based on static analysis.

  1. Use a WebGL2 context on Windows (D3D11 backend).
  2. Bind a PBO: gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);.
  3. Create a 3D texture and a 2D array texture with the same width ($W$), height ($H$), and format.
  4. Attach layer 0 of the 3D texture to an FBO.
  5. Perform a readPixels operation from the 3D texture layer to prime the cache with a 3D staging texture of size $W \times H \times 1$.
  6. Attach layer $N \ge 1$ of the 2D-array texture to the FBO.
  7. Perform a readPixels operation from the 2D-array texture layer with the same width ($W$), height ($H$), and format.
  8. Observe that the second call reuses the 3D staging texture and incorrectly sets srcBox.front = N in the CopySubresourceRegion call, triggering the vulnerability.

Suggested Fix

Update the cache validation check in Buffer11::PackStorage::packPixels to also verify that the resource types match:

if (!mStagingTexture.get() || mStagingTexture.getFormat() != srcTexture->getFormat() ||
    mStagingTexture.getExtents() != srcTextureSize ||
    mStagingTexture.getTextureType() != srcTexture->getTextureType())

Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker