[Lldb-commits] [lldb] [lldb] Allow forks to occur in expression evaluation (PR #184815)
Philip DePetro via lldb-commits
lldb-commits at lists.llvm.org
Wed Apr 15 11:29:23 PDT 2026
https://github.com/pdepetro updated https://github.com/llvm/llvm-project/pull/184815
>From ef1fe58c7bc375bb1eb11df926242a82836082d1 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Thu, 19 Feb 2026 07:45:23 -0800
Subject: [PATCH 1/8] [lldb] Handle forking in expressions
Previously, forking events would cause expression evaluation to stop.
---
lldb/include/lldb/Target/StopInfo.h | 1 +
lldb/source/Target/ThreadPlanCallFunction.cpp | 15 +++++++++++++++
2 files changed, 16 insertions(+)
diff --git a/lldb/include/lldb/Target/StopInfo.h b/lldb/include/lldb/Target/StopInfo.h
index cdd6a6fbe6aa4..5c1f37edcbd3e 100644
--- a/lldb/include/lldb/Target/StopInfo.h
+++ b/lldb/include/lldb/Target/StopInfo.h
@@ -21,6 +21,7 @@ class StopInfo : public std::enable_shared_from_this<StopInfo> {
friend class Process::ProcessEventData;
friend class ThreadPlanBase;
friend class ThreadPlanReverseContinue;
+ friend class ThreadPlanCallFunction;
public:
// Constructors and Destructors
diff --git a/lldb/source/Target/ThreadPlanCallFunction.cpp b/lldb/source/Target/ThreadPlanCallFunction.cpp
index 218111d4faf60..e555745754aab 100644
--- a/lldb/source/Target/ThreadPlanCallFunction.cpp
+++ b/lldb/source/Target/ThreadPlanCallFunction.cpp
@@ -284,6 +284,21 @@ bool ThreadPlanCallFunction::DoPlanExplainsStop(Event *event_ptr) {
if (stop_reason == eStopReasonBreakpoint && BreakpointsExplainStop())
return true;
+ if ((stop_reason == eStopReasonFork) ||
+ (stop_reason == eStopReasonVFork) ||
+ (stop_reason == eStopReasonVForkDone)) {
+ if (stop_reason == eStopReasonFork)
+ LLDB_LOGF(log, "ThreadPlanCallFunction::PlanExplainsStop hit a fork not stopping.");
+ else if (stop_reason == eStopReasonVFork)
+ LLDB_LOGF(log, "ThreadPlanCallFunction::PlanExplainsStop hit a vfork not stopping.");
+ else if (stop_reason == eStopReasonVForkDone)
+ LLDB_LOGF(log, "ThreadPlanCallFunction::PlanExplainsStop hit a vforkdone not stopping.");
+
+ m_real_stop_info_sp->PerformAction(event_ptr);
+ m_real_stop_info_sp->OverrideShouldStop(false);
+ return true;
+ }
+
// One more quirk here. If this event was from Halt interrupting the target,
// then we should not consider ourselves complete. Return true to
// acknowledge the stop.
>From e4177b490a6ff5d94841606d6cf540ae2a9c4e69 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Thu, 19 Feb 2026 09:12:35 -0800
Subject: [PATCH 2/8] Add a test for fork in an expression
---
.../expression/expr-with-fork/Makefile | 4 +++
.../expr-with-fork/TestExprWithFork.py | 33 +++++++++++++++++++
.../expression/expr-with-fork/main.cpp | 21 ++++++++++++
3 files changed, 58 insertions(+)
create mode 100644 lldb/test/API/commands/expression/expr-with-fork/Makefile
create mode 100644 lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
create mode 100644 lldb/test/API/commands/expression/expr-with-fork/main.cpp
diff --git a/lldb/test/API/commands/expression/expr-with-fork/Makefile b/lldb/test/API/commands/expression/expr-with-fork/Makefile
new file mode 100644
index 0000000000000..f016d5b15d839
--- /dev/null
+++ b/lldb/test/API/commands/expression/expr-with-fork/Makefile
@@ -0,0 +1,4 @@
+CXX_SOURCES := main.cpp
+USE_SYSTEM_STDLIB := 1
+
+include Makefile.rules
diff --git a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
new file mode 100644
index 0000000000000..8de07a32ab7cd
--- /dev/null
+++ b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
@@ -0,0 +1,33 @@
+"""
+Test that expressions that call functions which fork
+can be evaluated successfully.
+
+This tests the ThreadPlanCallFunction handling of fork/vfork/vforkdone
+stop reasons, which should be silently resumed rather than causing the
+expression evaluation to fail.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class ExprWithForkTestCase(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_fork(self):
+ """Test that expression evaluation succeeds when the expression calls fork()."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ # Evaluate an expression that calls fork() inside a user function.
+ # The fork will generate a fork stop event which ThreadPlanCallFunction
+ # must handle transparently for the expression to complete.
+ self.expect_expr(
+ "fork_and_return(42)", result_type="int", result_value="42"
+ )
diff --git a/lldb/test/API/commands/expression/expr-with-fork/main.cpp b/lldb/test/API/commands/expression/expr-with-fork/main.cpp
new file mode 100644
index 0000000000000..80565cf6c4420
--- /dev/null
+++ b/lldb/test/API/commands/expression/expr-with-fork/main.cpp
@@ -0,0 +1,21 @@
+#include <sys/wait.h>
+#include <unistd.h>
+
+int fork_and_return(int value) {
+ pid_t pid = fork();
+ if (pid == -1)
+ return -1;
+ if (pid == 0) {
+ // child
+ _exit(0);
+ }
+ // parent
+ int status;
+ waitpid(pid, &status, 0);
+ return value;
+}
+
+int main() {
+ int x = 42;
+ return 0; // break here
+}
>From 957817bc30fa975d6b1fb54a66dc3210085e8e62 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Wed, 4 Mar 2026 12:17:43 -0800
Subject: [PATCH 3/8] Add separate test for vfork and check exit status
---
.../expr-with-fork/TestExprWithFork.py | 18 +++++++++++++++++-
.../expression/expr-with-fork/main.cpp | 8 ++++----
2 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
index 8de07a32ab7cd..80ee288ee709a 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
+++ b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
@@ -29,5 +29,21 @@ def test_expr_with_fork(self):
# The fork will generate a fork stop event which ThreadPlanCallFunction
# must handle transparently for the expression to complete.
self.expect_expr(
- "fork_and_return(42)", result_type="int", result_value="42"
+ "fork_and_return(42, false)", result_type="int", result_value="42"
+ )
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_vfork(self):
+ """Test that expression evaluation succeeds when the expression calls vfork()."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ # Evaluate an expression that calls fork() inside a user function.
+ # The fork will generate a fork stop event which ThreadPlanCallFunction
+ # must handle transparently for the expression to complete.
+ self.expect_expr(
+ "fork_and_return(42, true)", result_type="int", result_value="42"
)
diff --git a/lldb/test/API/commands/expression/expr-with-fork/main.cpp b/lldb/test/API/commands/expression/expr-with-fork/main.cpp
index 80565cf6c4420..4e210df3d7682 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/main.cpp
+++ b/lldb/test/API/commands/expression/expr-with-fork/main.cpp
@@ -1,18 +1,18 @@
#include <sys/wait.h>
#include <unistd.h>
-int fork_and_return(int value) {
- pid_t pid = fork();
+int fork_and_return(int value, bool use_vfork) {
+ pid_t pid = use_vfork ? vfork() : fork();
if (pid == -1)
return -1;
if (pid == 0) {
// child
- _exit(0);
+ _exit(value);
}
// parent
int status;
waitpid(pid, &status, 0);
- return value;
+ return WEXITSTATUS(status);
}
int main() {
>From 9b5183376e9a63437e52a39d64126962df8f8456 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Wed, 8 Apr 2026 12:45:29 -0700
Subject: [PATCH 4/8] Revert "[lldb] Handle forking in expressions"
This reverts commit ef1fe58c7bc375bb1eb11df926242a82836082d1.
---
lldb/include/lldb/Target/StopInfo.h | 1 -
lldb/source/Target/ThreadPlanCallFunction.cpp | 15 ---------------
2 files changed, 16 deletions(-)
diff --git a/lldb/include/lldb/Target/StopInfo.h b/lldb/include/lldb/Target/StopInfo.h
index 5c1f37edcbd3e..cdd6a6fbe6aa4 100644
--- a/lldb/include/lldb/Target/StopInfo.h
+++ b/lldb/include/lldb/Target/StopInfo.h
@@ -21,7 +21,6 @@ class StopInfo : public std::enable_shared_from_this<StopInfo> {
friend class Process::ProcessEventData;
friend class ThreadPlanBase;
friend class ThreadPlanReverseContinue;
- friend class ThreadPlanCallFunction;
public:
// Constructors and Destructors
diff --git a/lldb/source/Target/ThreadPlanCallFunction.cpp b/lldb/source/Target/ThreadPlanCallFunction.cpp
index e555745754aab..218111d4faf60 100644
--- a/lldb/source/Target/ThreadPlanCallFunction.cpp
+++ b/lldb/source/Target/ThreadPlanCallFunction.cpp
@@ -284,21 +284,6 @@ bool ThreadPlanCallFunction::DoPlanExplainsStop(Event *event_ptr) {
if (stop_reason == eStopReasonBreakpoint && BreakpointsExplainStop())
return true;
- if ((stop_reason == eStopReasonFork) ||
- (stop_reason == eStopReasonVFork) ||
- (stop_reason == eStopReasonVForkDone)) {
- if (stop_reason == eStopReasonFork)
- LLDB_LOGF(log, "ThreadPlanCallFunction::PlanExplainsStop hit a fork not stopping.");
- else if (stop_reason == eStopReasonVFork)
- LLDB_LOGF(log, "ThreadPlanCallFunction::PlanExplainsStop hit a vfork not stopping.");
- else if (stop_reason == eStopReasonVForkDone)
- LLDB_LOGF(log, "ThreadPlanCallFunction::PlanExplainsStop hit a vforkdone not stopping.");
-
- m_real_stop_info_sp->PerformAction(event_ptr);
- m_real_stop_info_sp->OverrideShouldStop(false);
- return true;
- }
-
// One more quirk here. If this event was from Halt interrupting the target,
// then we should not consider ourselves complete. Return true to
// acknowledge the stop.
>From fd3438dcb55551ef94a4c97ec927da31ed803137 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Tue, 7 Apr 2026 08:32:27 -0700
Subject: [PATCH 5/8] [lldb] Do not stop if forks occur in expressions
---
lldb/source/Target/StopInfo.cpp | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/lldb/source/Target/StopInfo.cpp b/lldb/source/Target/StopInfo.cpp
index 5110ed16edc91..34a4598f131a8 100644
--- a/lldb/source/Target/StopInfo.cpp
+++ b/lldb/source/Target/StopInfo.cpp
@@ -1501,6 +1501,16 @@ class StopInfoFork : public StopInfo {
thread_sp->GetProcess()->DidFork(m_child_pid, m_child_tid);
}
+ bool ShouldStopSynchronous(Event *event_ptr) override {
+ if (!m_performed_action) {
+ m_performed_action = true;
+ ThreadSP thread_sp(m_thread_wp.lock());
+ if (thread_sp)
+ thread_sp->GetProcess()->DidFork(m_child_pid, m_child_tid);
+ }
+ return false;
+ }
+
bool m_performed_action = false;
private:
@@ -1541,6 +1551,15 @@ class StopInfoVFork : public StopInfo {
if (thread_sp)
thread_sp->GetProcess()->DidVFork(m_child_pid, m_child_tid);
}
+ bool ShouldStopSynchronous(Event *event_ptr) override {
+ if (!m_performed_action) {
+ m_performed_action = true;
+ ThreadSP thread_sp(m_thread_wp.lock());
+ if (thread_sp)
+ thread_sp->GetProcess()->DidVFork(m_child_pid, m_child_tid);
+ }
+ return false;
+ }
bool m_performed_action = false;
@@ -1573,6 +1592,15 @@ class StopInfoVForkDone : public StopInfo {
if (thread_sp)
thread_sp->GetProcess()->DidVForkDone();
}
+ bool ShouldStopSynchronous(Event *event_ptr) override {
+ if (!m_performed_action) {
+ m_performed_action = true;
+ ThreadSP thread_sp(m_thread_wp.lock());
+ if (thread_sp)
+ thread_sp->GetProcess()->DidVForkDone();
+ }
+ return false;
+ }
bool m_performed_action = false;
};
>From 7e81d47f428243520415ae08a3f58f35054cd583 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Tue, 14 Apr 2026 14:25:49 -0700
Subject: [PATCH 6/8] [lldb] Handle fork/vfork during expression evaluation
---
lldb/include/lldb/API/SBExpressionOptions.h | 4 +
lldb/include/lldb/Target/Target.h | 5 +
lldb/source/API/SBExpressionOptions.cpp | 12 +
.../Process/gdb-remote/ProcessGDBRemote.cpp | 36 ++-
lldb/source/Target/Process.cpp | 73 ++++-
lldb/source/Target/StopInfo.cpp | 62 ++--
lldb/source/Target/ThreadPlanCallFunction.cpp | 10 +
.../expr-with-fork/TestExprWithFork.py | 283 +++++++++++++++++-
8 files changed, 429 insertions(+), 56 deletions(-)
diff --git a/lldb/include/lldb/API/SBExpressionOptions.h b/lldb/include/lldb/API/SBExpressionOptions.h
index edfdbb5aaf62f..8dcd6ea8e511a 100644
--- a/lldb/include/lldb/API/SBExpressionOptions.h
+++ b/lldb/include/lldb/API/SBExpressionOptions.h
@@ -67,6 +67,10 @@ class LLDB_API SBExpressionOptions {
void SetTrapExceptions(bool trap_exceptions = true);
+ bool GetStopOnFork() const;
+
+ void SetStopOnFork(bool stop_on_fork = false);
+
void SetLanguage(lldb::LanguageType language);
/// Set the language using a pair of language code and version as
/// defined by the DWARF 6 specification.
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 67f373aa5a325..8ce89b9ba67f8 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -446,6 +446,10 @@ class EvaluateExpressionOptions {
void SetTrapExceptions(bool b) { m_trap_exceptions = b; }
+ bool GetStopOnFork() const { return m_stop_on_fork; }
+
+ void SetStopOnFork(bool b) { m_stop_on_fork = b; }
+
bool GetREPLEnabled() const { return m_repl; }
void SetREPLEnabled(bool b) { m_repl = b; }
@@ -530,6 +534,7 @@ class EvaluateExpressionOptions {
bool m_stop_others = true;
bool m_debug = false;
bool m_trap_exceptions = true;
+ bool m_stop_on_fork = false;
bool m_repl = false;
bool m_generate_debug_info = false;
bool m_ansi_color_errors = false;
diff --git a/lldb/source/API/SBExpressionOptions.cpp b/lldb/source/API/SBExpressionOptions.cpp
index 10ac4f26d0ba9..7786daa98d1cb 100644
--- a/lldb/source/API/SBExpressionOptions.cpp
+++ b/lldb/source/API/SBExpressionOptions.cpp
@@ -151,6 +151,18 @@ void SBExpressionOptions::SetTrapExceptions(bool trap_exceptions) {
m_opaque_up->SetTrapExceptions(trap_exceptions);
}
+bool SBExpressionOptions::GetStopOnFork() const {
+ LLDB_INSTRUMENT_VA(this);
+
+ return m_opaque_up->GetStopOnFork();
+}
+
+void SBExpressionOptions::SetStopOnFork(bool stop_on_fork) {
+ LLDB_INSTRUMENT_VA(this, stop_on_fork);
+
+ m_opaque_up->SetStopOnFork(stop_on_fork);
+}
+
void SBExpressionOptions::SetLanguage(lldb::LanguageType language) {
LLDB_INSTRUMENT_VA(this, language);
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index 1ae0aeeba55f9..ae791071086ef 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -6053,6 +6053,16 @@ void ProcessGDBRemote::DidForkSwitchHardwareTraps(bool enable) {
void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
Log *log = GetLog(GDBRLog::Process);
+ // During expression evaluation, force follow-parent. The expression is
+ // running on the parent's thread and following the child would cause the
+ // expression thread to vanish (the child has different thread IDs).
+ FollowForkMode follow_fork_mode = GetFollowForkMode();
+ if (follow_fork_mode == eFollowChild && GetModIDRef().IsRunningExpression()) {
+ LLDB_LOG(log, "ProcessGDBRemote::DidFork() overriding follow-fork-mode "
+ "to parent during expression evaluation");
+ follow_fork_mode = eFollowParent;
+ }
+
lldb::pid_t parent_pid = m_gdb_comm.GetCurrentProcessID();
// Any valid TID will suffice, thread-relevant actions will set a proper TID
// anyway.
@@ -6061,7 +6071,7 @@ void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
lldb::pid_t follow_pid, detach_pid;
lldb::tid_t follow_tid, detach_tid;
- switch (GetFollowForkMode()) {
+ switch (follow_fork_mode) {
case eFollowParent:
follow_pid = parent_pid;
follow_tid = parent_tid;
@@ -6088,7 +6098,7 @@ void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
// Remove hardware breakpoints / watchpoints from parent process if we're
// following child.
- if (GetFollowForkMode() == eFollowChild)
+ if (follow_fork_mode == eFollowChild)
DidForkSwitchHardwareTraps(false);
// Switch to the process that is going to be followed
@@ -6108,7 +6118,7 @@ void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
// Hardware breakpoints/watchpoints are not inherited implicitly,
// so we need to readd them if we're following child.
- if (GetFollowForkMode() == eFollowChild) {
+ if (follow_fork_mode == eFollowChild) {
DidForkSwitchHardwareTraps(true);
// Update our PID
SetID(child_pid);
@@ -6120,10 +6130,18 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
LLDB_LOG(
log,
- "ProcessGDBRemote::DidFork() called for child_pid: {0}, child_tid {1}",
+ "ProcessGDBRemote::DidVFork() called for child_pid: {0}, child_tid {1}",
child_pid, child_tid);
++m_vfork_in_progress_count;
+ // See comment in DidFork(): force follow-parent during expression evaluation.
+ FollowForkMode follow_fork_mode = GetFollowForkMode();
+ if (follow_fork_mode == eFollowChild && GetModIDRef().IsRunningExpression()) {
+ LLDB_LOG(log, "ProcessGDBRemote::DidVFork() overriding follow-fork-mode "
+ "to parent during expression evaluation");
+ follow_fork_mode = eFollowParent;
+ }
+
// Disable all software breakpoints for the duration of vfork.
if (m_gdb_comm.SupportsGDBStoppointPacket(eBreakpointSoftware))
DidForkSwitchSoftwareBreakpoints(false);
@@ -6131,7 +6149,7 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
lldb::pid_t detach_pid;
lldb::tid_t detach_tid;
- switch (GetFollowForkMode()) {
+ switch (follow_fork_mode) {
case eFollowParent:
detach_pid = child_pid;
detach_tid = child_tid;
@@ -6144,7 +6162,7 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
// Switch to the parent process before detaching it.
if (!m_gdb_comm.SetCurrentThread(detach_tid, detach_pid)) {
- LLDB_LOG(log, "ProcessGDBRemote::DidFork() unable to set pid/tid");
+ LLDB_LOG(log, "ProcessGDBRemote::DidVFork() unable to set pid/tid");
return;
}
@@ -6154,7 +6172,7 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
// Switch to the child process.
if (!m_gdb_comm.SetCurrentThread(child_tid, child_pid) ||
!m_gdb_comm.SetCurrentThreadForRun(child_tid, child_pid)) {
- LLDB_LOG(log, "ProcessGDBRemote::DidFork() unable to reset pid/tid");
+ LLDB_LOG(log, "ProcessGDBRemote::DidVFork() unable to reset pid/tid");
return;
}
break;
@@ -6164,12 +6182,12 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
Status error = m_gdb_comm.Detach(false, detach_pid);
if (error.Fail()) {
LLDB_LOG(log,
- "ProcessGDBRemote::DidFork() detach packet send failed: {0}",
+ "ProcessGDBRemote::DidVFork() detach packet send failed: {0}",
error.AsCString() ? error.AsCString() : "<unknown error>");
return;
}
- if (GetFollowForkMode() == eFollowChild) {
+ if (follow_fork_mode == eFollowChild) {
// Update our PID
SetID(child_pid);
}
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index e725f9eac7d5f..27174a0df0f19 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -5386,6 +5386,7 @@ Process::RunThreadPlan(ExecutionContext &exe_ctx,
// still succeed.
bool miss_first_event = true;
#endif
+ bool pending_stop_on_vfork_done = false;
while (true) {
// We usually want to resume the process if we get to the top of the
// loop. The only exception is if we get two running events with no
@@ -5539,13 +5540,71 @@ Process::RunThreadPlan(ExecutionContext &exe_ctx,
do_resume = false;
handle_running_event = true;
} else {
- const bool handle_interrupts = true;
- return_value = *HandleStoppedEvent(
- expr_thread_id, thread_plan_sp, thread_plan_restorer,
- event_sp, event_to_broadcast_sp, options,
- handle_interrupts);
- if (return_value == eExpressionThreadVanished)
- keep_going = false;
+ // Check for fork/vfork/vforkdone stop reasons. DidFork /
+ // DidVFork / DidVForkDone have already been called by
+ // PerformAction (via DoOnRemoval).
+ bool handled_fork = false;
+ if (ThreadSP fork_thread_sp =
+ GetThreadList().FindThreadByID(expr_thread_id)) {
+ if (StopInfoSP stop_info_sp = fork_thread_sp->GetStopInfo()) {
+ StopReason reason = stop_info_sp->GetStopReason();
+ if (reason == eStopReasonFork ||
+ reason == eStopReasonVFork ||
+ reason == eStopReasonVForkDone) {
+ handled_fork = true;
+ if (reason == eStopReasonFork &&
+ options.GetStopOnFork()) {
+ // Fork + stop-on-fork: DidFork already ran via
+ // PerformAction. Parent breakpoints are unaffected.
+ LLDB_LOGF(log, "Process::RunThreadPlan(): stopped for "
+ "fork, stop-on-fork is set.");
+ return_value = eExpressionInterrupted;
+ } else if (reason == eStopReasonVFork &&
+ options.GetStopOnFork()) {
+ // VFork + stop-on-fork: DidVFork already disabled
+ // software breakpoints (parent and child share
+ // address space). Interrupting now would leave the
+ // user with non-functional breakpoints. Defer the
+ // stop until vforkdone, when DidVForkDone restores
+ // breakpoint state.
+ LLDB_LOGF(log,
+ "Process::RunThreadPlan(): got vfork with "
+ "stop-on-fork, deferring stop to "
+ "vforkdone.");
+ pending_stop_on_vfork_done = true;
+ keep_going = true;
+ do_resume = true;
+ handle_running_event = true;
+ } else if (reason == eStopReasonVForkDone &&
+ pending_stop_on_vfork_done) {
+ // Deferred vfork stop: the vfork cycle has
+ // completed. DidVForkDone has re-enabled software
+ // breakpoints and decremented
+ // m_vfork_in_progress_count.
+ LLDB_LOGF(log, "Process::RunThreadPlan(): vfork cycle "
+ "complete, stop-on-fork is set.");
+ pending_stop_on_vfork_done = false;
+ return_value = eExpressionInterrupted;
+ } else {
+ LLDB_LOGF(log, "Process::RunThreadPlan(): got fork "
+ "event, continuing.");
+ keep_going = true;
+ do_resume = true;
+ handle_running_event = true;
+ }
+ }
+ }
+ }
+
+ if (!handled_fork) {
+ const bool handle_interrupts = true;
+ return_value = *HandleStoppedEvent(
+ expr_thread_id, thread_plan_sp, thread_plan_restorer,
+ event_sp, event_to_broadcast_sp, options,
+ handle_interrupts);
+ if (return_value == eExpressionThreadVanished)
+ keep_going = false;
+ }
}
} break;
diff --git a/lldb/source/Target/StopInfo.cpp b/lldb/source/Target/StopInfo.cpp
index 34a4598f131a8..4370ecdd0fae0 100644
--- a/lldb/source/Target/StopInfo.cpp
+++ b/lldb/source/Target/StopInfo.cpp
@@ -1476,7 +1476,19 @@ class StopInfoFork : public StopInfo {
~StopInfoFork() override = default;
- bool ShouldStop(Event *event_ptr) override { return false; }
+ bool ShouldStop(Event *event_ptr) override {
+ // During expression evaluation, return true so that the fork event
+ // reaches RunThreadPlan as a real stop (not auto-restarted by
+ // DoOnRemoval). RunThreadPlan decides whether to stop or continue
+ // based on the stop-on-fork option.
+ ThreadSP thread_sp(m_thread_wp.lock());
+ if (thread_sp) {
+ ProcessSP process_sp = thread_sp->GetProcess();
+ if (process_sp && process_sp->GetModIDRef().IsRunningExpression())
+ return true;
+ }
+ return false;
+ }
StopReason GetStopReason() const override { return eStopReasonFork; }
@@ -1501,16 +1513,6 @@ class StopInfoFork : public StopInfo {
thread_sp->GetProcess()->DidFork(m_child_pid, m_child_tid);
}
- bool ShouldStopSynchronous(Event *event_ptr) override {
- if (!m_performed_action) {
- m_performed_action = true;
- ThreadSP thread_sp(m_thread_wp.lock());
- if (thread_sp)
- thread_sp->GetProcess()->DidFork(m_child_pid, m_child_tid);
- }
- return false;
- }
-
bool m_performed_action = false;
private:
@@ -1528,7 +1530,15 @@ class StopInfoVFork : public StopInfo {
~StopInfoVFork() override = default;
- bool ShouldStop(Event *event_ptr) override { return false; }
+ bool ShouldStop(Event *event_ptr) override {
+ ThreadSP thread_sp(m_thread_wp.lock());
+ if (thread_sp) {
+ ProcessSP process_sp = thread_sp->GetProcess();
+ if (process_sp && process_sp->GetModIDRef().IsRunningExpression())
+ return true;
+ }
+ return false;
+ }
StopReason GetStopReason() const override { return eStopReasonVFork; }
@@ -1551,15 +1561,6 @@ class StopInfoVFork : public StopInfo {
if (thread_sp)
thread_sp->GetProcess()->DidVFork(m_child_pid, m_child_tid);
}
- bool ShouldStopSynchronous(Event *event_ptr) override {
- if (!m_performed_action) {
- m_performed_action = true;
- ThreadSP thread_sp(m_thread_wp.lock());
- if (thread_sp)
- thread_sp->GetProcess()->DidVFork(m_child_pid, m_child_tid);
- }
- return false;
- }
bool m_performed_action = false;
@@ -1576,7 +1577,15 @@ class StopInfoVForkDone : public StopInfo {
~StopInfoVForkDone() override = default;
- bool ShouldStop(Event *event_ptr) override { return false; }
+ bool ShouldStop(Event *event_ptr) override {
+ ThreadSP thread_sp(m_thread_wp.lock());
+ if (thread_sp) {
+ ProcessSP process_sp = thread_sp->GetProcess();
+ if (process_sp && process_sp->GetModIDRef().IsRunningExpression())
+ return true;
+ }
+ return false;
+ }
StopReason GetStopReason() const override { return eStopReasonVForkDone; }
@@ -1592,15 +1601,6 @@ class StopInfoVForkDone : public StopInfo {
if (thread_sp)
thread_sp->GetProcess()->DidVForkDone();
}
- bool ShouldStopSynchronous(Event *event_ptr) override {
- if (!m_performed_action) {
- m_performed_action = true;
- ThreadSP thread_sp(m_thread_wp.lock());
- if (thread_sp)
- thread_sp->GetProcess()->DidVForkDone();
- }
- return false;
- }
bool m_performed_action = false;
};
diff --git a/lldb/source/Target/ThreadPlanCallFunction.cpp b/lldb/source/Target/ThreadPlanCallFunction.cpp
index 218111d4faf60..427693f02722f 100644
--- a/lldb/source/Target/ThreadPlanCallFunction.cpp
+++ b/lldb/source/Target/ThreadPlanCallFunction.cpp
@@ -350,6 +350,16 @@ bool ThreadPlanCallFunction::DoPlanExplainsStop(Event *event_ptr) {
// say we explain the stop but aren't done and everything will continue on
// from there.
+ // Fork events are not handled by this plan — let them fall through
+ // to ThreadPlanBase. DidFork is called via PerformAction when the
+ // event is delivered.
+ if (m_real_stop_info_sp) {
+ StopReason reason = m_real_stop_info_sp->GetStopReason();
+ if (reason == eStopReasonFork || reason == eStopReasonVFork ||
+ reason == eStopReasonVForkDone)
+ return false;
+ }
+
if (m_real_stop_info_sp &&
m_real_stop_info_sp->ShouldStopSynchronous(event_ptr)) {
SetPlanComplete(false);
diff --git a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
index 80ee288ee709a..407db0203336f 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
+++ b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
@@ -2,9 +2,9 @@
Test that expressions that call functions which fork
can be evaluated successfully.
-This tests the ThreadPlanCallFunction handling of fork/vfork/vforkdone
-stop reasons, which should be silently resumed rather than causing the
-expression evaluation to fail.
+Fork events during expression evaluation are handled by RunThreadPlan,
+which silently resumes them by default. The stop-on-fork option on
+EvaluateExpressionOptions can be used to interrupt the expression on fork.
"""
import lldb
@@ -16,6 +16,8 @@
class ExprWithForkTestCase(TestBase):
NO_DEBUG_INFO_TESTCASE = True
+ # --- Basic expression evaluation across fork/vfork ---
+
@skipIfWindows
@add_test_categories(["fork"])
def test_expr_with_fork(self):
@@ -25,9 +27,6 @@ def test_expr_with_fork(self):
self, "// break here", lldb.SBFileSpec("main.cpp")
)
- # Evaluate an expression that calls fork() inside a user function.
- # The fork will generate a fork stop event which ThreadPlanCallFunction
- # must handle transparently for the expression to complete.
self.expect_expr(
"fork_and_return(42, false)", result_type="int", result_value="42"
)
@@ -41,9 +40,275 @@ def test_expr_with_vfork(self):
self, "// break here", lldb.SBFileSpec("main.cpp")
)
- # Evaluate an expression that calls fork() inside a user function.
- # The fork will generate a fork stop event which ThreadPlanCallFunction
- # must handle transparently for the expression to complete.
self.expect_expr(
"fork_and_return(42, true)", result_type="int", result_value="42"
)
+
+ # --- follow-fork-mode child override during expression evaluation ---
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_fork_follow_child(self):
+ """Test that expression evaluation succeeds with follow-fork-mode child."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ original_pid = process.GetProcessID()
+ self.runCmd("settings set target.process.follow-fork-mode child")
+
+ # During expression evaluation, DidFork should override follow-fork-mode
+ # to parent so the expression thread is not lost.
+ self.expect_expr(
+ "fork_and_return(42, false)", result_type="int", result_value="42"
+ )
+
+ # Verify we are still debugging the original process.
+ self.assertEqual(process.GetProcessID(), original_pid)
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_vfork_follow_child(self):
+ """Test that expression evaluation succeeds with vfork and follow-fork-mode child."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ original_pid = process.GetProcessID()
+ self.runCmd("settings set target.process.follow-fork-mode child")
+
+ self.expect_expr(
+ "fork_and_return(42, true)", result_type="int", result_value="42"
+ )
+
+ self.assertEqual(process.GetProcessID(), original_pid)
+
+ # --- stop-on-fork: fork interrupts expression immediately ---
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_fork_stop_on_fork(self):
+ """Test that stop-on-fork interrupts expression evaluation on fork."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(True)
+
+ value = thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, false)", options
+ )
+
+ # The expression should be interrupted due to the fork.
+ self.assertTrue(value.GetError().Fail())
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_fork_stop_on_fork_process_state(self):
+ """Test that process state is valid after stop-on-fork interrupts on fork."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ original_pid = process.GetProcessID()
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(True)
+
+ thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, false)", options
+ )
+
+ # After stop-on-fork interruption, the process should still be
+ # the original and should be in a stopped state.
+ self.assertEqual(process.GetProcessID(), original_pid)
+ self.assertEqual(process.GetState(), lldb.eStateStopped)
+
+ # --- stop-on-fork with vfork: deferred to vforkdone ---
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_vfork_stop_on_fork(self):
+ """Test that stop-on-fork with vfork defers to vforkdone and interrupts."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(True)
+
+ value = thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, true)", options
+ )
+
+ # The expression should be interrupted at vforkdone (deferred from
+ # vfork) because stop-on-fork is set.
+ self.assertTrue(value.GetError().Fail())
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_vfork_stop_on_fork_process_state(self):
+ """Test that process state is clean after vfork stop-on-fork interruption.
+
+ When stop-on-fork interrupts during vfork, the stop is deferred to
+ vforkdone. At that point DidVForkDone has already restored software
+ breakpoints and decremented m_vfork_in_progress_count, so the process
+ should be in a fully functional state."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ original_pid = process.GetProcessID()
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(True)
+
+ thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, true)", options
+ )
+
+ # Process should still be the original and in stopped state.
+ self.assertEqual(process.GetProcessID(), original_pid)
+ self.assertEqual(process.GetState(), lldb.eStateStopped)
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_vfork_stop_on_fork_breakpoints_work(self):
+ """Test that breakpoints are functional after vfork stop-on-fork.
+
+ This is the key regression test for the degraded vfork state bug.
+ After the deferred vfork stop-on-fork interruption, DidVForkDone
+ should have re-enabled software breakpoints. Verify by evaluating
+ another expression that would require functional breakpoints."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(True)
+
+ # First expression: vfork + stop-on-fork → interrupted at vforkdone.
+ value = thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, true)", options
+ )
+ self.assertTrue(value.GetError().Fail())
+
+ # Second expression without stop-on-fork: should complete normally.
+ # This verifies that breakpoints are working (expression evaluation
+ # relies on internal breakpoints for function call returns).
+ self.expect_expr("x", result_type="int", result_value="42")
+
+ # --- stop-on-fork=false (default): no interruption ---
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_fork_stop_on_fork_false(self):
+ """Test that stop-on-fork=false allows fork expression to complete."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(False)
+
+ value = thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, false)", options
+ )
+
+ # With stop-on-fork disabled, the expression should complete normally.
+ self.assertSuccess(value.GetError())
+ self.assertEqual(value.GetValueAsSigned(), 42)
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_vfork_stop_on_fork_false(self):
+ """Test that stop-on-fork=false allows vfork expression to complete."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(False)
+
+ value = thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, true)", options
+ )
+
+ # With stop-on-fork disabled, vfork expression should complete normally.
+ self.assertSuccess(value.GetError())
+ self.assertEqual(value.GetValueAsSigned(), 42)
+
+ # --- SBExpressionOptions stop-on-fork API ---
+
+ def test_stop_on_fork_default(self):
+ """Test that stop-on-fork defaults to false."""
+ options = lldb.SBExpressionOptions()
+ self.assertFalse(options.GetStopOnFork())
+
+ def test_stop_on_fork_set_get(self):
+ """Test that SetStopOnFork/GetStopOnFork round-trip correctly."""
+ options = lldb.SBExpressionOptions()
+
+ options.SetStopOnFork(True)
+ self.assertTrue(options.GetStopOnFork())
+
+ options.SetStopOnFork(False)
+ self.assertFalse(options.GetStopOnFork())
+
+ # --- stop-on-fork with follow-fork-mode child ---
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_fork_stop_on_fork_follow_child(self):
+ """Test stop-on-fork + follow-child: expression interrupted, still on parent."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ original_pid = process.GetProcessID()
+ self.runCmd("settings set target.process.follow-fork-mode child")
+
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(True)
+
+ value = thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, false)", options
+ )
+
+ # Expression should be interrupted by fork, and DidFork should have
+ # overridden follow-fork-mode to parent during expression evaluation.
+ self.assertTrue(value.GetError().Fail())
+ self.assertEqual(process.GetProcessID(), original_pid)
+
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_vfork_stop_on_fork_follow_child(self):
+ """Test stop-on-fork + vfork + follow-child: interrupted at vforkdone, still on parent."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ original_pid = process.GetProcessID()
+ self.runCmd("settings set target.process.follow-fork-mode child")
+
+ options = lldb.SBExpressionOptions()
+ options.SetStopOnFork(True)
+
+ value = thread.GetSelectedFrame().EvaluateExpression(
+ "fork_and_return(42, true)", options
+ )
+
+ # Expression should be interrupted (deferred to vforkdone), and
+ # DidVFork should have overridden follow-fork-mode to parent.
+ self.assertTrue(value.GetError().Fail())
+ self.assertEqual(process.GetProcessID(), original_pid)
>From cd3314eb1ed8a1baa47d7b21426ff3d13968b6a1 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Wed, 15 Apr 2026 08:43:48 -0700
Subject: [PATCH 7/8] [lldb] Retain _start trap in forked child during
expression eval
Skip internal breakpoints (the expression-return trap at _start) when
removing software breakpoints from a forked child during expression
evaluation. This ensures the child dies deterministically with SIGTRAP
if it returns from the forked function, rather than crashing
unpredictably from executing _start with a corrupted stack.
---
.../Process/gdb-remote/ProcessGDBRemote.cpp | 17 ++++++++++++++++-
.../expr-with-fork/TestExprWithFork.py | 13 +++++++++++++
.../expression/expr-with-fork/main.cpp | 19 +++++++++++++++++++
3 files changed, 48 insertions(+), 1 deletion(-)
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index ae791071086ef..f15fe918f6e28 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -6018,10 +6018,25 @@ CommandObject *ProcessGDBRemote::GetPluginCommandObject() {
}
void ProcessGDBRemote::DidForkSwitchSoftwareBreakpoints(bool enable) {
- GetBreakpointSiteList().ForEach([this, enable](BreakpointSite *bp_site) {
+ Log *log = GetLog(GDBRLog::Process);
+ bool is_expr = !enable && GetModIDRef().IsRunningExpression();
+
+ GetBreakpointSiteList().ForEach([this, enable, is_expr,
+ log](BreakpointSite *bp_site) {
if (bp_site->IsEnabled() &&
(bp_site->GetType() == BreakpointSite::eSoftware ||
bp_site->GetType() == BreakpointSite::eExternal)) {
+ // During expression evaluation, retain internal breakpoints (e.g. the
+ // _start return trap) in the forked child so it dies deterministically
+ // on SIGTRAP rather than executing _start with a corrupted stack.
+ if (is_expr && bp_site->IsInternal()) {
+ LLDB_LOG(log,
+ "DidForkSwitchSoftwareBreakpoints: retaining internal "
+ "breakpoint at {0:x} in forked child during expression "
+ "evaluation",
+ bp_site->GetLoadAddress());
+ return;
+ }
m_gdb_comm.SendGDBStoppointTypePacket(
eBreakpointSoftware, enable, bp_site->GetLoadAddress(),
GetSoftwareBreakpointTrapOpcode(bp_site), GetInterruptTimeout());
diff --git a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
index 407db0203336f..0a3520951effa 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
+++ b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
@@ -44,6 +44,19 @@ def test_expr_with_vfork(self):
"fork_and_return(42, true)", result_type="int", result_value="42"
)
+ @skipIfWindows
+ @add_test_categories(["fork"])
+ def test_expr_with_fork_trap(self):
+ """Test that expression evaluation handles a child process that triggers a SIGTRAP."""
+ self.build()
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ self.expect_expr(
+ "fork_and_return_trap(42)", result_type="int", result_value="1"
+ )
+
# --- follow-fork-mode child override during expression evaluation ---
@skipIfWindows
diff --git a/lldb/test/API/commands/expression/expr-with-fork/main.cpp b/lldb/test/API/commands/expression/expr-with-fork/main.cpp
index 4e210df3d7682..8c274cdae6bd8 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/main.cpp
+++ b/lldb/test/API/commands/expression/expr-with-fork/main.cpp
@@ -1,3 +1,4 @@
+#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -15,6 +16,24 @@ int fork_and_return(int value, bool use_vfork) {
return WEXITSTATUS(status);
}
+int fork_and_return_trap(int value) {
+ pid_t pid = fork();
+ if (pid == -1)
+ return -1;
+ if (pid == 0) {
+ // child returning from the JITed function wrapper will hit a trap
+ // instruction and terminate with SIGTRAP.
+ return value;
+ }
+ // parent
+ int status;
+ waitpid(pid, &status, 0);
+ if (WIFSIGNALED(status) && WTERMSIG(status) == SIGTRAP) {
+ return 1; // Success: child terminated with SIGTRAP
+ }
+ return 0; // Failure
+}
+
int main() {
int x = 42;
return 0; // break here
>From 008f47dda647dd48bb8d79cd1925a895f63b2b5b Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Wed, 15 Apr 2026 10:15:26 -0700
Subject: [PATCH 8/8] [lldb] Check if the forking thread is in an expression
---
lldb/include/lldb/Target/Process.h | 6 ++--
lldb/include/lldb/Target/Thread.h | 4 +++
.../Process/gdb-remote/ProcessGDBRemote.cpp | 19 ++++++-----
.../Process/gdb-remote/ProcessGDBRemote.h | 9 ++++--
lldb/source/Target/StopInfo.cpp | 32 +++++++++++++++----
lldb/source/Target/Thread.cpp | 9 ++++++
6 files changed, 59 insertions(+), 20 deletions(-)
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index 307b4e932d396..0a6ca3c4393f2 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -1027,10 +1027,12 @@ class Process : public std::enable_shared_from_this<Process>,
virtual void DoDidExec() {}
/// Called after a reported fork.
- virtual void DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {}
+ virtual void DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
+ bool is_expression_fork = false) {}
/// Called after a reported vfork.
- virtual void DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {}
+ virtual void DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
+ bool is_expression_fork = false) {}
/// Called after reported vfork completion.
virtual void DidVForkDone() {}
diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h
index 4353725ca47f6..88e46c5b19346 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -1027,6 +1027,10 @@ class Thread : public std::enable_shared_from_this<Thread>,
/// A pointer to the next executed plan.
ThreadPlan *GetCurrentPlan() const;
+ /// Returns true if this thread has a ThreadPlanCallFunction on its
+ /// plan stack, indicating it is running a debugger-injected expression.
+ bool IsRunningCallFunctionPlan() const;
+
/// Unwinds the thread stack for the innermost expression plan currently
/// on the thread plan stack.
///
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index f15fe918f6e28..7279674f1a0f6 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -6017,9 +6017,10 @@ CommandObject *ProcessGDBRemote::GetPluginCommandObject() {
return m_command_sp.get();
}
-void ProcessGDBRemote::DidForkSwitchSoftwareBreakpoints(bool enable) {
+void ProcessGDBRemote::DidForkSwitchSoftwareBreakpoints(
+ bool enable, bool is_expression_fork) {
Log *log = GetLog(GDBRLog::Process);
- bool is_expr = !enable && GetModIDRef().IsRunningExpression();
+ bool is_expr = !enable && is_expression_fork;
GetBreakpointSiteList().ForEach([this, enable, is_expr,
log](BreakpointSite *bp_site) {
@@ -6065,14 +6066,15 @@ void ProcessGDBRemote::DidForkSwitchHardwareTraps(bool enable) {
}
}
-void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
+void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
+ bool is_expression_fork) {
Log *log = GetLog(GDBRLog::Process);
// During expression evaluation, force follow-parent. The expression is
// running on the parent's thread and following the child would cause the
// expression thread to vanish (the child has different thread IDs).
FollowForkMode follow_fork_mode = GetFollowForkMode();
- if (follow_fork_mode == eFollowChild && GetModIDRef().IsRunningExpression()) {
+ if (follow_fork_mode == eFollowChild && is_expression_fork) {
LLDB_LOG(log, "ProcessGDBRemote::DidFork() overriding follow-fork-mode "
"to parent during expression evaluation");
follow_fork_mode = eFollowParent;
@@ -6109,7 +6111,7 @@ void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
// Disable all software breakpoints in the forked process.
if (m_gdb_comm.SupportsGDBStoppointPacket(eBreakpointSoftware))
- DidForkSwitchSoftwareBreakpoints(false);
+ DidForkSwitchSoftwareBreakpoints(false, is_expression_fork);
// Remove hardware breakpoints / watchpoints from parent process if we're
// following child.
@@ -6140,7 +6142,8 @@ void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
}
}
-void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
+void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
+ bool is_expression_fork) {
Log *log = GetLog(GDBRLog::Process);
LLDB_LOG(
@@ -6151,7 +6154,7 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
// See comment in DidFork(): force follow-parent during expression evaluation.
FollowForkMode follow_fork_mode = GetFollowForkMode();
- if (follow_fork_mode == eFollowChild && GetModIDRef().IsRunningExpression()) {
+ if (follow_fork_mode == eFollowChild && is_expression_fork) {
LLDB_LOG(log, "ProcessGDBRemote::DidVFork() overriding follow-fork-mode "
"to parent during expression evaluation");
follow_fork_mode = eFollowParent;
@@ -6159,7 +6162,7 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
// Disable all software breakpoints for the duration of vfork.
if (m_gdb_comm.SupportsGDBStoppointPacket(eBreakpointSoftware))
- DidForkSwitchSoftwareBreakpoints(false);
+ DidForkSwitchSoftwareBreakpoints(false, is_expression_fork);
lldb::pid_t detach_pid;
lldb::tid_t detach_tid;
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
index 434c4f29201e5..179a556e3b945 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
@@ -248,8 +248,10 @@ class ProcessGDBRemote : public Process,
std::string HarmonizeThreadIdsForProfileData(
StringExtractorGDBRemote &inputStringExtractor);
- void DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) override;
- void DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) override;
+ void DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
+ bool is_expression_fork = false) override;
+ void DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
+ bool is_expression_fork = false) override;
void DidVForkDone() override;
void DidExec() override;
@@ -496,7 +498,8 @@ class ProcessGDBRemote : public Process,
const ProcessGDBRemote &operator=(const ProcessGDBRemote &) = delete;
// fork helpers
- void DidForkSwitchSoftwareBreakpoints(bool enable);
+ void DidForkSwitchSoftwareBreakpoints(bool enable,
+ bool is_expression_fork = false);
void DidForkSwitchHardwareTraps(bool enable);
void ParseExpeditedRegisters(ExpeditedRegisterMap &expedited_register_map,
diff --git a/lldb/source/Target/StopInfo.cpp b/lldb/source/Target/StopInfo.cpp
index 4370ecdd0fae0..2a7ea2808d87f 100644
--- a/lldb/source/Target/StopInfo.cpp
+++ b/lldb/source/Target/StopInfo.cpp
@@ -1466,6 +1466,7 @@ class StopInfoExec : public StopInfo {
bool m_performed_action = false;
};
+
// StopInfoFork
class StopInfoFork : public StopInfo {
@@ -1481,10 +1482,15 @@ class StopInfoFork : public StopInfo {
// reaches RunThreadPlan as a real stop (not auto-restarted by
// DoOnRemoval). RunThreadPlan decides whether to stop or continue
// based on the stop-on-fork option.
+ //
+ // We check per-thread (not just process-wide IsRunningExpression)
+ // because other threads may fork concurrently after the
+ // try-all-threads timeout releases them.
ThreadSP thread_sp(m_thread_wp.lock());
if (thread_sp) {
ProcessSP process_sp = thread_sp->GetProcess();
- if (process_sp && process_sp->GetModIDRef().IsRunningExpression())
+ if (process_sp && process_sp->GetModIDRef().IsRunningExpression() &&
+ thread_sp->IsRunningCallFunctionPlan())
return true;
}
return false;
@@ -1509,8 +1515,13 @@ class StopInfoFork : public StopInfo {
return;
m_performed_action = true;
ThreadSP thread_sp(m_thread_wp.lock());
- if (thread_sp)
- thread_sp->GetProcess()->DidFork(m_child_pid, m_child_tid);
+ if (thread_sp) {
+ bool is_expression_fork =
+ thread_sp->GetProcess()->GetModIDRef().IsRunningExpression() &&
+ thread_sp->IsRunningCallFunctionPlan();
+ thread_sp->GetProcess()->DidFork(m_child_pid, m_child_tid,
+ is_expression_fork);
+ }
}
bool m_performed_action = false;
@@ -1534,7 +1545,8 @@ class StopInfoVFork : public StopInfo {
ThreadSP thread_sp(m_thread_wp.lock());
if (thread_sp) {
ProcessSP process_sp = thread_sp->GetProcess();
- if (process_sp && process_sp->GetModIDRef().IsRunningExpression())
+ if (process_sp && process_sp->GetModIDRef().IsRunningExpression() &&
+ thread_sp->IsRunningCallFunctionPlan())
return true;
}
return false;
@@ -1558,8 +1570,13 @@ class StopInfoVFork : public StopInfo {
return;
m_performed_action = true;
ThreadSP thread_sp(m_thread_wp.lock());
- if (thread_sp)
- thread_sp->GetProcess()->DidVFork(m_child_pid, m_child_tid);
+ if (thread_sp) {
+ bool is_expression_fork =
+ thread_sp->GetProcess()->GetModIDRef().IsRunningExpression() &&
+ thread_sp->IsRunningCallFunctionPlan();
+ thread_sp->GetProcess()->DidVFork(m_child_pid, m_child_tid,
+ is_expression_fork);
+ }
}
bool m_performed_action = false;
@@ -1581,7 +1598,8 @@ class StopInfoVForkDone : public StopInfo {
ThreadSP thread_sp(m_thread_wp.lock());
if (thread_sp) {
ProcessSP process_sp = thread_sp->GetProcess();
- if (process_sp && process_sp->GetModIDRef().IsRunningExpression())
+ if (process_sp && process_sp->GetModIDRef().IsRunningExpression() &&
+ thread_sp->IsRunningCallFunctionPlan())
return true;
}
return false;
diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index c199fd236f5cd..d44e4ffb77afe 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -1180,6 +1180,15 @@ ThreadPlan *Thread::GetCurrentPlan() const {
return GetPlans().GetCurrentPlan().get();
}
+bool Thread::IsRunningCallFunctionPlan() const {
+ for (ThreadPlan *plan = GetCurrentPlan(); plan;
+ plan = GetPreviousPlan(plan)) {
+ if (plan->GetKind() == ThreadPlan::eKindCallFunction)
+ return true;
+ }
+ return false;
+}
+
ThreadPlanSP Thread::GetCompletedPlan() const {
return GetPlans().GetCompletedPlan();
}
More information about the lldb-commits
mailing list