fix(libkernel): join the calling thread and create from an unset attribute as the console does

On a PS5 (firmware 13.60) scePthreadJoin on the calling thread returns
EDEADLK and scePthreadCreate with a zeroed attribute creates a thread
with the default attributes. Here both threw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hugodearaujo 2026-10-11 21:09:08 -03:00
parent bca7a29d25
commit 0b7e4ddf9f
4 changed files with 52 additions and 3 deletions

View File

@ -507,6 +507,16 @@ if(BUILD_TESTING)
set_tests_properties(guest_pthread_rwlock PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_pthread_join_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadJoinSelf.cpp)
target_include_directories(guest_pthread_join_self_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_pthread_join_self_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_pthread_join_self_tests)
add_test(NAME guest_pthread_join_self COMMAND guest_pthread_join_self_tests)
set_tests_properties(guest_pthread_join_self PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_pthread_join_self PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_pthread_key_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadKey.cpp)
target_include_directories(guest_pthread_key_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_pthread_key_tests PRIVATE libkernel libc)

View File

@ -533,7 +533,7 @@ extern "C" {
int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) {
if (!thread || !entry) throw std::runtime_error("scePthreadCreate: null arg");
if (attr && !*attr) throw std::runtime_error("scePthreadCreate: null attributes");
if (attr && !*attr) attr = nullptr;
auto p = std::make_unique<PthreadPrivate>(attr ? *attr : nullptr);
bool detached = false;
if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED);
@ -620,8 +620,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread
int APS5_VABI scePthreadJoin(Pthread thread, void** retval) {
if (!thread) throw std::runtime_error("scePthreadJoin: null thread");
if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL;
if (thread == CurrentGuestState().current)
throw std::runtime_error("scePthreadJoin: cannot join current thread");
if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK;
ThreadCancel::Check();
{
std::unique_lock lock(thread->_join_mtx);

View File

@ -0,0 +1,39 @@
#include "SceTypes.hpp"
#include <cstdint>
#include <cstdlib>
extern "C" {
int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name);
int APS5_VABI scePthreadJoin(Pthread thread, void** retval);
Pthread APS5_VABI scePthreadSelf(void);
int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval);
}
static constexpr int SCE_OK = 0;
static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast<int>(0x8002000Bu);
static constexpr int GUEST_EDEADLK = 11;
static void Require(bool value) { if (!value) std::abort(); }
static void* APS5_VABI JoinSelf(void*) {
void* result = reinterpret_cast<void*>(std::uintptr_t{0x55});
Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK);
Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK);
Require(result == reinterpret_cast<void*>(std::uintptr_t{0x55}));
return reinterpret_cast<void*>(std::uintptr_t{0x1234});
}
int main() {
Pthread thread = nullptr;
void* result = nullptr;
Require(scePthreadCreate(&thread, nullptr, JoinSelf, nullptr, "join-self") == SCE_OK);
Require(scePthreadJoin(thread, &result) == SCE_OK);
Require(result == reinterpret_cast<void*>(std::uintptr_t{0x1234}));
PthreadAttr unset = nullptr;
thread = nullptr;
result = nullptr;
Require(scePthreadCreate(&thread, &unset, JoinSelf, nullptr, "unset-attr") == SCE_OK);
Require(scePthreadJoin(thread, &result) == SCE_OK);
Require(result == reinterpret_cast<void*>(std::uintptr_t{0x1234}));
}

View File

@ -447,6 +447,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceAgcDebugRaiseException](../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents
- [sigaltstack](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported
- [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`.
- [scePthreadJoin, scePthreadCreate](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console
- [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls.
- [cpuset_setaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set
- [cpuset_getaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified