Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1749,3 +1749,20 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
endif()

if(BUILD_TESTING)
add_test_executable(guest_sleep_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepCancel.cpp)
target_include_directories(guest_sleep_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_sleep_cancel_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_sleep_cancel_tests)
foreach(sleepCase IN ITEMS blocked disabled normal pending)
add_test(NAME guest_sleep_cancel_${sleepCase} COMMAND guest_sleep_cancel_tests ${sleepCase})
add_test(NAME guest_sleep_cancel_${sleepCase}_coarse COMMAND guest_sleep_cancel_tests ${sleepCase})
set_tests_properties(guest_sleep_cancel_${sleepCase} guest_sleep_cancel_${sleepCase}_coarse PROPERTIES TIMEOUT 20)
set_tests_properties(guest_sleep_cancel_${sleepCase}_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1")
if(WIN32)
set_tests_properties(guest_sleep_cancel_${sleepCase} guest_sleep_cancel_${sleepCase}_coarse PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
endforeach()
endif()
12 changes: 10 additions & 2 deletions core/libs/prx/libkernel/Pthread/Posix/Thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#include "Common.hpp"
#include "prx/libkernel/KernelErrors.hpp"
#include <atomic>
#include <chrono>
#include <mutex>
#include <cstring>
#include <thread>

Expand Down Expand Up @@ -301,7 +301,15 @@ void APS5_VABI pthread_yield_nid_postfix(void) {

unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds) {
ThreadCancel::Check();
std::this_thread::sleep_for(std::chrono::seconds(seconds));
{
const auto deadline = TimedWait::DeadlineNanos(static_cast<std::uint64_t>(seconds) * 1000000ULL);
std::mutex mutex;
TimedWait::Condition condition;
std::unique_lock lock(mutex);
while (!ThreadCancel::Requested()) {
if (!ThreadCancel::Wait(condition, lock, deadline)) break;
}
}
ThreadCancel::Check();
return 0;
}
Expand Down
108 changes: 108 additions & 0 deletions core/libs/tests/GuestSleepCancel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#include "SceTypes.hpp"
#include "prx/libkernel/Pthread/include/Pthread.hpp"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <source_location>
#include <string_view>
#include <thread>

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** result);
int APS5_VABI scePthreadCancel(Pthread thread);
int APS5_VABI scePthreadSetcancelstate(int state, int* oldState);
void APS5_VABI scePthreadTestcancel();
unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds);
}

static void Require(bool value, std::source_location location = std::source_location::current()) {
if (value) return;
std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line());
std::abort();
}

static constexpr auto ObservationLimit = std::chrono::seconds(3);
static constexpr unsigned LongSleep = 10;
static std::atomic<bool> ready{false};
static std::atomic<bool> release{false};
static std::atomic<bool> returned{false};
static bool disabled = false;
static bool pending = false;
static unsigned seconds = LongSleep;
static std::chrono::steady_clock::duration elapsed{};

static void* APS5_VABI Sleeper(void*) {
if (disabled) Require(scePthreadSetcancelstate(1, nullptr) == 0);
ready.store(true);
ready.notify_all();
if (pending) release.wait(false);
const auto start = std::chrono::steady_clock::now();
Require(sleep_nid_postfix(seconds) == 0);
elapsed = std::chrono::steady_clock::now() - start;
returned.store(true);
if (disabled) {
Require(scePthreadSetcancelstate(0, nullptr) == 0);
scePthreadTestcancel();
Require(false);
}
return nullptr;
}

static bool WaitForCompletion(Pthread thread) {
const auto limit = std::chrono::steady_clock::now() + ObservationLimit;
while (!thread->_finished.load(std::memory_order_acquire)) {
if (std::chrono::steady_clock::now() >= limit) return false;
std::this_thread::yield();
}
return true;
}

static bool WaitForSleep(Pthread thread) {
const auto limit = std::chrono::steady_clock::now() + ObservationLimit;
for (;;) {
{
std::lock_guard lock(thread->cancelLock);
if (thread->cancelWait != nullptr) return true;
}
if (std::chrono::steady_clock::now() >= limit) return false;
std::this_thread::yield();
}
}

int main(int count, char** arguments) {
Require(count == 2);
const std::string_view mode(arguments[1]);
disabled = mode == "disabled";
const bool zero = mode == "pending";
pending = zero || disabled;
const bool normal = mode == "normal";
Require(mode == "blocked" || disabled || pending || normal);
if (disabled || normal) seconds = 1;
if (zero) seconds = 0;
Pthread thread = nullptr;
Require(scePthreadCreate(&thread, nullptr, Sleeper, nullptr, nullptr) == 0);
ready.wait(false);
bool wasSleeping = false;
if (!normal) {
if (!pending) wasSleeping = WaitForSleep(thread);
Require(scePthreadCancel(thread) == 0);
release.store(true);
release.notify_all();
}
const bool completed = WaitForCompletion(thread);
void* result = nullptr;
Require(scePthreadJoin(thread, &result) == 0);
if (!completed) std::fputs("sleep stayed blocked after cancellation\n", stderr);
Require(completed);
if (normal) {
Require(result == nullptr && returned.load());
} else {
Require(result == reinterpret_cast<void*>(std::uintptr_t{1}));
Require(returned.load() == disabled);
if (!pending) Require(wasSleeping);
}
if (normal || disabled) Require(elapsed >= std::chrono::seconds(1));
}
2 changes: 1 addition & 1 deletion docs/dev/debt/libkernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
- [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
- [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it
- [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`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. 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. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. Detaching a thread while another thread waits to join it returns `EINVAL`, and a second concurrent join returns `EOPNOTSUPP`, as FreeBSD 11.4's [`thr_detach.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_detach.c) and [`thr_join.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_join.c) check `joiner`; before this PPSA34015 stopped once with `Joining guest thread: Access is denied` when a thread it was joining was released under the join, and which of the two calls released it was not captured. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory
- [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.
- [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. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep 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) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. 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. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured
- [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. 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
- [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified
Expand Down
Loading