Skip to content

Linux: a bundled libc.prx loader shim (ps5-native-app-boilerplate) binds libc imports to placeholder exports; DOOM PPSA99666 finds no WAD #1497

Closed

Description

@hikazey

Summary

On Linux, imports bound to a bundled sce_module/libc.prx built by ps5-native-app-boilerplate resolve to placeholder functions that return 0. DOOM Shareware PPSA99666 v1.1.1 (lowbit/ps5-doom, GPL-2.0) ships this module (built at boilerplate commit b1315a9, per its THIRD_PARTY_NOTICES.md). Its snprintf/vsnprintf write nothing, so the title never builds a WAD path, finds no game and exits with code 0. #684 fixed the same interception for Windows ("A title's bundled libc.prx shim can then intercept imports ... with its placeholder exports"); its description notes that Linux keeps the existing behaviour.

What the module is

The boilerplate calls it a "loader shim": the PS5 loader needs a libc.prx next to a native title. It is generated by tooling/native/libc_builder.cpp from tooling/native/runtime/api-surface.txt (2572 API records: NID, binding, type, size). In build_export_symbols, every function export (type == 2) gets the address 0x30, 0x40 (_longjmp, _setjmp) or 0x50. In the shipped module these are xor eax,eax; ret and, at 0x50, pxor xmm0,xmm0; xor eax,eax; ret. Since the title works on hardware, the console presumably serves these imports from the system libc. I haven't verified that on a console.

Evidence (Linux, main 0e173a46 + #1278, #1279, #1307)

  • Relinking eboot.bin (unwrapped from the fake-signed SELF) with the bundled libc.prx binds 70 of the title's imports to the guest module (#guest), among them snprintf, vsnprintf, strlen, memcpy, malloc, fopen, qsort and strcasecmp.
  • plat_alert builds its message with snprintf(request.message, …, "DOOM: %s", message). AnyPS5 prints [notification] with an empty message, and the request layout matches Notification.cpp (message at offset 45).
  • /download0/doom.log (written through vsnprintf) stays empty.
  • The WAD scan lists /app0/wads correctly: getdents64 returns ., .. and doom1.wad, and the guest records match the FreeBSD 11 layout the title parses. But the title never opens doom1.wad. Instead it opens a bare /download0 about 30 times, the result of snprintf("%s/%s", …) that writes nothing.

Workaround and result

Relinking with --exclude-sce-module libc.prx binds those imports to AnyPS5's libraries. The title then finds doom1.wad, shows its launcher and reaches E1M1 on Linux. Moving and firing work (ammo 50 → 48).

Performance (Ubuntu 24.04 container on a Ryzen 9 5900X, Xvfb, no GPU; Mesa llvmpipe for Vulkan):

  • The launcher presents at 59.94 FPS.
  • In game it holds 35.00 FPS, DOOM's tic rate, over a 30 s run while moving and turning (window-title counter).
  • The process averages about 2.1 cores.
  • As in the Windows notes in docs: record DOOM homebrew integration compatibility #690, the first AGC frame never completes on llvmpipe (gpu: frame 1 never completed), and the title switches to its CPU scaler.

Question

Should the Linux relinker treat imports whose only provider is a module like this the way #684 does on Windows, binding each qualified import to its declared system module? Or should titles that ship this shim be relinked with --exclude-sce-module libc.prx? The flag is deprecated and documented as unsuitable for normal use. Any title built with ps5-native-app-boilerplate should hit the same problem.

Screenshots of the launcher and E1M1 to follow.

AI-assisted: yes (Claude Code).

Activity

  1. hikazey commented on Oct 8, 2026

    @hikazey
    ContributorAuthor
    Image Image Image
  2. johnmaia commented on Oct 8, 2026

    @johnmaia
    Contributor

    Root cause on Linux: GuestModuleBuilder renames every executable import whose NID a guest module exports to its guest-suffixed name (GuestModuleBuilder.cpp:180), whatever module the import declares. DOOM PPSA99666 declares snprintf, vsnprintf, strlen, memcpy, malloc and fopen from libSceLibcInternal (#G#H, module 7) and declares nothing from libc; the bundled shim exports the same NIDs, so 71 imports become <nid>#guest (eLdDw6l0-bU#guest is snprintf) and bind to its placeholders. On Windows the same build binds by the declared module (WindowsEntryStubBuilder) and opens /app0/wads/doom1.wad (checked with main + #1278, #1279, #1307; DOOM built from source at v1.1.1, byte-identical to the release).

    Plan: on Linux, rename an import to the guest-suffixed name only when the module it declares is that guest module, as Windows binds; imports without import-module metadata keep the current behaviour, so a title's own libc.prx imported from libc still binds to it (7103f39, test_guest_host_libc.py). I'm working on it.

    (Edited: my first version blamed the DT_NEEDED order; the rename is what binds these imports to the guest module.)

  3. klosixgaming-arch commented on Oct 8, 2026

    @klosixgaming-arch
    Contributor

    PR #1529 ("fix(relinker): keep Linux imports declared from host modules off guest exports") directly addresses this issue. The PR fixes the problem where on Linux, GuestModuleBuilder was renaming every import whose NID a guest module exports to its guest-suffixed name, regardless of which module the import declares.

    The fix adds logic to check if an import is declared from a host module vs. a guest module, and only binds to guest exports when appropriate. This should resolve the DOOM PPSA99666 issue where libc imports were being bound to placeholder functions in the bundled libc.prx loader shim.

    The PR has been reviewed and approved, pending merge by maintainers.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions