diff --git a/core/libs/prx/libkernel/Socket/src/Pselect.cpp b/core/libs/prx/libkernel/Socket/src/Pselect.cpp index 931cc83a54..66996aadfa 100644 --- a/core/libs/prx/libkernel/Socket/src/Pselect.cpp +++ b/core/libs/prx/libkernel/Socket/src/Pselect.cpp @@ -1,32 +1,76 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include extern "C" { int* APS5_VABI __error_nid_postfix(); int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); +int APS5_VABI SetSignalMaskCatching_nid_no_patch(const void* set, void* previousSet); + +} + +namespace { + +constexpr int SetMask = 3; +constexpr int Eintr = 4; +constexpr int DescriptorLimit = 1024; + +struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +using SavedSet = std::array; + +void Save(SavedSet& saved, const void* set, std::size_t bytes) { + if (set != nullptr) std::memcpy(saved.data(), set, bytes); +} + +void Restore(void* set, const SavedSet& saved, std::size_t bytes) { + if (set != nullptr) std::memcpy(set, saved.data(), bytes); +} + +int ScanAfterCaughtSignal(int nfds, void* readfds, void* writefds, void* exceptfds, const Timeval* wait) { + const bool validWait = wait == nullptr || (wait->seconds >= 0 && wait->microseconds >= 0 && wait->microseconds < 1000000); + if (nfds < 0 || nfds > DescriptorLimit || !validWait) return select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + const std::size_t bytes = static_cast((nfds + 63) / 64) * sizeof(std::uint64_t); + SavedSet read{}, write{}, except{}; + Save(read, readfds, bytes); + Save(write, writefds, bytes); + Save(except, exceptfds, bytes); + const Timeval zero{}; + const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, &zero); + if (result != 0 || (wait != nullptr && wait->seconds == 0 && wait->microseconds == 0)) return result; + Restore(readfds, read, bytes); + Restore(writefds, write, bytes); + Restore(exceptfds, except, bytes); + *__error_nid_postfix() = Eintr; + return -1; +} + +} + +extern "C" { int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { - constexpr int SetMask = 3; - struct Timeval { - std::int64_t seconds; - std::int64_t microseconds; - } limit{}; + Timeval limit{}; const Timeval* wait = nullptr; if (timeout != nullptr) { const auto* spec = static_cast(timeout); limit = {spec[0], spec[1] / 1000}; wait = &limit; } + if (sigmask == nullptr) return select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); std::uint32_t previous[4]{}; - if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; - const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); - if (sigmask != nullptr) { - const int error = *__error_nid_postfix(); - sigprocmask_nid_postfix(SetMask, previous, nullptr); - *__error_nid_postfix() = error; - } + const bool caught = SetSignalMaskCatching_nid_no_patch(sigmask, previous) != 0; + const int result = caught ? ScanAfterCaughtSignal(nfds, readfds, writefds, exceptfds, wait) + : select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + const int error = *__error_nid_postfix(); + sigprocmask_nid_postfix(SetMask, previous, nullptr); + *__error_nid_postfix() = error; return result; } diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 0137030675..2a5b941662 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -478,6 +478,45 @@ namespace { constexpr int SsDisable = 4; constexpr std::size_t MinSignalStackSize = 2048; thread_local GuestStack alternateStack{nullptr, 0, SsDisable}; + +int ChangeSignalMask(int how, const GuestSignalSet* set, GuestSignalSet* previousSet, bool* caught) { + if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } + { + std::lock_guard lock(registration); + if (previousSet != nullptr) { + previousSet->bits[0] = blockedMask.load(); + for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); + } + if (set != nullptr) { + switch (how) { + case 1: + blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + break; + case 3: + blockedMask.store(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + break; + } + } + } + std::atomic_thread_fence(std::memory_order_seq_cst); + for (int guest = 1; guest <= MaxSignal; ++guest) { + if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; + if (DeliverPendingSignal(guest) && caught != nullptr) *caught = true; + } + return 0; +} +} + +extern "C" int APS5_VABI SetSignalMaskCatching_nid_no_patch(const GuestSignalSet* set, GuestSignalSet* previousSet) { + bool caught = false; + ChangeSignalMask(GuestSigsetmask, set, previousSet, &caught); + return caught ? 1 : 0; } extern "C" { @@ -560,36 +599,7 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } - { - std::lock_guard lock(registration); - if (previousSet != nullptr) { - previousSet->bits[0] = blockedMask.load(); - for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); - } - if (set != nullptr) { - switch (how) { - case 1: - blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); - break; - case 2: - blockedMask.fetch_and(~set->bits[0]); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); - break; - case 3: - blockedMask.store(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); - break; - } - } - } - std::atomic_thread_fence(std::memory_order_seq_cst); - for (int guest = 1; guest <= MaxSignal; ++guest) { - if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; - DeliverPendingSignal(guest); - } - return 0; + return ChangeSignalMask(how, set, previousSet, nullptr); } int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet) { diff --git a/core/libs/tests/GuestKernelSysPselect.cpp b/core/libs/tests/GuestKernelSysPselect.cpp index e3924a7f43..e3fbee46ed 100644 --- a/core/libs/tests/GuestKernelSysPselect.cpp +++ b/core/libs/tests/GuestKernelSysPselect.cpp @@ -15,10 +15,14 @@ int APS5_VABI close_nid_postfix(int descriptor); std::int64_t APS5_VABI write_nid_postfix(int descriptor, const char* buffer, std::int64_t size); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask); +using Handler = void (APS5_VABI *)(int); +Handler APS5_VABI signal_nid_postfix(int guest, Handler handler); +int APS5_VABI raise_nid_postfix(int guest); } namespace { +constexpr int Eintr = 4; constexpr int Einval = 22; constexpr int SigBlock = 1; constexpr int SigSetmask = 3; @@ -59,6 +63,17 @@ bool Same(const SignalSet& left, const SignalSet& right) { return std::memcmp(left.bits, right.bits, sizeof(left.bits)) == 0; } +int caught = 0; + +void APS5_VABI Count(int) { + ++caught; +} + +void RaiseBlocked(const SignalSet& blocked) { + Require(sigprocmask_nid_postfix(SigBlock, &blocked, nullptr) == 0); + Require(raise_nid_postfix(SigUsr1) == 0 && caught == 0); +} + } int main() { @@ -101,6 +116,60 @@ int main() { const Timespec zero{0, 0}; Require(pselect_nid_postfix(-1, nullptr, nullptr, nullptr, &zero, nullptr) == -1 && Error() == Einval); } + Require(signal_nid_postfix(SigUsr1, Count) != reinterpret_cast(static_cast(-1))); + const SignalSet empty{}; + { + RaiseBlocked(blocked); + const SignalSet withUsr1 = CurrentMask(); + DescriptorSet read{}; + read.Add(writer); + const DescriptorSet original = read; + const Timespec limit{0, 300000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(writer + 1, read.words.data(), nullptr, nullptr, &limit, &empty) == -1 && Error() == Eintr); + Require(std::chrono::steady_clock::now() - start < std::chrono::milliseconds(150)); + Require(caught == 1); + Require(read.words == original.words); + Require(Same(CurrentMask(), withUsr1)); + caught = 0; + } + { + RaiseBlocked(blocked); + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, nullptr, &empty) == -1 && Error() == Eintr && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + DescriptorSet read{}; + read.Add(reader); + const Timespec limit{0, 300000000}; + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit, &empty) == 1 && caught == 1); + Require(read.Has(reader)); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec zero{0, 0}; + DescriptorSet read{}; + read.Add(writer); + Require(pselect_nid_postfix(writer + 1, read.words.data(), nullptr, nullptr, &zero, &empty) == 0 && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec invalid{0, 1000000000}; + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &invalid, &empty) == -1 && Error() == Einval && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec limit{0, 200000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &limit, &blocked) == 0 && caught == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + Require(sigprocmask_nid_postfix(SigSetmask, &empty, nullptr) == 0 && caught == 1); + caught = 0; + } Require(sigprocmask_nid_postfix(SigSetmask, &before, nullptr) == 0); Require(close_nid_postfix(reader) == 0 && close_nid_postfix(writer) == 0); std::puts("pselect passed"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 63512e62d1..b3200f639e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,7 +158,7 @@ - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store - [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified -- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it +- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only