High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in ANGLE
DescriptionUninitialized resource in ANGLE
ComponentANGLE
Sub-componentD3D11
Bug ClassUninitialized Memory
Tracker553881031
Fix commitad72ad15e2c8 (angle/angle) +133/-7
CISA KEVNot listed
Creditedweihengqiuu
Disclosed2026-10-06

Files Changed

  • src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
  • src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h
  • src/tests/gl_tests/RobustResourceInitTest.cpp
  • src/libANGLE/renderer/d3d/TextureStorage.h
  • src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
  • src/libANGLE/renderer/d3d/TextureD3D.cpp
From ad72ad15e2c861de84807a86a024f5746c27cb38 Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Thu, 10 Sep 2026 18:03:27 -0400
Subject: [PATCH] D3D11: Fix stale swizzle cache on robust resource init

Restricts swizzle generation to defined mip levels and invalidates the
swizzle cache when texture storage levels are cleared during robust
resource initialization, mipmap generation, or compressed copies.

Test:
angle_end2end_tests --gtest_filter="*UndefinedMipCachedThenInitialized*"
Bug: b/553881031
Change-Id: I7112a4c0d255a9aacd4022cbeb8e3d658390f559
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8394097
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Ran Wang <wangra@google.com>
---
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index fa1a434..3dbff7c 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -997,6 +997,8 @@
             image->markClean();
         }
 
+        mTexStorage->markLevelDirty(index.getLevelIndex());
+
         return angle::Result::Continue;
     }
 
diff --git a/src/libANGLE/renderer/d3d/TextureStorage.h b/src/libANGLE/renderer/d3d/TextureStorage.h
index 6623582..dee3d4c 100644
--- a/src/libANGLE/renderer/d3d/TextureStorage.h
+++ b/src/libANGLE/renderer/d3d/TextureStorage.h
@@ -76,6 +76,9 @@
 
     virtual void invalidateTextures() {}
 
+    virtual void markLevelDirty(int mipLevel) {}
+    virtual void markDirty() {}
+
     // Called by outer object when label has changed via KHR_debug extension
     void setLabel(const std::string &newLabel);
 
diff --git a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
index 427c219..02b7308 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
@@ -2828,6 +2828,8 @@
     mDeviceContext->CopySubresourceRegion(destResource->get(), destSubresource, 0, 0, 0,
                                           sourceResource->get(), sourceSubresource, nullptr);
 
+    destStorage11->markLevelDirty(destLevel);
+
     return angle::Result::Continue;
 }
 
@@ -3270,6 +3272,12 @@
 
     mDeviceContext->GenerateMips(srv->get());
 
+    for (size_t level = textureState.getEffectiveBaseLevel() + 1;
+         level <= textureState.getEffectiveMaxLevel(); ++level)
+    {
+        storage11->markLevelDirty(static_cast<int>(level));
+    }
+
     return angle::Result::Continue;
 }
 
diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
index 69b0a09..c192c80 100644
--- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
@@ -239,7 +239,7 @@
 
     if (swizzleRequired)
     {
-        verifySwizzleExists(GetEffectiveSwizzle(textureState));
+        verifySwizzleExists(GetEffectiveSwizzle(textureState), effectiveBaseLevel, mipLevels);
     }
 
     // We drop the stencil when sampling from the SRV if three conditions hold:
@@ -476,7 +476,10 @@
                                                  const gl::TextureState &textureState)
 {
     gl::SwizzleState swizzleTarget = GetEffectiveSwizzle(textureState);
-    for (int level = 0; level < getLevelCount(); level++)
+    const int baseLevel            = static_cast<int>(textureState.getEffectiveBaseLevel());
+    const int maxLevel =
+        std::min(static_cast<int>(textureState.getEffectiveMaxLevel()), getLevelCount() - 1);
+    for (int level = baseLevel; level <= maxLevel; level++)
     {
         // Check if the swizzle for this level is out of date
         if (mSwizzleCache[level] != swizzleTarget)
@@ -731,9 +734,11 @@
                                 false);
 }
 
