[Lldb-commits] [lldb] [lldb] Allow forks to occur in expression evaluation (PR #184815)

Philip DePetro via lldb-commits lldb-commits at lists.llvm.org
Fri Apr 17 10:15:06 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 01/11] [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 02/11] 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 03/11] 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 04/11] 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 05/11] [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 06/11] [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 07/11] [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 08/11] [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();
 }

>From ad6cbed64a8f018ac66d7c582043d0dd3656a96e Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Wed, 15 Apr 2026 11:56:50 -0700
Subject: [PATCH 09/11] [lldb] Only retain the _start return trap in child
 processes that were forked from expressions

---
 .../Process/gdb-remote/ProcessGDBRemote.cpp   | 25 ++++++++++++-------
 1 file changed, 16 insertions(+), 9 deletions(-)

diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index 671fa4972fd5d..6b9c31a85b01b 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -6048,21 +6048,28 @@ CommandObject *ProcessGDBRemote::GetPluginCommandObject() {
 void ProcessGDBRemote::DidForkSwitchSoftwareBreakpoints(
     bool enable, bool is_expression_fork) {
   Log *log = GetLog(GDBRLog::Process);
-  bool is_expr = !enable && is_expression_fork;
 
-  GetBreakpointSiteList().ForEach([this, enable, is_expr,
+  // Resolve the expression-return sentinel address (_start) once. This is
+  // the same address ThreadPlanCallFunction uses as the return trap.
+  lldb::addr_t entry_addr = LLDB_INVALID_ADDRESS;
+  if (!enable && is_expression_fork) {
+    if (auto entry = GetTarget().GetEntryPointAddress())
+      entry_addr = entry->GetLoadAddress(&GetTarget());
+  }
+
+  GetBreakpointSiteList().ForEach([this, enable, entry_addr,
                                    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()) {
+      // During expression evaluation, retain the expression-return trap
+      // at _start in the forked child so it dies deterministically on
+      // SIGTRAP rather than executing _start with a corrupted stack.
+      if (entry_addr != LLDB_INVALID_ADDRESS &&
+          bp_site->GetLoadAddress() == entry_addr) {
         LLDB_LOG(log,
-                 "DidForkSwitchSoftwareBreakpoints: retaining internal "
-                 "breakpoint at {0:x} in forked child during expression "
-                 "evaluation",
+                 "DidForkSwitchSoftwareBreakpoints: retaining expression-"
+                 "return trap at {0:x} in forked child",
                  bp_site->GetLoadAddress());
         return;
       }

>From dfb9c8c41d2625452572c39d29681f2a09c9d7d9 Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Wed, 15 Apr 2026 14:30:31 -0700
Subject: [PATCH 10/11] [lldb] Add tests for other threads forking during
 expression evaluation

---
 .../expression/expr-with-fork/Makefile        |  1 +
 .../expr-with-fork/TestExprWithFork.py        | 75 +++++++++++++++++++
 .../expression/expr-with-fork/main.cpp        | 42 +++++++++++
 3 files changed, 118 insertions(+)

diff --git a/lldb/test/API/commands/expression/expr-with-fork/Makefile b/lldb/test/API/commands/expression/expr-with-fork/Makefile
index f016d5b15d839..59fef83eb32f9 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/Makefile
+++ b/lldb/test/API/commands/expression/expr-with-fork/Makefile
@@ -1,4 +1,5 @@
 CXX_SOURCES := main.cpp
 USE_SYSTEM_STDLIB := 1
+ENABLE_THREADS := YES
 
 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
index 0a3520951effa..53926280392de 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
+++ b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
@@ -325,3 +325,78 @@ def test_expr_with_vfork_stop_on_fork_follow_child(self):
         # DidVFork should have overridden follow-fork-mode to parent.
         self.assertTrue(value.GetError().Fail())
         self.assertEqual(process.GetProcessID(), original_pid)
+
+    # --- concurrent fork on another thread during expression ---
+
+    @skipIfWindows
+    @add_test_categories(["fork"])
+    def test_expr_concurrent_fork_other_thread(self):
+        """Test that a fork on another thread during expression is handled correctly.
+
+        The expression function releases a mutex, letting a helper thread
+        fork, then waits for it to complete.  With SetStopOthers(false),
+        the helper thread runs and forks while the expression is active.
+        The fork must be handled as a normal (non-expression) fork so
+        follow-fork-mode is respected."""
+        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.SetStopOthers(False)
+
+        value = thread.GetSelectedFrame().EvaluateExpression(
+            "expr_with_concurrent_fork()", options
+        )
+
+        # Expression should complete successfully.
+        self.assertSuccess(value.GetError())
+
+        # The return value is the child PID — must be > 0 (fork succeeded).
+        child_pid = value.GetValueAsSigned()
+        self.assertGreater(child_pid, 0, "fork() should have succeeded")
+
+        # We should still be debugging the original parent process.
+        self.assertEqual(process.GetProcessID(), original_pid)
+        self.assertEqual(process.GetState(), lldb.eStateStopped)
+
+        # Breakpoints should still be functional.
+        self.expect_expr("x", result_type="int", result_value="42")
+
+    @skipIfWindows
+    @add_test_categories(["fork"])
+    def test_expr_concurrent_fork_other_thread_follow_child(self):
+        """Test that follow-fork-mode child is respected for a non-expression fork.
+
+        When follow-fork-mode is child and a non-expression thread forks,
+        DidFork follows the child (switching the process via SetID).  This
+        causes the expression thread to vanish, so the expression is
+        interrupted.  The per-thread check ensures we do NOT override
+        follow-fork-mode to parent for non-expression forks."""
+        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.SetStopOthers(False)
+
+        value = thread.GetSelectedFrame().EvaluateExpression(
+            "expr_with_concurrent_fork()", options
+        )
+
+        # The expression should be interrupted because follow-fork-mode
+        # child switches to the child process, causing the expression
+        # thread (on the parent) to vanish.  This confirms that the
+        # per-thread check correctly did NOT override follow-fork-mode
+        # for the non-expression fork.
+        self.assertTrue(value.GetError().Fail())
+
+        # The process should have switched to the child (different PID).
+        self.assertNotEqual(process.GetProcessID(), original_pid)
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 8c274cdae6bd8..3867b690d6dfb 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 <pthread.h>
 #include <signal.h>
 #include <sys/wait.h>
 #include <unistd.h>
@@ -34,7 +35,48 @@ int fork_and_return_trap(int value) {
   return 0; // Failure
 }
 
+// Mutex-based synchronization for concurrent fork test.
+// The main thread holds the mutex, starts a helper thread that waits
+// on the mutex then forks, and the expression releases the mutex and
+// waits to reacquire it - forcing the fork to happen on a different
+// thread while the expression is running.
+static pthread_mutex_t g_fork_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pid_t g_child_pid = -1;
+
+static void *concurrent_fork_thread(void *arg) {
+  // Wait until the expression releases the mutex.
+  pthread_mutex_lock(&g_fork_mutex);
+  g_child_pid = fork();
+  if (g_child_pid == 0) {
+    // child
+    _exit(42);
+  }
+  // parent - release mutex so expression can reacquire.
+  pthread_mutex_unlock(&g_fork_mutex);
+  return nullptr;
+}
+
+// Called as an expression while another thread forks.
+// The caller must hold g_fork_mutex before evaluating this expression.
+int expr_with_concurrent_fork() {
+  pthread_t t;
+  pthread_create(&t, nullptr, concurrent_fork_thread, nullptr);
+
+  // Release mutex - lets the helper thread proceed to fork.
+  pthread_mutex_unlock(&g_fork_mutex);
+
+  // Wait for the helper thread to finish (it forks and unlocks).
+  pthread_join(t, nullptr);
+
+  // Reacquire mutex to synchronize.
+  pthread_mutex_lock(&g_fork_mutex);
+
+  // Return the child PID so the test can verify fork happened.
+  return (int)g_child_pid;
+}
+
 int main() {
+  pthread_mutex_lock(&g_fork_mutex);
   int x = 42;
   return 0; // break here
 }

>From 1d2bf970b4a3a2899afbc683479d53713bcedc7e Mon Sep 17 00:00:00 2001
From: Philip DePetro <pdepetro at meta.com>
Date: Fri, 17 Apr 2026 09:54:16 -0700
Subject: [PATCH 11/11] [lldb] Prevent fork follow child mode anytime an
 expression is running

---
 .../Process/gdb-remote/ProcessGDBRemote.cpp   | 61 +++++++++++++++----
 .../expr-with-fork/TestExprWithFork.py        | 33 +++++-----
 2 files changed, 68 insertions(+), 26 deletions(-)

diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index 6b9c31a85b01b..4dbe3d7cdcf86 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -6105,14 +6105,26 @@ 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).
+  // During expression evaluation, force follow-parent regardless of which
+  // thread forked. The expression is running on the parent and following the
+  // child would cause the expression thread to vanish (the child has different
+  // thread IDs). Even if a *different* thread forks, switching to the child
+  // would destroy the expression thread's process context.
   FollowForkMode follow_fork_mode = GetFollowForkMode();
-  if (follow_fork_mode == eFollowChild && is_expression_fork) {
-    LLDB_LOG(log, "ProcessGDBRemote::DidFork() overriding follow-fork-mode "
-                  "to parent during expression evaluation");
+  bool overrode_follow_mode = false;
+  if (follow_fork_mode == eFollowChild &&
+      GetModIDRef().IsRunningExpression()) {
+    if (is_expression_fork) {
+      LLDB_LOG(log, "ProcessGDBRemote::DidFork() overriding follow-fork-mode "
+                    "to parent during expression evaluation");
+    } else {
+      LLDB_LOG(log, "ProcessGDBRemote::DidFork() overriding follow-fork-mode "
+                    "to parent during expression evaluation. Child process "
+                    "{0} is available for manual attachment.",
+               child_pid);
+    }
     follow_fork_mode = eFollowParent;
+    overrode_follow_mode = true;
   }
 
   lldb::pid_t parent_pid = m_gdb_comm.GetCurrentProcessID();
@@ -6161,7 +6173,15 @@ void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
   }
 
   LLDB_LOG(log, "Detaching process {0}", detach_pid);
-  Status error = m_gdb_comm.Detach(false, detach_pid);
+  // When we overrode follow-child because of a concurrent expression, try to
+  // keep the child stopped so the user can attach to it manually.
+  bool keep_stopped = overrode_follow_mode && !is_expression_fork;
+  Status error = m_gdb_comm.Detach(keep_stopped, detach_pid);
+  if (error.Fail() && keep_stopped) {
+    LLDB_LOG(log, "ProcessGDBRemote::DidFork() detach-and-stay-stopped not "
+                  "supported, falling back to normal detach");
+    error = m_gdb_comm.Detach(false, detach_pid);
+  }
   if (error.Fail()) {
     LLDB_LOG(log, "ProcessGDBRemote::DidFork() detach packet send failed: {0}",
              error.AsCString() ? error.AsCString() : "<unknown error>");
@@ -6187,12 +6207,23 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
       child_pid, child_tid);
   ++m_vfork_in_progress_count;
 
-  // See comment in DidFork(): force follow-parent during expression evaluation.
+  // See comment in DidFork(): force follow-parent during expression evaluation
+  // regardless of which thread triggered the vfork.
   FollowForkMode follow_fork_mode = GetFollowForkMode();
-  if (follow_fork_mode == eFollowChild && is_expression_fork) {
-    LLDB_LOG(log, "ProcessGDBRemote::DidVFork() overriding follow-fork-mode "
-                  "to parent during expression evaluation");
+  bool overrode_follow_mode = false;
+  if (follow_fork_mode == eFollowChild &&
+      GetModIDRef().IsRunningExpression()) {
+    if (is_expression_fork) {
+      LLDB_LOG(log, "ProcessGDBRemote::DidVFork() overriding follow-fork-mode "
+                    "to parent during expression evaluation");
+    } else {
+      LLDB_LOG(log, "ProcessGDBRemote::DidVFork() overriding follow-fork-mode "
+                    "to parent during expression evaluation. Child process "
+                    "{0} is available for manual attachment.",
+               child_pid);
+    }
     follow_fork_mode = eFollowParent;
+    overrode_follow_mode = true;
   }
 
   // Disable all software breakpoints for the duration of vfork.
@@ -6232,7 +6263,13 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid,
   }
 
   LLDB_LOG(log, "Detaching process {0}", detach_pid);
-  Status error = m_gdb_comm.Detach(false, detach_pid);
+  bool keep_stopped = overrode_follow_mode && !is_expression_fork;
+  Status error = m_gdb_comm.Detach(keep_stopped, detach_pid);
+  if (error.Fail() && keep_stopped) {
+    LLDB_LOG(log, "ProcessGDBRemote::DidVFork() detach-and-stay-stopped not "
+                  "supported, falling back to normal detach");
+    error = m_gdb_comm.Detach(false, detach_pid);
+  }
   if (error.Fail()) {
       LLDB_LOG(log,
                "ProcessGDBRemote::DidVFork() detach packet send failed: {0}",
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 53926280392de..9ebd0b242ae2c 100644
--- a/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
+++ b/lldb/test/API/commands/expression/expr-with-fork/TestExprWithFork.py
@@ -369,13 +369,13 @@ def test_expr_concurrent_fork_other_thread(self):
     @skipIfWindows
     @add_test_categories(["fork"])
     def test_expr_concurrent_fork_other_thread_follow_child(self):
-        """Test that follow-fork-mode child is respected for a non-expression fork.
+        """Test that follow-fork-mode child is overridden during expression evaluation.
 
-        When follow-fork-mode is child and a non-expression thread forks,
-        DidFork follows the child (switching the process via SetID).  This
-        causes the expression thread to vanish, so the expression is
-        interrupted.  The per-thread check ensures we do NOT override
-        follow-fork-mode to parent for non-expression forks."""
+        When follow-fork-mode is child and a non-expression thread forks while
+        an expression is running, DidFork detects the process-wide expression
+        state and overrides follow-fork-mode to parent, protecting the
+        expression thread.  The expression should complete successfully and
+        the process should remain the original parent."""
         self.build()
         (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
             self, "// break here", lldb.SBFileSpec("main.cpp")
@@ -391,12 +391,17 @@ def test_expr_concurrent_fork_other_thread_follow_child(self):
             "expr_with_concurrent_fork()", options
         )
 
-        # The expression should be interrupted because follow-fork-mode
-        # child switches to the child process, causing the expression
-        # thread (on the parent) to vanish.  This confirms that the
-        # per-thread check correctly did NOT override follow-fork-mode
-        # for the non-expression fork.
-        self.assertTrue(value.GetError().Fail())
+        # Expression should complete successfully because DidFork overrides
+        # follow-fork-mode to parent when any expression is running.
+        self.assertSuccess(value.GetError())
+
+        # The return value is the child PID — must be > 0 (fork succeeded).
+        child_pid = value.GetValueAsSigned()
+        self.assertGreater(child_pid, 0, "fork() should have succeeded")
 
-        # The process should have switched to the child (different PID).
-        self.assertNotEqual(process.GetProcessID(), original_pid)
+        # We should still be debugging the original parent process.
+        self.assertEqual(process.GetProcessID(), original_pid)
+        self.assertEqual(process.GetState(), lldb.eStateStopped)
+
+        # Breakpoints should still be functional.
+        self.expect_expr("x", result_type="int", result_value="42")



More information about the lldb-commits mailing list