CVE-2026-113315
Overview
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.)
[BASE, MAX], but immutable-texture levels there are still readable; initializing all levels for immutable textures closes the uninitialized-read.Attack Path
- ENTRY
From WebGL, create an immutable texture (
glTexStorage2D) and setBASE/MAXto a sub-range. - TRIGGER
Attach/read a level outside
[BASE, MAX](still complete for immutable textures) before it is initialized. - MISSING CHECK
Robust-init only covered
BASE+, leaving the out-of-range level uninitialized. - IMPACT The read returns stale/uninitialized GPU memory. (Info-leak of uninitialized contents; the advisory’s “memory corruption” overstates it.)
Impact Assessment
[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.cppsrc/tests/gl_tests/CopyTextureTest.cppsrc/libANGLE/Texture.cppsrc/tests/angle_end2end_tests_expectations.txt
Audit Directions
- Mutable vs immutable init assumptionsAudit robust-init/level-range logic for assumptions that hold only for mutable textures; immutable textures expose the full level range.
- Attachment completenessEnumerate paths where a level outside
[BASE, MAX]becomes readable and confirm each is initialized.
Patch
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),
Regression Test / PoC
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),
Original Bug Report
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.cppthird_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):
- It calls
source->ensureInitialized(context, EnsureInitializedLevels::AllEnabledLevels)(line 1782), which only initializes levels $\ge \text{effectiveBaseLevel}$, leaving level 0 untouched. - The backend copies the raw uninitialized backing memory from
sourceLevel = 0(e.g.CopySubresourceRegioninRenderer11.cpp:2696-2698on D3D11 orTextureMtl::copySubTextureImplon Metal). - It marks the destination level
InitState::InitializedviasetInitState(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 withpInitialData = nullptrwithout zero-fill (mInitializeAllocationsis only true under debug devices,Renderer11.cpp:1010). - Metal:
TextureMtlallocates nativeMTLTexturestorage without zeroing becauseallocateNonZeroTexturesis not enabled in production and standard formats likeGL_RGBA8are not emulated (TextureMtl.mm:2627-2655). - Vulkan: Not affected because
TextureVk::initImagehandles backend-side whole-image clears when robust init is enabled.
Potential Trigger Steps (Compromised Renderer)
- Create an immutable source texture with multiple mip levels:
glTexStorage2D(GL_TEXTURE_2D, 4, GL_RGBA8, 1024, 1024) - Set the base level to bypass lower-level initialization in
ensureInitialized:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2) - Create a mutable destination texture:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 1024, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr) - 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) - Attach
dst(level 0) to a framebuffer object asGL_COLOR_ATTACHMENT0and callglReadPixelsto read back uninitialized GPU memory.
Suggested Fix
- In
third_party/angle/src/libANGLE/Texture.cpp:2592-2641(Texture::ensureInitialized), ensure all allocated levels (0throughmState.getMipmapMaxLevel()) are initialized whenlevels == EnsureInitializedLevels::AllEnabledLevels, or explicitly ensure the specificsourceLevelbeing copied incopyTexture/copySubTextureis initialized before invoking the backend copy. - In
third_party/angle/src/libANGLE/validationES2.cpp, validate thatsourceLevelforglCopy(Sub)TextureCHROMIUMis 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.