-void TextureStorage11::verifySwizzleExists(const gl::SwizzleState &swizzleState)
+void TextureStorage11::verifySwizzleExists(const gl::SwizzleState &swizzleState,
+                                           unsigned int baseLevel,
+                                           unsigned int mipLevels)
 {
-    for (unsigned int level = 0; level < mMipLevels; level++)
+    for (unsigned int level = baseLevel; level < baseLevel + mipLevels; level++)
     {
         ASSERT(mSwizzleCache[level] == swizzleState);
     }
diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h
index 65818f0..b98e7f2 100644
--- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h
+++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h
@@ -62,8 +62,8 @@
                                const d3d11::SharedSRV **outSRV);
     angle::Result generateSwizzles(const gl::Context *context,
                                    const gl::TextureState &textureState);
-    void markLevelDirty(int mipLevel);
-    void markDirty();
+    void markLevelDirty(int mipLevel) override;
+    void markDirty() override;
 
     angle::Result updateSubresourceLevel(const gl::Context *context,
                                          const TextureHelper11 &texture,
@@ -191,7 +191,9 @@
                                             const TextureHelper11 &texture,
                                             d3d11::SharedUAV *outUAV) = 0;
 
-    void verifySwizzleExists(const gl::SwizzleState &swizzleState);
+    void verifySwizzleExists(const gl::SwizzleState &swizzleState,
+                             unsigned int baseLevel,
+                             unsigned int mipLevels);
 
     // Clear all cached non-swizzle SRVs and invalidate the swizzle cache.
     void clearSRVCache();
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 733e7af..acf7d05 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1741,6 +1741,112 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Test that stencil swizzle cache entries generated for undefined mips do not survive robust
+// initialization when subsequent mips are defined and sampled.
+TEST_P(RobustResourceInitTestES3, StencilTexturingUndefinedMipCachedThenInitialized)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_stencil_texturing") &&
+                       !(getClientMajorVersion() == 3 && getClientMinorVersion() >= 1) &&
+                       getClientMajorVersion() <= 3);
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 256, 256, 0, GL_DEPTH_STENCIL,
+                 GL_UNSIGNED_INT_24_8, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE_ANGLE, GL_STENCIL_INDEX);
+
+    constexpr char kStencilVS[] = R"(#version 300 es
+in vec4 aPosition;
+void main() {
+    gl_Position = aPosition;
+})";
+    constexpr char kStencilFS[] = R"(#version 300 es
+precision highp float;
+precision highp usampler2D;
+uniform usampler2D uTex;
+out vec4 outColor;
+void main() {
+    uint val = texelFetch(uTex, ivec2(0, 0), 0).r;
+    outColor = vec4(float(val) / 255.0, 0.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(stencilProg, kStencilVS, kStencilFS);
+    glUseProgram(stencilProg);
+
+    // Sample mip 0. This populates swizzle cache for active levels.
+    drawQuad(stencilProg, "aPosition", 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+
+    // Define mip 1.
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, 128, 128, 0, GL_DEPTH_STENCIL,
+                 GL_UNSIGNED_INT_24_8, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+
+    constexpr char kStencilMip1FS[] = R"(#version 300 es
+precision highp float;
+precision highp usampler2D;
+uniform usampler2D uTex;
+out vec4 outColor;
+void main() {
+    uint val = texelFetch(uTex, ivec2(0, 0), 1).r;
+    outColor = vec4(float(val) / 255.0, 0.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(stencilMip1Prog, kStencilVS, kStencilMip1FS);
+    glUseProgram(stencilMip1Prog);
+
+    // Sample mip 1. Robust initialization runs and must clear stencil,
+    // and swizzle cache must reflect the initialized content.
+    drawQuad(stencilMip1Prog, "aPosition", 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+    ASSERT_GL_NO_ERROR();
+}
+
+// Test that RGBA swizzle cache entries generated for undefined mips do not survive robust
+// initialization when subsequent mips are defined and sampled.
+TEST_P(RobustResourceInitTestES3, RGBASwizzleUndefinedMipCachedThenInitialized)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_GREEN);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_BLUE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
+
+    ANGLE_GL_PROGRAM(prog, essl3_shaders::vs::Texture2DLod(), essl3_shaders::fs::Texture2DLod());
+    glUseProgram(prog);
+    GLint lodLoc = glGetUniformLocation(prog, essl3_shaders::LodUniform());
+    ASSERT_NE(-1, lodLoc);
+
+    // Sample mip 0.
+    glUniform1f(lodLoc, 0.0f);
+    drawQuad(prog, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor(0, 0, 0, 255));
+
+    // Define mip 1.
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+
+    // Sample mip 1. Robust initialization runs and must clear RGBA8,
+    // and swizzle cache must reflect the initialized content.
+    glUniform1f(lodLoc, 1.0f);
+    drawQuad(prog, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor(0, 0, 0, 255));
+    ASSERT_GL_NO_ERROR();
+}
+
 // Reading a partially initialized texture (texImage2D) should succeed with all uninitialized bytes
 // set to 0 and initialized bytes untouched.
 TEST_P(RobustResourceInitTest, ReadingPartiallyInitializedTexture)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 733e7af..acf7d05 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1741,6 +1741,112 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Test that stencil swizzle cache entries generated for undefined mips do not survive robust
+// initialization when subsequent mips are defined and sampled.
+TEST_P(RobustResourceInitTestES3, StencilTexturingUndefinedMipCachedThenInitialized)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_stencil_texturing") &&
+                       !(getClientMajorVersion() == 3 && getClientMinorVersion() >= 1) &&
+                       getClientMajorVersion() <= 3);
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 256, 256, 0, GL_DEPTH_STENCIL,
+                 GL_UNSIGNED_INT_24_8, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE_ANGLE, GL_STENCIL_INDEX);
+
+    constexpr char kStencilVS[] = R"(#version 300 es
+in vec4 aPosition;
+void main() {
+    gl_Position = aPosition;
+})";
+    constexpr char kStencilFS[] = R"(#version 300 es
+precision highp float;
+precision highp usampler2D;
+uniform usampler2D uTex;
+out vec4 outColor;
+void main() {
+    uint val = texelFetch(uTex, ivec2(0, 0), 0).r;
+    outColor = vec4(float(val) / 255.0, 0.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(stencilProg, kStencilVS, kStencilFS);
+    glUseProgram(stencilProg);
+
+    // Sample mip 0. This populates swizzle cache for active levels.
+    drawQuad(stencilProg, "aPosition", 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+
+    // Define mip 1.
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, 128, 128, 0, GL_DEPTH_STENCIL,
+                 GL_UNSIGNED_INT_24_8, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+
+    constexpr char kStencilMip1FS[] = R"(#version 300 es
+precision highp float;
+precision highp usampler2D;
+uniform usampler2D uTex;
+out vec4 outColor;
+void main() {
+    uint val = texelFetch(uTex, ivec2(0, 0), 1).r;
+    outColor = vec4(float(val) / 255.0, 0.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(stencilMip1Prog, kStencilVS, kStencilMip1FS);
+    glUseProgram(stencilMip1Prog);
+
+    // Sample mip 1. Robust initialization runs and must clear stencil,
+    // and swizzle cache must reflect the initialized content.
+    drawQuad(stencilMip1Prog, "aPosition", 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+    ASSERT_GL_NO_ERROR();
+}
+
+// Test that RGBA swizzle cache entries generated for undefined mips do not survive robust
+// initialization when subsequent mips are defined and sampled.
+TEST_P(RobustResourceInitTestES3, RGBASwizzleUndefinedMipCachedThenInitialized)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_GREEN);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_BLUE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
+
+    ANGLE_GL_PROGRAM(prog, essl3_shaders::vs::Texture2DLod(), essl3_shaders::fs::Texture2DLod());
+    glUseProgram(prog);
+    GLint lodLoc = glGetUniformLocation(prog, essl3_shaders::LodUniform());
+    ASSERT_NE(-1, lodLoc);
+
+    // Sample mip 0.
+    glUniform1f(lodLoc, 0.0f);
+    drawQuad(prog, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor(0, 0, 0, 255));
+
+    // Define mip 1.
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+
+    // Sample mip 1. Robust initialization runs and must clear RGBA8,
+    // and swizzle cache must reflect the initialized content.
+    glUniform1f(lodLoc, 1.0f);
+    drawQuad(prog, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor(0, 0, 0, 255));
+    ASSERT_GL_NO_ERROR();
+}
+
 // Reading a partially initialized texture (texImage2D) should succeed with all uninitialized bytes
 // set to 0 and initialized bytes untouched.
 TEST_P(RobustResourceInitTest, ReadingPartiallyInitializedTexture)
Loading diff…

Original Bug Report

reported by we...@gmail.com

A stencil swizzle cache entry generated from a physical mip the texture has not defined survives that mip's robust initialization, and returns another origin's freed GPU memory


Report description

A stencil swizzle cache entry generated from a physical mip the texture has not defined survives that mip’s robust initialization, and returns another origin’s freed GPU memory


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

When a WebGL texture samples the stencil component through WEBGL_stencil_texturing, ANGLE’s D3D11 backend renders a derived RGBA32F copy of each mip and caches it in TextureStorage11::mSwizzleCache. The cache is generated for every physical mip of the storage, including mips the texture has not logically defined. The robust-resource-init fast path clears the real mip with ClearDepthStencilView and does not reset that cache entry; the other write paths into the storage do.

Defining only mip 0 of a mutable texture still allocates the whole physical mip chain. ID3D11Device::CreateTexture2D is called with no initial data, ANGLE’s own allocation-time fill is enabled only for a debug device, and the GL-level robust initialization skips a level that has no image description — so physical mip 1 holds whatever the driver returns. Sampling mip 0 caches a copy of it; defining mip 1 with texImage2D(..., null) then initializes the source, and sampling mip 1 afterwards returns the copy taken before that clear.

In the attached PoC the victim origin’s image is credentialed: http://localhost:8101/private.png returns a private image when that origin’s session cookie is present and a public placeholder otherwise. The victim page fetches it same-origin and writes it into the stencil bytes of a DEPTH24_STENCIL8 texture. The attacker page on http://localhost:8100 draws that same image on a canvas of its own and Chrome refuses to let it read it back; it then runs the sequence above on its own texture and reads the victim’s pixels out of the cache.

The stencil channel carries 8 bits per texel, so one round recovers one colour plane; three rounds reassemble the full-colour image. The same page run twice, differing only in whether the browser holds the victim origin’s cookie:

matches the private image matches the public placeholder
control — readback before anything is loaded 0.0 % 0.0 %
without the victim origin’s cookie 0.0 % 100.0 %
with the victim origin’s cookie 100.0 % 0.0 %

recovered.png is what the PoC exported in the cookie case and secret.png is what the victim origin served — 16384 pixels, all three channels, 49152 bytes byte-identical. view.png places both runs’ recoveries next to the two images the victim origin can serve.

Environment

Build Chrome 154.0.8029.0, win32-release_x64_asan-…-1687465, chromium 5f46c6098e0f, ANGLE 37c976ffba9d
OS / GPU Windows 11 x64, NVIDIA GeForce RTX 4080, D3D11 backend
Renderer string ANGLE (NVIDIA, NVIDIA GeForce RTX 4080 (0x00002704) Direct3D11 vs_5_0 ps_5_0, D3D11)
Flags none

Nothing beyond loading the page is required: no service worker, no cache priming, no user gesture, no extension, no browser restart, no special response headers. The page uses only getContext("webgl2"), getExtension("WEBGL_stencil_texturing"), a DEPTH24_STENCIL8 texture, DEPTH_STENCIL_TEXTURE_MODE_WEBGL = STENCIL_INDEX_WEBGL, a highp usampler2D and readPixels().

Analysis

Line numbers are ANGLE 37c976ffba9d, the revision the build above pins.

1. The physical mip is allocated without initial data, and neither initialization mechanism covers it.

TextureStorage11_2D::ensureTextureExists (d3d11/TextureStorage11.cpp:1117) builds a D3D11_TEXTURE2D_DESC whose MipLevels comes from creationLevels(), and calls Renderer11::allocateTexture (:1141) → ResourceManager11::allocate<ID3D11Texture2D> (d3d11/ResourceManager11.cpp:475) → ID3D11Device::CreateTexture2D(desc, initData, texture) (:231). Declaring only mip 0 therefore allocates the whole chain, and mip 1 is created with D3D11_USAGE_DEFAULT and no initial data.

ResourceManager11::allocate can fill an allocation at creation time:

    GetInitDataFromD3D11<T> *shadowInitData = initData;
    if (!shadowInitData && mInitializeAllocations)          // :485
    {
        shadowInitData = createInitDataIfNeeded<T>(desc);
    }
    HRESULT hr = CreateResource(device, desc, shadowInitData, &resource);
    if (!shadowInitData && mInitializeAllocations)          // :493
    {
        ANGLE_TRY(ClearResource(context, renderer, desc, resource));
    }

mInitializeAllocations defaults to false (:449) and is assigned in exactly one place, Renderer11.cpp:1010:

    mResourceManager11.setAllocationsInitialized(mCreateDebugDevice);

so it is on only when a D3D11 debug device is created. (createInitDataIfNeeded also fills with kDebugInitTextureDataValue = 0x48, ResourceManager11.cpp:22, rather than zero.)

The initialization that does run in a normal browser is the GL-level one, Texture::ensureInitialized (src/libANGLE/Texture.cpp:2592), which acts per image description:

        if (desc.initState == InitState::MayNeedInit && !desc.size.empty())   // :2616

Before mip 1 is declared it has no image description, so desc.size is empty and the level is skipped. At that point the physical mip exists in the D3D11 texture, has never been written by this context, and is not reachable by either initialization path.

2. The swizzle cache covers every physical mip. TextureStorage11::generateSwizzles, d3d11/TextureStorage11.cpp:475, iterates for (int level = 0; level < getLevelCount(); level++) (:479) and sets mSwizzleCache[level] = swizzleTarget after each blit. The bound is the storage’s physical level count, not the texture’s base/max range or its logically defined images. Since TextureD3D_2D::createCompleteStorage sizes the storage with creationLevels(), defining mip 0 alone allocates the full chain, and the entry cached for mip 1 is a copy of recycled memory.

3. The robust-init fast path does not reset the entry. TextureD3D::initializeContents (TextureD3D.cpp:924) has two paths, selected at :976-977:

    if (canCreateRenderTargetForImage(index) &&
        (shouldUseClear || features.allowClearForRobustResourceInit.enabled))
    {
        ... ANGLE_TRY(mRenderer->initRenderTarget(context, renderTarget));       // :983
        return angle::Result::Continue;
    }
    // Slow path: ... mTexStorage->setData(...)

RendererD3D::initRenderTarget (RendererD3D.cpp:150) forwards to Renderer11::clearRenderTarget (Renderer11.cpp:4200), which issues ClearDepthStencilView at :4215 and returns. Nothing on that path calls TextureStorage11::markLevelDirty (TextureStorage11.cpp:506), which resets a cache entry. Its four callers are markDirty (:530), updateSubresourceLevel (:543), generateMipmap (:703) and setData (:789); an ordinary framebuffer clear also reaches it through Framebuffer11::markAttachmentsDirty (Framebuffer11.cpp:52). The slow path of initializeContents reaches setData.

4. The vendor gate selects the fast path here. renderer11_utils.cpp:2218:

    // Qualcomm D3D11 drivers have trouble clearing textures when robust resource initialization
    // uses ClearRenderTargetView, so use the upload path instead.
    ANGLE_FEATURE_CONDITION(features, allowClearForRobustResourceInit, !isQualcomm);

Every CPU and D3D11 access on this path is in bounds, and ASan reports nothing.

Impact analysis

Bisection

The loop bound is original. git log -S "for (int level = 0; level < getLevelCount(); level++)" -- src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp returns one commit, 2b5420c07f, 2014-11-21, “Merge libGLESv2 and libEGL classes into libANGLE.”

The two init paths were introduced together. git log -S "mRenderer->initRenderTarget" -- src/libANGLE/renderer/d3d/TextureD3D.cpp returns one commit:

commit date author change
05b35b210e 2017-10-03 Jamie Madill “D3D11: Lazy robust resource init.” — moves robust init to the front end and splits initializeContents into an upload path and a render-target-clear path

The upload path reaches TextureStorage11::setData, which already called the swizzle invalidation. The render-target-clear path did not, and does not today.

The clear path has been switched five times since. git log -L on the feature line in renderer11_utils.cpp:

commit date condition stated reason
5ff8cae9bc 2019-06-25 false “Never clear for robust resource init. This matches Chrome’s texture clearning behaviour.”
9e263aa8a4 2020-03-10 isNvidia “Intel and AMD drivers have trouble clearing textures without causing corruption. NVidia, on the other hand, can handle.”
49ad8eaae1 2020-04-15 isNvidia || isIntel “Intel drivers that have trouble have been blocklisted to the DX9 runtime by Chromium.”
d45b30b5a5 2020-05-20 true
9dccbb1c96 2026-06-16 !isQualcomm “Qualcomm D3D11 drivers have trouble clearing textures…”

Between 2019-06-25 and 2020-03-10 the condition was false for every D3D11 device.

Reproduction

Put the attachments in one directory and run python3 server.py. Then, in the browser under test:

  1. open http://localhost:8101/login once — this sets the victim origin’s session cookie;
  2. open http://localhost:8100/ and let it run.

Everything is rendered into the page; DevTools are not needed. Running step 2 without step 1 is a negative control: the page then recovers the public placeholder instead, and says so. The victim handler sends no CORS headers — no access-control-allow-origin, no cross-origin-resource-policy, no timing-allow-origin.

$ curl -s http://localhost:8101/private.png                             | wc -c
1500                                       <- the public placeholder: no cookie was sent
$ curl -s -H "Cookie: poc_session=1" http://localhost:8101/private.png  | wc -c
5035                                       <- the private image

The attacker sequence:

// Only mip 0 is defined, but D3D11 allocates the whole physical chain.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 0);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH24_STENCIL8, 256, 256, 0,
              gl.DEPTH_STENCIL, gl.UNSIGNED_INT_24_8, null);
gl.texParameteri(gl.TEXTURE_2D, st.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, st.STENCIL_INDEX_WEBGL);

sample(0);                       // generateSwizzles() caches a copy of the undefined mip 1

gl.texImage2D(gl.TEXTURE_2D, 1, gl.DEPTH24_STENCIL8, 128, 128, 0,
              gl.DEPTH_STENCIL, gl.UNSIGNED_INT_24_8, null);   // mip 1 -> MayNeedInit
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 1);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);

