High CVSS 9.6 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Sub-componentVulkan
Bug ClassLogic Error
Tracker516837126
Fix commit1a7742a98d71 (angle/angle) +133/-52
CWECWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-26 (per vendor advisory)
Fix landed2026-06-03 (public git)
Disclosed2026-07-29
Patch gap56 days — fix public in source before the advisory

Background

glCopyTextureCHROMIUM / glCopySubTextureCHROMIUM
CHROMIUM texture-copy entry points; the source texture must be framebuffer-attachment-complete.
BASE level / sync
changing GL_TEXTURE_BASE_LEVEL changes which image is current; ANGLE’s Vulkan backend must sync the texture’s image state to reflect that.
syncAsAttachmentRenderTarget
new helper (extracted from getAttachmentRenderTarget) that syncs a texture’s image as if it were attached to a framebuffer.

Root Cause Analysis

The copy paths called ensureImageInitialized() on the source texture but never synced it as a render-target after a base-level change, so the source’s Vulkan image state could be stale/incomplete when the copy read from it.

The fix adds syncAsAttachmentRenderTarget(...) before ensureImageInitialized(...) in both copyTexture and copySubTexture, refactoring getAttachmentRenderTarget to share the sync. Classification: the mapped commit (bug chromium:516837126, matching this CVE) is a missing-synchronization fix — it adds state sync before the copy and contains no integer arithmetic, bounds math, or overflow guard, so the advisory’s “integer overflow” label is not substantiated by the diff. The diff-grounded class is therefore LogicError (a missing source sync after a base-level change). The plausible memory primitive is a copy using stale/mismatched level state (a size miscalculation or out-of-range read), but the exact sink is not shown by the patch — treat the specific primitive as unverified pending the bug.

Key insight
The copy source was not synced for the current base level before being read, so it could be used in a stale/incomplete state; the fix syncs it as a render target first. The class is LogicError (missing sync); the advisory’s “integer overflow” is an unverified downstream primitive, not shown in the diff.

Attack Path

  1. ENTRY From WebGL, create a texture, populate mip levels, then change GL_TEXTURE_BASE_LEVEL.
  2. TRIGGER Call glCopyTextureCHROMIUM/glCopySubTextureCHROMIUM with that texture as source.
  3. MISSING CHECK The source is not synced as an attachment render target after the base-level change, so its image state is stale/incomplete.
  4. IMPACT The copy proceeds from a stale/incomplete source; the advisory reports an integer overflow. (The overflow/OOB sink is NOT in the patch — inferred and flagged for review.)

Impact Assessment

Reachable from web content via the CHROMIUM copy extensions; the root cause (missing sync before copy) is clear, but the concrete memory primitive — and thus whether “integer overflow” is the right characterization — is not evidenced by the patch and should be confirmed against bug 516837126 before a PoC. Advisory severity high; no ITW stated.

Changed Functions

FunctionChangeNotes
BasicCopyTextureTest
src/tests/gl_tests/CopyTextureTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/vulkan/TextureVk.cpp
  • src/libANGLE/renderer/vulkan/TextureVk.h
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/CopyTextureTest.cpp

Audit Directions

  • Completeness before copy/read
    Audit copy/read paths for a sync of the source after state changes (base/max level, reallocation) — not just ensureImageInitialized.
  • Confirm the primitive
    Pull bug 516837126 to determine whether the overflow is in level-dimension math or a stale-image read.
From 1a7742a98d713597c64aa2e1b23ba229cd5da4ad Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 01 Jun 2026 13:38:11 -0400
Subject: [PATCH] Vulkan: Fix texture copy after base level change

glCopy*TextureCHROMIUM does not sync the source texture, but the texture
may not be complete, but there should have been _some_ sort of sync to
take BASE level etc changes into account.

Since the source texture is required to be
framebuffer-attachment-complete, we can simply take a render target from
the texture, which syncs it as if it was attached to a framebuffer.

Bug: chromium:516837126
Change-Id: Ia3b9bf03cfd8c2f432d12c6e2eef7b7d5343d450
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7886847
Reviewed-by: Charlie Lao <cclao@google.com>
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index 68641b8..fc0284c 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -1515,7 +1515,13 @@
     const gl::InternalFormat &dstFormatInfo = gl::GetInternalFormatInfo(internalFormat, type);
     const vk::Format &dstVkFormat = renderer->getFormat(dstFormatInfo.sizedInternalFormat);
 
