Skip to content

Linux: PPSA28997 (Sons of Sparta) aborts at start-up with "RTLD application heap: cannot replace an active allocator" (#693 regressed after #1515) #1930

Closed

Description

@igordev23

Opening this as a tracking issue because #693 is closed while its fix (#1156) was reverted by #1515. Full details are in my comment there: #693 (comment)

Title: PPSA28997 (God of War: Sons of Sparta), native Linux output. The title bundles sce_module/libc.prx.

Current main (f1451396) + #1482 (_umtx_op) + a local workaround for #918 aborts at start-up, right after opening /app0/Media/boot.config:

[libc] unhandled exception 'St13runtime_error' thrown from 0x7cac0c9be567
terminate called after throwing an instance of 'std::runtime_error'
  what():  RTLD application heap: cannot replace an active allocator

This is the guard in registerApplicationHeapApi (core/libs/prx/libkernel/System/src/Rtld.cpp): sceKernelRtldSetApplicationHeapAPI runs a second time with a different allocator table, so both libc instances perform the application-heap handshake.

Older build without that guard (df16c4c): the title starts, then later glibc aborts with corrupted size vs. prev_size.

What the title needs on Linux, none of it in main yet

Environment: Ubuntu, GCC 15, AMD Ryzen APU (Barcelo, RADV).

I can test branches on PPSA28997 / Linux and provide logs or backtraces.

Activity

  1. igordev23 commented on Oct 9, 2026

    @igordev23
    Author

    Test Environment (after reapplying #1156 on origin/main)

    Build: AnyPS5 commit d70b8998 + revert #2096c66a + port #1482 (_umtx_op) + RecordWrittenPath stub + sceKernelRaiseException implementation + SN function stubs.

    Result: The game PPSA28997 (God of War: Sons of Sparta) now boots past the initial "RTLD application heap: cannot replace an active allocator" error and reaches the Vulkan device initialization, presentation window creation (1920x1080), and shader cache loading phase.

    Error after correction: After the boot sequence completes, the process exhibits the same intermittent heap corruption previously observed: corrupted size vs. prev_size at varying workload counts (typically ~92 workloads, range 60-130 depending on timing). This is a timing-dependent race condition, not an immediate crash. The ASan-run21 previously captured: SEGV on unknown address 0x004000003202 (WRITE, thread T3).

    Status: Game loads and runs, but heap corruption persists as a timing-dependent issue. All 3 original missing NIDs + 1 new are now handled:

    • _umtx_op: implemented with FreeBSD futex semantics (mutex ops 2/3/4, wake 15/16, wait/wake 0/1/9/13/14/19; rest → ENOSYS)
    • sceNpUnregisterNpReachabilityStateCallback: stubbed (returns 0, logged in TechnicalDebt)
    • sceNpEntitlementAccessGetEntitlementKey: stubbed (returns 0, logged in TechnicalDebt)
    • sceKernelRaiseException: implemented via pthread_kill + SA_SIGINFO trampoline
    • RecordWrittenPath_nid_no_patch: stub added (previously caused symbol lookup error)

    Next: Capture ASan backtrace when corruption appears, or test with -job-worker-count 1 to reduce variability.

    Branch: test/1930-reapply-1156 based on origin/main (d70b8998).

  2. igordev23 commented on Oct 9, 2026

    @igordev23
    Author

    Test Environment (after reapplying #1156 on origin/main)

    Build: AnyPS5 commit d70b8998 + revert #2096c66a + port #1482 (_umtx_op) + RecordWrittenPath stub + sceKernelRaiseException implementation + SN function stubs.

    Result: The game PPSA28997 (God of War: Sons of Sparta) now boots past the initial "RTLD application heap: cannot replace an active allocator" error and reaches the Vulkan device initialization, presentation window creation (1920x1080), and shader cache loading phase.

    Error after correction: After the boot sequence completes, the process exhibits the same intermittent heap corruption previously observed: corrupted size vs. prev_size at varying workload counts (typically ~92 workloads, range 60-130 depending on timing). This is a timing-dependent race condition, not an immediate crash. The ASan-run21 previously captured: SEGV on unknown address 0x004000003202 (WRITE, thread T3).

    Status: Game loads and runs, but heap corruption persists as a timing-dependent issue. All 3 original missing NIDs + 1 new are now handled:

    • _umtx_op: implemented with FreeBSD futex semantics (mutex ops 2/3/4, wake 15/16, wait/wake 0/1/9/13/14/19; rest → ENOSYS)
    • sceNpUnregisterNpReachabilityStateCallback: stubbed (returns 0, logged in TechnicalDebt)
    • sceNpEntitlementAccessGetEntitlementKey: stubbed (returns 0, logged in TechnicalDebt)
    • sceKernelRaiseException: implemented via pthread_kill + SA_SIGINFO trampoline
    • RecordWrittenPath_nid_no_patch: stub added (previously caused symbol lookup error)

    Next: Capture ASan backtrace when corruption appears, or test with -job-worker-count 1 to reduce variability.

    Branch: test/1930-reapply-1156 based on origin/main (d70b8998).

  3. oneandonlydean commented on Oct 11, 2026

    @oneandonlydean
    Collaborator

    #1102 (module parameter block) is merged, and PPSA28997 is reported booting on main 942a174 with #1482 and #1431 (#1482 (comment)). Does it still stop at RTLD application heap: cannot replace an active allocator for you on current main plus #1482?

  4. igordev23 commented on Oct 11, 2026

    @igordev23
    Author

    Tested exactly that: clean origin/main (a11c8f7f7, no revert), which already contains #1482, #1431 and #1102. Rebuilt libkernel.prx / libSceNpManager.prx / libSceNpEntitlementAccess.prx and ran PPSA28997 (Linux, RADV Barcelo).

    The allocator error is gone: zero occurrences of RTLD application heap: cannot replace an active allocator in the log.

    Boot now stops earlier at a different missing NID:

    ./app.elf: symbol lookup error: .../libs/libkernel.prx: undefined symbol: RecordWrittenPath_nid_no_patch
    

    EXIT: 127, immediate, before any Vulkan init. So on current main the double-handshake no longer trips the guard, but the title needs RecordWrittenPath exported from libkernel, which main does not provide yet. Happy to add it as NotImplemented_nid_no_patch per the contributing rules and re-test if that helps.

  5. MarcZX10 commented on Oct 11, 2026

    @MarcZX10
    Contributor

    I checked this symbol against current upstream/main (a11c8f7f7): it is defined in core/libs/prx/libc/src/General.cpp, declared in libc/include/General.hpp, and libkernel links to the libc target in its CMake file. The reported verification rebuilt libkernel.prx, libSceNpManager.prx, and libSceNpEntitlementAccess.prx, but doesn't mention rebuilding or replacing the matching libc.prx.

    Could you try rebuilding/deploying libc.prx from the same checkout too, then rerun? If a clean set of PRXs still leaves RecordWrittenPath_nid_no_patch unresolved, the link/load logs from that build should help isolate why the existing definition isn't being resolved. I don't see a source-level missing-export fix to submit yet.

  6. igordev23 commented on Oct 11, 2026

    @igordev23
    Author

    You were right: after rebuilding and deploying libc.prx from the same checkout (a11c8f7f7), RecordWrittenPath_nid_no_patch resolves — zero symbol errors in the log.

    Boot now proceeds further (threads spawn, /app0/Media/RuntimeInitializeOnLoads.json opens) and then aborts:

    [libc] unhandled exception St13runtime_error thrown from 0x7fefea9ff48b
    terminate called after throwing an instance of std::runtime_error
      what():  APR: truncated command buffer
    

    EXIT: 134, only 74 log lines total — it dies before any Vulkan init, inside Apr::_execute (core/libs/prx/libkernel/Apr/src/Apr.cpp:530): the cursor walk hits a cut-off CommandHeader (cursor + sizeof(header) > buffer.offset) on the first asset-read batch after RuntimeInitializeOnLoads.json.

    So current status on clean main: no allocator error, no missing NIDs — the title now dies in APR command-buffer execution during early file reads. Let me know if a trace of the APR opcodes/buffer sizes leading up to it would help, I can instrument _execute locally.

  7. MarcZX10 commented on Oct 11, 2026

    @MarcZX10
    Contributor

    The new APR failure is at the parser boundary in Apr.cpp::_execute: it means fewer than sizeof(CommandHeader) bytes remain at cursor, before an opcode can be decoded. Could you capture the following at that throw on an unchanged a11c8f7f7 build?

    • buffer.offset, cursor, and buffer.offset - cursor;
    • buffer.base and the first up to 16 bytes from buffer.base + cursor (hex);
    • the last successfully parsed command offset/opcode/header.bytes and the buffer object address;
    • whether the same command buffer is submitted from any other thread or modified after submission.

    Please keep the guard in place and log immediately before throwing. That will distinguish a genuinely truncated guest command stream from a bad/stale offset or concurrent buffer mutation; without it, relaxing the parser check risks reading beyond the captured commands.

  8. MarcZX10 commented on Oct 11, 2026

    @MarcZX10
    Contributor

    Separate finding from my parser inspection: APR records with a valid header could declare fewer bytes than the opcode-specific struct, while _execute still copied the full struct; the old cursor + header.bytes bound could also overflow. I opened #3808 with size checks and malformed-record regressions. This addresses malformed command records only and does not diagnose or claim to fix your reported buffer ending before a complete header; the offset/cursor/raw-tail capture requested above is still needed for that.

  9. igordev23 commented on Oct 11, 2026

    @igordev23
    Author

    Capture from an unchanged a11c8f7f7 build with only a pre-throw log added (guard intact). Two consecutive runs, identical values:

    [apr-diag] truncated: obj=0x2101a0580 base=0x1000100000001 offset=2 size=40 numCommands=1048576 cursor=0 remain=2 last={has=0 off=0 opcode=0 bytes=0}
      what():  APR: truncated command buffer
    EXIT: 134
    

    Field by field against your checklist:

    • buffer.offset = 2, cursor = 0, remain = 2.
    • buffer.base = 0x1000100000001. No tail bytes to show: dereferencing base to dump them faults (first attempt died with SIGSEGV instead of the throw), so the base is not host-readable.
    • Last parsed command: none (has=0) — it fails on the very first header. Buffer object at 0x2101a0580; size = 40, numCommands = 1048576 (0x100000).
    • Threading: single tid observed at the throw; values are bit-identical across runs (same obj, same fields), which argues against concurrent mutation — a racy writer would be unlikely to reproduce the exact same garbage twice.

    Read: this does not look like a partially-filled valid stream. base is odd (...001) and unmapped, numCommands is 0x100000, and nothing was consumed — the CommandBufferObject itself looks stale or misread (bad guest address or layout skew) rather than truncated mid-stream. The submitter runs right after /app0/Media/RuntimeInitializeOnLoads.json is opened, before any graphics init.

    Happy to dump the raw 0x18 object bytes at obj or trace the submitter (_execute caller at Apr.cpp:732 and the bytes-based path at :662) next.

  10. oneandonlydean commented on Oct 11, 2026

    @oneandonlydean
    Collaborator

    The allocator error is gone on main, so I'm closing this.

    The APR: truncated command buffer stop is a mixed build. Read with the object layout from before #2789, your dump is a healthy buffer: 1 MiB at 0x2..., offset 40, one command. So libSceAmpr.prx is older than libkernel.prx; #2789 changed that layout in both. Rebuild and deploy all of libs/ from one checkout, and open a new issue if PPSA28997 still stops after that.

  11. igordev23 commented on Oct 12, 2026

    @igordev23
    Author

    Confirmed — it was the mixed build. Rebuilt all 122 patched .prx files from the single a11c8f7f7 checkout and deployed the full set (backup of the old mixed libs/ kept aside).

    Result on PPSA28997 (Linux, RADV Barcelo): no stop at all. Zero APR: truncated, zero allocator/symbol errors. Boot proceeds through Vulkan device init, 1920x1080 presentation window, swapchain (1109x624), shader draws — and the process was still alive when timeout 300 killed it (EXIT: 124). The trailing ProcessShutdown throw is just the shutdown path reacting to SIGTERM.

    So: clean main + full single-checkout libs/ runs the title past every previously reported stop. No new issue needed from my side. Thanks for the layout pointer — that obj dump decoding as a healthy buffer under the pre-#2789 layout was the giveaway.

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