diff --git a/src/windows/common/relay.cpp b/src/windows/common/relay.cpp index 9801edccf..2842428d1 100644 --- a/src/windows/common/relay.cpp +++ b/src/windows/common/relay.cpp @@ -324,23 +324,77 @@ void wsl::windows::common::relay::BidirectionalRelay(_In_ HANDLE LeftHandle, _In DWORD leftBytesRead = 0; if (!leftReadPending && LeftHandle) { + bool leftEof = false; if (!ReadFile(LeftHandle, leftReadSpan.data(), gsl::narrow_cast(leftReadSpan.size()), &leftBytesRead, &leftOverlapped)) { - THROW_LAST_ERROR_IF(GetLastError() != ERROR_IO_PENDING); + const auto lastError = GetLastError(); + if ((lastError == ERROR_HANDLE_EOF) || (lastError == ERROR_BROKEN_PIPE)) + { + leftEof = true; + } + else + { + THROW_LAST_ERROR_IF(lastError != ERROR_IO_PENDING); + leftReadPending = true; + } + } + else if (leftBytesRead == 0) + { + leftEof = true; + } + else + { + leftReadPending = true; } - leftReadPending = true; + if (leftEof) + { + LeftHandle = nullptr; + if (WI_IsFlagSet(Flags, RelayFlags::RightIsSocket)) + { + LOG_LAST_ERROR_IF(shutdown(reinterpret_cast(RightHandle), SD_SEND) == SOCKET_ERROR); + } + + continue; + } } DWORD rightBytesRead = 0; if (!rightReadPending && RightHandle) { + bool rightEof = false; if (!ReadFile(RightHandle, rightReadSpan.data(), gsl::narrow_cast(rightReadSpan.size()), &rightBytesRead, &rightOverlapped)) { - THROW_LAST_ERROR_IF(GetLastError() != ERROR_IO_PENDING); + const auto lastError = GetLastError(); + if ((lastError == ERROR_HANDLE_EOF) || (lastError == ERROR_BROKEN_PIPE)) + { + rightEof = true; + } + else + { + THROW_LAST_ERROR_IF(lastError != ERROR_IO_PENDING); + rightReadPending = true; + } + } + else if (rightBytesRead == 0) + { + rightEof = true; } + else + { + rightReadPending = true; + } + + if (rightEof) + { + RightHandle = nullptr; + if (WI_IsFlagSet(Flags, RelayFlags::LeftIsSocket)) + { + LOG_LAST_ERROR_IF(shutdown(reinterpret_cast(LeftHandle), SD_SEND) == SOCKET_ERROR); + } - rightReadPending = true; + continue; + } } const DWORD waitResult = WaitForMultipleObjects(RTL_NUMBER_OF(waitObjects), waitObjects, FALSE, INFINITE); @@ -917,6 +971,11 @@ try THROW_LAST_ERROR_IF(lastError != ERROR_IO_PENDING); } + else if (Transferred == 0) + { + e.State = Eof; + continue; + } // IO is available. Write(i, gsl::make_span(e.Buffer.data(), Transferred)); @@ -938,6 +997,12 @@ try DWORD BytesRead{}; if (ReadFile(e.Handle, e.Buffer.data(), static_cast(e.Buffer.size()), &BytesRead, &e.Overlapped)) { + if (BytesRead == 0) + { + e.State = Eof; + continue; + } + // IO is available. Write(i, gsl::make_span(e.Buffer.data(), BytesRead)); @@ -960,8 +1025,12 @@ try } } - // Only wait if all non-completed inputs have a scheduled ReadFile to avoid a pipe hang. - if (std::all_of(Inputs.begin(), Inputs.end(), [](const auto& e) { return e.State == Eof || e.State == Pending; })) + // Only wait if every non-completed input has a scheduled ReadFile (to avoid a pipe hang) and at least + // one input is still pending. If all inputs have reached EOF there is nothing to wait for, so loop back + // and let the top-of-loop check terminate the relay instead of blocking forever on the exit event. + const bool anyPending = std::any_of(Inputs.begin(), Inputs.end(), [](const auto& e) { return e.State == Pending; }); + if (anyPending && + std::all_of(Inputs.begin(), Inputs.end(), [](const auto& e) { return e.State == Eof || e.State == Pending; })) { // Wait until a handle is signaled. std::vector waits{m_exitEvent.get()}; diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index c8aa0791d..436198812 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -6314,6 +6314,119 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", VERIFY_ARE_EQUAL(expandedHash, expectedHash); } + // Validates that relay functions properly detect EOF (zero-byte read) on synchronous completion + // and terminate instead of spinning. See: https://github.com/microsoft/WSL/issues/40651 + TEST_METHOD(RelayEofDetection) + { + // Helper: create an overlapped pipe pair for unidirectional use (server=read, client=write). + auto createOverlappedPipe = [](wil::unique_handle& readHandle, wil::unique_handle& writeHandle) { + static std::atomic pipeCounter{0}; + auto pipeName = std::format(L"\\\\.\\pipe\\WslTest_RelayEof_{}", pipeCounter++); + + SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE}; + readHandle.reset(CreateNamedPipeW( + pipeName.c_str(), PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, &sa)); + VERIFY_IS_NOT_NULL(readHandle.get()); + + writeHandle.reset(CreateFileW(pipeName.c_str(), GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr)); + VERIFY_IS_NOT_NULL(writeHandle.get()); + }; + + // Helper: create a duplex overlapped pipe pair (both handles support read+write). + auto createDuplexPipe = [](wil::unique_handle& serverHandle, wil::unique_handle& clientHandle) { + static std::atomic pipeCounter{0}; + auto pipeName = std::format(L"\\\\.\\pipe\\WslTest_RelayEofDuplex_{}", pipeCounter++); + + SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE}; + serverHandle.reset(CreateNamedPipeW( + pipeName.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, &sa)); + VERIFY_IS_NOT_NULL(serverHandle.get()); + + clientHandle.reset(CreateFileW(pipeName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr)); + VERIFY_IS_NOT_NULL(clientHandle.get()); + }; + + // Test InterruptableRelay: close the write end of a pipe and verify the relay terminates promptly. + { + wil::unique_handle readPipe, writePipe; + createOverlappedPipe(readPipe, writePipe); + + // Write some data, then close the write end to signal EOF. + constexpr std::string_view testData = "hello"; + DWORD written{}; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(writePipe.get(), testData.data(), static_cast(testData.size()), &written, nullptr)); + writePipe.reset(); + + // Create an output pipe to capture relayed data. + wil::unique_handle outputRead, outputWrite; + createOverlappedPipe(outputRead, outputWrite); + + // Run the relay in a thread — it must terminate once it hits EOF. + auto relayThread = + std::thread([&]() { wsl::windows::common::relay::InterruptableRelay(readPipe.get(), outputWrite.get()); }); + + // Wait up to 5 seconds for the relay to finish. If it doesn't, the EOF check is broken. + VERIFY_ARE_EQUAL(WaitForSingleObject(relayThread.native_handle(), 5000), WAIT_OBJECT_0); + relayThread.join(); + + // Verify the data was relayed. + outputWrite.reset(); + char buf[64]{}; + DWORD bytesRead{}; + ReadFile(outputRead.get(), buf, sizeof(buf), &bytesRead, nullptr); + VERIFY_ARE_EQUAL(bytesRead, static_cast(testData.size())); + VERIFY_ARE_EQUAL(std::string_view(buf, bytesRead), testData); + } + + // Test BidirectionalRelay: close both peer ends and verify it terminates. + { + // BidirectionalRelay reads from and writes to both handles, so we need duplex pipes. + wil::unique_handle leftServer, leftClient, rightServer, rightClient; + createDuplexPipe(leftServer, leftClient); + createDuplexPipe(rightServer, rightClient); + + // Close the client ends to simulate peer EOF on both sides. + leftClient.reset(); + rightClient.reset(); + + // BidirectionalRelay should detect EOF on both sides and return promptly. + auto relayThread = + std::thread([&]() { wsl::windows::common::relay::BidirectionalRelay(leftServer.get(), rightServer.get()); }); + + VERIFY_ARE_EQUAL(WaitForSingleObject(relayThread.native_handle(), 5000), WAIT_OBJECT_0); + relayThread.join(); + } + + // Test ScopedMultiRelay: close write ends and verify it terminates. + { + wil::unique_handle read1, write1, read2, write2; + createOverlappedPipe(read1, write1); + createOverlappedPipe(read2, write2); + + // Write data to one pipe, close both. + constexpr std::string_view testData = "relay_test"; + DWORD written{}; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(write1.get(), testData.data(), static_cast(testData.size()), &written, nullptr)); + write1.reset(); + write2.reset(); + + std::string captured; + std::mutex captureLock; + + { + wsl::windows::common::relay::ScopedMultiRelay relay({read1.get(), read2.get()}, [&](size_t, const gsl::span& buffer) { + std::lock_guard lock(captureLock); + captured.append(reinterpret_cast(buffer.data()), buffer.size()); + }); + + // Sync should return promptly once both inputs hit EOF. + relay.Sync(); + } + + VERIFY_ARE_EQUAL(captured, std::string(testData)); + } + } + TEST_METHOD(EtcHosts) { {