-    ANGLE_TRY(sourceVk->ensureImageInitialized(contextVk, ImageMipLevels::EnabledLevels));
+    // The source image must be framebuffer-attachment-complete, so sync it as if a render target is
+    // needed.
+    {
+        ANGLE_TRY(sourceVk->syncAsAttachmentRenderTarget(
+            context, gl::ImageIndex::MakeFromType(source->getType(), sourceLevelGL), 0));
+        ANGLE_TRY(sourceVk->ensureImageInitialized(contextVk, ImageMipLevels::EnabledLevels));
+    }
 
     // Fall back to renderable format if copy cannot be done in transfer.  Must be done before
     // the dst format is accessed anywhere (in |redefineLevel| and |copySubTextureImpl|).
@@ -1547,8 +1553,12 @@
     const gl::InternalFormat &dstFormatInfo =
         *mState.getImageDesc(target, dstLevelGL.get()).format.info;
 
-    ANGLE_TRY(
-        vk::GetImpl(source)->ensureImageInitialized(contextVk, ImageMipLevels::EnabledLevels));
+    {
+        TextureVk *sourceVk = vk::GetImpl(source);
+        ANGLE_TRY(sourceVk->syncAsAttachmentRenderTarget(
+            context, gl::ImageIndex::MakeFromType(source->getType(), srcLevelGL), 0));
+        ANGLE_TRY(sourceVk->ensureImageInitialized(contextVk, ImageMipLevels::EnabledLevels));
+    }
 
     // Fall back to renderable format if copy cannot be done in transfer.  Must be done before
     // the dst format is accessed anywhere (in |copySubTextureImpl|).
@@ -3304,15 +3314,10 @@
     return angle::Result::Continue;
 }
 
-angle::Result TextureVk::getAttachmentRenderTarget(const gl::Context *context,
-                                                   GLenum binding,
-                                                   const gl::ImageIndex &imageIndex,
-                                                   GLsizei samples,
-                                                   FramebufferAttachmentRenderTarget **rtOut)
+angle::Result TextureVk::syncAsAttachmentRenderTarget(const gl::Context *context,
+                                                      const gl::ImageIndex &imageIndex,
+                                                      GLsizei samples)
 {
-    GLint requestedLevel = imageIndex.getLevelIndex();
-    ASSERT(requestedLevel >= 0);
-
     ContextVk *contextVk = vk::GetImpl(context);
 
     // Sync the texture's image.  See comment on this function in the header.
@@ -3328,6 +3333,21 @@
                             ImageMipLevels::EnabledLevels));
     }
 
+    return angle::Result::Continue;
+}
+
+angle::Result TextureVk::getAttachmentRenderTarget(const gl::Context *context,
+                                                   GLenum binding,
+                                                   const gl::ImageIndex &imageIndex,
+                                                   GLsizei samples,
+                                                   FramebufferAttachmentRenderTarget **rtOut)
+{
+    ContextVk *contextVk = vk::GetImpl(context);
+    GLint requestedLevel = imageIndex.getLevelIndex();
+    ASSERT(requestedLevel >= 0);
+
+    ANGLE_TRY(syncAsAttachmentRenderTarget(context, imageIndex, samples));
+
     const bool hasRenderToTextureEXT =
         contextVk->getFeatures().supportsMultisampledRenderToSingleSampled.enabled;
 
diff --git a/src/libANGLE/renderer/vulkan/TextureVk.h b/src/libANGLE/renderer/vulkan/TextureVk.h
index 29da5a0..9c264c8 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.h
+++ b/src/libANGLE/renderer/vulkan/TextureVk.h
@@ -394,6 +394,9 @@
                         uint32_t imageLevelOffset,
                         uint32_t imageLayerOffset,
                         bool selfOwned);
