From f2d201fbbfafff3ecd031eae76cef840237e02ff Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Thu, 8 Oct 2026 17:05:33 -0600 Subject: [PATCH 1/2] fix(ajm, apr): keep AJM and APR state alive through static teardown 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. --- core/libs/prx/libSceAjm.native/CMakeLists.txt | 7 ++ core/libs/prx/libSceAjm.native/src/Ajm.cpp | 8 +- core/libs/prx/libkernel/Apr/src/Apr.cpp | 14 ++-- core/libs/tests/GuestAjmTeardown.cpp | 81 +++++++++++++++++++ 4 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 core/libs/tests/GuestAjmTeardown.cpp diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index aa4e24f83..a231a1529 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -36,6 +36,13 @@ if(BUILD_TESTING) target_link_libraries(guest_ajm_tests PRIVATE libSceAjm.native libc) configure_windows_unwind(guest_ajm_tests) add_test(NAME guest_ajm COMMAND guest_ajm_tests) + if(UNIX) + add_test_executable(guest_ajm_teardown_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmTeardown.cpp) + target_include_directories(guest_ajm_teardown_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_teardown_tests PRIVATE libc ${CMAKE_DL_LIBS}) + add_dependencies(guest_ajm_teardown_tests libSceAjm.native) + add_test(NAME guest_ajm_teardown COMMAND guest_ajm_teardown_tests $) + endif() if(WIN32) set_tests_properties(guest_ajm PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index b7649e151..2a10bf7d4 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -144,13 +144,13 @@ struct Instance { } }; -std::mutex g_lock; -std::map> g_instances; +std::mutex& g_lock = *new std::mutex(); +std::map>& g_instances = *new std::map>(); std::atomic g_nextContext{1}; std::atomic g_nextInstance{1}; std::atomic g_nextBatch{1}; -std::mutex g_batchLock; -std::set g_batches; +std::mutex& g_batchLock = *new std::mutex(); +std::set& g_batches = *new std::set(); enum class JobKind : std::uint32_t { Initialize = 1, diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index a363671b9..d4e4cd421 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -39,15 +39,15 @@ struct AprFile { std::uint64_t size; }; -std::mutex g_filesLock; -std::vector g_files; -std::unordered_map g_idsByPath; +std::mutex& g_filesLock = *new std::mutex(); +std::vector& g_files = *new std::vector(); +std::unordered_map& g_idsByPath = *new std::unordered_map(); // File sizes by directory, listed on first use. Titles resolve thousands of package paths at startup, // and one size query per path took over 20 s; a listing costs one directory read. Names are compared // case-insensitively, like the host file system. -std::mutex g_directoriesLock; -std::unordered_map> g_directories; +std::mutex& g_directoriesLock = *new std::mutex(); +std::unordered_map>& g_directories = *new std::unordered_map>(); std::string _foldCase(std::string text) { for (auto& character : text) { @@ -164,7 +164,7 @@ void _writeAddress(const Apr::WriteAddressCommand& command) { std::array, 256> g_counters{}; -std::mutex g_counterWrites; +std::mutex& g_counterWrites = *new std::mutex(); std::uint32_t _counter(std::uint32_t index) { return g_counters[index % g_counters.size()].load(std::memory_order_acquire); @@ -263,7 +263,7 @@ struct AmmState { }; AmmState& _amm() { - static AmmState state; + static auto& state = *new AmmState(); return state; } diff --git a/core/libs/tests/GuestAjmTeardown.cpp b/core/libs/tests/GuestAjmTeardown.cpp new file mode 100644 index 000000000..c62f9ffff --- /dev/null +++ b/core/libs/tests/GuestAjmTeardown.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +namespace { + +using Initialize = int (APS5_VABI*)(std::int64_t, std::uint32_t*); +using InstanceCreate = int (APS5_VABI*)(std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t*); +using InstanceDestroy = int (APS5_VABI*)(std::uint32_t, std::uint32_t); +using BatchInitialize = int (APS5_VABI*)(void*, std::size_t, AjmBatchInfo*); +using BatchJobClearContext = int (APS5_VABI*)(AjmBatchInfo*, std::uint32_t, void*); +using BatchStart = int (APS5_VABI*)(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); +using BatchWait = int (APS5_VABI*)(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); + +struct Functions { + Initialize initialize; + InstanceCreate instanceCreate; + InstanceDestroy instanceDestroy; + BatchInitialize batchInitialize; + BatchJobClearContext batchJobClearContext; + BatchStart batchStart; + BatchWait batchWait; +}; + +struct SidebandResult { + std::int32_t result; + std::int32_t internalResult; +}; + +Functions ajm{}; +std::uint32_t context = 0; +std::uint32_t instance = 0; + +[[noreturn]] void Fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + std::fflush(stderr); + std::_Exit(1); +} + +template +T Resolve(void* library, const char* name) { + auto* symbol = dlsym(library, name); + if (symbol == nullptr) Fail(name); + return reinterpret_cast(symbol); +} + +void UseAfterStaticTeardown() { + alignas(16) std::uint8_t buffer[0x100]{}; + AjmBatchInfo info{}; + SidebandResult sideband{-1, -1}; + if (ajm.batchInitialize(buffer, sizeof(buffer), &info) != 0 || ajm.batchJobClearContext(&info, instance, &sideband) != 0) Fail("a batch cannot be built after static teardown"); + std::uint32_t batch = 0; + AjmBatchError error{}; + if (ajm.batchStart(context, &info, 0, &error, &batch) != 0) Fail("sceAjmBatchStart failed after static teardown"); + if (sideband.result != 0) Fail("an instance created before exit is unknown after static teardown"); + if (ajm.batchWait(context, batch, 0, &error) != 0) Fail("sceAjmBatchWait does not find a batch started after static teardown"); + if (ajm.instanceDestroy(context, instance) != 0) Fail("sceAjmInstanceDestroy does not find the instance after static teardown"); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) Fail("usage: guest_ajm_teardown_tests "); + if (std::atexit(UseAfterStaticTeardown) != 0) Fail("atexit failed"); + auto* library = dlopen(argv[1], RTLD_NOW); + if (library == nullptr) Fail(dlerror()); + ajm = { + Resolve(library, "sceAjmInitialize"), + Resolve(library, "sceAjmInstanceCreate"), + Resolve(library, "sceAjmInstanceDestroy"), + Resolve(library, "sceAjmBatchInitialize"), + Resolve(library, "sceAjmBatchJobClearContext"), + Resolve(library, "sceAjmBatchStart"), + Resolve(library, "sceAjmBatchWait"), + }; + if (ajm.initialize(0, &context) != 0 || ajm.instanceCreate(context, 0, 0, &instance) != 0) Fail("AJM setup failed"); + return 0; +} From 78388c247e38777c83348242e0b2970f3c287726 Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Fri, 9 Oct 2026 09:13:28 -0600 Subject: [PATCH 2/2] docs(debt): record the deliberately leaked AJM, APR and NGS2 state --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 29cfa2b3a..c98ed1825 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -395,3 +395,4 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported - [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers and other signals (`SIGBUS`, `SIGHUP`, ...) are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`; `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` of the other signals still returns `EINVAL` +- [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix.