Skip to content

Commit

Permalink
[lldb] Fix race condition between lldb-vscode and stop hooks executor
Browse files Browse the repository at this point in the history
The race is between these two pieces of code that are executed in two separate
lldb-vscode threads (the first is in the main thread and another is in the
event-handling thread):

```
// lldb-vscode.cpp
g_vsc.debugger.SetAsync(false);
g_vsc.target.Launch(launch_info, error);
g_vsc.debugger.SetAsync(true);
```

```
// Target.cpp
bool old_async = debugger.GetAsyncExecution();
debugger.SetAsyncExecution(true);
debugger.GetCommandInterpreter().HandleCommands(GetCommands(), exc_ctx,
                                                options, result);
debugger.SetAsyncExecution(old_async);
```

The sequence that leads to the bug is this one:
1. Main thread enables synchronous mode and launches the process.
2. When the process is launched, it generates the first stop event.
3. This stop event is catched by the event-handling thread and DoOnRemoval
   is invoked.
4. Inside DoOnRemoval, this thread runs stop hooks. And before running stop
   hooks, the current synchronization mode is stored into old_async (and
   right now it is equal to "false").
5. The main thread finishes the launch and returns to lldb-vscode, the
   synchronization mode is restored to asynchronous by lldb-vscode.
6. Event-handling thread finishes stop hooks processing and restores the
   synchronization mode according to old_async (i.e. makes the mode synchronous)
7. And now the mode is synchronous while lldb-vscode expects it to be
   asynchronous. Synchronous mode forbids the process to broadcast public stop
   events, so, VS Code just hangs because lldb-vscode doesn't notify it about
   stops.

So, this diff makes the target intercept the first stop event if the process is
launched in the synchronous mode, thus preventing stop hooks execution.

The bug is only present on Windows because other platforms already
intercept this event using their own hijacking listeners.

So, this diff also fixes some problems with lldb-vscode tests on Windows to make
it possible to run the related test. Other tests still can't be enabled because
the debugged program prints something into stdout and LLDB can't intercept this
output and redirect it to lldb-vscode properly.

Reviewed By: jingham

Differential Revision: https://reviews.llvm.org/D119548
  • Loading branch information
ilya-nozhkin authored and labath committed Feb 22, 2022
1 parent 3c0096a commit a2c267e
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 138 deletions.
3 changes: 3 additions & 0 deletions lldb/include/lldb/Target/Process.h
Expand Up @@ -3073,6 +3073,9 @@ void PruneThreadPlans();

void ControlPrivateStateThread(uint32_t signal);

Status LaunchPrivate(ProcessLaunchInfo &launch_info, lldb::StateType &state,
lldb::EventSP &event_sp);

Process(const Process &) = delete;
const Process &operator=(const Process &) = delete;
};
Expand Down
5 changes: 5 additions & 0 deletions lldb/packages/Python/lldbsuite/test/lldbtest.py
Expand Up @@ -231,6 +231,11 @@ def pointer_size():

def is_exe(fpath):
"""Returns true if fpath is an executable."""
if fpath == None:
return False
if sys.platform == 'win32':
if not fpath.endswith(".exe"):
fpath += ".exe"
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)


Expand Down
Expand Up @@ -11,8 +11,8 @@ class VSCodeTestCaseBase(TestBase):

