Repository: pwndbg/pwndbg

Issue:
`nextret` keeps going after the binary segfaults
If the binary segfaults when running `nextret` (and potentially the other `next*` commands), it keeps trying to continue until you kill it:
```
Program received signal SIGSEGV, Segmentation fault.
0x00007f7a8b4f9ae4 in __vfscanf_internal (s=0x7f7a8b6b0aa0 <_IO_2_1_stdin_>, format=0x4020ca "%s", argptr=argptr@entry=0x7ffeb13483a8, mode_flags=mode_flags@entry=2) at ./stdio-c
ommon/vfscanf-internal.c:339
339     in ./stdio-common/vfscanf-internal.c

Program received signal SIGSEGV, Segmentation fault.
0x00007f7a8b4f9ae4 in __vfscanf_internal (s=0x7f7a8b6b0aa0 <_IO_2_1_stdin_>, format=0x4020ca "%s", argptr=argptr@entry=0x7ffeb13483a8, mode_flags=mode_flags@entry=2) at ./stdio-c
ommon/vfscanf-internal.c:339
339     in ./stdio-common/vfscanf-internal.c

Program received signal SIGSEGV, Segmentation fault.
0x00007f7a8b4f9ae4 in __vfscanf_internal (s=0x7f7a8b6b0aa0 <_IO_2_1_stdin_>, format=0x4020ca "%s", argptr=argptr@entry=0x7ffeb13483a8, mode_flags=mode_flags@entry=2) at ./stdio-c
ommon/vfscanf-internal.c:339
339     in ./stdio-common/vfscanf-internal.c
```

We should stop after the first segfault and return to the prompt.


Discussion:
FWIW this is what GEF does to detect thread stop reason like segfaults:
```
 7616         def reason() -> str:
 7617             res = gdb.execute("info program", to_string=True)
 7618             if not res:
 7619                 return "NOT RUNNING"
 7620
 7621             for line in res.splitlines():
 7622                 line = line.strip()
 7623                 if line.startswith("It stopped with signal "):
 7624                     return line.replace("It stopped with signal ", "").split(",", 1)[0]
 7625                 if line == "The program being debugged is not being run.":
 7626                     return "NOT RUNNING"
 7627                 if line == "It stopped at a breakpoint that has since been deleted.":
 7628                     return "TEMPORARY BREAKPOINT"
 7629                 if line.startswith("It stopped at breakpoint "):
 7630                     return "BREAKPOINT"
 7631                 if line == "It stopped after being stepped.":
 7632                     return "SINGLE STEP"
 7633
 7634             return "STOPPED"
```

Tbh this doesn't look good, I mean, we definetely do not want to execute this every step. If we don't find a better way, we could e.g. do this every 50 steps or something...

Resolving patch:
diff --git a/pwndbg/gdblib/next.py b/pwndbg/gdblib/next.py
--- a/pwndbg/gdblib/next.py
+++ b/pwndbg/gdblib/next.py
@@ -91,6 +91,10 @@ def break_next_interrupt(address=None):
 
 def break_next_call(symbol_regex=None):
     while pwndbg.gdblib.proc.alive:
+        # Break on signal as it may be a segfault
+        if pwndbg.gdblib.proc.stopped_with_signal:
+            return
+
         ins = break_next_branch()
 
         if not ins:
@@ -115,6 +119,10 @@ def break_next_call(symbol_regex=None):
 
 def break_next_ret(address=None):
     while pwndbg.gdblib.proc.alive:
+        # Break on signal as it may be a segfault
+        if pwndbg.gdblib.proc.stopped_with_signal:
+            return
+
         ins = break_next_branch(address)
 
         if not ins:
@@ -126,13 +134,14 @@ def break_next_ret(address=None):
 
 def break_on_program_code():
     """
-    Breaks on next instruction that belongs to process' objfile code.
-    :return: True for success, False when process ended or when pc is at the code.
+    Breaks on next instruction that belongs to process' objfile code
+
+    :return: True for success, False when process ended or when pc is not at the code or if a signal occurred
     """
     exe = pwndbg.gdblib.proc.exe
-    binary_exec_page_ranges = [
+    binary_exec_page_ranges = tuple(
         (p.start, p.end) for p in pwndbg.vmmap.get() if p.objfile == exe and p.execute
-    ]
+    )
 
     pc = pwndbg.gdblib.regs.pc
     for start, end in binary_exec_page_ranges:
@@ -140,12 +149,18 @@ def break_on_program_code():
             print(message.error("The pc is already at the binary objfile code. Not stepping."))
             return False
 
-    while pwndbg.gdblib.proc.alive:
-        gdb.execute("si", from_tty=False, to_string=False)
+    proc = pwndbg.gdblib.proc
+    regs = pwndbg.gdblib.regs
+
+    while proc.alive:
+        # Break on signal as it may be a segfault
+        if proc.stopped_with_signal:
+            return False
+
+        o = gdb.execute("si", from_tty=False, to_string=True)
 
-        pc = pwndbg.gdblib.regs.pc
         for start, end in binary_exec_page_ranges:
-            if start <= pc < end:
+            if start <= regs.pc < end:
                 return True
 
     return False
diff --git a/pwndbg/gdblib/proc.py b/pwndbg/gdblib/proc.py
--- a/pwndbg/gdblib/proc.py
+++ b/pwndbg/gdblib/proc.py
@@ -42,6 +42,11 @@ def tid(self):
 
     @property
     def alive(self):
+        """
+        Informs whether the process has a thread. However, note that it will
+        still return True for a segfaulted thread. To detect that, consider
+        using the `stopped_with_signal` method.
+        """
         return gdb.selected_thread() is not None
 
     @property
@@ -56,6 +61,15 @@ def thread_is_stopped(self):
         """
         return gdb.selected_thread().is_stopped()
 
+    @property
+    def stopped_with_signal(self) -> bool:
+        """
+        Returns whether the program has stopped with a signal
+
+        Can be used to detect segfaults (but will also detect other signals)
+        """
+        return "It stopped with signal " in gdb.execute("info program", to_string=True)
+
     @property
     def exe(self):
         """


Test patch:
diff --git a/tests/test_commands_next.py b/tests/test_commands_next.py
--- a/tests/test_commands_next.py
+++ b/tests/test_commands_next.py
@@ -1,9 +1,11 @@
 import gdb
+import pytest
 
 import pwndbg.gdblib.regs
 import tests
 
 REFERENCE_BINARY = tests.binaries.get("reference-binary.out")
+CRASH_SIMPLE_BINARY = tests.binaries.get("crash_simple.out.hardcoded")
 
 
 def test_command_nextproginstr_binary_not_running():
@@ -40,3 +42,19 @@ def test_command_nextproginstr(start_binary):
     # Ensure that nextproginstr won't jump now
     out = gdb.execute("nextproginstr", to_string=True)
     assert out == "The pc is already at the binary objfile code. Not stepping.\n"
+
+
+@pytest.mark.parametrize(
+    "command",
+    ("nextcall", "nextjump", "nextproginstr", "nextret", "nextsyscall", "stepret", "stepsyscall"),
+)
+def test_next_command_doesnt_freeze_crashed_binary(start_binary, command):
+    start_binary(REFERENCE_BINARY)
+
+    # The nextproginstr won't step if we are already on the binary address
+    # and interestingly, other commands won't step if the address can't be disassemblied
+    if command == "nextproginstr":
+        pwndbg.gdblib.regs.pc = 0x1234
+
+    # This should not halt/freeze the program
+    gdb.execute(command, to_string=True)