[Bug backtrace/34476] New: MIPS o32: backtrace broken through syscall stubs that adjust SP after the prologue

orgads at gmail dot com via Gdb-prs <[email protected]> Mon, 03 Aug 2026 17:50:16 +0000
Newsgroups gmane.comp.gdb.bugs.discuss
Message-ID <[email protected]/bugzilla/>
https://sourceware.org/bugzilla/show_bug.cgi?id=3D34476

            Bug ID: 34476
           Summary: MIPS o32: backtrace broken through syscall stubs that
                    adjust SP after the prologue
           Product: gdb
           Version: HEAD
            Status: UNCONFIRMED
          Severity: normal
          Priority: P2
         Component: backtrace
          Assignee: unassigned at sourceware dot org
          Reporter: orgads at gmail dot com
  Target Milestone: ---

Created attachment 16900
  --> https://sourceware.org/bugzilla/attachment.cgi?id=3D16900&action=3Ded=
it
Reproducer: call chain main -> f1 -> f2 -> stub

On MIPS o32, syscalls taking five or more arguments pass the extra arguments
on the stack. Libc syscall stubs therefore temporarily lower SP around the
actual `syscall` instruction, *after* the function prologue. uClibc's
`__syscall_ipc` (compiled C) is representative:

```
addiu   sp,sp,-8        # prologue
sw      s0,4(sp)
lw      v0,24(sp)       # two "non-prologue" instructions
lw      s0,28(sp)
addiu   sp,sp,-32       # temp area for the syscall's stack arguments
...
syscall                 # <- a blocked thread's pc is here
addiu   sp,sp,32
```

uClibc's hand-written `syscall.S` and musl's stack-argument syscall wrappers
have the same shape. None of them carry CFI, so gdb unwinds them with the
heuristic prologue analyzer, `mips32_scan_prologue` (gdb/mips-tdep.c). That
function permits at most one non-prologue instruction:

```c
      /* A jump or branch, or enough non-prologue insns seen?  If so,
         then we must have reached the end of the prologue by now.  */
      if (prev_delay_slot || non_prologue_insns > 1)
        break;
```

so for a thread whose pc is at/after the `syscall` instruction, the scan
stops at the two `lw`s and never sees the `addiu sp,sp,-32`. The computed
frame base is 32 bytes too low, the caller's saved-ra slot is read from the
wrong address (it lands in the outgoing-argument area), and the backtrace
degenerates into a garbage frame right after the libc stub.

This makes core dumps of blocked multi-threaded programs on MIPS o32
routers/embedded systems (uClibc and musl userlands, e.g. OpenWrt) largely
undebuggable: every thread blocked in `msgrcv`, `ppoll`, `futex` via
`syscall()`, etc. loses its call chain after frame #1.

## Reproducer (stock Debian packages)

`apt install gcc-mips-linux-gnu libc6-dev-mips-cross qemu-user gdb-multiarc=
h`

stub.S, main.c: see attachments; `stub` reproduces the uClibc stub shape.

```
$ mips-linux-gnu-gcc -O2 -g -static -o repro main.c stub.S
$ qemu-mips -g 1234 ./repro &
$ gdb-multiarch -batch -ex 'target remote :1234' \
    -ex 'break stub.S:17' -ex 'continue' -ex 'bt' -ex 'kill' ./repro
```

Actual:

```
#0  stub () at stub.S:17
#1  0x00400724 in f2 () at main.c:2
#2  0x0049a0ec in __init_array_start ()
Backtrace stopped: frame did not save the PC
```

(#1 is found only because `stub` is a leaf and $ra is live; #2 is garbage
read 32 bytes below the real saved-ra slot.)

Expected:

```
#0  stub () at stub.S:17
#1  0x00400724 in f2 () at main.c:2
#2  0x00400758 in f1 () at main.c:3
#3  0x00400554 in main () at main.c:4
```