+    angle::Result syncAsAttachmentRenderTarget(const gl::Context *context,
+                                               const gl::ImageIndex &imageIndex,
+                                               GLsizei samples);
 
     vk::ImageViewHelper &getImageViews() { return mImageView; }
     const vk::ImageViewHelper &getImageViews() const { return mImageView; }
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 5d3c155..e9a55f4 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -131,6 +131,7 @@
 448658624 WIN INTEL VULKAN : GLSLTestLoops.ForContinueInSwitchComplex/* = SKIP
 448658624 WIN INTEL VULKAN : GLSLTest_ES31.ConstantPrecisionPropagation/* = SKIP
 452159379 WIN INTEL VULKAN : ProgramBinaryTest.ReturnedBinaryTwiceShouldMatch/* = SKIP
+519062400 WIN INTEL OPENGL : CopyTextureTestES3.ChangeBaseLevel/* = SKIP
 
 42266241 PIXEL6 VULKAN : VertexAttributeTestES31.MismatchingSignsChangingAttributeType/* = SKIP
 42266241 PIXEL6 VULKAN : VertexAttributeTestES31.MismatchingSignsChangingProgramType/* = SKIP
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index 4e0d0d7..219d1eb 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -3452,50 +3452,82 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
-ANGLE_INSTANTIATE_TEST_ES2(CopyTextureTest);
-ANGLE_INSTANTIATE_TEST_COMBINE_6(CopyTextureVariationsTest,
-                                 CopyTextureVariationsTestPrint,
-                                 testing::ValuesIn(kCopyTextureVariationsSrcFormats),
-                                 testing::ValuesIn(kCopyTextureVariationsDstFormats),
-                                 testing::Bool(),  // flipY
-                                 testing::Bool(),  // premultiplyAlpha
-                                 testing::Bool(),  // unmultiplyAlpha
-                                 testing::ValuesIn(kMesaYFlips),
-                                 ES2_D3D9(),
-                                 ES2_D3D11(),
-                                 ES2_OPENGL(),
-                                 ES2_OPENGLES(),
-                                 ES2_VULKAN(),
-                                 ES2_METAL(),
-                                 ES2_WEBGPU());
-ANGLE_INSTANTIATE_TEST_ES2(CopyTextureTestWebGL);
-ANGLE_INSTANTIATE_TEST(CopyTextureTestDest,
-                       ES2_D3D11(),
-                       ES2_OPENGL(),
-                       ES2_OPENGLES(),
-                       ES2_VULKAN(),
-                       ES2_METAL());
-
-GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CopyTextureTestES3);
-ANGLE_INSTANTIATE_TEST_ES3(CopyTextureTestES3);
-
-class BasicCopyTextureTest : public ANGLETest<>
+// Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM work if the texture base level
+// changes after the texture is synced.
+TEST_P(CopyTextureTestES3, ChangeBaseLevel)
 {
-  protected:
-    BasicCopyTextureTest()
-    {
-        setWindowWidth(64);
-        setWindowHeight(64);
-        setConfigRedBits(8);
-        setConfigGreenBits(8);
-        setConfigBlueBits(8);
-        setConfigAlphaBits(8);
-    }
-};
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
+
+    constexpr uint32_t kWidth  = 33;
+    constexpr uint32_t kHeight = 17;
+
+    const std::vector<GLColor> kMip0(kWidth * kHeight, GLColor::red);
+    const std::vector<GLColor> kMip2(kWidth * kHeight, GLColor::green);
+    const std::vector<GLColor> kMip4(kWidth * kHeight, GLColor::blue);
+
+    // Define levels 0, 2 and 4 for the src texture.
+    GLTexture src;
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kMip0.data());
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kMip2.data());
+    glTexImage2D(GL_TEXTURE_2D, 4, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kMip4.data());
+
+    // Sync the texture to level 2 and copy from it.  The texture is expected to be
+    // framebuffer-attachment-complete, so it's ok to copy from the base level without having the
+    // rest of the mip chain.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+
+    // Copy from level 2
+    GLTexture dst2;
+    glBindTexture(GL_TEXTURE_2D, dst2);
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 5d3c155..e9a55f4 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -131,6 +131,7 @@
 448658624 WIN INTEL VULKAN : GLSLTestLoops.ForContinueInSwitchComplex/* = SKIP
 448658624 WIN INTEL VULKAN : GLSLTest_ES31.ConstantPrecisionPropagation/* = SKIP
 452159379 WIN INTEL VULKAN : ProgramBinaryTest.ReturnedBinaryTwiceShouldMatch/* = SKIP
+519062400 WIN INTEL OPENGL : CopyTextureTestES3.ChangeBaseLevel/* = SKIP
 
 42266241 PIXEL6 VULKAN : VertexAttributeTestES31.MismatchingSignsChangingAttributeType/* = SKIP
 42266241 PIXEL6 VULKAN : VertexAttributeTestES31.MismatchingSignsChangingProgramType/* = SKIP
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index 4e0d0d7..219d1eb 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -3452,50 +3452,82 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
-ANGLE_INSTANTIATE_TEST_ES2(CopyTextureTest);
-ANGLE_INSTANTIATE_TEST_COMBINE_6(CopyTextureVariationsTest,
-                                 CopyTextureVariationsTestPrint,
-                                 testing::ValuesIn(kCopyTextureVariationsSrcFormats),
-                                 testing::ValuesIn(kCopyTextureVariationsDstFormats),
-                                 testing::Bool(),  // flipY
-                                 testing::Bool(),  // premultiplyAlpha
-                                 testing::Bool(),  // unmultiplyAlpha
-                                 testing::ValuesIn(kMesaYFlips),
-                                 ES2_D3D9(),
-                                 ES2_D3D11(),
-                                 ES2_OPENGL(),
-                                 ES2_OPENGLES(),
-                                 ES2_VULKAN(),
-                                 ES2_METAL(),
-                                 ES2_WEBGPU());
-ANGLE_INSTANTIATE_TEST_ES2(CopyTextureTestWebGL);
-ANGLE_INSTANTIATE_TEST(CopyTextureTestDest,
-                       ES2_D3D11(),
-                       ES2_OPENGL(),
-                       ES2_OPENGLES(),
-                       ES2_VULKAN(),
-                       ES2_METAL());
-
-GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CopyTextureTestES3);
-ANGLE_INSTANTIATE_TEST_ES3(CopyTextureTestES3);
-
-class BasicCopyTextureTest : public ANGLETest<>
+// Test that glCopyTextureCHROMIUM and glCopySubTextureCHROMIUM work if the texture base level
+// changes after the texture is synced.
+TEST_P(CopyTextureTestES3, ChangeBaseLevel)
 {
-  protected:
-    BasicCopyTextureTest()
-    {
-        setWindowWidth(64);
-        setWindowHeight(64);
-        setConfigRedBits(8);
-        setConfigGreenBits(8);
-        setConfigBlueBits(8);
-        setConfigAlphaBits(8);
-    }
-};
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
+
+    constexpr uint32_t kWidth  = 33;
+    constexpr uint32_t kHeight = 17;
+
+    const std::vector<GLColor> kMip0(kWidth * kHeight, GLColor::red);
+    const std::vector<GLColor> kMip2(kWidth * kHeight, GLColor::green);
+    const std::vector<GLColor> kMip4(kWidth * kHeight, GLColor::blue);
+
+    // Define levels 0, 2 and 4 for the src texture.
+    GLTexture src;
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kMip0.data());
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kMip2.data());
+    glTexImage2D(GL_TEXTURE_2D, 4, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kMip4.data());
+
+    // Sync the texture to level 2 and copy from it.  The texture is expected to be
+    // framebuffer-attachment-complete, so it's ok to copy from the base level without having the
+    // rest of the mip chain.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+
+    // Copy from level 2
+    GLTexture dst2;
+    glBindTexture(GL_TEXTURE_2D, dst2);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    glCopyTextureCHROMIUM(src, 2, GL_TEXTURE_2D, dst2, 0, GL_RGBA, GL_UNSIGNED_BYTE, GL_FALSE,
+                          GL_FALSE, GL_FALSE);
+    ASSERT_GL_NO_ERROR();
+
+    // Then switch to level 0 and copy from that
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+
+    GLTexture dst0;
+    glBindTexture(GL_TEXTURE_2D, dst0);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    glCopyTextureCHROMIUM(src, 0, GL_TEXTURE_2D, dst0, 0, GL_RGBA, GL_UNSIGNED_BYTE, GL_FALSE,
+                          GL_FALSE, GL_FALSE);
+    ASSERT_GL_NO_ERROR();
+
+    // Switch to level 4 and copy with glCopySubTextureCHROMIUM.
+    glBindTexture(GL_TEXTURE_2D, src);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
+
+    GLTexture dst4;
+    glBindTexture(GL_TEXTURE_2D, dst4);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    glCopySubTextureCHROMIUM(src, 4, GL_TEXTURE_2D, dst4, 0, 0, 0, 0, 0, kWidth, kHeight, GL_FALSE,
+                             GL_FALSE, GL_FALSE);
+    ASSERT_GL_NO_ERROR();
+
+    // Verify the destinations.
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst0, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, kMip0[0]);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst2, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, kMip2[0]);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst4, 0);
+    EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, kMip4[0]);
+    ASSERT_GL_NO_ERROR();
+}
 
 // Test that self-copying a cube map face with an OOB source rectangle doesn't cause lead to an OOB
 // write in texSubImage2D. See https:crbug.com/506377574
-TEST_P(BasicCopyTextureTest, SelfCopyOOBWrite)
+TEST_P(CopyTextureTest, SelfCopyOOBWrite)
 {
     const int kSmallSize = 8;
     const int kBigSize   = 4096;
@@ -3554,6 +3586,31 @@
 
     ASSERT_GL_NO_ERROR();
 }
-ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(BasicCopyTextureTest);
 
+ANGLE_INSTANTIATE_TEST_ES2(CopyTextureTest);
+ANGLE_INSTANTIATE_TEST_COMBINE_6(CopyTextureVariationsTest,
+                                 CopyTextureVariationsTestPrint,
+                                 testing::ValuesIn(kCopyTextureVariationsSrcFormats),
+                                 testing::ValuesIn(kCopyTextureVariationsDstFormats),
+                                 testing::Bool(),  // flipY
+                                 testing::Bool(),  // premultiplyAlpha
+                                 testing::Bool(),  // unmultiplyAlpha
+                                 testing::ValuesIn(kMesaYFlips),
+                                 ES2_D3D9(),
+                                 ES2_D3D11(),
+                                 ES2_OPENGL(),
+                                 ES2_OPENGLES(),
+                                 ES2_VULKAN(),
+                                 ES2_METAL(),
+                                 ES2_WEBGPU());
+ANGLE_INSTANTIATE_TEST_ES2(CopyTextureTestWebGL);
+ANGLE_INSTANTIATE_TEST(CopyTextureTestDest,
+                       ES2_D3D11(),
+                       ES2_OPENGL(),
+                       ES2_OPENGLES(),
+                       ES2_VULKAN(),
+                       ES2_METAL());
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CopyTextureTestES3);
+ANGLE_INSTANTIATE_TEST_ES3(CopyTextureTestES3);
 }  // namespace angle
Loading diff…

Original Bug Report

reported by vm...@google.com

Integer Underflow in ANGLE Vulkan Backend via glCopySubTextureCHROMIUM State Desync

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 potential logic desynchronization between ANGLE’s frontend validation and its Vulkan backend allows the framebuffer attachment completeness validation to be bypassed. By changing the texture base level and executing a copy operation without synchronizing the texture’s dirty state, the backend’s allocated range and frontend state desynchronize. This causes an integer underflow in toVkLevel when resolving the mipmap level index, passing an out-of-bounds value directly to Vulkan commands.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp
  • third_party/angle/src/libANGLE/Context.cpp
  • third_party/angle/src/libANGLE/Texture.cpp
  • third_party/angle/src/libANGLE/validationES2.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_utils.cpp

Estimated timestamp from git blame: 2023-07-12

Root Cause

Static analysis of the ANGLE source code reveals a potential logic desynchronization between OpenGL ES state validation and the Vulkan backend during texture copy operations.

During validation in ValidateCopySubTextureCHROMIUM (in third_party/angle/src/libANGLE/validationES2.cpp), the source level is validated using isFramebufferAttachmentComplete:

if (!source->isFramebufferAttachmentComplete(sourceLevel, &error))
{
    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, error);
    return false;
}

Inside Texture::isFramebufferAttachmentComplete (in third_party/angle/src/libANGLE/Texture.cpp), level-range checks are performed:

if (attachmentMipLevel != mState.mBaseLevel &&
    (attachmentMipLevel < mState.mBaseLevel ||
     attachmentMipLevel > mState.getMipmapMaxLevel())) {
    return false;
}

If the requested sourceLevel (e.g., 0) matches the current frontend base level (mState.mBaseLevel = 0), this condition evaluates to 0 != 0 (false), which bypasses this level-range check.

Normally, modifying parameters like GL_TEXTURE_BASE_LEVEL marks the texture’s parameter state as dirty (DIRTY_BIT_BASE_LEVEL) and defers backend state synchronization. However, when executing a copy, Context::copySubTexture (in third_party/angle/src/libANGLE/Context.cpp) only performs synchronization via syncStateForTexImage():

ANGLE_CONTEXT_TRY(syncStateForTexImage());

syncStateForTexImage() synchronizes using kTexImageDirtyBits and mTexImageDirtyObjects (which contains an empty dirty objects list kTexImageDirtyObjects). This means the source texture’s pending base level change dirty bit is not synchronized before execution.

Consequently, the Vulkan backend retains a stale mFirstAllocatedLevel (e.g., 2 from a prior allocation) while the frontend has updated mState.mBaseLevel to 0. When the copy operation executes and attempts to resolve the Vulkan level index using toVkLevel(sourceLevelGL = 0), it triggers an integer underflow in GetLevelIndex:

vk::LevelIndex GetLevelIndex(gl::LevelIndex levelGL, gl::LevelIndex baseLevel) {
    ASSERT(baseLevel <= levelGL); // Stripped in release builds
    return vk::LevelIndex(levelGL.get() - baseLevel.get()); // 0 - 2 -> underflow to 0xFFFFFFFE
}

The resulting out-of-bounds mipmap level (0xFFFFFFFE) is written directly into the VkImageSubresourceLayers structure’s mipLevel field and submitted to driver-level Vulkan commands (such as vkCmdCopyImage), circumventing host-side boundary checks.


Potential Trigger Path

Based on code flow analysis, the following potential sequence of GLES commands could trigger the underflow on an ES3 context with ANGLE-on-Vulkan (note: these steps are theoretical as our tooling currently lacks the capability to execute code):

  1. Generate and bind a source texture src:
    GLuint src;
    glGenTextures(1, &src);
    glBindTexture(GL_TEXTURE_2D, src);
    
  2. Specify and initialize level 0 and level 2 of src to mark them initialized:
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    
  3. Set GL_TEXTURE_BASE_LEVEL of src to 2:
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
    
  4. Bind the texture and perform a dummy draw call to force the Vulkan backend to allocate the underlying VkImage with mFirstAllocatedLevel = 2:
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
  5. Revert the GL_TEXTURE_BASE_LEVEL of src to 0 (which flags DIRTY_BIT_BASE_LEVEL but does not trigger synchronization yet):
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
    
  6. Generate and allocate a destination texture dst at level 0:
    GLuint dst;
    glGenTextures(1, &dst);
    glBindTexture(GL_TEXTURE_2D, dst);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
  7. Execute the copy operation referencing sourceLevel = 0:
    glCopySubTextureCHROMIUM(src, 0, GL_TEXTURE_2D, dst, 0, 0, 0, 0, 0, 64, 64, GL_FALSE, GL_FALSE, GL_FALSE);
    

At this step, toVkLevel(0) calculates 0 - 2 = 0xFFFFFFFE, recording a malformed copy command straight to the Vulkan driver.


Security Impact

If successfully exploited, this underflow could allow an out-of-bounds GPU memory read (information disclosure). By reading back the destination texture dst (e.g., using glReadPixels), a compromised renderer process could potentially exfiltrate cross-origin GPU memory. On platforms like Android, where the GPU process runs unsandboxed, this represents a significant privilege boundary bypass.


Suggested Fix

To prevent this state desynchronization, the source and destination texture states must be fully synchronized before initiating copy commands.

Inside Context::copySubTexture (and other similar copy functions in Context.cpp), rather than only calling syncStateForTexImage(), ANGLE should explicitly synchronize the source and destination textures. This can be achieved by utilizing syncTextureForCopy:

ANGLE_CONTEXT_TRY(syncTextureForCopy(sourceTexture));
ANGLE_CONTEXT_TRY(syncTextureForCopy(destTexture));

Calling syncTextureForCopy ensures that any pending dirty bits on the texture parameter state (including DIRTY_BIT_BASE_LEVEL) are flushed and synchronized with the Vulkan backend prior to executing copySubTexture operations.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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