CVE-2026-14425
Overview
Files Changed
src/libANGLE/renderer/vulkan/ContextVk.hsrc/libANGLE/renderer/vulkan/SamplerVk.cppsrc/libANGLE/renderer/vulkan/ShareGroupVk.cppsrc/libANGLE/renderer/vulkan/TextureVk.cppsrc/libANGLE/renderer/vulkan/UtilsVk.cppsrc/libANGLE/renderer/vulkan/vk_cache_utils.cpp
Patch
From 52232eaf409a28d77947df5622af274e1ef770c6 Mon Sep 17 00:00:00 2001 From: Charlie Lao <[email protected]> Date: Wed, 03 Jun 2026 16:37:04 -0700 Subject: [PATCH] Vulkan: Fix UAF bug associated with orphaned sampler clean up This change addresses a lifetime issue in the Vulkan backend where samplers could be destroyed while still in use by a command buffer in a different share group. This only occurs when using the EGL_ANGLE_display_texture_share_group extension. Since this extension is only used by chromium, and it guarantees its usage is always thread safe, this CL adds back a per renderer mSamplerCache. If the extension is used, per renderer sampler cache is used, otherwise per share group cache is used. This way we will no longer have orphaned sampler, thus avoids the threading problem and synchronization problem associated with cleanupOrphanedSamplers. Bug: chromium:517935753 Change-Id: I5f6b3ec9dc526a72d11eff7b0a939f40a0c01bc4 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7910772 Commit-Queue: Charlie Lao <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- diff --git a/src/libANGLE/renderer/vulkan/ContextVk.h b/src/libANGLE/renderer/vulkan/ContextVk.h index f88b655..097243e 100644 --- a/src/libANGLE/renderer/vulkan/ContextVk.h +++ b/src/libANGLE/renderer/vulkan/ContextVk.h @@ -253,6 +253,22 @@ { return mShareGroupVk->getMetaDescriptorPools(); } + SamplerCache &getSamplerCache() + { + if (hasDisplayTextureShareGroup()) + { + return mRenderer->getSamplerCache(); + } + return mShareGroupVk->getSamplerCache(); + } + SamplerYcbcrConversionCache &getYuvConversionCache() + { + if (hasDisplayTextureShareGroup()) + { + return mRenderer->getYuvConversionCache(); + } + return mShareGroupVk->getYuvConversionCache(); + } // Device loss gl::GraphicsResetStatus getResetStatus() override; diff --git a/src/libANGLE/renderer/vulkan/SamplerVk.cpp b/src/libANGLE/renderer/vulkan/SamplerVk.cpp index f15b039..1344921 100644 --- a/src/libANGLE/renderer/vulkan/SamplerVk.cpp +++ b/src/libANGLE/renderer/vulkan/SamplerVk.cpp @@ -40,7 +40,7 @@ } vk::SamplerDesc desc(contextVk, mState, false, nullptr, static_cast<angle::FormatID>(0)); - ANGLE_TRY(contextVk->getShareGroup()->getSamplerCache().getSampler(contextVk, desc, &mSampler)); + ANGLE_TRY(contextVk->getSamplerCache().getSampler(contextVk, desc, &mSampler)); return angle::Result::Continue; } diff --git a/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp b/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp index e7a430e..dee9b6e 100644 --- a/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp +++ b/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp @@ -176,8 +176,8 @@ mPipelineLayoutCache.destroy(mRenderer); mDescriptorSetLayoutCache.destroy(mRenderer); - mSamplerCache.destroy(mRenderer, hasDisplayTextureShareGroup); - mYuvConversionCache.destroy(mRenderer, hasDisplayTextureShareGroup); + mSamplerCache.destroy(mRenderer); + mYuvConversionCache.destroy(mRenderer); mMetaDescriptorPools[DescriptorSetIndex::UniformsAndXfb].destroy(mRenderer); mMetaDescriptorPools[DescriptorSetIndex::Texture].destroy(mRenderer); diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp index e46cba8..23c3b9b 100644 --- a/src/libANGLE/renderer/vulkan/TextureVk.cpp +++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp @@ -2822,8 +2822,7 @@ vk::SharedSamplerPtr sampler; vk::SamplerDesc samplerDesc(contextVk, samplerState, false, nullptr, static_cast<angle::FormatID>(0)); - ANGLE_TRY( - contextVk->getShareGroup()->getSamplerCache().getSampler(contextVk, samplerDesc, &sampler)); + ANGLE_TRY(contextVk->getSamplerCache().getSampler(contextVk, samplerDesc, &sampler)); // If the image has more levels than supported, generate as many mips as possible at a time. const vk::LevelIndex maxGenerateLevels(UtilsVk::GetGenerateMipmapMaxLevels(contextVk)); @@ -3960,10 +3959,9 @@ vk::SamplerDesc samplerDescSamplerExternal2DY2YEXT(contextVk, mState.getSamplerState(), mState.isStencilMode(), &y2yConversionDesc, mImage->getIntendedFormatID()); - ANGLE_TRY(contextVk->getShareGroup()->getSamplerCache().getSampler(contextVk, samplerDesc, - &mSampler)); - ANGLE_TRY(contextVk->getShareGroup()->getSamplerCache().getSampler( - contextVk, samplerDescSamplerExternal2DY2YEXT, &mY2YSampler)); + ANGLE_TRY(contextVk->getSamplerCache().getSampler(contextVk, samplerDesc, &mSampler)); + ANGLE_TRY(contextVk->getSamplerCache().getSampler(contextVk, samplerDescSamplerExternal2DY2YEXT, + &mY2YSampler)); updateCachedImageViewSerials(); diff --git a/src/libANGLE/renderer/vulkan/UtilsVk.cpp b/src/libANGLE/renderer/vulkan/UtilsVk.cpp index cb97e0e..8317f1b 100644 --- a/src/libANGLE/renderer/vulkan/UtilsVk.cpp +++ b/src/libANGLE/renderer/vulkan/UtilsVk.cpp @@ -1602,8 +1602,7 @@ } vk::SharedSamplerPtr sampler; - ANGLE_TRY( - contextVk->getShareGroup()->getSamplerCache().getSampler(contextVk, samplerDesc, &sampler)); + ANGLE_TRY(contextVk->getSamplerCache().getSampler(contextVk, samplerDesc, &sampler)); vk::DescriptorSetLayoutDesc descriptorSetDesc; descriptorSetDesc.addBinding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, diff --git a/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp b/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp index edee73d..97611ad 100644 --- a/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp +++ b/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp @@ -5797,7 +5797,7 @@ ASSERT((contextVk->getFeatures().supportsYUVSamplerConversion.enabled)); samplerYcbcrConversionInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO; samplerYcbcrConversionInfo.pNext = nullptr; - ANGLE_TRY(contextVk->getShareGroup()->getYuvConversionCache().getSamplerYcbcrConversion( + ANGLE_TRY(contextVk->getYuvConversionCache().getSamplerYcbcrConversion( contextVk, mYcbcrConversionDesc, &samplerYcbcrConversionInfo.conversion)); AddToPNextChain(&createInfo, &samplerYcbcrConversionInfo); @@ -8556,44 +8556,27 @@ ASSERT(mExternalFormatPayload.empty() && mVkFormatPayload.empty()); } -void SamplerYcbcrConversionCache::destroy(vk::Renderer *renderer, bool orphanConversionInfo) +void SamplerYcbcrConversionCache::destroy(vk::Renderer *renderer) { renderer->accumulateCacheStats(VulkanCacheType::SamplerYcbcrConversion, mCacheStats); - // If the EGL_ANGLE_display_texture_share_group extension is causing some samplers to - // stay alive, there is no way to know which conversion info object needs to stay alive. - // stash them all in the renderer to be destroyed when possible. - if (orphanConversionInfo) - { - for (auto &iter : mExternalFormatPayload) - { - renderer->addSamplerYcbcrConversionToOrphanList(iter.second.release()); - } - for (auto &iter : mVkFormatPayload) - { - renderer->addSamplerYcbcrConversionToOrphanList(iter.second.release()); - } - } - else - { - VkDevice device = renderer->getDevice(); + VkDevice device = renderer->getDevice(); - uint32_t count = static_cast<uint32_t>(mExternalFormatPayload.size()); - for (auto &iter : mExternalFormatPayload) - { - vk::SamplerYcbcrConversion &samplerYcbcrConversion = iter.second; - samplerYcbcrConversion.destroy(device); - } - renderer->onDeallocateHandle(vk::HandleType::SamplerYcbcrConversion, count); - - count = static_cast<uint32_t>(mExternalFormatPayload.size()); - for (auto &iter : mVkFormatPayload) - { - vk::SamplerYcbcrConversion &samplerYcbcrConversion = iter.second; - samplerYcbcrConversion.destroy(device); - } - renderer->onDeallocateHandle(vk::HandleType::SamplerYcbcrConversion, count); + uint32_t count = static_cast<uint32_t>(mExternalFormatPayload.size()); + for (auto &iter : mExternalFormatPayload) + { + vk::SamplerYcbcrConversion &samplerYcbcrConversion = iter.second; + samplerYcbcrConversion.destroy(device); } + renderer->onDeallocateHandle(vk::HandleType::SamplerYcbcrConversion, count); + + count = static_cast<uint32_t>(mVkFormatPayload.size()); + for (auto &iter : mVkFormatPayload) + { + vk::SamplerYcbcrConversion &samplerYcbcrConversion = iter.second; + samplerYcbcrConversion.destroy(device); + } + renderer->onDeallocateHandle(vk::HandleType::SamplerYcbcrConversion, count); mExternalFormatPayload.clear(); mVkFormatPayload.clear(); @@ -8642,33 +8625,18 @@ ASSERT(mPayload.empty()); } -void SamplerCache::destroy(vk::Renderer *renderer, bool orphanReferencedSamplers)
Regression Test / PoC
diff --git a/src/tests/egl_tests/EGLContextSharingTest.cpp b/src/tests/egl_tests/EGLContextSharingTest.cpp
index c4b6de0..3d3edd5 100644
--- a/src/tests/egl_tests/EGLContextSharingTest.cpp
+++ b/src/tests/egl_tests/EGLContextSharingTest.cpp
@@ -411,6 +411,205 @@
eglDestroyContext(display, context2);
}
+// Regression test for sampler lifetime bug when EGL_ANGLE_display_texture_share_group is used.
+TEST_P(EGLContextSharingTest, DisplayShareGroupSamplerInFlightWhenOwningShareGroupDestroyed)
+{
+ EGLDisplay display = getEGLWindow()->getDisplay();
+ ANGLE_SKIP_TEST_IF(
+ !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group"));
+ ANGLE_SKIP_TEST_IF(!IsVulkan());
+
+ EGLConfig config = getEGLWindow()->getConfig();
+ EGLSurface surface = getEGLWindow()->getSurface();
+
+ const EGLint inShareGroupContextAttribs[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE};
+
+ // Two contexts in the *display* texture share group but each in its own share group
+ EGLContext contextA = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs);
+ EGLContext contextB = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs);
+ ASSERT_NE(contextA, EGL_NO_CONTEXT);
+ ASSERT_NE(contextB, EGL_NO_CONTEXT);
+
+ // A larger render target widens the GPU window for the heavy draw in B.
+ constexpr int kHeavyDim = 256;
+ const EGLint pbufferAttribs[] = {EGL_WIDTH, kHeavyDim, EGL_HEIGHT, kHeavyDim, EGL_NONE};
+ EGLSurface pbufferB = eglCreatePbufferSurface(display, config, pbufferAttribs);
+ ASSERT_NE(pbufferB, EGL_NO_SURFACE);
+
+ // Context A: create the shared texture and draw with it once.
+ ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, contextA));
+ GLTexture sharedTex;
+ glBindTexture(GL_TEXTURE_2D, sharedTex);
+ constexpr GLsizei kTexSize = 4;
+ std::vector<GLColor> texData(kTexSize * kTexSize, GLColor::green);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ texData.data());
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ {
+ ANGLE_GL_PROGRAM(progA, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+ drawQuad(progA, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_NO_ERROR();
+ glFlush();
+ }
+
+ // Context B: bind the same display-shared texture and submit a long running draw.
+ ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB));
+ glViewport(0, 0, kHeavyDim, kHeavyDim);
+ glBindTexture(GL_TEXTURE_2D, sharedTex);
+
+ constexpr char kHeavyFS[] = R"(precision highp float;
+varying vec2 v_texCoord;
+uniform sampler2D u_tex2D;
+void main()
+{
+ vec4 acc = vec4(0.0);
+ // Busy work to make sure the GPU is using the texture when context A is destroyed.
+ for (int i = 0; i < 4000; ++i)
+ {
+ acc += texture2D(u_tex2D, v_texCoord + vec2(float(i) * 0.000001, 0.0));
+ }
+ gl_FragColor = acc * 0.00025;
+})";
+ ANGLE_GL_PROGRAM(progHeavy, essl1_shaders::vs::Texture2D(), kHeavyFS);
+ glUseProgram(progHeavy);
+ glUniform1i(glGetUniformLocation(progHeavy, "u_tex2D"), 0);
+ // Several expensive draw calls so the GPU is using the texture when context A is destroyed
+ for (int i = 0; i < 16; ++i)
+ {
+ drawQuad(progHeavy, essl1_shaders::PositionAttrib(), 0.5f);
+ }
+ EXPECT_GL_NO_ERROR();
+ glFlush(); // batch_B submitted, GPU now executing with S_A in its descriptor set.
+
+ // Still in B: dirty a sampler-state bit and draw again.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ {
+ ANGLE_GL_PROGRAM(progB2, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+ drawQuad(progB2, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_NO_ERROR();
+ glFlush();
+ }
+
+ // Destroy context A. The texture's sampler currently in use by context B should not be freed.
+ SafeDestroyContext(display, contextA);
+
+ // Finish, to make sure the work is done.
+ ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB));
+ glFinish();
+ sharedTex.reset();
+ EXPECT_GL_NO_ERROR();
+
+ eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglDestroySurface(display, pbufferB);
+ SafeDestroyContext(display, contextB);
+ getEGLWindow()->makeCurrent();
+}
+
+// Variant of DisplayShareGroupSamplerInFlightWhenOwningShareGroupDestroyed, except the texture's
+// sampler state is not modified
+TEST_P(EGLContextSharingTest, DisplayShareGroupOrphanedSamplerReapedWhileInFlight)
+{
+ EGLDisplay display = getEGLWindow()->getDisplay();
+ ANGLE_SKIP_TEST_IF(
+ !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group"));
+ ANGLE_SKIP_TEST_IF(!IsVulkan());
+
+ EGLConfig config = getEGLWindow()->getConfig();
+ EGLSurface surface = getEGLWindow()->getSurface();
+
+ const EGLint inShareGroupContextAttribs[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE};
+
+ EGLContext contextA = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs);
+ EGLContext contextB = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs);
+ EGLContext contextC = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs);
+ ASSERT_NE(contextA, EGL_NO_CONTEXT);
+ ASSERT_NE(contextB, EGL_NO_CONTEXT);
+ ASSERT_NE(contextC, EGL_NO_CONTEXT);
+
+ constexpr int kHeavyDim = 256;
+ const EGLint pbufferAttribs[] = {EGL_WIDTH, kHeavyDim, EGL_HEIGHT, kHeavyDim, EGL_NONE};
+ EGLSurface pbufferB = eglCreatePbufferSurface(display, config, pbufferAttribs);
+ ASSERT_NE(pbufferB, EGL_NO_SURFACE);
+
+ // A: Create texture and draw: this creates an internal sampler for the texture
+ ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, contextA));
+ GLTexture sharedTex;
+ glBindTexture(GL_TEXTURE_2D, sharedTex);
+ std::vector<GLColor> texData(16, GLColor::cyan);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texData.data());
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ {
+ ANGLE_GL_PROGRAM(p, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+ drawQuad(p, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_NO_ERROR();
+ glFlush();
+ }
+
+ // B: Draw with texture, using an expensive shader so the GPU is busy when context C is
+ // destroyed
+ ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB));
+ glViewport(0, 0, kHeavyDim, kHeavyDim);
+ glBindTexture(GL_TEXTURE_2D, sharedTex);
+ constexpr char kHeavyFS[] = R"(precision highp float;
+varying vec2 v_texCoord;
+uniform sampler2D u_tex2D;
+void main()
+{
+ vec4 acc = vec4(0.0);
+ for (int i = 0; i < 4000; ++i)
+ {
+ acc += texture2D(u_tex2D, v_texCoord + vec2(float(i) * 0.000001, 0.0));
+ }
+ gl_FragColor = acc * 0.00025;
+})";
+ ANGLE_GL_PROGRAM(progHeavy, essl1_shaders::vs::Texture2D(), kHeavyFS);
+ glUseProgram(progHeavy);
+ glUniform1i(glGetUniformLocation(progHeavy, "u_tex2D"), 0);
+ for (int i = 0; i < 16; ++i)
+ {
+ drawQuad(progHeavy, essl1_shaders::PositionAttrib(), 0.5f);
+ }
+ EXPECT_GL_NO_ERROR();
+ glFlush();
+
+ // Destroy A first; context B still references the texture's internal sampler.
+ SafeDestroyContext(display, contextA);
+
+ // B: dirty sampler state and draw so the texture's internal sampler is recreated.
+ ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB));
+ glBindTexture(GL_TEXTURE_2D, sharedTex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ {
+ ANGLE_GL_PROGRAM(p2, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+ drawQuad(p2, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_NO_ERROR();
+ glFlush();
+ }
+
+ // Destroy unrelated context C. The original texture's internal sampler, still in use by the
+ // GPU, should not be freed.
+ SafeDestroyContext(display, contextC);
+
+ ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB));
+ glFinish();
+ sharedTex.reset();
+ EXPECT_GL_NO_ERROR();
+
+ eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglDestroySurface(display, pbufferB);
+ SafeDestroyContext(display, contextB);
+ getEGLWindow()->makeCurrent();
+}
+
// Tests that after creating a texture using EGL_ANGLE_display_texture_share_group,
// and use it for sampling, and then deleting the Context (which destroys shareGroup) works. If
// anything cached in ShareGroup, it should be handled nicely if texture can outlive ShareGroup (for
Original Bug Report
Potential ANGLE Vulkan: Use-After-Free of VkSampler in cleanupOrphanedSamplers
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 use-after-free vulnerability exists in ANGLE’s Vulkan backend due to lifetime mismanagement of orphaned samplers across distinct share groups. When textures are shared via display texture share groups, a sampler can be synchronously destroyed while still referenced by in-flight GPU commands. This can result in driver-level memory corruption or undefined behavior inside the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/vk_renderer.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_utils.hthird_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/ShareGroupVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
Estimated timestamp from git blame: 2025-08-25
Root Cause
In ANGLE’s Vulkan backend, Renderer::cleanupOrphanedSamplers() (in third_party/angle/src/libANGLE/renderer/vulkan/vk_renderer.cpp) reaps orphaned SharedSamplerPtr objects based purely on their CPU reference counts, without checking active GPU queue serials or ResourceUse bounds.
When the last CPU reference to a SharedSamplerPtr is released during garbage collection, the underlying smart pointer invokes releaseRef() in third_party/angle/src/libANGLE/renderer/vulkan/vk_utils.h. This synchronously destroys the sampler via SamplerHelper::destroy(), which immediately executes vkDestroySampler(). Unlike other garbage-collected objects (which are pushed to mSharedGarbageList and only freed once their corresponding GPU queue serials complete), the sampler is freed immediately.
Simultaneously, ANGLE’s descriptor set updates write the raw VkSampler handle directly into the Vulkan descriptor set in ProgramExecutableVk.cpp:
imageInfo->sampler = samplerHelper.get().getHandle();
Because the Vulkan descriptor set only tracks the raw handle and does not maintain a strong CPU-side reference to the SharedSamplerPtr, the sampler’s lifetime is not synchronized with the GPU timeline when it is shared across different share groups.
Potential Trigger Steps
Note: These steps are analytical and represent a potential trigger path; our tooling agent does not have the capability to run code to confirm a functional proof-of-concept.
- Cross-Sharegroup Setup: Two distinct WebGL contexts (Context A and Context B) are initialized under separate share groups but with
EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLEenabled (default-enabled for the passthrough command decoder on Linux and ChromeOS). This allows them to bind and share the identicalTextureVkinstanceT. - Context A Draws: Context A performs a draw call with
T.TextureVk::syncState()populatesT.mSampler = S_Afrom Context A’sSamplerCache(resulting in a CPU refcount of 2: one in Context A’s cache, one inT.mSampler). - Context B Draws: Context B performs a draw call with
T. SinceT.mSampleris already non-null and the state is unchanged,syncState()returns early. The raw handle ofS_Ais written into Context B’s descriptor set, and Context B flushes its command buffer, leaving its batch (batch_B) in-flight on the GPU. - Context A is Destroyed: Context A’s WebGL context is closed. During destruction,
S_Ais moved toRenderer::mOrphanedSamplerssince its refcount is 2 (still referenced byT.mSampler). - Context B Modifies State: Context B modifies a parameter of
T(e.g.,glTexParameteri(T, GL_TEXTURE_WRAP_S, GL_REPEAT)). - Context B Draws Again: Context B issues a new draw. The state change causes
TextureVk::syncState()to callresetSampler(), dropping the reference toS_A. The CPU refcount ofS_Afalls to 1 (unique insidemOrphanedSamplers). - Immediate Deallocation: Garbage collection runs and calls
Renderer::cleanupOrphanedSamplers(). SinceS_Ais unique, it is erased frommOrphanedSamplersandvkDestroySampler()is called synchronously. - Use-After-Free: If the GPU is still executing
batch_B, the driver attempts to dereference the destroyedVkSamplerhandle, causing a driver-level Use-After-Free in the GPU process.
Suggested Fix
To resolve this issue, the deletion of orphaned samplers must be deferred until all active GPU commands referencing them have completed. Instead of executing vkDestroySampler immediately when a sampler becomes unique in cleanupOrphanedSamplers(), the SamplerHelper or its raw VkSampler handle should be transferred to the renderer’s garbage collector (such as mSharedGarbageList) and associated with the current queue serial, ensuring it is only destroyed once the GPU has finished executing all pending commands.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.