#include "prx/libSceUlt/include/Api.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include #include #include #include #include #include #include #include #include #include "../prx/libSceUlt/Export.cpp" namespace { void Require(bool value, int line) { if (value) return; std::fprintf(stderr, "ULT lifecycle check failed at line %d\n", line); std::abort(); } #define CHECK(value) Require((value), __LINE__) struct alignas(8) Runtime { std::byte bytes[4096]{}; }; struct alignas(8) Thread { std::byte bytes[512]{}; }; struct HostThread { std::thread thread; void* result = nullptr; std::uint64_t generation = 0; }; std::atomic entryFinished{false}; std::atomic joinCalls{0}; std::atomic hostThreads{0}; std::binary_semaphore joining{0}; std::binary_semaphore releaseJoin{0}; bool gateJoin = false; int createFailure = 0; int joinFailure = 0; int sentinelExecutions = 0; void APS5_VABI Sentinel(void*) { ++sentinelExecutions; } #ifdef ULT_LIFECYCLE_INTERPOSE thread_local int actor = 0; std::atomic pauseSelection{false}; std::atomic failCleanupSetup{false}; std::atomic originalGeneration{0}; std::binary_semaphore selected{0}; std::binary_semaphore releaseSelection{0}; std::binary_semaphore creating{0}; std::binary_semaphore releaseCreate{0}; std::binary_semaphore registryAttempt{0}; std::binary_semaphore releaseRegistryAttempt{0}; std::binary_semaphore replacementStarted{0}; std::binary_semaphore releaseReplacement{0}; bool gateCreate = false; #endif void Await(std::binary_semaphore& gate) { CHECK(gate.try_acquire_for(std::chrono::seconds(2))); } std::int32_t APS5_VABI Entry(std::uint64_t argument) { void* self = nullptr; CHECK(sceUltUlthreadGetSelf(&self) == ULT_OK && self != nullptr); CHECK(sceUltUlthreadYield() == ULT_OK); return static_cast(argument); } void Create(Runtime& runtime, Thread& thread) { CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == ULT_OK); CHECK(sceUltUlthreadCreate(&thread, "thread", Entry, 73, nullptr, 0, &runtime, nullptr, 0) == ULT_OK); entryFinished.wait(false); } void Finish(Runtime& runtime, Thread& thread) { std::int32_t status = -1; CHECK(sceUltUlthreadJoin(&thread, &status) == ULT_OK && status == 73); CHECK(hostThreads.load() == 0); CHECK(sceUltUlthreadJoin(&thread, &status) == ULT_ERROR_STATE && status == 73); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == ULT_OK); CHECK(sceUltFinalize() == ULT_OK); } void CreateFailure(int failure) { Runtime runtime; Thread thread; CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == ULT_OK); createFailure = failure; bool threw = false; int result = ULT_OK; try { result = sceUltUlthreadCreate(&thread, "thread", Entry, 73, nullptr, 0, &runtime, nullptr, 0); } catch (const std::runtime_error&) { threw = true; } CHECK(threw == (failure != 1)); if (failure == 1) CHECK(result == ULT_ERROR_AGAIN); if (failure == 3) { CHECK(sceUltFinalize() == ULT_ERROR_BUSY); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == ULT_ERROR_STATE); Finish(runtime, thread); return; } CHECK(hostThreads.load() == 0 && !entryFinished.load()); CHECK(sceUltUlthreadTryJoin(&thread, nullptr) == ULT_ERROR_STATE); CHECK(sceUltUlthreadCreate(&thread, "retry", Entry, 73, nullptr, 0, &runtime, nullptr, 0) == ULT_OK); Finish(runtime, thread); } void Ownership(bool nonblocking) { Runtime runtime; Thread thread; Create(runtime, thread); gateJoin = true; std::int32_t firstStatus = -1; int firstResult = ULT_ERROR_STATE; std::thread first([&] { firstResult = nonblocking ? sceUltUlthreadTryJoin(&thread, &firstStatus) : sceUltUlthreadJoin(&thread, &firstStatus); }); Await(joining); std::int32_t secondStatus = -2; CHECK(sceUltUlthreadJoin(&thread, &secondStatus) == ULT_ERROR_STATE && secondStatus == -2); CHECK(sceUltUlthreadTryJoin(&thread, &secondStatus) == ULT_ERROR_STATE && secondStatus == -2); CHECK(joinCalls.load() == 1); CHECK(sceUltUlthreadCreate(&thread, "replacement", Entry, 74, nullptr, 0, &runtime, nullptr, 0) == ULT_ERROR_STATE); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == ULT_ERROR_STATE); CHECK(sceUltFinalize() == ULT_ERROR_BUSY); releaseJoin.release(); first.join(); CHECK(firstResult == ULT_OK && firstStatus == 73 && hostThreads.load() == 0); CHECK(sceUltUlthreadTryJoin(&thread, &secondStatus) == ULT_ERROR_STATE && secondStatus == -2); gateJoin = false; entryFinished.store(false); CHECK(sceUltUlthreadCreate(&thread, "replacement", Entry, 73, nullptr, 0, &runtime, nullptr, 0) == ULT_OK); Finish(runtime, thread); } void JoinFailure(int failure) { Runtime runtime; Thread thread; Create(runtime, thread); void* const before = ThreadCancel::CleanupMark(); if (failure == 3) pthread_cleanup_push_nid_postfix(Sentinel, nullptr); joinFailure = failure; std::int32_t status = -1; bool threw = false; int result = ULT_OK; try { result = sceUltUlthreadJoin(&thread, &status); } catch (const std::runtime_error&) { threw = true; } CHECK(threw == (failure >= 2)); if (!threw) CHECK(result == ULT_ERROR_STATE); CHECK(status == -1 && hostThreads.load() == 1); if (failure == 3) { CHECK(sentinelExecutions == 1 && ThreadCancel::CleanupMark() == before); std::thread replacement([&] { Finish(runtime, thread); }); replacement.join(); } else Finish(runtime, thread); } #ifdef ULT_LIFECYCLE_INTERPOSE void Publication() { Runtime runtime; Thread thread; CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == ULT_OK); gateCreate = true; std::thread creator([&] { CHECK(sceUltUlthreadCreate(&thread, "thread", Entry, 73, nullptr, 0, &runtime, nullptr, 0) == ULT_OK); }); Await(creating); std::int32_t status = -1; std::thread joiner([&] { actor = 2; CHECK(sceUltUlthreadJoin(&thread, &status) == ULT_OK); }); Await(registryAttempt); const bool registryAvailable = gMutex.try_lock(); if (registryAvailable) gMutex.unlock(); CHECK(!registryAvailable && joinCalls.load() == 0); releaseRegistryAttempt.release(); releaseCreate.release(); creator.join(); joiner.join(); CHECK(status == 73 && hostThreads.load() == 0); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == ULT_OK); CHECK(sceUltFinalize() == ULT_OK); } std::int32_t APS5_VABI Replacement(std::uint64_t argument) { replacementStarted.release(); Await(releaseReplacement); return static_cast(argument); } void Generation() { Runtime runtime; Thread thread; Create(runtime, thread); pauseSelection.store(true); std::int32_t originalStatus = -1; int originalResult = ULT_ERROR_STATE; std::thread original([&] { actor = 1; originalResult = sceUltUlthreadTryJoin(&thread, &originalStatus); }); Await(selected); std::int32_t contenderStatus = -2; const int contender = sceUltUlthreadJoin(&thread, &contenderStatus); if (contender == ULT_OK) { CHECK(contenderStatus == 73); CHECK(sceUltUlthreadCreate(&thread, "replacement", Replacement, 92, nullptr, 0, &runtime, nullptr, 0) == ULT_OK); Await(replacementStarted); } else { CHECK(contender == ULT_ERROR_STATE && contenderStatus == -2); CHECK(sceUltUlthreadCreate(&thread, "replacement", Replacement, 92, nullptr, 0, &runtime, nullptr, 0) == ULT_ERROR_STATE); } releaseSelection.release(); original.join(); CHECK(originalGeneration.load() == 73 && originalResult == ULT_OK && originalStatus == 73); CHECK(contender == ULT_ERROR_STATE && hostThreads.load() == 0); CHECK(sceUltUlthreadCreate(&thread, "replacement", Replacement, 92, nullptr, 0, &runtime, nullptr, 0) == ULT_OK); Await(replacementStarted); CHECK(sceUltUlthreadTryJoin(&thread, &contenderStatus) == ULT_ERROR_AGAIN && contenderStatus == -2); releaseReplacement.release(); CHECK(sceUltUlthreadJoin(&thread, &contenderStatus) == ULT_OK && contenderStatus == 92); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == ULT_OK); CHECK(sceUltFinalize() == ULT_OK); } void CleanupSetup() { Runtime runtime; Thread thread; Create(runtime, thread); failCleanupSetup.store(true); std::int32_t status = -1; bool threw = false; try { sceUltUlthreadJoin(&thread, &status); } catch (const std::runtime_error&) { threw = true; } CHECK(threw && status == -1 && joinCalls.load() == 0); std::thread replacement([&] { Finish(runtime, thread); }); replacement.join(); } #endif } extern "C" { #ifdef ULT_LIFECYCLE_INTERPOSE int __real_pthread_mutex_lock(pthread_mutex_t* mutex); int __real_pthread_mutex_unlock(pthread_mutex_t* mutex); void APS5_VABI __real___pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); int __wrap_pthread_mutex_lock(pthread_mutex_t* mutex) { if (actor == 2 && mutex == gMutex.native_handle()) { actor = 0; registryAttempt.release(); Await(releaseRegistryAttempt); } return __real_pthread_mutex_lock(mutex); } int __wrap_pthread_mutex_unlock(pthread_mutex_t* mutex) { const int result = __real_pthread_mutex_unlock(mutex); if (actor == 1 && mutex == gMutex.native_handle() && pauseSelection.exchange(false)) { selected.release(); Await(releaseSelection); } return result; } void APS5_VABI __wrap___pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info) { if (failCleanupSetup.exchange(false)) throw std::runtime_error("Injected cleanup setup failure"); __real___pthread_cleanup_push_imp_nid_postfix(routine, argument, info); } #endif int APS5_VABI scePthreadCreate(Pthread* value, const PthreadAttr*, PthreadEntry entry, void* argument, const char*) { const int failure = createFailure; createFailure = 0; if (failure == 1) return 11; if (failure == 2) throw std::runtime_error("Injected prepublication creation failure"); #ifdef ULT_LIFECYCLE_INTERPOSE if (gateCreate) { gateCreate = false; creating.release(); Await(releaseCreate); } #endif auto state = std::make_unique(); state->generation = static_cast(argument)->_arg; auto* raw = state.get(); state->thread = std::thread([=] { raw->result = entry(argument); entryFinished.store(true); entryFinished.notify_all(); }); ++hostThreads; *value = reinterpret_cast(state.release()); if (failure == 3) throw std::runtime_error("Injected postpublication creation failure"); return 0; } int APS5_VABI scePthreadJoin(Pthread value, void** result) { const int call = joinCalls.fetch_add(1); #ifdef ULT_LIFECYCLE_INTERPOSE if (actor == 1) { CHECK(value != nullptr); const auto generation = reinterpret_cast(value)->generation; originalGeneration.store(generation); if (generation != 73) return 22; } #endif if (gateJoin) { if (call != 0) return 22; joining.release(); Await(releaseJoin); } const int failure = joinFailure; joinFailure = 0; if (failure == 1) return 22; if (failure == 2) throw std::runtime_error("Injected precommit join failure"); if (failure == 3) { __pthread_cleanup_pop_imp_nid_postfix(1); __pthread_cleanup_pop_imp_nid_postfix(1); throw std::runtime_error("Injected failure after cancellation cleanup"); } CHECK(value != nullptr); std::unique_ptr state(reinterpret_cast(value)); state->thread.join(); if (result != nullptr) *result = state->result; --hostThreads; return 0; } } int main(int argc, char** argv) { CHECK(argc == 2); if (std::strcmp(argv[1], "create-error") == 0) CreateFailure(1); else if (std::strcmp(argv[1], "create-exception") == 0) CreateFailure(2); else if (std::strcmp(argv[1], "published-exception") == 0) CreateFailure(3); else if (std::strcmp(argv[1], "join-owner") == 0) Ownership(false); else if (std::strcmp(argv[1], "tryjoin-owner") == 0) Ownership(true); else if (std::strcmp(argv[1], "join-error") == 0) JoinFailure(1); else if (std::strcmp(argv[1], "join-exception") == 0) JoinFailure(2); else if (std::strcmp(argv[1], "join-cleanup-exception") == 0) JoinFailure(3); #ifdef ULT_LIFECYCLE_INTERPOSE else if (std::strcmp(argv[1], "publication") == 0) Publication(); else if (std::strcmp(argv[1], "generation") == 0) Generation(); else if (std::strcmp(argv[1], "cleanup-setup") == 0) CleanupSetup(); #endif else CHECK(false); }