High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in ANGLE
DescriptionMemory corruption in ANGLE
ComponentANGLE
Sub-componentInternals>GPU>ANGLE
Bug ClassUninitialized Memory
Tracker553117511
Fix commitd219c4f45f93 (angle/angle) +99/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Background

Robust resource initialization
ANGLE zero-initializes texture memory before it can be read, to prevent reading stale GPU contents.
EnsureInitializedLevels
controls which levels get robust-initialized.

Root Cause Analysis

A previous change restricted robust-init to BASE+ levels, which is safe for mutable textures (levels outside [BASE, MAX] are inaccessible) but not for immutable textures, whose levels outside [BASE, MAX] remain framebuffer-attachment-complete and thus readable. So an immutable texture’s out-of-range level could be attached to a framebuffer and read while still uninitialized — exposing stale GPU memory.

The fix computes initFirstLevel = (initializeBaseOnly || !getImmutableFormat()) ? baseLevel : 0, i.e. immutable textures initialize all levels from 0. The advisory labels this “memory corruption,” but the defect is an uninitialized-memory read (robust-init gap); reclassified to UninitializedMemory. (Read of uninitialized contents is the mechanism; no corruption primitive is shown.)

Key insight
Robust-init skipped levels outside [BASE, MAX], but immutable-texture levels there are still readable; initializing all levels for immutable textures closes the uninitialized-read.

Attack Path

  1. ENTRY From WebGL, create an immutable texture (glTexStorage2D) and set BASE/MAX to a sub-range.
  2. TRIGGER Attach/read a level outside [BASE, MAX] (still complete for immutable textures) before it is initialized.
  3. MISSING CHECK Robust-init only covered BASE+, leaving the out-of-range level uninitialized.
  4. IMPACT The read returns stale/uninitialized GPU memory. (Info-leak of uninitialized contents; the advisory’s “memory corruption” overstates it.)

Impact Assessment

A content-reachable read of uninitialized GPU texture memory via immutable textures with a restricted [BASE, MAX] range (GPU process) — an uninitialized-memory/info-leak defect, not the advisory’s “memory corruption.” Severity high per the advisory; no ITW stated.

Files Changed

  • src/tests/gl_tests/RobustResourceInitTest.cpp
  • src/tests/gl_tests/CopyTextureTest.cpp
  • src/libANGLE/Texture.cpp
  • src/tests/angle_end2end_tests_expectations.txt

Audit Directions

  • Mutable vs immutable init assumptions
    Audit robust-init/level-range logic for assumptions that hold only for mutable textures; immutable textures expose the full level range.
  • Attachment completeness
    Enumerate paths where a level outside [BASE, MAX] becomes readable and confirm each is initialized.
From d219c4f45f932603b53e6ffb5c5295c2ed33758d Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Tue, 15 Sep 2026 16:05:05 -0400
Subject: [PATCH] Always robust init all levels of immutable textures

A previous change made it such that only BASE+ levels are
robust-initialized.  This is fine for mutable textures, since levels
outside this range are inaccessible to GL.  However, that's not true for
immutable textures, where framebuffer completeness is always true
regardless of which level the framebuffer is attached to.

Bug: chromium:553117511
Change-Id: Ied52d392f79bed56152c063816be2986b810080f
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8415406
Reviewed-by: Ran Wang <wangra@google.com>
---
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index 34ff710..8e1c9e2 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -2600,18 +2600,24 @@
         return angle::Result::Continue;
     }
 
+    const bool initializeBaseOnly = levels == EnsureInitializedLevels::BaseOnly;
+    const GLint baseLevel         = mState.getEffectiveBaseLevel();
+    // Start initializing from baseLevel if specifically requested or if the texture is mutable.  If
+    // the texture is immutable, initialize all the levels because framebuffers outside [BASE, MAX]
+    // range are complete for immutable textures.
+    const GLint initFirstLevel = (initializeBaseOnly || !getImmutableFormat()) ? baseLevel : 0;
+
     bool anyDirty = false;
