Medium watch CVSS 6.5 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in ANGLE
DescriptionUninitialized resource in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
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.
Tracker513918923
Fix commitff68f46b370c (angle/angle) +97/-6
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-08-25
Patch gap49 days — fix public in source before the advisory

Background

ANGLE D3D11 texture storage
ANGLE backs a GL texture with a backend mTexStorage; when the storage is torn down, per-image data can be copied back into staging ImageD3D objects so it survives.
Cube map redefinition
Calling glTexImage2D on one cube face with a mismatched size/format forces TextureD3D_Cube::redefineImage to discard the existing storage, which requires preserving the five sibling faces at that level.
`gl::TexLevelMask`
A per-mip-level bitmask telling releaseTexStorage which levels’ data must be copied from storage back into staging images before the storage is destroyed.
`gl::CubeFaceArray<gl::TexLevelMask>`
The new per-face array of level masks introduced by the patch, giving one independent copy mask for each of the six cube faces.

Root Cause Analysis

TextureD3D_Cube::redefineImage (the function edited in the second hunk; the exact name is not in the hunk header) built a single gl::TexLevelMask copyImageMask that set every level and then cleared the redefined level with copyImageMask.set(level, false), and passed it to releaseTexStorage. Inside releaseTexStorage, the mask was consulted per level as copyStorageToImagesMask[index.getLevelIndex()], so clearing the level excluded that level for all six faces at once — not just the face being redefined. The result was that the other five sibling faces at that level were never copied back into staging images even though the front-end state still considered them initialized, so when the cube map was completed and read again it returned uninitialized VRAM.

The fix adds a releaseTexStorage overload taking a gl::CubeFaceArray<gl::TexLevelMask> and, for gl::TextureType::CubeMap, selects the copy decision per face via copyStorageToImagesMask[index.cubeMapFaceIndex()][index.getLevelIndex()]; redefineImage now fills all faces’ masks and clears only copyImageMasks[faceIndex].set(level, false), so sibling faces are preserved before the storage is destroyed.

Key insight
The copy-back mask was per-level but the operation was per-face, so excluding one face’s level excluded all six faces at that level; the fix makes the mask per-face (gl::CubeFaceArray<gl::TexLevelMask>) so only the redefined face is skipped and sibling face data is copied back before storage teardown.

Attack Path

  1. Create and initialize a cube map A WebGL/GLES page defines all six faces of a cube map at a level with real (or default-initialized) contents so the front end marks them initialized.
  2. Redefine one face with a mismatch The page calls glTexImage2D on a single face (e.g. GL_TEXTURE_CUBE_MAP_POSITIVE_X) with a mismatched size, forcing redefineImage to discard mTexStorage.
  3. Lose sibling data The single per-level copyImageMask cleared the level for all faces, so releaseTexStorage skipped copying the five sibling faces back into staging images while the front end still deemed them initialized.
  4. Read back uninitialized VRAM The page restores the face to make the cube complete again and samples/reads the sibling faces, which now return stale, uninitialized GPU memory rather than their original contents (as the RedefineSiblingFace test demonstrates).

Impact Assessment

A malicious web page using WebGL can read back uninitialized VRAM contents for sibling cube-map faces after a mismatched face redefinition, an information-disclosure/uninitialized-resource issue in ANGLE running in the GPU process, as the advisory states. The precondition is the D3D11 backend on Windows and a script that redefines a cube face with a size/format mismatch; the advisory does not report in-the-wild exploitation. Whether the disclosed VRAM contains cross-origin or otherwise sensitive data is (inferred).

Files Changed

  • src/tests/gl_tests/RobustResourceInitTest.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/libANGLE/renderer/d3d/TextureD3D.cpp
  • src/libANGLE/renderer/d3d/TextureD3D.h