sample(1);                       // the clear runs; the cache entry is not reset
gl.readPixels(0, 0, 128, 128, gl.RGBA, gl.UNSIGNED_BYTE, out);   // another origin's pixels

sample(n) draws one full-screen triangle running texelFetch(usampler2D, ivec2(gl_FragCoord.xy), n) into a 128x128 render target, so a single draw recovers an entire mip.

Measured

The victim image is cross-origin, and the page prints what it caught when reading it the ordinary way:

getImageData() -> SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D':
                  The canvas has been tainted by cross-origin data.
toDataURL()    -> SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement':
                  Tainted canvases may not be exported.

The cookie run, with each recovered plane scored against all three channels of the reference. Without the cookie the two result columns swap and every diagonal entry becomes 0.0 %.

round private placeholder recovered plane vs the private image’s R / G / B
control, before load 0.0 % 0.0 % 0/16384 non-zero
target: red channel 100.0 % 0.0 % R=100.0 G=60.2 B=59.0
target: green channel 100.0 % 0.0 % R=60.2 G=100.0 B=60.1
target: blue channel 100.0 % 0.0 % R=59.0 G=60.1 B=100.0
control, no early sample 0.0 % 0.0 % 0/16384 non-zero
control, no early sample 0.0 % 0.0 % 0/16384 non-zero

The three target rows form a diagonal: each round recovers the channel the victim uploaded in that round, and the victim reports a different self-check checksum for each. The off-diagonal figures near 60 % are the fraction of pixels whose own channels are equal, the grey areas.

Assembled RGB: 100.0 % of 16384 pixels, 49152 bytes, byte-identical in all three channels, exported with toDataURL() from a same-origin canvas. Level 0 of the same texture, in the same draw sequence, reads 0/16384 non-zero. The no-early-sample control defines mip 1 before the first sample and returns all zeros; an explicit FBO clear after the leak returns zeros on 3/3 rounds.


The cause

What version of Chrome have you found the security issue in?

154.0.8029.0

No, it is not related to a crash.

Choose the type of vulnerability

Information Leak

View on issue tracker