Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/windows/common/relay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,22 @@ void wsl::windows::common::relay::BidirectionalRelay(_In_ HANDLE LeftHandle, _In
if (!ReadFile(LeftHandle, leftReadSpan.data(), gsl::narrow_cast<DWORD>(leftReadSpan.size()), &leftBytesRead, &leftOverlapped))
{
THROW_LAST_ERROR_IF(GetLastError() != ERROR_IO_PENDING);
leftReadPending = true;
}
else if (leftBytesRead == 0)
{
LeftHandle = nullptr;
if (WI_IsFlagSet(Flags, RelayFlags::RightIsSocket))
{
LOG_LAST_ERROR_IF(shutdown(reinterpret_cast<SOCKET>(RightHandle), SD_SEND) == SOCKET_ERROR);
}

leftReadPending = true;
continue;
}
else
{
leftReadPending = true;
}
}

DWORD rightBytesRead = 0;
Expand All @@ -338,9 +351,22 @@ void wsl::windows::common::relay::BidirectionalRelay(_In_ HANDLE LeftHandle, _In
if (!ReadFile(RightHandle, rightReadSpan.data(), gsl::narrow_cast<DWORD>(rightReadSpan.size()), &rightBytesRead, &rightOverlapped))
{
THROW_LAST_ERROR_IF(GetLastError() != ERROR_IO_PENDING);
rightReadPending = true;
}
else if (rightBytesRead == 0)
{
RightHandle = nullptr;
if (WI_IsFlagSet(Flags, RelayFlags::LeftIsSocket))
{
LOG_LAST_ERROR_IF(shutdown(reinterpret_cast<SOCKET>(LeftHandle), SD_SEND) == SOCKET_ERROR);
}

rightReadPending = true;
continue;
}
else
{
rightReadPending = true;
}
}

const DWORD waitResult = WaitForMultipleObjects(RTL_NUMBER_OF(waitObjects), waitObjects, FALSE, INFINITE);
Expand Down Expand Up @@ -917,6 +943,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));
Expand All @@ -938,6 +969,12 @@ try
DWORD BytesRead{};
if (ReadFile(e.Handle, e.Buffer.data(), static_cast<DWORD>(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));

Expand Down
113 changes: 113 additions & 0 deletions test/windows/UnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6241,6 +6241,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<int> 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());
Comment on lines +6254 to +6256

writeHandle.reset(CreateFileW(pipeName.c_str(), GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr));
VERIFY_IS_NOT_NULL(writeHandle.get());
Comment on lines +6258 to +6259
};

// 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<int> 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());
Comment on lines +6268 to +6270

clientHandle.reset(CreateFileW(pipeName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr));
VERIFY_IS_NOT_NULL(clientHandle.get());
Comment on lines +6272 to +6273
};

// Test InterruptableRelay: close the write end of a pipe and verify the relay terminates promptly.
{
wil::unique_handle readPipe, writePipe;
createOverlappedPipe(readPipe, writePipe);

Comment on lines +6278 to +6280
// 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<DWORD>(testData.size()), &written, nullptr));
writePipe.reset();

// Create an output pipe to capture relayed data.
wil::unique_handle outputRead, outputWrite;
createOverlappedPipe(outputRead, outputWrite);

Comment on lines +6287 to +6290
// 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();
Comment thread
benhillis marked this conversation as resolved.
Comment on lines +6295 to +6297
Comment on lines +6295 to +6297
Comment on lines +6293 to +6297

// 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<DWORD>(testData.size()));
VERIFY_ARE_EQUAL(std::string_view(buf, bytesRead), testData);
Comment on lines +6300 to +6305
}

// 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();
Comment thread
benhillis marked this conversation as resolved.
Comment on lines +6322 to +6324
Comment on lines +6320 to +6324
}

// Test ScopedMultiRelay: close write ends and verify it terminates.
{
wil::unique_handle read1, write1, read2, write2;
createOverlappedPipe(read1, write1);
createOverlappedPipe(read2, write2);

Comment on lines +6329 to +6332
// 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<DWORD>(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<gsl::byte>& buffer) {
std::lock_guard lock(captureLock);
captured.append(reinterpret_cast<const char*>(buffer.data()), buffer.size());
});

// Sync should return promptly once both inputs hit EOF.
relay.Sync();
}
Comment on lines +6349 to +6351

VERIFY_ARE_EQUAL(captured, std::string(testData));
}
}

TEST_METHOD(EtcHosts)
{
{
Expand Down
Loading