Skip to content

Commit 1022276

Browse files
committed
[lldb] Avoid data race in IOHandlerProcessSTDIO
This patch fixes a data race in IOHandlerProcessSTDIO. The race is happens between the main thread and the event handling thread. The main thread is running the IOHandler (IOHandlerProcessSTDIO::Run()) when an event comes in that makes us pop the process IO handler which involves cancelling the IOHandler (IOHandlerProcessSTDIO::Cancel). The latter calls SetIsDone(true) which modifies m_is_done. At the same time, we have the main thread reading the variable through GetIsDone(). This patch avoids the race by using a mutex to synchronize the two threads. On the event thread, in IOHandlerProcessSTDIO ::Cancel method, we obtain the lock before changing the value of m_is_done. On the main thread, in IOHandlerProcessSTDIO::Run(), we obtain the lock before reading the value of m_is_done. Additionally, we delay calling SetIsDone until after the loop exists, to avoid a potential race between the two writes. Write of size 1 at 0x00010b66bb68 by thread T7 (mutexes: write M2862, write M718324145051843688): #0 lldb_private::IOHandler::SetIsDone(bool) IOHandler.h:90 (liblldb.15.0.0git.dylib:arm64+0x971d84) #1 IOHandlerProcessSTDIO::Cancel() Process.cpp:4382 (liblldb.15.0.0git.dylib:arm64+0x5ddfec) #2 lldb_private::Debugger::PopIOHandler(std::__1::shared_ptr<lldb_private::IOHandler> const&) Debugger.cpp:1156 (liblldb.15.0.0git.dylib:arm64+0x3cb2a8) #3 lldb_private::Debugger::RemoveIOHandler(std::__1::shared_ptr<lldb_private::IOHandler> const&) Debugger.cpp:1063 (liblldb.15.0.0git.dylib:arm64+0x3cbd2c) #4 lldb_private::Process::PopProcessIOHandler() Process.cpp:4487 (liblldb.15.0.0git.dylib:arm64+0x5c583c) #5 lldb_private::Debugger::HandleProcessEvent(std::__1::shared_ptr<lldb_private::Event> const&) Debugger.cpp:1549 (liblldb.15.0.0git.dylib:arm64+0x3ceabc) #6 lldb_private::Debugger::DefaultEventHandler() Debugger.cpp:1622 (liblldb.15.0.0git.dylib:arm64+0x3cf2c0) #7 std::__1::__function::__func<lldb_private::Debugger::StartEventHandlerThread()::$_2, std::__1::allocator<lldb_private::Debugger::StartEventHandlerThread()::$_2>, void* ()>::operator()() function.h:352 (liblldb.15.0.0git.dylib:arm64+0x3d1bd8) #8 lldb_private::HostNativeThreadBase::ThreadCreateTrampoline(void*) HostNativeThreadBase.cpp:62 (liblldb.15.0.0git.dylib:arm64+0x4c71ac) #9 lldb_private::HostThreadMacOSX::ThreadCreateTrampoline(void*) HostThreadMacOSX.mm:18 (liblldb.15.0.0git.dylib:arm64+0x29ef544) Previous read of size 1 at 0x00010b66bb68 by main thread: #0 lldb_private::IOHandler::GetIsDone() IOHandler.h:92 (liblldb.15.0.0git.dylib:arm64+0x971db8) #1 IOHandlerProcessSTDIO::Run() Process.cpp:4339 (liblldb.15.0.0git.dylib:arm64+0x5ddc7c) #2 lldb_private::Debugger::RunIOHandlers() Debugger.cpp:982 (liblldb.15.0.0git.dylib:arm64+0x3cb48c) #3 lldb_private::CommandInterpreter::RunCommandInterpreter(lldb_private::CommandInterpreterRunOptions&) CommandInterpreter.cpp:3298 (liblldb.15.0.0git.dylib:arm64+0x506478) #4 lldb::SBDebugger::RunCommandInterpreter(bool, bool) SBDebugger.cpp:1166 (liblldb.15.0.0git.dylib:arm64+0x53604) #5 Driver::MainLoop() Driver.cpp:634 (lldb:arm64+0x100006294) #6 main Driver.cpp:853 (lldb:arm64+0x100007344) Differential revision: https://reviews.llvm.org/D120762
1 parent b05918f commit 1022276

File tree

4 files changed

+74
-23
lines changed

4 files changed

+74
-23
lines changed

lldb/source/Target/Process.cpp

