CVE-2026-106376
Overview
Files Changed
src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cppsrc/libANGLE/renderer/d3d/d3d11/TextureStorage11.hsrc/tests/gl_tests/RobustResourceInitTest.cppsrc/libANGLE/renderer/d3d/TextureStorage.hsrc/libANGLE/renderer/d3d/d3d11/Renderer11.cppsrc/libANGLE/renderer/d3d/TextureD3D.cpp
Patch
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)
Regression Test / PoC
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)
Original Bug Report
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:
- open
http://localhost:8101/loginonce — this sets the victim origin’s session cookie; - 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
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Information Leak