CVE-2026-17704
Overview
Files Changed
src/libANGLE/renderer/vulkan/TextureVk.cppsrc/tests/gl_tests/VulkanPerformanceCounterTest.cpp
Patch
From ac3ce93e2f4a26108bbaaef70812ce16a219e86a Mon Sep 17 00:00:00 2001 From: Zhenyao Mo <[email protected]> Date: Tue, 02 Jun 2026 15:11:29 -0700 Subject: [PATCH] Vulkan: Fix Use-After-Free in TextureVk::releaseImage finalizeImageLayoutInAllSharedContexts() may re-entrantly invoke ContextVk::flushAndSubmitCommands() -> submitCommands(), which can dereference FramebufferVk::mRenderTargetCache.mDepthStencilRenderTarget pointing into the texture's mSingleLayerRenderTargets vectors. This CL moves finalizeImageLayoutInAllSharedContexts() prior to releaseImageViews() in TextureVk::releaseImage(), ensuring the render targets are not freed when the re-entrant submit occurs. A regression test is included to verify this behavior. Bug: chromium:519259107 Change-Id: I69f90e3688cd02933cb93c5adc3ae56421944db4 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7897336 Reviewed-by: Geoff Lang <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Charlie Lao <[email protected]> Auto-Submit: Zhenyao Mo <[email protected]> Commit-Queue: Zhenyao Mo <[email protected]> --- diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp index fc0284c..5c2673f 100644 --- a/src/libANGLE/renderer/vulkan/TextureVk.cpp +++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp @@ -4409,11 +4409,20 @@ { ShareGroupVk *shareGroupVk = contextVk->getShareGroup(); + if (mImage) + { + // finalizeImageLayoutInAllSharedContexts() may re-entrantly call + // ContextVk::flushAndSubmitCommands() -> submitCommands(), which can dereference + // FramebufferVk::mRenderTargetCache.mDepthStencilRenderTarget pointing into this + // texture's mSingleLayerRenderTargets vectors. Finalize before releaseImageViews() + // frees those vectors. + shareGroupVk->finalizeImageLayoutInAllSharedContexts(mImage); + } + releaseImageViews(contextVk); if (mImage) { - shareGroupVk->finalizeImageLayoutInAllSharedContexts(mImage); if (mOwnsImage) { mImage->releaseImage(contextVk); diff --git a/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp b/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp index 2244cec..ba3cb1c 100644 --- a/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp +++ b/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp @@ -14,6 +14,7 @@ #include "include/platform/Feature.h" #include "test_utils/ANGLETest.h" +#include "test_utils/MultiThreadSteps.h" #include "test_utils/angle_test_instantiate.h" #include "test_utils/gl_raii.h" #include "util/EGLWindow.h" @@ -10354,6 +10355,138 @@ .enable(Feature::PreferMonolithicPipelinesOverLibraries) .disable(Feature::MergeProgramPipelineCachesToGlobalCache)); +// Regression test: redefining a depth texture that is bound as the depth attachment of the current +// draw FBO in two share-group contexts (each with an open render pass and a surviving +// mImageWithTileMemory pointer) must not dereference a freed RenderTargetVk via +// FramebufferVk::getImageWithTileMemory() during the re-entrant submitCommands() inside +// TextureVk::releaseImage(). See ContextVk::submitCommands' hasAnyDirtyBit() guard. +TEST_P(VulkanPerformanceCounterTest_TileMemory, RedefineSharedDepthTextureWithOpenRenderPasses) +{ + ANGLE_SKIP_TEST_IF(!isFeatureEnabled(Feature::SimulateTileMemoryForTesting) && + !isFeatureEnabled(Feature::SupportsTileMemoryHeap)); + + constexpr GLsizei kSize = 64; + + enum class Step + { + Start, + Thread1CreatedDepthTex, + Thread0OpenedRP, + Thread1OpenedRP, + Thread0Redefined, + Finish, + Abort, + }; + Step currentStep = Step::Start; + std::mutex mutex; + std::condition_variable condVar; + GLuint sharedDepthTex = 0; + + // Per-context setup: + // 1. FBO_A: Color and a depth renderbuffer that is invalidated. The Vulkan backend may use + // tile memory for this depth renderbuffer + // 2. FBO_B: Color and the shared depth texture. The Vulkan backend does not use tile memory + // for textures. + auto setupContextState = [&](GLuint colorTexA, GLuint depthRB, GLuint fboA, GLuint colorTexB, + GLuint fboB, GLuint depthTex, GLuint program) { + glBindTexture(GL_TEXTURE_2D, colorTexA); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize); + glBindRenderbuffer(GL_RENDERBUFFER, depthRB); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, kSize, kSize); + glBindFramebuffer(GL_FRAMEBUFFER, fboA); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexA, 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRB); + EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + glViewport(0, 0, kSize, kSize); + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + glDepthFunc(GL_ALWAYS); + glClearDepthf(1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); + const GLenum kDepthAttachment = GL_DEPTH_ATTACHMENT; + glInvalidateFramebuffer(GL_FRAMEBUFFER, 1, &kDepthAttachment); + EXPECT_GL_NO_ERROR(); + + glBindTexture(GL_TEXTURE_2D, colorTexB); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize); + glBindFramebuffer(GL_FRAMEBUFFER, fboB); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexB, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0); + EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); + EXPECT_GL_NO_ERROR(); + }; + + auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { + ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); + EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); + + ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1CreatedDepthTex)); + + ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); + GLTexture colorTexA, colorTexB; + GLRenderbuffer depthRB; + GLFramebuffer fboA, fboB; + setupContextState(colorTexA, depthRB, fboA, colorTexB, fboB, sharedDepthTex, program); + + threadSynchronization.nextStep(Step::Thread0OpenedRP); + ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1OpenedRP)); + + // Both contexts now have an open render pass with |sharedDepthTex| as depth attachment. + // In the Vulkan backend if tile memory is used, a reference to |depthRB| may be kept. + // The draw framebuffer is not dirty. + // + // Redefine level 0 of |sharedDepthTex| to recreate its views. This should not cause + // use-after-free. + glBindTexture(GL_TEXTURE_2D, sharedDepthTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, kSize * 2, kSize * 2, 0, + GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr); + EXPECT_GL_NO_ERROR(); + + glFinish(); + threadSynchronization.nextStep(Step::Thread0Redefined); + ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish)); + EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); + }; + + auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) { + ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar); + EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context)); + + ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); + + // Create the shared depth texture as a mutable texture so glTexImage2D can redefine it. + GLuint depthTex; + glGenTextures(1, &depthTex); + glBindTexture(GL_TEXTURE_2D, depthTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, kSize, kSize, 0, GL_DEPTH_COMPONENT, + GL_UNSIGNED_INT, nullptr); + EXPECT_GL_NO_ERROR(); + sharedDepthTex = depthTex; + + threadSynchronization.nextStep(Step::Thread1CreatedDepthTex); + ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0OpenedRP)); + + GLTexture colorTexA, colorTexB; + GLRenderbuffer depthRB; + GLFramebuffer fboA, fboB; + setupContextState(colorTexA, depthRB, fboA, colorTexB, fboB, sharedDepthTex, program); + + threadSynchronization.nextStep(Step::Thread1OpenedRP); + ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Redefined)); + + glFinish(); + glDeleteTextures(1, &depthTex); + threadSynchronization.nextStep(Step::Finish); + EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); + }; + + std::array<LockStepThreadFunc, 2> threadFuncs = {std::move(thread0), std::move(thread1)}; + RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data()); + ASSERT_NE(currentStep, Step::Abort); +} +
Regression Test / PoC
diff --git a/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp b/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp
index 2244cec..ba3cb1c 100644
--- a/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp
+++ b/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp
@@ -14,6 +14,7 @@
#include "include/platform/Feature.h"
#include "test_utils/ANGLETest.h"
+#include "test_utils/MultiThreadSteps.h"
#include "test_utils/angle_test_instantiate.h"
#include "test_utils/gl_raii.h"
#include "util/EGLWindow.h"
@@ -10354,6 +10355,138 @@
.enable(Feature::PreferMonolithicPipelinesOverLibraries)
.disable(Feature::MergeProgramPipelineCachesToGlobalCache));
+// Regression test: redefining a depth texture that is bound as the depth attachment of the current
+// draw FBO in two share-group contexts (each with an open render pass and a surviving
+// mImageWithTileMemory pointer) must not dereference a freed RenderTargetVk via
+// FramebufferVk::getImageWithTileMemory() during the re-entrant submitCommands() inside
+// TextureVk::releaseImage(). See ContextVk::submitCommands' hasAnyDirtyBit() guard.
+TEST_P(VulkanPerformanceCounterTest_TileMemory, RedefineSharedDepthTextureWithOpenRenderPasses)
+{
+ ANGLE_SKIP_TEST_IF(!isFeatureEnabled(Feature::SimulateTileMemoryForTesting) &&
+ !isFeatureEnabled(Feature::SupportsTileMemoryHeap));
+
+ constexpr GLsizei kSize = 64;
+
+ enum class Step
+ {
+ Start,
+ Thread1CreatedDepthTex,
+ Thread0OpenedRP,
+ Thread1OpenedRP,
+ Thread0Redefined,
+ Finish,
+ Abort,
+ };
+ Step currentStep = Step::Start;
+ std::mutex mutex;
+ std::condition_variable condVar;
+ GLuint sharedDepthTex = 0;
+
+ // Per-context setup:
+ // 1. FBO_A: Color and a depth renderbuffer that is invalidated. The Vulkan backend may use
+ // tile memory for this depth renderbuffer
+ // 2. FBO_B: Color and the shared depth texture. The Vulkan backend does not use tile memory
+ // for textures.
+ auto setupContextState = [&](GLuint colorTexA, GLuint depthRB, GLuint fboA, GLuint colorTexB,
+ GLuint fboB, GLuint depthTex, GLuint program) {
+ glBindTexture(GL_TEXTURE_2D, colorTexA);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
+ glBindRenderbuffer(GL_RENDERBUFFER, depthRB);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, kSize, kSize);
+ glBindFramebuffer(GL_FRAMEBUFFER, fboA);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexA, 0);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRB);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ glViewport(0, 0, kSize, kSize);
+ glEnable(GL_DEPTH_TEST);
+ glDepthMask(GL_TRUE);
+ glDepthFunc(GL_ALWAYS);
+ glClearDepthf(1.0f);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ const GLenum kDepthAttachment = GL_DEPTH_ATTACHMENT;
+ glInvalidateFramebuffer(GL_FRAMEBUFFER, 1, &kDepthAttachment);
+ EXPECT_GL_NO_ERROR();
+
+ glBindTexture(GL_TEXTURE_2D, colorTexB);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
+ glBindFramebuffer(GL_FRAMEBUFFER, fboB);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexB, 0);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_NO_ERROR();
+ };
+
+ auto thread0 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) {
+ ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar);
+ EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context));
+
+ ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1CreatedDepthTex));
+
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ GLTexture colorTexA, colorTexB;
+ GLRenderbuffer depthRB;
+ GLFramebuffer fboA, fboB;
+ setupContextState(colorTexA, depthRB, fboA, colorTexB, fboB, sharedDepthTex, program);
+
+ threadSynchronization.nextStep(Step::Thread0OpenedRP);
+ ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread1OpenedRP));
+
+ // Both contexts now have an open render pass with |sharedDepthTex| as depth attachment.
+ // In the Vulkan backend if tile memory is used, a reference to |depthRB| may be kept.
+ // The draw framebuffer is not dirty.
+ //
+ // Redefine level 0 of |sharedDepthTex| to recreate its views. This should not cause
+ // use-after-free.
+ glBindTexture(GL_TEXTURE_2D, sharedDepthTex);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, kSize * 2, kSize * 2, 0,
+ GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ glFinish();
+ threadSynchronization.nextStep(Step::Thread0Redefined);
+ ASSERT_TRUE(threadSynchronization.waitForStep(Step::Finish));
+ EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
+ };
+
+ auto thread1 = [&](EGLDisplay dpy, EGLSurface surface, EGLContext context) {
+ ThreadSynchronization<Step> threadSynchronization(¤tStep, &mutex, &condVar);
+ EXPECT_EGL_TRUE(eglMakeCurrent(dpy, surface, surface, context));
+
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
+
+ // Create the shared depth texture as a mutable texture so glTexImage2D can redefine it.
+ GLuint depthTex;
+ glGenTextures(1, &depthTex);
+ glBindTexture(GL_TEXTURE_2D, depthTex);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, kSize, kSize, 0, GL_DEPTH_COMPONENT,
+ GL_UNSIGNED_INT, nullptr);
+ EXPECT_GL_NO_ERROR();
+ sharedDepthTex = depthTex;
+
+ threadSynchronization.nextStep(Step::Thread1CreatedDepthTex);
+ ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0OpenedRP));
+
+ GLTexture colorTexA, colorTexB;
+ GLRenderbuffer depthRB;
+ GLFramebuffer fboA, fboB;
+ setupContextState(colorTexA, depthRB, fboA, colorTexB, fboB, sharedDepthTex, program);
+
+ threadSynchronization.nextStep(Step::Thread1OpenedRP);
+ ASSERT_TRUE(threadSynchronization.waitForStep(Step::Thread0Redefined));
+
+ glFinish();
+ glDeleteTextures(1, &depthTex);
+ threadSynchronization.nextStep(Step::Finish);
+ EXPECT_EGL_TRUE(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
+ };
+
+ std::array<LockStepThreadFunc, 2> threadFuncs = {std::move(thread0), std::move(thread1)};
+ RunLockStepThreads(getEGLWindow(), threadFuncs.size(), threadFuncs.data());
+ ASSERT_NE(currentStep, Step::Abort);
+}
+
// Enable SimulateTileMemoryForTesting feature to get some test coverage on bots. Note that if both
// SimulateTileMemoryForTesting and SupportsTileMemoryHeap are enabled, SupportsTileMemoryHeap will
// take precedence.
Original Bug Report
Potential Vulkan Use-After-Free in TextureVk::releaseImage due to Bypassed Dirty-Bit Guard
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 because TextureVk::releaseImage frees RenderTargetVk vector elements before performing a re-entrant flush. Since the front-end framebuffers have not yet been notified of the change, the hasAnyDirtyBit() safety check is bypassed, leading to a dangling pointer dereference during submitCommands.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/ShareGroupVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cppthird_party/angle/src/libANGLE/renderer/RenderTargetCache.h
Estimated timestamp from git blame: 2026-06-01
Description
A potential Use-After-Free (UAF) vulnerability exists in ANGLE’s Vulkan backend due to an order-of-operations issue in TextureVk::releaseImage. The function frees vector-backed RenderTargetVk elements before performing a re-entrant flush that triggers submitCommands. This bypasses the !drawFramebuffer->hasAnyDirtyBit() safety check because the front-end framebuffers have not yet been notified of the texture change via observer messages.
Root Cause Analysis
In third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp, the safety check designed to prevent accessing stale/dangling RenderTargetVk cached pointers gates the unsafe getImageWithTileMemory() call on !drawFramebuffer->hasAnyDirtyBit():
// ContextVk.cpp
if (mImageWithTileMemory != nullptr)
{
...
const gl::Framebuffer *drawFramebuffer = mState.getDrawFramebuffer();
const vk::ImageHelper *drawFBOImageWithTileMemory =
(drawFramebuffer != nullptr && !drawFramebuffer->hasAnyDirtyBit())
? getDrawFramebuffer()->getImageWithTileMemory() // <-- Potential UAF
: nullptr;
However, in TextureVk::releaseImage (third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp), the by-value elements of mSingleLayerRenderTargets are cleared (deallocated) before the layout is finalized in shared contexts and before observer notifications are sent:
void TextureVk::releaseImage(ContextVk *contextVk)
{
ShareGroupVk *shareGroupVk = contextVk->getShareGroup();
releaseImageViews(contextVk); // (a) FREE RenderTargetVk vectors
if (mImage)
{
shareGroupVk->finalizeImageLayoutInAllSharedContexts(mImage); // (b) RE-ENTRANT FLUSH → submitCommands
...
}
...
onStateChange(angle::SubjectMessage::SubjectChanged); // FBO-dirty-bit notification is too late
}
Inside releaseImageViews(contextVk), renderTargetLevels.clear() is called, which frees the std::vector<RenderTargetVk> heap storage. Meanwhile, the active FramebufferVk’s mRenderTargetCache.mDepthStencilRenderTarget still holds a bare pointer to one of those elements (&layerRenderTargets[layerIndex]).
When finalizeImageLayoutInAllSharedContexts is called, under specific conditions (e.g., shared contexts with attachment usage or foreign images), it triggers a re-entrant queue submission via flushAndSubmitCommands -> submitCommands. Because onStateChange(SubjectChanged) has not yet run, the front-end gl::Framebuffer dirty bits are empty. Consequently, !drawFramebuffer->hasAnyDirtyBit() evaluates to true, and the code executes getImageWithTileMemory(), causing a UAF read on the freed RenderTargetVk memory.
Potential Trigger Steps
Note: Our tooling does not currently have the capability to run code or execute physical proof-of-concept tests. The following are potential steps that would trigger the path on a target configuration supporting VK_QCOM_tile_memory_heap (e.g., Qualcomm Adreno GPU with driver >= 512.868.1):
- Allocate a depth renderbuffer
Rwith tile memory. Render toFBO_A(R), registeringContextVk::mImageWithTileMemory = R.mImage. - Invalidate the depth attachment of
FBO_AviaglInvalidateFramebuffer(GL_DEPTH_ATTACHMENT), forcingR.mImage->isVkImageContentDefined() = false. - Create a depth texture
depthTexthat does not use tile memory, and bind it toFBO_B. Issue a draw onFBO_B. This closes theFBO_Arender pass. Since the content was undefined,mImageWithTileMemorysurvives and remains pointing toR.mImage. - During
syncStateforFBO_B,mRenderTargetCache.mDepthStencilRenderTargetis populated with a bare pointer intodepthTex’s single-layer render targets vector. - Trigger a level redefinition on
depthTex(for example, viaglTexImage2Dwith incompatible dimensions or via foreign EGLImage target bindings). This invokesredefineLevel->releaseImage. releaseImagefrees the single-layer render targets vector backing the cached pointer, then executesfinalizeImageLayoutInAllSharedContextsondepthTex.mImage.- This triggers a layout finalization flush and re-entrant submission before any dirty bit is propagated. The safety guard evaluates to true, resulting in a UAF read in
FramebufferVk::getImageWithTileMemory()on the stale cached pointer.
Suggested Fix
To prevent this vulnerability, ensure that the front-end observers/framebuffers are notified of the state changes, or that the render targets are not prematurely freed before the re-entrant shared-context layout finalization occurs.
One potential solution is to re-order the operations in TextureVk::releaseImage so that releaseImageViews(contextVk) (which frees the render target vectors) is called after layout finalization and after observer notifications, or notify the observers of state changes before performing shared context layout finalization:
void TextureVk::releaseImage(ContextVk *contextVk)
{
ShareGroupVk *shareGroupVk = contextVk->getShareGroup();
if (mImage)
{
shareGroupVk->finalizeImageLayoutInAllSharedContexts(mImage);
...
}
// Free the image views and render targets after layout finalization has completed
releaseImageViews(contextVk);
...
onStateChange(angle::SubjectMessage::SubjectChanged);
}
Evaluated with Chrome root at commit: 87214e6721f6c34afd9181b80769a24c0c601c50
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.