def create_debug_adaptor(self, lldbVSCodeEnv=None):
'''Create the Visual Studio Code debug adaptor'''
self.assertTrue(os.path.exists(self.lldbVSCodeExec),
'lldb-vscode must exist')
self.assertTrue(is_exe(self.lldbVSCodeExec),
'lldb-vscode must exist and be executable')
log_file_path = self.getBuildArtifact('vscode.txt')
self.vscode = vscode.DebugAdaptor(
executable=self.lldbVSCodeExec, init_commands=self.setUpCommands(),
Expand Down
24 changes: 4 additions & 20 deletions lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp
Expand Up @@ -443,9 +443,8 @@ lldb::ProcessSP PlatformPOSIX::DebugProcess(ProcessLaunchInfo &launch_info,

// Now create the gdb-remote process.
LLDB_LOG(log, "having target create process with gdb-remote plugin");
process_sp =
target.CreateProcess(launch_info.GetListener(), "gdb-remote", nullptr,
true);
process_sp = target.CreateProcess(launch_info.GetListener(), "gdb-remote",
nullptr, true);

if (!process_sp) {
error.SetErrorString("CreateProcess() failed for gdb-remote process");
Expand All @@ -454,15 +453,8 @@ lldb::ProcessSP PlatformPOSIX::DebugProcess(ProcessLaunchInfo &launch_info,
}

LLDB_LOG(log, "successfully created process");
// Adjust launch for a hijacker.
ListenerSP listener_sp;
if (!launch_info.GetHijackListener()) {
LLDB_LOG(log, "setting up hijacker");
listener_sp =
Listener::MakeListener("lldb.PlatformLinux.DebugProcess.hijack");
launch_info.SetHijackListener(listener_sp);
process_sp->HijackProcessEvents(listener_sp);
}

process_sp->HijackProcessEvents(launch_info.GetHijackListener());

// Log file actions.
if (log) {
Expand All @@ -480,14 +472,6 @@ lldb::ProcessSP PlatformPOSIX::DebugProcess(ProcessLaunchInfo &launch_info,
// Do the launch.
error = process_sp->Launch(launch_info);
if (error.Success()) {
// Handle the hijacking of process events.
if (listener_sp) {
const StateType state = process_sp->WaitForProcessToStop(
llvm::None, nullptr, false, listener_sp);

LLDB_LOG(log, "pid {0} state {0}", process_sp->GetID(), state);
}

// Hook up process PTY if we have one (which we should for local debugging
// with llgs).
int pty_fd = launch_info.GetPTY().ReleasePrimaryFileDescriptor();
Expand Down
10 changes: 5 additions & 5 deletions lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp
Expand Up @@ -217,11 +217,12 @@ lldb::ProcessSP PlatformQemuUser::DebugProcess(ProcessLaunchInfo &launch_info,
launch_info.GetListener(),
process_gdb_remote::ProcessGDBRemote::GetPluginNameStatic(), nullptr,
true);
if (!process_sp) {
error.SetErrorString("Failed to create GDB process");
return nullptr;
}

ListenerSP listener_sp =
Listener::MakeListener("lldb.platform_qemu_user.debugprocess");
launch_info.SetHijackListener(listener_sp);
Process::ProcessEventHijacker hijacker(*process_sp, listener_sp);
process_sp->HijackProcessEvents(launch_info.GetHijackListener());

error = process_sp->ConnectRemote(("unix-connect://" + socket_path).str());
if (error.Fail())
Expand All @@ -232,7 +233,6 @@ lldb::ProcessSP PlatformQemuUser::DebugProcess(ProcessLaunchInfo &launch_info,
process_sp->SetSTDIOFileDescriptor(
launch_info.GetPTY().ReleasePrimaryFileDescriptor());

process_sp->WaitForProcessToStop(llvm::None, nullptr, false, listener_sp);
return process_sp;
}

Expand Down
22 changes: 12 additions & 10 deletions lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
Expand Up @@ -488,18 +488,20 @@ ProcessSP PlatformWindows::DebugProcess(ProcessLaunchInfo &launch_info,
// This is a process attach. Don't need to launch anything.
ProcessAttachInfo attach_info(launch_info);
return Attach(attach_info, debugger, &target, error);
} else {
ProcessSP process_sp = target.CreateProcess(
launch_info.GetListener(), launch_info.GetProcessPluginName(), nullptr,
false);
}

// We need to launch and attach to the process.
launch_info.GetFlags().Set(eLaunchFlagDebug);
if (process_sp)
error = process_sp->Launch(launch_info);
ProcessSP process_sp =
target.CreateProcess(launch_info.GetListener(),
launch_info.GetProcessPluginName(), nullptr, false);

return process_sp;
}
process_sp->HijackProcessEvents(launch_info.GetHijackListener());

// We need to launch and attach to the process.
launch_info.GetFlags().Set(eLaunchFlagDebug);
if (process_sp)
error = process_sp->Launch(launch_info);

return process_sp;
}

lldb::ProcessSP PlatformWindows::Attach(ProcessAttachInfo &attach_info,
Expand Down
Expand Up @@ -428,6 +428,8 @@ PlatformRemoteGDBServer::DebugProcess(ProcessLaunchInfo &launch_info,
"gdb-remote", nullptr, true);

if (process_sp) {
process_sp->HijackProcessEvents(launch_info.GetHijackListener());

error = process_sp->ConnectRemote(connect_url.c_str());
// Retry the connect remote one time...
if (error.Fail())
Expand Down
188 changes: 105 additions & 83 deletions lldb/source/Target/Process.cpp
Expand Up @@ -2429,6 +2429,39 @@ void Process::LoadOperatingSystemPlugin(bool flush) {
}

Status Process::Launch(ProcessLaunchInfo &launch_info) {
StateType state_after_launch = eStateInvalid;
EventSP first_stop_event_sp;
Status status =
LaunchPrivate(launch_info, state_after_launch, first_stop_event_sp);
if (status.Fail())
return status;

if (state_after_launch != eStateStopped &&
state_after_launch != eStateCrashed)
return Status();

// Note, the stop event was consumed above, but not handled. This
// was done to give DidLaunch a chance to run. The target is either
// stopped or crashed. Directly set the state. This is done to
// prevent a stop message with a bunch of spurious output on thread
// status, as well as not pop a ProcessIOHandler.
SetPublicState(state_after_launch, false);

if (PrivateStateThreadIsValid())
ResumePrivateStateThread();
else
StartPrivateStateThread();

// Target was stopped at entry as was intended. Need to notify the
// listeners about it.
if (launch_info.GetFlags().Test(eLaunchFlagStopAtEntry))
HandlePrivateEvent(first_stop_event_sp);

return Status();
}

Status Process::LaunchPrivate(ProcessLaunchInfo &launch_info, StateType &state,
EventSP &event_sp) {
Status error;
m_abi_sp.reset();
m_dyld_up.reset();
Expand All @@ -2445,7 +2478,7 @@ Status Process::Launch(ProcessLaunchInfo &launch_info) {
// be a way to express this path, without actually having a module.
// The way to do that is to set the ExecutableFile in the LaunchInfo.
// Figure that out here:

FileSpec exe_spec_to_use;
if (!exe_module) {
if (!launch_info.GetExecutableFile()) {
Expand All @@ -2455,7 +2488,7 @@ Status Process::Launch(ProcessLaunchInfo &launch_info) {
exe_spec_to_use = launch_info.GetExecutableFile();
} else
exe_spec_to_use = exe_module->GetFileSpec();

if (exe_module && FileSystem::Instance().Exists(exe_module->GetFileSpec())) {
// Install anything that might need to be installed prior to launching.
// For host systems, this will do nothing, but if we are connected to a
Expand All @@ -2464,6 +2497,7 @@ Status Process::Launch(ProcessLaunchInfo &launch_info) {
if (error.Fail())
return error;
}

// Listen and queue events that are broadcasted during the process launch.
ListenerSP listener_sp(Listener::MakeListener("LaunchEventHijack"));
HijackProcessEvents(listener_sp);
Expand All @@ -2473,93 +2507,81 @@ Status Process::Launch(ProcessLaunchInfo &launch_info) {
PausePrivateStateThread();

error = WillLaunch(exe_module);
if (error.Success()) {
const bool restarted = false;
SetPublicState(eStateLaunching, restarted);
m_should_detach = false;
if (error.Fail()) {
std::string local_exec_file_path = exe_spec_to_use.GetPath();
return Status("file doesn't exist: '%s'", local_exec_file_path.c_str());
}

if (m_public_run_lock.TrySetRunning()) {
// Now launch using these arguments.
error = DoLaunch(exe_module, launch_info);
} else {
// This shouldn't happen
error.SetErrorString("failed to acquire process run lock");
}
const bool restarted = false;
SetPublicState(eStateLaunching, restarted);
m_should_detach = false;

if (error.Fail()) {
if (GetID() != LLDB_INVALID_PROCESS_ID) {
SetID(LLDB_INVALID_PROCESS_ID);
const char *error_string = error.AsCString();
if (error_string == nullptr)
error_string = "launch failed";
SetExitStatus(-1, error_string);
}
} else {
EventSP event_sp;
if (m_public_run_lock.TrySetRunning()) {
// Now launch using these arguments.
error = DoLaunch(exe_module, launch_info);
} else {
// This shouldn't happen
error.SetErrorString("failed to acquire process run lock");
}

// Now wait for the process to launch and return control to us, and then
// call DidLaunch:
StateType state = WaitForProcessStopPrivate(event_sp, seconds(10));

if (state == eStateInvalid || !event_sp) {
// We were able to launch the process, but we failed to catch the
// initial stop.
error.SetErrorString("failed to catch stop after launch");
SetExitStatus(0, "failed to catch stop after launch");
Destroy(false);
} else if (state == eStateStopped || state == eStateCrashed) {
DidLaunch();

DynamicLoader *dyld = GetDynamicLoader();
if (dyld)
dyld->DidLaunch();

GetJITLoaders().DidLaunch();

SystemRuntime *system_runtime = GetSystemRuntime();
if (system_runtime)
system_runtime->DidLaunch();

if (!m_os_up)
LoadOperatingSystemPlugin(false);

// We successfully launched the process and stopped, now it the
// right time to set up signal filters before resuming.
UpdateAutomaticSignalFiltering();

// Note, the stop event was consumed above, but not handled. This
// was done to give DidLaunch a chance to run. The target is either
// stopped or crashed. Directly set the state. This is done to
// prevent a stop message with a bunch of spurious output on thread
// status, as well as not pop a ProcessIOHandler.
// We are done with the launch hijack listener, and this stop should
// go to the public state listener:
RestoreProcessEvents();
SetPublicState(state, false);

if (PrivateStateThreadIsValid())
ResumePrivateStateThread();
else
StartPrivateStateThread();

// Target was stopped at entry as was intended. Need to notify the
// listeners about it.
if (state == eStateStopped &&
launch_info.GetFlags().Test(eLaunchFlagStopAtEntry))
HandlePrivateEvent(event_sp);
} else if (state == eStateExited) {
// We exited while trying to launch somehow. Don't call DidLaunch
// as that's not likely to work, and return an invalid pid.
HandlePrivateEvent(event_sp);
}
if (error.Fail()) {
if (GetID() != LLDB_INVALID_PROCESS_ID) {
SetID(LLDB_INVALID_PROCESS_ID);
const char *error_string = error.AsCString();
if (error_string == nullptr)
error_string = "launch failed";
SetExitStatus(-1, error_string);
}
} else {
std::string local_exec_file_path = exe_spec_to_use.GetPath();
error.SetErrorStringWithFormat("file doesn't exist: '%s'",
local_exec_file_path.c_str());
return error;
}

return error;
// Now wait for the process to launch and return control to us, and then
// call DidLaunch:
state = WaitForProcessStopPrivate(event_sp, seconds(10));

if (state == eStateInvalid || !event_sp) {
// We were able to launch the process, but we failed to catch the
// initial stop.
error.SetErrorString("failed to catch stop after launch");
SetExitStatus(0, error.AsCString());
Destroy(false);
return error;
}

if (state == eStateExited) {
// We exited while trying to launch somehow. Don't call DidLaunch
// as that's not likely to work, and return an invalid pid.
HandlePrivateEvent(event_sp);
return Status();
}

if (state == eStateStopped || state == eStateCrashed) {
DidLaunch();

DynamicLoader *dyld = GetDynamicLoader();
if (dyld)
dyld->DidLaunch();

GetJITLoaders().DidLaunch();

SystemRuntime *system_runtime = GetSystemRuntime();
if (system_runtime)
system_runtime->DidLaunch();

if (!m_os_up)
LoadOperatingSystemPlugin(false);

// We successfully launched the process and stopped, now it the
// right time to set up signal filters before resuming.
UpdateAutomaticSignalFiltering();
return Status();
}

return Status("Unexpected process state after the launch: %s, expected %s, "
"%s, %s or %s",
StateAsCString(state), StateAsCString(eStateInvalid),
StateAsCString(eStateExited), StateAsCString(eStateStopped),
StateAsCString(eStateCrashed));
}

Status Process::LoadCore() {
Expand Down

0 comments on commit a2c267e

Please sign in to comment.