Repository: pwndbg/pwndbg

Issue:
Flaky threading test for vmmap issue 1565
**EDIT: Turns out this is not only on Arch Linux, this test can fail on Ubuntu 20.04 as well**

Seems we are getting some recursion within `context` display on Arch?
```
test_vmmap_issue_1565                                                  FAILED

pwndbg: loaded 152 pwndbg commands and 40 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $ida GDB functions (can be used with print/break)
Launching pytest with args: ['/pwndbg/tests/gdb-tests/tests/test_command_vmmap.py::test_vmmap_issue_1565', '-vvv', '-s', '--showlocals', '--color=yes']
============================= test session starts ==============================
platform linux -- Python 3.11.6, pytest-7.3.1, pluggy-1.3.0 -- /usr/bin/python
cachedir: .pytest_cache
rootdir: /pwndbg/tests/gdb-tests
plugins: cov-4.1.0
collecting ... collected 1 item

tests/test_command_vmmap.py::test_vmmap_issue_1565 Breakpoint 1 at 0x1185: file issue_1565.c, line 7.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
[New Thread 0x7ffff7dd76c0 (LWP 1983)]
[Thread 0x7ffff7fbd640 (LWP 1968) exited]
[Switching to Thread 0x7ffff7dd76c0 (LWP 1983)]

Thread 2 "issue_[156](https://github.com/pwndbg/pwndbg/actions/runs/7350856612/job/20013254007?pr=1970#step:5:157)5.out" hit Breakpoint 1, thread_function (arg=0x0)
    at issue_1565.c:7
7	  free(malloc(0x20));
8	  sleep(100);
FAILED

=================================== FAILURES ===================================
____________________________ test_vmmap_issue_1565 _____________________________

start_binary = <function start_binary.<locals>._start_binary at 0x7fadc0fc3b00>

    def test_vmmap_issue_1565(start_binary):
        """
        https://github.com/pwndbg/pwndbg/issues/1565
    
        In tests this bug is reported as:
        >       gdb.execute("context")
        E       gdb.error: Error occurred in Python: maximum recursion depth exceeded in comparison
    
        In a normal GDB session this is reported as:
            Exception occurred: context: maximum recursion depth exceeded while calling a Python object (<class 'RecursionError'>)
        """
        gdb.execute(f"file {BINARY_ISSUE_1565}")
        gdb.execute("break thread_function")
        gdb.execute("run")
        gdb.execute("next")
>       gdb.execute("context")
E       gdb.error: Error occurred in Python: maximum recursion depth exceeded

start_binary = <function start_binary.<locals>._start_binary at 0x7fadc0fc3b00>

tests/test_command_vmmap.py:[184](https://github.com/pwndbg/pwndbg/actions/runs/7350856612/job/20013254007?pr=1970#step:5:185): error
=========================== short test summary info ============================
FAILED tests/test_command_vmmap.py::test_vmmap_issue_1565 - gdb.error: Error occurred in Python: maximum recursion depth exceeded
============================== 1 failed in 0.30s ===============================
--------------------------------------------------------------------------------
If you want to debug tests locally, run ./tests.sh with the --pdb flag
```


Discussion:
Reproduce on Arch Linux with:
```
cd ./tests/gdb-tests/tests/binaries && make issue_1565 && cd -
```

And then:
```
gdb
file ./tests/gdb-tests/tests/binaries/issue_1565.out
break thread_function
run
```
Part of repeating stacktrace:
```
Traceback (most recent call last):
  File "/pwndbg/pwndbg/gdblib/events.py", line 184, in after_reload
    f()
  File "/pwndbg/pwndbg/gdblib/events.py", line 119, in caller
    raise e
  File "/pwndbg/pwndbg/gdblib/events.py", line 114, in caller
    func()
  File "/pwndbg/pwndbg/lib/cache.py", line 138, in decorator
    value = func(*a, **kw)
            ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/stack.py", line 71, in is_executable
    ehdr = pwndbg.gdblib.elf.exe()
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/proc.py", line 149, in wrapper
    return func(*a, **kw)
           ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/lib/cache.py", line 138, in decorator
    value = func(*a, **kw)
            ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/elf.py", line 218, in exe
    return load(e)
           ^^^^^^^
  File "/pwndbg/pwndbg/gdblib/elf.py", line 256, in load
    return get_ehdr(pointer)[1]
           ^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/elf.py", line 286, in get_ehdr
    vmmap = pwndbg.gdblib.vmmap.find(pointer)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/lib/cache.py", line 138, in decorator
    value = func(*a, **kw)
            ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/vmmap.py", line 169, in find
    return explore(address)
           ^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/abi.py", line 57, in caller
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/vmmap.py", line 198, in explore
    flags |= 1 if not pwndbg.gdblib.stack.is_executable() else 0
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/lib/cache.py", line 138, in decorator
    value = func(*a, **kw)
            ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/stack.py", line 71, in is_executable
    ehdr = pwndbg.gdblib.elf.exe()
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/proc.py", line 149, in wrapper
    return func(*a, **kw)
           ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/lib/cache.py", line 138, in decorator
    value = func(*a, **kw)
            ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/elf.py", line 218, in exe
    return load(e)
           ^^^^^^^
  File "/pwndbg/pwndbg/gdblib/elf.py", line 256, in load
    return get_ehdr(pointer)[1]
           ^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/elf.py", line 286, in get_ehdr
    vmmap = pwndbg.gdblib.vmmap.find(pointer)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/lib/cache.py", line 138, in decorator
    value = func(*a, **kw)
            ^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/vmmap.py", line 169, in find
    return explore(address)
           ^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/abi.py", line 57, in caller
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/pwndbg/pwndbg/gdblib/vmmap.py", line 198, in explore
    flags |= 1 if not pwndbg.gdblib.stack.is_executable() else 0
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```
What the f!@#?
<img width="645" alt="image" src="https://github.com/pwndbg/pwndbg/assets/10009354/c2bc049e-6bfb-457f-8813-8d7607b4f352">

