[PATCH 4/9] scripts/qemugdb: coroutine: Don't unwind past the outermost frame

Andrey Drobyshev <[email protected]> Tue, 4 Aug 2026 18:17:38 +0300
Newsgroups gmane.comp.emulators.qemu
Message-ID <[email protected]>
dump_backtrace() walks the frame pointer chain until rbp is NULL.
However, when glibc is built with no frame pointers, rbp is never NULL
during unwind.  Instead, we see garbage in the outermost frame.  This
results into:

  #17  0x5610b6e9f2d9 in main<+1720> () at ../qemu-io.c:674
  #18  0x7f26cc10230e in __libc_start_call_main<+125> () at
      ../sysdeps/nptl/libc_start_call_main.h:58
  Traceback (most recent call last):
    File "/.../scripts/qemugdb/coroutine.py", line 409, in invoke
      bt_jmpbuf(coroutine_to_jmpbuf(co_ptr), is_coredump,
    File "/.../scripts/qemugdb/coroutine.py", line 296, in bt_jmpbuf
      dump_backtrace(regs)
    File "/.../scripts/qemugdb/coroutine.py", line 247, in dump_backtrace
      while rbp:
            ^^^
  gdb.MemoryError: Cannot access memory at address 0x4
  Error occurred in Python: Cannot access memory at address 0x4

As a fix, stop on the first link that can't be read.  Since
gdb.parse_and_eval() returns a lazy value, force the actual memory read
by wrapping it in int() and immediately catching potential gdb.MemoryError.

Fixes: 772f86839f77 ("scripts/qemu-gdb: Support coroutine dumps in coredumps")
Signed-off-by: Andrey Drobyshev <[email protected]>
---
 scripts/qemugdb/coroutine.py | 25 +++++++++++++++++++++----
 1 file changed, 21 insertions(+), 4 deletions(-)

diff --git a/scripts/qemugdb/coroutine.py b/scripts/qemugdb/coroutine.py
index 649579378db..da395a1a13e 100644
--- a/scripts/qemugdb/coroutine.py
+++ b/scripts/qemugdb/coroutine.py
@@ -235,13 +235,24 @@ def dump_backtrace_patched(regs):
     out = run_with_pty(cmd).split('----split----')[1]
     gdb.write(out)
 
+def read_word(addr):
+    '''
+    Read a 64-bit word, None if that memory isn't accessible.
+    '''
+    try:
+        # gdb.parse_and_eval() returns lazy values.  Force memory access
+        # by wrapping in int()
+        return int(gdb.parse_and_eval(f"*(uint64_t *){hex(addr)}"))
+    except gdb.MemoryError:
+        return None
+
 def dump_backtrace(regs):
     '''
     Backtrace dump with raw registers, mimic GDB command 'bt'.
     '''
     # Here only rbp and rip that matter..
-    rbp = regs['rbp']
-    rip = regs['rip']
+    rbp = int(regs['rbp'])
+    rip = int(regs['rip'])
     i = 0
 
     while rbp:
@@ -250,8 +261,14 @@ def dump_backtrace(regs):
         # instruction instead of the CALL.  Here -1 would work for any
         # sized CALL instruction.
         print(f"#{i}  {hex(rip)} in {symbol_lookup(rip if i == 0 else rip-1)}")
-        rip = gdb.parse_and_eval(f"*(uint64_t *)(uint64_t)({hex(rbp)} + 8)")
-        rbp = gdb.parse_and_eval(f"*(uint64_t *)(uint64_t)({hex(rbp)})")
+
+        # The 'rbp != NULL' condition is insufficient: the outermost glibc
+        # frames might leave garbage in rbp if built without frame pointers.
+        # Break the loop on the frame that leads nowhere.
+        rip, rbp = read_word(rbp + 8), read_word(rbp)
+        if rip is None or rbp is None:
+            break
+
         i += 1
 
 def dump_backtrace_live(regs):
-- 
2.47.1