+33-23
Original file line numberDiff line numberDiff line change
@@ -4310,6 +4310,12 @@ class IOHandlerProcessSTDIO : public IOHandler {
43104310

43114311
~IOHandlerProcessSTDIO() override = default;
43124312

4313+
void SetIsRunning(bool running) {
4314+
std::lock_guard<std::mutex> guard(m_mutex);
4315+
SetIsDone(!running);
4316+
m_is_running = running;
4317+
}
4318+
43134319
// Each IOHandler gets to run until it is done. It should read data from the
43144320
// "in" and place output into "out" and "err and return when done.
43154321
void Run() override {
@@ -4329,49 +4335,52 @@ class IOHandlerProcessSTDIO : public IOHandler {
43294335
// FD_ZERO, FD_SET are not supported on windows
43304336
#ifndef _WIN32
43314337
const int pipe_read_fd = m_pipe.GetReadFileDescriptor();
4332-
m_is_running = true;
4333-
while (!GetIsDone()) {
4338+
SetIsRunning(true);
4339+
while (true) {
4340+
{
4341+
std::lock_guard<std::mutex> guard(m_mutex);
4342+
if (GetIsDone())
4343+
break;
4344+
}
4345+
43344346
SelectHelper select_helper;
43354347
select_helper.FDSetRead(read_fd);
43364348
select_helper.FDSetRead(pipe_read_fd);
43374349
Status error = select_helper.Select();
43384350

4339-
if (error.Fail()) {
4340-
SetIsDone(true);
4341-
} else {
4342-
char ch = 0;
4343-
size_t n;
4344-
if (select_helper.FDIsSetRead(read_fd)) {
4345-
n = 1;
4346-
if (m_read_file.Read(&ch, n).Success() && n == 1) {
4347-
if (m_write_file.Write(&ch, n).Fail() || n != 1)
4348-
SetIsDone(true);
4349-
} else
4350-
SetIsDone(true);
4351-
}
4351+
if (error.Fail())
4352+
break;
4353+
4354+
char ch = 0;
4355+
size_t n;
4356+
if (select_helper.FDIsSetRead(read_fd)) {
4357+
n = 1;
4358+
if (m_read_file.Read(&ch, n).Success() && n == 1) {
4359+
if (m_write_file.Write(&ch, n).Fail() || n != 1)
4360+
break;
4361+
} else
4362+
break;
4363+
43524364
if (select_helper.FDIsSetRead(pipe_read_fd)) {
43534365
size_t bytes_read;
43544366
// Consume the interrupt byte
43554367
Status error = m_pipe.Read(&ch, 1, bytes_read);
43564368
if (error.Success()) {
4357-
switch (ch) {
4358-
case 'q':
4359-
SetIsDone(true);
4369+
if (ch == 'q')
43604370
break;
4361-
case 'i':
4371+
if (ch == 'i')
43624372
if (StateIsRunningState(m_process->GetState()))
43634373
m_process->SendAsyncInterrupt();
4364-
break;
4365-
}
43664374
}
43674375
}
43684376
}
43694377
}
4370-
m_is_running = false;
4378+
SetIsRunning(false);
43714379
#endif
43724380
}
43734381

43744382
void Cancel() override {
4383+
std::lock_guard<std::mutex> guard(m_mutex);
43754384
SetIsDone(true);
43764385
// Only write to our pipe to cancel if we are in
43774386
// IOHandlerProcessSTDIO::Run(). We can end up with a python command that
@@ -4428,7 +4437,8 @@ class IOHandlerProcessSTDIO : public IOHandler {
44284437
NativeFile m_write_file; // Write to this file (usually the primary pty for
44294438
// getting io to debuggee)
44304439
Pipe m_pipe;
4431-
std::atomic<bool> m_is_running{false};
4440+
std::mutex m_mutex;
4441+
bool m_is_running = false;
44324442
};
44334443

44344444
void Process::SetSTDIOFileDescriptor(int fd) {
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CXX_SOURCES := main.cpp
2+
3+
include Makefile.rules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import lldb
2+
from lldbsuite.test.decorators import *
3+
from lldbsuite.test.lldbtest import *
4+
from lldbsuite.test.lldbpexpect import PExpectTest
5+
6+
class TestIOHandlerProcessSTDIO(PExpectTest):
7+
8+
mydir = TestBase.compute_mydir(__file__)
9+
NO_DEBUG_INFO_TESTCASE = True
10+
11+
# PExpect uses many timeouts internally and doesn't play well
12+
# under ASAN on a loaded machine..
13+
@skipIfAsan
14+
def test(self):
15+
self.build()
16+
self.launch(executable=self.getBuildArtifact("a.out"))
17+
self.child.sendline("run")
18+
19+
self.child.send("foo\n")
20+
self.child.expect_exact("stdout: foo")
21+
22+
self.child.send("bar\n")
23+
self.child.expect_exact("stdout: bar")
24+
25+
self.child.send("baz\n")
26+
self.child.expect_exact("stdout: baz")
27+
28+
self.child.sendcontrol('d')
29+
self.expect_prompt()
30+
self.quit()
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include <iostream>
2+
#include <string>
3+
4+
int main(int argc, char **argv) {
5+
for (std::string line; std::getline(std::cin, line);)
6+
std::cout << "stdout: " << line << '\n';
7+
return 0;
8+
}

0 commit comments

Comments
 (0)