Skip to content

feat(relinker): support --trace-imports for Linux output - #7417

Merged
oneandonlydean merged 5 commits into
boykopovar:mainfrom
IshmamDC217:feat/relinker-trace-imports-linux
Oct 20, 2026
Merged

oneandonlydean merged 5 commits into
boykopovar:mainfrom
IshmamDC217:feat/relinker-trace-imports-linux

Conversation

@IshmamDC217

Copy link
Copy Markdown
Contributor

What

--trace-imports (#4547) only worked for Windows output. This adds it for Linux output, as the second half of #3815's Linux support, on top of the libc runtime in #7411.

With the flag, LinuxElfPatcher now:

  • adds an undefined TraceImport_nid_no_patch symbol and a GLOB_DAT relocation for it, bound eagerly like every other import, so a libc.prx without it fails at load with ld.so's undefined symbol error;
  • writes a table of the traced import slots with their NID and library, as offsets so it works for PIE and --linux-fixed-base output alike;
  • puts a prologue in front of the existing entry stub. For each entry it calls TraceImport_nid_no_patch(*slot, name, library) and stores the result back in the slot, then falls through to the usual stub. rdx (ld.so's fini pointer) and the stack are preserved.

The same imports are traced as on Windows: functions bound through a PLT slot, or through a GOT slot whose symbol is a function, with no addend, and not weak.

Before storing, the prologue makes each slot's page readable, writable and executable with mprotect. Today the Linux output drops PT_GNU_RELRO, but #6406 keeps it, and real eboots have their JUMP_SLOT targets inside it. Without this, the store would fault once both are merged. I merged #6406 locally with a RELRO range over the slot's page and the traced output still ran correctly. The trade-off (those pages stay writable, and data pages become executable while tracing) is recorded in docs/dev/debt/relinker.md.

Other changes:

  • --trace-imports no longer requires --windows. It is refused with --macos, and it still requires libc.prx among the libraries and conflicts with --lazy-binding.
  • Usage, TRACING.md and the debt entries now cover Linux.

Tested

Linux x86-64, GCC 13. Relinker-only build: the full suite of 195 tests passes.

  • New linux_trace_imports test. It builds a fake a.prx and a fake libc.prx with cc, like linux_executable_exports, and runs the relinked fixture. Its hook replaces a.prx's shared with a function that exits with 77.
    • Untraced: exits with 11.
    • Traced: exits with 77, both PIE and with --linux-fixed-base.
    • Data and weak imports are left alone: exits with 11.
    • A libc.prx without the hook fails at load naming the symbol.
    • The relinker refuses no libc.prx dependency, --lazy-binding and --macos.
    • I checked that the test fails when the expected exit code is wrong.
  • windows_trace_imports loses only its "requires --windows" assertion.
  • End to end with the real libc.prx from feat(libc): record traced system library calls on Linux #7411: APS5_TRACE_CALLS=a ./out.elf on a fixture whose import calls abort() prints:
[libc] SIGABRT
  last traced system library calls (APS5_TRACE_CALLS):
    #1 thread 8574 a.prx shared from 0x000055903df90006 out.elf+0x6
  most called traced imports (1 calls in total):
               1 a.prx shared

Not tested on a real title, Windows or macOS.

python3 tools/check_conventions.py --base origin/main passes.

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: feat(libc): record traced system library calls on Linux #7411
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

}

void ReportOnAbort(int number, siginfo_t* info, void* context) {
std::fprintf(stderr, "[libc] SIGABRT\n");

@MarcZX10 MarcZX10 Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SIGABRT handler calls fprintf here, then ReportCallTrace() calls dladdr, stdio, and the trace-copy routines. These are not async-signal-safe; if abort() is raised while the thread holds a stdio/loader/trace lock, this handler can deadlock before the prior handler runs or the process terminates. Could the trace be emitted from a non-signal-context abort path (or otherwise use a signal-safe handoff) so the diagnostic cannot turn an abort into a hang?

@IshmamDC217 IshmamDC217 Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cheers, sorted in #7411 (f670141), and this one's now rebased on it. The handler only uses write(2) now, with no stdio, dladdr or locks. There's a regression test for the held stderr lock case too. Details are over on #7411.

@github-actions

github-actions Bot commented Oct 18, 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
PR 7532 core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp L280, L286, L295, L324, L326, L347-348, L398
PR 7918 docs/dev/debt/relinker.md L30-31

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/libs/prx/libc/CMakeLists.txt
core/relinker/CMakeLists.txt
docs/dev/debt/libc.md
docs/dev/debt/relinker.md
docs/user/USAGE.md
PR 5431 docs/dev/ENVIRONMENT.md
PR 5925 docs/user/USAGE.md
PR 6044 core/libs/prx/libc/CMakeLists.txt
core/libs/tests/GuestCallTrace.cpp
core/relinker/CMakeLists.txt
PR 6226 docs/dev/debt/libc.md
PR 6469 docs/dev/debt/libc.md
PR 6618 core/relinker/CMakeLists.txt
PR 6636 docs/dev/ENVIRONMENT.md
PR 6788 docs/user/USAGE.md
PR 6819 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 6879 core/relinker/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 6924 docs/dev/ENVIRONMENT.md
docs/user/USAGE.md
PR 6946 docs/dev/ENVIRONMENT.md
docs/user/USAGE.md
PR 6947 docs/dev/ENVIRONMENT.md
docs/user/USAGE.md
PR 6977 core/relinker/CMakeLists.txt
docs/dev/debt/relinker.md
PR 7081 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7201 core/libs/prx/libc/CMakeLists.txt
PR 7464 docs/dev/ENVIRONMENT.md
PR 7473 docs/dev/debt/libc.md
PR 7484 docs/dev/ENVIRONMENT.md
PR 7604 docs/dev/ENVIRONMENT.md
docs/dev/TRACING.md
PR 7642 docs/dev/ENVIRONMENT.md
docs/user/USAGE.md
PR 7648 docs/dev/debt/libc.md
PR 7715 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7800 docs/user/USAGE.md
PR 7922 docs/dev/ENVIRONMENT.md
PR 8004 docs/dev/ENVIRONMENT.md
PR 8076 docs/dev/debt/relinker.md
PR 8077 docs/dev/ENVIRONMENT.md
PR 8150 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 8206 docs/dev/ENVIRONMENT.md
PR 8352 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 8542 docs/dev/ENVIRONMENT.md

Build the call tracer for Linux as well as Windows. The thunk area is a
memfd mapped twice, writable and executable, and threads are named by
gettid. Linux has no crash report, so the first traced import installs a
SIGABRT handler that prints the last calls and the most called imports,
then passes the signal on to the previous handler.

guest_call_trace and guest_call_trace_abort now run on Linux too.
The SIGABRT handler used stdio and dladdr, so an abort raised while another thread held the stderr lock or the loader lock hung instead of ending. The report is now formatted into a stack buffer and written with write(2). Callers are named from a module list taken with dl_iterate_phdr when imports are traced, and import counts are read without a lock from a fixed record array. The abort test now holds the stderr lock in another thread, which timed out before this change.
A thread that lost the race to write the report passed SIGABRT on straight away, and the default action ended the process while the first thread was still writing. It now waits for the report to finish, polling with nanosleep for at most 2 seconds, before it chains. guest_call_trace_abort_race aborts from two threads at once and checks the whole report; it failed 35 times in 50 without the wait. Reported by jotitadev.
The Linux entry stub now passes each traced import through libc.prx's
TraceImport_nid_no_patch before it calls the guest entry, and stores the
returned thunk in the import's slot. The same imports are traced as on
Windows: functions bound through a PLT slot or a GOT slot, without an
addend, and not weak. TraceImport_nid_no_patch is bound eagerly through
an added GLOB_DAT relocation, so a libc.prx without it fails at load.
Each slot's page is made writable with mprotect first, so slots inside a
PT_GNU_RELRO range can be replaced.

--trace-imports now requires libc.prx among the libraries on Linux too,
and is refused with --macos.
@IshmamDC217
IshmamDC217 force-pushed the feat/relinker-trace-imports-linux branch from 3a2c02f to 78634fb Compare October 19, 2026 21:59
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 20, 2026
Resolve docs/dev/debt/libc.md: keep main's _Throw_Cpp_error entry and this branch's call tracing entry.
@oneandonlydean
oneandonlydean merged commit 29e4c46 into boykopovar:main Oct 20, 2026
17 checks passed
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.

3 participants