diff --git a/core/libs/prx/libSceLibcInternal/UserContext.cpp b/core/libs/prx/libSceLibcInternal/UserContext.cpp index 9ef01bd612f..b65d3eef0ae 100644 --- a/core/libs/prx/libSceLibcInternal/UserContext.cpp +++ b/core/libs/prx/libSceLibcInternal/UserContext.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libSceLibcInternal/include/Api.hpp" #include "prx/libc/include/GuestStacks.hpp" +#include "prx/libc/include/JumpBuffer.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/System/include/UserContext.hpp" @@ -8,6 +9,7 @@ #include #include #include +#include namespace { @@ -98,6 +100,8 @@ void APS5_VABI makecontext_nid_postfix(UserUcontext* context, void (APS5_VABI* s machine.rbx = reinterpret_cast(stack); machine.rsp = reinterpret_cast(stack); machine.rip = reinterpret_cast(&Aps5MakecontextStart_nid_no_patch); + const auto identity = JumpBuffer::GuestContextCreate_nid_no_patch(machine.rsp); + std::memcpy(machine.spare, &identity, sizeof(identity)); GuestStacks::GuestStackRegister_nid_no_patch(context->stackPointer, context->stackSize); } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 9e14d46d5eb..511028623ba 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1146,7 +1146,6 @@ int APS5_VABI sceNetSocketClose(int s) { if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); return close_nid_postfix(s) == 0 ? 0 : fail(*__error_nid_postfix()); } - KernelReleaseSocketNumber_nid_no_patch(s); if (native) { #ifdef _WIN32 ::shutdown(native->value, SD_BOTH); @@ -1154,6 +1153,7 @@ int APS5_VABI sceNetSocketClose(int s) { ::shutdown(native->value, SHUT_RDWR); #endif } + KernelReleaseSocketNumber_nid_no_patch(s); g_cv.NotifyAll(); return 0; } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 0c826bc9150..0811369d503 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -158,6 +158,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Mergesort.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostErrno.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileLock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DescriptorAccess.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StandardStreams.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StreamPurge.cpp diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 929bc87d422..8b373c2e5f7 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -8,6 +8,7 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/GuestErrno.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/JumpBuffer.hpp" #include "prx/libc/include/exceptions/Unwind.hpp" #include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/LegacySignals.hpp" diff --git a/core/libs/prx/libc/include/DescriptorAccess.hpp b/core/libs/prx/libc/include/DescriptorAccess.hpp new file mode 100644 index 00000000000..be2e20cbc1e --- /dev/null +++ b/core/libs/prx/libc/include/DescriptorAccess.hpp @@ -0,0 +1,123 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_DESCRIPTORACCESS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_DESCRIPTORACCESS_HPP + +#include +#include +#include +#include + +extern "C" { +void FileStreamSetCloseHookForTest_nid_no_patch(void (*hook)(int, int)); +bool FileStreamCloseHookActive_nid_no_patch(); +void FileStreamCloseEventForTest_nid_no_patch(int descriptor, int event); +void DescriptorAccessInitialize_nid_no_patch(void* access, int descriptor); +void DescriptorAccessLock_nid_no_patch(void* access); +bool DescriptorAccessTryLock_nid_no_patch(void* access); +void DescriptorAccessUnlock_nid_no_patch(void* access); +bool DescriptorAccessReadOrWait_nid_no_patch(void* access); +void DescriptorAccessWake_nid_no_patch(void* access) noexcept; +bool DescriptorVersionUnchanged_nid_no_patch(const void* version); +void DescriptorAccessSnapshot_nid_no_patch(const void* access, void* version); +void DescriptorOperationInitialize_nid_no_patch(void* operation, void* access); +void DescriptorOperationDestroy_nid_no_patch(void* operation); +bool DescriptorOperationInterrupted_nid_no_patch(); +void DescriptorWakeInitialize_nid_no_patch(void* registration); +void DescriptorWakeDestroy_nid_no_patch(void* registration); +} + +class GuestDescriptorAccess { + struct State; +public: + class Version { + public: + bool Unchanged() const { return DescriptorVersionUnchanged_nid_no_patch(this); } + private: + friend class GuestDescriptorAccess; + friend bool DescriptorVersionUnchanged_nid_no_patch(const void* version); + friend void DescriptorAccessSnapshot_nid_no_patch(const void* access, void* version); + Version() = default; + std::shared_ptr state; + std::uint64_t value = 0; + }; + enum class Mode { Read, Change, Update, Close }; + class Operation { + public: + explicit Operation(GuestDescriptorAccess& access) { DescriptorOperationInitialize_nid_no_patch(this, &access); } + ~Operation() { DescriptorOperationDestroy_nid_no_patch(this); } + Operation(const Operation&) = delete; + Operation& operator=(const Operation&) = delete; + bool Interrupted() const { return interrupted.load(); } + static bool CurrentInterrupted() { return DescriptorOperationInterrupted_nid_no_patch(); } + class WakeRegistration { + public: + WakeRegistration(void (*interrupt)(void*) noexcept, void* argument) + : interrupt(interrupt), argument(argument) { DescriptorWakeInitialize_nid_no_patch(this); } + ~WakeRegistration() { DescriptorWakeDestroy_nid_no_patch(this); } + WakeRegistration(const WakeRegistration&) = delete; + WakeRegistration& operator=(const WakeRegistration&) = delete; + private: + friend class Operation; + friend void DescriptorWakeInitialize_nid_no_patch(void* registration); + friend void DescriptorWakeDestroy_nid_no_patch(void* registration); + WakeRegistration* previous = nullptr; + Operation* operation = nullptr; + void (*interrupt)(void*) noexcept; + void* argument; + }; + private: + friend struct State; + friend void DescriptorOperationInitialize_nid_no_patch(void* operation, void* access); + friend void DescriptorOperationDestroy_nid_no_patch(void* operation); + friend bool DescriptorOperationInterrupted_nid_no_patch(); + friend void DescriptorWakeInitialize_nid_no_patch(void* registration); + friend void DescriptorWakeDestroy_nid_no_patch(void* registration); + static thread_local Operation* current; + std::shared_ptr state; + Operation* previous = nullptr; + WakeRegistration* wake = nullptr; + std::atomic interrupted{false}; + void Interrupt() noexcept; + }; + GuestDescriptorAccess(int descriptor, Mode mode) + : GuestDescriptorAccess(descriptor, mode, std::defer_lock) { Lock(); } + GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t) + : mode(mode) { DescriptorAccessInitialize_nid_no_patch(this, descriptor); } + ~GuestDescriptorAccess() { if (held) Unlock(); } + GuestDescriptorAccess(const GuestDescriptorAccess&) = delete; + GuestDescriptorAccess& operator=(const GuestDescriptorAccess&) = delete; + void Lock() { DescriptorAccessLock_nid_no_patch(this); } + bool TryLock() { return DescriptorAccessTryLock_nid_no_patch(this); } + void Unlock() { DescriptorAccessUnlock_nid_no_patch(this); } + bool ReadOrWait() { return DescriptorAccessReadOrWait_nid_no_patch(this); } + void Wake() noexcept { DescriptorAccessWake_nid_no_patch(this); } + Version Snapshot() const { + Version result; + DescriptorAccessSnapshot_nid_no_patch(this, &result); + return result; + } + static void LockTogether(GuestDescriptorAccess& first, GuestDescriptorAccess& second) { + struct Lockable { + GuestDescriptorAccess& access; + void lock() { access.Lock(); } + bool try_lock() { return access.TryLock(); } + void unlock() { access.Unlock(); } + } firstLock{first}, secondLock{second}; + std::lock(firstLock, secondLock); + } + +private: + friend void DescriptorAccessInitialize_nid_no_patch(void* access, int descriptor); + friend void DescriptorAccessLock_nid_no_patch(void* access); + friend bool DescriptorAccessTryLock_nid_no_patch(void* access); + friend void DescriptorAccessUnlock_nid_no_patch(void* access); + friend bool DescriptorAccessReadOrWait_nid_no_patch(void* access); + friend void DescriptorAccessWake_nid_no_patch(void* access) noexcept; + friend void DescriptorAccessSnapshot_nid_no_patch(const void* access, void* version); + friend void DescriptorOperationInitialize_nid_no_patch(void* operation, void* access); + static std::shared_ptr Get(int descriptor); + std::shared_ptr state; + Mode mode; + bool held = false; +}; + +#endif diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index a2f9bedde02..cc739d535cd 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -13,6 +13,7 @@ #include #include #include +#include "prx/libc/include/DescriptorAccess.hpp" #ifdef _WIN32 #include #include "prx/libc/include/FileLock.hpp" @@ -43,9 +44,9 @@ class ClosingDescriptor { public: ClosingDescriptor([[maybe_unused]] std::FILE* stream, int descriptor) #ifdef _WIN32 - : _stream(stream), _closing(BeginClosingDescriptor_nid_no_patch(descriptor)) {} + : _stream(stream), _access(descriptor, GuestDescriptorAccess::Mode::Change), _closing(BeginClosingDescriptor_nid_no_patch(descriptor)) {} #else - : _closing(BeginClosingDescriptor_nid_no_patch(descriptor)) {} + : _access(descriptor, GuestDescriptorAccess::Mode::Change), _closing(BeginClosingDescriptor_nid_no_patch(descriptor)) {} #endif ~ClosingDescriptor() { EndClosingDescriptor_nid_no_patch(_closing); } ClosingDescriptor(const ClosingDescriptor&) = delete; @@ -61,6 +62,7 @@ class ClosingDescriptor { std::FILE* stream; } _stream; #endif + GuestDescriptorAccess _access; void* _closing; }; @@ -89,7 +91,53 @@ static constexpr const char* FOPEN_MSG_OPEN_FAILED = "open failed"; extern "C" int KeepHostStandardError_nid_no_patch(); #endif +extern "C" bool TryLockNativeStream_nid_no_patch(std::FILE* stream); + class FileStream { + class NativeLock { + std::FILE* handle; + public: + explicit NativeLock(std::FILE* handle, bool closing = false) : handle(handle) { + if (!handle) return; + if (closing && FileStreamCloseHookActive_nid_no_patch()) { +#ifdef _WIN32 + const bool acquired = TryLockNativeStream_nid_no_patch(handle); + const int descriptor = ::_fileno(handle); +#else + const bool acquired = ::ftrylockfile(handle) == 0; + const int descriptor = ::fileno(handle); +#endif + FileStreamCloseEventForTest_nid_no_patch(descriptor, acquired ? 3 : 2); + if (acquired) return; + } +#ifdef _WIN32 + ::_lock_file(handle); +#else + ::flockfile(handle); +#endif + } + ~NativeLock() { + if (!handle) return; +#ifdef _WIN32 + ::_unlock_file(handle); +#else + ::funlockfile(handle); +#endif + } + NativeLock(const NativeLock&) = delete; + NativeLock& operator=(const NativeLock&) = delete; + void Forget() { handle = nullptr; } + void Retain(std::FILE* value) { handle = value; } + int Close() { +#ifdef _WIN32 + const int result = ::_fclose_nolock(handle); + ::_unlock_file(std::exchange(handle, nullptr)); + return result; +#else + return std::fclose(std::exchange(handle, nullptr)); +#endif + } + }; // Only the macro-accessed FreeBSD prefix is exposed. The remaining guest // FILE area is reserved; host state lives beyond it and is never a guest FILE*. GuestFilePrefix _guest{}; @@ -217,6 +265,7 @@ class FileStream { return Adopt(open(filename, mode)); } auto* previous = GetHandle(); + NativeLock streamLock(previous, true); _guest = {}; guestError = false; randomDevice = false; @@ -234,7 +283,7 @@ class FileStream { if (open && !filename.empty()) { if (!IsHostStandard()) { _handle = nullptr; - std::fclose(previous); + streamLock.Close(); closing.reset(); _handle = open(filename, mode); } else { @@ -275,10 +324,13 @@ class FileStream { KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); + const bool hostStandard = IsHostStandard(); const ClosingDescriptor closing(previous, ::fileno(previous)); const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(::fileno(previous)) : -1; if (kept >= 0) ::flockfile(previous); + if (!hostStandard) streamLock.Forget(); _handle = std::freopen(filename.c_str(), mode, previous); + if (!hostStandard && _handle) streamLock.Retain(_handle); if (kept >= 0) RestoreHostStandard(previous, kept); #endif return Adopt(_handle); @@ -438,6 +490,7 @@ class FileStream { int Close() { GetHandle(); + NativeLock streamLock(_handle, true); #ifdef _WIN32 GuestFileLockForget_nid_no_patch(::_fileno(_handle)); if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(::_fileno(_handle)); @@ -445,7 +498,7 @@ class FileStream { #else const ClosingDescriptor closing(_handle, ::fileno(_handle)); #endif - const int result = std::fclose(_handle); + const int result = streamLock.Close(); _handle = nullptr; _guest.flags = 0; _guest.descriptor = -1; diff --git a/core/libs/prx/libc/include/HostThreadLocal.hpp b/core/libs/prx/libc/include/HostThreadLocal.hpp index 076459cc9a7..357b1b62ccf 100644 --- a/core/libs/prx/libc/include/HostThreadLocal.hpp +++ b/core/libs/prx/libc/include/HostThreadLocal.hpp @@ -5,12 +5,40 @@ #include #ifdef _WIN32 #include +#include +#endif + +#ifdef _WIN32 +template +class HostThreadLocalSlot { +public: + static DWORD Get() { + static const DWORD slot = [] { + const DWORD created = FlsAlloc(+[](void* value) { delete static_cast(value); }); + published.store(created, std::memory_order_release); + return created; + }(); + return slot; + } + static TValue* Find() { + const DWORD slot = published.load(std::memory_order_acquire); + if (slot == FLS_OUT_OF_INDEXES) return nullptr; + const DWORD saved = GetLastError(); + auto* value = static_cast(FlsGetValue(slot)); + SetLastError(saved); + return value; + } +private: + static inline std::atomic published{FLS_OUT_OF_INDEXES}; +}; +template +TValue* FindHostThreadLocal() { return HostThreadLocalSlot::Find(); } #endif template TValue& HostThreadLocal() { #ifdef _WIN32 - static const DWORD slot = FlsAlloc(+[](void* value) { delete static_cast(value); }); + const DWORD slot = HostThreadLocalSlot::Get(); if (slot == FLS_OUT_OF_INDEXES) throw std::runtime_error("host thread-local slot allocation failed"); auto* value = static_cast(FlsGetValue(slot)); if (value == nullptr) { diff --git a/core/libs/prx/libc/include/JumpBuffer.hpp b/core/libs/prx/libc/include/JumpBuffer.hpp index 07e272b61e6..fb14ae729f7 100644 --- a/core/libs/prx/libc/include/JumpBuffer.hpp +++ b/core/libs/prx/libc/include/JumpBuffer.hpp @@ -2,14 +2,34 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_JUMPBUFFER_HPP #include "prx/libc/include/general/VabiMacros.hpp" +#include namespace JumpBuffer { using Sigprocmask = int (APS5_VABI*)(int how, const void* set, void* previousSet); +using StackRestore = void (*)(std::uintptr_t stack, std::uint32_t context); + +struct Context { + std::uintptr_t stack; + std::uint32_t identity; + std::uint32_t signature; +}; + +static_assert(sizeof(Context) == 16); extern "C" { void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask); +void GuestIoJumpRegister_nid_no_patch(StackRestore restore); +std::uint32_t GuestContextCurrent_nid_no_patch(); +void GuestContextSet_nid_no_patch(std::uint32_t context); +Context GuestContextCapture_nid_no_patch(std::uintptr_t stack); +Context GuestContextCreate_nid_no_patch(std::uintptr_t stack); +std::uint32_t GuestContextIdentify_nid_no_patch(const Context& context, std::uintptr_t stack); +void APS5_VABI JumpBufferSaveContext_nid_no_patch(unsigned char* buffer); +void APS5_VABI JumpBufferSaveMask_nid_no_patch(unsigned char* buffer); +void APS5_VABI JumpBufferRestoreMask_nid_no_patch(const unsigned char* buffer); +void APS5_VABI GuestIoStackRestore_nid_no_patch(std::uintptr_t stack, std::uint32_t context); } diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp new file mode 100644 index 00000000000..db81a3d0ee2 --- /dev/null +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -0,0 +1,241 @@ +#include "prx/libc/include/DescriptorAccess.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestDescriptorAccess::State { + std::mutex mutex; + std::condition_variable changed; + unsigned readers = 0; + unsigned changesWaiting = 0; + std::thread::id writer; + unsigned depth = 0; + std::uint64_t generation = 0; + bool replacement = false; + unsigned closesWaiting = 0; + std::vector operations; + + void Interrupt() noexcept { + for (auto* operation : operations) operation->Interrupt(); + } + + bool CanRead() const { + return writer == std::this_thread::get_id() || (writer == std::thread::id{} && changesWaiting == 0); + } + bool CanChange() const { + return writer == std::this_thread::get_id() || (writer == std::thread::id{} && readers == 0); + } +}; + +std::shared_ptr GuestDescriptorAccess::Get(int descriptor) { + struct Entry { + State* identity; + std::weak_ptr state; + }; + struct Bucket { + std::mutex mutex; + std::map entries; + }; + static auto* buckets = new std::array; + auto& bucket = (*buckets)[static_cast(descriptor) % buckets->size()]; + { + std::lock_guard lock(bucket.mutex); + const auto found = bucket.entries.find(descriptor); + if (found != bucket.entries.end()) + if (auto state = found->second.state.lock()) return state; + } + auto state = std::shared_ptr(new State, [&bucket, descriptor](State* released) { + { + std::lock_guard lock(bucket.mutex); + const auto found = bucket.entries.find(descriptor); + if (found != bucket.entries.end() && found->second.identity == released) bucket.entries.erase(found); + } + delete released; + }); + { + std::lock_guard lock(bucket.mutex); + const auto found = bucket.entries.find(descriptor); + if (found != bucket.entries.end()) + if (auto existing = found->second.state.lock()) return existing; + bucket.entries.insert_or_assign(descriptor, Entry{state.get(), state}); + } + return state; +} + +void DescriptorAccessInitialize_nid_no_patch(void* access, int descriptor) { + auto& self = *static_cast(access); + self.state = descriptor < 0 ? nullptr : GuestDescriptorAccess::Get(descriptor); +} + +void DescriptorAccessLock_nid_no_patch(void* access) { + auto& self = *static_cast(access); + if (!self.state) { self.held = true; return; } + std::unique_lock lock(self.state->mutex); + if (self.mode == GuestDescriptorAccess::Mode::Read) { + self.state->changed.wait(lock, [&] { return self.state->CanRead(); }); + ++self.state->readers; + } else { + ++self.state->changesWaiting; + if (self.mode == GuestDescriptorAccess::Mode::Close) ++self.state->closesWaiting; + try { + if (self.mode == GuestDescriptorAccess::Mode::Close) { + auto retry = std::chrono::milliseconds(1); + while (!self.state->CanChange()) { + self.state->Interrupt(); + self.state->changed.wait_for(lock, retry, [&] { return self.state->CanChange(); }); + retry = std::min(retry * 2, std::chrono::milliseconds(16)); + } + } else self.state->changed.wait(lock, [&] { return self.state->CanChange(); }); + } catch (...) { + --self.state->changesWaiting; + if (self.mode == GuestDescriptorAccess::Mode::Close) --self.state->closesWaiting; + self.state->changed.notify_all(); + throw; + } + --self.state->changesWaiting; + if (self.mode == GuestDescriptorAccess::Mode::Close) --self.state->closesWaiting; + self.state->writer = std::this_thread::get_id(); + ++self.state->depth; + self.state->replacement = self.state->replacement || self.mode == GuestDescriptorAccess::Mode::Change || self.mode == GuestDescriptorAccess::Mode::Close; + } + self.held = true; +} + +bool DescriptorAccessTryLock_nid_no_patch(void* access) { + auto& self = *static_cast(access); + if (!self.state) { self.held = true; return true; } + std::lock_guard lock(self.state->mutex); + if (self.mode == GuestDescriptorAccess::Mode::Read) { + if (!self.state->CanRead()) return false; + ++self.state->readers; + } else { + if (!self.state->CanChange()) return false; + self.state->writer = std::this_thread::get_id(); + ++self.state->depth; + self.state->replacement = self.state->replacement || self.mode == GuestDescriptorAccess::Mode::Change || self.mode == GuestDescriptorAccess::Mode::Close; + } + self.held = true; + return true; +} + +void DescriptorAccessUnlock_nid_no_patch(void* access) { + auto& self = *static_cast(access); + if (!self.state) { self.held = false; return; } + { + std::lock_guard lock(self.state->mutex); + if (self.mode == GuestDescriptorAccess::Mode::Read) --self.state->readers; + else if (--self.state->depth == 0) { + self.state->writer = {}; + if (self.state->replacement && self.state->generation != std::numeric_limits::max()) ++self.state->generation; + self.state->replacement = false; + } + self.held = false; + } + self.state->changed.notify_all(); +} + +bool DescriptorAccessReadOrWait_nid_no_patch(void* access) { + auto& self = *static_cast(access); + if (!self.state) { self.held = true; return true; } + std::unique_lock lock(self.state->mutex); + if (self.state->CanRead()) { + ++self.state->readers; + self.held = true; + return true; + } + self.state->changed.wait(lock); + return false; +} + +void DescriptorAccessWake_nid_no_patch(void* access) noexcept { + auto& self = *static_cast(access); + if (self.state) self.state->changed.notify_all(); +} + +bool DescriptorVersionUnchanged_nid_no_patch(const void* version) { + const auto& self = *static_cast(version); + if (!self.state) return false; + std::lock_guard lock(self.state->mutex); + return self.value != std::numeric_limits::max() && self.state->generation == self.value && self.state->closesWaiting == 0; +} + +void DescriptorAccessSnapshot_nid_no_patch(const void* access, void* version) { + const auto& self = *static_cast(access); + auto& result = *static_cast(version); + if (!self.state) return; + std::lock_guard lock(self.state->mutex); + result.state = self.state; + result.value = self.state->generation; +} + +thread_local GuestDescriptorAccess::Operation* GuestDescriptorAccess::Operation::current = nullptr; + +void DescriptorOperationInitialize_nid_no_patch(void* operation, void* access) { + auto& self = *static_cast(operation); + self.state = static_cast(access)->state; + self.previous = GuestDescriptorAccess::Operation::current; + if (self.state) { + std::lock_guard lock(self.state->mutex); + self.state->operations.push_back(&self); + self.interrupted.store(self.state->closesWaiting != 0); + } + GuestDescriptorAccess::Operation::current = &self; +} + +void DescriptorOperationDestroy_nid_no_patch(void* operation) { + auto& self = *static_cast(operation); + GuestDescriptorAccess::Operation::current = self.previous; + if (self.state) { + std::lock_guard lock(self.state->mutex); + std::erase(self.state->operations, &self); + } +} + +bool DescriptorOperationInterrupted_nid_no_patch() { + auto* current = GuestDescriptorAccess::Operation::current; + return current && current->Interrupted(); +} + +void GuestDescriptorAccess::Operation::Interrupt() noexcept { + interrupted.store(true); + for (auto* registration = wake; registration; registration = registration->previous) + registration->interrupt(registration->argument); +} + +void DescriptorWakeInitialize_nid_no_patch(void* registration) { + auto& self = *static_cast(registration); + self.operation = GuestDescriptorAccess::Operation::current; + if (!self.operation || !self.operation->state) { self.operation = nullptr; return; } + std::lock_guard lock(self.operation->state->mutex); + self.previous = self.operation->wake; + self.operation->wake = &self; +} + +void DescriptorWakeDestroy_nid_no_patch(void* registration) { + auto& self = *static_cast(registration); + if (!self.operation) return; + std::lock_guard lock(self.operation->state->mutex); + self.operation->wake = self.previous; +} + +namespace { +std::atomic streamCloseHook{nullptr}; +} + +void FileStreamSetCloseHookForTest_nid_no_patch(void (*hook)(int, int)) { + streamCloseHook.store(hook); +} + +bool FileStreamCloseHookActive_nid_no_patch() { + return streamCloseHook.load() != nullptr; +} + +void FileStreamCloseEventForTest_nid_no_patch(int descriptor, int event) { + if (const auto hook = streamCloseHook.load()) hook(descriptor, event); +} diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index bb1792d85bf..e3f04c3a714 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -134,6 +134,7 @@ static bool MoveAppendingHandle(int descriptor, HANDLE handle, DWORD access, DWO } static bool AppendThroughDescriptor(int descriptor) { + const GuestDescriptorAccess access(descriptor, GuestDescriptorAccess::Mode::Update); const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); if (handle == INVALID_HANDLE_VALUE) return false; if (descriptor <= 2 || GetFileType(handle) != FILE_TYPE_DISK) return true; @@ -141,6 +142,7 @@ static bool AppendThroughDescriptor(int descriptor) { } static bool AppendOnlyDescriptor(int descriptor) { + const GuestDescriptorAccess admission(descriptor, GuestDescriptorAccess::Mode::Update); const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); if (handle == INVALID_HANDLE_VALUE) return false; if (GetFileType(handle) != FILE_TYPE_DISK) return true; diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index 187225c8f1b..afd31e97256 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace JumpBuffer { namespace { @@ -13,8 +15,21 @@ constexpr std::size_t MaskOffset = 72; constexpr std::size_t MaskSavedOffset = 88; constexpr int SigBlock = 1; constexpr int SigSetmask = 3; +constexpr std::uint32_t ContextSignature = 0x43545835; +constexpr std::size_t ContextOffset = 92; std::atomic guestSigprocmask{nullptr}; +std::atomic ioStackRestore{nullptr}; +std::atomic nextContext{1}; +constinit thread_local std::uint32_t currentContext = 0; + +std::uint32_t NewContext() { + auto value = nextContext.load(std::memory_order_relaxed); + for (;;) { + if (value == std::numeric_limits::max()) throw std::overflow_error("Guest context identities exhausted"); + if (nextContext.compare_exchange_weak(value, value + 1, std::memory_order_relaxed)) return value; + } +} } @@ -24,17 +39,58 @@ void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask) { guestSigprocmask.store(sigprocmask); } +void GuestIoJumpRegister_nid_no_patch(StackRestore restore) { ioStackRestore.store(restore); } + +std::uint32_t GuestContextCurrent_nid_no_patch() { + if (!currentContext) currentContext = NewContext(); + return currentContext; +} + +void GuestContextSet_nid_no_patch(std::uint32_t context) { currentContext = context; } + +Context GuestContextCapture_nid_no_patch(std::uintptr_t stack) { + const auto identity = GuestContextCurrent_nid_no_patch(); + return {stack, identity, ContextSignature ^ identity}; +} + +Context GuestContextCreate_nid_no_patch(std::uintptr_t stack) { + const auto identity = NewContext(); + return {stack, identity, ContextSignature ^ identity}; +} + +std::uint32_t GuestContextIdentify_nid_no_patch(const Context& context, std::uintptr_t stack) { + if (context.stack == stack && context.identity != 0 && context.identity < nextContext.load(std::memory_order_relaxed) && + context.signature == (ContextSignature ^ context.identity)) return context.identity; + return NewContext(); +} + +void APS5_VABI JumpBufferSaveContext_nid_no_patch(unsigned char* buffer) { + const auto context = GuestContextCurrent_nid_no_patch(); + std::memcpy(buffer + ContextOffset, &context, sizeof(context)); +} + +void APS5_VABI GuestIoStackRestore_nid_no_patch(std::uintptr_t stack, std::uint32_t context) { + if (const auto restore = ioStackRestore.load()) restore(stack, context); + else GuestContextSet_nid_no_patch(context); +} + void APS5_VABI JumpBufferSaveMask_nid_no_patch(unsigned char* buffer) { const auto sigprocmask = guestSigprocmask.load(); const std::int32_t saved = sigprocmask != nullptr && sigprocmask(SigBlock, nullptr, buffer + MaskOffset) == 0; std::memcpy(buffer + MaskSavedOffset, &saved, sizeof(saved)); + JumpBufferSaveContext_nid_no_patch(buffer); } void APS5_VABI JumpBufferRestoreMask_nid_no_patch(const unsigned char* buffer) { + std::uintptr_t stack = 0; + std::memcpy(&stack, buffer + 16, sizeof(stack)); std::int32_t saved = 0; std::memcpy(&saved, buffer + MaskSavedOffset, sizeof(saved)); const auto sigprocmask = guestSigprocmask.load(); if (saved != 0 && sigprocmask != nullptr) sigprocmask(SigSetmask, buffer + MaskOffset, nullptr); + std::uint32_t context = 0; + std::memcpy(&context, buffer + ContextOffset, sizeof(context)); + GuestIoStackRestore_nid_no_patch(stack, context); } } @@ -98,6 +154,9 @@ asm(".text\n" " fldcw 68(%rdi)\n" " jmp *0(%rdi)\n" APS5_ASM_FUNCTION("_setjmp_nid_postfix") + " push %rdi\n" + APS5_JUMP_ASM_CALL("JumpBufferSaveContext_nid_no_patch") + " pop %rdi\n" " movl $0, 88(%rdi)\n" " mov (%rsp), %rax\n" " mov %rax, 0(%rdi)\n" @@ -114,15 +173,18 @@ asm(".text\n" " xor %eax, %eax\n" " ret\n" APS5_ASM_FUNCTION("_longjmp_nid_postfix") -#ifdef _WIN32 " mov %rdi, %rbx\n" " mov %esi, %r12d\n" " and $-16, %rsp\n" + " mov 16(%rbx), %rdi\n" + " mov 92(%rbx), %esi\n" + APS5_JUMP_ASM_CALL("GuestIoStackRestore_nid_no_patch") +#ifdef _WIN32 " mov 16(%rbx), %rdi\n" " call GuestStackSwitch_nid_no_patch\n" +#endif " mov %rbx, %rdi\n" " mov %r12d, %esi\n" -#endif " sub $8, %rsp\n" " stmxcsr (%rsp)\n" " mov 64(%rdi), %eax\n" diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index eb8ad0c317a..447eeea933c 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -204,15 +204,19 @@ void APS5_VABI funlockfile_nid_postfix(FileStream* stream) { } -int APS5_VABI ftrylockfile_nid_postfix(FileStream* stream) { - if (stream == nullptr) throw std::invalid_argument("ftrylockfile: null stream"); +bool TryLockNativeStream_nid_no_patch(std::FILE* stream) { #ifdef _WIN32 - return TryEnterCriticalSection(&reinterpret_cast(GetStreamHandle(stream))->lock) ? 0 : -1; + return TryEnterCriticalSection(&reinterpret_cast(stream)->lock) != 0; #else - return ftrylockfile(GetStreamHandle(stream)) == 0 ? 0 : -1; + return ::ftrylockfile(stream) == 0; #endif } +int APS5_VABI ftrylockfile_nid_postfix(FileStream* stream) { + if (stream == nullptr) throw std::invalid_argument("ftrylockfile: null stream"); + return TryLockNativeStream_nid_no_patch(GetStreamHandle(stream)) ? 0 : -1; +} + std::uint64_t APS5_VABI _Stoul_nid_postfix(const char* str, char** endptr, int base) { if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoull); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8b32c6ddb47..575ad48a47f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -34,6 +34,8 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Cond.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Attr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Thread.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/NativeIo.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/IoSignals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/GuestTlsMacOs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp @@ -114,6 +116,251 @@ endif() configure_windows_unwind(libkernel) if(BUILD_TESTING) + add_test_executable(guest_io_context_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoContext.cpp) + target_include_directories(guest_io_context_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_context_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_io_context_tests) + if(WIN32) + add_test(NAME guest_io_context_deferred_drain COMMAND guest_io_context_tests deferred_drain) + set_tests_properties(guest_io_context_deferred_drain PROPERTIES TIMEOUT 15 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + foreach(ioContext underscorejump manual_underscorejump adopted adopted_exit swap jump sigjump manual_swap manual_jump manual_sigjump manual_leave manual_sigleave) + add_test(NAME guest_io_context_${ioContext} COMMAND guest_io_context_tests ${ioContext}) + set_tests_properties(guest_io_context_${ioContext} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_io_context_${ioContext} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test(NAME guest_io_context_churn COMMAND guest_io_context_tests churn) + set_tests_properties(guest_io_context_churn PROPERTIES TIMEOUT 15) + endif() + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_io_signal_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoSignalClose.cpp) + target_include_directories(guest_io_signal_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_signal_close_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_io_signal_close_tests) + foreach(ioSignalSource native fault_signal exception native_socket exception_socket) + foreach(ioSignalMode enabled disabled unregistered) + if(WIN32 AND (NOT ioSignalSource MATCHES "^exception" OR ioSignalMode STREQUAL "unregistered")) + continue() + endif() + if(ioSignalSource MATCHES "^exception" AND ioSignalMode STREQUAL "unregistered") + continue() + endif() + set(ioSignalTest guest_io_signal_${ioSignalSource}_${ioSignalMode}) + add_test(NAME ${ioSignalTest} COMMAND guest_io_signal_close_tests ${ioSignalSource} ${ioSignalMode}) + set_tests_properties(${ioSignalTest} PROPERTIES TIMEOUT 15) + add_test(NAME ${ioSignalTest}_resume COMMAND guest_io_signal_close_tests ${ioSignalSource} ${ioSignalMode} resume) + add_test(NAME ${ioSignalTest}_resume_errno COMMAND guest_io_signal_close_tests ${ioSignalSource} ${ioSignalMode} resume_errno) + set_tests_properties(${ioSignalTest}_resume_errno PROPERTIES TIMEOUT 15) + set_tests_properties(${ioSignalTest}_resume PROPERTIES TIMEOUT 15) + foreach(ioSignalJump jump local_jump starting) + add_test(NAME ${ioSignalTest}_${ioSignalJump} COMMAND guest_io_signal_close_tests ${ioSignalSource} ${ioSignalMode} ${ioSignalJump}) + set_tests_properties(${ioSignalTest}_${ioSignalJump} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${ioSignalTest}_${ioSignalJump} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + if(NOT ioSignalSource MATCHES "^exception") + add_test(NAME ${ioSignalTest}_interrupt COMMAND guest_io_signal_close_tests ${ioSignalSource} ${ioSignalMode} interrupt) + set_tests_properties(${ioSignalTest}_interrupt PROPERTIES TIMEOUT 15) + endif() + if(ioSignalSource MATCHES "_socket$") + add_test(NAME ${ioSignalTest}_timeout COMMAND guest_io_signal_close_tests ${ioSignalSource} ${ioSignalMode} timeout) + set_tests_properties(${ioSignalTest}_timeout PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${ioSignalTest}_timeout PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endif() + if(WIN32) + set_tests_properties(${ioSignalTest} ${ioSignalTest}_resume ${ioSignalTest}_resume_errno PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + endif() + add_test_executable(guest_io_descriptor_access_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoDescriptorAccess.cpp) + target_include_directories(guest_io_descriptor_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_descriptor_access_tests PRIVATE libkernel libc libSceNet) + configure_windows_unwind(guest_io_descriptor_access_tests) + foreach(closeTransport 0 1) + set(closeTest guest_io_close_sce_${closeTransport}) + add_test(NAME ${closeTest} COMMAND guest_io_descriptor_access_tests posix_close ${closeTransport} 0 0 2) + set_tests_properties(${closeTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${closeTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + foreach(closeCase close_unregistered close_readers) + set(closeTest guest_io_${closeCase}_${closeTransport}) + add_test(NAME ${closeTest} COMMAND guest_io_descriptor_access_tests ${closeCase} ${closeTransport}) + set_tests_properties(${closeTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${closeTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + foreach(closeApi 0 1 2 3 4) + if(closeTransport EQUAL 0 AND closeApi GREATER 1) + continue() + endif() + foreach(closeDisabled 0 1) + set(closeTest guest_io_close_${closeTransport}_${closeApi}_${closeDisabled}) + add_test(NAME ${closeTest} COMMAND guest_io_descriptor_access_tests posix_close ${closeTransport} ${closeApi} ${closeDisabled} 0) + set_tests_properties(${closeTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${closeTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + endforeach() + add_test(NAME guest_io_close_socket_direct COMMAND guest_io_descriptor_access_tests posix_close 1 2 0 1) + set_tests_properties(guest_io_close_socket_direct PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_io_close_socket_direct PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test(NAME guest_io_close_native_errors COMMAND guest_io_descriptor_access_tests close_native_errors) + set_tests_properties(guest_io_close_native_errors PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + foreach(descriptorCase registry_lifetime close_before_native) + add_test(NAME guest_io_descriptor_${descriptorCase} COMMAND guest_io_descriptor_access_tests ${descriptorCase}) + set_tests_properties(guest_io_descriptor_${descriptorCase} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_io_descriptor_${descriptorCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test(NAME guest_io_descriptor_capacity COMMAND guest_io_descriptor_access_tests capacity) + add_test(NAME guest_io_descriptor_no_aliases COMMAND guest_io_descriptor_access_tests no_aliases) + add_test(NAME guest_io_descriptor_access_cancel COMMAND guest_io_descriptor_access_tests access_cancel) + add_test(NAME guest_io_descriptor_protected_close COMMAND guest_io_descriptor_access_tests protected_close) + add_test(NAME guest_io_descriptor_invalid_duplicate COMMAND guest_io_descriptor_access_tests invalid_duplicate) + set_tests_properties(guest_io_descriptor_invalid_duplicate PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_io_descriptor_invalid_duplicate PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME guest_io_descriptor_stdio_close COMMAND guest_io_descriptor_access_tests stdio_close) + add_test(NAME guest_io_descriptor_stdio_reopen COMMAND guest_io_descriptor_access_tests stdio_reopen) + set_tests_properties(guest_io_descriptor_capacity guest_io_descriptor_no_aliases guest_io_descriptor_access_cancel guest_io_descriptor_protected_close guest_io_descriptor_stdio_close guest_io_descriptor_stdio_reopen PROPERTIES TIMEOUT 15) + if(WIN32) + add_test(NAME guest_io_descriptor_close_progress COMMAND guest_io_descriptor_access_tests close_progress) + add_test(NAME guest_io_descriptor_position_cancel COMMAND guest_io_descriptor_access_tests position_cancel) + add_test(NAME guest_io_descriptor_pipe_vectors COMMAND guest_io_descriptor_access_tests pipe_vectors) + add_test(NAME guest_io_descriptor_append_status COMMAND guest_io_descriptor_access_tests append_status) + add_test(NAME guest_io_descriptor_append_stream COMMAND guest_io_descriptor_access_tests append_stream) + set_tests_properties(guest_io_descriptor_append_status guest_io_descriptor_append_stream PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_io_descriptor_close_progress guest_io_descriptor_position_cancel guest_io_descriptor_pipe_vectors PROPERTIES TIMEOUT 15) + set_tests_properties(guest_io_descriptor_capacity guest_io_descriptor_no_aliases guest_io_descriptor_access_cancel guest_io_descriptor_close_progress guest_io_descriptor_position_cancel guest_io_descriptor_pipe_vectors guest_io_descriptor_protected_close guest_io_descriptor_stdio_close guest_io_descriptor_stdio_reopen PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + else() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test(NAME guest_io_descriptor_invalid_registry COMMAND guest_io_descriptor_access_tests invalid_registry) + set_tests_properties(guest_io_descriptor_invalid_registry PROPERTIES TIMEOUT 15) + endif() + add_test(NAME guest_io_descriptor_record_locks COMMAND guest_io_descriptor_access_tests record_locks) + add_test(NAME guest_io_descriptor_borrowed_net_close COMMAND guest_io_descriptor_access_tests borrowed_net_close) + add_test(NAME guest_io_descriptor_borrowed_posix_close COMMAND guest_io_descriptor_access_tests borrowed_posix_close) + set_tests_properties(guest_io_descriptor_borrowed_posix_close PROPERTIES TIMEOUT 15) + set_tests_properties(guest_io_descriptor_record_locks guest_io_descriptor_borrowed_net_close PROPERTIES TIMEOUT 15) + endif() + add_test_executable(guest_io_native_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoNativeThreads.cpp) + target_include_directories(guest_io_native_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_native_threads_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_io_native_threads_tests) + add_test(NAME guest_io_native_threads COMMAND guest_io_native_threads_tests) + set_tests_properties(guest_io_native_threads PROPERTIES TIMEOUT 20) + if(WIN32) + add_test(NAME guest_io_restart_only_retirement COMMAND guest_io_native_threads_tests retire_restart) + add_test(NAME guest_io_empty_retirement COMMAND guest_io_native_threads_tests retire_empty) + add_test(NAME guest_io_empty_guest_return COMMAND guest_io_native_threads_tests return_empty) + add_test(NAME guest_io_empty_guest_exit COMMAND guest_io_native_threads_tests exit_empty) + set_tests_properties(guest_io_restart_only_retirement guest_io_empty_retirement guest_io_empty_guest_return guest_io_empty_guest_exit PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + if(WIN32) + set_tests_properties(guest_io_native_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_io_read_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoReadCancel.cpp) + target_include_directories(guest_io_read_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_read_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_io_read_cancel_tests) + foreach(ioReadTransport pipe loopback datagram) + if(ioReadTransport STREQUAL "pipe") + set(ioReadApis read alias readv) + else() + set(ioReadApis read alias readv recv recvfrom recvmsg) + endif() + foreach(ioReadApi IN LISTS ioReadApis) + foreach(ioReadScenario cancel complete disabled pending reuse reuse_complete partial race) + set(ioReadTest guest_io_read_${ioReadTransport}_${ioReadApi}_${ioReadScenario}) + add_test(NAME ${ioReadTest} COMMAND guest_io_read_cancel_tests ${ioReadTransport} ${ioReadApi} ${ioReadScenario}) + set_tests_properties(${ioReadTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${ioReadTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + endforeach() + foreach(ioReadPositionedApi pread preadv) + add_test(NAME guest_io_read_pending_${ioReadPositionedApi} COMMAND guest_io_read_cancel_tests pipe ${ioReadPositionedApi} pending) + set_tests_properties(guest_io_read_pending_${ioReadPositionedApi} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_io_read_pending_${ioReadPositionedApi} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + foreach(ioReadZeroApi recvmsg_empty recvmsg_zero) + foreach(ioReadZeroCase cancel complete disabled pending dontwait eof) + set(ioReadZeroTest guest_io_read_zero_${ioReadZeroApi}_${ioReadZeroCase}) + add_test(NAME ${ioReadZeroTest} COMMAND guest_io_read_cancel_tests loopback ${ioReadZeroApi} ${ioReadZeroCase}) + set_tests_properties(${ioReadZeroTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${ioReadZeroTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + foreach(ioReadErrorTransport loopback datagram) + foreach(ioReadErrorApi read recv recvfrom recvmsg) + foreach(ioReadErrorCase timeout nonblocking dontwait) + if(ioReadErrorApi STREQUAL "read" AND ioReadErrorCase STREQUAL "dontwait") + continue() + endif() + set(ioReadErrorTest guest_io_read_${ioReadErrorTransport}_${ioReadErrorApi}_${ioReadErrorCase}) + add_test(NAME ${ioReadErrorTest} COMMAND guest_io_read_cancel_tests ${ioReadErrorTransport} ${ioReadErrorApi} ${ioReadErrorCase}) + set_tests_properties(${ioReadErrorTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${ioReadErrorTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + endforeach() + foreach(ioReadWaitAllApi recv recvfrom recvmsg) + set(ioReadWaitAllTest guest_io_read_waitall_${ioReadWaitAllApi}) + add_test(NAME ${ioReadWaitAllTest} COMMAND guest_io_read_cancel_tests loopback ${ioReadWaitAllApi} waitall) + set_tests_properties(${ioReadWaitAllTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${ioReadWaitAllTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + foreach(ioReadSceTransport pipe loopback datagram) + foreach(ioReadSceApi sce_read sce_readv) + set(ioReadSceTest guest_io_read_${ioReadSceTransport}_${ioReadSceApi}) + add_test(NAME ${ioReadSceTest} COMMAND guest_io_read_cancel_tests ${ioReadSceTransport} ${ioReadSceApi} cancel) + set_tests_properties(${ioReadSceTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${ioReadSceTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test_executable(kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/KernelError.cpp) target_include_directories(kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(kernel_error_tests PRIVATE libkernel libc) @@ -1453,12 +1700,30 @@ if(BUILD_TESTING) target_include_directories(guest_raise_exception_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_exception_tests PRIVATE libkernel libc) configure_windows_unwind(guest_raise_exception_tests) - add_test(NAME guest_raise_exception COMMAND guest_raise_exception_tests) - set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 60) - add_asan_test_options(guest_raise_exception detect_stack_use_after_return=0) + set(raiseCases self busy waiting host leaving context starting locks finished nested_later nested_outer address_waking address_waiting address_idle) + if(WIN32) + list(APPEND raiseCases continuing selfing looping vectors drain) + endif() + foreach(raiseCase IN LISTS raiseCases) + set(raiseBatches 1) + if(raiseCase STREQUAL "address_waking" OR raiseCase STREQUAL "address_waiting") + set(raiseBatches 5) + endif() + foreach(raiseBatch RANGE 1 ${raiseBatches}) + set(raiseTest guest_raise_exception_${raiseCase}) + if(raiseBatches GREATER 1) + string(APPEND raiseTest _${raiseBatch}) + endif() + add_test(NAME ${raiseTest} COMMAND guest_raise_exception_tests ${raiseCase}) + set_tests_properties(${raiseTest} PROPERTIES TIMEOUT 25) + add_asan_test_options(${raiseTest} detect_stack_use_after_return=0) + if(WIN32) + set_tests_properties(${raiseTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() if(WIN32) - set_tests_properties(guest_raise_exception PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") add_test_executable(guest_raise_nested_sleep_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseNestedSleep.cpp) target_include_directories(guest_raise_nested_sleep_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_nested_sleep_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index 6250e823f3b..d97887e5875 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -13,6 +13,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/DescriptorAccess.hpp" extern "C" { @@ -34,6 +35,27 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ bool DescriptorIsOpen_nid_no_patch(int descriptor); namespace File { +class IoDescriptor { +public: + explicit IoDescriptor(int descriptor); + ~IoDescriptor(); + IoDescriptor(const IoDescriptor&) = delete; + IoDescriptor& operator=(const IoDescriptor&) = delete; + int Value() const { return value; } + +private: + int value; + GuestDescriptorAccess access; + std::optional operation; +}; + +template +std::int64_t WithIoDescriptor(int descriptor, TCall&& call) { + IoDescriptor retained(descriptor); + if (retained.Value() < 0) return -1; + return call(retained.Value()); +} + #ifdef _WIN32 std::optional WindowsDescriptorPath_nid_no_patch(int descriptor); #endif @@ -42,6 +64,8 @@ void ForgetClosedDescriptor(int descriptor); class PositionedTransferLock { public: explicit PositionedTransferLock(std::shared_ptr mutex) : _mutex(std::move(mutex)), _lock(*_mutex) {} + PositionedTransferLock(std::shared_ptr mutex, std::unique_lock lock) + : _mutex(std::move(mutex)), _lock(std::move(lock)) {} PositionedTransferLock(PositionedTransferLock&&) = default; PositionedTransferLock& operator=(PositionedTransferLock&&) = delete; void unlock() { _lock.unlock(); } @@ -53,6 +77,7 @@ class PositionedTransferLock { [[nodiscard]] PositionedTransferLock LockPositionedTransfer(int descriptor); void SharePositionedTransfer(int from, int to); #ifdef _WIN32 +std::optional IoPositionedTransfer(int descriptor); std::int64_t WriteAtEnd(int descriptor, const void* buf, std::size_t nbytes); #elif defined(__APPLE__) std::int64_t WritevAtEnd(int descriptor, const iovec* iov, int iovcnt); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index c77a275ab93..bf999473524 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -387,14 +388,34 @@ Entry& Materialize(int fd) { } int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { - std::lock_guard lock(g_mutex); - auto& status = *Materialize(fd).status; - const int next = (status & ~mask) | (value & mask); - const int changed = status ^ next; - if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Socket || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); - ApplyHostStatus(fd, kind, changed, next, function); - status = next; - return 0; + for (;;) { + std::vector> admissions; + std::unique_lock lock(g_mutex); + auto& status = *Materialize(fd).status; + const int next = (status & ~mask) | (value & mask); + const int changed = status ^ next; +#ifdef _WIN32 + if ((changed & Append) && kind == Kind::File) { + bool blocked = false; + for (const auto& [descriptor, entry] : g_entries) { + if (entry.status.get() != &status) continue; + admissions.push_back(std::make_unique(descriptor, GuestDescriptorAccess::Mode::Update, std::defer_lock)); + if (!admissions.back()->TryLock()) { blocked = true; break; } + } + if (blocked) { + auto waiting = std::move(admissions.back()); + lock.unlock(); + admissions.clear(); + waiting->Lock(); + continue; + } + } +#endif + if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Socket || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); + ApplyHostStatus(fd, kind, changed, next, function); + status = next; + return 0; + } } int CurrentStatus(int fd) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 420a1f39257..5262968f1e1 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -20,6 +20,7 @@ #include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "SceTypes.hpp" @@ -74,8 +75,10 @@ static int NativeRead(int fd, void* buf, std::size_t n) { errno = EISDIR; return -1; } - const auto positioned = File::LockPositionedTransfer(fd); - const int result = ::_read(fd, buf, static_cast(n)); + const auto positioned = File::IoPositionedTransfer(fd); + if (!positioned) return -1; + const int result = ThreadCancel::Native([&] { return ::_read(fd, buf, static_cast(n)); }); + if (result < 0 && _doserrno == ERROR_OPERATION_ABORTED) errno = EINTR; if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; return result; } @@ -155,7 +158,7 @@ static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { return result == -2 ? ::lseek(fd, static_cast(offset), whence) : result; } static std::int64_t NativeRead(int fd, void* buf, std::size_t n) { - return ::read(fd, buf, n); + return ThreadCancel::Native([&] { return ::read(fd, buf, n); }); } static std::int64_t NativeWrite(int fd, const void* buf, std::size_t n) { const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); @@ -394,6 +397,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std int APS5_VABI sceKernelClose(int d) { if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EPERM; + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Close); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) return socketError == 0 ? 0 : SceKernelError(socketError); if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 8a2230af73c..51995e5d658 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/Api.hpp" #include #include @@ -297,7 +298,7 @@ struct PositionedLock { }; std::mutex g_positionedTableLock; std::unordered_map> g_positionedLocks; -File::PositionedTransferLock File::LockPositionedTransfer(int descriptor) { +static std::shared_ptr PositionedMutex(int descriptor) { std::shared_ptr entry; { std::lock_guard table(g_positionedTableLock); @@ -305,8 +306,39 @@ File::PositionedTransferLock File::LockPositionedTransfer(int descriptor) { if (!slot) slot = std::make_shared(); entry = slot; } + return entry; +} +File::PositionedTransferLock File::LockPositionedTransfer(int descriptor) { + const auto entry = PositionedMutex(descriptor); + if (FileStreamCloseHookActive_nid_no_patch()) { + std::unique_lock lock(entry->mutex, std::try_to_lock); + if (!lock.owns_lock()) { + FileStreamCloseEventForTest_nid_no_patch(descriptor, 1); + lock.lock(); + } + return PositionedTransferLock(std::shared_ptr(entry, &entry->mutex), std::move(lock)); + } return PositionedTransferLock(std::shared_ptr(entry, &entry->mutex)); } +std::optional File::IoPositionedTransfer(int descriptor) { + if (!ThreadCancel::IoActive()) return LockPositionedTransfer(descriptor); + const auto entry = PositionedMutex(descriptor); + TimedWait::Condition changed; + std::mutex mutex; + ThreadCancel::IoWait wait([](void* argument) noexcept { + static_cast(argument)->NotifyAll(); + }, &changed); + for (;;) { + if (ThreadCancel::IoInterrupted()) { + errno = EINTR; + return std::nullopt; + } + std::unique_lock lock(entry->mutex, std::try_to_lock); + if (lock.owns_lock()) return PositionedTransferLock(std::shared_ptr(entry, &entry->mutex), std::move(lock)); + std::unique_lock sleep(mutex); + changed.WaitUntil(sleep, TimedWait::DeadlineNanos(1000)); + } +} void File::SharePositionedTransfer(int from, int to) { std::lock_guard table(g_positionedTableLock); auto& source = g_positionedLocks[from]; @@ -353,20 +385,22 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz overlapped.OffsetHigh = static_cast(at >> 32u); DWORD done = 0; const BOOL ok = write ? ::WriteFile(handle, buf, static_cast(nbytes), &done, &overlapped) - : ::ReadFile(handle, buf, static_cast(nbytes), &done, &overlapped); + : ThreadCancel::Native([&] { return ::ReadFile(handle, buf, static_cast(nbytes), &done, &overlapped) ? 0 : -1; }) == 0; const DWORD error = ok ? ERROR_SUCCESS : ::GetLastError(); if (!::SetFilePointerEx(handle, position, nullptr, FILE_BEGIN)) { throw std::runtime_error("NativePositioned: cannot restore the file offset"); } if (!ok) { if (!write && error == ERROR_HANDLE_EOF) return 0; - errno = error == ERROR_ACCESS_DENIED ? EBADF : EIO; + errno = ThreadCancel::IoInterrupted() ? EINTR : error == ERROR_ACCESS_DENIED ? EBADF : EIO; return -1; } return done; } std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { - const auto positioned = File::LockPositionedTransfer(descriptor); + const auto positioned = File::IoPositionedTransfer(descriptor); + if (!positioned) return -1; + if (write) FileStreamCloseEventForTest_nid_no_patch(descriptor, 0); return NativePositionedTransfer(descriptor, buf, nbytes, offset, write); } std::int64_t File::WriteAtEnd(int descriptor, const void* buf, std::size_t nbytes) { @@ -407,7 +441,11 @@ static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t if (appended != -2) return appended; } } - if (!write) return ::_read(descriptor, buf, static_cast(nbytes)); + if (!write) { + const auto result = ThreadCancel::Native([&] { return ::_read(descriptor, buf, static_cast(nbytes)); }); + if (result < 0 && _doserrno == ERROR_OPERATION_ABORTED) errno = EINTR; + return result; + } const int result = ::_write(descriptor, buf, static_cast(nbytes)); if (result < 0 && (_doserrno == ERROR_NO_DATA || _doserrno == ERROR_BROKEN_PIPE) && ::GetFileType(reinterpret_cast(::_get_osfhandle(descriptor))) == FILE_TYPE_PIPE) errno = EPIPE; return result; @@ -483,7 +521,7 @@ static int NativeFlock(int descriptor, int operation) { return ::flock(descriptor, operation); } static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { - return static_cast(::pread(descriptor, buf, nbytes, static_cast(offset))); + return ThreadCancel::Native([&] { return static_cast(::pread(descriptor, buf, nbytes, static_cast(offset))); }); } #ifdef __APPLE__ std::int64_t File::WritevAtEnd(int descriptor, const iovec* iov, int iovcnt) { @@ -528,6 +566,7 @@ static constexpr int GUEST_ENOTDIR = 20; static constexpr int GUEST_EISDIR = 21; static constexpr int GUEST_EWOULDBLOCK = 35; static constexpr int GUEST_EOPNOTSUPP = 45; +static constexpr int GUEST_EINTR = 4; static constexpr int GUEST_ENOTEMPTY = 66; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_ELOOP = 62; @@ -899,8 +938,64 @@ int APS5_VABI nmount_nid_postfix(const KernelIovec* iov, unsigned int niov, int return -1; } +} + +File::IoDescriptor::IoDescriptor(int descriptor) + : value(descriptor), access(descriptor, GuestDescriptorAccess::Mode::Read, std::defer_lock) { + if (descriptor < 0) { + *__error_nid_postfix() = GUEST_EBADF; + return; + } + if (!ThreadCancel::TrackIoDescriptor(descriptor)) { + value = -1; + *__error_nid_postfix() = GUEST_EINTR; + return; + } + if (access.TryLock()) { + if (!ThreadCancel::TrackIoDescriptor(descriptor)) { + value = -1; + *__error_nid_postfix() = GUEST_EINTR; + return; + } + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + value = -1; + *__error_nid_postfix() = GUEST_EBADF; + } else operation.emplace(access); + return; + } + ThreadCancel::IoWait wait([](void* argument) noexcept { + static_cast(argument)->Wake(); + }, &access); + for (;;) { + if (ThreadCancel::IoInterrupted()) { + value = -1; + *__error_nid_postfix() = GUEST_EINTR; + return; + } + if (access.ReadOrWait()) { + if (!ThreadCancel::TrackIoDescriptor(descriptor)) { + value = -1; + *__error_nid_postfix() = GUEST_EINTR; + return; + } + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + value = -1; + *__error_nid_postfix() = GUEST_EBADF; + } else operation.emplace(access); + return; + } + } +} + +File::IoDescriptor::~IoDescriptor() { + if (operation && operation->Interrupted()) ThreadCancel::IoNoRestart(); +} + +extern "C" { + int APS5_VABI close_nid_postfix(int d) { if (File::IsHostStream(d)) return PosixFailure(GUEST_EPERM); + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Close); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; @@ -950,6 +1045,7 @@ int File::HostStandardError() { extern "C" { int APS5_VABI dup_nid_postfix(int d) { + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Read); if (GuestSockets::IsOpen(d)) return GuestSockets::Duplicate(d); if (d < 0) return PosixFailure(GUEST_EBADF); if (IsKqueueDescriptor_nid_no_patch(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); @@ -970,6 +1066,19 @@ int APS5_VABI dup_nid_postfix(int d) { int APS5_VABI dup2_nid_postfix(int from, int to) { if (from < 0 || to < 0) return PosixFailure(GUEST_EBADF); + { + const GuestDescriptorAccess validation(from, GuestDescriptorAccess::Mode::Read); + const bool socket = GuestSockets::IsOpen(from); + if (socket && from != to && to <= 2) + throw std::runtime_error(std::string(__func__) + ": moving a guest socket onto standard descriptor " + std::to_string(to) + " is not supported"); + if (!socket && from != to && IsKqueueDescriptor_nid_no_patch(from)) + throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); + if (!socket && !DescriptorIsOpen_nid_no_patch(from)) return PosixFailure(GUEST_EBADF); + } + GuestDescriptorAccess source(from, GuestDescriptorAccess::Mode::Read, std::defer_lock); + GuestDescriptorAccess target(to, GuestDescriptorAccess::Mode::Change, std::defer_lock); + if (from == to) source.Lock(); + else GuestDescriptorAccess::LockTogether(source, target); if (GuestSockets::IsOpen(from)) { if (from != to && to <= 2) throw std::runtime_error(std::string(__func__) + ": moving a guest socket onto standard descriptor " + std::to_string(to) + " is not supported"); @@ -1171,17 +1280,22 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { - if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); - if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (const int error = PositionedArgumentError(d, false, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); - if (FailPosixPositionedGuestSocket(d)) return -1; - const GuestArena::HostWrite destination(buf, nbytes); - if (!destination.Open()) return PosixFailure(GUEST_EFAULT); - if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); - if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; - char empty = 0; - const auto result = NativePread(d, buf == nullptr ? &empty : buf, nbytes, offset); - return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; + return ThreadCancel::Io([&]() -> std::int64_t { + if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + if (IsKqueueDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_ESPIPE); + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + if (const int error = PositionedArgumentError(d, false, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); + if (FailPosixPositionedGuestSocket(d)) return -1; + const GuestArena::HostWrite destination(buf, nbytes); + if (!destination.Open()) return PosixFailure(GUEST_EFAULT); + if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; + char empty = 0; + const auto result = NativePread(d, buf == nullptr ? &empty : buf, nbytes, offset); + return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; + }); + }); } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { @@ -1201,19 +1315,27 @@ std::int64_t PosixPwrite_nid_no_patch(int d, const void* buf, std::size_t nbytes } int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { - if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (GuestSockets::IsOpen(d)) return GuestSockets::Read(d, buf, static_cast(nbytes)); - if (const int error = TransferArgumentError(d, buf == nullptr && nbytes != 0)) return PosixFailure(error); - char emptyBuffer = 0; - return PosixResult(sceKernelRead(d, buf == nullptr ? &emptyBuffer : buf, static_cast(nbytes))); + return ThreadCancel::Io([&]() -> std::int64_t { + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + if (GuestSockets::IsOpen(d)) return GuestSockets::Read(d, buf, static_cast(nbytes)); + if (const int error = TransferArgumentError(d, buf == nullptr && nbytes != 0)) return PosixFailure(error); + char emptyBuffer = 0; + return PosixResult(sceKernelRead(d, buf == nullptr ? &emptyBuffer : buf, static_cast(nbytes))); + }); + }); } std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { - if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); - if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Read(descriptor, buffer, count); - if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); - char emptyBuffer = 0; - return PosixResult(sceKernelRead(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); + return ThreadCancel::Io([&]() -> std::int64_t { + if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); + return File::WithIoDescriptor(descriptor, [&](int descriptor) -> std::int64_t { + if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Read(descriptor, buffer, count); + if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); + char emptyBuffer = 0; + return PosixResult(sceKernelRead(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); + }); + }); } int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { @@ -1676,7 +1798,8 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn if (!write) { if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, offset)) return *archived; } - const auto positioned = File::LockPositionedTransfer(d); + const auto positioned = File::IoPositionedTransfer(d); + if (!positioned) return SceErrorFromErrno(errno); if (total == 0) { char none = 0; const auto result = offset != nullptr ? NativePositionedTransfer(d, &none, 0, *offset, write) : NativeTransferBuffer(d, &none, 0, write); @@ -1784,7 +1907,8 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) #ifdef __APPLE__ if (iovcnt == 0) return EmptyIovecsResult(d, false, false); #endif - const auto result = static_cast(::readv(d, NativeIovecs(iov), iovcnt)); + const auto result = ThreadCancel::Native([&] { return static_cast(::readv(d, NativeIovecs(iov), iovcnt)); }); + return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1822,7 +1946,8 @@ int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* guestIov, int iovcnt #ifdef __APPLE__ if (iovcnt == 0) return EmptyIovecsResult(d, false, true); #endif - const auto result = static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + const auto result = ThreadCancel::Native([&] { return static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); }); + return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1848,8 +1973,16 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcn #endif int64_t APS5_VABI preadv_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { - const auto result = sceKernelPreadv(d, iov, iovcnt, offset); - return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + return ThreadCancel::Io([&]() -> std::int64_t { + if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); + std::vector vectors; + if (const int error = CopyIovecs(iov, iovcnt, vectors)) return PosixFailure(error & 0xffff); + if (IsKqueueDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_ESPIPE); + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + const auto result = sceKernelPreadv(d, vectors.data(), iovcnt, offset); + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + }); + }); } int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { @@ -2060,8 +2193,15 @@ int APS5_VABI fdatasync_nid_postfix(int fd) { } int64_t APS5_VABI readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { - const auto result = sceKernelReadv(d, iov, iovcnt); - return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + return ThreadCancel::Io([&]() -> std::int64_t { + if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); + std::vector vectors; + if (const int error = CopyIovecs(iov, iovcnt, vectors)) return PosixFailure(error & 0xffff); + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + const auto result = sceKernelReadv(d, vectors.data(), iovcnt); + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + }); + }); } int64_t APS5_VABI writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index e45cbaee3ff..be3cbfa25fe 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -3,8 +3,11 @@ #include "SceTypes.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libc/include/DescriptorAccess.hpp" #include +#include #include +#include inline constexpr int GUEST_CANCEL_ENABLE = 0; inline constexpr int GUEST_CANCEL_DISABLE = 1; @@ -26,6 +29,10 @@ void APS5_VABI pthread_cleanup_push_nid_postfix(void (APS5_VABI* routine)(void*) void APS5_VABI pthread_cleanup_pop_nid_postfix(int execute); } +#ifdef _WIN32 +extern "C" void GuestExceptionQueueIoForTest_nid_no_patch(void (APS5_VABI* handler)(int, void*)); +#endif + namespace ThreadCancel { bool Requested(); @@ -35,6 +42,103 @@ void CleanupRestore(void* mark); void BeginWait(TimedWait::Condition* condition); void EndWait(); +bool IoEnabled(); +bool IoActive(); +bool IoInterrupted(); +bool IoRestartError(); +class IoRestart { +public: + struct State; + explicit IoRestart(bool allowed); + ~IoRestart(); + bool Ready() const; + void Clear(); + IoRestart(const IoRestart&) = delete; + IoRestart& operator=(const IoRestart&) = delete; +private: + friend void BeginIoSignals(Pthread, IoRestart&, std::optional&); + State* state; +}; +void BeginIoSignals(Pthread thread, IoRestart& restart, std::optional& deferral); +void EndIoSignals(std::optional& deferral); +bool DeferIoSignal(int signal, bool restart = true); +bool DeferIoCallback(void (*callback)(), void (*retire)()); +void WakeIoDelivery(Pthread thread); +bool TrackIoDescriptor(int descriptor); +void IoNoRestart(); +void RetireIoStack(std::uintptr_t stack, std::uint32_t context = 0); + +class IoPoint { +public: + explicit IoPoint(IoRestart& restart); + ~IoPoint() noexcept(false); + IoPoint(const IoPoint&) = delete; + IoPoint& operator=(const IoPoint&) = delete; + +private: + std::optional delivery; +}; + +class IoWait { +public: + using Interrupt = void (*)(void*) noexcept; + IoWait(Interrupt interrupt, void* argument); + ~IoWait(); + IoWait(const IoWait&) = delete; + IoWait& operator=(const IoWait&) = delete; + void Wake() noexcept { interrupt(argument); } + +private: + void BeginSignal(); + void EndSignal(); + Interrupt interrupt; + void* argument; + IoWait* previous = nullptr; + Pthread thread = nullptr; + void* signalState = nullptr; + IoWait* signalPrevious = nullptr; + GuestDescriptorAccess::Operation::WakeRegistration descriptorWake; +}; + +class NativeIo { +public: + NativeIo(); + ~NativeIo(); + static void InterruptError(); + NativeIo(const NativeIo&) = delete; + NativeIo& operator=(const NativeIo&) = delete; + +private: + struct State; + State* state = nullptr; +}; + +template +auto Native(TCall&& call) { + NativeIo operation; + if (IoInterrupted()) { + NativeIo::InterruptError(); + return static_cast(-1); + } + return std::forward(call)(); +} + +template +auto Io(TCall&& call, bool restartable = true) { + Check(); + IoRestart restart(restartable); + for (;;) { + decltype(call()) result; + { + IoPoint point(restart); + result = call(); + } + if (result >= 0) return result; + if (Requested()) { restart.Clear(); Check(); } + if (!restart.Ready()) return result; + } +} + template struct WaitLock { TLock& inner; diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 0f1c2d83fdd..136e46408d6 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -127,6 +127,8 @@ struct PthreadAttrPrivate { int _scope = GUEST_SCOPE_SYSTEM; }; +namespace ThreadCancel { class IoWait; } + inline std::atomic nextThreadId{100000}; struct PthreadPrivate { @@ -173,6 +175,7 @@ struct PthreadPrivate { TimedWait::Condition _join_cv; std::atomic cancelPending{false}; std::mutex cancelLock; + ThreadCancel::IoWait* cancelIo = nullptr; TimedWait::Condition* cancelWait = nullptr; bool cancelWaitExiting = false; diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp new file mode 100644 index 00000000000..fbf75a32807 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -0,0 +1,463 @@ +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libc/include/DescriptorAccess.hpp" +#include "prx/libc/include/JumpBuffer.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +namespace ThreadCancel { + +struct IoRestart::State { + struct Descriptor { + int number; + GuestDescriptorAccess::Version version; + }; + State* previous; + std::uintptr_t stack; + std::uint32_t context; + bool allowed; + bool ready = false; + std::vector expected; + State(State* previous, std::uintptr_t stack, bool allowed) + : previous(previous), stack(stack), context(JumpBuffer::GuestContextCurrent_nid_no_patch()), allowed(allowed) {} + bool Abandoned(std::uintptr_t target, std::uint32_t targetContext) const { return context == targetContext && stack < target; } +}; + +namespace { + +class SignalManager; +std::atomic published{nullptr}; + +class SetupBlock { +public: + SetupBlock() { +#ifndef _WIN32 + sigset_t signals; + sigfillset(&signals); + if (const int error = pthread_sigmask(SIG_BLOCK, &signals, &previous); error != 0) + throw std::system_error(error, std::generic_category(), "I/O delivery: blocking setup signals"); +#endif + } + ~SetupBlock() { +#ifndef _WIN32 + pthread_sigmask(SIG_SETMASK, &previous, nullptr); +#endif + } +#ifndef _WIN32 + const sigset_t& Previous() const { return previous; } +#endif +private: +#ifndef _WIN32 + sigset_t previous{}; +#endif +}; + +struct SignalState { + SignalManager* manager; + Pthread guest; + std::mutex mutex; + IoWait* wait = nullptr; + bool active = true; + std::atomic signals{0}; + std::atomic delivery{false}; + std::atomic restart{true}; + void (*callback)() = nullptr; + void (*retire)() = nullptr; + IoRestart::State* replay = nullptr; + std::vector descriptors; + std::shared_ptr previousDelivery; +#ifndef _WIN32 + sigset_t deliveryMask{}; + sigset_t suspendedMask{}; + bool suspended = false; +#endif + + bool Pending() const { return signals.load() != 0 || delivery.load(); } +}; + +static_assert(std::atomic::is_always_lock_free); +constinit thread_local SignalState* current = nullptr; +struct SignalOwnership { + std::shared_ptr owner; + std::shared_ptr delivering; +}; +SignalOwnership& Ownership() { return HostThreadLocal(); } +constinit thread_local IoRestart::State* restarts = nullptr; +constinit thread_local unsigned depth = 0; + +class SignalManager { +public: + std::mutex mutex; + std::vector> entries; + + SignalManager() { +#ifdef _WIN32 + event = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!event) throw std::system_error(GetLastError(), std::system_category(), "I/O delivery: creating wake event"); +#elif defined(__APPLE__) + static std::atomic serial{0}; + char name[32]; + do { + std::snprintf(name, sizeof(name), "/a5io-%x-%x", static_cast(getpid()), serial.fetch_add(1)); + event = sem_open(name, O_CREAT | O_EXCL, 0600, 0); + } while (event == SEM_FAILED && errno == EEXIST); + if (event == SEM_FAILED) throw std::system_error(errno, std::generic_category(), "I/O delivery: creating wake semaphore"); + if (sem_unlink(name) != 0) { + const int error = errno; + sem_close(event); + throw std::system_error(error, std::generic_category(), "I/O delivery: unlinking wake semaphore"); + } +#else + if (sem_init(&event, 0, 0) != 0) throw std::system_error(errno, std::generic_category(), "I/O delivery: creating wake semaphore"); +#endif + try { + std::thread([this] { Run(); }).detach(); + } catch (...) { +#ifdef _WIN32 + CloseHandle(event); +#elif defined(__APPLE__) + sem_close(event); +#else + sem_destroy(&event); +#endif + throw; + } + } + + void Notify() noexcept { +#ifdef _WIN32 + SetEvent(event); +#else + sem_post(Semaphore()); +#endif + } + +private: +#ifdef _WIN32 + HANDLE event; +#elif defined(__APPLE__) + sem_t* event; +#else + sem_t event{}; +#endif + +#ifndef _WIN32 + sem_t* Semaphore() noexcept { +#ifdef __APPLE__ + return event; +#else + return &event; +#endif + } +#endif + + void Run() { + for (;;) { +#ifdef _WIN32 + WaitForSingleObject(event, INFINITE); +#else + while (sem_wait(Semaphore()) != 0 && errno == EINTR) {} +#endif + bool pending; + auto retry = std::chrono::milliseconds(1); + do { + pending = false; + { + std::lock_guard registered(mutex); + for (const auto& state : entries) { + std::lock_guard operation(state->mutex); + if (!state->active || !state->Pending()) continue; + pending = true; + if (state->wait) state->wait->Wake(); + } + } + if (pending) { + std::this_thread::sleep_for(retry); + retry = std::min(retry * 2, std::chrono::milliseconds(16)); + } + } while (pending); + } + } +}; + +SignalManager& Manager() { + static auto* manager = [] { + auto* value = new SignalManager; + published.store(value, std::memory_order_release); + return value; + }(); + return *manager; +} + +} + +IoRestart::IoRestart(bool allowed) { + const SetupBlock blocked; + JumpBuffer::GuestIoJumpRegister_nid_no_patch(RetireIoStack); + state = new State(restarts, reinterpret_cast(this), allowed); + restarts = state; +} + +IoRestart::~IoRestart() { + const int saved = errno; + const SetupBlock blocked; + auto** cursor = &restarts; + while (*cursor && (*cursor != state || (*cursor)->stack != reinterpret_cast(this))) cursor = &(*cursor)->previous; + if (*cursor) { + *cursor = state->previous; + delete state; + } + errno = saved; +} + +void IoRestart::Clear() { + const int saved = errno; + const SetupBlock blocked; + state->ready = false; + state->expected.clear(); + errno = saved; +} + +bool IoRestart::Ready() const { return state->ready && IoRestartError(); } + +void BeginIoSignals(Pthread thread, IoRestart& restart, std::optional& deferral) { + if (current) { + if (!deferral) deferral.emplace(); + ++depth; + return; + } + const SetupBlock blocked; + auto& ownership = Ownership(); + auto& manager = Manager(); + auto state = std::make_shared(); + state->manager = &manager; + state->guest = thread; + state->replay = restart.state; + restart.state->ready = false; + if (!deferral) deferral.emplace(); + { + std::lock_guard lock(manager.mutex); + manager.entries.push_back(state); + } + ownership.owner = std::move(state); + current = ownership.owner.get(); + depth = 1; +#ifdef _WIN32 + TimedWait::DeliverDeferred(); +#endif +} + +void EndIoSignals(std::optional& deferral) { + if (--depth != 0) { deferral.reset(); return; } + const int saved = errno; + SignalState* state; + bool pending; + { + const SetupBlock blocked; + auto finishing = std::move(Ownership().owner); + state = finishing.get(); +#ifndef _WIN32 + state->deliveryMask = blocked.Previous(); +#endif + { + std::lock_guard lock(state->mutex); + state->active = false; + } + { + std::lock_guard lock(state->manager->mutex); + std::erase(state->manager->entries, finishing); + } + current = nullptr; + pending = state->Pending(); + state->previousDelivery = std::move(Ownership().delivering); + Ownership().delivering = std::move(finishing); + deferral.reset(); + } + for (int signal = 1; signal < 64; ++signal) + if ((state->signals.fetch_and(~(std::uint64_t{1} << signal)) & (std::uint64_t{1} << signal)) != 0) std::raise(signal); + if (state->callback) state->callback(); + state->callback = nullptr; + TimedWait::DeliverDeferred(); + { + const SetupBlock blocked; + const bool unchanged = std::all_of(state->descriptors.begin(), state->descriptors.end(), + [](const auto& descriptor) { return descriptor.version.Unchanged(); }); + state->replay->ready = pending && state->restart.load() && state->replay->allowed && !state->descriptors.empty() && unchanged; + if (state->replay->ready) state->replay->expected = std::move(state->descriptors); + else state->replay->expected.clear(); + auto* cursor = &Ownership().delivering; + while (*cursor && cursor->get() != state) cursor = &(*cursor)->previousDelivery; + if (*cursor) *cursor = std::move(state->previousDelivery); + } + errno = saved; +} + +bool IoInterrupted() { + if (GuestDescriptorAccess::Operation::CurrentInterrupted()) { IoNoRestart(); return true; } + return (current && current->Pending()) || (IoEnabled() && Requested()); +} + +bool DeferIoSignal(int signal, bool restart) { + if (!current || signal <= 0 || signal >= 64) return false; + const int saved = errno; + if (!restart) current->restart.store(false); + current->signals.fetch_or(std::uint64_t{1} << signal); + current->manager->Notify(); + errno = saved; + return true; +} + +bool DeferIoCallback(void (*callback)(), void (*retire)()) { + if (!current) return false; + current->callback = callback; + current->retire = retire; + current->delivery.store(true); + current->manager->Notify(); + return true; +} + +void IoNoRestart() { if (current) current->restart.store(false); } + +bool TrackIoDescriptor(int descriptor) { + if (!current || descriptor < 0) return true; + GuestDescriptorAccess access(descriptor, GuestDescriptorAccess::Mode::Read, std::defer_lock); + const auto expected = std::find_if(current->replay->expected.begin(), current->replay->expected.end(), + [descriptor](const auto& entry) { return entry.number == descriptor; }); + if (expected != current->replay->expected.end() && !expected->version.Unchanged()) { + current->restart.store(false); + return false; + } + auto observed = std::find_if(current->descriptors.begin(), current->descriptors.end(), + [descriptor](const auto& entry) { return entry.number == descriptor; }); + if (observed == current->descriptors.end()) current->descriptors.push_back({descriptor, access.Snapshot()}); + else observed->version = access.Snapshot(); + return true; +} + +void RetireIoStack(std::uintptr_t stack, std::uint32_t context) { + std::uint64_t signals = 0; + void (*callback)() = nullptr; +#ifndef _WIN32 + std::optional mask; +#endif + { + const SetupBlock blocked; + const bool exiting = stack == std::numeric_limits::max(); + if (!current && !restarts) { + if (!exiting) JumpBuffer::GuestContextSet_nid_no_patch(context); + return; + } + const auto previousContext = JumpBuffer::GuestContextCurrent_nid_no_patch(); + if (!exiting) JumpBuffer::GuestContextSet_nid_no_patch(context); +#ifdef _WIN32 + auto* ownership = FindHostThreadLocal(); +#else + auto* ownership = &Ownership(); +#endif + std::shared_ptr noDelivery; + auto* deliveryCursor = ownership ? &ownership->delivering : &noDelivery; +#ifndef _WIN32 + const auto here = reinterpret_cast(&blocked); + bool savedCurrent = false; +#endif + while (*deliveryCursor) { + auto& delivery = **deliveryCursor; +#ifndef _WIN32 + if (!exiting && !savedCurrent && delivery.replay->context == previousContext && here < delivery.replay->stack) { + delivery.suspendedMask = blocked.Previous(); + delivery.suspended = true; + savedCurrent = true; + mask = delivery.deliveryMask; + } +#endif + if (!exiting && !delivery.replay->Abandoned(stack, context)) { + deliveryCursor = &delivery.previousDelivery; + continue; + } + signals |= delivery.signals.load(); + if (const auto next = exiting ? delivery.retire : delivery.callback) callback = next; +#ifndef _WIN32 + if (!exiting) mask = delivery.deliveryMask; +#endif + *deliveryCursor = std::move(delivery.previousDelivery); + } +#ifndef _WIN32 + if (!exiting) { + for (auto delivery = ownership->delivering; delivery; delivery = delivery->previousDelivery) { + if (delivery->suspended && delivery->replay->context == context && stack < delivery->replay->stack) { + mask = delivery->suspendedMask; + break; + } + } + } +#endif + auto** restartCursor = &restarts; + while (*restartCursor) { + if (!exiting && !(*restartCursor)->Abandoned(stack, context)) { + restartCursor = &(*restartCursor)->previous; + continue; + } + auto* retired = *restartCursor; + *restartCursor = retired->previous; + delete retired; + } + if (exiting) signals = 0; + } +#ifndef _WIN32 + if (mask) pthread_sigmask(SIG_SETMASK, &*mask, nullptr); +#endif + for (int signal = 1; signal < 64; ++signal) + if ((signals & (std::uint64_t{1} << signal)) != 0) std::raise(signal); + if (callback) callback(); +} + +void WakeIoDelivery(Pthread thread) { + if (!thread) return; + auto* manager = published.load(std::memory_order_acquire); + if (!manager) return; + std::lock_guard registered(manager->mutex); + for (const auto& state : manager->entries) { + if (state->guest != thread) continue; + state->delivery.store(true); + manager->Notify(); + } +} + +void IoWait::BeginSignal() { + if (!current) return; + std::lock_guard lock(current->mutex); + signalState = current; + signalPrevious = current->wait; + current->wait = this; +} + +void IoWait::EndSignal() { + if (!signalState) return; + auto& state = *static_cast(signalState); + std::lock_guard lock(state.mutex); + state.wait = signalPrevious; +} + +} diff --git a/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp new file mode 100644 index 00000000000..a512c9da436 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp @@ -0,0 +1,121 @@ +#include "prx/libkernel/Pthread/include/Cancel.hpp" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +namespace ThreadCancel { + +struct NativeIo::State { +#ifdef _WIN32 + HANDLE thread = nullptr; +#else + pthread_t thread = pthread_self(); + sigset_t previous{}; + sigset_t signal{}; +#endif + std::optional wait; + + static void Interrupt(void* argument) noexcept { + auto& state = *static_cast(argument); +#ifdef _WIN32 + CancelSynchronousIo(state.thread); +#else + pthread_kill(state.thread, SIGUSR2); +#endif + } + + State() { +#ifdef _WIN32 + if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &thread, + THREAD_TERMINATE, FALSE, 0)) + throw std::system_error(GetLastError(), std::system_category(), "cancellable I/O: retaining thread handle"); +#else + static const bool installed = [] { + struct sigaction previous{}; + if (sigaction(SIGUSR2, nullptr, &previous) != 0) + throw std::system_error(errno, std::generic_category(), "cancellable I/O: querying interrupt signal"); + if (previous.sa_handler != SIG_DFL) + throw std::runtime_error("cancellable I/O: SIGUSR2 is already in use"); + struct sigaction action{}; + action.sa_handler = [](int) {}; + sigemptyset(&action.sa_mask); + if (sigaction(SIGUSR2, &action, nullptr) != 0) + throw std::system_error(errno, std::generic_category(), "cancellable I/O: installing interrupt signal"); + return true; + }(); + static_cast(installed); + sigemptyset(&signal); + sigaddset(&signal, SIGUSR2); + if (const int error = pthread_sigmask(SIG_UNBLOCK, &signal, &previous); error != 0) + throw std::system_error(error, std::generic_category(), "cancellable I/O: unblocking interrupt signal"); +#endif + try { + wait.emplace(Interrupt, this); + } catch (...) { + release(); + throw; + } + } + + ~State() { release(); } + +private: + void release() { +#ifdef _WIN32 + const DWORD error = GetLastError(); +#endif + const int saved = errno; + wait.reset(); +#ifdef _WIN32 + CloseHandle(thread); +#else + pthread_sigmask(SIG_BLOCK, &signal, nullptr); + sigset_t pending{}; + while (sigpending(&pending) == 0 && sigismember(&pending, SIGUSR2) == 1) { + int received = 0; + sigwait(&signal, &received); + } + pthread_sigmask(SIG_SETMASK, &previous, nullptr); +#endif + errno = saved; +#ifdef _WIN32 + SetLastError(error); +#endif + } +}; + +NativeIo::NativeIo() { + if (IoActive()) state = new State; +} + +void NativeIo::InterruptError() { + errno = EINTR; +#ifdef _WIN32 + _doserrno = ERROR_OPERATION_ABORTED; + SetLastError(ERROR_OPERATION_ABORTED); +#endif +} + +NativeIo::~NativeIo() { +#ifdef _WIN32 + const DWORD error = GetLastError(); +#endif + const int saved = errno; + delete state; + errno = saved; +#ifdef _WIN32 + SetLastError(error); +#endif +} + +} diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 0d9151719f6..e284752961a 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -822,6 +822,7 @@ static void finishThread(PthreadPrivate* self, void* retval) { throw std::runtime_error("Guest thread is already finishing"); CurrentGuestState().finishing = true; CurrentGuestState().exiting = true; + ThreadCancel::RetireIoStack(std::numeric_limits::max()); UnregisterStack(self); if (const auto callback = threadDtors.load()) callback(); @@ -865,6 +866,48 @@ void ThreadCancel::EndWait() { BeginWait(nullptr); } +static thread_local unsigned ioPoints = 0; + +ThreadCancel::IoPoint::IoPoint(IoRestart& restart) { +#ifdef _WIN32 + delivery.emplace(); +#endif + BeginIoSignals(CurrentGuestState().current, restart, delivery); + ++ioPoints; +} +ThreadCancel::IoPoint::~IoPoint() noexcept(false) { --ioPoints; EndIoSignals(delivery); } + +bool ThreadCancel::IoActive() { return ioPoints != 0; } +bool ThreadCancel::IoRestartError() { return *__error_nid_postfix() == 4; } + +bool ThreadCancel::IoEnabled() { + return ioPoints != 0 && CurrentGuestState().current && !CurrentGuestState().exiting && + CurrentGuestState().cancelState == GUEST_CANCEL_ENABLE; +} + +ThreadCancel::IoWait::IoWait(Interrupt interrupt, void* argument) + : interrupt(interrupt), argument(argument), descriptorWake(interrupt, argument) { + BeginSignal(); + if (!IoEnabled()) return; + thread = CurrentGuestState().current; + try { + std::lock_guard lock(thread->cancelLock); + previous = thread->cancelIo; + thread->cancelIo = this; + } catch (...) { + EndSignal(); + throw; + } +} + +ThreadCancel::IoWait::~IoWait() { + if (thread) { + std::lock_guard lock(thread->cancelLock); + thread->cancelIo = previous; + } + EndSignal(); +} + extern "C" void* APS5_VABI Aps5CallGuestEntry_nid_no_patch(PthreadEntry entry, void* arg); asm(".text\n" @@ -1338,6 +1381,7 @@ void APS5_VABI scePthreadExit(void* retval) { throw std::runtime_error("scePthreadExit: current thread is not registered"); auto* self = CurrentGuestState().current; CurrentGuestState().exiting = true; + ThreadCancel::RetireIoStack(std::numeric_limits::max()); while (CurrentGuestState().cleanupHandlers) __pthread_cleanup_pop_imp_nid_postfix(1); finishThread(self, retval); CurrentGuestState().current = nullptr; @@ -1420,8 +1464,10 @@ int APS5_VABI scePthreadCancel(Pthread thread) { for (;;) { { std::lock_guard lock(thread->cancelLock); - if (!thread->cancelWait || thread->cancelWaitExiting) return SCE_OK; - thread->cancelWait->NotifyAll(); + if (thread->cancelWaitExiting) return SCE_OK; + if (thread->cancelIo) thread->cancelIo->Wake(); + else if (!thread->cancelWait) return SCE_OK; + if (thread->cancelWait) thread->cancelWait->NotifyAll(); } scePthreadYield(); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 11d3088b207..9800901b3a5 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/Api.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -530,6 +531,7 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l } int GuestSockets::Close(int descriptor) { + const GuestDescriptorAccess descriptorLock(descriptor, GuestDescriptorAccess::Mode::Close); std::shared_ptr released; { std::lock_guard lock(socketsMutex); @@ -666,6 +668,7 @@ extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native) { } extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number) { + const GuestDescriptorAccess descriptorLock(number, GuestDescriptorAccess::Mode::Change); ReleaseNumber(number); } @@ -944,7 +947,92 @@ int SocketSendFlags(Socket& socket, int flags) { std::lock_guard lock(socket.modeMutex); return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); } +int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* address, socklen_t* addressLength) { +#ifdef _WIN32 + const bool cancellable = ThreadCancel::IoActive(); + bool nonblocking = (flags & GuestDontWait) != 0; + std::int64_t timeout = 0; + { + std::lock_guard lock(socket.modeMutex); + nonblocking = nonblocking || socket.options.nonblocking; + timeout = socket.options.receiveTimeout; + } + if (cancellable) { + if (timeout != 0) ThreadCancel::IoNoRestart(); + const bool waitAll = socket.type == 1 && (flags & GuestWaitAll) != 0 && !nonblocking; + if (waitAll && (flags & GuestPeek) != 0) { + WSASetLastError(WSAEOPNOTSUPP); + return -1; + } + int transferred = 0; + for (;;) { + OVERLAPPED operation{}; + operation.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!operation.hEvent) { + WSASetLastError(WSAENOBUFS); + return transferred != 0 ? transferred : -1; + } + struct Event { + HANDLE handle; + ~Event() { + const int error = WSAGetLastError(); + CloseHandle(handle); + WSASetLastError(error); + } + } event{operation.hEvent}; + struct Pending { + Socket& socket; + OVERLAPPED& operation; + static void Interrupt(void* argument) noexcept { + auto& pending = *static_cast(argument); + CancelIoEx(reinterpret_cast(pending.socket.value), &pending.operation); + } + } pending{socket, operation}; + DWORD done = 0; + DWORD nativeFlags = static_cast(NativeReceiveFlags(flags & ~GuestWaitAll, socket.type)); + int result = -1; + int error = WSAEINTR; + { + ThreadCancel::IoWait wait(Pending::Interrupt, &pending); + if (!ThreadCancel::IoInterrupted()) { + WSABUF destination{static_cast(length - transferred), + transferred == 0 ? static_cast(buffer) : static_cast(buffer) + transferred}; + const int started = address + ? WSARecvFrom(socket.value, &destination, 1, &done, &nativeFlags, address, addressLength, &operation, nullptr) + : WSARecv(socket.value, &destination, 1, &done, &nativeFlags, &operation, nullptr); + error = started == 0 ? 0 : WSAGetLastError(); + if (error == WSA_IO_PENDING) { + const auto milliseconds = nonblocking ? 0 : timeout == 0 ? INFINITE : static_cast(std::min( + ((timeout >> 32) * 1000) + ((static_cast(static_cast(timeout)) * 1000 + 0xffffffffu) >> 32), INFINITE - 1)); + const DWORD waited = WaitForSingleObject(operation.hEvent, milliseconds); + if (waited != WAIT_OBJECT_0) Pending::Interrupt(&pending); + const BOOL completed = WSAGetOverlappedResult(socket.value, &operation, &done, TRUE, &nativeFlags); + error = completed ? 0 : WSAGetLastError(); + if (waited == WAIT_TIMEOUT && error == WSA_OPERATION_ABORTED) error = WSAEWOULDBLOCK; + } + if (error == WSA_OPERATION_ABORTED && ThreadCancel::IoInterrupted()) error = WSAEINTR; + if (error == 0) result = static_cast(done); + } + } + WSASetLastError(error); + if (result < 0) return transferred != 0 ? transferred : result; + transferred += result; + if (!waitAll || result == 0 || transferred == length || ThreadCancel::IoInterrupted()) return transferred; + } + } + return address + ? ::recvfrom(socket.value, static_cast(buffer), length, NativeReceiveFlags(flags, socket.type), address, addressLength) + : ::recv(socket.value, static_cast(buffer), length, NativeReceiveFlags(flags, socket.type)); +#else + return ThreadCancel::Native([&] { return address + ? static_cast(::recvfrom(socket.value, buffer, length, NativeReceiveFlags(flags, socket.type), address, addressLength)) + : static_cast(::recv(socket.value, buffer, length, NativeReceiveFlags(flags, socket.type))); }); +#endif +} int WaitForTransfer(Socket& socket, bool send, int flags) { +#ifdef _WIN32 + if (!send && ThreadCancel::IoActive()) return 0; +#endif if ((flags & GuestDontWait) != 0) { #ifdef _WIN32 NativePollDescriptor entry{}; @@ -962,9 +1050,11 @@ int WaitForTransfer(Socket& socket, bool send, int flags) { timeout = send ? socket.options.sendTimeout : socket.options.receiveTimeout; } if (timeout == 0) return 0; + ThreadCancel::IoNoRestart(); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout >> 32) + std::chrono::nanoseconds((static_cast(static_cast(timeout)) * 1000000000) >> 32); for (;;) { + if (ThreadCancel::IoInterrupted()) return 4; const auto left = deadline - std::chrono::steady_clock::now(); if (left <= std::chrono::steady_clock::duration::zero()) return 35; const int milliseconds = static_cast(std::min(std::chrono::ceil(left).count(), INT_MAX)); @@ -976,7 +1066,7 @@ int WaitForTransfer(Socket& socket, bool send, int flags) { if (ready != 0) return 0; #else entry.events = static_cast(send ? POLLOUT : POLLIN); - const int ready = ::poll(&entry, 1, milliseconds); + const int ready = ThreadCancel::Native([&] { return ::poll(&entry, 1, milliseconds); }); if (ready > 0 || (ready < 0 && errno != EINTR)) return 0; #endif } @@ -1612,7 +1702,7 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); return result < 0 ? Fail(SendError(socket->type)) : result; } -std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { +static std::int64_t Receive(int descriptor, void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); @@ -1622,8 +1712,7 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!destination.Open()) return Fail(14); if (const int answered = ReceiveAnsweredHere(*socket, length); answered >= 0) return answered == 0 ? 0 : Fail(answered); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); - const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), - NativeReceiveFlags(flags, socket->type)); + const auto result = ReceiveNative(*socket, buffer, static_cast(length), flags, nullptr, nullptr); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); #endif @@ -1831,7 +1920,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st const int error = SendError(socket->type); return socket->type == 2 && address && SourceFaults(buffer, length) ? Fail(14) : Fail(error); } -std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, +static std::int64_t ReceiveFrom(int descriptor, void* buffer, std::uint64_t length, int flags, void* address, std::uint32_t* addressLength) { std::uint32_t capacity = 0; if (addressLength && !GuestCopyIn_nid_no_patch(&capacity, addressLength, sizeof(capacity))) return Fail(14); @@ -1873,8 +1962,8 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if (error) return Fail(error); } #endif - std::int64_t result = ::recvfrom(socket->value, static_cast(buffer), static_cast(limit), - NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); + auto result = ReceiveNative(*socket, buffer, static_cast(limit), flags, reinterpret_cast(&native), &size); + #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(limit); #endif @@ -2155,7 +2244,7 @@ static std::vector ArrivalTimestamps(const Socket& socket, int ke if (micro) append(2, seconds, nanoseconds / 1000); return records; } -std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags) { +static std::int64_t ReceiveMessage(int descriptor, GuestMsghdr* message, int flags) { GuestMsghdr header{}; if (!message || !ReadGuest(&header, message, sizeof(header))) return Fail(14); if (header.iovLength < 0 || header.iovLength > 1024) return Fail(40); @@ -2164,6 +2253,8 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, header.iov = vectors.data(); const auto total = MessageLength(&header); if (total < 0) return -1; + const File::IoDescriptor retained(descriptor); + if (retained.Value() < 0) return -1; const auto socket = Lookup(descriptor); if (!socket) return -1; const std::size_t controlCapacity = header.controlLength; @@ -2208,7 +2299,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, #else constexpr int receiveCloseOnExec = 0; #endif - received = ::recvmsg(socket->value, &host, NativeReceiveFlags(flags, socket->type) | ((flags & GuestControlCloseOnExec) != 0 ? receiveCloseOnExec : 0)); + received = ThreadCancel::Native([&] { return ::recvmsg(socket->value, &host, NativeReceiveFlags(flags, socket->type) | ((flags & GuestControlCloseOnExec) != 0 ? receiveCloseOnExec : 0)); }); if (received < 0) return Fail(NativeError()); size = host.msg_namelen; controlTruncated = (host.msg_flags & MSG_CTRUNC) != 0; @@ -2227,8 +2318,8 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (const int error = rights.Install((flags & GuestControlCloseOnExec) != 0)) return Fail(error); } else #endif - received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), - NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); + received = ReceiveNative(*socket, buffer.data(), static_cast(buffer.size()), flags, reinterpret_cast(&native), &size); + if (received < 0) { #ifdef _WIN32 if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); @@ -2655,9 +2746,23 @@ extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* return ::ReadGuest(destination, source, bytes); } +std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { + return ThreadCancel::Io([&] { return File::WithIoDescriptor(descriptor, [&](int descriptor) { + return Receive(descriptor, buffer, length, flags); + }); }); +} +std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, + int flags, void* address, std::uint32_t* addressLength) { + return ThreadCancel::Io([&] { return File::WithIoDescriptor(descriptor, [&](int descriptor) { + return ReceiveFrom(descriptor, buffer, length, flags, address, addressLength); + }); }); +} +std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags) { + return ThreadCancel::Io([&] { return ReceiveMessage(descriptor, message, flags); }); +} std::int64_t GuestSockets::Read(int descriptor, void* buffer, std::size_t length) { if (length == 0) return IsOpen(descriptor) ? 0 : Fail(9); - return recv_nid_postfix(descriptor, buffer, std::min(length, INT_MAX), 0); + return Receive(descriptor, buffer, std::min(length, INT_MAX), 0); } std::int64_t GuestSockets::Write(int descriptor, const void* buffer, std::size_t length) { diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index f468bbd5808..3a867bc9e67 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/HostFault.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include #include @@ -345,8 +346,13 @@ struct QueuedDelivery { std::atomic posted{false}; std::atomic closed{false}; std::atomic references{1}; + QueuedDelivery* next = nullptr; }; +thread_local QueuedDelivery* ioDeliveries = nullptr; +void DeliverIoCallbacks(); +void RetireIoCallbacks(); + void ReleaseQueued(QueuedDelivery* queued) { if (queued != nullptr && queued->references.fetch_sub(1, std::memory_order_acq_rel) == 1) delete queued; } @@ -369,6 +375,11 @@ void DeferredWaitingEntry(void* parameter) { void CALLBACK WaitingEntry(ULONG_PTR parameter) { auto* queued = reinterpret_cast(parameter); + if (ThreadCancel::DeferIoCallback(DeliverIoCallbacks, RetireIoCallbacks)) { + queued->next = ioDeliveries; + ioDeliveries = queued; + return; + } const auto handler = queued->handler; const int signum = queued->signum; const bool claimed = !queued->taken.exchange(true, std::memory_order_acq_rel); @@ -379,6 +390,21 @@ void CALLBACK WaitingEntry(ULONG_PTR parameter) { Deliver(handler, signum, context); } +void DeliverIoCallbacks() { + while (auto* queued = ioDeliveries) { + if (ThreadCancel::DeferIoCallback(DeliverIoCallbacks, RetireIoCallbacks)) return; + ioDeliveries = queued->next; + WaitingEntry(reinterpret_cast(queued)); + } +} + +void RetireIoCallbacks() { + while (auto* queued = ioDeliveries) { + ioDeliveries = queued->next; + ReleaseQueued(queued); + } +} + static_assert(HomeArea + 8 == 40, "Aps5RedirectedEntryStub finds the delivery 40 bytes above its stack pointer"); static_assert(offsetof(Delivery, context) == 16 && offsetof(CONTEXT, Rax) == 0x78 && offsetof(CONTEXT, Rbp) == 0xa0 && offsetof(CONTEXT, R15) == 0xf0, "Aps5RedirectedEntryStub stores the live registers into the delivery's context"); static_assert(offsetof(Delivery, vectors) == 1248 && offsetof(Delivery, saveVectors) == 1760, "Aps5RedirectedEntryStub stores ymm0-ymm15 into the delivery when saveVectors is set"); @@ -808,6 +834,7 @@ Attempt TryDeliver(Pthread thread, HANDLE native, GuestExceptionHandler handler, const bool accepted = QueueWaitingDelivery(native, queued); ResumeThread(native); if (!accepted) throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread"); + ThreadCancel::WakeIoDelivery(thread); return Attempt::Delivered; } const bool restoring = RestoringContext(delivery.context.Rip); @@ -1146,6 +1173,7 @@ void ToHost(const GuestMcontext& m, mcontext_t& host) { } void Deliver(int signum, siginfo_t*, void* context) { + if (ThreadCancel::DeferIoSignal(signum)) return; if (TimedWait::DeferDelivery(signum)) return; const auto handler = Handler(RaisedSignal); if (handler == nullptr) { @@ -1326,6 +1354,11 @@ bool RaiseOn(Pthread thread) { } #ifdef _WIN32 +extern "C" void GuestExceptionQueueIoForTest_nid_no_patch(void (APS5_VABI* handler)(int, void*)) { + auto* queued = new QueuedDelivery{handler, 30}; + WaitingEntry(reinterpret_cast(queued)); + DeliverIoCallbacks(); +} extern "C" [[noreturn]] void Aps5RedirectedEntry(void* delivery) { RedirectedEntry(static_cast(delivery)); } diff --git a/core/libs/prx/libkernel/System/src/SignalSets.cpp b/core/libs/prx/libkernel/System/src/SignalSets.cpp index 8444422f5dd..3ef91d55c0c 100644 --- a/core/libs/prx/libkernel/System/src/SignalSets.cpp +++ b/core/libs/prx/libkernel/System/src/SignalSets.cpp @@ -67,6 +67,13 @@ int APS5_VABI sigismember_nid_postfix(const GuestSignalSet* set, int signal) { asm(".text\n" APS5_SIGNAL_ASM_FUNCTION("sigsetjmp_nid_postfix") + " pushq %rdi\n" + " pushq %rsi\n" + " subq $8, %rsp\n" + APS5_SIGNAL_ASM_CALL("JumpBufferSaveContext_nid_no_patch") + " addq $8, %rsp\n" + " popq %rsi\n" + " popq %rdi\n" " movl %esi, 88(%rdi)\n" " testl %esi, %esi\n" " jz 2f\n" @@ -106,6 +113,15 @@ asm(".text\n" " popq %rsi\n" " popq %rdi\n" "2:\n" + " pushq %rdi\n" + " pushq %rsi\n" + " movl 92(%rdi), %esi\n" + " movq 16(%rdi), %rdi\n" + " subq $8, %rsp\n" + APS5_SIGNAL_ASM_CALL("GuestIoStackRestore_nid_no_patch") + " addq $8, %rsp\n" + " popq %rsi\n" + " popq %rdi\n" #ifdef _WIN32 " pushq %rdi\n" " pushq %rsi\n" diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 2af448ba386..7927cf6d6cf 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/JumpBuffer.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -211,22 +212,29 @@ void RunHandler(int guest, GuestHandler handler) { target.fetch_or(replaced[word] & before[word]); } } -void Dispatch(int native) { +void Dispatch(int native, bool asynchronous) { int guest = 0; for (int candidate : {2, 4, 6, 8, 11, 13, 15}) if (NativeSignal(candidate) == native) { guest = candidate; break; } if (!guest) return; -#ifdef _WIN32 - // Preserve the guest's persistent registration across CRT delivery. - std::signal(native, Dispatch); -#endif if (SignalBlocked(guest)) { MarkPending(guest); return; } + if (asynchronous && ThreadCancel::DeferIoSignal(native, (handlerFlags[guest].load() & SaRestart) != 0)) return; const auto callback = handlers[guest].load(); if (reinterpret_cast(callback) > 1) RunHandler(guest, callback); } +#ifdef _WIN32 +void Dispatch(int native) { + std::signal(native, Dispatch); + Dispatch(native, native == SIGINT || native == SIGTERM || native == SIGABRT); +} +#else +void HostDispatch(int native, siginfo_t* information, void*) { + Dispatch(native, native == SIGINT || native == SIGTERM || (information && information->si_code <= 0)); +} +#endif bool Delivered(const GuestSigaction& action) { return action.handler > 1 && (action.flags & SaSiginfo) == 0; } @@ -237,8 +245,20 @@ bool Install(int guest, const GuestSigaction& action) { const int native = NativeSignal(guest); if (!native) return true; const auto previous = handlers[guest].exchange(Delivered(action) ? reinterpret_cast(action.handler) : nullptr); - auto hostHandler = action.handler == 1 ? SIG_IGN : Delivered(action) ? Dispatch : SIG_DFL; - if (std::signal(native, hostHandler) == SIG_ERR) { +#ifdef _WIN32 + auto hostHandler = action.handler == 1 ? SIG_IGN : Delivered(action) ? static_cast(Dispatch) : SIG_DFL; + const bool installed = std::signal(native, hostHandler) != SIG_ERR; +#else + struct sigaction host{}; + sigemptyset(&host.sa_mask); + host.sa_flags = SA_RESTART; + if (Delivered(action)) { + host.sa_sigaction = HostDispatch; + host.sa_flags |= SA_SIGINFO; + } else host.sa_handler = action.handler == 1 ? SIG_IGN : SIG_DFL; + const bool installed = ::sigaction(native, &host, nullptr) == 0; +#endif + if (!installed) { handlers[guest].store(previous); return false; } diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index c9844305647..ed88da5310a 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -225,6 +225,8 @@ int APS5_VABI Aps5GetcontextFinish_nid_no_patch(UserUcontext* context, const Reg machine.len = sizeof(UserMcontext); machine.fpformat = FpFormatXmm; machine.ownedfp = FpOwnedFpu; + const auto identity = JumpBuffer::GuestContextCapture_nid_no_patch(frame->rsp); + std::memcpy(machine.spare, &identity, sizeof(identity)); std::memcpy(machine.fpstate, frame->fxsave, sizeof(frame->fxsave)); if (!GuestCopyOut_nid_no_patch(context, &captured, offsetof(UserUcontext, link))) return Fail(GuestEfault); return 0; @@ -270,6 +272,9 @@ enum class FpuSource { Context, Current, Initial }; mxcsr &= HostMxcsrMask(); std::memcpy(frame.fxsave + FxsaveMxcsr, &mxcsr, sizeof(mxcsr)); sigprocmask_nid_postfix(GuestSigSetmask, context->sigmask, nullptr); + JumpBuffer::Context identity{}; + std::memcpy(&identity, machine.spare, sizeof(identity)); + JumpBuffer::GuestIoStackRestore_nid_no_patch(machine.rsp, JumpBuffer::GuestContextIdentify_nid_no_patch(identity, machine.rsp)); Aps5ContextRestore_nid_no_patch(&frame); } diff --git a/core/libs/tests/GuestIoContext.cpp b/core/libs/tests/GuestIoContext.cpp new file mode 100644 index 00000000000..9f1d2d80920 --- /dev/null +++ b/core/libs/tests/GuestIoContext.cpp @@ -0,0 +1,315 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libkernel/System/include/UserContext.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#include +#include +#endif + +static void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +template +static bool Observe(TPredicate predicate) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +struct Fibre { + UserUcontext context{}; + alignas(16) std::uint64_t jump[12]{}; + alignas(16) std::uint64_t outer[12]{}; + std::unique_ptr stack; + bool started = false; + bool handled = false; + bool finished = false; + int descriptors[2]{}; +}; + +struct State { + Fibre dispatcher; + std::array fibres; + Fibre* active = nullptr; + bool jumps = false; + bool signalJumps = false; + bool underscoreJumps = false; + bool manual = false; + bool leave = false; + bool signalLeave = false; + bool exitHandler = false; + std::atomic phase{0}; + std::atomic cleanups{0}; + std::atomic destructors{0}; + std::atomic adopted{nullptr}; +}; + +static State* state; + +#ifndef _WIN32 +static sigset_t NativeMask() { + sigset_t mask; + Require(pthread_sigmask(SIG_SETMASK, nullptr, &mask) == 0, "capture native mask"); + return mask; +} + +static void CheckMask(const sigset_t& before) { + const auto after = NativeMask(); + for (int signal = 1; signal < NSIG; ++signal) + Require(sigismember(&before, signal) == sigismember(&after, signal), "fibre switch lost native signal mask"); +} +#endif + +static void Switch(Fibre& from, Fibre& to) { + if (!state->jumps) { + const auto target = to.context; + Require(swapcontext_nid_postfix(&from.context, &target) == 0, "swap I/O context"); + return; + } + from.started = true; + if (state->signalJumps) { + if (sigsetjmp_nid_postfix(from.jump, 1) == 0) { + if (to.started) { + alignas(16) std::uint64_t target[12]; + std::memcpy(target, to.jump, sizeof(target)); + siglongjmp_nid_postfix(target, 1); + } + setcontext_nid_postfix(&to.context); + Require(false, "signal context restore returned"); + } + return; + } + if (state->underscoreJumps) { + if (_setjmp_nid_postfix(from.jump) == 0) { + if (to.started) { + alignas(16) std::uint64_t target[12]; + std::memcpy(target, to.jump, sizeof(target)); + _longjmp_nid_postfix(target, 1); + } + setcontext_nid_postfix(&to.context); + Require(false, "underscore context restore returned"); + } + return; + } + if (setjmp_nid_postfix(from.jump) == 0) { + if (to.started) { + alignas(16) std::uint64_t target[12]; + std::memcpy(target, to.jump, sizeof(target)); + longjmp_nid_postfix(target, 1); + } + setcontext_nid_postfix(&to.context); + Require(false, "context restore returned"); + } +} + +static void APS5_VABI Handler(int, void*) { + if (state->exitHandler) pthread_exit_nid_postfix(GUEST_CANCELED); + auto& fibre = *state->active; +#ifndef _WIN32 + const auto mask = NativeMask(); +#endif + Switch(fibre, state->dispatcher); +#ifndef _WIN32 + CheckMask(mask); +#endif + Require(close_nid_postfix(fibre.descriptors[0]) == 0, "close resumed fibre descriptor"); + fibre.descriptors[0] = -1; + fibre.handled = true; + if (state->signalLeave) siglongjmp_nid_postfix(fibre.outer, 1); + if (state->leave) longjmp_nid_postfix(fibre.outer, 1); +} + +static void APS5_VABI FibreBody() { + auto& fibre = *state->active; + char byte = 0; + bool escaped = false; + if (state->leave) { + if (state->signalLeave) escaped = sigsetjmp_nid_postfix(fibre.outer, 1) != 0; + else escaped = setjmp_nid_postfix(fibre.outer) != 0; + } + if (!escaped) { + const auto result = read_nid_postfix(fibre.descriptors[0], &byte, 1); + Require(!state->leave && result == -1 && *__error_nid_postfix() == 4 && fibre.handled, "suspended handler did not resume safely"); + } + fibre.finished = true; + Switch(fibre, state->dispatcher); + Require(false, "completed fibre resumed"); +} + +static void* APS5_VABI Scheduler(void*) { + constexpr std::size_t StackBytes = 128 * 1024; +#ifndef _WIN32 + const auto mask = NativeMask(); +#endif + int index = 0; + for (auto& fibre : state->fibres) { + fibre.stack = std::make_unique(StackBytes); + Require(getcontext_nid_postfix(&fibre.context) == 0, "capture fibre template"); + fibre.context.stackPointer = fibre.stack.get(); + fibre.context.stackSize = StackBytes; + if (state->manual) { + auto& machine = fibre.context.mcontext; + machine.rsp = ((reinterpret_cast(fibre.stack.get()) + StackBytes) & ~std::uintptr_t{15}) - 8; + *reinterpret_cast(machine.rsp) = 0; + machine.rip = reinterpret_cast(FibreBody); + machine.rbp = 0; + } else makecontext_nid_postfix(&fibre.context, FibreBody, 0); + state->active = &fibre; + state->phase.store(++index); + Switch(state->dispatcher, fibre); +#ifndef _WIN32 + CheckMask(mask); +#endif + Require(!fibre.handled && !fibre.finished, "handler did not suspend"); + alignas(16) std::uint64_t local[12]{}; + if (setjmp_nid_postfix(local) == 0) longjmp_nid_postfix(local, 1); + } + for (auto& fibre : state->fibres) { + state->active = &fibre; + Switch(state->dispatcher, fibre); +#ifndef _WIN32 + CheckMask(mask); +#endif + Require(fibre.finished && fibre.handled, "fibre completion lost delivery ownership"); + } + return nullptr; +} + +static void APS5_VABI Cleanup(void*) { state->cleanups.fetch_add(1); } +static void APS5_VABI Destroy(void*) { + Require(state->cleanups.load() == 1, "adopted cleanup order"); + state->destructors.fetch_add(1); +} + +#ifdef __linux__ +static std::size_t ResidentBytes() { + std::ifstream memory("/proc/self/statm"); + std::size_t mapped = 0; + std::size_t resident = 0; + Require(static_cast(memory >> mapped >> resident), "read context resident memory"); + return resident * static_cast(::sysconf(_SC_PAGESIZE)); +} + +static void ContextChurn() { + constexpr std::size_t StackBytes = 2048; + constexpr std::size_t Count = 32768; + std::vector stacks(StackBytes * Count); + auto* volatileBytes = reinterpret_cast(stacks.data()); + for (std::size_t offset = 0; offset < stacks.size(); offset += 1024) volatileBytes[offset] = std::byte{1}; + UserUcontext original{}; + Require(getcontext_nid_postfix(&original) == 0, "capture churn template"); + const auto before = ResidentBytes(); + for (std::size_t index = 0; index < Count; ++index) { + auto context = original; + context.stackPointer = stacks.data() + StackBytes * index; + context.stackSize = StackBytes; + makecontext_nid_postfix(&context, FibreBody, 0); + Require(context.mcontext.len == sizeof(UserMcontext), "create churn context"); + } + Require(ResidentBytes() <= before + 1024 * 1024, "context creation retains stack registry metadata"); +} +#endif + +#ifdef _WIN32 +static std::atomic drainCallbacks{0}; +static void APS5_VABI DrainHandler(int, void*) { drainCallbacks.fetch_add(1); } +static void DeferredDrain() { + scePthreadSelf(); + ThreadCancel::IoRestart restart(false); + { + ThreadCancel::IoPoint point(restart); + GuestExceptionQueueIoForTest_nid_no_patch(DrainHandler); + Require(drainCallbacks.load() == 0, "delivery escaped active I/O ownership"); + } + Require(drainCallbacks.load() == 1, "deferred delivery was lost or repeated"); +} +#endif + +int main(int argc, char** argv) { + Require(argc == 2, "expected adopted, adopted_exit, swap, jump or sigjump"); + State reading; + state = &reading; +#ifdef _WIN32 + if (std::strcmp(argv[1], "deferred_drain") == 0) { DeferredDrain(); return 0; } +#endif +#ifdef __linux__ + if (std::strcmp(argv[1], "churn") == 0) { ContextChurn(); return 0; } +#endif + reading.exitHandler = std::strcmp(argv[1], "adopted_exit") == 0; + if (reading.exitHandler || std::strcmp(argv[1], "adopted") == 0) { + if (reading.exitHandler) Require(sceKernelInstallExceptionHandler(30, reinterpret_cast(Handler)) == 0, "install adopted exit handler"); + int descriptors[2]{}; + Require(pipe_nid_postfix(descriptors) == 0, "create adopted pipe"); + PthreadKey key = -1; + Require(scePthreadKeyCreate(&key, reinterpret_cast(Destroy)) == 0, "create adopted key"); + std::thread native([&] { + auto self = scePthreadSelf(); + Require(scePthreadSetspecific(key, &reading) == 0, "set adopted TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, nullptr, &cleanup); + reading.adopted.store(self); + char byte = 0; + read_nid_postfix(descriptors[0], &byte, 1); + Require(false, "adopted read returned"); + }); + Require(Observe([&] { + auto thread = reading.adopted.load(); + if (!thread) return false; + std::lock_guard lock(thread->cancelLock); + return thread->cancelIo != nullptr; + }), "adopted read did not block"); + if (reading.exitHandler) Require(sceKernelRaiseException(reading.adopted.load(), 30) == 0, "exit adopted reader from handler"); + else Require(pthread_cancel_nid_postfix(reading.adopted.load()) == 0, "cancel adopted reader"); + native.join(); + Require(reading.cleanups.load() == 1 && reading.destructors.load() == 1, "adopted cleanup repeated"); + Require(scePthreadKeyDelete(key) == 0 && close_nid_postfix(descriptors[0]) == 0 && close_nid_postfix(descriptors[1]) == 0, "release adopted resources"); + if (reading.exitHandler) Require(sceKernelRemoveExceptionHandler(30) == 0, "remove adopted exit handler"); + return 0; + } + const std::string scenario = argv[1]; + reading.manual = scenario.starts_with("manual_"); + reading.leave = scenario.ends_with("leave"); + reading.signalLeave = scenario.ends_with("sigleave"); + reading.signalJumps = scenario.ends_with("sigjump"); + reading.underscoreJumps = scenario.ends_with("underscorejump"); + reading.jumps = scenario.ends_with("jump"); + for (auto& fibre : reading.fibres) Require(pipe_nid_postfix(fibre.descriptors) == 0, "create fibre pipe"); + Require(sceKernelInstallExceptionHandler(30, reinterpret_cast(Handler)) == 0, "install fibre handler"); + Pthread worker = nullptr; + Require(scePthreadCreate(&worker, nullptr, Scheduler, nullptr, nullptr) == 0, "create fibre scheduler"); + for (int phase = 1; phase <= 2; ++phase) { + Require(Observe([&] { + if (reading.phase.load() != phase) return false; + std::lock_guard lock(worker->cancelLock); + return worker->cancelIo != nullptr; + }), "fibre read did not block"); + Require(sceKernelRaiseException(worker, 30) == 0, "interrupt fibre read"); + } + Require(Observe([&] { return worker->_finished.load(); }), "fibre scheduler did not finish"); + Require(scePthreadJoin(worker, nullptr) == 0, "join fibre scheduler"); + Require(sceKernelRemoveExceptionHandler(30) == 0, "remove fibre handler"); + for (auto& fibre : reading.fibres) + Require(fibre.descriptors[0] == -1 && close_nid_postfix(fibre.descriptors[1]) == 0, "close fibre pipe"); +} diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp new file mode 100644 index 00000000000..54db4159cc9 --- /dev/null +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -0,0 +1,841 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libSceNet/libSceNet.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/FileStream.hpp" + +#ifdef _WIN32 +#include +#include "prx/libc/include/WindowsDescriptor.hpp" +static int NativeDescriptor(std::FILE* file) { return ::_fileno(file); } +static int NativeDuplicate(int descriptor) { return ::_dup(descriptor); } +static void NativeClose(int descriptor) { ::_close(descriptor); } +static int NativeReplace(int from, int to) { return ::_dup2(from, to); } +#else +#include +#include +#include +#include +#include +#include +static int NativeDescriptor(std::FILE* file) { return ::fileno(file); } +static int NativeDuplicate(int descriptor) { return ::dup(descriptor); } +static void NativeClose(int descriptor) { ::close(descriptor); } +static int NativeReplace(int from, int to) { return ::dup2(from, to) < 0 ? -1 : 0; } +#endif + +static void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s, guest errno %d, host errno %d\n", message, *__error_nid_postfix(), errno); + std::exit(1); +} + +static std::FILE* OpenTemporary() { + auto* file = std::tmpfile(); + Require(file != nullptr, "create file"); + Require(std::fwrite("xyz", 1, 3, file) == 3 && std::fflush(file) == 0 && std::fseek(file, 0, SEEK_SET) == 0, "seed file"); + return file; +} + +static void Capacity() { + auto* file = OpenTemporary(); + const int descriptor = NativeDescriptor(file); + int sockets[2]{}; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0, "create socket pair"); + Require(send_nid_postfix(sockets[1], "s", 1, 0) == 1, "seed socket"); + scePthreadSelf(); + std::vector occupied; + occupied.reserve(1024); +#ifdef _WIN32 + const int previous = ::_getmaxstdio(); + Require(::_setmaxstdio(512) == 512, "bound descriptor table"); +#else + rlimit previous{}; + Require(::getrlimit(RLIMIT_NOFILE, &previous) == 0, "read descriptor limit"); + auto limit = previous; + limit.rlim_cur = 64; + Require(::setrlimit(RLIMIT_NOFILE, &limit) == 0, "bound descriptor table"); +#endif + for (int copy; (copy = NativeDuplicate(descriptor)) >= 0;) occupied.push_back(copy); + Require(errno == EMFILE, "fill descriptor table"); + char byte = 0; + Require(read_nid_postfix(descriptor, &byte, 1) == 1 && byte == 'x', "read at capacity"); + KernelIovec vector{&byte, 1}; + Require(readv_nid_postfix(descriptor, &vector, 1) == 1 && byte == 'y', "readv at capacity"); + Require(pread_nid_postfix(descriptor, &byte, 1, 2) == 1 && byte == 'z', "pread at capacity"); + Require(preadv_nid_postfix(descriptor, &vector, 1, 0) == 1 && byte == 'x', "preadv at capacity"); + Require(read_nid_postfix(descriptor, &byte, 0) == 0, "zero read at capacity"); + Require(read_nid_postfix(sockets[0], &byte, 1) == 1 && byte == 's', "socket read at capacity"); + for (const int copy : occupied) NativeClose(copy); +#ifdef _WIN32 + Require(::_setmaxstdio(previous) == previous, "restore descriptor table"); +#else + Require(::setrlimit(RLIMIT_NOFILE, &previous) == 0, "restore descriptor limit"); +#endif + Require(close_nid_postfix(sockets[0]) == 0 && close_nid_postfix(sockets[1]) == 0, "close sockets"); + Require(std::fclose(file) == 0, "close file"); +} + +template +static bool Observe(TPredicate predicate) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +struct BlockedRead { + int descriptor; + char byte = 0; + std::int64_t result = -2; +}; + +static void* APS5_VABI Reader(void* argument) { + auto& read = *static_cast(argument); + read.result = read_nid_postfix(read.descriptor, &read.byte, 1); + return nullptr; +} + +static std::array OpenDescriptors() { + std::array result{}; + for (std::size_t descriptor = 0; descriptor < result.size(); ++descriptor) { +#ifdef _WIN32 + result[descriptor] = WindowsDescriptorHandle_nid_no_patch(static_cast(descriptor)) != INVALID_HANDLE_VALUE; +#else + result[descriptor] = ::fcntl(static_cast(descriptor), F_GETFD) >= 0; +#endif + } + return result; +} + +static void NoAliases() { + int pipe[2]{}; + Require(pipe_nid_postfix(pipe) == 0, "create alias probe pipe"); + Require(fcntl_nid_postfix(pipe[0], 2, 1) == 0 && fcntl_nid_postfix(pipe[1], 2, 1) == 0, "set close-on-exec originals"); + BlockedRead read{pipe[0]}; + const auto before = OpenDescriptors(); + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, Reader, &read, nullptr) == 0, "start alias probe"); + const bool registered = Observe([&] { + std::lock_guard lock(reader->cancelLock); + return reader->cancelIo != nullptr; + }); + const auto during = OpenDescriptors(); + Require(write_nid_postfix(pipe[1], "x", 1) == 1, "release alias probe"); + Require(Observe([&] { return reader->_finished.load(); }), "alias probe did not finish"); + Require(scePthreadJoin(reader, nullptr) == 0, "join alias probe"); + Require(registered && read.result == 1 && read.byte == 'x', "alias probe did not read"); + Require(before == during, "blocked read allocated a publicly reusable or inheritable descriptor"); + Require(before == OpenDescriptors(), "read changed descriptor ownership"); + Require(fcntl_nid_postfix(pipe[0], 1) == 1 && fcntl_nid_postfix(pipe[1], 1) == 1, "read changed close-on-exec flags"); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0, "close alias probe pipe"); +} + +#ifdef _WIN32 +static void AppendOwnership(bool stream) { + auto* file = OpenTemporary(); + const int original = NativeDescriptor(file); + const int descriptor = dup_nid_postfix(original); + Require(descriptor >= 0, "duplicate append ownership file"); + const auto before = WindowsDescriptorHandle_nid_no_patch(descriptor); + std::atomic started{false}; + std::atomic finished{false}; + FileStream* converted = nullptr; + std::thread changer; + bool premature = false; + { + const GuestDescriptorAccess admission(original, GuestDescriptorAccess::Mode::Read); + const GuestDescriptorAccess aliasAdmission(descriptor, GuestDescriptorAccess::Mode::Read); + changer = std::thread([&] { + started.store(true); + if (stream) { + converted = fdopen_nid_postfix(descriptor, "a+"); + Require(converted != nullptr, "append fdopen"); + } else Require(fcntl_nid_postfix(original, 4, 8) == 0, "change append status"); + finished.store(true); + }); + Require(Observe([&] { return started.load(); }), "start append replacement"); + premature = Observe([&] { return finished.load(); }); + Require(WindowsDescriptorHandle_nid_no_patch(descriptor) == before, "append replaced an admitted handle"); + Require(fcntl_nid_postfix(original, 3) >= 0, "append waiter held the status table lock"); + } + Require(Observe([&] { return finished.load(); }), "append replacement did not resume"); + changer.join(); + Require(!premature, "append replacement bypassed read admission"); + if (converted) Require(fclose_nid_postfix(converted) == 0, "close converted stream"); + else Require(close_nid_postfix(descriptor) == 0, "close append alias"); + Require(std::fclose(file) == 0, "close append file"); +} +#endif + +static bool Registered(Pthread reader) { + return Observe([&] { + std::lock_guard lock(reader->cancelLock); + return reader->cancelIo != nullptr; + }); +} + +struct CloseRead { + int descriptor; + int api; + bool disabled; + PthreadKey key; + std::int64_t result = -2; + int error = 0; + std::atomic returned{false}; + std::atomic cleanups{0}, destructors{0}; + std::array data{}; +}; + +static void APS5_VABI CloseCleanup(void* argument) { ++static_cast(argument)->cleanups; } +static void APS5_VABI CloseDestructor(void* argument) { ++static_cast(argument)->destructors; } + +static void* APS5_VABI ReadUntilClose(void* argument) { + auto& read = *static_cast(argument); + if (read.disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable close reader cancellation"); + Require(scePthreadSetspecific(read.key, &read) == 0, "set close reader TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(CloseCleanup, &read, &cleanup); + char byte = 0; + KernelIovec vector{&byte, 1}; + GuestMsghdr message{}; + message.iov = &vector; + message.iovLength = 1; + if (read.api == 0) read.result = read_nid_postfix(read.descriptor, &byte, 1); + else if (read.api == 1) read.result = readv_nid_postfix(read.descriptor, &vector, 1); + else if (read.api == 2) read.result = recv_nid_postfix(read.descriptor, &byte, 1, 0); + else if (read.api == 3) read.result = recvmsg_nid_postfix(read.descriptor, &message, 0); + else read.result = recv_nid_postfix(read.descriptor, read.data.data(), read.data.size(), 0x40); + read.error = *__error_nid_postfix(); + read.returned.store(true); + if (read.disabled) { + Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable close reader cancellation"); + scePthreadTestcancel(); + } + __pthread_cleanup_pop_imp_nid_postfix(1); + return nullptr; +} + +static void PosixClose(bool socket, int api, bool disabled, int entry = 0) { + int descriptors[2]{}; + Require((socket ? socketpair_nid_postfix(1, 1, 0, descriptors) : pipe_nid_postfix(descriptors)) == 0, "create close-read transport"); + const int alias = dup_nid_postfix(descriptors[0]); + Require(alias >= 0, "duplicate close-read endpoint"); + PthreadKey key = -1; + Require(scePthreadKeyCreate(&key, reinterpret_cast(CloseDestructor)) == 0, "create close reader TSD key"); + CloseRead read{descriptors[0], api, disabled, key}; + if (api == 4) { + Require(write_nid_postfix(descriptors[1], "p", 1) == 1, "seed partial close read"); + Require(Observe([&] { + int available = -1; + Require(ioctl_nid_postfix(descriptors[0], 0x4004667f, &available) == 0, "inspect partial input"); + return available == 1; + }), "partial input did not arrive"); + } + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, ReadUntilClose, &read, nullptr) == 0, "start close reader"); + const bool admitted = Observe([&] { + GuestDescriptorAccess probe(descriptors[0], GuestDescriptorAccess::Mode::Update, std::defer_lock); + return !probe.TryLock(); + }); + if (api == 4) Require(Observe([&] { + int available = -1; + Require(ioctl_nid_postfix(descriptors[0], 0x4004667f, &available) == 0, "inspect partial progress"); + return available == 0; + }), "WAITALL read did not consume its partial input"); + if (disabled) Require(pthread_cancel_nid_postfix(reader) == 0, "leave cancellation pending in close reader"); + std::atomic closed{false}; + std::thread closer([&] { + Require((entry == 1 ? GuestSockets::Close(descriptors[0]) : entry == 2 ? sceKernelClose(descriptors[0]) : close_nid_postfix(descriptors[0])) == 0, "close active read descriptor"); + closed.store(true); + }); + const bool prompt = Observe([&] { return closed.load(); }); + if (!prompt) Require(write_nid_postfix(descriptors[1], "xxxx", api == 4 ? 4 : 1) == (api == 4 ? 4 : 1), "release failed close reader"); + Require(Observe([&] { return reader->_finished.load(); }), "close reader did not finish"); + void* result = nullptr; + Require(scePthreadJoin(reader, &result) == 0, "join close reader"); + closer.join(); + Require(admitted && prompt && read.returned.load() && (api == 4 ? read.result == 1 && read.data[0] == 'p' : read.result == -1 && read.error == 4), "close waited for external input or returned the wrong read result"); + Require(result == (disabled ? GUEST_CANCELED : nullptr) && read.cleanups.load() == 1 && read.destructors.load() == 1, "close reader lost pending cancellation or repeated cleanup/TSD"); + Require(write_nid_postfix(descriptors[1], "a", 1) == 1, "write surviving alias"); + char byte = 0; + Require(read_nid_postfix(alias, &byte, 1) == 1 && byte == 'a', "close shut down a surviving alias"); + Require(dup2_nid_postfix(alias, descriptors[0]) == descriptors[0], "reuse closed descriptor"); + Require(write_nid_postfix(descriptors[1], "b", 1) == 1 && read_nid_postfix(descriptors[0], &byte, 1) == 1 && byte == 'b', "old close interruption affected reused descriptor"); + Require(close_nid_postfix(descriptors[0]) == 0 && close_nid_postfix(alias) == 0 && close_nid_postfix(descriptors[1]) == 0, "close close-read endpoints"); + Require(scePthreadKeyDelete(key) == 0, "delete close reader TSD key"); +} + +#ifdef _WIN32 +static void CloseNativeErrors() { + int descriptors[2]{}; + Require(pipe_nid_postfix(descriptors) == 0, "create native-error close pipe"); + std::atomic admitted{false}; + std::thread reader([&] { + bool called = false; + const auto result = ThreadCancel::Io([&] { return File::WithIoDescriptor(descriptors[0], [&](int) -> std::int64_t { + admitted.store(true); + Require(Observe([] { return GuestDescriptorAccess::Operation::CurrentInterrupted(); }), "close did not interrupt pre-call reader"); + _doserrno = ERROR_NO_DATA; + SetLastError(ERROR_HANDLE_EOF); + const auto transferred = ThreadCancel::Native([&] { called = true; return std::int64_t{0}; }); + const DWORD error = GetLastError(); + Require(!called && transferred == -1 && errno == EINTR && _doserrno == ERROR_OPERATION_ABORTED && error == ERROR_OPERATION_ABORTED, "pre-call close interruption left stale native errors"); + return transferred; + }); }); + Require(result == -1, "pre-call close read returned success"); + }); + Require(Observe([&] { return admitted.load(); }), "pre-call reader did not enter"); + Require(close_nid_postfix(descriptors[0]) == 0, "close pre-call descriptor"); + reader.join(); + Require(close_nid_postfix(descriptors[1]) == 0, "close pre-call peer"); +} +#endif + +static void CloseUnregistered(bool socket) { + int descriptors[2]{}; + Require((socket ? socketpair_nid_postfix(1, 1, 0, descriptors) : pipe_nid_postfix(descriptors)) == 0, "create unregistered close transport"); + BlockedRead read{descriptors[0]}; + std::thread reader([&] { Reader(&read); }); + const bool admitted = Observe([&] { + GuestDescriptorAccess probe(descriptors[0], GuestDescriptorAccess::Mode::Update, std::defer_lock); + return !probe.TryLock(); + }); + std::atomic closed{false}; + std::thread closer([&] { + Require(close_nid_postfix(descriptors[0]) == 0, "close unregistered read"); + closed.store(true); + }); + const bool prompt = Observe([&] { return closed.load(); }); + if (!prompt) Require(write_nid_postfix(descriptors[1], "x", 1) == 1, "release unregistered close failure"); + closer.join(); + reader.join(); + Require(admitted && prompt && read.result == -1, "close did not interrupt an unregistered reader"); + Require(close_nid_postfix(descriptors[1]) == 0, "close unregistered peer"); +} + +static void CloseReaders(bool socket) { + int descriptors[2]{}; + Require((socket ? socketpair_nid_postfix(1, 1, 0, descriptors) : pipe_nid_postfix(descriptors)) == 0, "create multiple-reader close transport"); + std::array reads{{{descriptors[0]}, {descriptors[0]}, {descriptors[0]}}}; + std::array readers{}; + for (std::size_t index = 0; index < readers.size(); ++index) { + Require(scePthreadCreate(&readers[index], nullptr, Reader, &reads[index], nullptr) == 0, "start close cohort reader"); + Require(Registered(readers[index]), "close cohort reader did not register"); + } + std::atomic closed{false}; + std::thread closer([&] { + Require(close_nid_postfix(descriptors[0]) == 0, "close multiple readers"); + closed.store(true); + }); + const bool prompt = Observe([&] { return closed.load(); }); + if (!prompt) Require(write_nid_postfix(descriptors[1], "xxx", 3) == 3, "release multiple-reader close failure"); + closer.join(); + for (std::size_t index = 0; index < readers.size(); ++index) { + Require(Observe([&] { return readers[index]->_finished.load(); }), "close cohort did not finish"); + Require(scePthreadJoin(readers[index], nullptr) == 0 && reads[index].result == -1, "close cohort read did not fail"); + } + Require(prompt && close_nid_postfix(descriptors[1]) == 0, "close failed to wake every admitted reader"); +} + +static void InvalidDuplicate() { + int pipe[2]{}; + Require(pipe_nid_postfix(pipe) == 0, "create invalid duplicate pipe"); + BlockedRead read{pipe[0]}; + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, Reader, &read, nullptr) == 0, "start invalid duplicate reader"); + Require(Registered(reader), "invalid duplicate reader did not block"); + const int closed = dup_nid_postfix(pipe[1]); + Require(closed >= 0 && close_nid_postfix(closed) == 0, "create closed duplicate source"); + std::atomic finished{false}; + std::thread duplicator([&] { + Require(dup2_nid_postfix(closed, pipe[0]) == -1 && *__error_nid_postfix() == 9, "invalid duplicate result"); + finished.store(true); + }); + const bool prompt = Observe([&] { return finished.load(); }); + Require(write_nid_postfix(pipe[1], "x", 1) == 1, "complete invalid duplicate reader"); + duplicator.join(); + Require(Observe([&] { return reader->_finished.load(); }), "invalid duplicate reader did not finish"); + Require(scePthreadJoin(reader, nullptr) == 0, "join invalid duplicate reader"); + Require(prompt && read.result == 1 && read.byte == 'x', "invalid source waited for target admission"); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0, "close invalid duplicate pipe"); +} + +static void AccessCancellation(bool positionedWait = false) { + int pipe[2]{}; + Require(pipe_nid_postfix(pipe) == 0, "create access probe pipe"); + std::optional changing; + std::FILE* file = nullptr; + int input = pipe[0]; +#ifdef _WIN32 + std::optional positioned; + if (positionedWait) { + file = OpenTemporary(); + input = NativeDescriptor(file); + positioned.emplace(File::LockPositionedTransfer(input)); + } else +#endif + changing.emplace(input, GuestDescriptorAccess::Mode::Change); + BlockedRead read{input}; + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, Reader, &read, nullptr) == 0, "start access probe"); + const bool registered = Registered(reader); + std::atomic canceled{false}; + std::thread requester([&] { + Require(pthread_cancel_nid_postfix(reader) == 0, "cancel access probe"); + canceled.store(true); + }); + const bool prompt = Observe([&] { return canceled.load(); }); + changing.reset(); +#ifdef _WIN32 + positioned.reset(); +#endif + if (!prompt && !positionedWait) Require(write_nid_postfix(pipe[1], "x", 1) == 1, "release failed access probe"); + requester.join(); + Require(Observe([&] { return reader->_finished.load(); }), "access probe did not finish"); + void* result = nullptr; + Require(scePthreadJoin(reader, &result) == 0, "join access probe"); + Require(registered && prompt && result == GUEST_CANCELED && read.result == -2, "descriptor access did not cancel while exclusive access was held"); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0, "close access probe pipe"); + if (file) Require(std::fclose(file) == 0, "close positioned probe file"); +} + +static void ProtectedClose() { + const int saved = NativeDuplicate(0); + int pipe[2]{}; + Require(pipe_nid_postfix(pipe) == 0, "create protected-stream pipe"); + Require(NativeReplace(pipe[0], 0) == 0, "replace stdin for protected close"); + BlockedRead read{0}; + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, Reader, &read, nullptr) == 0, "start protected-stream reader"); + const bool registered = Registered(reader); + std::atomic posixDone{false}, sceDone{false}; + int posixResult = 0, posixError = 0, sceResult = 0; + std::thread posix([&] { + posixResult = close_nid_postfix(0); + posixError = *__error_nid_postfix(); + posixDone.store(true); + }); + std::thread sce([&] { + sceResult = sceKernelClose(0); + sceDone.store(true); + }); + const bool prompt = Observe([&] { return posixDone.load() && sceDone.load(); }); + const bool blocked = !reader->_finished.load(); + Require(write_nid_postfix(pipe[1], "x", 1) == 1, "release protected-stream reader"); + Require(Observe([&] { return reader->_finished.load(); }), "protected-stream reader did not finish"); + Require(scePthreadJoin(reader, nullptr) == 0, "join protected-stream reader"); + posix.join(); + sce.join(); + if (saved >= 0) { + Require(NativeReplace(saved, 0) == 0, "restore stdin"); + NativeClose(saved); + } else NativeClose(0); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0, "close protected-stream pipe"); + Require(registered && prompt && blocked && read.result == 1, "protected close waited for input"); + Require(posixResult == -1 && posixError == 1 && sceResult == static_cast(0x80020001u), "protected close returned wrong error"); +} + +struct StreamRead { + FileStream* stream; + int descriptor; + std::atomic held{false}; + std::atomic go{false}; + std::atomic returned{false}; + std::atomic unlocks{0}; + std::int64_t result = -2; +}; + +static void APS5_VABI UnlockStream(void* argument) { + auto& read = *static_cast(argument); + funlockfile_nid_postfix(read.stream); + ++read.unlocks; +} + +static void* APS5_VABI StreamReader(void* argument) { + auto& read = *static_cast(argument); + flockfile_nid_postfix(read.stream); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(UnlockStream, &read, &cleanup); + read.held.store(true); + Require(Observe([&] { return read.go.load(); }), "release stdio reader"); + char byte = 0; + read.result = read_nid_postfix(read.descriptor, &byte, 1); + Require(read.result == 1 && byte == 'x', "stdio reader lost input"); + read.returned.store(true); + __pthread_cleanup_pop_imp_nid_postfix(1); + return nullptr; +} + +static void StreamOrder(bool reopen) { + int pipe[2]{}; + Require(pipe_nid_postfix(pipe) == 0, "create stdio ordering pipe"); + Require(write_nid_postfix(pipe[1], "x", 1) == 1, "seed stdio ordering pipe"); +#ifdef _WIN32 + auto* native = ::_fdopen(pipe[0], "rb"); +#else + auto* native = ::fdopen(pipe[0], "rb"); +#endif + Require(native != nullptr, "open stdio ordering stream"); + FileStream stream(native); + const auto path = std::filesystem::path("io-stdio-order-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".tmp"); + if (reopen) { + std::ofstream output(path, std::ios::binary); + output.put('r'); + Require(output.good(), "create reopen destination"); + } + StreamRead read{&stream, pipe[0]}; + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, StreamReader, &read, nullptr) == 0, "start stdio ordering reader"); + Require(Observe([&] { return read.held.load(); }), "stdio reader did not acquire FILE lock"); + std::atomic started{false}, done{false}; + bool changed = false; + std::thread closer([&] { + started.store(true); + changed = reopen ? freopen_nid_postfix(path.string().c_str(), "rb", &stream) == &stream : fclose_nid_postfix(&stream) == 0; + done.store(true); + }); + Require(Observe([&] { return started.load(); }), "stdio closer did not start"); + const auto observation = std::chrono::steady_clock::now() + std::chrono::milliseconds(50); + while (std::chrono::steady_clock::now() < observation) { + GuestDescriptorAccess access(pipe[0], GuestDescriptorAccess::Mode::Read, std::defer_lock); + if (!access.TryLock()) break; + std::this_thread::yield(); + } + read.go.store(true); + const bool progressed = Observe([&] { return read.returned.load(); }); + if (!progressed) Require(pthread_cancel_nid_postfix(reader) == 0, "release failed stdio ordering reader"); + Require(Observe([&] { return reader->_finished.load(); }), "stdio ordering reader did not finish"); + Require(scePthreadJoin(reader, nullptr) == 0, "join stdio ordering reader"); + Require(Observe([&] { return done.load(); }), "stdio close or reopen did not finish"); + closer.join(); + if (reopen && changed) Require(fclose_nid_postfix(&stream) == 0, "close reopened stream"); + Require(close_nid_postfix(pipe[1]) == 0, "close stdio ordering writer"); + if (reopen) Require(std::filesystem::remove(path), "remove reopen destination"); + Require(progressed && changed && read.unlocks.load() == 1, "stdio FILE and descriptor locks inverted"); +} + +#ifdef __linux__ +static std::uint64_t PeakResidentKiB() { + rusage usage{}; + Require(::getrusage(RUSAGE_SELF, &usage) == 0, "read resident memory"); + return static_cast(usage.ru_maxrss); +} + +static void InvalidRegistry() { + char byte = 0; + for (int index = 0; index < 1024; ++index) + Require(read_nid_postfix(1000000000 + index, &byte, 0) == -1 && *__error_nid_postfix() == 9, "warm invalid read"); + const auto before = PeakResidentKiB(); + for (int index = 1024; index < 101024; ++index) { + Require(read_nid_postfix(1000000000 + index, &byte, 0) == -1 && *__error_nid_postfix() == 9, "invalid descriptor read"); + Require(read_nid_postfix(-index, &byte, 0) == -1 && *__error_nid_postfix() == 9, "negative descriptor read"); + } + Require(PeakResidentKiB() - before < 8192, "invalid reads retained descriptor registry allocations"); +} +#endif + +#ifdef _WIN32 +struct VectorRace { + static constexpr std::size_t Readers = 3; + static constexpr std::size_t Records = 32; + static constexpr std::size_t Parts = 64; + std::barrier<> start{Readers + 1}; + std::array descriptors{}; + std::array, Records>, Readers> data{}; +}; + +struct VectorReader { + VectorRace* race; + std::size_t index; +}; + +static void* APS5_VABI ReadRecords(void* argument) { + const auto reader = *static_cast(argument); + auto& race = *reader.race; + race.start.arrive_and_wait(); + for (auto& record : race.data[reader.index]) { + std::int64_t read; + if (reader.index == VectorRace::Readers - 1) read = read_nid_postfix(race.descriptors[reader.index], record.data(), sizeof(record)); + else { + std::array vectors{}; + for (std::size_t part = 0; part < vectors.size(); ++part) vectors[part] = {&record[part], sizeof(record[part])}; + read = readv_nid_postfix(race.descriptors[reader.index], vectors.data(), static_cast(vectors.size())); + } + Require(read == sizeof(record), "pipe record was shortened"); + for (std::size_t part = 0; part < record.size(); ++part) + Require(record[part] == record[0] + part, "pipe readv interleaved with another reader"); + } + return nullptr; +} + +static void PipeVectors() { + int pipe[2]{}; + Require(pipe_nid_postfix(pipe) == 0, "create vector race pipe"); + VectorRace race; + const int alias = dup_nid_postfix(pipe[0]); + Require(alias >= 0, "duplicate vector race reader"); + race.descriptors = {pipe[0], alias, pipe[0]}; + std::array source{}; + for (std::size_t index = 0; index < source.size(); ++index) source[index] = static_cast(index); + Require(write_nid_postfix(pipe[1], reinterpret_cast(source.data()), sizeof(source)) == sizeof(source), "seed vector records"); + std::array threads{}; + std::array readers{}; + for (std::size_t index = 0; index < threads.size(); ++index) { + readers[index] = {&race, index}; + Require(scePthreadCreate(&threads[index], nullptr, ReadRecords, &readers[index], nullptr) == 0, "start vector race reader"); + } + race.start.arrive_and_wait(); + for (const auto thread : threads) Require(scePthreadJoin(thread, nullptr) == 0, "join vector race reader"); + std::array seen{}; + for (const auto& records : race.data) for (const auto& record : records) for (const auto value : record) { + Require(value < seen.size() && !seen[value], "pipe read lost ownership of a record"); + seen[value] = true; + } + for (const bool value : seen) Require(value, "pipe read lost data"); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(alias) == 0 && close_nid_postfix(pipe[1]) == 0, "close vector race pipe"); +} + +static void CloseProgress() { + int pipe[2]{}; + Require(pipe_nid_postfix(pipe) == 0, "create close-progress pipe"); + Require(write_nid_postfix(pipe[1], "x", 1) == 1, "seed close-progress pipe"); + auto positioned = File::LockPositionedTransfer(pipe[1]); + std::atomic closed{false}; + std::thread closer([&] { + Require(close_nid_postfix(pipe[1]) == 0, "close positioned descriptor"); + closed.store(true); + }); + const bool waiting = Observe([&] { + GuestDescriptorAccess access(pipe[1], GuestDescriptorAccess::Mode::Read, std::defer_lock); + return !access.TryLock(); + }); + BlockedRead read{pipe[0]}; + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, Reader, &read, nullptr) == 0, "start drain reader"); + const bool drained = Observe([&] { return reader->_finished.load(); }); + const bool closeStayedBlocked = !closed.load(); + positioned.unlock(); + closer.join(); + Require(Observe([&] { return reader->_finished.load(); }), "drain reader did not finish"); + Require(scePthreadJoin(reader, nullptr) == 0, "join drain reader"); + Require(waiting && closeStayedBlocked && drained && read.result == 1 && read.byte == 'x', "blocked close prevented an independent pipe drain"); + Require(close_nid_postfix(pipe[0]) == 0, "close drain reader"); +} +#endif + +#ifndef _WIN32 +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; + +static bool ChildBlocked(int descriptor) { + const pid_t child = ::fork(); + Require(child >= 0, "fork record-lock probe"); + if (child == 0) { + struct flock lock{}; + lock.l_type = F_WRLCK; + lock.l_whence = SEEK_SET; + const int result = ::fcntl(descriptor, F_SETLK, &lock); + const bool blocked = result == -1 && (errno == EACCES || errno == EAGAIN); + ::_exit(blocked ? 7 : result == 0 ? 0 : 9); + } + int status = 0; + Require(::waitpid(child, &status, 0) == child && WIFEXITED(status), "join record-lock probe"); + Require(WEXITSTATUS(status) == 0 || WEXITSTATUS(status) == 7, "record-lock probe failed"); + return WEXITSTATUS(status) == 7; +} + +static void BorrowedNetClose(bool posix = false) { + const int listener = sceNetSocket("io-read-listener", 2, 1, 0); + Require(listener >= 0, "create borrowed listener"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind borrowed listener"); + Require(sceNetListen(listener, 4) == 0, "listen borrowed socket"); + std::uint32_t length = address.size(); + Require(sceNetGetsockname(listener, address.data(), &length) == 0, "get borrowed listener address"); + const int client = sceNetSocket("io-read-client", 2, 1, 0); + Require(client >= 0 && sceNetConnect(client, address.data(), length) == 0, "connect borrowed socket"); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0, "accept borrowed socket"); + BlockedRead read{accepted}; + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, Reader, &read, nullptr) == 0, "start borrowed reader"); + const bool registered = Registered(reader); + std::atomic closed{false}; + std::thread closer([&] { + Require((posix ? close_nid_postfix(accepted) : sceNetSocketClose(accepted)) == 0, "close borrowed socket"); + closed.store(true); + }); + const bool prompt = Observe([&] { return closed.load(); }); + if (!prompt) Require(pthread_cancel_nid_postfix(reader) == 0, "release failed borrowed close"); + closer.join(); + Require(Observe([&] { return reader->_finished.load(); }), "borrowed reader did not finish"); + Require(scePthreadJoin(reader, nullptr) == 0, "join borrowed reader"); + Require(registered && prompt && (posix ? read.result == -1 : read.result == 0), "socket close did not wake the borrowed POSIX read"); + Require(sceNetSocketClose(client) == 0 && sceNetSocketClose(listener) == 0, "close borrowed socket pair"); +} + +static void RecordLocks() { + auto* file = OpenTemporary(); + const int descriptor = NativeDescriptor(file); + GuestLock lock{0, 0, 0, 3, 0, 0}; + Require(fcntl_nid_postfix(descriptor, 12, &lock) == 0, "acquire guest record lock"); + Require(ChildBlocked(descriptor), "record lock was not established"); + char byte = 0; + KernelIovec vector{&byte, 1}; + Require(read_nid_postfix(descriptor, &byte, 1) == 1, "locked read"); + Require(ChildBlocked(descriptor), "read released record lock"); + Require(pread_nid_postfix(descriptor, &byte, 1, 0) == 1, "locked pread"); + Require(ChildBlocked(descriptor), "pread released record lock"); + Require(readv_nid_postfix(descriptor, &vector, 1) == 1, "locked readv"); + Require(ChildBlocked(descriptor), "readv released record lock"); + Require(preadv_nid_postfix(descriptor, &vector, 1, 0) == 1, "locked preadv"); + Require(ChildBlocked(descriptor), "preadv released record lock"); + lock.type = 2; + Require(fcntl_nid_postfix(descriptor, 12, &lock) == 0 && !ChildBlocked(descriptor), "release guest record lock"); + Require(std::fclose(file) == 0, "close locked file"); +} +#endif + +static void RegistryLifetime() { + std::array workers; + for (std::size_t worker = 0; worker < workers.size(); ++worker) { + workers[worker] = std::thread([worker] { + for (int turn = 0; turn < 512; ++turn) { + const int descriptor = 100000 + turn * 512 + static_cast(worker) * 64; + GuestDescriptorAccess first(descriptor, GuestDescriptorAccess::Mode::Read, std::defer_lock); + const auto version = first.Snapshot(); + { + GuestDescriptorAccess second(descriptor, GuestDescriptorAccess::Mode::Change); + } + Require(!version.Unchanged(), "registry split a live descriptor generation"); + GuestDescriptorAccess replacement(descriptor, GuestDescriptorAccess::Mode::Read); + Require(replacement.Snapshot().Unchanged(), "registry lost a retained descriptor generation"); + } + }); + } + for (auto& worker : workers) worker.join(); +} + +static void CloseBeforeNative() { + int descriptors[2]{}; + Require(pipe_nid_postfix(descriptors) == 0, "create pre-entry close pipe"); + std::atomic entered{false}, interrupted{false}, release{false}, closed{false}; + std::int64_t result = -2; + std::thread reader([&] { + scePthreadSelf(); + ThreadCancel::IoRestart restart(false); + ThreadCancel::IoPoint point(restart); + result = File::WithIoDescriptor(descriptors[0], [&](int descriptor) { + return ThreadCancel::Native([&] { +#ifndef _WIN32 + sigset_t signal{}, previous{}; + sigemptyset(&signal); + sigaddset(&signal, SIGUSR2); + Require(pthread_sigmask(SIG_BLOCK, &signal, &previous) == 0, "hold pre-entry interrupt signal"); +#endif + entered.store(true); + Require(Observe([&] { return GuestDescriptorAccess::Operation::CurrentInterrupted(); }), "close did not interrupt before native entry"); + GuestDescriptorAccess synchronized(descriptor, GuestDescriptorAccess::Mode::Read, std::defer_lock); + synchronized.Snapshot(); +#ifndef _WIN32 + sigset_t pending{}; + while (sigpending(&pending) == 0 && sigismember(&pending, SIGUSR2) == 1) { + int delivered = 0; + Require(sigwait(&signal, &delivered) == 0 && delivered == SIGUSR2, "consume completed pre-entry interrupt"); + } +#endif + interrupted.store(true); + Require(Observe([&] { return release.load(); }), "release native entry"); +#ifndef _WIN32 + Require(pthread_sigmask(SIG_SETMASK, &previous, nullptr) == 0, "restore pre-entry signal mask"); +#endif + char byte = 0; +#ifdef _WIN32 + return ::_read(descriptor, &byte, 1); +#else + return static_cast(::read(descriptor, &byte, 1)); +#endif + }); + }); + }); + Require(Observe([&] { return entered.load(); }), "observe native entry gap"); + std::thread closer([&] { + Require(close_nid_postfix(descriptors[0]) == 0, "close pre-entry reader"); + closed.store(true); + }); + Require(Observe([&] { return interrupted.load(); }), "observe interruption before native entry"); + release.store(true); + const bool completed = Observe([&] { return closed.load(); }); + if (!completed) Require(write_nid_postfix(descriptors[1], "x", 1) == 1, "release missing-retry control"); + reader.join(); + closer.join(); + Require(completed && result == -1, "close lost interruption across native entry"); + Require(close_nid_postfix(descriptors[1]) == 0, "close pre-entry peer"); +} + +int main(int argc, char** argv) { + Require(argc >= 2, "expected descriptor-access case"); + if (std::strcmp(argv[1], "posix_close") == 0) { + Require(argc == 6, "expected close transport, API, cancellation state and entry point"); + PosixClose(std::atoi(argv[2]) != 0, std::atoi(argv[3]), std::atoi(argv[4]) != 0, std::atoi(argv[5])); + return 0; + } + if (std::strcmp(argv[1], "close_unregistered") == 0 || std::strcmp(argv[1], "close_readers") == 0) { + Require(argc == 3, "expected close transport"); + if (std::strcmp(argv[1], "close_unregistered") == 0) CloseUnregistered(std::atoi(argv[2]) != 0); + else CloseReaders(std::atoi(argv[2]) != 0); + return 0; + } + if (std::strcmp(argv[1], "registry_lifetime") == 0) RegistryLifetime(); + else if (std::strcmp(argv[1], "close_before_native") == 0) CloseBeforeNative(); + else if (std::strcmp(argv[1], "capacity") == 0) Capacity(); + else if (std::strcmp(argv[1], "no_aliases") == 0) NoAliases(); + else if (std::strcmp(argv[1], "access_cancel") == 0) AccessCancellation(); + else if (std::strcmp(argv[1], "protected_close") == 0) ProtectedClose(); + else if (std::strcmp(argv[1], "invalid_duplicate") == 0) InvalidDuplicate(); + else if (std::strcmp(argv[1], "stdio_close") == 0) StreamOrder(false); + else if (std::strcmp(argv[1], "stdio_reopen") == 0) StreamOrder(true); +#ifdef __linux__ + else if (std::strcmp(argv[1], "invalid_registry") == 0) InvalidRegistry(); +#endif +#ifdef _WIN32 + else if (std::strcmp(argv[1], "close_native_errors") == 0) CloseNativeErrors(); + else if (std::strcmp(argv[1], "append_status") == 0) AppendOwnership(false); + else if (std::strcmp(argv[1], "append_stream") == 0) AppendOwnership(true); + else if (std::strcmp(argv[1], "close_progress") == 0) CloseProgress(); + else if (std::strcmp(argv[1], "position_cancel") == 0) AccessCancellation(true); + else if (std::strcmp(argv[1], "pipe_vectors") == 0) PipeVectors(); +#endif +#ifndef _WIN32 + else if (std::strcmp(argv[1], "record_locks") == 0) RecordLocks(); + else if (std::strcmp(argv[1], "borrowed_net_close") == 0) BorrowedNetClose(); + else if (std::strcmp(argv[1], "borrowed_posix_close") == 0) BorrowedNetClose(true); +#endif + else Require(false, "unknown descriptor-access case"); +} diff --git a/core/libs/tests/GuestIoNativeThreads.cpp b/core/libs/tests/GuestIoNativeThreads.cpp new file mode 100644 index 00000000000..c3a7fef0077 --- /dev/null +++ b/core/libs/tests/GuestIoNativeThreads.cpp @@ -0,0 +1,152 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" +#include "prx/libc/include/JumpBuffer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +static void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s, guest errno %d\n", message, *__error_nid_postfix()); + std::exit(1); +} + +static void HostThreads() { + for (unsigned round = 0; round < 96; ++round) { + const auto transfer = [] { + int descriptors[2]{}; + Require(pipe_nid_postfix(descriptors) == 0, "create host-thread pipe"); + Require(write_nid_postfix(descriptors[1], "host", 4) == 4, "host-thread write"); + std::array bytes{}; + Require(read_nid_postfix(descriptors[0], bytes.data(), bytes.size()) == 4 && std::memcmp(bytes.data(), "host", 4) == 0, "host-thread read"); + Require(close_nid_postfix(descriptors[0]) == 0 && close_nid_postfix(descriptors[1]) == 0, "close host-thread pipe"); + }; +#ifdef _WIN32 + if ((round & 1) != 0) { + auto entry = transfer; + const auto worker = CreateThread(nullptr, 0, +[](void* opaque) -> DWORD { (*static_cast(opaque))(); return 0; }, &entry, 0, nullptr); + Require(worker && WaitForSingleObject(worker, 5000) == WAIT_OBJECT_0, "join native host thread"); + CloseHandle(worker); + continue; + } +#endif + std::thread worker(transfer); + worker.join(); + } +} + +#ifdef _WIN32 +template +static bool Observe(TPredicate predicate) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} +class FlsExhaustion { +public: + FlsExhaustion() { + for (unsigned count = 0; count < 65536; ++count) { + const auto slot = FlsAlloc(nullptr); + if (slot == FLS_OUT_OF_INDEXES) return; + slots.push_back(slot); + } + Release(); + Require(false, "FLS exhaustion exceeded its bounded budget"); + } + ~FlsExhaustion() { Release(); } + void Release() { for (const auto slot : slots) FlsFree(slot); slots.clear(); } +private: + std::vector slots; +}; +static void EmptyRetirement(bool withRestart) { + std::atomic ready{false}, release{false}, failed{false}; + std::thread worker([&] { + std::optional restart; + if (withRestart) restart.emplace(false); + ready.store(true); + Require(Observe([&] { return release.load(); }), "release empty retirement"); + try { + ThreadCancel::RetireIoStack(std::numeric_limits::max()); + const auto target = JumpBuffer::GuestContextCreate_nid_no_patch(1); + ThreadCancel::RetireIoStack(0, target.identity); + if (JumpBuffer::GuestContextCurrent_nid_no_patch() != target.identity) failed.store(true); + } + catch (...) { failed.store(true); } + }); + Require(Observe([&] { return ready.load(); }), "start empty retirement"); + FlsExhaustion exhausted; + release.store(true); + worker.join(); + exhausted.Release(); + Require(!failed.load(), "empty I/O retirement allocated unavailable FLS storage"); +} +struct ExitState { + PthreadKey key = -1; + bool explicitExit = true; + std::atomic ready{false}, release{false}; + std::atomic cleanups{0}, destructors{0}; +}; +static void APS5_VABI ExitCleanup(void* opaque) { ++static_cast(opaque)->cleanups; } +static void APS5_VABI ExitDestroy(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.load() == 1, "no-I/O exit TSD preceded cleanup"); + ++state.destructors; +} +static void* APS5_VABI ExitWorker(void* opaque) { + auto& state = *static_cast(opaque); + Require(scePthreadSetspecific(state.key, &state) == 0, "set no-I/O exit TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(ExitCleanup, &state, &cleanup); + state.ready.store(true); + Require(Observe([&] { return state.release.load(); }), "release no-I/O exit"); + if (state.explicitExit) scePthreadExit(reinterpret_cast(std::uintptr_t{7})); + __pthread_cleanup_pop_imp_nid_postfix(1); + return reinterpret_cast(std::uintptr_t{7}); +} +static void EmptyGuestExit(bool explicitExit) { + ExitState state; + state.explicitExit = explicitExit; + Require(scePthreadKeyCreate(&state.key, reinterpret_cast(ExitDestroy)) == 0, "create no-I/O exit key"); + Pthread worker = nullptr; + Require(scePthreadCreate(&worker, nullptr, ExitWorker, &state, nullptr) == 0, "create no-I/O exit worker"); + Require(Observe([&] { return state.ready.load(); }), "start no-I/O exit worker"); + FlsExhaustion exhausted; + state.release.store(true); + const bool finished = Observe([&] { return worker->_finished.load(); }); + exhausted.Release(); + Require(finished, "no-I/O guest exit required freeing FLS slots"); + void* result = nullptr; + Require(scePthreadJoin(worker, &result) == 0 && result == reinterpret_cast(std::uintptr_t{7}), "join no-I/O exit worker"); + Require(state.cleanups.load() == 1 && state.destructors.load() == 1, "no-I/O exit cleanup/TSD count"); + Require(scePthreadKeyDelete(state.key) == 0, "delete no-I/O exit key"); +} +#endif +int main(int argc, char** argv) { +#ifdef _WIN32 + if (argc == 2 && std::strcmp(argv[1], "retire_restart") == 0) { EmptyRetirement(true); return 0; } + if (argc == 2 && std::strcmp(argv[1], "retire_empty") == 0) { EmptyRetirement(false); return 0; } + if (argc == 2 && std::strcmp(argv[1], "return_empty") == 0) { EmptyGuestExit(false); return 0; } + if (argc == 2 && std::strcmp(argv[1], "exit_empty") == 0) { EmptyGuestExit(true); return 0; } +#else + static_cast(argc); + static_cast(argv); +#endif + HostThreads(); +} diff --git a/core/libs/tests/GuestIoReadCancel.cpp b/core/libs/tests/GuestIoReadCancel.cpp new file mode 100644 index 00000000000..eaecad260c9 --- /dev/null +++ b/core/libs/tests/GuestIoReadCancel.cpp @@ -0,0 +1,238 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +struct State { + int input = -1; + int output = -1; + PthreadKey key = -1; + std::string operation; + bool disabled = false; + bool pending = false; + bool sce = false; + bool partial = false; + bool waitAll = false; + bool dontWait = false; + bool zero = false; + bool zeroCancel = false; + int error = 0; + std::atomic ready{false}; + std::atomic release{false}; + std::atomic cleanups{0}; + std::atomic destructors{0}; + std::atomic returned{false}; + std::int64_t transferred = -2; + std::array buffer{}; +}; + +static void APS5_VABI Cleanup(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.fetch_add(1) == 0, "cleanup repeated"); + Require(state.destructors.load() == 0, "TSD ran before cleanup"); +} + +static void APS5_VABI Destroy(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.load() == 1, "cleanup did not precede TSD"); + Require(state.destructors.fetch_add(1) == 0, "TSD repeated"); +} + +template +static bool Observe(TPredicate predicate) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +static void* APS5_VABI Reader(void* opaque) { + auto& state = *static_cast(opaque); + if (state.disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable cancellation"); + Require(scePthreadSetspecific(state.key, &state) == 0, "set TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); + state.ready.store(true); + if (state.pending) Require(Observe([&] { return state.release.load(); }), "release pending reader"); + const std::size_t length = state.zero ? 0 : state.partial || state.waitAll ? state.buffer.size() : 1; + KernelIovec vector{state.buffer.data(), length}; + GuestMsghdr message{}; + message.iov = &vector; + message.iovLength = state.operation == "recvmsg_empty" ? 0 : 1; + if (state.operation == "read") state.transferred = read_nid_postfix(state.input, state.buffer.data(), length); + else if (state.operation == "alias") state.transferred = _read_nid_postfix(state.input, state.buffer.data(), length); + else if (state.operation == "readv") state.transferred = readv_nid_postfix(state.input, &vector, 1); + else if (state.operation == "readv_alias") state.transferred = _readv_nid_postfix(state.input, &vector, 1); + else if (state.operation == "pread") state.transferred = pread_nid_postfix(state.input, state.buffer.data(), length, 0); + else if (state.operation == "preadv") state.transferred = preadv_nid_postfix(state.input, &vector, 1, 0); + else if (state.operation == "recv") state.transferred = recv_nid_postfix(state.input, state.buffer.data(), length, state.waitAll ? 0x40 : state.dontWait ? 0x80 : 0); + else if (state.operation == "recvfrom") state.transferred = recvfrom_nid_postfix(state.input, state.buffer.data(), length, state.waitAll ? 0x40 : state.dontWait ? 0x80 : 0, nullptr, nullptr); + else if (state.operation.starts_with("recvmsg")) state.transferred = recvmsg_nid_postfix(state.input, &message, state.waitAll ? 0x40 : state.dontWait ? 0x80 : 0); + else if (state.operation == "sce_read") state.transferred = sceKernelRead(state.input, state.buffer.data(), length); + else if (state.operation == "sce_readv") state.transferred = sceKernelReadv(state.input, &vector, 1); + else Require(false, "unknown read API"); + state.error = *__error_nid_postfix(); + state.returned.store(true); + if (state.zeroCancel) { + Require(state.transferred == 0, "zero receive returned an error without canceling"); + Require(Observe([&] { return state.release.load(); }), "release successful zero receive"); + scePthreadTestcancel(); + } + if (state.disabled || state.sce) { + Require(state.zero ? state.transferred == 0 : state.transferred == 1 && state.buffer[0] == 'x', "disabled read lost data"); + Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable cancellation"); + scePthreadTestcancel(); + } + __pthread_cleanup_pop_imp_nid_postfix(1); + return reinterpret_cast(std::uintptr_t{7}); +} + +static void Loopback(State& state) { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0, "create listener"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0, "bind loopback"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0, "get loopback address"); + Require(listen_nid_postfix(listener, 4) == 0, "listen loopback"); + state.output = socket_nid_postfix(2, 1, 0); + Require(state.output >= 0, "create client"); + Require(connect_nid_postfix(state.output, address.data(), address.size()) == 0, "connect loopback"); + state.input = accept_nid_postfix(listener, nullptr, nullptr); + Require(state.input >= 0, "accept loopback"); + Require(close_nid_postfix(listener) == 0, "close listener"); +} + +static void Run(int argc, char** argv) { + Require(argc == 4, "expected transport, API, and scenario"); + State state; + state.operation = argv[2]; + state.zero = state.operation == "recvmsg_empty" || state.operation == "recvmsg_zero"; + state.sce = state.operation == "sce_read" || state.operation == "sce_readv"; + const std::string scenario = argv[3]; + state.zeroCancel = state.zero && scenario == "cancel"; + state.disabled = scenario == "disabled"; + state.pending = scenario == "pending"; + state.partial = scenario == "partial"; + state.waitAll = scenario == "waitall"; + const bool completion = scenario == "complete" || scenario == "eof" || scenario == "reuse_complete" || state.partial; + const bool reuse = scenario == "reuse" || scenario == "reuse_complete"; + const bool race = scenario == "race"; + const bool wouldBlock = scenario == "timeout" || scenario == "nonblocking" || scenario == "dontwait"; + state.dontWait = scenario == "dontwait"; + if (std::strcmp(argv[1], "pipe") == 0) { + int descriptors[2]{}; + Require(pipe_nid_postfix(descriptors) == 0, "create pipe"); + state.input = descriptors[0]; + state.output = descriptors[1]; + } else if (std::strcmp(argv[1], "datagram") == 0) { + int descriptors[2]{}; + Require(socketpair_nid_postfix(1, 2, 0, descriptors) == 0, "create datagram pair"); + state.input = descriptors[0]; + state.output = descriptors[1]; + } else Loopback(state); + if (scenario == "nonblocking") Require(fcntl_nid_postfix(state.input, 4, 6) == 0, "set nonblocking descriptor"); + if (scenario == "timeout") { + const std::array timeout{0, 60000}; + Require(setsockopt_nid_postfix(state.input, 0xffff, 0x1006, timeout.data(), sizeof(timeout)) == 0, "set receive timeout"); + } + Require(scePthreadKeyCreate(&state.key, reinterpret_cast(Destroy)) == 0, "create TSD key"); + if (state.waitAll) Require(write_nid_postfix(state.output, "x", 1) == 1, "seed partial WAITALL"); + const auto started = std::chrono::steady_clock::now(); + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, Reader, &state, nullptr) == 0, "create reader"); + Require(Observe([&] { return state.ready.load(); }), "reader did not start"); + if (!state.disabled && !state.pending && !state.sce && !wouldBlock) { + const bool registered = Observe([&] { + std::lock_guard lock(reader->cancelLock); + return reader->cancelIo != nullptr; + }); + Require(registered, "read never registered its blocked operation"); + } + int replacement[2]{-1, -1}; + std::thread replacer; + std::atomic replaced{false}; + if (reuse) { + Require(pipe_nid_postfix(replacement) == 0, "create replacement pipe"); + Require(write_nid_postfix(replacement[1], "n", 1) == 1, "seed replacement pipe"); + replacer = std::thread([&] { + Require(dup2_nid_postfix(replacement[0], state.input) == state.input, "replace blocked descriptor"); + Require(close_nid_postfix(replacement[0]) == 0, "close replacement source"); + replaced.store(true); + }); + } + bool prompt = true; + if (!completion && !wouldBlock) { + std::atomic canceled{false}; + std::atomic raceGo{false}; + std::thread writer; + if (race) writer = std::thread([&] { + Require(Observe([&] { return raceGo.load(); }), "release racing writer"); + Require(write_nid_postfix(state.output, "x", 1) == 1, "racing write"); + }); + std::thread requester([&] { + if (race) Require(Observe([&] { return raceGo.load(); }), "release racing cancellation"); + Require(pthread_cancel_nid_postfix(reader) == 0, "request cancellation"); + canceled.store(true); + }); + raceGo.store(true); + prompt = Observe([&] { return canceled.load(); }); + if (!prompt || state.disabled || state.sce) Require(write_nid_postfix(state.output, "x", 1) == 1, "release failed or disabled read"); + requester.join(); + if (writer.joinable()) writer.join(); + state.release.store(true); + } else if (scenario == "eof") Require(shutdown_nid_postfix(state.output, 1) == 0, "complete zero receive at EOF"); + else if (!wouldBlock) Require(write_nid_postfix(state.output, "x", 1) == 1, "complete read"); + Require(Observe([&] { return reader->_finished.load(); }), "reader did not exit"); + void* result = nullptr; + Require(scePthreadJoin(reader, &result) == 0, "join reader"); + Require(prompt, "cancellation required external I/O completion"); + const bool partialCompletion = state.waitAll && result == reinterpret_cast(std::uintptr_t{7}); + const bool racingCompletion = race && result == reinterpret_cast(std::uintptr_t{7}); + Require(result == (completion || partialCompletion || racingCompletion || wouldBlock ? reinterpret_cast(std::uintptr_t{7}) : GUEST_CANCELED), "wrong thread result"); + if (!state.zeroCancel) Require(state.returned.load() == (completion || partialCompletion || racingCompletion || wouldBlock || state.disabled || state.sce), "unexpected read return"); + if (partialCompletion) Require(state.transferred == 1 && state.buffer[0] == 'x', "interrupted WAITALL lost progress"); + if (completion || racingCompletion) Require(state.zero ? state.transferred == 0 : state.transferred == 1 && state.buffer[0] == 'x', "completion lost data"); + if (state.zero && (completion || state.disabled)) { + char byte = 0; + Require(read_nid_postfix(state.input, &byte, 1) == (scenario == "eof" ? 0 : 1), "zero receive consumed stream data"); + if (scenario != "eof") Require(byte == 'x', "zero receive changed stream data"); + } + Require(state.cleanups.load() == 1 && state.destructors.load() == 1, "wrong cleanup or TSD count"); + Require(scePthreadKeyDelete(state.key) == 0, "delete TSD key"); + if (wouldBlock) Require(state.transferred == -1 && state.error == 35, "wrong would-block result"); + if (scenario == "timeout") Require(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(50), "receive timeout returned early"); + if (reuse) { + Require(Observe([&] { return replaced.load(); }), "descriptor replacement did not finish"); + replacer.join(); + char byte = 0; + Require(read_nid_postfix(state.input, &byte, 1) == 1 && byte == 'n', "operation consumed the replacement descriptor"); + Require(close_nid_postfix(replacement[1]) == 0, "close replacement writer"); + } + Require(close_nid_postfix(state.input) == 0 && close_nid_postfix(state.output) == 0, "close endpoints"); +} + +int main(int argc, char** argv) { + const int rounds = argc == 4 && std::strcmp(argv[3], "race") == 0 ? 32 : 1; + for (int round = 0; round < rounds; ++round) Run(argc, argv); +} diff --git a/core/libs/tests/GuestIoSignalClose.cpp b/core/libs/tests/GuestIoSignalClose.cpp new file mode 100644 index 00000000000..2ccae928287 --- /dev/null +++ b/core/libs/tests/GuestIoSignalClose.cpp @@ -0,0 +1,226 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +static void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +template +static bool Observe(TPredicate predicate) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +struct State { + int input = -1; + int replacement = -1; + bool disabled = false; + bool replace = true; + bool leave = false; + bool localJump = false; + bool changeErrno = false; + bool socket = false; + std::intptr_t nativeInput = -1; + alignas(16) std::array jump{}; + std::atomic nativeId{0}; + std::atomic entering{false}; + std::atomic finished{false}; + std::atomic handlers{0}; + int closed = -2; + int replaced = -2; + std::int64_t result = -2; + int error = 0; + char byte = 0; + bool maskPreserved = false; + std::thread::id reader; + std::thread::id handler; +}; + +static State* state; + +static void APS5_VABI SignalHandler(int) { + state->handler = std::this_thread::get_id(); + if (state->localJump) { + alignas(16) std::uint64_t local[12]{}; + if (setjmp_nid_postfix(local) == 0) longjmp_nid_postfix(local, 1); + } + if (state->changeErrno) Require(close_nid_postfix(-1) == -1 && *__error_nid_postfix() == 9, "handler changes errno"); + if (state->replace) { + state->closed = close_nid_postfix(state->input); + state->replaced = dup2_nid_postfix(state->replacement, state->input); + } + state->handlers.fetch_add(1); + if (state->leave) longjmp_nid_postfix(state->jump.data(), 1); +} + +static void APS5_VABI ExceptionHandler(int signal, void*) { SignalHandler(signal); } + +static void* APS5_VABI Reader(void* opaque) { + auto& reading = *static_cast(opaque); + reading.reader = std::this_thread::get_id(); + if (reading.disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable cancellation"); +#ifndef _WIN32 + sigset_t blocked; + sigemptyset(&blocked); + sigaddset(&blocked, SIGUSR2); + Require(pthread_sigmask(SIG_BLOCK, &blocked, nullptr) == 0, "block native interrupt before read"); + sigset_t before; + Require(pthread_sigmask(SIG_SETMASK, nullptr, &before) == 0, "get native mask before read"); + reading.nativeId.store(::syscall(SYS_gettid)); +#endif + reading.entering.store(true); + if (reading.leave && setjmp_nid_postfix(reading.jump.data()) != 0) reading.result = -3; + else reading.result = reading.socket ? recv_nid_postfix(reading.input, &reading.byte, 1, 0) : read_nid_postfix(reading.input, &reading.byte, 1); + reading.error = *__error_nid_postfix(); +#ifndef _WIN32 + sigset_t after; + Require(pthread_sigmask(SIG_SETMASK, nullptr, &after) == 0, "get native mask after read"); + reading.maskPreserved = true; + for (int signal = 1; signal < NSIG; ++signal) + if (sigismember(&before, signal) != sigismember(&after, signal)) reading.maskPreserved = false; +#else + reading.maskPreserved = true; +#endif + reading.finished.store(true); + return reinterpret_cast(std::uintptr_t{7}); +} + +#ifndef _WIN32 +static bool NativeReadBlocked(const State& reading) { + const auto native = reading.nativeId.load(); + if (!native) return false; + std::ifstream input("/proc/self/task/" + std::to_string(native) + "/syscall"); + long call = -1; + std::uint64_t descriptor = 0; + input >> call >> std::hex >> descriptor; + return input && ((call == SYS_read && descriptor == static_cast(reading.input)) || + (reading.socket && ((call == SYS_recvfrom && descriptor == static_cast(reading.nativeInput)) || call == SYS_poll))); +} +#endif + +static void RunCase(int argc, char** argv) { + Require(argc == 3 || argc == 4, "expected signal source, cancellation mode and optional action"); + const std::string source = argv[1]; + const bool exception = source.starts_with("exception"); + const bool unregistered = std::strcmp(argv[2], "unregistered") == 0; + State reading; + state = &reading; + reading.disabled = std::strcmp(argv[2], "disabled") == 0; + reading.socket = source.ends_with("_socket"); + const bool starting = argc == 4 && std::strcmp(argv[3], "starting") == 0; + reading.changeErrno = argc == 4 && std::strcmp(argv[3], "resume_errno") == 0; + const bool resume = starting || reading.changeErrno || (argc == 4 && std::strcmp(argv[3], "resume") == 0); + const bool interrupt = argc == 4 && std::strcmp(argv[3], "interrupt") == 0; + const bool timeout = argc == 4 && std::strcmp(argv[3], "timeout") == 0; + reading.leave = argc == 4 && std::strcmp(argv[3], "jump") == 0; + reading.localJump = argc == 4 && std::strcmp(argv[3], "local_jump") == 0; + reading.replace = !resume && !interrupt && !timeout; + int original[2]{}; + int replacement[2]{}; + Require((reading.socket ? socketpair_nid_postfix(1, 1, 0, original) : pipe_nid_postfix(original)) == 0 && + pipe_nid_postfix(replacement) == 0, "create signal transports"); + reading.input = original[0]; + reading.replacement = replacement[0]; + if (reading.socket) reading.nativeInput = GuestSockets::NativeDescriptor(reading.input); + if (timeout) { + const std::array duration{2, 0}; + Require(setsockopt_nid_postfix(reading.input, 0xffff, 0x1006, duration.data(), sizeof(duration)) == 0, "set interrupted receive timeout"); + } + Require(write_nid_postfix(replacement[1], "n", 1) == 1, "seed replacement pipe"); + const int guestSignal = source.starts_with("fault_signal") ? 11 : 2; + GuestHandler previous = nullptr; + if (exception) Require(sceKernelInstallExceptionHandler(30, reinterpret_cast(ExceptionHandler)) == 0, "install exception handler"); + else if (interrupt) { + const GuestSigaction action{reinterpret_cast(SignalHandler), 0, {}}; + Require(sigaction_nid_postfix(guestSignal, &action, nullptr) == 0, "install non-restarting handler"); + } + else previous = signal_nid_postfix(guestSignal, SignalHandler); + Pthread thread = nullptr; + std::thread native; + if (unregistered) native = std::thread([&] { Reader(&reading); }); + else Require(scePthreadCreate(&thread, nullptr, Reader, &reading, nullptr) == 0, "create signal reader"); + if (starting) Require(Observe([&] { return reading.entering.load(); }), "reader did not start"); + else { +#ifdef _WIN32 + Require(Observe([&] { + BOOL pending = FALSE; + return GetThreadIOPendingFlag(thread->nativeHandle, &pending) && pending; + }), "read did not enter native syscall"); +#else + Require(Observe([&] { return NativeReadBlocked(reading); }), "read did not enter native syscall"); +#endif + } + std::atomic requested{false}; + std::thread sender([&] { + if (exception) Require(sceKernelRaiseException(thread, 30) == 0, "raise exception during read"); +#ifndef _WIN32 + else Require(pthread_kill(unregistered ? native.native_handle() : thread->hostThread, + guestSignal == 11 ? SIGSEGV : SIGINT) == 0, "raise native signal during read"); +#endif + requested.store(true); + }); + if (resume) { + Require(Observe([&] { return reading.handlers.load() == 1 && requested.load(); }), "restart handler was not delivered"); + Require(!reading.finished.load(), "restarting signal ended read"); + Require(write_nid_postfix(original[1], "x", 1) == 1, "complete restarted read"); + } + Require(Observe([&] { return reading.finished.load() && requested.load(); }), "signal handler close did not release interrupted read"); + sender.join(); + if (native.joinable()) native.join(); + else { + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == 0 && result == reinterpret_cast(std::uintptr_t{7}), "join signal reader"); + } + Require(reading.handlers.load() == 1 && reading.handler == reading.reader, "wrong signal delivery"); + if (reading.replace) Require(reading.closed == 0 && reading.replaced == reading.input, "signal close/reuse failed"); + if ((!resume && !reading.leave && (reading.result != -1 || reading.error != 4 || reading.byte != 0)) || + (resume && (reading.result != 1 || reading.byte != 'x'))) + std::fprintf(stderr, "signal receive result %lld, errno %d, byte %d\n", static_cast(reading.result), reading.error, reading.byte); + Require(reading.leave ? reading.result == -3 && reading.byte == 0 : + resume ? reading.result == 1 && reading.byte == 'x' : reading.result == -1 && reading.error == 4 && reading.byte == 0, + "wrong interrupted or restarted read result"); + Require(reading.maskPreserved, "I/O signal mask was not restored"); + char byte = 0; + if (reading.replace) Require(read_nid_postfix(reading.input, &byte, 1) == 1 && byte == 'n', "interrupted read consumed reused descriptor"); + else { + Require(write_nid_postfix(original[1], "p", 1) == 1, "seed original descriptor after signal"); + Require(read_nid_postfix(reading.input, &byte, 1) == 1 && byte == 'p', "signal lost original descriptor"); + } + Require(close_nid_postfix(reading.input) == 0 && close_nid_postfix(original[1]) == 0 && + close_nid_postfix(replacement[0]) == 0 && close_nid_postfix(replacement[1]) == 0, "close signal pipes"); + if (exception) Require(sceKernelRemoveExceptionHandler(30) == 0, "remove exception handler"); + else signal_nid_postfix(guestSignal, previous); +} + +int main(int argc, char** argv) { + const int rounds = argc == 4 && std::strcmp(argv[3], "starting") == 0 ? 32 : 1; + for (int round = 0; round < rounds; ++round) RunCase(argc, argv); +} diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 5079190b402..2d8ab634bfb 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -428,7 +428,7 @@ static void NestedWaitKeepsOuterWake() { Require(sceKernelDeleteSema(sem) == 0); } -static constexpr int AddressRounds = 50; +static constexpr int AddressRounds = 10; static std::uint32_t addressWord = 0; static std::uint32_t otherAddressWord = 0; static std::atomic holdingEntered{false}; @@ -598,7 +598,7 @@ static void ExpectDeliveryWhileLockHeld(int (*lock)(), int (*unlock)(), int befo Require(scePthreadJoin(thread, nullptr) == 0); } -int main() { +static void CaseSelf() { Require(sceKernelRaiseException(scePthreadSelf(), 11) == SCE_KERNEL_ERROR_EINVAL); bool rejected = false; try { @@ -613,17 +613,24 @@ int main() { Require(sceKernelRaiseException(scePthreadSelf(), GUEST_SIGUSR1) == 0); Require(calls.load() == 1 && handlerThread.load() == std::this_thread::get_id()); +} + +static void CaseBusy() { + const int callsBefore = calls.load(); Worker busy; Pthread busyThread = nullptr; Require(scePthreadCreate(&busyThread, nullptr, Busy, &busy, "busy") == 0); while (!busy.started.load()) std::this_thread::yield(); for (int raised = 0; raised < Repeats; ++raised) { Require(sceKernelRaiseException(busyThread, GUEST_SIGUSR1) == 0); - ExpectDelivery(1 + raised, busy.id); + ExpectDelivery(callsBefore + raised, busy.id); } busy.stop.store(true); Require(scePthreadJoin(busyThread, nullptr) == 0); +} +static void CaseWaiting() { + const int callsBefore = calls.load(); Worker waiting; Require(sceKernelCreateSema(&waiting.sem, "raise", 0, 0, 1, nullptr) == 0); Pthread waitingThread = nullptr; @@ -632,13 +639,16 @@ int main() { std::this_thread::sleep_for(std::chrono::milliseconds(50)); for (int raised = 0; raised < Repeats; ++raised) { Require(sceKernelRaiseException(waitingThread, GUEST_SIGUSR1) == 0); - ExpectDelivery(1 + Repeats + raised, waiting.id); + ExpectDelivery(callsBefore + raised, waiting.id); } Require(sceKernelSignalSema(waiting.sem, 1) == 0); Require(scePthreadJoin(waitingThread, nullptr) == 0); Require(waiting.waitResult == 0); Require(sceKernelDeleteSema(waiting.sem) == 0); +} +static void CaseHost() { + const int callsBefore = calls.load(); Worker blocked; Pthread blockedThread = nullptr; hostLock.lock(); @@ -648,7 +658,7 @@ int main() { std::this_thread::sleep_for(std::chrono::milliseconds(20)); Require(sceKernelRaiseException(blockedThread, GUEST_SIGUSR1) == 0); hostLock.unlock(); - ExpectDelivery(1 + 2 * Repeats + round, blocked.id); + ExpectDelivery(callsBefore + round, blocked.id); while (hostAcquired.load() != round + 1) std::this_thread::yield(); hostLock.lock(); blocked.started.store(false); @@ -656,7 +666,10 @@ int main() { } hostLock.unlock(); Require(scePthreadJoin(blockedThread, nullptr) == 0); +} +static void CaseLeaving() { + const int callsBefore = calls.load(); Worker leaving; Require(sceKernelCreateSema(&leaving.sem, "leaving", 0, 0, 1, nullptr) == 0); Pthread leavingThread = nullptr; @@ -669,13 +682,16 @@ int main() { const auto offset = std::chrono::steady_clock::now() + std::chrono::microseconds(round % 16 * 8); while (std::chrono::steady_clock::now() < offset) {} Require(sceKernelRaiseException(leavingThread, GUEST_SIGUSR1) == 0); - ExpectDelivery(1 + 2 * Repeats + HostRounds + round, leaving.id); + ExpectDelivery(callsBefore + round, leaving.id); leavingRound.store(round + 1); } Require(scePthreadJoin(leavingThread, nullptr) == 0); Require(sceKernelDeleteSema(leaving.sem) == 0); +} #ifdef _WIN32 +static void CaseContinuing() { + const int callsBefore = calls.load(); void* const vectored = AddVectoredExceptionHandler(1, ContinueRaised); Require(vectored != nullptr); Worker continuing; @@ -684,24 +700,29 @@ int main() { while (!continuing.started.load() || continued.load() == 0) std::this_thread::yield(); for (int raised = 0; raised < ContinuingRounds; ++raised) { Require(sceKernelRaiseException(continuingThread, GUEST_SIGUSR1) == 0); - ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + raised, continuing.id); + ExpectDelivery(callsBefore + raised, continuing.id); } continuing.stop.store(true); Require(scePthreadJoin(continuingThread, nullptr) == 0); Require(RemoveVectoredExceptionHandler(vectored) != 0); +} +static void CaseSelfing() { + const int callsBefore = calls.load(); Worker selfing; Pthread selfingThread = nullptr; Require(scePthreadCreate(&selfingThread, nullptr, Selfing, &selfing, "selfing") == 0); while (!selfing.started.load()) std::this_thread::yield(); for (int raised = 0; raised < SelfingRounds; ++raised) { Require(sceKernelRaiseException(selfingThread, GUEST_SIGUSR1) == 0); - ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + ContinuingRounds + raised, selfing.id); + ExpectDelivery(callsBefore + raised, selfing.id); Require(handlerSelf.load() == selfingThread); } selfing.stop.store(true); Require(scePthreadJoin(selfingThread, nullptr) == 0); +} +static void CaseLooping() { Worker looping; Pthread loopingThread = nullptr; Require(scePthreadCreate(&loopingThread, nullptr, Looping, &looping, "looping") == 0); @@ -729,7 +750,10 @@ int main() { Require(ResumeThread(loopingHandle) != static_cast(-1)); CloseHandle(loopingHandle); Require(scePthreadJoin(loopingThread, nullptr) == 0); +} +static void CaseVectors() { + const int callsBefore = calls.load(); if (__builtin_cpu_supports("avx")) { for (std::size_t i = 0; i < VectorBytes; ++i) vectorPattern[i] = static_cast(i * 7 + 1); clobberVectors.store(true); @@ -740,13 +764,16 @@ int main() { std::this_thread::sleep_for(std::chrono::milliseconds(20)); for (int raised = 0; raised < VectorRounds; ++raised) { Require(sceKernelRaiseException(vectorsThread, GUEST_SIGUSR1) == 0); - ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + ContinuingRounds + SelfingRounds + raised, vectors.id); + ExpectDelivery(callsBefore + raised, vectors.id); } clobberVectors.store(false); vectorStop = 1; Require(scePthreadJoin(vectorsThread, nullptr) == 0); Require(std::memcmp(vectorPattern, vectorResult, VectorBytes) == 0); } +} + +static void CaseDrain() { Worker churning; Pthread churningThread = nullptr; @@ -822,8 +849,10 @@ int main() { ExpectDelivery(settledCalls + round, settled.id); } Require(scePthreadJoin(settledThread, nullptr) == 0); +} #endif +static void CaseContext() { SpinState spin; Pthread spinningThread = nullptr; Require(scePthreadCreate(&spinningThread, nullptr, Spinning, &spin, "spinning") == 0); @@ -837,7 +866,9 @@ int main() { Require(observedR13.load() == SpinMarker && observedXmm8.load() == SpinMarker); Require(spin.r12 == PatchedR12 && spin.xmm8 == PatchedXmm8); Require(spin.mxcsr == (observedMxcsr.load() | FlushToZero)); +} +static void CaseStarting() { for (int round = 0; round < StartingRounds; ++round) { const int before = calls.load(); startingRelease.store(false); @@ -850,7 +881,9 @@ int main() { startingRelease.store(true); Require(scePthreadJoin(startingThread, nullptr) == 0); } +} +static void CaseLocks() { const int beforeLocks = calls.load(); Require(scePthreadMutexInit(&guestMutex, nullptr, "raise") == 0); ExpectDeliveryWhileLockHeld(LockMutex, UnlockMutex, beforeLocks); @@ -859,24 +892,57 @@ int main() { Require(scePthreadRwlockInit(&guestRwlock, nullptr, "raise") == 0); ExpectDeliveryWhileLockHeld(LockRwlock, UnlockRwlock, beforeLocks + 2 * LockRounds); Require(scePthreadRwlockDestroy(&guestRwlock) == 0); +} +static void CaseFinished() { Pthread finishedThread = nullptr; Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0); while (!finishedReturned.load()) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); Require(sceKernelRaiseException(finishedThread, GUEST_SIGUSR1) == SCE_KERNEL_ERROR_ESRCH); Require(scePthreadJoin(finishedThread, nullptr) == 0); +} - Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == 0); - - Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&NestedHandler)) == 0); - NestedWaitKeepsLaterWaiters(); - NestedWaitKeepsOuterWake(); - Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == 0); - - Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&HoldingHandler)) == 0); - HandlerLeavesAddressLock(AddressWaking, "address waking"); - HandlerLeavesAddressLock(AddressWaiting, "address waiting"); - HandlerRunsInUnboundedAddressWait(); - Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == 0); +static void CaseAddressWaking() { HandlerLeavesAddressLock(AddressWaking, "address waking"); } +static void CaseAddressWaiting() { HandlerLeavesAddressLock(AddressWaiting, "address waiting"); } + +int main(int argc, char** argv) { + Require(argc == 2); + using ExceptionHandler = void (APS5_VABI *)(int, void*); + struct Case { + const char* name; + void (*run)(); + ExceptionHandler handler; + }; + const Case cases[] = { + {"self", CaseSelf, nullptr}, + {"busy", CaseBusy, Handler}, + {"waiting", CaseWaiting, Handler}, + {"host", CaseHost, Handler}, + {"leaving", CaseLeaving, Handler}, + {"context", CaseContext, Handler}, + {"starting", CaseStarting, Handler}, + {"locks", CaseLocks, Handler}, + {"finished", CaseFinished, Handler}, +#ifdef _WIN32 + {"continuing", CaseContinuing, Handler}, + {"selfing", CaseSelfing, Handler}, + {"looping", CaseLooping, Handler}, + {"vectors", CaseVectors, Handler}, + {"drain", CaseDrain, Handler}, +#endif + {"nested_later", NestedWaitKeepsLaterWaiters, NestedHandler}, + {"nested_outer", NestedWaitKeepsOuterWake, NestedHandler}, + {"address_waking", CaseAddressWaking, HoldingHandler}, + {"address_waiting", CaseAddressWaiting, HoldingHandler}, + {"address_idle", HandlerRunsInUnboundedAddressWait, HoldingHandler}, + }; + for (const auto& selected : cases) { + if (std::strcmp(argv[1], selected.name) != 0) continue; + if (selected.handler) Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(selected.handler)) == 0); + selected.run(); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == 0); + return 0; + } + Require(false); } diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index 5440dc7032f..0f067513858 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -137,7 +137,6 @@ static void CheckSigsetjmp() { Require(sigsetjmp_nid_postfix(buffer, 0) == 0); for (std::size_t offset = 72; offset < 88; offset += 4) Require(BufferWord(buffer, offset) == Filler); Require(BufferWord(buffer, 88) == 0); - Require(BufferWord(buffer, 92) == Filler); Require(BlockedMask() == 0x30u); SetBlockedMask(0x80000005u); @@ -145,7 +144,6 @@ static void CheckSigsetjmp() { Require(BufferWord(buffer, 72) == 0x80000005u); for (std::size_t offset = 76; offset < 88; offset += 4) Require(BufferWord(buffer, offset) == 0); Require(BufferWord(buffer, 88) == 1); - Require(BufferWord(buffer, 92) == Filler); Require(BlockedMask() == 0x80000005u); SetBlockedMask(0x6u); diff --git a/core/libs/tests/GuestSocketReadWrite.cpp b/core/libs/tests/GuestSocketReadWrite.cpp index f320cf65691..cf5ff3effa1 100644 --- a/core/libs/tests/GuestSocketReadWrite.cpp +++ b/core/libs/tests/GuestSocketReadWrite.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -110,8 +111,15 @@ int main() { Require(close_nid_postfix(client) == 0); Require(Readable(server, In | Hup) && read_nid_postfix(server, received, sizeof(received)) == 0); Require(sceKernelRead(server, received, sizeof(received)) == 0); - std::int64_t written = 0; - for (int i = 0; i < 100 && written >= 0; ++i) written = write_nid_postfix(server, request, sizeof(request)); + std::int64_t written = write_nid_postfix(server, request, sizeof(request)); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while ((written >= 0 || *__error_nid_postfix() != 32) && std::chrono::steady_clock::now() < deadline) { + Require(written >= 0 || *__error_nid_postfix() == 35 || *__error_nid_postfix() == 54); + GuestPollDescriptor disconnected{server, 0, 0}; + const auto left = std::chrono::ceil(deadline - std::chrono::steady_clock::now()).count(); + Require(poll_nid_postfix(&disconnected, 1, static_cast(left > 0 ? left : 0)) >= 0); + written = write_nid_postfix(server, request, sizeof(request)); + } Require(written == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(server) == 0); *__error_nid_postfix() = 0; diff --git a/core/libs/tests/GuestStreamClose.cpp b/core/libs/tests/GuestStreamClose.cpp index bfd1f21311b..a414f363fbb 100644 --- a/core/libs/tests/GuestStreamClose.cpp +++ b/core/libs/tests/GuestStreamClose.cpp @@ -28,7 +28,6 @@ static constexpr const char* Reused = "guest_stream_close_reused.txt"; static constexpr const char* Racing = "guest_stream_close_racing.bin"; static constexpr int ReadWrite = 0x0002; static constexpr int CreateReadWrite = 0x0202; -static constexpr std::int64_t BadDescriptor = static_cast(0x80020009u); static constexpr int Append = 0x0008; static constexpr int CreateWriteAppend = 0x0209; static constexpr int Truncate = 0x0400; @@ -55,27 +54,56 @@ static FileStream* Reopen(int closed, const char* mode) { return stream; } +static std::atomic observedEvents{0}; +static std::atomic releaseWrite{false}; +static int observedDescriptor = -1; + +static bool Await(auto&& ready) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!ready()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +static void ObserveClose(int descriptor, int event) { + if (descriptor != observedDescriptor) return; + observedEvents.fetch_or(1 << event); + if (event == 0) Require(Await([] { return releaseWrite.load(); })); +} + static void CloseWaitsForPositionedWrites() { - const std::vector data(std::size_t{8} << 20, 'p'); - for (int round = 0; round < 16; ++round) { +#ifdef _WIN32 + const std::vector data(4096, 'p'); + for (bool reopen : {false, true}) { const int descriptor = sceKernelOpen(Racing, CreateReadWrite | Truncate, 0644); Require(descriptor >= 0); auto* stream = fdopen_nid_postfix(descriptor, "r+"); Require(stream != nullptr); - std::atomic started = false; + observedDescriptor = descriptor; + observedEvents = 0; + releaseWrite = false; + FileStreamSetCloseHookForTest_nid_no_patch(ObserveClose); std::int64_t written = 0; std::thread writer([&] { - started = true; written = sceKernelPwrite(descriptor, data.data(), data.size(), 0); }); - while (!started) std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(round % 4)); - if (round % 2 != 0) stream = freopen_nid_postfix(Reused, "r", stream); - Require(stream != nullptr && fclose_nid_postfix(stream) == 0); + Require(Await([] { return (observedEvents.load() & 1) != 0; })); + std::thread closer([&] { + if (reopen) stream = freopen_nid_postfix(Reused, "r", stream); + Require(stream != nullptr && fclose_nid_postfix(stream) == 0); + }); + Require(Await([] { return (observedEvents.load() & 2) != 0; })); + releaseWrite = true; writer.join(); - Require(written == static_cast(data.size()) || written == BadDescriptor); + closer.join(); + FileStreamSetCloseHookForTest_nid_no_patch(nullptr); + Require(written == static_cast(data.size())); + Require(Contents(Racing) == std::string(data.begin(), data.end())); } std::remove(Racing); +#endif } static void CloseWaitsForALockedStream() { @@ -83,19 +111,18 @@ static void CloseWaitsForALockedStream() { Require(descriptor >= 0); auto* stream = fdopen_nid_postfix(descriptor, "r+"); Require(stream != nullptr); - std::atomic locked = false; - std::int64_t position = -1; - std::thread holder([&] { - flockfile_nid_postfix(stream); - locked = true; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - position = sceKernelLseek(descriptor, 0, FromStart); - funlockfile_nid_postfix(stream); - }); - while (!locked) std::this_thread::yield(); - Require(fclose_nid_postfix(stream) == 0); - holder.join(); - Require(position == 0); + observedDescriptor = descriptor; + observedEvents = 0; + FileStreamSetCloseHookForTest_nid_no_patch(ObserveClose); + flockfile_nid_postfix(stream); + std::thread closer([&] { Require(fclose_nid_postfix(stream) == 0); }); + Require(Await([] { return (observedEvents.load() & 4) != 0; })); + Require(sceKernelLseek(descriptor, 0, FromStart) == 0); + char byte = 0; + Require(read_nid_postfix(descriptor, &byte, 1) == 0); + funlockfile_nid_postfix(stream); + closer.join(); + FileStreamSetCloseHookForTest_nid_no_patch(nullptr); std::remove(Racing); } diff --git a/core/libs/tests/GuestUnderscoreJumps.cpp b/core/libs/tests/GuestUnderscoreJumps.cpp index 9ea166d82a7..05b8495eb0e 100644 --- a/core/libs/tests/GuestUnderscoreJumps.cpp +++ b/core/libs/tests/GuestUnderscoreJumps.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/JumpBuffer.hpp" #include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -102,6 +103,7 @@ static std::uint32_t BufferWord(const std::uint64_t* buffer, std::size_t offset) static int JumpBack(int value, std::uint32_t maskAtSet, std::uint32_t maskAtJump) { std::uint64_t buffer[12]; for (auto& word : buffer) word = 0xa5a5a5a5a5a5a5a5ull; + const auto context = JumpBuffer::GuestContextCurrent_nid_no_patch(); volatile int jumped = 0; SetBlockedMask(maskAtSet); const int result = _setjmp_nid_postfix(buffer); @@ -109,11 +111,16 @@ static int JumpBack(int value, std::uint32_t maskAtSet, std::uint32_t maskAtJump Require(result == 0); for (std::size_t offset = 72; offset < 88; offset += 4) Require(BufferWord(buffer, offset) == Filler); Require(BufferWord(buffer, 88) == 0); - Require(BufferWord(buffer, 92) == Filler); + Require(BufferWord(buffer, 92) == context); jumped = 1; SetBlockedMask(maskAtJump); - _longjmp_nid_postfix(buffer, value); + alignas(16) std::uint64_t copied[12]; + std::memcpy(copied, buffer, sizeof(copied)); + const auto other = JumpBuffer::GuestContextCreate_nid_no_patch(0); + JumpBuffer::GuestContextSet_nid_no_patch(other.identity); + _longjmp_nid_postfix(copied, value); } + Require(JumpBuffer::GuestContextCurrent_nid_no_patch() == context); return result; } diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index e87831a5b7c..91bf086415e 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -300,7 +300,6 @@ void CheckCapture() { Require(machine.len == sizeof(UserMcontext) && machine.fpformat == 0x10002 && machine.ownedfp == 0x20001, "getcontext sets mc_len, mc_fpformat and mc_ownedfp"); Require(machine.onstack == 0 && machine.trapno == 0 && machine.addr == 0 && machine.flags == 0 && machine.err == 0, "getcontext zeroes the trap fields"); Require(machine.fsbase == 0 && machine.gsbase == 0 && machine.xfpustate == 0 && machine.xfpustateLength == 0, "getcontext zeroes the bases and the extended FPU state"); - for (const auto word : machine.spare) Require(word == 0, "getcontext zeroes mc_spare"); Require(context.sigmask[0] == 0x24 && context.sigmask[1] == 0 && context.sigmask[2] == 0 && context.sigmask[3] == 0, "getcontext saves the signal mask"); std::uint32_t mxcsr = 0; std::memcpy(&mxcsr, reinterpret_cast(machine.fpstate) + FxsaveMxcsr, sizeof(mxcsr)); diff --git a/core/libs/tests/HostThreadLocalLibrary.cpp b/core/libs/tests/HostThreadLocalLibrary.cpp index 9f11e94d279..097b5644a18 100644 --- a/core/libs/tests/HostThreadLocalLibrary.cpp +++ b/core/libs/tests/HostThreadLocalLibrary.cpp @@ -14,6 +14,7 @@ struct Value { }; struct First {}; struct Second {}; +struct Unused {}; } #ifdef _WIN32 @@ -23,9 +24,15 @@ struct Second {}; #endif extern "C" TEST_EXPORT void TouchHostThreadLocal() { +#ifdef _WIN32 + if (FindHostThreadLocal() != nullptr) std::abort(); +#endif auto& first = HostThreadLocal(); auto& second = HostThreadLocal(); if (&first == &second || &first != &HostThreadLocal()) std::abort(); +#ifdef _WIN32 + if (FindHostThreadLocal() != &first || FindHostThreadLocal() != &second) std::abort(); +#endif } extern "C" TEST_EXPORT unsigned DestroyedHostThreadLocals() { return destroyed.load(); } diff --git a/core/libs/tests/NetDontWait.cpp b/core/libs/tests/NetDontWait.cpp index c91b7d0bb75..73170d66d56 100644 --- a/core/libs/tests/NetDontWait.cpp +++ b/core/libs/tests/NetDontWait.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -135,6 +136,18 @@ void CheckReceive(std::string_view api) { for (int iteration = 0; iteration < 32; ++iteration) { const char byte = 'x'; Require(sceNetSend(connection.sender, &byte, 1, 0) == 1, "send race payload"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + for (;;) { + char pending = 0; + const auto result = sceNetRecv(connection.receiver, &pending, 1, DontWait | 0x2); + if (result == 1) { + Require(pending == byte, "peeked race payload changed"); + break; + } + Require(result < 0 && *sceNetErrnoLoc() == 35, "peek before reader race must return data or EAGAIN"); + Require(std::chrono::steady_clock::now() < deadline, "reader race payload did not become readable"); + std::this_thread::yield(); + } std::barrier start(readers + 1); std::vector>> receiving; for (int index = 0; index < readers; ++index) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 5548fba8683..ed582685d19 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -46,7 +46,7 @@ - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [_Thrd_lt](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `OCbJ96N1utA`) compares the unsigned 64-bit values of two `thrd_t` handles, matching firmware 13.42's `libSceLibcInternal.sprx` (plain `first < second`, returning 1 or 0, verified on all 121 tested handle pairs). C++ only requires a strict total order over thread IDs; arbitrary handle bit patterns still rely on the integer comparison. -- [setjmp, longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) save the signal mask at 72 in the jump buffer and restore it, as FreeBSD 11.4's [`setjmp` and `longjmp`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/amd64/gen/setjmp.S#L51-L85) do, which is unverified on a console. They go through the `sigprocmask` libkernel registers when it loads and mark the buffer at 88 as `sigsetjmp` does, so a buffer saved before then is jumped to without changing the mask +- [setjmp, longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) save the signal mask at 72 in the jump buffer and restore it, as FreeBSD 11.4's [`setjmp` and `longjmp`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/amd64/gen/setjmp.S#L51-L85) do, which is unverified on a console. They go through the `sigprocmask` libkernel registers when it loads and mark the buffer at 88 as `sigsetjmp` does, so a buffer saved before then is jumped to without changing the mask. The in-process opaque buffer also stores a context identity at 92 for I/O delivery lifetime; this implementation metadata is not console-verified - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_setjmp, _longjmp, longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) - `_setjmp` and `_longjmp` use the buffer layout of `setjmp` here, not that of the `libc.prx` bundled with PPSA21564 (build `J03093718`), whose `_setjmp` (0x1153a0) stores the stack pointer at the call, the x87 control word at 64 and MXCSR at 68, as FreeBSD 11.4's [`_setjmp.S`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/amd64/gen/_setjmp.S) does. `_setjmp` also writes 0 at 88, where that binary's leaves the buffer, so `longjmp` restores no signal mask from it. `longjmp` restores the saved MXCSR exception flags, while that binary's `longjmp` (0x54900) and `_longjmp` (0x1153e0) keep the current flags and take only the other MXCSR bits from the buffer, as `_longjmp` here does - [__std_min_8i](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `HWeZH0iNZU8`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, signed 64-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x62830`, size `0x1f4`, read from disassembly and run directly on the host, not measured on a console. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6607f8093d7..8f7ad1dbc96 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -220,7 +220,7 @@ - [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`, `/system/common/cert/CA_LIST.cer` and `/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. Whether the console serves a file at `/system/common/cert/CA_LIST.cer` or `/common/cert/CA_LIST.cer` is unverified: the only evidence for the two is that PPSA99058 opens them for its certificate store and networking works once they resolve, which shows a call site and not a console listing, so a title that tries several locations may take a branch here that it never takes on the console (`/system/common/` is at least a prefix `sceKernelLoadStartModule` already maps, for `/system/common/lib/`; nothing shows that a sandboxed process can open `/common/cert/` at the sandbox root). 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, 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), as PPSA99039's OpenSSL `ASYNC` passes. `getcontext` writes and `setcontext` reads the 0x330 bytes before `uc_link` (`UC_COPY_SIZE`). Unwritable or unreadable pointers fail with `EFAULT` (14) via checked guest copies (`copyout`/`copyin` on FreeBSD); on Linux these make `process_vm_writev`/`process_vm_readv` syscalls instead of plain memory copies, a swap making two. The console's own layout is unverified; exception contexts differ (see `sceKernelRaiseException`). +- [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), as PPSA99039's OpenSSL `ASYNC` passes. `getcontext` writes and `setcontext` reads the 0x330 bytes before `uc_link` (`UC_COPY_SIZE`). Unwritable or unreadable pointers fail with `EFAULT` (14) via checked guest copies (`copyout`/`copyin` on FreeBSD); on Linux these make `process_vm_writev`/`process_vm_readv` syscalls instead of plain memory copies, a swap making two. The console's own layout is unverified; exception contexts differ (see `sceKernelRaiseException`). Opaque contexts use 16 `mc_spare` bytes for an identity and saved stack pointer; copies retain it and a manually retargeted stack replaces it. This metadata has no console evidence. - [ksem_init, ksem_post, ksem_wait, ksem_trywait, ksem_timedwait, ksem_getvalue and ksem_destroy](../../../core/libs/prx/libkernel/Semaphore/src/Ksem.cpp) (libkernel) - `dup` of a ksem id, or `dup2` from one, throws, where FreeBSD shares the semaphore, and a guest signal does not interrupt a wait with `EINTR`. Otherwise they follow FreeBSD 11 `uipc_sem.c`: each semaphore is kept in a table keyed by a descriptor reserved on the host null device, as a ksem id is a descriptor there; a guest `close`, or `dup2` over the id, drops it through libkernel's close notification; `ksem_init` fails with `ENOSPC` when no descriptor is left or 30 semaphores (`SEM_MAX`) exist; ksem_getvalue rejects an unwritable value pointer with EFAULT (14) via checked guest copy, as FreeBSD's sys_ksem_getvalue does through copyout - [getcontext, setcontext and swapcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (WebAssembly) throw because switching guest x86-64 register contexts on a WebAssembly host is not implemented - [Guest memory mappings](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (WebAssembly) throw for file-backed or shared mappings and for mapping one direct-memory range at two addresses; WebAssembly linear memory has no host mapping primitive for these aliases @@ -235,21 +235,23 @@ - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) (libkernel) delivers a fault raised inside a running handler again, as with `SA_NODEFER`; FreeBSD blocks the signal while its handler runs and ends the process on a second synchronous fault. A handler that returns without changing the context runs again on the same fault, as FreeBSD's `sigreturn` resumes the faulting instruction; the console's behaviour there is not verified - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) (libkernel) on Windows runs the handler after a stub has saved YMM0-15, outside the exception dispatcher, on the faulting stack below the 128-byte red zone. The kernel writes the exception frame directly below the faulting `rsp`, so the red zone of the faulting function is overwritten before the handler runs and a function that kept values there loses them when it resumes. The page-fault error code is rebuilt from the exception record: the write and instruction-fetch bits from its access type, the present bit from whether `VirtualQuery` reports the page committed and accessible, though the hardware clears it for such a page until it is first touched. A fault raised on a thread between the redirection of an earlier fault and the start of its delivery, which only a `sceKernelRaiseException` handler can cause there, is left to the crash report - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) (libkernel) on Linux stops receiving SIGSEGV, SIGBUS, SIGILL or SIGFPE when a guest `signal` for it is installed after the first fault handler, which replaces the host handler. The conversion stubs that `--to-intel` and `--to-rosetta` add to the executable count as title code, so a fault inside one reports the stub's address. With glibc older than 2.35 the faulting object is found with `dladdr1`, which POSIX does not list as async-signal-safe; newer glibc uses `_dl_find_object`, which is. The host handlers use `SA_NODEFER`, leave a page fault the write watch claims to it, and pass the faults they do not deliver, and signals the kernel did not raise, to the handlers installed before. Other hosts record the handler and do not deliver host faults -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. A thread holding the address-wait lock (`sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake`, `_umtx_op`) records the signal and runs the handler once it releases the lock or while it waits; other emulator locks are not deferred. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux uses host SIGUSR1. A handler needing a host lock the interrupted code holds deadlocks; on the console, kernel objects are syscalls and libthr defers signals under internal locks. Address waits and POSIX read ownership defer asynchronous delivery until their guards are released; other host locks are not deferred. A waiting handler retains the interrupted host locks. Restartable POSIX reads resume only while descriptor generations remain unchanged. Non-restarting host calls still end early: sleeps that do not retry return before their time, and sceNetEpollWait and socket transfers with timeouts return EINTR. - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler after the raise has returned when the target is not in guest code (the executable and the `*.guest.prx` modules): a target in a libkernel wait takes it as an APC at that wait; a target elsewhere in host code, running or blocked, takes it when it returns into guest code, through a debug register 0 execute breakpoint at that return address or a host thread that keeps sampling it. A target in guest code is redirected at once. Unlike the console, the handler's context at such a return holds no vector register beyond what the C runtime context saves; a target that stays in host code waiting on a thread already stopped is never stopped; a target inside NtContinue makes the raise throw after 1 s; a debugger or title using debug register 0 on a raised thread is not modeled - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`; FreeBSD keeps a mask per thread - 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 +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. POSIX read/receive waits also wake and defer delivery until their ownership guards are released. A thread blocked in another host wait, including a wait under `APS5_COARSE_TIMED_WAITS`, is redirected and runs the handler only when that wait returns - 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 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 - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - 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 storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console - [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread and sets the handle to 0 (32 and 33 tried), 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here; `pthread_create_name_np` goes through `scePthreadCreate` and so also clears the handle on a long name, but only the sce form was measured -- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - PS5 kernel wakeup on mid-sleep cancellation remains unmeasured. [Firmware 13.42 disassembly and emulated checks](https://github.com/boykopovar/AnyPS5/pull/8916#issuecomment-6416760904) support the SCE cancellation checks before and after sleep. Long SCE waits still do not wake for cancellation here; six deferred tests pin that model until measured. POSIX `usleep`/`nanosleep`/`_nanosleep` wake on enabled cancellation only above 1 ms; shorter waits and short SCE waits retain the GPU wake path. POSIX `sleep` wakes on enabled cancellation. Requests act only in `scePthreadTestcancel`, condition-variable, semaphore, join and sleep waits and their POSIX names; file/socket calls are not cancellation points. States/types 0/1 and 0/2, canceled value 1 and cleanup layout follow FreeBSD libthr, not console measurements. Asynchronous cancellation acts like deferred except at the two set calls. +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - PS5 kernel wakeup on mid-sleep cancellation remains unmeasured. [Firmware 13.42 disassembly and emulated checks](https://github.com/boykopovar/AnyPS5/pull/8916#issuecomment-6416760904) support the SCE cancellation checks before and after sleep. Long SCE waits still do not wake for cancellation here; six deferred tests pin that model until measured. POSIX `usleep`/`nanosleep`/`_nanosleep` wake on enabled cancellation only above 1 ms; shorter waits and short SCE waits retain the GPU wake path. POSIX `sleep` wakes on enabled cancellation. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. States/types 0/1 and 0/2, canceled value 1 and cleanup layout follow FreeBSD libthr, not console measurements. Asynchronous cancellation acts like deferred except at the two set calls. +- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) is not console-verified. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`; SCE reads and libSceNet gain no cancellation points. Unix requires unused host `SIGUSR2` and throws if occupied. Non-interruptible drivers may delay cancellation and close indefinitely. POSIX `close`, `sceKernelClose` and guest-socket close interrupt and drain admitted POSIX reads, even with cancellation disabled; completed or partial results win, otherwise the read returns `EINTR`. This differs from the reported console behavior where close returns while the read stays blocked, which is unverified here. Descriptor replacement and stdio closure may still wait indefinitely. Descriptor-access waits are cancellable. +- [POSIX I/O signal delivery](../../../core/libs/prx/libkernel/Pthread/src/IoSignals.cpp) (libkernel) has no console verification; its macOS backend has not been run. Asynchronous caught native signals and raised exceptions interrupt POSIX read waits and run after operation guards are released, including disabled cancellation and unregistered callers. Restartable reads retry only if descriptor generations remain unchanged; socket receive timeouts prevent restart. Synchronous host faults keep the existing immediate delivery behavior. Guest context restoration and longjmp identify suspended contexts without retaining a stack-address registry; abandoned delivery/restart state is retired and host signal masks are restored. Context identity exhaustion throws rather than reusing an identity. - [Cancellation wait registration](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - cancellation of a wait registered before thread exit still waits for the target to unregister, including while cancellation is disabled. A blocked handler or self-cancellation inside the interrupted wait can prevent the requester from returning. Nested waits do not restore the outer registration. If Windows waiter allocation throws after registration, `cancelWait` remains set and a join retains its `_joining` claim. Runtime-created thread exit skips C++ stack unwinding, so destructors alone cannot clean up registrations. These host progress limitations are not console measurements. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - Console behavior remains unverified; affinity is bookkeeping for guest CPU reporting and inheritance, not host scheduling. ROOT/CPUSET setters, numbered sets, jails, the WHICH base setter and interrupts throw. [`kern_cpuset_setaffinity`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1206-L1314) supplies level/scope error ordering. FreeBSD rejects any bit outside the parent set and the console returns `EPERM` for a CPU outside the process's set (rooklz, #7135); here the mask is clipped to `sceKernelGetAvailableCpumask` and 0 returned, with `EDEADLK` when none remain. Sizes 17 to 23 throw (unmeasured). A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - The title's base set at CPU_LEVEL_CPUSET and CPU_WHICH_CPUSET id -1 is assumed to be `sceKernelGetAvailableCpumask`, unverified on a console. CPU_LEVEL_ROOT answers `0xffff`, as a PS5 payload process read it (rooklz, #7135 comment 6390891591); a title's root set was not measured (PRWB00001 asks for that level). Numbered sets, jails and WHICH interrupt/domain masks throw. [`kern_cpuset_getaffinity`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1088-L1187) supplies target/level error ordering and process-mask union; threads not created through libkernel are missing from that union. A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured @@ -280,8 +282,8 @@ - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and the first stream `send` after the peer closed succeeds on the loopback connection, where the console returns `EPIPE`; the sends after it report `EPIPE`. `read` and `write` on a pair end go to the host as on every guest socket. `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported - Guest sockets on macOS ([the equeue wait, poll, kevent, fstat and ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp)): macOS has no `ppoll`, so the wait uses `poll` with the timeout rounded up to a millisecond; it has no `POLLRDHUP`, so a stream that the peer closed is found by peeking one byte when it is readable; XNU's `poll` reports `POLLHUP`, with `POLLPRI` and without `POLLOUT`, once either direction of a stream has ended, so a guest stream socket it reports hung up is asked about each direction alone and keeps `POLLHUP` only once both have ended, as FreeBSD does; a call that asks for no event gets nothing, where FreeBSD reports `POLLHUP`; `FIONREAD` on a datagram socket, and the byte count that `fstat` and `kevent` report for it, is `SO_NREAD`, the size of the next datagram, because macOS adds the source address to `FIONREAD`; and `gethostbyname` replaces `gethostbyname_r`, which macOS lacks. - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws -- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count -- [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag. A POSIX receive uses a distinct overlapped request, cancels it when an immediate operation would wait, and drains completion before returning. Other Windows transfer paths first ask `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count +- [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules. For POSIX receives, Windows bounds each native receive after progress by `SO_RCVTIMEO`; the other paths bound only the wait for the first data. Windows blocking `MSG_PEEK | MSG_WAITALL` remains unsupported (`EOPNOTSUPP`). Any other flag still fails with `EOPNOTSUPP`. - [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer some cases without the host. A zero-length receive on a stream socket returns 0, or `ENOTCONN` when it is not connected ([L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)), as on a PS5 (firmware 13.60). On an empty datagram socket it returns 0 at once; the console was measured only with `MSG_DONTWAIT`, and the blocking case follows FreeBSD 11.4's `soreceive_dgram`, which returns 0 before waiting. `recvmsg` answers the empty datagram socket the same way but asks the host in the stream cases. A datagram another thread takes after the check makes a zero-length receive wait - [`shutdown(SHUT_RD)`](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on a Windows TCP socket resets the connection when data is unread or arrives later, so the peer's next send fails with `EPIPE`, where FreeBSD's [`sorflush`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2387-L2432) discards the data and the peer's sends succeed. The reset also makes `poll` report `POLLHUP` and `POLLERR` beside `POLLIN` for the shut socket, where FreeBSD's [`sopoll_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3060-L3103) reports only `POLLIN` and `POLLRDNORM`. On a Unix stream socket FreeBSD keeps appending the peer's later data ([`uipc_send`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_usrreq.c#L936-L1001) does not check the receiver's `SBS_CANTRCVMORE`) and receives return it; here receives return 0 and the peer's send fails with `EPIPE` on Linux. - [sendmsg and recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) pass descriptors in `SCM_RIGHTS` messages on Unix-domain sockets through the host's `SCM_RIGHTS` (Linux only), in FreeBSD's guest layout: a 12-byte `cmsghdr`, data at offset 16, `CMSG_SPACE` records. Several `SCM_RIGHTS` messages in one `sendmsg` arrive as one record because the host merges them (FreeBSD keeps one per message), so a buffer sized for the first can't receive it alone. A received socket gets default emulated socket options. Sending a guest kqueue returns `EOPNOTSUPP`, as FreeBSD's `unp_internalize` does; a random device, directory or JIT shared memory descriptor (libkernel's state) throws, as does a zero-byte stream send with descriptors (Linux carries no record without data). Windows throws (socket pairs are loopback TCP), as do other control message types and control data on IP sockets. `MSG_PEEK` with a control buffer reads only the data and leaves passed descriptors queued. @@ -389,3 +391,4 @@ - [send and recv](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) pass the guest buffer to the host call without a guest copy helper, so an unreadable or unwritable buffer is whatever the host does (a host fault signal is possible), where `sendto` and `recvfrom` fail with `EFAULT`. On Windows a stream receive whose writable prefix is shorter than the queued data fails with `EFAULT`, but data arriving after the check, or `MSG_WAITALL`, can return the prefix as a short success where FreeBSD fails. Linux and macOS return the bytes copied before a fault on a stream, where FreeBSD returns `EFAULT`. - [equeue pending-event bound](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - an undrained registration keeps the armed event plus at most 64 queued events: a trigger past the cap drops the oldest queued event and keeps the latest payload, and an `AddEvent` carrying more than 64 queued events keeps the latest 64. Drained bursts report as before. The console's queue depth and which completion it keeps are unknown - [title sandbox app0 alias](../../../core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp) (libkernel) - aliases `/mnt/sandbox/_000/app0` to the application directory `/app0` when metadata is loaded; console jail sandbox mounts for other virtual directories under `/mnt/sandbox` remain unmeasured. +- [POSIX I/O cancellation cost](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - scePthreadCancel blocks the cancelling caller while the target retains its I/O or condition wait registration, repeatedly waking it and yielding without a deadline. A target that cannot leave its native call can keep the caller spinning indefinitely. SignalManager registration still uses a global lock and per-call allocation and signal-mask syscalls; their cost is unmeasured. Close interrupt retries use bounded backoff, but the cancelling caller does not.