diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp index 21485ee14c..a417c832ed 100644 --- a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp +++ b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp @@ -4012,6 +4012,24 @@ angle::Result FramebufferVk::startNewRenderPass(ContextVk *contextVk, contextVk->onFragmentShadingRateRead(&mFragmentShadingRateImage); } + // [POC INSTRUMENTATION] On hardware with VK_KHR_fragment_shading_rate + + // foveated rendering configured, the branch above stores + // &mFragmentShadingRateImage (an EMBEDDED member of this heap-allocated + // FramebufferVk) into mRenderPassCommands->mFragmentShadingRateAtachment.mImage + // as a bare ImageHelper*. SwiftShader lacks the extension, so we simulate + // that exact pointer-storage operation here. No Vulkan calls are involved + // in either the storage or the later UAF dereference (endRenderPass -> + // finalizeFragmentShadingRateImageLayout -> isReadBarrierNecessary reads + // mCurrentAccess from freed memory). Gated on env var. + { + static const bool sInjectFSR = ::getenv("ANGLE_POC_FSR_INJECT") != nullptr; + if (sInjectFSR && + !mCurrentFramebufferDesc.hasFragmentShadingRateAttachment()) + { + contextVk->onFragmentShadingRateRead(&mFragmentShadingRateImage); + } + } + return angle::Result::Continue; } diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 11cb22f514..68485c8e5e 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -14982,6 +14982,94 @@ TEST_P(Texture2DTestES3Foveation, ReuseTextureForFoveatedDraw) EXPECT_PIXEL_COLOR_EQ(0, 0, angle::GLColor::blue); } +// Regression test for a use-after-free in ANGLE's Vulkan backend. +// +// FramebufferVk embeds a vk::ImageHelper member (mFragmentShadingRateImage) by +// value. At render-pass start, &mFragmentShadingRateImage is stored as a bare +// ImageHelper* in RenderPassCommandBufferHelper::mFragmentShadingRateAtachment. +// FramebufferVk::destroy() calls mFragmentShadingRateImage.releaseImage(renderer) +// which does NOT clear that bare pointer (only releaseImageFromShareContexts -> +// finalizeImageLayoutInShareContexts does, and that path is not taken). Because +// gl::Framebuffer is not refcounted, glDeleteFramebuffers synchronously frees the +// FramebufferVk heap allocation. A subsequent draw on a different FBO fails +// render-pass reactivation, falls into flushDirtyGraphicsRenderPass -> +// endRenderPass, which dereferences the dangling pointer: +// - vk_helpers.cpp:2277 mFragmentShadingRateAtachment.getImage() != nullptr +// - vk_helpers.cpp:2006 image->isReadBarrierNecessary() reads mCurrentAccess +// - vk_helpers.cpp:2011 image->resetRenderPassUsageFlags() writes +// +// This test reproduces the lifetime sequence on SwiftShader (which lacks +// VK_KHR_fragment_shading_rate) by relying on a small instrumentation hook in +// FramebufferVk::startNewRenderPass that performs the same pointer-storage +// operation that real foveated-rendering hardware would. No Vulkan calls are +// involved in the storage or the dereference. Run under ASAN with +// ANGLE_POC_FSR_INJECT=1. +TEST_P(Texture2DTestES3Foveation, FoveatedFramebufferDeletedWhileRenderPassOpen) +{ + ANGLE_SKIP_TEST_IF(!IsVulkan()); + ANGLE_SKIP_TEST_IF(::getenv("ANGLE_POC_FSR_INJECT") == nullptr); + + ANGLE_GL_PROGRAM(prog, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); + glUseProgram(prog); + + // FBO X — the victim. Heap-allocates a FramebufferVk that embeds + // mFragmentShadingRateImage by value. + GLuint fboX; + glGenFramebuffers(1, &fboX); + GLTexture texX; + glBindTexture(GL_TEXTURE_2D, texX); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 64, 64); + glBindFramebuffer(GL_FRAMEBUFFER, fboX); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texX, 0); + ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + + // FBO Y — bystander, used to displace fboX from the current binding so + // that detachFramebuffer is a no-op when fboX is deleted. + GLuint fboY; + glGenFramebuffers(1, &fboY); + GLTexture texY; + glBindTexture(GL_TEXTURE_2D, texY); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 64, 64); + glBindFramebuffer(GL_FRAMEBUFFER, fboY); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texY, 0); + ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + + // Step 1: Draw on fboX. Starts a render pass. The instrumentation hook + // (or real foveation on Adreno) stores &fboX_heap->mFragmentShadingRateImage + // into mRenderPassCommands->mFragmentShadingRateAtachment.mImage. + glBindFramebuffer(GL_FRAMEBUFFER, fboX); + drawQuad(prog, essl1_shaders::PositionAttrib(), 0.5f); + ASSERT_GL_NO_ERROR(); + + // Step 2: Bind fboY. This sets DIRTY_BIT_DRAW_FRAMEBUFFER_BINDING but does + // NOT close the render pass (ContextVk.cpp comment: "We don't actually + // close the render pass here"). mRenderPassCommands stays started(). + glBindFramebuffer(GL_FRAMEBUFFER, fboY); + + // Step 3: Delete fboX. detachFramebuffer is a no-op (fboX is not the + // current binding). gl::Framebuffer is not refcounted, so this + // synchronously runs FramebufferVk::destroy() — which calls + // mFragmentShadingRateImage.releaseImage(renderer), NOT + // releaseImageFromShareContexts(), so mFragmentShadingRateAtachment.mImage + // is never cleared — and then `delete framebuffer` frees the heap slot + // that embeds mFragmentShadingRateImage. The bare pointer now dangles. + glDeleteFramebuffers(1, &fboX); + + // Step 4: Draw on fboY. ContextVk::syncState processes the dirty + // framebuffer-binding bit, calls onRenderPassFinished, sets + // DIRTY_BIT_RENDER_PASS. handleDirtyGraphicsRenderPass: reactivation check + // fails (fboY's mLastRenderPassQueueSerial doesn't match the open pass, + // which was started for fboX); mRenderPassCommands->started() is true, so + // flushDirtyGraphicsRenderPass -> flushCommandsAndEndRenderPass -> + // mRenderPassCommands->endRenderPass. endRenderPass sees + // mFragmentShadingRateAtachment.getImage() != nullptr (dangling, non-null) + // and calls finalizeFragmentShadingRateImageLayout, which dereferences the + // freed memory. ASAN catches heap-use-after-free here. + drawQuad(prog, essl1_shaders::PositionAttrib(), 0.5f); + + glDeleteFramebuffers(1, &fboY); +} + // QCOM framebuffer foveated rendering with MSAA framebuffer TEST_P(Texture2DTestES3Foveation, DrawWithMsaaFramebuffer) {