Audit Directions

  • Per-level vs per-face masks
    Search other cube-map or array-texture teardown paths where a per-level mask governs a per-face/per-layer operation; a granularity mismatch can drop sibling layers’ data.
  • State-vs-storage divergence
    Look for code that discards backend storage while the front end still marks images initialized; any copy-back skip there risks exposing uninitialized memory on later reads.
  • Robust-resource-init gaps
    Audit texture redefinition/resize paths against RobustResourceInit guarantees to ensure previously-initialized data is either preserved or re-cleared, not left stale in reused GPU allocations.
From ff68f46b370cccfff2410dd10428f7e57e386332 Mon Sep 17 00:00:00 2001
From: Tzarial <zork@google.com>
Date: Tue, 02 Jun 2026 19:21:31 +0000
Subject: [PATCH] Fix VRAM leak in D3D11 cube map redefinition

When a cube map face is redefined with a size or format mismatch,
TextureD3D_Cube::redefineImage discards the existing texture storage.
Previously, it used a per-level TexLevelMask that excluded the entire
level from being copied back to staging textures, causing the data
for the other 5 sibling faces at that level to be lost. Since the
front-end state still considered them initialized, this led to
uninitialized VRAM exposure when the cube map was completed and
read from again.

This adds a granular releaseTexStorage overload that takes an array
of TexLevelMasks (one for each face). redefineImage now specifically
excludes only the face being redefined, ensuring the data for sibling
faces is preserved in staging textures before the backend storage is
destroyed.

Bug: b/513918923
Test: angle_end2end_tests --gtest_filter=CubeMapTextureTest.RedefineSiblingFace*
Change-Id: I3a4f9ff69eec00cd7b5b2510f472814b19ce09b9
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7893496
Commit-Queue: Tzarial <zork@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index 2fcb093..83b4ecf 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -820,6 +820,15 @@
 angle::Result TextureD3D::releaseTexStorage(const gl::Context *context,
                                             const gl::TexLevelMask &copyStorageToImagesMask)
 {
+    gl::CubeFaceArray<gl::TexLevelMask> copyMasks;
+    copyMasks.fill(copyStorageToImagesMask);
+    return releaseTexStorage(context, copyMasks);
+}
+
+angle::Result TextureD3D::releaseTexStorage(
+    const gl::Context *context,
+    const gl::CubeFaceArray<gl::TexLevelMask> &copyStorageToImagesMask)
+{
     if (!mTexStorage)
     {
         return angle::Result::Continue;
@@ -837,11 +846,22 @@
             ImageD3D *image            = getImage(index);
             const int storageWidth     = std::max(1, getLevelZeroWidth() >> index.getLevelIndex());
             const int storageHeight    = std::max(1, getLevelZeroHeight() >> index.getLevelIndex());
+
+            bool copyImage = false;
+            if (mState.getType() == gl::TextureType::CubeMap)
+            {
+                copyImage =
+                    copyStorageToImagesMask[index.cubeMapFaceIndex()][index.getLevelIndex()];
+            }
+            else
+            {
+                copyImage = copyStorageToImagesMask[0][index.getLevelIndex()];
+            }
+
             if (image && isImageComplete(index) && image->getWidth() == storageWidth &&
                 image->getHeight() == storageHeight &&
                 image->getInternalFormat() == storageFormat &&
-                index.getLevelIndex() < static_cast<int>(storageLevels) &&
-                copyStorageToImagesMask[index.getLevelIndex()])
+                index.getLevelIndex() < static_cast<int>(storageLevels) && copyImage)
             {
                 ANGLE_TRY(image->copyFromTexStorage(context, index, mTexStorage));
             }
@@ -2531,11 +2551,11 @@
         {
             markAllImagesDirty();
 
-            gl::TexLevelMask copyImageMask;
-            copyImageMask.set();
-            copyImageMask.set(level, false);
+            gl::CubeFaceArray<gl::TexLevelMask> copyImageMasks;
+            copyImageMasks.fill(gl::TexLevelMask().set());
+            copyImageMasks[faceIndex].set(level, false);
 
-            ANGLE_TRY(releaseTexStorage(context, copyImageMask));
+            ANGLE_TRY(releaseTexStorage(context, copyImageMasks));
         }
     }
 
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.h b/src/libANGLE/renderer/d3d/TextureD3D.h
index 9b81f30..ee2dbe6 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.h
+++ b/src/libANGLE/renderer/d3d/TextureD3D.h
@@ -204,6 +204,9 @@
 
     angle::Result releaseTexStorage(const gl::Context *context,
                                     const gl::TexLevelMask &copyStorageToImagesMask);
