[Lldb-commits] [lldb] [lldb] Fix 'process signal' when the process is stopped (PR #186821)
via lldb-commits
lldb-commits at lists.llvm.org
Thu Jun 11 14:52:35 PDT 2026
https://github.com/youngd007 updated https://github.com/llvm/llvm-project/pull/186821
>From e8c50279dc5a900f76c230a5963350458ea5ac89 Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Mon, 16 Mar 2026 08:30:49 -0700
Subject: [PATCH] [lldb] Fix 'process signal' when the process is stopped
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Process::Signal() only works when the inferior is running because
SendAsyncSignal() relies on interrupting the running process and
replacing the continue packet. When the process is already stopped
(e.g. at a breakpoint), SendAsyncSignal() fails because there is no
continue loop to interrupt.
Fix by falling back to Host::Kill() + Process::Resume() when Signal()
fails on a stopped process. This queues the signal via kill(), then
resumes the process so ptrace intercepts the pending signal — giving
the debugger a chance to handle it according to the current 'process
handle' settings, consistent with the running-process behavior.
---
lldb/source/Commands/CommandObjectProcess.cpp | 16 ++++++
.../functionalities/signal/TestSendSignal.py | 56 +++++++++++++++++++
2 files changed, 72 insertions(+)
diff --git a/lldb/source/Commands/CommandObjectProcess.cpp b/lldb/source/Commands/CommandObjectProcess.cpp
index b0adbf8156bed..eac01a5c5bd5c 100644
--- a/lldb/source/Commands/CommandObjectProcess.cpp
+++ b/lldb/source/Commands/CommandObjectProcess.cpp
@@ -18,6 +18,7 @@
#include "lldb/Breakpoint/BreakpointSite.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/PluginManager.h"
+#include "lldb/Host/Host.h"
#include "lldb/Host/OptionParser.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/CommandOptionArgumentTable.h"
@@ -1185,6 +1186,21 @@ class CommandObjectProcessSignal : public CommandObjectParsed {
Status error(process->Signal(signo));
if (error.Success()) {
result.SetStatus(eReturnStatusSuccessFinishResult);
+ } else if (StateIsStoppedState(process->GetState(), true)) {
+ // Signal() only works when the process is running (it interrupts
+ // the process and replaces the continue packet with a
+ // continue-with-signal packet). When the process is stopped, we
+ // queue the signal via kill() and resume the process. ptrace will
+ // intercept the pending signal, giving the debugger a chance to
+ // handle it according to the current 'process handle' settings.
+ Host::Kill(process->GetID(), signo);
+ error = process->Resume();
+ if (error.Success()) {
+ result.SetStatus(eReturnStatusSuccessFinishResult);
+ return;
+ }
+ result.AppendErrorWithFormat("Failed to send signal %i: %s\n", signo,
+ error.AsCString());
} else {
result.AppendErrorWithFormat("Failed to send signal %i: %s\n", signo,
error.AsCString());
diff --git a/lldb/test/API/functionalities/signal/TestSendSignal.py b/lldb/test/API/functionalities/signal/TestSendSignal.py
index 5b6a50a461cdf..81c50bd962011 100644
--- a/lldb/test/API/functionalities/signal/TestSendSignal.py
+++ b/lldb/test/API/functionalities/signal/TestSendSignal.py
@@ -86,6 +86,62 @@ def test_with_run_command(self):
self.match("statistics dump", [r'"signals": \[', r'"SIGUSR1": 1'])
+ @expectedFailureNetBSD(bugnumber="llvm.org/pr43959")
+ @skipIfWindows # Windows does not support signals
+ def test_send_signal_while_stopped(self):
+ """Test that 'process signal SIGUSR1' works when the process is stopped."""
+ self.build()
+ exe = self.getBuildArtifact("a.out")
+
+ target = self.dbg.CreateTarget(exe)
+ self.assertTrue(target, VALID_TARGET)
+
+ breakpoint = target.BreakpointCreateByLocation("main.c", self.line)
+ self.assertTrue(
+ breakpoint and breakpoint.GetNumLocations() == 1, VALID_BREAKPOINT
+ )
+
+ process_listener = lldb.SBListener("signal_test_listener")
+ launch_info = target.GetLaunchInfo()
+ launch_info.SetWorkingDirectory(self.get_process_working_directory())
+ launch_info.SetListener(process_listener)
+ error = lldb.SBError()
+ process = target.Launch(launch_info, error)
+ self.assertTrue(process, PROCESS_IS_VALID)
+
+ # Configure SIGUSR1 to stop and pass through to the inferior.
+ self.runCmd("process handle -n False -p True -s True SIGUSR1")
+
+ thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonBreakpoint)
+ self.assertTrue(thread.IsValid(), "We hit the first breakpoint.")
+
+ # The process is now stopped. Disable the breakpoint so we don't hit
+ # it again.
+ breakpoint.SetEnabled(False)
+
+ self.setAsync(True)
+
+ # Send a signal while the process is stopped. This should resume the
+ # process with the signal, matching GDB's behavior.
+ self.runCmd("process signal SIGUSR1")
+
+ self.match_state(process_listener, lldb.eStateRunning)
+ self.match_state(process_listener, lldb.eStateStopped)
+
+ # Verify the process stopped because of SIGUSR1.
+ threads = lldbutil.get_stopped_threads(process, lldb.eStopReasonSignal)
+ self.assertEqual(len(threads), 1, "One thread stopped for a signal.")
+ thread = threads[0]
+
+ self.assertGreaterEqual(
+ thread.GetStopReasonDataCount(), 1, "There was data in the event."
+ )
+ self.assertEqual(
+ thread.GetStopReasonDataAtIndex(0),
+ lldbutil.get_signal_number("SIGUSR1"),
+ "The stop signal was SIGUSR1",
+ )
+
def match_state(self, process_listener, expected_state):
num_seconds = 5
broadcaster = self.process().GetBroadcaster()
More information about the lldb-commits
mailing list