Skip to content

fix(libc): let a title's own libunwind read _Unwind_Backtrace frames - #4945

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
nicolasfalesy:fix/libc-unwind-backtrace-cursor
Oct 13, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
nicolasfalesy:fix/libc-unwind-backtrace-cursor

Conversation

@nicolasfalesy

Copy link
Copy Markdown
Contributor

What

Homebrew built with the ps5-payload-dev SDK links LLVM libunwind statically, but imports _Unwind_Backtrace (s62MgBhosjU) from libSceLibcInternal. Its trace callback then reads each frame with its own _Unwind_GetIP, i.e. __unw_get_reg(cursor, UNW_REG_IP, &ip), which treats the context as a libunwind cursor: co->validReg(regNum) then co->getReg(regNum), through the virtual table in the cursor's first word (slots +0x10 and +0x18).

AnyPS5's _Unwind_Context starts with the saved RAX, so that call jumps to a register value. vkQuake (PPSA99010) without its game data crashes this way on Linux instead of showing its error:

=> call   *0x10(%rax)        ; rax = 0xc0, the context's first word (saved RAX)

(at app.elf+0x367e, inside the title's own libunwind; the format string next to it is libunwind: __unw_get_reg(cursor=%p, regNum=%d, &value=%p).)

The context now starts with a table in LLVM libunwind's AbstractUnwindCursor slot order (UnwindCursor.hpp: two destructors, validReg, getReg, setReg, validFloatReg, getFloatReg, setFloatReg, step, getInfo, jumpto, isSignalFrame, getFunctionName, setInfoBasedOnIPRegister, getRegisterName). It answers register reads and writes (DWARF numbers 0 to 16, UNW_REG_IP = -1, UNW_REG_SP = -2), validFloatReg (false, as libunwind's Registers_x86_64) and isSignalFrame. The other slots throw NotImplemented. AnyPS5's own _Unwind_* functions are unchanged; the register capture and restore code addresses the context through registers, so moving it by one word does not affect it.

13 of the catalog's titles have both the static libunwind and the _Unwind_Backtrace import: PPSA59322, PPSA76677, PPSA85300, PPSA97358, PPSA97510, PPSA98510, PPSA99002, PPSA99010, PPSA99141, PPSA99303, PPSA99309, PPSA99310, PPSA99810.

Tested

Linux (Arch, GCC 16.2.1, Release), on main 7e181fc:

  • exception_runtime: a second _Unwind_Backtrace check reads every frame through the table the way a title's libunwind does (validReg, getReg of IP, SP and RBX, setReg, validFloatReg, isSignalFrame) and compares with _Unwind_GetIP and _Unwind_GetGR. The RegisterContext layout assertion now checks cfa relative to registers.
  • Full ctest: 1011 of 1011 pass (the display-only VideoOut and AGC tests skip).
  • vkQuake (PPSA99010, catalog release, --to-intel) without id1: on main it dies with SIGSEGV (exit 139). With this change it exits 0 after its own error, as on a console: Quake Error: W_LoadWadFile: couldn't load gfx.wad ... Check that this has an id1 subdirectory containing pak0.pak and pak1.pak.
  • tools/check_conventions.py --base upstream/main passes.
  • Not tested on Windows or macOS; the change there is the same struct field and table.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: PPSA99010 (vkQuake) error path on Linux
  • AI-assisted: yes

Homebrew built with the ps5-payload-dev SDK links LLVM libunwind
statically but imports _Unwind_Backtrace from libSceLibcInternal. Its
trace callback then reads each frame with its own _Unwind_GetIP, which
treats the context as a libunwind cursor and calls through the virtual
table in its first word. AnyPS5's context started with the saved RAX, so
the call jumped to a register value and the title crashed on its error
path (vkQuake, PPSA99010, without its game data). The context now starts
with a table in AbstractUnwindCursor's slot order that answers register
reads and writes, validFloatReg and isSignalFrame; other slots throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvDbpwUPTa78oCAJqe7m9K
@github-actions

Copy link
Copy Markdown

Progress report (ea2bbef → db3ec02)

📉 System libraries: 87.92% (-0.06%, +3 implemented, +3 declared functions)

✅ 3 implemented
Library Name
libc CursorIsSignalFrame
libc CursorValidFloatRegister
libc CursorValidRegister
⚪ 1 behaviourally empty (constant return or throw)
Library Name
libc CursorValidFloatRegister
🆕 3 declared as stubs
Library Name
libc CursorGetRegister
libc CursorSetRegister
libc CursorUnsupported

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#4822 docs/dev/debt/libc.md L43

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on the PR merged onto main: exception_runtime and register_context pass, and exception_runtime fails without the change (the new _Unwind_Backtrace callback reads every frame through the cursor table). The register layout and slot order match LLVM libunwind's AbstractUnwindCursor, and unsupported slots throw. Merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants