[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