CVE-2026-17682
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
BasicCopyTextureTestsrc/tests/gl_tests/CopyTextureTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/vulkan/TextureVk.cppsrc/libANGLE/renderer/vulkan/TextureVk.hsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/CopyTextureTest.cpp
Patch
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);
Regression Test / PoC
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
Original Bug Report
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.cppthird_party/angle/src/libANGLE/Context.cppthird_party/angle/src/libANGLE/Texture.cppthird_party/angle/src/libANGLE/validationES2.cppthird_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):
- Generate and bind a source texture
src:GLuint src; glGenTextures(1, &src); glBindTexture(GL_TEXTURE_2D, src); - Specify and initialize level 0 and level 2 of
srcto 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); - Set
GL_TEXTURE_BASE_LEVELofsrcto2:glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2); - Bind the texture and perform a dummy draw call to force the Vulkan backend to allocate the underlying
VkImagewithmFirstAllocatedLevel = 2:glDrawArrays(GL_TRIANGLES, 0, 3); - Revert the
GL_TEXTURE_BASE_LEVELofsrcto0(which flagsDIRTY_BIT_BASE_LEVELbut does not trigger synchronization yet):glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); - Generate and allocate a destination texture
dstat 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); - 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.