diff --git a/src/dawn/tests/white_box/VulkanImageWrappingTests.cpp b/src/dawn/tests/white_box/VulkanImageWrappingTests.cpp index 7cab352610..eeeac6a719 100644 --- a/src/dawn/tests/white_box/VulkanImageWrappingTests.cpp +++ b/src/dawn/tests/white_box/VulkanImageWrappingTests.cpp @@ -27,6 +27,9 @@ #include "dawn/tests/white_box/VulkanImageWrappingTests.h" +#include +#include + #include #include "dawn/common/Math.h" @@ -1070,6 +1073,133 @@ TEST_P(VulkanImageWrappingMultithreadTests, WrapAndClear_OnMultipleThreads) { }); } +// ===================================================================================== +// POC: Stale-FD double-close in ImportedTextureBase when ExportSemaphore fails after +// closing the previous handle. +// +// ImportedTextureBase::OnAfterSubmit() does: +// 1) close(mExternalSemaphoreHandle) <-- field NOT reset to -1 +// 2) DAWN_TRY_ASSIGN(mExternalSemaphoreHandle, ExportSemaphore(...)) +// If (2) fails (e.g. vkGetSemaphoreFdKHR -> VK_ERROR_OUT_OF_HOST_MEMORY under FD +// exhaustion), DAWN_TRY_ASSIGN early-returns WITHOUT writing the LHS, so +// mExternalSemaphoreHandle still holds the integer value of the FD that was just closed. +// The error forces device loss but the texture object survives. When the texture is +// finally released, ~ImportedTextureBase() sees mExternalSemaphoreHandle != -1 and +// close()s the stale FD value again -- which by now may have been reused by the kernel +// for an unrelated descriptor in the same process. +// +// This test simulates the production trigger by hooking GetSemaphoreFdKHR to return +// VK_ERROR_OUT_OF_HOST_MEMORY on the second call (the same error code the driver would +// return under FD exhaustion). It then places a "victim" pipe FD into the freed slot +// and shows that destroying the texture closes the victim. +// ===================================================================================== + +namespace poc_double_close { +PFN_vkGetSemaphoreFdKHR gReal = nullptr; +int gCallCount = 0; +int gRecordedFd = -1; + +::VkResult VKAPI_PTR Hook(VkDevice dev, const VkSemaphoreGetFdInfoKHR* info, int* pFd) { + gCallCount++; + if (gCallCount == 1) { + ::VkResult r = gReal(dev, info, pFd); + if (r == VK_SUCCESS) { + gRecordedFd = *pFd; + } + return r; + } + // Simulate vkGetSemaphoreFdKHR failure under FD exhaustion (a documented return + // code for this entry point per the Vulkan spec). + return VK_ERROR_OUT_OF_HOST_MEMORY; +} +} // namespace poc_double_close + +class VulkanImageWrappingDoubleCloseTest : public VulkanImageWrappingUsageTests {}; + +TEST_P(VulkanImageWrappingDoubleCloseTest, StaleFdDoubleCloseOnExportFailure) { + DAWN_TEST_UNSUPPORTED_IF(GetParam().mExternalImageType != ExternalImageType::OpaqueFD); + + using namespace poc_double_close; + gCallCount = 0; + gRecordedFd = -1; + + // Install the GetSemaphoreFdKHR hook on |secondDevice| (whose loss won't disrupt the + // primary test device). + auto& fn = const_cast(secondDeviceVk->fn); + gReal = fn.GetSemaphoreFdKHR; + fn.GetSemaphoreFdKHR = Hook; + + // Import the external image on |secondDevice|. + wgpu::Texture wrappedTexture = + WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture, {}, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + ASSERT_NE(wrappedTexture.Get(), nullptr); + + // -------- Submit #1: succeeds. mExternalSemaphoreHandle := gRecordedFd -------- + ClearImage(secondDevice, wrappedTexture, {0, 0, 0, 0}); + ASSERT_EQ(gCallCount, 1); + ASSERT_GE(gRecordedFd, 0); + fprintf(stderr, "[POC] Submit #1 done. mExternalSemaphoreHandle = fd %d\n", gRecordedFd); + + // -------- Submit #2: OnAfterSubmit closes fd gRecordedFd, then ExportSemaphore + // fails -> mExternalSemaphoreHandle still == gRecordedFd. Device lost. -- + ClearImage(secondDevice, wrappedTexture, {0, 0, 0, 0}); + ASSERT_EQ(gCallCount, 2); + + // gRecordedFd was close()d at TextureVk.cpp:1850 but the field still holds its value. + // Prove the slot is currently free (or reused by something else): + int probe = fcntl(gRecordedFd, F_GETFD); + fprintf(stderr, + "[POC] Submit #2 failed in OnAfterSubmit (device lost). fcntl(%d, F_GETFD) = %d " + "errno=%d (%s)\n", + gRecordedFd, probe, errno, probe == -1 ? "slot free" : "slot reused"); + + // Place a *victim* descriptor into the stale FD slot. This is what an unrelated + // GPU-process component (Mojo pipe, dmabuf, DRM render node, ...) would do in + // production. Use dup2() to deterministically occupy exactly slot |gRecordedFd|. + int pipefd[2]; + ASSERT_EQ(pipe(pipefd), 0); + if (pipefd[1] != gRecordedFd) { + ASSERT_EQ(dup2(pipefd[1], gRecordedFd), gRecordedFd); + close(pipefd[1]); + } + // Victim is now at fd |gRecordedFd|: the write end of our pipe. + char c = 'A'; + ASSERT_EQ(write(gRecordedFd, &c, 1), 1) + << "victim fd should be a working pipe before texture destruction"; + fprintf(stderr, "[POC] Victim pipe placed at fd %d; write() succeeded.\n", gRecordedFd); + + // -------- Destroy the texture: ~ImportedTextureBase runs and close()s the stale + // mExternalSemaphoreHandle == gRecordedFd, which is now OUR victim pipe. -- + wrappedTexture = nullptr; + + errno = 0; + ssize_t w = write(gRecordedFd, &c, 1); + int saved_errno = errno; + fprintf(stderr, + "[POC] After texture destruction: write(%d) = %zd, errno = %d%s\n", + gRecordedFd, w, saved_errno, + (w == -1 && saved_errno == EBADF) + ? " <-- VICTIM FD WAS CLOSED BY ~ImportedTextureBase (BUG CONFIRMED)" + : ""); + + close(pipefd[0]); + fn.GetSemaphoreFdKHR = gReal; + + // The bug is *confirmed* if the victim was closed by texture destruction. With the + // fix (reset mExternalSemaphoreHandle to -1 before DAWN_TRY_ASSIGN), this passes. + EXPECT_EQ(w, 1) << "BUG: ~ImportedTextureBase performed a stale-FD double-close and " + "closed an unrelated file descriptor (fd " + << gRecordedFd << ")."; +} + +DAWN_INSTANTIATE_TEST_P(VulkanImageWrappingDoubleCloseTest, + {VulkanBackend()}, + {ExternalImageType::OpaqueFD, ExternalImageType::DmaBuf}, + {false}, // UseDedicatedAllocation + {false} // DetectDedicatedAllocation +); + DAWN_INSTANTIATE_TEST_P(VulkanImageWrappingValidationTests, {VulkanBackend()}, {ExternalImageType::OpaqueFD, ExternalImageType::DmaBuf},