Skip to content

fix(ajm, apr): keep AJM and APR state alive through static teardown - #1926

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
fisherjoey:fix/exit-teardown-order
Oct 10, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
fisherjoey:fix/exit-teardown-order

Conversation

@fisherjoey

Copy link
Copy Markdown
Contributor

What

A window close ends in std::exit on a helper thread (LibcRequestExit_nid_postfix), and std::exit destroys the host libraries' statics while the title's threads are still running. AJM and APR keep their state in namespace-scope statics, so a title thread that calls them during exit reads freed memory:

  • libSceAjm.native: g_instances and g_batches (and their mutexes). A job on a freed Instance decodes a garbage codec, sceAjmBatchWait returns SCE_AJM_ERROR_INVALID_BATCH for a batch it just started, and sceAjmInstanceDestroy frees an instance twice.
  • libkernel APR: g_files, g_idsByPath, g_directories, g_counterWrites and the AMM state from _amm(). A read resolves its file id through the freed vector.

All of these are now allocated once and never destroyed, the same approach #1197 took for NGS2. Callers don't change: the namespace-scope names are now references to the heap objects.

Closes #1925

Tested

Nobara 44 x86-64 (Linux 7.2), GCC 16.2, RTX 5070 Ti, driver 595.91.07. Base: main f145139.

  • New test guest_ajm_teardown (UNIX only, it needs dlopen). It registers an atexit handler, then dlopens libSceAjm.native and creates an instance. Because the library is loaded after the handler is registered, its statics are destroyed before the handler runs. The handler then runs a batch on the instance, waits for the batch and destroys the instance. With main's Ajm.cpp it aborts 3 of 3 times (free(): double free detected in tcache 2). With this change it passes.
  • ctest -j1 --timeout 60: 494 of 495 pass. The one failure, guest_dynamic_loader, also fails here without this change. tools/check_conventions.py and tools/check_test_registration.py pass.
  • Demon's Souls PPSA01341 v1.07, closed with the window's close button at the attract movie after 150 s. These runs used our test stack (main from 10-07 with fix(agc): release the current address space with the image mirrors #1071, perf(agc): pipelined draws on by default #1202, feat(recompiler): bindless image tables up to the device's sampled-image limit #1184 and local performance changes) with this commit on top. Before: 15 of 15 closes crashed (SIGABRT on AJM: decoding codec 4026830656 is not implemented, APR: cannot open <garbage> or double free or corruption, or SIGSEGV after NCA DecoderManager: sceAjmBatchWait() failed with error code 0x80930004), and 3 of 3 with fix(libc): run guest exit handlers before the host exit handlers #1658. After: 5 of 5 closes exit 0 with no core. One more run aborted at boot, before any close, on an X RandR error (BadRROutput) and VideoOut: presentation thread cannot stop itself. That is unrelated to this change.
  • Windows: not built. The change is plain C++.

Other libkernel state lives in namespace-scope statics too (Equeue.cpp, SyncOnAddress.cpp, DirectMemory.cpp). Demon's Souls didn't hit those in these runs, so they are left as they are.

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 in TechnicalDebt
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

@klosixgaming-arch klosixgaming-arch left a comment

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.

I can approve this.

  • The *new T idiom to make these statics immortal is the established pattern here (you cite #1197 for NGS2), so it's not a novel hack — it's the project's chosen remedy for "title thread reads freed statics during std::exit". Consistent.
  • Callers are unchanged because the namespace-scope names become references to heap objects — correct, and the smallest possible surface for the change.
  • The test is excellent: it dlopens the library after registering the atexit handler so the library's statics are destroyed before the handler runs, then exercises AJM from the handler. main aborts 3/3 (double free in tcache 2), this branch passes. That's a genuine regression test for the exact failure class, not a symbol check.
  • You correctly scope it to AJM+APR and explicitly note the other libkernel namespace statics (Equeue.cpp, SyncOnAddress.cpp, DirectMemory.cpp) that weren't hit — honest boundary, not silent.

Non-blocking note: the deliberate leak is the whole point, but a reader glancing at std::mutex& g_lock = *new std::mutex(); might not know it's intentional project policy. A one-line debt/CONVENTIONS pointer (or a shared helper Immortal<T>) would stop a future reviewer from "fixing" it back to a normal static. Worth it given it's now in at least three files.

Written with AI assistance.

@popcoder565

Copy link
Copy Markdown
Contributor

Windows build check, since the description says Windows wasn't built. Setup: Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. Merged onto main 745b775 together with #1926, #1950, #1260 and #1988. Full ctest: 507/507.

  • It builds.
  • The existing guest_ajm and APR tests pass.
  • The new guest_ajm_teardown is registered under if(UNIX), so it didn't run here.

🤖 Addressed by Claude Code

@fisherjoey

Copy link
Copy Markdown
Contributor Author

@klosixgaming-arch thanks. I added a TechnicalDebt entry for the leaked statics (NGS2, AJM, APR) in f79d69c, since code comments aren't allowed here. A shared helper would also change NGS2, so I'd leave it for another PR.

@popcoder565 thanks for the Windows build. The teardown test is Unix-only on purpose. It depends on the ELF order in which a dlopen'd library's statics are destroyed relative to atexit handlers. On Windows a DLL's own atexit handlers and static destructors run when that DLL detaches (DLL_PROCESS_DETACH), so the same setup doesn't reproduce the ordering there.

AI-assisted: yes

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 9, 2026
@fisherjoey
fisherjoey force-pushed the fix/exit-teardown-order branch from f79d69c to b0011a1 Compare October 9, 2026 17:14
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 9, 2026
@fisherjoey
fisherjoey force-pushed the fix/exit-teardown-order branch from b0011a1 to 5eb2a13 Compare October 9, 2026 22:14
@github-actions github-actions Bot added docs and removed needs rebase Conflicts with main, rebase needed labels Oct 9, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 9, 2026
AJM's instance and batch tables and APR's file, directory and AMM
state were namespace-scope statics, destroyed by std::exit while the
title's threads keep calling the libraries. A decoder thread then read
a freed instance (an unknown codec), sceAjmBatchWait no longer found
its batch, and APR resolved file ids through a freed vector. The state
is now allocated once and never destroyed.
@fisherjoey
fisherjoey force-pushed the fix/exit-teardown-order branch from 5eb2a13 to 78388c2 Compare October 10, 2026 02:15
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 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
#1969 docs/dev/TechnicalDebt.md L398
#2142 docs/dev/TechnicalDebt.md L398
#2545 core/libs/tests/GuestAjmTeardown.cpp

@oneandonlydean
oneandonlydean merged commit 9ac075e into boykopovar:main Oct 10, 2026
12 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
…order

fix(ajm, apr): keep AJM and APR state alive through static teardown
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.

Linux: closing the window crashes in AJM/APR calls made after static teardown

4 participants