Repository: pwndbg/pwndbg

Issue:
retaddr command is broken
### Description

the `retaddr` command is broken it prints the entire stack when debugging a kernel

<!--
Briefly describe the problem you are having in a few paragraphs.
-->

### Steps to reproduce

1. debug kernel: qemu-tests 6.6.18
2. run `retaddr` command

<!--
What do we have to do to reproduce the problem?
If this is connected to particular C/asm code, 
please provide the smallest C code that reproduces the issue.
-->

### My setup
```
pwndbg> version
Gdb:      12.1
Python:   3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
Pwndbg:   2024.02.14 build: e7548c7
Capstone: 5.0.1280
Unicorn:  2.0.1
pwndbg> retaddr
0xffffffff83203e98 —▸ 0xffffffff8204b66a ◂— call 0xffffffff8204ab60
0xffffffff83203ea0 —▸ 0xffffffff810f0dc2 ◂— jmp 0xffffffff810f0ea3
0xffffffff83203ea8 ◂— 0x202
0xffffffff83203eb0 ◂— 0xa35fb6fbd66e7200
0xffffffff83203eb8 ◂— 0xed
0xffffffff83203ec0 ◂— 0x0
0xffffffff83203ec8 ◂— 0x0
0xffffffff83203ed0 ◂— 0x0
0xffffffff83203ee0 ◂— 0x0
0xffffffff83203f00 ◂— 0x0
0xffffffff83203f30 ◂— 0x0
```



Discussion:
@chrf01 Shall we just make the command not work on qemu kernel or shall we implement it somehow for it?
The current impl is as follows:

```py
@pwndbg.commands.ArgparsedCommand(
    "Print out the stack addresses that contain return addresses.", category=CommandCategory.STACK
)
@pwndbg.commands.OnlyWhenRunning
def retaddr() -> None:
    sp = pwndbg.gdblib.regs.sp
    stack = pwndbg.gdblib.vmmap.find(sp)

    # Enumerate all return addresses
    frame = gdb.newest_frame()
    addresses = []
    while frame:
        addresses.append(int(frame.pc()))
        frame = frame.older()

    # Find all of them on the stack
    start = stack.vaddr
    stop = start + stack.memsz
    while addresses and start < sp < stop:
        value = pwndbg.gdblib.memory.u(sp)

        if value in addresses:
            index = addresses.index(value)
            del addresses[:index]
            print(pwndbg.chain.format(sp))

        sp += pwndbg.gdblib.arch.ptrsize
```
The last frame reported by GDB is at address 0x0:
```
pwndbg> ipi

In [1]:     sp = pwndbg.gdblib.regs.sp
   ...:     stack = pwndbg.gdblib.vmmap.find(sp)
   ...:
   ...:     # Enumerate all return addresses
   ...:     frame = gdb.newest_frame()
   ...:     addresses = []
   ...:     while frame:
   ...:         addresses.append(int(frame.pc()))
   ...:         frame = frame.older()
   ...:

In [2]: addresses
Out[2]:
[18446743799118392792,
 18446743799118392764,
 18446743799118392824,
 18446743799118446620,
 18446743799101139984,
 18446743799101139984,
 18446743799101140444,
 18446743799100277488,
 0]
```

1) We should manually remove 0x0 from the `addresses` array if it exists
2) I'm confused why `del addresses[:index]` didn't remove it from the list. Printing it at all is wrong, but even then it should have only been printed one time.
> I'm confused why del addresses[:index] didn't remove it from the list. Printing it at all is wrong, but even then it should have only been printed one time.

Oh, `del addresses[:index]` deletes everything before `index`, not including `index`. I'm not sure if this is intentional or not.
> @chrf01 Shall we just make the command not work on qemu kernel or shall we implement it somehow for it?

I don't know; it would be nice to have this feature, but implementing it across all supported kernel archs might not be straightforward. For instance, on aarch64, we need to account for PAC-signed return addresses on the stack. Additionally, we should attempt to read the first byte to verify if it's a valid address and check whether the mapping is marked as executable.