Also observed on real-world core dumps from a MIPS32r2 (Lantiq GRX) system
running uClibc 0.9.33.2: in a core of a large multi-threaded process, every
thread blocked in `msgrcv` showed `#3 0x00000004 in ?? ()` (the `msgsz`
argument misread as a return address) instead of the real caller chain.

## Suggested fix

When scanning for frame computation (not when skipping the prologue), after
the prologue-end heuristic fires, continue scanning up to the current pc,
accumulating further SP adjustments. Apply the extra adjustment to the frame
base and to the recorded register save slots (same restart mechanism the
scanner already uses for alloca). Patch attached
(0001-gdb-mips-unwind-past-post-prologue-SP-adjustments-in.patch, includes a
gdb.arch testcase); with it, the reproducer above unwinds fully:

```
#0  stub () at stub.S:17
#1  0x00400724 in f2 () at main.c:2
#2  0x00400758 in f1 () at main.c:3
#3  0x00400554 in main () at main.c:4
```

and on the real-world core, garbage frames across all 58 threads went from
32 to 0 (including a thread blocked in ppoll, whose syscall window is 216
bytes into the function - past the prologue scanner's 200-byte cap).

## Related bugs (searched, none is this issue)

- PR 20406 - microMIPS heuristic prologue scanner missing the alloca
  handling that mips32_scan_prologue has. Same area; also a reminder that
  the microMIPS and MIPS16 scanners would need the equivalent of this fix
  (the attached patch covers mips32_scan_prologue only).
- PR 11348 - broken backtrace from a MIPS signal handler (gdb 6.6, 2010).
  Superficially similar symptom, different mechanism: that one failed at
  the signal-trampoline crossing (sigframe unwinder); verified working on
  current master. Note that a signal interrupting a thread blocked in a
  stack-argument syscall reproduces *this* bug right after the
  "<signal handler called>" frame.
- PR 8823 - i386 prologue analyzer losing track of a scheduled prologue
  (fixed in 2004); the same class of heuristic-scanner limitation on
  another architecture.

## References: the SP dance in libc sources

The temporary SP adjustment around `syscall` is spelled out in the libc
sources for every o32 syscall with stack arguments (5+ args):

- uClibc 0.9.33.2, `internal_syscall5/6/7`
  (`subu $29, 32 ... sw args 16/20/24($29) ... syscall ... addiu $29, 32`):
=20
https://github.com/kraj/uClibc/blob/v0.9.33.2/libc/sysdeps/linux/mips/bits/=
syscalls.h#L186-L253
- uClibc 0.9.33.2, hand-written generic `syscall ()`:
=20
https://github.com/kraj/uClibc/blob/v0.9.33.2/libc/sysdeps/linux/mips/sysca=
ll.S#L53-L62
- uClibc-ng (current master), same macros:
=20
https://github.com/wbx-github/uclibc-ng/blob/master/libc/sysdeps/linux/mips=
/bits/syscalls.h#L196-L231
- musl (current master), `__syscall5/6/7`
  (`subu $sp,$sp,32 ... addu $sp,$sp,32`):
  https://git.musl-libc.org/cgit/musl/tree/arch/mips/syscall_arch.h#n96

Modern glibc is immune by design: since the switch to out-of-line
`__mips_syscall5/6/7` wrappers (sysdeps/unix/sysv/linux/mips/mips32/), the
stack arguments live in the compiled caller's frame and SP never moves in
the wrapper. Older glibc used the same inline-asm `subu $29, 32` pattern.

## Note: wrong CFI variant of the same problem

If the libc is built with -g, GCC on MIPS emits .debug_frame, and gdb then
prefers DWARF CFI over the prologue scanner - but that CFI is *also* wrong
for these stubs, because the SP adjustment happens inside an inline-asm
block GCC knows nothing about (hand-written CFI, as glibc does in its asm
stubs, is the only fix there). That variant is a libc issue, not a gdb one;
this report and patch address the common deployed case of stripped/CFI-less
libraries.

--=20
You are receiving this mail because:
You are on the CC list for the bug.=