<img width="920" alt="image" src="https://github.com/pwndbg/pwndbg/assets/10009354/d9c741fd-c29d-4f10-832a-689b7b7bcc43">

It turns out this is not only on Arch Linux, this test can fail on Ubuntu 20.04 as well as some CI builds show up this.

Changing the title of this issue.

Resolving patch:
diff --git a/pwndbg/gdblib/vmmap.py b/pwndbg/gdblib/vmmap.py
--- a/pwndbg/gdblib/vmmap.py
+++ b/pwndbg/gdblib/vmmap.py
@@ -93,18 +93,19 @@ def get() -> tuple[pwndbg.lib.memory.Page, ...]:
 
     proc_maps = None
     if pwndbg.gdblib.qemu.is_qemu_usermode():
-        # On Qemu < 8.1 info proc maps are not supported. In that case we callback on proc_pid_maps
+        # On Qemu < 8.1 info proc maps are not supported. In that case we callback on proc_tid_maps
         proc_maps = info_proc_maps()
 
     if not proc_maps:
-        proc_maps = proc_pid_maps()
+        proc_maps = proc_tid_maps()
 
     # The `proc_maps` is usually a tuple of Page objects but it can also be:
-    #   None    - when /proc/$pid/maps does not exist/is not available
+    #   None    - when /proc/$tid/maps does not exist/is not available
     #   tuple() - when the process has no maps yet which happens only during its very early init
     #             (usually when we attach to a process)
     if proc_maps is not None:
         return proc_maps
+
     pages = []
     if pwndbg.gdblib.qemu.is_qemu_kernel() and pwndbg.gdblib.arch.current in (
         "i386",
@@ -184,7 +185,7 @@ def explore(address_maybe: int) -> Any | None:
 
         Also assumes the entire contiguous section has the same permission.
     """
-    if proc_pid_maps():
+    if proc_tid_maps():
         return None
 
     address_maybe = pwndbg.lib.memory.page_align(address_maybe)
@@ -349,6 +350,10 @@ def info_proc_maps():
     """
     Parse the result of info proc mappings.
 
+    Note: this may return no pages due to a bug/behavior of GDB.
+    See https://sourceware.org/bugzilla/show_bug.cgi?id=31207
+    for more information.
+
     Returns:
         A tuple of pwndbg.lib.memory.Page objects or None if
         info proc mapping is not supported on the target.
@@ -393,13 +398,14 @@ def info_proc_maps():
 
 
 @pwndbg.lib.cache.cache_until("start", "stop")
-def proc_pid_maps():
+def proc_tid_maps():
     """
-    Parse the contents of /proc/$PID/maps on the server.
+    Parse the contents of /proc/$TID/maps on the server.
+    (TID == Thread Identifier. We do not use PID since it may not be correct)
 
     Returns:
         A tuple of pwndbg.lib.memory.Page objects or None if
-        /proc/$pid/maps doesn't exist or when we debug a qemu-user target
+        /proc/$tid/maps doesn't exist or when we debug a qemu-user target
     """
 
     # If we debug remotely a qemu-user or qemu-system target,
@@ -407,7 +413,7 @@ def proc_pid_maps():
     if pwndbg.gdblib.qemu.is_qemu():
         return None
 
-    # Example /proc/$pid/maps
+    # Example /proc/$tid/maps
     # 7f95266fa000-7f95268b5000 r-xp 00000000 08:01 418404                     /lib/x86_64-linux-gnu/libc-2.19.so
     # 7f95268b5000-7f9526ab5000 ---p 001bb000 08:01 418404                     /lib/x86_64-linux-gnu/libc-2.19.so
     # 7f9526ab5000-7f9526ab9000 r--p 001bb000 08:01 418404                     /lib/x86_64-linux-gnu/libc-2.19.so
@@ -428,11 +434,11 @@ def proc_pid_maps():
     # 7fff3c1e8000-7fff3c1ea000 r-xp 00000000 00:00 0                          [vdso]
     # ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
 
-    pid = pwndbg.gdblib.proc.pid
+    tid = pwndbg.gdblib.proc.tid
     locations = [
-        f"/proc/{pid}/maps",
-        f"/proc/{pid}/map",
-        f"/usr/compat/linux/proc/{pid}/maps",
+        f"/proc/{tid}/maps",
+        f"/proc/{tid}/map",
+        f"/usr/compat/linux/proc/{tid}/maps",
     ]
 
     for location in locations:
@@ -785,7 +791,7 @@ def check_aslr():
     # Check the personality of the process
     if pwndbg.gdblib.proc.alive:
         try:
-            data = pwndbg.gdblib.file.get("/proc/%i/personality" % pwndbg.gdblib.proc.pid)
+            data = pwndbg.gdblib.file.get("/proc/%i/personality" % pwndbg.gdblib.proc.tid)
             personality = int(data, 16)
             return (personality & 0x40000 == 0), "read status from process' personality"
         except Exception:


Test patch: