blob: d279295a5acac2cd845967913e1942e6bbf463f3 [file]
"""
Test that expressions that call functions which fork
can be evaluated successfully.
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
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
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):
"""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")
)
self.expect_expr(
"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")
)
self.expect_expr(
"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
@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)
# --- 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 overridden during expression evaluation.
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")
)
original_pid = process.GetProcessID()
self.runCmd("settings set target.process.follow-fork-mode child")
options = lldb.SBExpressionOptions()
options.SetStopOthers(False)
# Redirect debugger output to capture the async warning message
# that DidFork emits when overriding follow-fork-mode.
output_path = self.getBuildArtifact("debugger_output.txt")
outfile = open(output_path, "w")
self.dbg.SetOutputFileHandle(outfile, False)
value = thread.GetSelectedFrame().EvaluateExpression(
"expr_with_concurrent_fork()", options
)
self.dbg.GetOutputFile().Flush()
outfile.close()
self.dbg.SetOutputFileHandle(None, False)
with open(output_path, "r") as f:
debugger_output = f.read()
# 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")
# 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")
# Verify the user-facing warning about the follow-fork-mode override.
self.assertIn(
"follow-fork-mode",
debugger_output,
"expected override warning in debugger output",
)
self.assertIn(
str(child_pid), debugger_output, "expected child PID in debugger output"
)
self.assertTrue(
"stopped" in debugger_output or "running" in debugger_output,
"expected child stop status in debugger output",
)