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
81 changes: 75 additions & 6 deletions src/windows/common/relay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<DWORD>(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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we set this to false here ? That block would only be hit if ReadFile() completes synchronously, right ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might make a good case for moving to MultiHandleWait for this relay (it has the capability to shutdown sockets: example)

}

leftReadPending = true;
if (leftEof)
{
LeftHandle = nullptr;
if (WI_IsFlagSet(Flags, RelayFlags::RightIsSocket))
{
LOG_LAST_ERROR_IF(shutdown(reinterpret_cast<SOCKET>(RightHandle), SD_SEND) == SOCKET_ERROR);
}

continue;
}
}

DWORD rightBytesRead = 0;
if (!rightReadPending && RightHandle)
{
bool rightEof = false;
if (!ReadFile(RightHandle, rightReadSpan.data(), gsl::narrow_cast<DWORD>(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<SOCKET>(LeftHandle), SD_SEND) == SOCKET_ERROR);
}

rightReadPending = true;
continue;
}
}

const DWORD waitResult = WaitForMultipleObjects(RTL_NUMBER_OF(waitObjects), waitObjects, FALSE, INFINITE);
Expand Down Expand Up @@ -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));
Expand All @@ -938,6 +997,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 All @@ -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<HANDLE> waits{m_exitEvent.get()};
Expand Down
113 changes: 113 additions & 0 deletions test/windows/UnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Comment on lines +6317 to +6321
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 +6327 to +6329

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 +6331 to +6332
Comment on lines +6329 to +6332
};

// 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 +6341 to +6343

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 +6345 to +6346
Comment on lines +6343 to +6346
};

// 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 +6351 to +6353
// 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 +6360 to +6363
// 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 +6368 to +6370
Comment on lines +6368 to +6370
Comment on lines +6366 to +6370
Comment on lines +6364 to +6370

// 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 +6373 to +6378
}

// 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 +6395 to +6397
Comment on lines +6393 to +6397
Comment on lines +6392 to +6397
}

// 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 +6402 to +6405
// 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 +6422 to +6424
Comment on lines +6421 to +6424

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

TEST_METHOD(EtcHosts)
{
{
Expand Down