Skip to content

Fix relay loops spinning at 100% CPU on synchronous EOF#40725

Open
benhillis wants to merge 1 commit into
masterfrom
benhill/fix-relay-eof-spin
Open

Fix relay loops spinning at 100% CPU on synchronous EOF#40725
benhillis wants to merge 1 commit into
masterfrom
benhill/fix-relay-eof-spin

Conversation

@benhillis

Copy link
Copy Markdown
Member

Summary

Fixes wsl.exe relay threads spinning at 100% CPU after the peer (VM-side hvsocket) closes gracefully.

Problem

When ReadFile completes synchronously with 0 bytes (the canonical EOF signal on stream sockets and pipes), three relay code paths failed to recognize this as EOF:

  • BidirectionalRelay — used by wslrelay.exe for localhost forwarding and socket relay
  • ScopedMultiRelay::Run — used for set-version stderr relay

Instead of breaking out of the loop, they re-issued ReadFile on the EOF'd handle, which returned TRUE + 0 bytes again immediately, producing a pure CPU-bound spin.

Fix

Add bytesRead == 0 checks on the synchronous-success path in:

  • BidirectionalRelay (both left and right read sites)
  • ScopedMultiRelay::Run (both the sync-completion and overlapped-completion paths)

Validation

Added RelayEofDetection unit test that creates pipe pairs, closes the write end (producing synchronous EOF), and verifies each relay function terminates within 5 seconds rather than spinning.

Fixes #40651

Copilot AI review requested due to automatic review settings June 5, 2026 22:07
@benhillis benhillis requested a review from a team as a code owner June 5, 2026 22:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a high-CPU spin in WSL relay loops by treating synchronous ReadFile success with bytesRead == 0 as EOF, ensuring relay threads terminate instead of repeatedly re-issuing reads after a graceful peer close.

Changes:

  • Add explicit bytesRead == 0 EOF handling on synchronous-success ReadFile paths in BidirectionalRelay.
  • Add explicit zero-byte EOF handling in ScopedMultiRelay::Run for both synchronous and overlapped-completion paths.
  • Add a new RelayEofDetection unit test to validate relay termination on EOF scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/windows/common/relay.cpp Adds zero-byte read detection to prevent CPU-bound spinning on synchronous EOF in relay loops.
test/windows/UnitTests.cpp Introduces a unit test intended to ensure relay functions terminate promptly on EOF.

Comment thread test/windows/UnitTests.cpp
Comment thread test/windows/UnitTests.cpp
Comment thread src/windows/common/relay.cpp
@benhillis benhillis force-pushed the benhill/fix-relay-eof-spin branch from 7a41534 to c06f4bc Compare June 5, 2026 22:58
OneBlue
OneBlue previously approved these changes Jun 5, 2026

@OneBlue OneBlue left a comment

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.

LGTM. I think at some point we should move those to use RelayHandle and factor all the IO scheduling in one place, but for now let's merge this

@benhillis

Copy link
Copy Markdown
Member Author

LGTM. I think at some point we should move those to use RelayHandle and factor all the IO scheduling in one place, but for now let's merge this

yep I considered doing that as part of this change, but we can do it as a follow-up.

Copilot AI review requested due to automatic review settings June 6, 2026 02:51
@benhillis benhillis force-pushed the benhill/fix-relay-eof-spin branch from c06f4bc to af6a873 Compare June 6, 2026 02:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment on lines +6268 to +6271
// Wait up to 5 seconds for the relay to finish. If it doesn't, the EOF check is broken.
auto threadHandle = wil::unique_handle(OpenThread(SYNCHRONIZE, FALSE, GetThreadId(relayThread.native_handle())));
VERIFY_ARE_NOT_EQUAL(WaitForSingleObject(relayThread.native_handle(), 5000), WAIT_TIMEOUT);
relayThread.join();
Comment thread test/windows/UnitTests.cpp Outdated
Comment on lines +6288 to +6291
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 thread test/windows/UnitTests.cpp Outdated
Comment on lines +6292 to +6294
writeHandle.reset(
CreateFileW(pipeName.c_str(), GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
VERIFY_IS_NOT_NULL(writeHandle.get());
Comment thread test/windows/UnitTests.cpp Outdated
Comment on lines +6311 to +6312
VERIFY_ARE_NOT_EQUAL(WaitForSingleObject(relayThread.native_handle(), 5000), WAIT_TIMEOUT);
relayThread.join();
@benhillis benhillis force-pushed the benhill/fix-relay-eof-spin branch from af6a873 to a4a502a Compare June 7, 2026 20:01
Copilot AI review requested due to automatic review settings June 7, 2026 21:15
@benhillis benhillis force-pushed the benhill/fix-relay-eof-spin branch from a4a502a to 0cfd705 Compare June 7, 2026 21:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

Comment thread test/windows/UnitTests.cpp Outdated
Comment on lines +6248 to +6260
// Helper: create an overlapped pipe pair (named pipe server + client).
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());

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 +6264 to +6266
wil::unique_handle readPipe, writePipe;
createOverlappedPipe(readPipe, writePipe);

Comment on lines +6273 to +6276
// Create an output pipe to capture relayed data.
wil::unique_handle outputRead, outputWrite;
createOverlappedPipe(outputRead, outputWrite);

Comment on lines +6281 to +6283
// Wait up to 5 seconds for the relay to finish. If it doesn't, the EOF check is broken.
VERIFY_ARE_NOT_EQUAL(WaitForSingleObject(relayThread.native_handle(), 5000), WAIT_TIMEOUT);
relayThread.join();
Comment thread test/windows/UnitTests.cpp Outdated
Comment on lines +6296 to +6299
wil::unique_handle leftRead, leftWrite, rightRead, rightWrite;
createOverlappedPipe(leftRead, leftWrite);
createOverlappedPipe(rightRead, rightWrite);

Comment on lines +6307 to +6309

VERIFY_ARE_NOT_EQUAL(WaitForSingleObject(relayThread.native_handle(), 5000), WAIT_TIMEOUT);
relayThread.join();
Comment on lines +6314 to +6317
wil::unique_handle read1, write1, read2, write2;
createOverlappedPipe(read1, write1);
createOverlappedPipe(read2, write2);

@benhillis benhillis force-pushed the benhill/fix-relay-eof-spin branch from 0cfd705 to d89dffc Compare June 8, 2026 14:28
When ReadFile completes synchronously with 0 bytes read (graceful close on
sockets/pipes), the relay loops failed to detect this as EOF and re-issued
the read in a tight loop, burning CPU indefinitely.

Fix by checking for zero bytes on the synchronous-success path in:
- BidirectionalRelay (both left and right sides)
- ScopedMultiRelay::Run (both sync and overlapped completion paths)

Fixes #40651

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 8, 2026 14:46
@benhillis benhillis force-pushed the benhill/fix-relay-eof-spin branch from d89dffc to 820c01f Compare June 8, 2026 14:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

Comment on lines +6254 to +6256
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 +6258 to +6259
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 +6268 to +6270
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 +6272 to +6273
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 +6293 to +6297
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 on lines +6300 to +6305
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 +6320 to +6324
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 on lines +6349 to +6351
// Sync should return promptly once both inputs hit EOF.
relay.Sync();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants