From 03722a1570207ba149a3ba8bf249588b1e7a7f46 Mon Sep 17 00:00:00 2001 From: Oscar45397 <238734290+Oscar45397@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:04:57 +0100 Subject: [PATCH] fix(libkernel): deliver raises at the spin that ends a sleep on Windows On Windows a sleep shorter than TimedWait's 0.5 ms lead, and the last 0.5 ms of a longer one, is a YieldProcessor spin in host code with no wait counted. A raise that found the target there could only be delivered through the return-address breakpoint or sampling, so sceKernelRaiseException at a thread looping on such sleeps was lost: a thread looping on sceKernelUsleep(200) lost the first raise in 5 of 5 runs. The spins in SleepUntil and SleepNanosUnlessChanged now run under a DeliveryDeferral and drain APCs afterwards, as BeginPark and Unpark already do. A raise that finds the target spinning is queued as an APC and runs when the sleep ends. GuestRaiseShortSleep raises 100 times each at a thread sleeping 100, 200 and 400 us in a loop, with sleep wakes on (SleepNanosUnlessChanged) and with APS5_NO_SLEEP_WAKE (SleepUntil). Both fail on main at the first or second raise. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ core/libs/prx/libkernel/Time/TimedWait.cpp | 28 +++++++--- core/libs/tests/GuestRaiseShortSleep.cpp | 64 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 4 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 core/libs/tests/GuestRaiseShortSleep.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index db4512f3ec..8dab563a2d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -2276,6 +2276,16 @@ if(BUILD_TESTING) add_test(NAME guest_raise_leaves_host_code COMMAND guest_raise_leaves_host_code_tests) set_tests_properties(guest_raise_leaves_host_code PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_raise_short_sleep_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseShortSleep.cpp) + target_include_directories(guest_raise_short_sleep_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_short_sleep_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_short_sleep_tests) + add_test(NAME guest_raise_short_sleep COMMAND guest_raise_short_sleep_tests) + set_tests_properties(guest_raise_short_sleep PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + add_test(NAME guest_raise_short_sleep_without_wake COMMAND guest_raise_short_sleep_tests) + set_tests_properties(guest_raise_short_sleep_without_wake PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_NO_SLEEP_WAKE=1" ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_mutex_contention_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexContention.cpp) target_include_directories(guest_mutex_contention_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mutex_contention_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index d79399e8e4..95f3550425 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -530,10 +530,14 @@ void SleepUntil(std::uint64_t deadlineNanos) { if (!TimerWait(timer, deadlineNanos - now - LEAD_NANOS)) AlertableSleep(WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); now = NowNanos(); } - while (now < deadlineNanos) { - YieldProcessor(); - now = NowNanos(); + { + DeliveryDeferral deferral; + while (now < deadlineNanos) { + YieldProcessor(); + now = NowNanos(); + } } + DrainApcs(); #else const std::uint64_t now = NowNanos(); if (deadlineNanos > now) SleepNanos(deadlineNanos - now); @@ -580,12 +584,20 @@ bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic +#include +#include +#include +#include + +static constexpr int SigUsr1 = 30; +static constexpr int Raises = 100; +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic deliveries{0}; +static std::atomic stop{false}; +static std::atomic naps{0}; +static std::atomic napMicros{0}; + +static void APS5_VABI Handler(int, void*) { deliveries.fetch_add(1); } + +static void* APS5_VABI Napper(void*) { + while (!stop.load()) { + sceKernelUsleep_nid_postfix(napMicros.load()); + naps.fetch_add(1); + } + return nullptr; +} + +static void RaiseAtNapper(KernelUseconds micros) { + napMicros.store(micros); + stop.store(false); + naps.store(0); + Pthread napper = nullptr; + Require(scePthreadCreate(&napper, nullptr, Napper, nullptr, "napper") == 0); + while (naps.load() == 0) std::this_thread::yield(); + for (int raise = 0; raise < Raises; ++raise) { + const int before = deliveries.load(); + Require(sceKernelRaiseException(napper, SigUsr1) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (deliveries.load() == before) { + if (std::chrono::steady_clock::now() >= deadline) { + std::fprintf(stderr, "raise %d at a thread sleeping %u us in a loop was not delivered within 3 s (%llu sleeps since)\n", raise, micros, naps.load()); + std::abort(); + } + std::this_thread::yield(); + } + } + stop.store(true); + Require(scePthreadJoin(napper, nullptr) == 0); +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 20000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + for (const KernelUseconds micros : {100u, 200u, 400u}) RaiseAtNapper(micros); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::printf("%d raises delivered\n", deliveries.load()); + done.store(true); + watchdog.join(); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c5a61ec3c5..6f1a6353f8 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -214,7 +214,7 @@ - On macOS the guest runs on a second thread and the main thread serves the main dispatch queue from its run loop: AppKit (SDL's Cocoa windows, Metal surfaces, event pumping and game controllers) only works on the main thread, and a guest never hands its thread back. The macOS entry stub of the relinker starts the guest through `Aps5StartGuest` ([MainThreadMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp)), on a thread with the stack size of the main thread (at least 8 MiB), and the guest ends the process with `exit`; a guest entry that returns aborts. libSceVideoOut sends its SDL calls to the main thread with `Aps5RunOnMainThread` (`OnMainThread` in [MainThread.hpp](../../../core/libs/prx/libSceVideoOut/include/MainThread.hpp)), which waits for them and rethrows their exceptions; a host program that never called `Aps5StartGuest` runs them in place - On macOS the Rosetta main stack and the stacks of the host threads lie inside the guest arena, where a title can map fixed addresses. `Aps5StartGuest` moves the main thread onto a 16 MiB stack at `0x600000000000`, copies `argv`, `environ`, the program name and the initial block off the old stack, and releases the old stack range to the arena ([MainThreadMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp)). The guest thread and the other host threads still take their stacks from the system, so a title that fixes a mapping over one of those stacks fails with a host region overlap. The move is skipped when the main stack lies outside the arena, when the new stack cannot be placed outside it, and on hosts that are not x86-64 - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs a queued delivery (an APC) in the first alertable wait of libkernel's TimedWait the target enters, or where libkernel drains APCs after releasing its address-wait or park queue lock (both are taken under a delivery deferral). Host code that holds a lock of its own across such a wait keeps it held while a waiting handler runs +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs a queued delivery (an APC) in the first alertable wait of libkernel's TimedWait the target enters, or where libkernel drains APCs after releasing its address-wait or park queue lock or after the spin that ends a sleep (each is taken under a delivery deferral). Host code that holds a lock of its own across such a wait keeps it held while a waiting handler runs - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) leaves the signal pending on a target thread that was created suspended and is not resumed yet, and delivers it on that thread once it is resumed, before its start routine; further raises while it is pending are merged into that one delivery. FreeBSD 11.4 libthr starts such a thread with every signal but `SIGTRAP` blocked ([`thr_create.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L177-L183)) and restores its mask only once it is resumed ([`thread_start`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L252-L294)), so a signal sent to it stays pending until then. The console's behaviour is unverified - [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. PPSA99420 imports it