[Lldb-commits] [lldb] [lldb][windows] fix a race condition when polling Pipes (PR #172063)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Fri Dec 12 10:32:55 PST 2025
https://github.com/charles-zablit created https://github.com/llvm/llvm-project/pull/172063
This patch fixes a deadlock due to a race condition inside the `PipeEvent` class.
The lldp-dap tests on Windows are very prone to this deadlock (from my testing almost 1/2 times `TestDAP_Launch.py` fails because of a deadlock inside `PipeEvent`.
Before this patch, the destructor could try to set `m_ready` right before `PipeEvent::Monitor` would reset it, causing `WaitForSingleObject(m_ready, INFINITE);` to deadlock.
This is fixed by ensuring all the `SetEvent` and `ResetEvent` calls inside the class are behind a mutex.
>From 55275e2f97752ab37ac4d67c011f93a2b3cbfe88 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Fri, 12 Dec 2025 18:28:39 +0000
Subject: [PATCH] [lldb][windows] fix a race condition when polling Pipes
---
lldb/source/Host/windows/MainLoopWindows.cpp | 61 +++++++++++++-------
1 file changed, 39 insertions(+), 22 deletions(-)
diff --git a/lldb/source/Host/windows/MainLoopWindows.cpp b/lldb/source/Host/windows/MainLoopWindows.cpp
index 5e5888aee2181..a22a8f002a2ee 100644
--- a/lldb/source/Host/windows/MainLoopWindows.cpp
+++ b/lldb/source/Host/windows/MainLoopWindows.cpp
@@ -53,9 +53,12 @@ class PipeEvent : public MainLoopWindows::IOEvent {
~PipeEvent() override {
if (m_monitor_thread.joinable()) {
- m_stopped = true;
- SetEvent(m_ready);
- CancelIoEx(m_handle, /*lpOverlapped=*/NULL);
+ {
+ std::lock_guard<std::mutex> guard(m_mutex);
+ m_stopped = true;
+ SetEvent(m_ready);
+ CancelIoEx(m_handle, &m_ov);
+ }
m_monitor_thread.join();
}
CloseHandle(m_event);
@@ -63,20 +66,25 @@ class PipeEvent : public MainLoopWindows::IOEvent {
}
void WillPoll() override {
- if (WaitForSingleObject(m_event, /*dwMilliseconds=*/0) != WAIT_TIMEOUT) {
- // The thread has already signalled that the data is available. No need
- // for further polling until we consume that event.
- return;
- }
- if (WaitForSingleObject(m_ready, /*dwMilliseconds=*/0) != WAIT_TIMEOUT) {
- // The thread is already waiting for data to become available.
+ std::lock_guard<std::mutex> guard(m_mutex);
+
+ HANDLE handles[2] = {m_event, m_ready};
+ if (WaitForMultipleObjects(2, handles, /*bWaitAll=*/FALSE,
+ /*dwMilliseconds=*/0) != WAIT_TIMEOUT) {
+ // Either:
+ // - The thread has already signalled that the data is available. No need
+ // for further polling until we consume that event.
+ // - The thread is already waiting for data to become available.
return;
}
// Start waiting.
SetEvent(m_ready);
}
- void Disarm() override { ResetEvent(m_event); }
+ void Disarm() override {
+ std::lock_guard<std::mutex> guard(m_mutex);
+ ResetEvent(m_event);
+ }
/// Monitors the handle performing a zero byte read to determine when data is
/// avaiable.
@@ -87,17 +95,16 @@ class PipeEvent : public MainLoopWindows::IOEvent {
do {
char buf[1];
DWORD bytes_read = 0;
- OVERLAPPED ov;
- ZeroMemory(&ov, sizeof(ov));
+ ZeroMemory(&m_ov, sizeof(m_ov));
// Block on a 0-byte read; this will only resume when data is
// available in the pipe. The pipe must be PIPE_WAIT or this thread
// will spin.
- BOOL success =
- ReadFile(m_handle, buf, /*nNumberOfBytesToRead=*/0, &bytes_read, &ov);
+ BOOL success = ReadFile(m_handle, buf, /*nNumberOfBytesToRead=*/0,
+ &bytes_read, &m_ov);
DWORD bytes_available = 0;
DWORD err = GetLastError();
if (!success && err == ERROR_IO_PENDING) {
- success = GetOverlappedResult(m_handle, &ov, &bytes_read,
+ success = GetOverlappedResult(m_handle, &m_ov, &bytes_read,
/*bWait=*/TRUE);
err = GetLastError();
}
@@ -119,12 +126,20 @@ class PipeEvent : public MainLoopWindows::IOEvent {
// Read may have been cancelled, try again.
continue;
}
-
- // Notify that data is available on the pipe. It's important to set this
- // before clearing m_ready to avoid a race with WillPoll.
- SetEvent(m_event);
- // Stop polling until we're told to resume.
- ResetEvent(m_ready);
+ {
+ std::lock_guard<std::mutex> guard(m_mutex);
+
+ // Notify that data is available on the pipe.
+ SetEvent(m_event);
+ if (m_stopped) {
+ // The destructor might have called SetEvent(m_ready) before this
+ // block. It that's the case, ResetEvent(m_ready) will cause
+ // WaitForSingleObject to wait forever unless we break early.
+ break;
+ }
+ // Stop polling until we're told to resume.
+ ResetEvent(m_ready);
+ }
// Wait until the current read is consumed before doing the next read.
WaitForSingleObject(m_ready, INFINITE);
@@ -134,8 +149,10 @@ class PipeEvent : public MainLoopWindows::IOEvent {
private:
HANDLE m_handle;
HANDLE m_ready;
+ OVERLAPPED m_ov;
std::thread m_monitor_thread;
std::atomic<bool> m_stopped = false;
+ std::mutex m_mutex;
};
class SocketEvent : public MainLoopWindows::IOEvent {
More information about the lldb-commits
mailing list