From fd597084f9c00880a7c7907d6b05e6dca06b1a29 Mon Sep 17 00:00:00 2001 From: muhzain05 Date: Thu, 8 Oct 2026 10:22:26 -0600 Subject: [PATCH] fix(ult): reject finalize with outstanding ulthreads sceUltFinalize cleared gUlthreads directly, dropping the last shared_ptr to an UltUlthreadState while its worker thread still held the raw pointer passed to scePthreadCreate. Finalize now returns ULT_ERROR_BUSY when an ulthread is still registered, before any global state is torn down, following the BUSY convention of the queue, semaphore and queue data pool destroy paths. Joining the ulthread remains the caller's responsibility, as sceUltUlthreadJoin already keeps the state alive across scePthreadJoin and erases it afterwards. --- core/libs/prx/libSceUlt/CMakeLists.txt | 13 ++++ core/libs/prx/libSceUlt/Export.cpp | 4 +- core/libs/tests/GuestUltThread.cpp | 94 ++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestUltThread.cpp diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 56c1216ca..e9ce2a507 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -15,4 +15,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + + add_test_executable(guest_ult_thread_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltThread.cpp) + target_include_directories(guest_ult_thread_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_thread_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_thread_tests) + foreach(testCase IN ITEMS outstanding validation) + add_test(NAME guest_ult_thread_${testCase} COMMAND guest_ult_thread_tests ${testCase}) + set_tests_properties(guest_ult_thread_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_thread_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index a224d3c35..979033e5a 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -123,6 +123,9 @@ int APS5_VABI sceUltFinalize() { std::vector> semaphores; { std::lock_guard lock(gMutex); + if (!gUlthreads.empty()) { + return ULT_ERROR_BUSY; + } semaphores.reserve(gSemaphores.size()); for (auto& entry : gSemaphores) { auto& state = entry.second; @@ -143,7 +146,6 @@ int APS5_VABI sceUltFinalize() { gQueues.clear(); gQueueDataPools.clear(); gRuntimes.clear(); - gUlthreads.clear(); } for (const auto& state : semaphores) { state->_available.notify_all(); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp new file mode 100644 index 000000000..e46308a40 --- /dev/null +++ b/core/libs/tests/GuestUltThread.cpp @@ -0,0 +1,94 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltInitialize(); +int APS5_VABI sceUltFinalize(); +std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); +} + +static constexpr int Ok = 0; +static constexpr int Null = -2139029503; +static constexpr int State = -2139029498; +static constexpr int Busy = -2139029497; + +static void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "ULT thread check failed at line %d\n", line); + std::abort(); + } +} + +#define CHECK(value) Require((value), __LINE__) + +static std::atomic gStarted{false}; +static std::atomic gRelease{false}; + +static std::int32_t APS5_VABI Parked(std::uint64_t arg) { + gStarted.store(true, std::memory_order_release); + while (!gRelease.load(std::memory_order_acquire)) std::this_thread::yield(); + return static_cast(arg); +} + +static std::int32_t APS5_VABI Immediate(std::uint64_t arg) { + return static_cast(arg); +} + +struct alignas(8) Runtime { std::uint8_t bytes[4096]{}; }; +struct alignas(8) Thread { std::uint8_t bytes[512]{}; }; + +static void Outstanding() { + Runtime runtime; + Thread thread; + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); + CHECK(sceUltUlthreadCreate(&thread, "parked", Parked, 0x5A, nullptr, 0, &runtime, nullptr, 0) == Ok); + while (!gStarted.load(std::memory_order_acquire)) std::this_thread::yield(); + + const int outstanding = sceUltFinalize(); + gRelease.store(true, std::memory_order_release); + std::int32_t status = 0; + const int joined = sceUltUlthreadJoin(&thread, &status); + + CHECK(outstanding == Busy); + CHECK(joined == Ok && status == 0x5A); + CHECK(sceUltUlthreadJoin(&thread, &status) == State); + CHECK(sceUltFinalize() == Ok); +} + +static void Validation() { + Runtime runtime; + Thread thread; + gStarted.store(false, std::memory_order_release); + gRelease.store(false, std::memory_order_release); + CHECK(sceUltUlthreadRuntimeCreate(nullptr, "runtime", 1, 1, nullptr, nullptr, 0) == Null); + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); + CHECK(sceUltUlthreadCreate(nullptr, "thread", Immediate, 0, nullptr, 0, &runtime, nullptr, 0) == Null); + CHECK(sceUltUlthreadCreate(&thread, "thread", nullptr, 0, nullptr, 0, &runtime, nullptr, 0) == Null); + CHECK(sceUltUlthreadCreate(&thread, "thread", Immediate, 0, nullptr, 0, nullptr, nullptr, 0) == Null); + CHECK(sceUltUlthreadJoin(nullptr, nullptr) == Null); + CHECK(sceUltUlthreadJoin(&thread, nullptr) == State); + + CHECK(sceUltUlthreadCreate(&thread, "thread", Immediate, 0x27, nullptr, 0, &runtime, nullptr, 0) == Ok); + CHECK(sceUltUlthreadCreate(&thread, "thread", Immediate, 0x27, nullptr, 0, &runtime, nullptr, 0) == State); + CHECK(sceUltFinalize() == Busy); + std::int32_t status = 0; + CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); + CHECK(sceUltFinalize() == Ok); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); +} + +int main(int argc, char** argv) { + CHECK(argc == 2); + CHECK(sceUltInitialize() == Ok); + if (std::strcmp(argv[1], "outstanding") == 0) Outstanding(); + else if (std::strcmp(argv[1], "validation") == 0) Validation(); + else CHECK(false); +}