-    bool allInitialized = (levels == EnsureInitializedLevels::AllEnabledLevels &&
-                           mState.getEffectiveBaseLevel() == 0);
+    bool allInitialized =
+        levels == EnsureInitializedLevels::AllEnabledLevels && initFirstLevel == 0;
 
     ImageIndexIterator it = ImageIndexIterator::MakeGeneric(
-        mState.mType, static_cast<GLint>(mState.getEffectiveBaseLevel()),
-        IMPLEMENTATION_MAX_TEXTURE_LEVELS + 1, ImageIndex::kEntireLevel, ImageIndex::kEntireLevel);
+        mState.mType, initFirstLevel, IMPLEMENTATION_MAX_TEXTURE_LEVELS + 1,
+        ImageIndex::kEntireLevel, ImageIndex::kEntireLevel);
     while (it.hasNext())
     {
         const ImageIndex index = it.next();
-        if (levels == EnsureInitializedLevels::BaseOnly &&
-            index.getLevelIndex() != static_cast<GLint>(mState.getEffectiveBaseLevel()))
+        if (initializeBaseOnly && index.getLevelIndex() != baseLevel)
         {
             break;
         }
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 4d808cb..661ea43 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -433,6 +433,9 @@
 556790356 NVIDIA GLES : Texture2DTestES3RobustInit.CopyTexSubImageOutOfBoundsAcrossMips/* = SKIP
 556790356 NVIDIA OPENGL : Texture2DTestES3RobustInit.CopyTexSubImagePartiallyOutOfBoundsAcrossMips/* = SKIP
 556790356 NVIDIA GLES : Texture2DTestES3RobustInit.CopyTexSubImagePartiallyOutOfBoundsAcrossMips/* = SKIP
+// NVIDIA is prone to losing data outside the BASE/MAX range.
+553117511 NVIDIA OPENGL : CopyTextureTestES3.ImmutableSourceLevelOutsideBaseMaxRange/* = SKIP
+553117511 NVIDIA GLES : CopyTextureTestES3.ImmutableSourceLevelOutsideBaseMaxRange/* = SKIP
 
 // Failing on NVidia OpenGL
 549916890 NVIDIA OPENGL : Texture3DIncreaseDepthTestES3.TexImage3DDepthIncreaseOutsideRangeDrawnVerifyByDraw/* = SKIP
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index 8bee8f7..5ce6664 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -3495,8 +3495,9 @@
 }
 
 // Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM fail validation if the source level
-// is outside the [BASE, MAX] range.
-TEST_P(CopyTextureTestES3, VerifySourceLevelInBaseMaxRange)
+// is outside the [BASE, MAX] range, which makes the source not framebuffer attachment complete.
+// This is only the case for mutable textures; for immutable textures all source levels are valid.
+TEST_P(CopyTextureTestES3, VerifyMutableSourceLevelInBaseMaxRange)
 {
     ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
 
@@ -3603,6 +3604,52 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
+// Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM work if the source level is outside
+// the [BASE, MAX] range but the texture is immutable.
+TEST_P(CopyTextureTestES3, ImmutableSourceLevelOutsideBaseMaxRange)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
+
+    const std::vector<GLColor> kLevel0(64 * 64, GLColor::red);
+    const std::vector<GLColor> kLevel1(32 * 32, GLColor::green);
+    const std::vector<GLColor> kLevel2(16 * 16, GLColor::blue);
+    const std::vector<GLColor> kLevel3(8 * 8, GLColor::yellow);
+    const std::vector<GLColor> kLevel4(4 * 4, GLColor::magenta);
+
+    GLTexture src;
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexStorage2D(GL_TEXTURE_2D, 5, GL_RGBA8, 64, 64);
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 64, 64, GL_RGBA, GL_UNSIGNED_BYTE, kLevel0.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 32, 32, GL_RGBA, GL_UNSIGNED_BYTE, kLevel1.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 2, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, kLevel2.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 3, 0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, kLevel3.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 4, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, kLevel4.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
+
+    // Copy from level 1 of source texture, which is below base level.
+    GLTexture dst1;
+    glBindTexture(GL_TEXTURE_2D, dst1);
+    glCopyTextureCHROMIUM(src, 1, GL_TEXTURE_2D, dst1, 0, GL_RGBA, GL_UNSIGNED_BYTE, GL_FALSE,
+                          GL_FALSE, GL_FALSE);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst1, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 32, 32, kLevel1[0]);
+
+    // Copy from level 4 of source texture, which is above max level.
+    GLTexture dst2;
+    glBindTexture(GL_TEXTURE_2D, dst2);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glCopySubTextureCHROMIUM(src, 4, GL_TEXTURE_2D, dst2, 0, 0, 0, 0, 0, 4, 4, GL_FALSE, GL_FALSE,
+                             GL_FALSE);
+
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst2, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 4, 4, kLevel4[0]);
+    ASSERT_GL_NO_ERROR();
+}
+
 // Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM work if the texture base level
 // changes after the texture is synced.
 TEST_P(CopyTextureTestES3, ChangeBaseLevel)
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 60efec6..733e7af 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -4596,6 +4596,41 @@
     }
 }
 
+// Test that source of glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM is initialized if the
+// source level is outside the [BASE, MAX] range but the texture is immutable.
+TEST_P(RobustResourceInitTestES3, ImmutableSourceLevelOutsideBaseMaxRange)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
+
+    GLTexture src;
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexStorage2D(GL_TEXTURE_2D, 5, GL_RGBA8, 64, 64);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
+
+    // Copy from level 1 of source texture, which is below base level.
+    GLTexture dst1;
+    glBindTexture(GL_TEXTURE_2D, dst1);
+    glCopyTextureCHROMIUM(src, 1, GL_TEXTURE_2D, dst1, 0, GL_RGBA, GL_UNSIGNED_BYTE, GL_FALSE,
+                          GL_FALSE, GL_FALSE);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst1, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 32, 32, GLColor::transparentBlack);
+
+    // Copy from level 4 of source texture, which is above max level.
+    GLTexture dst2;
+    glBindTexture(GL_TEXTURE_2D, dst2);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glCopySubTextureCHROMIUM(src, 4, GL_TEXTURE_2D, dst2, 0, 0, 0, 0, 0, 4, 4, GL_FALSE, GL_FALSE,
+                             GL_FALSE);
+
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst2, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 4, 4, GLColor::transparentBlack);
+    ASSERT_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 4d808cb..661ea43 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -433,6 +433,9 @@
 556790356 NVIDIA GLES : Texture2DTestES3RobustInit.CopyTexSubImageOutOfBoundsAcrossMips/* = SKIP
 556790356 NVIDIA OPENGL : Texture2DTestES3RobustInit.CopyTexSubImagePartiallyOutOfBoundsAcrossMips/* = SKIP
 556790356 NVIDIA GLES : Texture2DTestES3RobustInit.CopyTexSubImagePartiallyOutOfBoundsAcrossMips/* = SKIP
+// NVIDIA is prone to losing data outside the BASE/MAX range.
+553117511 NVIDIA OPENGL : CopyTextureTestES3.ImmutableSourceLevelOutsideBaseMaxRange/* = SKIP
+553117511 NVIDIA GLES : CopyTextureTestES3.ImmutableSourceLevelOutsideBaseMaxRange/* = SKIP
 
 // Failing on NVidia OpenGL
 549916890 NVIDIA OPENGL : Texture3DIncreaseDepthTestES3.TexImage3DDepthIncreaseOutsideRangeDrawnVerifyByDraw/* = SKIP
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index 8bee8f7..5ce6664 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -3495,8 +3495,9 @@
 }
 
 // Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM fail validation if the source level
-// is outside the [BASE, MAX] range.
-TEST_P(CopyTextureTestES3, VerifySourceLevelInBaseMaxRange)
+// is outside the [BASE, MAX] range, which makes the source not framebuffer attachment complete.
+// This is only the case for mutable textures; for immutable textures all source levels are valid.
+TEST_P(CopyTextureTestES3, VerifyMutableSourceLevelInBaseMaxRange)
 {
     ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
 
@@ -3603,6 +3604,52 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
+// Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM work if the source level is outside
+// the [BASE, MAX] range but the texture is immutable.
+TEST_P(CopyTextureTestES3, ImmutableSourceLevelOutsideBaseMaxRange)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
+
+    const std::vector<GLColor> kLevel0(64 * 64, GLColor::red);
+    const std::vector<GLColor> kLevel1(32 * 32, GLColor::green);
+    const std::vector<GLColor> kLevel2(16 * 16, GLColor::blue);
+    const std::vector<GLColor> kLevel3(8 * 8, GLColor::yellow);
+    const std::vector<GLColor> kLevel4(4 * 4, GLColor::magenta);
+
+    GLTexture src;
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexStorage2D(GL_TEXTURE_2D, 5, GL_RGBA8, 64, 64);
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 64, 64, GL_RGBA, GL_UNSIGNED_BYTE, kLevel0.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 32, 32, GL_RGBA, GL_UNSIGNED_BYTE, kLevel1.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 2, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, kLevel2.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 3, 0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, kLevel3.data());
+    glTexSubImage2D(GL_TEXTURE_2D, 4, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, kLevel4.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
+
+    // Copy from level 1 of source texture, which is below base level.
+    GLTexture dst1;
+    glBindTexture(GL_TEXTURE_2D, dst1);
+    glCopyTextureCHROMIUM(src, 1, GL_TEXTURE_2D, dst1, 0, GL_RGBA, GL_UNSIGNED_BYTE, GL_FALSE,
+                          GL_FALSE, GL_FALSE);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst1, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 32, 32, kLevel1[0]);
+
+    // Copy from level 4 of source texture, which is above max level.
+    GLTexture dst2;
+    glBindTexture(GL_TEXTURE_2D, dst2);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glCopySubTextureCHROMIUM(src, 4, GL_TEXTURE_2D, dst2, 0, 0, 0, 0, 0, 4, 4, GL_FALSE, GL_FALSE,
+                             GL_FALSE);
+
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst2, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 4, 4, kLevel4[0]);
+    ASSERT_GL_NO_ERROR();
+}
+
 // Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM work if the texture base level
 // changes after the texture is synced.
 TEST_P(CopyTextureTestES3, ChangeBaseLevel)
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 60efec6..733e7af 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -4596,6 +4596,41 @@
     }
 }
 
+// Test that source of glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM is initialized if the
+// source level is outside the [BASE, MAX] range but the texture is immutable.
+TEST_P(RobustResourceInitTestES3, ImmutableSourceLevelOutsideBaseMaxRange)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
+
+    GLTexture src;
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexStorage2D(GL_TEXTURE_2D, 5, GL_RGBA8, 64, 64);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
+
+    // Copy from level 1 of source texture, which is below base level.
+    GLTexture dst1;
+    glBindTexture(GL_TEXTURE_2D, dst1);
+    glCopyTextureCHROMIUM(src, 1, GL_TEXTURE_2D, dst1, 0, GL_RGBA, GL_UNSIGNED_BYTE, GL_FALSE,
+                          GL_FALSE, GL_FALSE);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst1, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 32, 32, GLColor::transparentBlack);
+
+    // Copy from level 4 of source texture, which is above max level.
+    GLTexture dst2;
+    glBindTexture(GL_TEXTURE_2D, dst2);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glCopySubTextureCHROMIUM(src, 4, GL_TEXTURE_2D, dst2, 0, 0, 0, 0, 0, 4, 4, GL_FALSE, GL_FALSE,
+                             GL_FALSE);
+
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst2, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, 4, 4, GLColor::transparentBlack);
+    ASSERT_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

ANGLE: Texture::ensureInitialized skips below-base levels causing GPU memory leak in copyTexture

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: Under robust resource initialization, ANGLE’s Texture::ensureInitialized only clears mip levels starting at the texture’s effective base level. Because validation for glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM permits reading below-base levels for immutable textures, uninitialized backing allocations are copied into destination textures and marked initialized. On D3D11 and Metal backends, this potentially exposes uninitialized GPU-process memory across origins to a compromised renderer.

Affected files:

  • third_party/angle/src/libANGLE/Texture.cpp
  • third_party/angle/src/libANGLE/validationES2.cpp

Estimated timestamp from git blame: 2026-07-30

Potential Vulnerability: Uninitialized GPU Memory Disclosure via ANGLE CopyTexture

Note: The following analysis and reproduction steps are based on static code tracing. Our tooling agent cannot run code dynamically, so these findings and steps should be treated as potential.

Root Cause Analysis

In Chromium, robust resource initialization is enabled by default for all passthrough command decoder contexts (gpu/command_buffer/service/service_utils.cc:152). ANGLE relies on lazy initialization via Texture::ensureInitialized (third_party/angle/src/libANGLE/Texture.cpp:2592-2641) to clear uninitialized textures before they are accessed.

However, Texture::ensureInitialized bounds its iteration loop to start at mState.getEffectiveBaseLevel():

// third_party/angle/src/libANGLE/Texture.cpp:2603-2605
ImageIndexIterator it = ImageIndexIterator::MakeGeneric(
    mState.mType, static_cast<GLint>(mState.getEffectiveBaseLevel()),
    IMPLEMENTATION_MAX_TEXTURE_LEVELS + 1, ImageIndex::kEntireLevel, ImageIndex::kEntireLevel);

If an immutable texture is created with multiple mip levels and its GL_TEXTURE_BASE_LEVEL is later increased (e.g. to level 2), mip levels 0 through getEffectiveBaseLevel() - 1 are completely skipped by ensureInitialized and remain uninitialized.

While normal sampling and framebuffer attachment paths restrict access or initialize individual attachments, glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM validation permits reading below-base levels of immutable textures because isFramebufferAttachmentComplete returns true unconditionally for immutable textures:

// third_party/angle/src/libANGLE/Texture.cpp:2539-2544
bool Texture::isFramebufferAttachmentComplete(GLuint attachmentMipLevel, const char **error) const
{
    if (getImmutableFormat())
    {
        return true;
    }
    ...
}

This is called by ValidateCopyTextureCHROMIUM (third_party/angle/src/libANGLE/validationES2.cpp:3539) and ValidateCopySubTextureCHROMIUM (third_party/angle/src/libANGLE/validationES2.cpp:3674). Additionally, IsValidCopyTextureSourceLevel (validationES2.cpp:244-249) accepts level == 0 or any level when client ES version is $\ge 3.0$.

When Texture::copySubTexture executes (third_party/angle/src/libANGLE/Texture.cpp:1768-1798):

  1. It calls source->ensureInitialized(context, EnsureInitializedLevels::AllEnabledLevels) (line 1782), which only initializes levels $\ge \text{effectiveBaseLevel}$, leaving level 0 untouched.
  2. The backend copies the raw uninitialized backing memory from sourceLevel = 0 (e.g. CopySubresourceRegion in Renderer11.cpp:2696-2698 on D3D11 or TextureMtl::copySubTextureImpl on Metal).
  3. It marks the destination level InitState::Initialized via setInitState(GL_NONE, index, InitState::Initialized) (line 1795).

Because the destination level is marked Initialized, subsequent reads (e.g. via glReadPixels on an FBO) skip robust initialization clears and return uninitialized GPU memory directly.

Backend Backing Allocations

  • D3D11: In release builds, ResourceManager11::allocate (third_party/angle/src/libANGLE/renderer/d3d/d3d11/ResourceManager11.cpp:474-502) creates texture allocations with pInitialData = nullptr without zero-fill (mInitializeAllocations is only true under debug devices, Renderer11.cpp:1010).
  • Metal: TextureMtl allocates native MTLTexture storage without zeroing because allocateNonZeroTextures is not enabled in production and standard formats like GL_RGBA8 are not emulated (TextureMtl.mm:2627-2655).
  • Vulkan: Not affected because TextureVk::initImage handles backend-side whole-image clears when robust init is enabled.

Potential Trigger Steps (Compromised Renderer)

  1. Create an immutable source texture with multiple mip levels:
    glTexStorage2D(GL_TEXTURE_2D, 4, GL_RGBA8, 1024, 1024)
  2. Set the base level to bypass lower-level initialization in ensureInitialized:
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2)
  3. Create a mutable destination texture:
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 1024, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)
  4. Copy the uninitialized level 0 of the source into the destination:
    glCopySubTextureCHROMIUM(src, 0, GL_TEXTURE_2D, dst, 0, 0, 0, 0, 0, 1024, 1024, GL_FALSE, GL_FALSE, GL_FALSE)
  5. Attach dst (level 0) to a framebuffer object as GL_COLOR_ATTACHMENT0 and call glReadPixels to read back uninitialized GPU memory.

Suggested Fix

  1. In third_party/angle/src/libANGLE/Texture.cpp:2592-2641 (Texture::ensureInitialized), ensure all allocated levels (0 through mState.getMipmapMaxLevel()) are initialized when levels == EnsureInitializedLevels::AllEnabledLevels, or explicitly ensure the specific sourceLevel being copied in copyTexture / copySubTexture is initialized before invoking the backend copy.
  2. In third_party/angle/src/libANGLE/validationES2.cpp, validate that sourceLevel for glCopy(Sub)TextureCHROMIUM is within the source texture’s effective [base, max] level range, or ensure that validation does not permit copying uninitialized below-base levels.

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


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