Resolving patch:
diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py
--- a/pwndbg/commands/__init__.py
+++ b/pwndbg/commands/__init__.py
@@ -697,6 +697,7 @@ def load_commands() -> None:
     import pwndbg.commands.procinfo
     import pwndbg.commands.radare2
     import pwndbg.commands.reload
+    import pwndbg.commands.retaddr
     import pwndbg.commands.rizin
     import pwndbg.commands.rop
     import pwndbg.commands.ropper
@@ -706,7 +707,6 @@ def load_commands() -> None:
     import pwndbg.commands.sigreturn
     import pwndbg.commands.slab
     import pwndbg.commands.spray
-    import pwndbg.commands.stack
     import pwndbg.commands.start
     import pwndbg.commands.telescope
     import pwndbg.commands.tips
diff --git a/pwndbg/commands/stack.py b/pwndbg/commands/retaddr.py
similarity index 78%
rename from pwndbg/commands/stack.py
rename to pwndbg/commands/retaddr.py
--- a/pwndbg/commands/stack.py
+++ b/pwndbg/commands/retaddr.py
@@ -1,7 +1,5 @@
 from __future__ import annotations
 
-import gdb
-
 import pwndbg.chain
 import pwndbg.commands
 import pwndbg.gdblib.arch
@@ -15,17 +13,12 @@
 )
 @pwndbg.commands.OnlyWhenRunning
 def retaddr() -> None:
+    addresses = pwndbg.gdblib.stack.callstack()
+
     sp = pwndbg.gdblib.regs.sp
     stack = pwndbg.gdblib.vmmap.find(sp)
 
-    # Enumerate all return addresses
-    frame = gdb.newest_frame()
-    addresses = []
-    while frame:
-        addresses.append(int(frame.pc()))
-        frame = frame.older()
-
-    # Find all of them on the stack
+    # Find all return addresses on the stack
     start = stack.vaddr
     stop = start + stack.memsz
     while addresses and start < sp < stop:
diff --git a/pwndbg/gdblib/stack.py b/pwndbg/gdblib/stack.py
--- a/pwndbg/gdblib/stack.py
+++ b/pwndbg/gdblib/stack.py
@@ -7,6 +7,8 @@
 
 from __future__ import annotations
 
+from typing import List
+
 import gdb
 
 import pwndbg.gdblib.abi
@@ -153,3 +155,18 @@ def _fetch_via_exploration() -> dict[int, pwndbg.lib.memory.Page]:
     curr_thread.switch()
 
     return stacks
+
+
+def callstack() -> List[int]:
+    """
+    Return the address of the return address for the current frame.
+    """
+    frame = gdb.newest_frame()
+    addresses = []
+    while frame:
+        addr = int(frame.pc())
+        if pwndbg.gdblib.memory.is_readable_address(addr):
+            addresses.append(addr)
+        frame = frame.older()
+
+    return addresses


Test patch:
diff --git a/tests/gdb-tests/tests/test_callstack.py b/tests/gdb-tests/tests/test_callstack.py
new file mode 100644
--- /dev/null
+++ b/tests/gdb-tests/tests/test_callstack.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+import gdb
+
+import pwndbg.gdblib.memory
+import pwndbg.gdblib.stack
+import tests
+
+REFERENCE_BINARY = tests.binaries.get("reference-binary.out")
+
+
+def test_callstack_readable(start_binary):
+    start_binary(REFERENCE_BINARY)
+    gdb.execute("b break_here")
+    gdb.execute("r")
+
+    addresses = pwndbg.gdblib.stack.callstack()
+
+    assert len(addresses) > 0
+    assert all(pwndbg.gdblib.memory.is_readable_address(address) for address in addresses)
diff --git a/tests/qemu-tests/tests/system/test_callstack.py b/tests/qemu-tests/tests/system/test_callstack.py
new file mode 100644
--- /dev/null
+++ b/tests/qemu-tests/tests/system/test_callstack.py
@@ -0,0 +1,11 @@
+from __future__ import annotations
+
+import pwndbg.gdblib.memory
+import pwndbg.gdblib.stack
+
+
+def test_callstack_readable():
+    addresses = pwndbg.gdblib.stack.callstack()
+
+    assert len(addresses) > 0
+    assert all(pwndbg.gdblib.memory.is_readable_address(address) for address in addresses)