+    angle::Result releaseTexStorage(
+        const gl::Context *context,
+        const gl::CubeFaceArray<gl::TexLevelMask> &copyStorageToImagesMask);
 
     GLuint getBaseLevel() const { return mBaseLevel; }
 
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 5ba8173..80e2980 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2633,6 +2633,10 @@
 // WGPU y-flip transforms have a bug when run on NVIDIA GPUs on Linux or Windows.
 468025322 NVIDIA WGPU : CopyTextureVariationsTest.Copy*Texture/ES2_WebGPU__*YFlip* = SKIP
 
+// WGPU does not support resizing textures or preserving sibling face data upon redefinition yet.
+513918923 WGPU : RobustResourceInitTest.RedefineSiblingFace/* = SKIP
+513918923 WIN OPENGL : RobustResourceInitTest.RedefineSiblingFace/* = SKIP
+
 // SPIR-V compilation error
 384593678 VULKAN : SimpleUniformTestES3.CopyOfUniformsWithArrays/* = SKIP
 
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index afab8d1..39fd638 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -4163,6 +4163,70 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Tests that redefining a single face of a cube map does not discard the
+// data of other faces at the same level.
+TEST_P(RobustResourceInitTest, RedefineSiblingFace)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    const GLint N = 4;
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
+
+    for (GLenum face = 0; face < 6; face++)
+    {
+        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA, N, N, 0, GL_RGBA,
+                     GL_UNSIGNED_BYTE, nullptr);
+    }
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    EXPECT_GL_NO_ERROR();
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
+                           tex, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    glViewport(0, 0, N, N);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+    EXPECT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+    // Redefine POSITIVE_X with a mismatched size
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, 2 * N, 2 * N, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    EXPECT_GL_NO_ERROR();
+
+    // Redefine POSITIVE_X back to normal size to make it cube complete again
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, N, N, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    EXPECT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
+         face++)
+    {
+        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, face, tex, 0);
+        if (face == GL_TEXTURE_CUBE_MAP_NEGATIVE_X)
+        {
+            // Previously set to green with glClear and not redefined.
+            EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+        }
+        else
+        {
+            EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+        }
+    }
+    EXPECT_GL_NO_ERROR();
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     RobustResourceInitTest,
     ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
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 5ba8173..80e2980 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2633,6 +2633,10 @@
 // WGPU y-flip transforms have a bug when run on NVIDIA GPUs on Linux or Windows.
 468025322 NVIDIA WGPU : CopyTextureVariationsTest.Copy*Texture/ES2_WebGPU__*YFlip* = SKIP
 
+// WGPU does not support resizing textures or preserving sibling face data upon redefinition yet.
+513918923 WGPU : RobustResourceInitTest.RedefineSiblingFace/* = SKIP
+513918923 WIN OPENGL : RobustResourceInitTest.RedefineSiblingFace/* = SKIP
+
 // SPIR-V compilation error
 384593678 VULKAN : SimpleUniformTestES3.CopyOfUniformsWithArrays/* = SKIP
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index afab8d1..39fd638 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -4163,6 +4163,70 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Tests that redefining a single face of a cube map does not discard the
+// data of other faces at the same level.
+TEST_P(RobustResourceInitTest, RedefineSiblingFace)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    const GLint N = 4;
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
+
+    for (GLenum face = 0; face < 6; face++)
+    {
+        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA, N, N, 0, GL_RGBA,
+                     GL_UNSIGNED_BYTE, nullptr);
+    }
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    EXPECT_GL_NO_ERROR();
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
+                           tex, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    glViewport(0, 0, N, N);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+    EXPECT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+    // Redefine POSITIVE_X with a mismatched size
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, 2 * N, 2 * N, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    EXPECT_GL_NO_ERROR();
+
+    // Redefine POSITIVE_X back to normal size to make it cube complete again
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, N, N, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    EXPECT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
+         face++)
+    {
+        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, face, tex, 0);
+        if (face == GL_TEXTURE_CUBE_MAP_NEGATIVE_X)
+        {
+            // Previously set to green with glClear and not redefined.
+            EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+        }
+        else
+        {
+            EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+        }
+    }
+    EXPECT_GL_NO_ERROR();
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     RobustResourceInitTest,
     ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential VRAM leak in ANGLE D3D11 via cube map sibling face redefinition

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in ANGLE’s D3D11 backend allows sibling faces of a cube map to lose their initialization state and data when another face at the same level is redefined. This leads to a desync between the front-end and backend, potentially allowing a malicious website to read uninitialized GPU memory via glReadPixels.

Affected files:

  • third_party/angle/src/libANGLE/renderer/d3d/TextureD3D.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
  • third_party/angle/src/libANGLE/Texture.cpp
  • third_party/angle/src/libANGLE/Framebuffer.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

Root Cause Analysis

A vulnerability exists in ANGLE’s D3D11 backend due to insufficient granularity in the storage-preservation logic during cube map redefinition. When a cube map face is redefined with a mismatched size or format, TextureD3D_Cube::redefineImage triggers a release of the underlying texture storage.

In third_party/angle/src/libANGLE/renderer/d3d/TextureD3D.cpp:

ANGLE_TRY(releaseTexStorage(context, copyImageMask));

The copyImageMask is a gl::TexLevelMask, which tracks initialization status on a per-level basis. However, a cube map contains six faces per level. When redefineImage unsets the bit for a level to exclude a redefined face from data preservation, it inadvertently instructs releaseTexStorage to skip the storage-to-image copy-back for all six faces at that level.

Exploitation Mechanism

The vulnerability is reachable because multiple layers of state management fail to maintain consistency:

  1. Association Failure: While ANGLE normally recovers associated images during storage destruction in TextureStorage11_Cube::onDestroy, an image may lose its association if mRecoveredFromStorageCount reaches its limit (currently 2). In this state, a face modified only via a RenderTarget (e.g., glClear) exists only in GPU storage and is not associated with an Image11 object.
  2. Data Loss: During redefinition of a sibling face, the mask-based bypass in releaseTexStorage skips preservation, and the lack of association skips recovery in onDestroy. The GPU storage is deleted, losing the cleared data.
  3. Backend Desync: The Image11::isDirty() check (Image11.cpp:135) incorrectly returns false if an image is dirty but lacks both a staging texture and a storage association. This prevents the lost data from being re-uploaded to new storage.
  4. Information Leak: When new storage is allocated, it remains uninitialized. Because the front-end still believes the face is Initialized, it bypasses robust resource initialization, allowing the attacker to read VRAM residue via glReadPixels.

Potential Reproduction Steps

  1. Create a 4x4 RGBA8 cube map and initialize all faces.
  2. Redefine a sibling face multiple times to increment the mRecoveredFromStorageCount for Face A until association is disabled.
  3. Attach Face A to an FBO and clear it (data is now in GPU-only storage).
  4. Redefine a different face (Face B) with a mismatched size (8x8). This triggers the buggy release logic.
  5. Redefine Face B back to 4x4 to make the cube complete.
  6. Bind Face A and perform glReadPixels. The operation may return uninitialized VRAM residue instead of the cleared color.

Note: These steps are based on static code analysis; actual behavior may vary depending on D3D11 driver specifics and ANGLE configuration.

Suggested Fix

The gl::TexLevelMask should be replaced or supplemented with a mechanism that tracks preservation requirements on a per-face basis for cube maps, ensuring that sibling faces are not excluded from releaseTexStorage preservation logic when only one face is being redefined.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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