From 1d653c9f00146c05529846df61daf22d332d107c Mon Sep 17 00:00:00 2001 From: liu <58376171+lgqdaxia@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:33:20 +0100 Subject: [PATCH 01/61] fix(libkernel): honor per-call nonblocking socket transfers --- core/libs/prx/libkernel/CMakeLists.txt | 10 + .../Socket/include/SocketOptions.hpp | 1 + .../libkernel/Socket/src/SocketOptions.cpp | 2 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 189 +++++-- core/libs/tests/GuestSocketDontWait.cpp | 508 ++++++++++++++++++ core/libs/tests/GuestSslTransport.cpp | 6 +- docs/dev/debt/libkernel.md | 5 +- 7 files changed, 680 insertions(+), 41 deletions(-) create mode 100644 core/libs/tests/GuestSocketDontWait.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1951f94ba65..1a1b556507f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1564,6 +1564,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_port_zero PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_dontwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketDontWait.cpp) + target_include_directories(guest_socket_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_dontwait_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_dontwait_tests) + add_test(NAME guest_socket_dontwait COMMAND guest_socket_dontwait_tests) + set_tests_properties(guest_socket_dontwait PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_dontwait PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_pair_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketPair.cpp) target_include_directories(guest_socket_pair_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_pair_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 44990b46545..42c859304a0 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -7,6 +7,7 @@ namespace GuestSockets { struct SocketOptionState { int reportedError = 0; bool nonblocking = false; + bool transportNonblocking = false; bool noSignal = false; std::int64_t sendTimeout = 0; std::int64_t receiveTimeout = 0; diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 83b52318431..5d6046be0fb 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -200,7 +200,7 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa } #ifdef _WIN32 unsigned long enabled = number != 0; - if (ioctlsocket(handle, FIONBIO, &enabled)) return HostError(); + if (!state.transportNonblocking && ioctlsocket(handle, FIONBIO, &enabled)) return HostError(); #else int enabled = number != 0; if (::ioctl(handle, FIONBIO, &enabled)) return HostError(); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 9de4a4e179c..890a6244483 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -45,6 +45,7 @@ #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketConnect.hpp" +#include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" #include #include #include @@ -86,10 +87,11 @@ int NativeSendFlags(int flags) { #endif } int NativeReceiveFlags(int flags, int type) { - const int waitAll = type == 1 && (flags & GuestWaitAll) != 0 ? MSG_WAITALL : 0; #ifdef _WIN32 - return (flags & GuestPeek ? MSG_PEEK : 0) | waitAll; + static_cast(type); + return flags & GuestPeek ? MSG_PEEK : 0; #else + const int waitAll = type == 1 && (flags & GuestWaitAll) != 0 ? MSG_WAITALL : 0; return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0) | waitAll; #endif } @@ -129,12 +131,82 @@ struct Socket : KernelSocketPoll::NativeOwner { ~Socket() { if (value == Invalid) return; #ifdef _WIN32 + if (options.transportNonblocking && !options.nonblocking) { + unsigned long blocking = 0; + ioctlsocket(value, FIONBIO, &blocking); + } closesocket(value); #else ::close(value); #endif } }; + +template +int Transfer(Socket& socket, bool send, int flags, int length, TOperation operation, bool timed = true) { +#ifdef _WIN32 + bool nonblocking; + std::int64_t timeout; + { + std::lock_guard lock(socket.modeMutex); + nonblocking = socket.options.nonblocking || (flags & GuestDontWait) != 0; + timeout = timed ? (send ? socket.options.sendTimeout : socket.options.receiveTimeout) : 0; + } + if (!send && socket.type == 1 && !nonblocking && (flags & (GuestWaitAll | GuestPeek)) == (GuestWaitAll | GuestPeek)) { + WSASetLastError(WSAEOPNOTSUPP); + return -1; + } + const bool complete = socket.type == 1 && !nonblocking && + (send || ((flags & GuestWaitAll) != 0 && (flags & GuestPeek) == 0)); + const auto deadline = timeout == 0 ? std::chrono::steady_clock::time_point::max() : + KernelSocketSelect::DeadlineAfter(std::chrono::steady_clock::now(), timeout >> 32, + (static_cast(static_cast(timeout)) * 1000000) >> 32); + int transferred = 0; + for (;;) { + const int result = operation(transferred); + if (result >= 0) { + transferred += result; + if (!complete || result == 0 || transferred == length) return transferred; + continue; + } + if (WSAGetLastError() != WSAEWOULDBLOCK || nonblocking) return transferred == 0 ? -1 : transferred; + int milliseconds = -1; + if (timeout != 0) { + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::steady_clock::duration::zero()) { + WSASetLastError(WSAEWOULDBLOCK); + return transferred == 0 ? -1 : transferred; + } + milliseconds = static_cast(std::min( + std::chrono::ceil(remaining).count(), INT_MAX)); + } + fd_set ready; + FD_ZERO(&ready); + FD_SET(socket.value, &ready); + timeval wait{milliseconds < 0 ? 0 : milliseconds / 1000, milliseconds < 0 ? 0 : milliseconds % 1000 * 1000}; + if (::select(0, send ? nullptr : &ready, send ? &ready : nullptr, nullptr, milliseconds < 0 ? nullptr : &wait) < 0) + return transferred == 0 ? -1 : transferred; + } +#else + static_cast(socket); + static_cast(send); + static_cast(flags); + static_cast(length); + static_cast(timed); + return operation(0); +#endif +} + +#ifdef _WIN32 +int TransferForNativeOwner(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length) { + auto& socket = *static_cast(owner); + return Transfer(socket, send, 0, length, [&](int offset) { + auto* bytes = offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset; + return send ? ::send(socket.value, bytes, length - offset, 0) : ::recv(socket.value, bytes, length - offset, 0); + }); +} +#endif + struct Waker { NativeSocket value = Invalid; Waker() { @@ -252,6 +324,12 @@ void ReleaseNumber(int number) { } #endif int Install(std::shared_ptr socket) { +#ifdef _WIN32 + unsigned long nonblocking = 1; + if (ioctlsocket(socket->value, FIONBIO, &nonblocking)) return Fail(NativeError()); + socket->options.transportNonblocking = true; + socket->transfer = TransferForNativeOwner; +#endif const int number = ReserveNumber(socket->value); if (number < 0) return -1; try { @@ -753,16 +831,13 @@ int SocketSendFlags(Socket& socket, int flags) { return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); } int WaitForTransfer(Socket& socket, bool send, int flags) { - if ((flags & GuestDontWait) != 0) { #ifdef _WIN32 - NativePollDescriptor entry{}; - entry.fd = socket.value; - entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); - return WSAPoll(&entry, 1, 0) == 0 ? 35 : 0; + static_cast(socket); + static_cast(send); + static_cast(flags); + return 0; #else - return 0; -#endif - } + if ((flags & GuestDontWait) != 0) return 0; std::int64_t timeout; { std::lock_guard lock(socket.modeMutex); @@ -778,16 +853,11 @@ int WaitForTransfer(Socket& socket, bool send, int flags) { const int milliseconds = static_cast(std::min(std::chrono::ceil(left).count(), INT_MAX)); NativePollDescriptor entry{}; entry.fd = socket.value; -#ifdef _WIN32 - entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); - const int ready = WSAPoll(&entry, 1, milliseconds); - if (ready != 0) return 0; -#else entry.events = static_cast(send ? POLLOUT : POLLIN); const int ready = ::poll(&entry, 1, milliseconds); if (ready > 0 || (ready < 0 && errno != EINTR)) return 0; -#endif } +#endif } #ifdef _WIN32 bool LoopbackName(NativeSocket socket, sockaddr_in& name) { @@ -1122,7 +1192,6 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { if ((flags & ~7) != 0) return Fail(45); #ifdef _WIN32 unsigned long enabled = (flags & 4) != 0; - if (ioctlsocket(socket->value, FIONBIO, &enabled)) return Fail(NativeError()); #else int enabled = (flags & 4) != 0; if (::ioctl(socket->value, FIONBIO, &enabled)) return Fail(NativeError()); @@ -1288,7 +1357,34 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint std::lock_guard lock(socket->modeMutex); socket->options.reportedError = 0; } - return ::connect(socket->value, reinterpret_cast(&native), size) ? Fail(ConnectError()) : 0; + if (::connect(socket->value, reinterpret_cast(&native), size) == 0) return 0; +#ifdef _WIN32 + const int error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK) return Fail(ConnectError()); + { + std::lock_guard lock(socket->modeMutex); + if (socket->options.nonblocking) return Fail(36); + } + for (;;) { + fd_set write; + fd_set except; + FD_ZERO(&write); + FD_ZERO(&except); + FD_SET(socket->value, &write); + FD_SET(socket->value, &except); + if (::select(0, nullptr, &write, &except, nullptr) < 0) return Fail(NativeError()); + int pending = 0; + int pendingBytes = sizeof(pending); + if (::getsockopt(socket->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &pendingBytes)) return Fail(NativeError()); + if (pending != 0) { + WSASetLastError(pending); + return Fail(ConnectError()); + } + if (FD_ISSET(socket->value, &write)) return 0; + } +#else + return Fail(ConnectError()); +#endif } int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const auto socket = Lookup(descriptor); @@ -1304,9 +1400,13 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l if (listener->type != 1 || (address && !length)) return Fail(22); sockaddr_storage peer{}; socklen_t size = sizeof(peer); - const auto native = ::accept(listener->value, address ? reinterpret_cast(&peer) : nullptr, - address ? &size : nullptr); - if (native == Invalid) return Fail(NativeError()); + NativeSocket native = Invalid; + const int result = Transfer(*listener, false, 0, 0, [&](int) { + size = sizeof(peer); + native = ::accept(listener->value, address ? reinterpret_cast(&peer) : nullptr, address ? &size : nullptr); + return native == Invalid ? -1 : 0; + }, false); + if (result < 0) return Fail(NativeError()); Socket guard(native, listener->family, listener->type); try { auto accepted = std::make_shared(native, listener->family, listener->type); @@ -1346,7 +1446,10 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); - const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); + const auto result = Transfer(*socket, true, flags, static_cast(length), [&](int offset) { + return ::send(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, + static_cast(length) - offset, 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) { @@ -1359,8 +1462,10 @@ 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 = Transfer(*socket, false, flags, static_cast(length), [&](int offset) { + return ::recv(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, + static_cast(length) - offset, NativeReceiveFlags(flags, socket->type)); + }); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); #endif @@ -1448,7 +1553,7 @@ int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* arg unsigned long value = 0; if (request == 0x8004667e) value = *static_cast(argument) != 0; #ifdef _WIN32 - const auto result = ioctlsocket(socket->value, request == 0x8004667e ? FIONBIO : FIONREAD, &value); + const auto result = request == 0x8004667e ? 0 : ioctlsocket(socket->value, FIONREAD, &value); #else int nativeValue = static_cast(value); const auto result = request == 0x8004667e ? ::ioctl(socket->value, FIONBIO, &nativeValue) : QueuedBytes(*socket, nativeValue); @@ -1476,14 +1581,19 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st int result; if (!address || (socket->type == 1 && PeerConnected(*socket))) { if (!address && addressLength != 0) return Fail(22); - result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); + result = Transfer(*socket, true, flags, static_cast(length), [&](int offset) { + return ::send(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, + static_cast(length) - offset, SocketSendFlags(*socket, flags)); + }); } else { sockaddr_storage native{}; socklen_t size; if (!Address(socket->family, address, addressLength, native, size, true)) return -1; if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; - result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), - reinterpret_cast(&native), size)); + result = Transfer(*socket, true, flags, static_cast(length), [&](int offset) { + return ::sendto(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, + static_cast(length) - offset, SocketSendFlags(*socket, flags), reinterpret_cast(&native), size); + }); } return result < 0 ? Fail(SendError(socket->type)) : result; } @@ -1504,8 +1614,11 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u sockaddr_storage native{}; socklen_t size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); - auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); + auto result = Transfer(*socket, false, flags, static_cast(length), [&](int offset) { + size = sizeof(native); + return ::recvfrom(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, + static_cast(length) - offset, NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); + }); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); #endif @@ -1643,8 +1756,11 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (total != 0 || socket->type == 1 || ReadableNow(*socket)) { size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); - received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), - NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); + received = Transfer(*socket, false, flags, static_cast(buffer.size()), [&](int offset) { + size = sizeof(native); + return ::recvfrom(socket->value, buffer.data() + offset, static_cast(buffer.size()) - offset, + NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); + }); if (received < 0) { #ifdef _WIN32 if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); @@ -1696,11 +1812,14 @@ static bool SendfileIovecs(const KernelIovec* iov, int count) { } return true; } -static int SendAll(const Socket& socket, const void* data, std::uint64_t length, std::int64_t& sent) { +static int SendAll(Socket& socket, const void* data, std::uint64_t length, std::int64_t& sent) { if (!data && length) return 14; const char* bytes = static_cast(data); while (length) { - const auto result = ::send(socket.value, bytes, static_cast(std::min(length, INT_MAX)), NativeSendFlags(GuestNoSignal)); + const int count = static_cast(std::min(length, INT_MAX)); + const auto result = Transfer(socket, true, 0, count, [&](int offset) { + return ::send(socket.value, bytes + offset, count - offset, NativeSendFlags(GuestNoSignal)); + }); if (result < 0) return NativeError(); bytes += result; length -= static_cast(result); @@ -1708,7 +1827,7 @@ static int SendAll(const Socket& socket, const void* data, std::uint64_t length, } return 0; } -static int SendIovecs(const Socket& socket, const KernelIovec* iov, int count, std::int64_t& sent) { +static int SendIovecs(Socket& socket, const KernelIovec* iov, int count, std::int64_t& sent) { for (int i = 0; iov && i < count; ++i) if (const int error = SendAll(socket, iov[i].base, iov[i].length, sent)) return error; return 0; diff --git a/core/libs/tests/GuestSocketDontWait.cpp b/core/libs/tests/GuestSocketDontWait.cpp new file mode 100644 index 00000000000..84a87c733eb --- /dev/null +++ b/core/libs/tests/GuestSocketDontWait.cpp @@ -0,0 +1,508 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; +using Clock = std::chrono::steady_clock; +constexpr int DontWait = 0x80; +constexpr int WaitAll = 0x40; +constexpr int NonBlocking = 0x20000000; +constexpr int WouldBlock = 35; +constexpr unsigned char FirstByte = 0x6a; +constexpr unsigned char SecondByte = 0x7b; + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +template +bool Within(TPredicate ready, std::chrono::milliseconds duration) { + const auto deadline = Clock::now() + duration; + while (!ready()) { + if (Clock::now() >= deadline) return ready(); + std::this_thread::sleep_for(1ms); + } + return true; +} + +class Watchdog { +public: + Watchdog() : thread([this] { + std::unique_lock lock(mutex); + if (!changed.wait_for(lock, 12s, [this] { return finished; })) { + std::fputs("socket dontwait watchdog expired\n", stderr); + std::fflush(stderr); + std::_Exit(99); + } + }) {} + ~Watchdog() { + { + std::lock_guard lock(mutex); + finished = true; + } + changed.notify_one(); + thread.join(); + } +private: + std::mutex mutex; + std::condition_variable changed; + bool finished = false; + std::thread thread; +}; + +class Pair { +public: + explicit Pair(bool nonblocking = false) { + Require(socketpair_nid_postfix(1, 1 | (nonblocking ? NonBlocking : 0), 0, descriptors.data()) == 0, "cannot create socketpair"); + } + Pair(int reader, int peer) : descriptors{reader, peer} {} + ~Pair() { + ReleasePeer(); + if (descriptors[0] >= 0) close_nid_postfix(descriptors[0]); + } + int Reader() const { return descriptors[0]; } + int Peer() const { return descriptors[1]; } + int Flags() const { return fcntl_nid_postfix(Reader(), 3, std::uint64_t{0}); } + void Send(unsigned char value = FirstByte) { + Require(send_nid_postfix(descriptors[1], &value, 1, 0x20000) == 1, "cannot queue a byte"); + } + void CheckReadable(bool expected) const { + GuestPollDescriptor entry{Reader(), 1, 0}; + const auto result = poll_nid_postfix(&entry, 1, expected ? 2000 : 0); + Require(expected ? result == 1 && (entry.revents & 1) != 0 : result == 0, "socket readability differs from the queued bytes"); + } + void ReleasePeer() noexcept { + if (descriptors[1] < 0) return; + shutdown_nid_postfix(descriptors[1], 2); + close_nid_postfix(descriptors[1]); + descriptors[1] = -1; + } +private: + std::array descriptors{-1, -1}; +}; + +class Receive { +public: + Receive(Pair& pair, int flags, std::size_t length = 2, std::atomic* gate = nullptr) : pair(pair), gate(gate), thread([this, flags, length] { + ready.store(true, std::memory_order_release); + while (this->gate != nullptr && !this->gate->load(std::memory_order_acquire)) std::this_thread::yield(); + *__error_nid_postfix() = 0; + entered.store(true, std::memory_order_release); + result = recv_nid_postfix(this->pair.Reader(), bytes.data(), length, flags); + error = *__error_nid_postfix(); + finished.store(true, std::memory_order_release); + }) {} + ~Receive() { + if (!thread.joinable()) return; + if (gate != nullptr) gate->store(true, std::memory_order_release); + pair.ReleasePeer(); + thread.join(); + } + bool Ready() const { return ready.load(std::memory_order_acquire); } + bool Entered() const { return entered.load(std::memory_order_acquire); } + bool Finished() const { return finished.load(std::memory_order_acquire); } + bool Wait(std::chrono::milliseconds duration) const { return Within([this] { return Finished(); }, duration); } + void Join() { thread.join(); } + void Prompt(const char* message) { + Require(Within([this] { return Entered(); }, 2s), "receiver did not enter its call"); + if (Wait(500ms)) { + Join(); + return; + } + pair.ReleasePeer(); + Require(Wait(2s), "receiver did not stop after peer shutdown"); + Join(); + throw std::runtime_error(message); + } + std::array bytes{0xcc, 0xdd}; + std::int64_t result = std::numeric_limits::min(); + int error = -1; +private: + Pair& pair; + std::atomic* gate; + std::atomic ready{false}; + std::atomic entered{false}; + std::atomic finished{false}; + std::thread thread; +}; + +class SendCall { +public: + SendCall(Pair& pair, int flags) : pair(pair), thread([this, flags] { + *__error_nid_postfix() = 0; + start = Clock::now(); + entered.store(true, std::memory_order_release); + result = send_nid_postfix(this->pair.Reader(), &value, 1, flags | 0x20000); + error = *__error_nid_postfix(); + end = Clock::now(); + finished.store(true, std::memory_order_release); + }) {} + ~SendCall() { + if (!thread.joinable()) return; + pair.ReleasePeer(); + thread.join(); + } + bool Entered() const { return entered.load(std::memory_order_acquire); } + bool Finished() const { return finished.load(std::memory_order_acquire); } + bool Wait(std::chrono::milliseconds duration) const { return Within([this] { return Finished(); }, duration); } + void Join() { thread.join(); } + void Prompt(const char* message) { + Require(Within([this] { return Entered(); }, 2s), "sender did not enter its call"); + if (Wait(500ms)) { + Join(); + return; + } + pair.ReleasePeer(); + Require(Wait(2s), "sender did not stop after peer shutdown"); + Join(); + throw std::runtime_error(message); + } + Clock::duration Elapsed() const { return end - start; } + std::int64_t result = std::numeric_limits::min(); + int error = -1; +private: + Pair& pair; + unsigned char value = FirstByte; + Clock::time_point start{}, end{}; + std::atomic entered{false}; + std::atomic finished{false}; + std::thread thread; +}; + +void AvailableWithWaitAll(Pair& pair) { + const auto flags = pair.Flags(); + Require(flags >= 0 && (flags & 4) == 0, "WAITALL test socket is not blocking"); + pair.CheckReadable(false); + pair.Send(); + pair.CheckReadable(true); + Receive call(pair, DontWait | WaitAll); + call.Prompt("MSG_DONTWAIT | MSG_WAITALL waited for a second byte despite one available byte"); + Require(call.result == 1 && call.bytes[0] == FirstByte && call.bytes[1] == 0xdd, "DONTWAIT | WAITALL did not return exactly the available byte"); + Require(pair.Flags() == flags, "DONTWAIT | WAITALL changed the socket mode"); +} + +void AvailableWithWaitAll() { + Pair pair; + AvailableWithWaitAll(pair); +} + +void OrdinaryWaitAll() { + Pair pair; + const auto flags = pair.Flags(); + pair.Send(); + pair.CheckReadable(true); + Receive call(pair, WaitAll); + Require(Within([&] { return call.Entered(); }, 2s), "blocking WAITALL did not enter"); + Require(!call.Wait(100ms), "ordinary WAITALL returned before its second byte"); + pair.Send(SecondByte); + call.Prompt("ordinary WAITALL did not return after both bytes arrived"); + Require(call.result == 2 && call.bytes[0] == FirstByte && call.bytes[1] == SecondByte, "ordinary WAITALL did not retain blocking semantics"); + Require(pair.Flags() == flags, "ordinary WAITALL changed the socket mode"); +} + +void OrdinaryDontWait(bool available) { + Pair pair; + const auto flags = pair.Flags(); + pair.CheckReadable(false); + if (available) pair.Send(); + pair.CheckReadable(available); + Receive call(pair, DontWait); + call.Prompt("MSG_DONTWAIT blocked"); + if (available) Require(call.result == 1 && call.bytes[0] == FirstByte && call.bytes[1] == 0xdd, "DONTWAIT did not return the available byte"); + else Require(call.result == -1 && call.error == WouldBlock && call.bytes[0] == 0xcc && call.bytes[1] == 0xdd, "empty DONTWAIT did not report EAGAIN without modifying the buffer"); + Require(pair.Flags() == flags, "DONTWAIT changed the socket mode"); +} + +void PeekWithWaitAll() { + Pair pair; + const auto flags = pair.Flags(); + pair.Send(); + pair.CheckReadable(true); + Receive peek(pair, DontWait | WaitAll | 2); + peek.Prompt("DONTWAIT | PEEK | WAITALL waited for another byte"); + Require(peek.result == 1 && peek.bytes[0] == FirstByte && peek.bytes[1] == 0xdd, "DONTWAIT peek did not return only the available byte"); + pair.CheckReadable(true); +#ifdef _WIN32 + Receive unsupported(pair, WaitAll | 2); + unsupported.Prompt("blocking PEEK | WAITALL did not preserve its unsupported result"); + Require(unsupported.result == -1 && unsupported.error == 45 && unsupported.bytes[0] == 0xcc && unsupported.bytes[1] == 0xdd, "blocking Windows PEEK | WAITALL did not report EOPNOTSUPP"); + pair.CheckReadable(true); +#endif + Receive consume(pair, DontWait); + consume.Prompt("receiving the peeked byte blocked"); + Require(consume.result == 1 && consume.bytes[0] == FirstByte, "PEEK consumed the queued byte"); + pair.CheckReadable(false); + Require(pair.Flags() == flags, "PEEK changed the socket mode"); +} + +void AlongsideBlockingRead() { + Pair pair; + const auto flags = pair.Flags(); + Receive blocking(pair, 0, 1); + Require(Within([&] { return blocking.Entered(); }, 2s), "ordinary receiver did not enter"); + Require(!blocking.Wait(100ms), "ordinary receiver did not block on an empty socket"); + Receive immediate(pair, DontWait, 1); + immediate.Prompt("DONTWAIT waited behind another blocking receiver"); + Require(immediate.result == -1 && immediate.error == WouldBlock, "DONTWAIT beside a blocking read did not return EAGAIN"); + Require(!blocking.Finished(), "DONTWAIT disturbed an ordinary blocking receive"); + pair.Send(); + blocking.Prompt("ordinary receiver did not wake when its byte arrived"); + Require(blocking.result == 1 && blocking.bytes[0] == FirstByte, "ordinary receiver lost its queued byte"); + Require(pair.Flags() == flags, "concurrent DONTWAIT changed the socket mode"); +} + +void CheckRestoredBlocking(Pair& pair) { + const auto flags = pair.Flags(); + Require(flags >= 0 && (flags & 4) == 0, "restored socket did not report blocking mode"); + Receive immediate(pair, DontWait, 1); + immediate.Prompt("DONTWAIT blocked after restoring blocking mode"); + Require(immediate.result == -1 && immediate.error == WouldBlock, "empty restored DONTWAIT did not report EAGAIN"); + Receive blocking(pair, 0, 1); + Require(Within([&] { return blocking.Entered(); }, 2s), "restored blocking receiver did not enter"); + Require(!blocking.Wait(100ms), "restored ordinary receive returned without a byte"); + pair.Send(); + blocking.Prompt("restored blocking receiver did not wake when its byte arrived"); + Require(blocking.result == 1 && blocking.bytes[0] == FirstByte, "restored blocking receiver lost its byte"); + Require(pair.Flags() == flags, "receiving changed the restored blocking mode"); + AvailableWithWaitAll(pair); +} + +void SetMode(Pair& pair, unsigned method, bool enabled) { + if (method == 0) { + Require(fcntl_nid_postfix(pair.Reader(), 4, std::uint64_t{enabled ? 4u : 0u}) == 0, "F_SETFL mode change failed"); + } else if (method == 1) { + const int value = enabled ? 1 : 0; + Require(setsockopt_nid_postfix(pair.Reader(), 0xffff, 0x1200, &value, sizeof(value)) == 0, "SO_NBIO mode change failed"); + } else { + std::uint32_t value = enabled ? 1 : 0; + Require(ioctl_nid_postfix(pair.Reader(), 0x8004667e, &value) == 0, "FIONBIO mode change failed"); + } +} + +void ModeChanges() { + for (unsigned method = 0; method < 3; ++method) { + Pair pair; + const auto flags = pair.Flags(); + Require(flags >= 0 && (flags & 4) == 0, "mode-change socket did not start blocking"); + SetMode(pair, method, true); + Require(pair.Flags() == (flags | 4), "enabling nonblocking mode was not reflected by F_GETFL"); + Receive immediate(pair, 0, 1); + immediate.Prompt("ordinary receive blocked after enabling nonblocking mode"); + Require(immediate.result == -1 && immediate.error == WouldBlock, "nonblocking ordinary receive did not report EAGAIN"); + SetMode(pair, method, false); + Require(pair.Flags() == flags, "disabling nonblocking mode did not restore F_GETFL"); + CheckRestoredBlocking(pair); + } +} + +class Descriptor { +public: + explicit Descriptor(int value) : value(value) { Require(value >= 0, "cannot create an accept test socket"); } + ~Descriptor() { if (value >= 0) close_nid_postfix(value); } + int Get() const { return value; } + int Take() { const int result = value; value = -1; return result; } +private: + int value; +}; + +std::unique_ptr TcpPair() { + Descriptor listener(socket_nid_postfix(2, 1, 0)); + Descriptor client(socket_nid_postfix(2, 1, 0)); + const int capacity = 4096; + Require(setsockopt_nid_postfix(listener.Get(), 0xffff, 0x1002, &capacity, sizeof(capacity)) == 0, "cannot limit the pressure peer's receive buffer"); + Require(setsockopt_nid_postfix(client.Get(), 0xffff, 0x1001, &capacity, sizeof(capacity)) == 0, "cannot limit the pressure sender's send buffer"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener.Get(), address.data(), address.size()) == 0 && listen_nid_postfix(listener.Get(), 4) == 0, "cannot start the pressure listener"); + std::uint32_t addressBytes = address.size(); + Require(getsockname_nid_postfix(listener.Get(), address.data(), &addressBytes) == 0 && addressBytes == address.size(), "cannot read the pressure listener's address"); + Require(connect_nid_postfix(client.Get(), address.data(), address.size()) == 0, "pressure client did not connect"); + Descriptor accepted(accept_nid_postfix(listener.Get(), nullptr, nullptr)); + return std::make_unique(client.Take(), accepted.Take()); +} + +void FillSendQueue(Pair& pair) { + Require(fcntl_nid_postfix(pair.Reader(), 4, std::uint64_t{4}) == 0, "cannot make the pressure sender nonblocking"); + const auto deadline = Clock::now() + 2s; + std::array bytes{}; + std::size_t total = 0; + unsigned stable = 0; + bool tiny = false; + while (total < 8u * 1024u * 1024u && Clock::now() < deadline && stable < 5) { + const auto sent = send_nid_postfix(pair.Reader(), bytes.data(), tiny ? 1 : bytes.size(), DontWait | 0x20000); + if (sent > 0) { + total += static_cast(sent); + stable = 0; + } else { + Require(sent == -1 && *__error_nid_postfix() == WouldBlock, "filling the send queue returned an unexpected error"); + if (tiny) ++stable; + else tiny = true; + std::this_thread::sleep_for(20ms); + } + } + Require(total != 0 && stable == 5, "the TCP send queue did not reach stable one-byte backpressure"); + Require(fcntl_nid_postfix(pair.Reader(), 4, std::uint64_t{0}) == 0 && (pair.Flags() & 4) == 0, "cannot restore the pressure sender's blocking mode"); +} + +void SendAfterPeerDrains() { + auto pair = TcpPair(); + FillSendQueue(*pair); + const auto flags = pair->Flags(); + SendCall immediate(*pair, DontWait); + immediate.Prompt("DONTWAIT send blocked on a full queue"); + Require(immediate.result == -1 && immediate.error == WouldBlock, "DONTWAIT send did not report EAGAIN for a full queue"); + SendCall blocking(*pair, 0); + Require(Within([&] { return blocking.Entered(); }, 2s), "blocking pressure sender did not enter"); + Require(!blocking.Wait(100ms), "blocking send returned before the full peer drained"); + Require(fcntl_nid_postfix(pair->Peer(), 4, std::uint64_t{4}) == 0, "cannot make the draining peer nonblocking"); + std::array scratch{}; + std::size_t drained = 0; + const auto deadline = Clock::now() + 2s; + while (!blocking.Finished() && Clock::now() < deadline) { + const auto received = recv_nid_postfix(pair->Peer(), scratch.data(), scratch.size(), DontWait); + if (received > 0) drained += static_cast(received); + else { + Require(received == -1 && *__error_nid_postfix() == WouldBlock, "draining the full peer failed"); + std::this_thread::sleep_for(1ms); + } + } + blocking.Prompt("blocking send did not wake after its peer drained"); + Require(drained != 0 && blocking.result == 1, "blocking send did not transfer its byte after peer progress"); + Require(pair->Flags() == flags, "send pressure changed the sender's mode"); +} + +void HalfClosedSendTimeout() { + auto pair = TcpPair(); + FillSendQueue(*pair); + const auto flags = pair->Flags(); + const std::array timeout{0, 60000}; + Require(setsockopt_nid_postfix(pair->Reader(), 0xffff, 0x1005, timeout.data(), sizeof(timeout)) == 0, "cannot set the pressure send timeout"); + Require(shutdown_nid_postfix(pair->Peer(), 1) == 0, "cannot half-close the pressure peer's write direction"); + GuestPollDescriptor eofReady{pair->Reader(), 1, 0}; + Require(poll_nid_postfix(&eofReady, 1, 2000) == 1 && (eofReady.revents & 0x11) != 0, "the pressure sender did not observe its peer's FIN"); + Receive eof(*pair, DontWait, 1); + eof.Prompt("receiving the pressure peer's FIN blocked"); + Require(eof.result == 0, "half-closing the peer did not produce read EOF"); + SendCall timed(*pair, 0); + timed.Prompt("a full-queue send ignored its timeout after read EOF"); + Require(timed.result == -1 && timed.error == WouldBlock, "timed full-queue send after read EOF did not report EAGAIN"); + Require(timed.Elapsed() >= 40ms && timed.Elapsed() < 1500ms, "full-queue send did not honor its receive-independent timeout"); + Require(pair->Flags() == flags, "timed send changed the sender's mode"); +} + +void BlockingAccept() { + Descriptor listener(socket_nid_postfix(2, 1, 0)); + Descriptor client(socket_nid_postfix(2, 1, 0)); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener.Get(), address.data(), address.size()) == 0 && listen_nid_postfix(listener.Get(), 4) == 0, "cannot start the accept test listener"); + std::uint32_t addressBytes = address.size(); + Require(getsockname_nid_postfix(listener.Get(), address.data(), &addressBytes) == 0 && addressBytes == address.size(), "cannot read the accept test address"); + const std::array timeout{2, 0}; + Require(setsockopt_nid_postfix(listener.Get(), 0xffff, 0x1006, timeout.data(), sizeof(timeout)) == 0, "cannot bound the accept test's wait"); + const int flags = fcntl_nid_postfix(listener.Get(), 3, std::uint64_t{0}); + Require(flags >= 0 && (flags & 4) == 0, "accept listener did not start blocking"); + std::atomic entered{false}, finished{false}; + int accepted = -1; + std::thread thread([&] { + entered.store(true, std::memory_order_release); + accepted = accept_nid_postfix(listener.Get(), nullptr, nullptr); + finished.store(true, std::memory_order_release); + }); + struct Cleanup { + std::thread& thread; + Descriptor& client; + const std::array& address; + std::atomic& finished; + int& accepted; + ~Cleanup() { + if (thread.joinable()) { + if (!finished.load(std::memory_order_acquire)) connect_nid_postfix(client.Get(), address.data(), address.size()); + thread.join(); + } + if (accepted >= 0) close_nid_postfix(accepted); + } + } cleanup{thread, client, address, finished, accepted}; + Require(Within([&] { return entered.load(std::memory_order_acquire); }, 2s), "blocking accept did not enter"); + Require(!Within([&] { return finished.load(std::memory_order_acquire); }, 100ms), "blocking accept returned before a client connected"); + Require(connect_nid_postfix(client.Get(), address.data(), address.size()) == 0, "accept test client did not connect"); + Require(Within([&] { return finished.load(std::memory_order_acquire); }, 2s), "blocking accept did not finish after a client connected"); + thread.join(); + Require(accepted >= 0 && fcntl_nid_postfix(accepted, 3, std::uint64_t{0}) == flags, "accepted socket did not inherit logical blocking mode"); + Require(fcntl_nid_postfix(listener.Get(), 3, std::uint64_t{0}) == flags, "blocking accept changed its listener's mode"); + const int reader = accepted; + accepted = -1; + Pair pair(reader, client.Take()); + CheckRestoredBlocking(pair); +} + +void CompetingReaders(bool nonblocking) { + constexpr std::size_t count = 8; + for (unsigned round = 0; round < 64; ++round) { + Pair pair(nonblocking); + const auto flags = pair.Flags(); + Require(flags >= 0 && ((flags & 4) != 0) == nonblocking, "race socket has the wrong mode"); + pair.CheckReadable(false); + pair.Send(); + pair.CheckReadable(true); + std::atomic gate{false}; + std::array, count> readers; + for (auto& reader : readers) reader = std::make_unique(pair, DontWait, 1, &gate); + const auto all = [&](auto state) { + for (const auto& reader : readers) if (!(reader.get()->*state)()) return false; + return true; + }; + Require(Within([&] { return all(&Receive::Ready); }, 2s), "competing receivers did not reach their gate"); + gate.store(true, std::memory_order_release); + Require(Within([&] { return all(&Receive::Entered); }, 2s), "competing receivers did not enter"); + const bool prompt = Within([&] { return all(&Receive::Finished); }, 500ms); + if (!prompt) pair.ReleasePeer(); + Require(Within([&] { return all(&Receive::Finished); }, 2s), "competing receivers did not stop after peer shutdown"); + for (auto& reader : readers) reader->Join(); + Require(prompt, "MSG_DONTWAIT blocked after another receiver consumed the ready byte"); + unsigned received = 0, again = 0; + for (const auto& reader : readers) { + received += reader->result == 1 && reader->bytes[0] == FirstByte; + again += reader->result == -1 && reader->error == WouldBlock; + Require(reader->bytes[1] == 0xdd, "one-byte receive wrote beyond its destination"); + } + Require(received == 1 && again == count - 1, "competing DONTWAIT receivers did not consume exactly one byte"); + Require(pair.Flags() == flags, "competing DONTWAIT receives changed the socket mode"); + } +} + +} + +int main() { + Watchdog watchdog; + try { + AvailableWithWaitAll(); + OrdinaryWaitAll(); + OrdinaryDontWait(false); + OrdinaryDontWait(true); + PeekWithWaitAll(); + AlongsideBlockingRead(); + ModeChanges(); + BlockingAccept(); + SendAfterPeerDrains(); + HalfClosedSendTimeout(); + CompetingReaders(true); + CompetingReaders(false); + std::puts("socket DONTWAIT preserves per-call and blocking receive semantics"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "socket DONTWAIT: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 94aafaf6225..744ce4e9369 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -322,7 +322,7 @@ int main(int argc, char** argv) { server.Serve(Serve::Close, true, Leaf::Expired); server.Serve(Serve::Close, true, Leaf::Future); server.Serve(Serve::Close, true, Leaf::Future); - if (sockets.net) server.Serve(Serve::Exchange); + server.Serve(Serve::Exchange); for (int index = 0; index < ParallelConnections; ++index) server.Serve(Serve::Exchange); }); @@ -515,13 +515,15 @@ int main(int argc, char** argv) { Require(sceSslConnect(futureChecked) == ErrorNotBeforeCheck, "a not yet valid server certificate passed the enabled start check"); Require(sceSslDeleteConnection(futureChecked) == 0 && sockets.Close(futureCheckedSocket) == 0, "release the checked future connection"); - if (sockets.net) { + { const int timedSocket = sockets.Open(server.port); const GuestTimeval timeout{0, 200000}; Require(sockets.Set(timedSocket, GuestReceiveTimeout, &timeout, sizeof(timeout)) == 0, "set the receive timeout"); const int timed = sceSslCreateConnection(context, timedSocket, "localhost"); Require(sceSslConnect(timed) == 0, "handshake over the socket with a receive timeout"); + const auto started = std::chrono::steady_clock::now(); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "an expired receive timeout did not ask to poll"); + Require(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(150), "the blocking SSL receive returned before its timeout"); Require(sceSslWrite(timed, request, 4, 0) == 4, "write the request after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the timed stream"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 1e749c6e63c..734244408b7 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -219,8 +219,7 @@ - [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, 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; `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_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - a blocking Windows stream receive with both `MSG_PEEK` and `MSG_WAITALL` still fails with `EOPNOTSUPP`, where FreeBSD 11.4's [`soreceive_stream`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2019) waits for more data without consuming it. Other hosts pass the flags to the host and `SO_RCVTIMEO` bounds only the initial readiness wait. These flag combinations are not measured on a console. A datagram socket ignores `MSG_WAITALL`. - [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 - [recv and recvfrom after `shutdown(SHUT_RD)`](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) return 0 on Windows ([`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562)) where Winsock fails with `EPIPE`; Linux and macOS still return data buffered before it, which the console discards. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). @@ -245,7 +244,7 @@ - [IP_TTL, IP_TOS, IP_MULTICAST_TTL, IP_MULTICAST_LOOP](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per `AF_INET` socket with the defaults (64, 0, 1, 1), the low-byte storage of `IP_TTL` and `IP_TOS`, the one-byte forms and the `EINVAL` above 255 of the multicast options that a payload process on a PS5 (firmware 13.60) showed. `IP_TTL` and the multicast options are also set on the host socket, except a time to live of 0, which hosts refuse; `IP_TOS` is only kept, so the host's packets carry its own type of service, and the host's default time to live (128 on Windows) is not changed to 64. An accepted socket takes `IP_TTL` and `IP_TOS` from its listener, as FreeBSD's `syncache_socket` does, and an `int` `IP_MULTICAST_LOOP` keeps only its low byte, as FreeBSD 11.4's `inp_setmoptions` does; neither was measured. Winsock applies `IP_MULTICAST_LOOP` on the receiving socket, not the sender, so on Windows the receiver's setting decides loopback - [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured - [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. Not measured on a console -- [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener +- [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - outside Windows the timeout only bounds the initial readiness wait: a host send can then block without a limit, a receive whose data another thread takes can block again, and `sendfile` ignores the timeout. Windows bounds each transfer, but each `sendfile` chunk starts a new deadline. The host wait has millisecond steps. The value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them, and an accepted socket takes both timeouts from its listener - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed From d3d5a4d225821f1bbc280459948d81bbed5873ec Mon Sep 17 00:00:00 2001 From: liu <58376171+lgqdaxia@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:35:02 +0100 Subject: [PATCH 02/61] fix(libkernel): restart socket timeouts after progress --- core/libs/prx/libkernel/CMakeLists.txt | 7 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 10 +- .../tests/GuestSocketTransferProgress.cpp | 280 ++++++++++++++++++ docs/dev/debt/libkernel.md | 3 +- 4 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestSocketTransferProgress.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1a1b556507f..ade685c28ca 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1573,6 +1573,13 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_socket_dontwait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_socket_transfer_progress_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketTransferProgress.cpp) + target_include_directories(guest_socket_transfer_progress_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_transfer_progress_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_transfer_progress_tests) + add_test(NAME guest_socket_transfer_progress COMMAND guest_socket_transfer_progress_tests) + set_tests_properties(guest_socket_transfer_progress PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_socket_pair_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketPair.cpp) target_include_directories(guest_socket_pair_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 890a6244483..32329851f6b 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -158,15 +158,19 @@ int Transfer(Socket& socket, bool send, int flags, int length, TOperation operat } const bool complete = socket.type == 1 && !nonblocking && (send || ((flags & GuestWaitAll) != 0 && (flags & GuestPeek) == 0)); - const auto deadline = timeout == 0 ? std::chrono::steady_clock::time_point::max() : - KernelSocketSelect::DeadlineAfter(std::chrono::steady_clock::now(), timeout >> 32, - (static_cast(static_cast(timeout)) * 1000000) >> 32); + const auto nextDeadline = [timeout] { + return timeout == 0 ? std::chrono::steady_clock::time_point::max() : + KernelSocketSelect::DeadlineAfter(std::chrono::steady_clock::now(), timeout >> 32, + (static_cast(static_cast(timeout)) * 1000000) >> 32); + }; + auto deadline = nextDeadline(); int transferred = 0; for (;;) { const int result = operation(transferred); if (result >= 0) { transferred += result; if (!complete || result == 0 || transferred == length) return transferred; + deadline = nextDeadline(); continue; } if (WSAGetLastError() != WSAEWOULDBLOCK || nonblocking) return transferred == 0 ? -1 : transferred; diff --git a/core/libs/tests/GuestSocketTransferProgress.cpp b/core/libs/tests/GuestSocketTransferProgress.cpp new file mode 100644 index 00000000000..f6e38b27360 --- /dev/null +++ b/core/libs/tests/GuestSocketTransferProgress.cpp @@ -0,0 +1,280 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; +using Clock = std::chrono::steady_clock; +constexpr int DontWait = 0x80; +constexpr int WaitAll = 0x40; +constexpr int WouldBlock = 35; +constexpr std::size_t ChunkBytes = 16384; + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +template +bool Within(TPredicate ready, std::chrono::milliseconds duration) { + const auto deadline = Clock::now() + duration; + while (!ready()) { + if (Clock::now() >= deadline) return ready(); + std::this_thread::sleep_for(1ms); + } + return true; +} + +class Watchdog { +public: + Watchdog() : thread([this] { + std::unique_lock lock(mutex); + if (!changed.wait_for(lock, 12s, [this] { return finished; })) { + std::fputs("socket transfer progress watchdog expired\n", stderr); + std::fflush(stderr); + std::_Exit(99); + } + }) {} + ~Watchdog() { + { + std::lock_guard lock(mutex); + finished = true; + } + changed.notify_one(); + thread.join(); + } +private: + std::mutex mutex; + std::condition_variable changed; + bool finished = false; + std::thread thread; +}; + +class Descriptor { +public: + explicit Descriptor(int value) : value(value) { Require(value >= 0, "cannot create a TCP socket"); } + ~Descriptor() { if (value >= 0) close_nid_postfix(value); } + int Get() const { return value; } + int Take() { const int result = value; value = -1; return result; } +private: + int value; +}; + +class Pair { +public: + Pair(int sender, int receiver) : descriptors{sender, receiver} {} + ~Pair() { for (const int descriptor : descriptors) close_nid_postfix(descriptor); } + int Sender() const { return descriptors[0]; } + int Receiver() const { return descriptors[1]; } + void Interrupt() noexcept { + for (const int descriptor : descriptors) shutdown_nid_postfix(descriptor, 2); + } +private: + std::array descriptors; +}; + +std::unique_ptr TcpPair(bool limited = false) { + Descriptor listener(socket_nid_postfix(2, 1, 0)); + Descriptor client(socket_nid_postfix(2, 1, 0)); + if (limited) { + const int capacity = 4096; + Require(setsockopt_nid_postfix(listener.Get(), 0xffff, 0x1002, &capacity, sizeof(capacity)) == 0, "cannot limit the TCP receive buffer"); + Require(setsockopt_nid_postfix(client.Get(), 0xffff, 0x1001, &capacity, sizeof(capacity)) == 0, "cannot limit the TCP send buffer"); + } + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener.Get(), address.data(), address.size()) == 0 && listen_nid_postfix(listener.Get(), 4) == 0, "cannot start the TCP listener"); + std::uint32_t addressBytes = address.size(); + Require(getsockname_nid_postfix(listener.Get(), address.data(), &addressBytes) == 0 && addressBytes == address.size(), "cannot read the TCP listener's address"); + Require(connect_nid_postfix(client.Get(), address.data(), address.size()) == 0, "TCP client did not connect"); + Descriptor accepted(accept_nid_postfix(listener.Get(), nullptr, nullptr)); + return std::make_unique(client.Take(), accepted.Take()); +} + +void SetTimeout(int descriptor, bool send, std::chrono::milliseconds duration) { + const auto microseconds = std::chrono::duration_cast(duration).count(); + const std::array timeout{microseconds / 1000000, microseconds % 1000000}; + Require(setsockopt_nid_postfix(descriptor, 0xffff, send ? 0x1005 : 0x1006, timeout.data(), sizeof(timeout)) == 0, "cannot set the transfer timeout"); +} + +unsigned char Pattern(std::size_t offset) { + return static_cast(1 + offset % 127); +} + +std::vector Payload(std::size_t size) { + std::vector result(size); + for (std::size_t offset = 0; offset < size; ++offset) result[offset] = Pattern(offset); + return result; +} + +class TransferCall { +public: + TransferCall(Pair& pair, bool send, std::size_t length, int flags) : bytes(send ? Payload(length) : std::vector(length, 0xe7)), pair(pair), thread([this, send, flags] { + *__error_nid_postfix() = 0; + start = Clock::now(); + entered.store(true, std::memory_order_release); + result = send ? send_nid_postfix(this->pair.Sender(), bytes.data(), bytes.size(), flags | 0x20000) : + recv_nid_postfix(this->pair.Receiver(), bytes.data(), bytes.size(), flags); + error = *__error_nid_postfix(); + end = Clock::now(); + finished.store(true, std::memory_order_release); + }) {} + ~TransferCall() { + if (!thread.joinable()) return; + pair.Interrupt(); + thread.join(); + } + bool Entered() const { return entered.load(std::memory_order_acquire); } + bool Finished() const { return finished.load(std::memory_order_acquire); } + bool Wait(std::chrono::milliseconds duration) const { return Within([this] { return Finished(); }, duration); } + void Join() { thread.join(); } + Clock::duration Elapsed() const { return end - start; } + Clock::time_point End() const { return end; } + std::vector bytes; + std::int64_t result = std::numeric_limits::min(); + int error = -1; +private: + Pair& pair; + Clock::time_point start{}, end{}; + std::atomic entered{false}; + std::atomic finished{false}; + std::thread thread; +}; + +void CheckPayload(const TransferCall& call, std::size_t length) { + Require(call.result == static_cast(length), "receive did not return exactly the delivered payload"); + for (std::size_t offset = 0; offset < call.bytes.size(); ++offset) { + Require(call.bytes[offset] == (offset < length ? Pattern(offset) : 0xe7), "receive lost payload bytes or wrote beyond its partial result"); + } +} + +void ReceiveNoProgress() { + auto pair = TcpPair(); + SetTimeout(pair->Receiver(), false, 300ms); + TransferCall call(*pair, false, 5 * ChunkBytes, WaitAll); + Require(call.Wait(2s), "empty WAITALL ignored its receive timeout"); + call.Join(); + Require(call.result == -1 && call.error == WouldBlock && call.Elapsed() >= 250ms, "WAITALL without progress did not time out with EAGAIN"); + Require(std::all_of(call.bytes.begin(), call.bytes.end(), [](auto value) { return value == 0xe7; }), "empty WAITALL modified its buffer"); +} + +void ReceiveProgress(bool complete) { + auto pair = TcpPair(); + SetTimeout(pair->Receiver(), false, 300ms); + SetTimeout(pair->Sender(), true, 2s); + const auto payload = Payload(4 * ChunkBytes); + Require(send_nid_postfix(pair->Sender(), payload.data(), ChunkBytes, 0x20000) == ChunkBytes, "cannot queue the first progress chunk"); + TransferCall call(*pair, false, (complete ? 4 : 5) * ChunkBytes, WaitAll); + Require(Within([&] { return call.Entered(); }, 2s), "progress receiver did not enter its call"); + Clock::time_point lastChunk; + for (std::size_t chunk = 1; chunk < 4; ++chunk) { + Require(!call.Wait(120ms), "WAITALL expired despite regular receive progress"); + lastChunk = Clock::now(); + Require(send_nid_postfix(pair->Sender(), payload.data() + chunk * ChunkBytes, ChunkBytes, 0x20000) == ChunkBytes, "cannot deliver a progress chunk"); + } + Require(call.Wait(2s), "WAITALL did not finish after completion or an idle timeout"); + call.Join(); + CheckPayload(call, payload.size()); + Require(call.Elapsed() >= 330ms, "WAITALL did not span its original timeout"); + if (!complete) Require(call.End() - lastChunk >= 250ms, "partial WAITALL timed out before a full idle interval after its last progress"); + std::printf("WAITALL %s: %lld bytes, %lld ms total, %lld ms since final chunk\n", complete ? "complete" : "idle partial", + static_cast(call.result), static_cast(std::chrono::duration_cast(call.Elapsed()).count()), + static_cast(std::chrono::duration_cast(call.End() - lastChunk).count())); +} + +std::size_t FillSendQueue(Pair& pair) { + Require(fcntl_nid_postfix(pair.Sender(), 4, std::uint64_t{4}) == 0, "cannot make the pressure sender nonblocking"); + const auto deadline = Clock::now() + 2s; + std::array bytes{}; + std::size_t total = 0; + unsigned stable = 0; + bool tiny = false; + while (total < 8u * 1024u * 1024u && Clock::now() < deadline && stable < 5) { + const auto sent = send_nid_postfix(pair.Sender(), bytes.data(), tiny ? 1 : bytes.size(), DontWait | 0x20000); + if (sent > 0) { + total += static_cast(sent); + stable = 0; + } else { + Require(sent == -1 && *__error_nid_postfix() == WouldBlock, "filling the TCP send queue failed"); + if (tiny) ++stable; + else tiny = true; + std::this_thread::sleep_for(20ms); + } + } + Require(total != 0 && stable == 5, "TCP did not reach stable one-byte backpressure"); + Require(fcntl_nid_postfix(pair.Sender(), 4, std::uint64_t{0}) == 0, "cannot restore blocking mode after priming"); + return total; +} + +void LargeSend() { + auto pair = TcpPair(true); + const auto priming = FillSendQueue(*pair); + constexpr std::size_t length = 1024 * 1024; + TransferCall immediate(*pair, true, length, DontWait); + Require(immediate.Wait(500ms), "large DONTWAIT send blocked on a full queue"); + immediate.Join(); + Require(immediate.result == -1 && immediate.error == WouldBlock, "large DONTWAIT send did not report EAGAIN on a full queue"); + SetTimeout(pair->Sender(), true, 200ms); + TransferCall timed(*pair, true, length, 0); + Require(timed.Wait(2s), "large send ignored its timeout without peer progress"); + timed.Join(); + Require(timed.result == -1 && timed.error == WouldBlock && timed.Elapsed() >= 150ms, "large send without progress did not time out with EAGAIN"); + SetTimeout(pair->Sender(), true, 0ms); + TransferCall blocking(*pair, true, length, 0); + Require(Within([&] { return blocking.Entered(); }, 2s), "large blocking sender did not enter its call"); + Require(!blocking.Wait(100ms), "large blocking send returned before its peer drained"); + std::array scratch{}; + std::size_t received = 0; + const auto deadline = Clock::now() + 3s; + while (received < priming + length && Clock::now() < deadline) { + const auto count = recv_nid_postfix(pair->Receiver(), scratch.data(), std::min(scratch.size(), priming + length - received), DontWait); + if (count > 0) { + for (std::size_t index = 0; index < static_cast(count); ++index) { + const auto offset = received + index; + Require(scratch[index] == (offset < priming ? 0 : Pattern(offset - priming)), "TCP did not retain the priming and large payload byte sequence"); + } + received += static_cast(count); + } else { + Require(count == -1 && *__error_nid_postfix() == WouldBlock, "draining the large TCP send failed"); + std::this_thread::sleep_for(1ms); + } + } + Require(received == priming + length && blocking.Wait(2s), "large blocking send did not finish after its peer drained"); + blocking.Join(); + Require(blocking.result == length, "large blocking send did not account for every delivered payload byte"); + std::printf("large send: %zu priming bytes followed by %zu payload bytes\n", priming, length); +} + +} + +int main(int argc, char** argv) { + Watchdog watchdog; + try { + const char* mode = argc == 2 ? argv[1] : "all"; + Require(argc <= 2, "expected at most one transfer progress mode"); + if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "send") == 0) LargeSend(); + if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "receive-empty") == 0) ReceiveNoProgress(); + if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "receive-partial") == 0) ReceiveProgress(false); + if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "receive-complete") == 0) ReceiveProgress(true); + Require(std::strcmp(mode, "all") == 0 || std::strcmp(mode, "send") == 0 || std::strcmp(mode, "receive-empty") == 0 || + std::strcmp(mode, "receive-partial") == 0 || std::strcmp(mode, "receive-complete") == 0, "unknown transfer progress mode"); + std::puts("socket transfer progress preserves payloads and inactivity timeouts"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "socket transfer progress: %s\n", error.what()); + return 1; + } +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 734244408b7..bd39069bc48 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -244,7 +244,8 @@ - [IP_TTL, IP_TOS, IP_MULTICAST_TTL, IP_MULTICAST_LOOP](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per `AF_INET` socket with the defaults (64, 0, 1, 1), the low-byte storage of `IP_TTL` and `IP_TOS`, the one-byte forms and the `EINVAL` above 255 of the multicast options that a payload process on a PS5 (firmware 13.60) showed. `IP_TTL` and the multicast options are also set on the host socket, except a time to live of 0, which hosts refuse; `IP_TOS` is only kept, so the host's packets carry its own type of service, and the host's default time to live (128 on Windows) is not changed to 64. An accepted socket takes `IP_TTL` and `IP_TOS` from its listener, as FreeBSD's `syncache_socket` does, and an `int` `IP_MULTICAST_LOOP` keeps only its low byte, as FreeBSD 11.4's `inp_setmoptions` does; neither was measured. Winsock applies `IP_MULTICAST_LOOP` on the receiving socket, not the sender, so on Windows the receiver's setting decides loopback - [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured - [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. Not measured on a console -- [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - outside Windows the timeout only bounds the initial readiness wait: a host send can then block without a limit, a receive whose data another thread takes can block again, and `sendfile` ignores the timeout. Windows bounds each transfer, but each `sendfile` chunk starts a new deadline. The host wait has millisecond steps. The value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them, and an accepted socket takes both timeouts from its listener +- [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - outside Windows the timeout only bounds the initial readiness wait: a host send can then block without a limit, a receive whose data another thread takes can block again, and `sendfile` ignores the timeout. Each Windows `sendfile` chunk starts a new wait timer, and host waits have millisecond steps. The value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them, and an accepted socket takes both timeouts from its listener +- [send and sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - a blocking Windows stream transfer returns the bytes already sent after any later host error. FreeBSD 11.4's [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_syscalls.c#L883) only suppresses `ERESTART`, `EINTR` and `EWOULDBLOCK` after progress; a later hard error is returned instead. Partial progress followed by a hard host error is not covered by the socket progress regression. - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed From 981fffb831611106872cf3c6ba4f765dfc729ef8 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:46:44 -0300 Subject: [PATCH 03/61] fix(ssl): bound Unix retained socket transfers --- core/libs/prx/libSceSsl/Export.cpp | 14 +++--- core/libs/prx/libkernel/Socket/src/Udp.cpp | 35 +++++++++++++-- core/libs/tests/GuestSocketTimeouts.cpp | 52 ++++++++++++++++++++++ docs/dev/debt/libSceSsl.md | 2 +- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index a3ae4afcecf..54c732ea172 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -379,13 +379,14 @@ struct Session { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); for (;;) { -#ifdef _WIN32 auto* owner = static_cast(session.socketOwner.get()); const auto sent = owner && owner->transfer ? owner->transfer(owner, true, const_cast(buffer), chunk) - : ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); + : +#ifdef _WIN32 + ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else - const auto sent = ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); + ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); #endif if (sent >= 0) return static_cast(sent); const int error = HostError(); @@ -401,13 +402,14 @@ struct Session { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); for (;;) { -#ifdef _WIN32 auto* owner = static_cast(session.socketOwner.get()); const auto received = owner && owner->transfer ? owner->transfer(owner, false, buffer, chunk) - : ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); + : +#ifdef _WIN32 + ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else - const auto received = ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); + ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); #endif if (received >= 0) return static_cast(received); const int error = HostError(); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 32329851f6b..4b0ada5510b 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -201,15 +201,44 @@ int Transfer(Socket& socket, bool send, int flags, int length, TOperation operat #endif } -#ifdef _WIN32 int TransferForNativeOwner(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length) { auto& socket = *static_cast(owner); +#ifdef _WIN32 return Transfer(socket, send, 0, length, [&](int offset) { auto* bytes = offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset; return send ? ::send(socket.value, bytes, length - offset, 0) : ::recv(socket.value, bytes, length - offset, 0); }); -} +#else + bool nonblocking; + std::int64_t timeout; + { + std::lock_guard lock(socket.modeMutex); + nonblocking = socket.options.nonblocking; + timeout = send ? socket.options.sendTimeout : socket.options.receiveTimeout; + } + const auto deadline = timeout == 0 ? std::chrono::steady_clock::time_point::max() : + KernelSocketSelect::DeadlineAfter(std::chrono::steady_clock::now(), timeout >> 32, + (static_cast(static_cast(timeout)) * 1000000) >> 32); + for (;;) { + const auto result = send + ? ::send(socket.value, buffer, static_cast(length), NativeSendFlags(GuestDontWait | GuestNoSignal)) + : ::recv(socket.value, buffer, static_cast(length), NativeReceiveFlags(GuestDontWait, socket.type)); + if (result >= 0 || nonblocking || (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) return static_cast(result); + int milliseconds = -1; + if (timeout != 0) { + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::steady_clock::duration::zero()) { + errno = EAGAIN; + return -1; + } + milliseconds = static_cast(std::min( + std::chrono::ceil(remaining).count(), INT_MAX)); + } + pollfd ready{socket.value, static_cast(send ? POLLOUT : POLLIN), 0}; + if (::poll(&ready, 1, milliseconds) < 0 && errno != EINTR) return -1; + } #endif +} struct Waker { NativeSocket value = Invalid; @@ -332,8 +361,8 @@ int Install(std::shared_ptr socket) { unsigned long nonblocking = 1; if (ioctlsocket(socket->value, FIONBIO, &nonblocking)) return Fail(NativeError()); socket->options.transportNonblocking = true; - socket->transfer = TransferForNativeOwner; #endif + socket->transfer = TransferForNativeOwner; const int number = ReserveNumber(socket->value); if (number < 0) return -1; try { diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp index 104d70406e7..dfb69d5ddea 100644 --- a/core/libs/tests/GuestSocketTimeouts.cpp +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -7,6 +7,13 @@ #include #include #include +#ifndef _WIN32 +#include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include +#include +#include +#include +#endif struct Timeval { std::int64_t seconds; @@ -51,6 +58,48 @@ static bool Reads(int descriptor, int option, std::int64_t seconds, std::int64_t time.seconds == seconds && time.microseconds == microseconds; } +#ifndef _WIN32 +static volatile std::sig_atomic_t nativeInterrupted = 0; + +static void RecordNativeInterrupt(int) { + nativeInterrupted = 1; +} + +static void RetainedTimeoutWithInterrupts() { + int descriptors[2]{}; + Require(socketpair_nid_postfix(1, 1, 0, descriptors) == 0); + Require(Set(descriptors[0], ReceiveTimeout, 0, 60000) == 0); + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelLookupSocketNative_nid_no_patch(descriptors[0], &owner, &native)); + auto* transport = static_cast(owner.get()); + Require(transport != nullptr && transport->transfer != nullptr); + Require(close_nid_postfix(descriptors[0]) == 0); + struct sigaction action{}; + action.sa_handler = RecordNativeInterrupt; + Require(sigemptyset(&action.sa_mask) == 0 && sigaction(SIGUSR1, &action, nullptr) == 0); + sigset_t signal; + Require(sigemptyset(&signal) == 0 && sigaddset(&signal, SIGUSR1) == 0); + Require(pthread_sigmask(SIG_UNBLOCK, &signal, nullptr) == 0); + const auto thread = pthread_self(); + std::jthread interrupts([thread] { + for (int index = 0; index < 8; ++index) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + pthread_kill(thread, SIGUSR1); + } + }); + char byte = 'x'; + const auto started = Clock::now(); + const int result = transport->transfer(transport, false, &byte, 1); + const int error = errno; + const auto elapsed = Clock::now() - started; + interrupts.join(); + Require(nativeInterrupted != 0 && result == -1 && error == EAGAIN && byte == 'x'); + Require(elapsed >= std::chrono::milliseconds(59)); + Require(close_nid_postfix(descriptors[1]) == 0); +} +#endif + int main() { const int datagram = socket_nid_postfix(2, 2, 0); std::array own{16, 2, 0, 0, 127, 0, 0, 1}; @@ -129,5 +178,8 @@ int main() { Require(close_nid_postfix(server) == 0 && close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0 && close_nid_postfix(datagram) == 0); +#ifndef _WIN32 + RetainedTimeoutWithInterrupts(); +#endif return 0; } diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index cbd359521e0..f20d06a7b76 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -6,7 +6,7 @@ - [sceSslDisableOption, sceSslDisableOptionInternal, sceSslDisableOptionInternalInsecure, sceSslEnableOption, sceSslEnableOptionInternal, sceSslGetNanoSSLModuleId, sceSslGetPeerCert, sceSslGetSslError, sceSslLoadRootCACert and sceSslSetSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. - [sceSslReuseConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - its name and NID appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); its parameter shape follows [SharpProspero's pinned binding](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L2231-L2235). No console observation or reliable implementation source for session reuse was found. -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. A second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. A second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown - [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS parses DER/PEM chains. CA imports still commit together instead of preserving successful entries before a later failure. Client certificate replacement clears the previous identity; raw private keys load independently and are parsed when configuring TLS. Partial client chains on decode failure, native key parse errors and incomplete identities during TLS setup remain unsupported. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. Native CA queries include built-in roots; the host still queries loaded authorities and returns `NOT_FOUND` for an empty context. No native root bundle is copied. Null-output and invalid-context precedence, output preservation on invalid contexts and the SDK 7.50 unload error boundary are recovered. Legacy clearing of foreign raw CA lists remains unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate From 9b50a9c09bf8c71b26743de35b31548fab03ebe3 Mon Sep 17 00:00:00 2001 From: liu Date: Sun, 25 Oct 2026 21:37:29 +0100 Subject: [PATCH 04/61] test(libkernel): reject retained socket deadline extension Keep native signals arriving beyond the receive timeout and bound the retained-owner wait. The same fixture rejects a source variant that incorrectly restarts its deadline after EINTR. AI-assisted: Codex --- core/libs/tests/GuestSocketTimeouts.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp index dfb69d5ddea..949f24cf4db 100644 --- a/core/libs/tests/GuestSocketTimeouts.cpp +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -82,8 +82,8 @@ static void RetainedTimeoutWithInterrupts() { Require(sigemptyset(&signal) == 0 && sigaddset(&signal, SIGUSR1) == 0); Require(pthread_sigmask(SIG_UNBLOCK, &signal, nullptr) == 0); const auto thread = pthread_self(); - std::jthread interrupts([thread] { - for (int index = 0; index < 8; ++index) { + std::jthread interrupts([thread](std::stop_token stop) { + for (int index = 0; index < 300 && !stop.stop_requested(); ++index) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); pthread_kill(thread, SIGUSR1); } @@ -93,9 +93,11 @@ static void RetainedTimeoutWithInterrupts() { const int result = transport->transfer(transport, false, &byte, 1); const int error = errno; const auto elapsed = Clock::now() - started; + interrupts.request_stop(); interrupts.join(); Require(nativeInterrupted != 0 && result == -1 && error == EAGAIN && byte == 'x'); Require(elapsed >= std::chrono::milliseconds(59)); + Require(elapsed < std::chrono::seconds(1)); Require(close_nid_postfix(descriptors[1]) == 0); } #endif From 904fcc6d85084a6fc155ee8b4e0c8ff34c6092c3 Mon Sep 17 00:00:00 2001 From: liu Date: Sun, 25 Oct 2026 22:39:49 +0100 Subject: [PATCH 05/61] fix(libkernel): honor live Unix socket modes in retained transfers Read O_NONBLOCK from the native socket for every retained transfer so descriptor aliases and passed sockets do not turn nonblocking I/O into a blocking wait. Keep guest timeout accounting and native query errors intact. AI-assisted: Codex --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 4b0ada5510b..fcd148a5510 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -213,7 +213,9 @@ int TransferForNativeOwner(KernelSocketPoll::NativeOwner* owner, bool send, void std::int64_t timeout; { std::lock_guard lock(socket.modeMutex); - nonblocking = socket.options.nonblocking; + const int flags = ::fcntl(socket.value, F_GETFL); + if (flags < 0) return -1; + nonblocking = (flags & O_NONBLOCK) != 0; timeout = send ? socket.options.sendTimeout : socket.options.receiveTimeout; } const auto deadline = timeout == 0 ? std::chrono::steady_clock::time_point::max() : From 6adaa5acdf716f37ecef7b410505f91d8a75b8b0 Mon Sep 17 00:00:00 2001 From: liu Date: Sun, 25 Oct 2026 22:42:29 +0100 Subject: [PATCH 06/61] test(libkernel): cover retained Unix socket mode changes Exercise real native descriptor aliases switching both blocking directions and a saturated nonblocking send after the guest descriptor closes, independently of SCM_RIGHTS. AI-assisted: Codex --- core/libs/prx/libkernel/CMakeLists.txt | 9 + core/libs/tests/GuestSocketNativeMode.cpp | 250 ++++++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 core/libs/tests/GuestSocketNativeMode.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ade685c28ca..ae060e26873 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1483,6 +1483,15 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_timeouts PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(NOT WIN32) + add_test_executable(guest_socket_native_mode_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketNativeMode.cpp) + target_include_directories(guest_socket_native_mode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_native_mode_tests PRIVATE libkernel libc) + foreach(mode receive-nonblocking receive-blocking send-nonblocking) + add_test(NAME guest_socket_native_mode_${mode} COMMAND guest_socket_native_mode_tests ${mode}) + set_tests_properties(guest_socket_native_mode_${mode} PROPERTIES TIMEOUT 10) + endforeach() + endif() add_test_executable(guest_udp_tests ${LIBS_INCLUDE_DIR}/tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestSocketNativeMode.cpp b/core/libs/tests/GuestSocketNativeMode.cpp new file mode 100644 index 00000000000..24ab56e5c86 --- /dev/null +++ b/core/libs/tests/GuestSocketNativeMode.cpp @@ -0,0 +1,250 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "native socket mode check failed at line %d, errno %d, guest errno %d\n", + line, errno, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int GetFlags = 3; +constexpr int SetFlags = 4; +constexpr int ReadWrite = 2; +constexpr int NonBlocking = 4; +using Clock = std::chrono::steady_clock; + +static bool WouldBlock(int error) { + return error == EAGAIN || error == EWOULDBLOCK; +} + +struct NativeDescriptor { + int value = -1; + + ~NativeDescriptor() { + if (value >= 0) ::close(value); + } +}; + +struct RetainedSocket { + std::shared_ptr owner; + int native = -1; + + KernelSocketPoll::NativeOwner* Transport() const { + auto* transport = static_cast(owner.get()); + Require(transport != nullptr && transport->transfer != nullptr); + return transport; + } +}; + +static RetainedSocket Lookup(int descriptor) { + RetainedSocket result; + std::uintptr_t native = 0; + Require(KernelLookupSocketNative_nid_no_patch(descriptor, &result.owner, &native)); + Require(result.owner != nullptr); + result.native = static_cast(native); + return result; +} + +struct SocketPair { + std::array descriptors{-1, -1}; + RetainedSocket socket; + RetainedSocket peer; + NativeDescriptor alias; + + explicit SocketPair(bool nonblocking) { + Require(socketpair_nid_postfix(1, 1, 0, descriptors.data()) == 0); + if (nonblocking) Require(fcntl_nid_postfix(descriptors[0], SetFlags, ReadWrite | NonBlocking) == 0); + Require(fcntl_nid_postfix(descriptors[0], GetFlags) == (ReadWrite | (nonblocking ? NonBlocking : 0))); + socket = Lookup(descriptors[0]); + peer = Lookup(descriptors[1]); + alias.value = ::dup(socket.native); + Require(alias.value >= 0 && alias.value != socket.native); + } + + ~SocketPair() { + for (const int descriptor : descriptors) { + if (descriptor >= 0) close_nid_postfix(descriptor); + } + } + + void SetNativeNonblocking(bool nonblocking, const char* mode) { + const int flags = ::fcntl(alias.value, F_GETFL); + Require(flags >= 0); + Require(::fcntl(alias.value, F_SETFL, nonblocking ? flags | O_NONBLOCK : flags & ~O_NONBLOCK) == 0); + const int nativeFlags = ::fcntl(socket.native, F_GETFL); + const int aliasFlags = ::fcntl(alias.value, F_GETFL); + const int guestFlags = fcntl_nid_postfix(descriptors[0], GetFlags); + Require(nativeFlags >= 0 && aliasFlags >= 0 && guestFlags >= 0); + Require(((nativeFlags & O_NONBLOCK) != 0) == nonblocking); + Require(((aliasFlags & O_NONBLOCK) != 0) == nonblocking); + std::fprintf(stderr, "%s: guest flags=0x%x, native flags=0x%x, alias flags=0x%x\n", + mode, guestFlags, nativeFlags, aliasFlags); + } + + void CloseGuestEndpoint() { + const int descriptor = descriptors[0]; + descriptors[0] = -1; + Require(descriptor >= 0 && close_nid_postfix(descriptor) == 0); + } +}; + +struct TransferResult { + int value = -2; + int error = 0; + char byte = '?'; + std::int64_t milliseconds = 0; + bool supplied = false; + int suppliedBytes = -2; + int suppliedError = 0; + bool released = false; + int releaseResult = -2; + int releaseError = 0; +}; + +static TransferResult TransferBounded(const SocketPair& pair, bool send, bool supplyData = false) { + auto* transport = pair.socket.Transport(); + std::mutex mutex; + std::condition_variable changed; + bool finished = false; + TransferResult result; + const auto started = Clock::now(); + std::jthread release([&] { + std::unique_lock lock(mutex); + if (supplyData) { + if (changed.wait_for(lock, std::chrono::milliseconds(100), [&] { return finished; })) return; + lock.unlock(); + const char byte = 'z'; + result.supplied = true; + result.suppliedBytes = static_cast(::send(pair.peer.native, &byte, 1, MSG_DONTWAIT | MSG_NOSIGNAL)); + result.suppliedError = errno; + lock.lock(); + } + if (changed.wait_for(lock, std::chrono::milliseconds(1500), [&] { return finished; })) return; + lock.unlock(); + result.released = true; + result.releaseResult = ::shutdown(pair.peer.native, SHUT_RDWR); + result.releaseError = errno; + }); + errno = 0; + result.value = transport->transfer(transport, send, &result.byte, 1); + result.error = errno; + result.milliseconds = std::chrono::duration_cast(Clock::now() - started).count(); + { + std::lock_guard lock(mutex); + finished = true; + } + changed.notify_one(); + release.join(); + return result; +} + +static void ReportTransfer(const char* mode, const TransferResult& result) { + std::fprintf(stderr, "%s: transfer=%d errno=%d byte=%d elapsed=%lldms, supplied=%d bytes=%d errno=%d, released=%d shutdown=%d errno=%d\n", + mode, result.value, result.error, static_cast(result.byte), static_cast(result.milliseconds), + result.supplied, result.suppliedBytes, result.suppliedError, result.released, result.releaseResult, result.releaseError); +} + +static void CheckReceiveNonblocking() { + constexpr auto mode = "receive-nonblocking"; + SocketPair pair{false}; + pair.SetNativeNonblocking(true, mode); + char byte = '?'; + errno = 0; + const auto started = Clock::now(); + const auto nativeResult = ::recv(pair.socket.native, &byte, 1, 0); + const int nativeError = errno; + const auto milliseconds = std::chrono::duration_cast(Clock::now() - started).count(); + std::fprintf(stderr, "%s: native empty read=%lld errno=%d byte=%d elapsed=%lldms\n", + mode, static_cast(nativeResult), nativeError, static_cast(byte), static_cast(milliseconds)); + Require(nativeResult == -1 && WouldBlock(nativeError) && byte == '?'); + pair.CloseGuestEndpoint(); + const auto result = TransferBounded(pair, false); + ReportTransfer(mode, result); + Require(!result.released && result.value == -1 && WouldBlock(result.error) && result.byte == '?'); + Require(result.milliseconds < 1000); +} + +static void CheckReceiveBlocking() { + constexpr auto mode = "receive-blocking"; + SocketPair pair{true}; + pair.SetNativeNonblocking(false, mode); + pair.CloseGuestEndpoint(); + const auto result = TransferBounded(pair, false, true); + ReportTransfer(mode, result); + Require(result.supplied && result.suppliedBytes == 1 && !result.released && result.value == 1 && result.byte == 'z'); + Require(result.milliseconds >= 50 && result.milliseconds < 1500); +} + +static void CheckSendNonblocking() { + constexpr auto mode = "send-nonblocking"; + SocketPair pair{false}; + pair.SetNativeNonblocking(true, mode); + const int sendBuffer = 4096; + Require(::setsockopt(pair.socket.native, SOL_SOCKET, SO_SNDBUF, &sendBuffer, sizeof(sendBuffer)) == 0); + std::array bytes{}; + std::size_t total = 0; + bool full = false; + for (int attempt = 0; attempt < 4096; ++attempt) { + const auto sent = ::send(pair.socket.native, bytes.data(), bytes.size(), MSG_DONTWAIT | MSG_NOSIGNAL); + if (sent > 0) { + total += static_cast(sent); + continue; + } + Require(sent == -1); + if (errno == EINTR) continue; + Require(WouldBlock(errno)); + full = true; + break; + } + Require(full && total != 0); + full = false; + const char byte = '?'; + for (int attempt = 0; attempt < 8192; ++attempt) { + const auto sent = ::send(pair.socket.native, &byte, 1, MSG_DONTWAIT | MSG_NOSIGNAL); + if (sent == 1) { + ++total; + continue; + } + Require(sent == -1); + if (errno == EINTR) continue; + Require(WouldBlock(errno)); + std::fprintf(stderr, "%s: native one-byte write=%lld errno=%d after filling %llu bytes\n", + mode, static_cast(sent), errno, static_cast(total)); + full = true; + break; + } + Require(full); + pair.CloseGuestEndpoint(); + const auto result = TransferBounded(pair, true); + ReportTransfer(mode, result); + Require(!result.released && result.value == -1 && WouldBlock(result.error) && result.byte == '?'); + Require(result.milliseconds < 1000); +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + const std::string_view mode = argv[1]; + if (mode == "receive-nonblocking") CheckReceiveNonblocking(); + else if (mode == "receive-blocking") CheckReceiveBlocking(); + else if (mode == "send-nonblocking") CheckSendNonblocking(); + else return 2; + return 0; +} From 0fbaef4d336df0927cb810d5fa6704a905e1fcf6 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 02:20:12 -0300 Subject: [PATCH 07/61] feat(libkernel): cancel blocked POSIX reads safely --- core/libs/prx/libkernel/CMakeLists.txt | 43 ++++ core/libs/prx/libkernel/File/include/File.hpp | 21 ++ core/libs/prx/libkernel/File/src/Open.cpp | 10 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 109 ++++++++--- .../prx/libkernel/Pthread/include/Cancel.hpp | 62 ++++++ .../prx/libkernel/Pthread/include/Pthread.hpp | 3 + .../prx/libkernel/Pthread/src/NativeIo.cpp | 88 +++++++++ .../libs/prx/libkernel/Pthread/src/Thread.cpp | 29 ++- core/libs/prx/libkernel/Socket/src/Udp.cpp | 96 +++++++-- core/libs/tests/GuestIoReadCancel.cpp | 184 ++++++++++++++++++ docs/dev/debt/libkernel.md | 6 +- 11 files changed, 601 insertions(+), 50 deletions(-) create mode 100644 core/libs/prx/libkernel/Pthread/src/NativeIo.cpp create mode 100644 core/libs/tests/GuestIoReadCancel.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 260211d7f20..026dd2f10a1 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -32,6 +32,7 @@ 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/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp @@ -99,6 +100,48 @@ endif() configure_windows_unwind(libkernel) if(BUILD_TESTING) + 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) + 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(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) diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index 79204181810..91b0ef22b35 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -32,6 +32,27 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ bool DescriptorIsOpen_nid_no_patch(int descriptor); namespace File { +std::recursive_mutex& DescriptorMutex(); + +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; +}; + +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 diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0bc36dad19d..0365061cdd4 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -19,6 +19,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" @@ -70,8 +71,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)); + std::optional> positioned; + if (GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) != FILE_TYPE_PIPE) + positioned.emplace(File::LockPositionedTransfer(fd)); + const int result = ThreadCancel::Native([&] { return ::_read(fd, buf, static_cast(n)); }); if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; return result; } @@ -151,7 +154,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); @@ -319,6 +322,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { + std::lock_guard descriptorLock(File::DescriptorMutex()); if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EPERM; int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) return socketError == 0 ? 0 : SceKernelError(socketError); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index c1a1d1cfcd9..dbf06185d66 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 @@ -289,7 +290,7 @@ 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"); @@ -338,7 +339,7 @@ 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) return ThreadCancel::Native([&] { return ::_read(descriptor, buf, static_cast(nbytes)); }); 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; @@ -411,7 +412,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) { @@ -785,7 +786,30 @@ int APS5_VABI nmount_nid_postfix(const KernelIovec* iov, unsigned int niov, int return -1; } +} + +std::recursive_mutex& File::DescriptorMutex() { + static std::recursive_mutex mutex; + return mutex; +} + +File::IoDescriptor::IoDescriptor(int descriptor) { + std::lock_guard lock(DescriptorMutex()); + value = dup_nid_postfix(descriptor); +} + +File::IoDescriptor::~IoDescriptor() { + const int saved = errno; + const int guestError = *__error_nid_postfix(); + if (value >= 0) close_nid_postfix(value); + *__error_nid_postfix() = guestError; + errno = saved; +} + +extern "C" { + int APS5_VABI close_nid_postfix(int d) { + std::lock_guard descriptorLock(File::DescriptorMutex()); if (File::IsHostStream(d)) return PosixFailure(GUEST_EPERM); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { @@ -840,6 +864,7 @@ int File::HostStandardError() { extern "C" { int APS5_VABI dup_nid_postfix(int d) { + std::lock_guard descriptorLock(File::DescriptorMutex()); if (GuestSockets::IsOpen(d)) return GuestSockets::Duplicate(d); if (d < 0) return PosixFailure(GUEST_EBADF); if (IsKqueueDescriptor(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); @@ -858,6 +883,7 @@ int APS5_VABI dup_nid_postfix(int d) { } int APS5_VABI dup2_nid_postfix(int from, int to) { + std::lock_guard descriptorLock(File::DescriptorMutex()); if (from < 0 || to < 0) return PosixFailure(GUEST_EBADF); if (GuestSockets::IsOpen(from)) { if (from != to && to <= 2) @@ -1062,17 +1088,21 @@ 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 { + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + 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; + }); + }); } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { @@ -1088,19 +1118,27 @@ int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_ } 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) { @@ -1464,7 +1502,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); + std::optional> positioned; + if (write || offset != nullptr || NativeIsDisk(d)) positioned.emplace(File::LockPositionedTransfer(d)); if (total == 0) { char none = 0; const auto result = offset != nullptr ? NativePositionedTransfer(d, &none, 0, *offset, write) : NativeTransferBuffer(d, &none, 0, write); @@ -1555,7 +1594,7 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) } if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, nullptr)) return *archived; - 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; } @@ -1587,7 +1626,7 @@ int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* guestIov, int iovcnt if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, &offset)) return *archived; - 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; } @@ -1612,8 +1651,12 @@ 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 { + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + const auto result = sceKernelPreadv(d, iov, 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) { @@ -1789,8 +1832,12 @@ 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 { + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + const auto result = sceKernelReadv(d, iov, 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..0ddef8db0da 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -4,7 +4,9 @@ #include "SceTypes.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include +#include #include +#include inline constexpr int GUEST_CANCEL_ENABLE = 0; inline constexpr int GUEST_CANCEL_DISABLE = 1; @@ -35,6 +37,66 @@ void CleanupRestore(void* mark); void BeginWait(TimedWait::Condition* condition); void EndWait(); +bool IoEnabled(); + +class IoPoint { +public: + IoPoint(); + ~IoPoint(); + IoPoint(const IoPoint&) = delete; + IoPoint& operator=(const IoPoint&) = delete; +}; + +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: + Interrupt interrupt; + void* argument; + IoWait* previous = nullptr; + Pthread thread = nullptr; +}; + +class NativeIo { +public: + NativeIo(); + ~NativeIo(); + NativeIo(const NativeIo&) = delete; + NativeIo& operator=(const NativeIo&) = delete; + +private: + struct State; + State* state = nullptr; +}; + +template +auto Native(TCall&& call) { + NativeIo operation; + if (IoEnabled() && Requested()) { + errno = EINTR; + return static_cast(-1); + } + return std::forward(call)(); +} + +template +auto Io(TCall&& call) { + Check(); + decltype(call()) result; + { + IoPoint point; + result = std::forward(call)(); + } + if (result < 0) Check(); + 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 b535fdb9967..072439c44a8 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -124,6 +124,8 @@ struct PthreadAttrPrivate { int _scope = GUEST_SCOPE_SYSTEM; }; +namespace ThreadCancel { class IoWait; } + inline std::atomic nextThreadId{100000}; struct PthreadPrivate { @@ -169,6 +171,7 @@ struct PthreadPrivate { TimedWait::Condition _join_cv; std::atomic cancelPending{false}; std::mutex cancelLock; + ThreadCancel::IoWait* cancelIo = nullptr; TimedWait::Condition* cancelWait = nullptr; explicit PthreadPrivate(const PthreadAttrPrivate* attr = nullptr); 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..83b9cad812f --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp @@ -0,0 +1,88 @@ +#include "prx/libkernel/Pthread/include/Cancel.hpp" + +#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 + wait.emplace(Interrupt, this); + } + + ~State() { + 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; + } +}; + +NativeIo::NativeIo() { + if (IoEnabled()) state = new State; +} + +NativeIo::~NativeIo() { delete state; } + +} diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index afe84bb4b06..bdefb7f8cb0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -769,6 +769,30 @@ void ThreadCancel::EndWait() { BeginWait(nullptr); } +static thread_local unsigned ioPoints = 0; + +ThreadCancel::IoPoint::IoPoint() { ++ioPoints; } +ThreadCancel::IoPoint::~IoPoint() { --ioPoints; } + +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) { + if (!IoEnabled()) return; + thread = CurrentGuestState().current; + std::lock_guard lock(thread->cancelLock); + previous = thread->cancelIo; + thread->cancelIo = this; +} + +ThreadCancel::IoWait::~IoWait() { + if (!thread) return; + std::lock_guard lock(thread->cancelLock); + thread->cancelIo = previous; +} + extern "C" void* APS5_VABI Aps5CallGuestEntry_nid_no_patch(PthreadEntry entry, void* arg); asm(".text\n" @@ -1298,8 +1322,9 @@ int APS5_VABI scePthreadCancel(Pthread thread) { for (;;) { { std::lock_guard lock(thread->cancelLock); - if (!thread->cancelWait) return SCE_OK; - thread->cancelWait->NotifyAll(); + 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 1131f91a2ae..fa2ddb87026 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 @@ -770,7 +771,72 @@ 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::IoEnabled(); + 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) { + OVERLAPPED operation{}; + operation.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!operation.hEvent) { + WSASetLastError(WSAENOBUFS); + return -1; + } + 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(nonblocking ? flags & ~GuestWaitAll : flags, socket.type)); + int result = -1; + int error = WSAEINTR; + { + ThreadCancel::IoWait wait(Pending::Interrupt, &pending); + if (!ThreadCancel::Requested()) { + WSABUF destination{static_cast(length), static_cast(buffer)}; + 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 == 0) result = static_cast(done); + } + } + CloseHandle(operation.hEvent); + WSASetLastError(error); + return result; + } + 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::IoEnabled()) return 0; +#endif if ((flags & GuestDontWait) != 0) { #ifdef _WIN32 NativePollDescriptor entry{}; @@ -791,6 +857,7 @@ int WaitForTransfer(Socket& socket, bool send, int flags) { 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::IoEnabled() && ThreadCancel::Requested()) 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)); @@ -802,7 +869,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 } @@ -1383,7 +1450,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); @@ -1393,8 +1460,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 @@ -1532,7 +1598,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st } return result < 0 ? Fail(SendError(socket->type)) : result; } -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) { const auto socket = Lookup(descriptor); if (!socket) return -1; @@ -1549,8 +1615,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u sockaddr_storage native{}; socklen_t size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); - auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); + auto result = ReceiveNative(*socket, buffer, static_cast(length), flags, reinterpret_cast(&native), &size); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); #endif @@ -1688,7 +1753,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); @@ -1715,8 +1780,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (!shut && (total != 0 || socket->type == 1 || ReadableNow(*socket))) { size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); - 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()); @@ -2031,9 +2095,19 @@ 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 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 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/tests/GuestIoReadCancel.cpp b/core/libs/tests/GuestIoReadCancel.cpp new file mode 100644 index 00000000000..6f576ccb95c --- /dev/null +++ b/core/libs/tests/GuestIoReadCancel.cpp @@ -0,0 +1,184 @@ +#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; + 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.partial || state.waitAll ? state.buffer.size() : 1; + KernelIovec vector{state.buffer.data(), length}; + GuestMsghdr message{}; + message.iov = &vector; + message.iovLength = 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 == "recv") state.transferred = recv_nid_postfix(state.input, state.buffer.data(), length, state.waitAll ? 0x40 : 0); + else if (state.operation == "recvfrom") state.transferred = recvfrom_nid_postfix(state.input, state.buffer.data(), length, state.waitAll ? 0x40 : 0, nullptr, nullptr); + else if (state.operation == "recvmsg") state.transferred = recvmsg_nid_postfix(state.input, &message, state.waitAll ? 0x40 : 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.returned.store(true); + if (state.disabled || state.sce) { + Require(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"); +} + +int main(int argc, char** argv) { + Require(argc == 4, "expected transport, API, and scenario"); + State state; + state.operation = argv[2]; + state.sce = state.operation == "sce_read" || state.operation == "sce_readv"; + const std::string scenario = argv[3]; + state.disabled = scenario == "disabled"; + state.pending = scenario == "pending"; + state.partial = scenario == "partial"; + state.waitAll = scenario == "waitall"; + const bool completion = scenario == "complete" || scenario == "reuse_complete" || state.partial; + const bool reuse = scenario == "reuse" || scenario == "reuse_complete"; + 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); + 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"); + 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) { + 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}; + if (reuse) { + Require(pipe_nid_postfix(replacement) == 0, "create replacement pipe"); + Require(dup2_nid_postfix(replacement[0], state.input) == state.input, "replace blocked descriptor"); + Require(close_nid_postfix(replacement[0]) == 0, "close replacement source"); + Require(write_nid_postfix(replacement[1], "n", 1) == 1, "seed replacement pipe"); + } + bool prompt = true; + if (!completion) { + std::atomic canceled{false}; + std::thread requester([&] { + Require(pthread_cancel_nid_postfix(reader) == 0, "request cancellation"); + canceled.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(); + state.release.store(true); + } else 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}); + Require(result == (completion || partialCompletion ? reinterpret_cast(std::uintptr_t{7}) : GUEST_CANCELED), "wrong thread result"); + Require(state.returned.load() == (completion || partialCompletion || state.disabled || state.sce), "unexpected read return"); + if (partialCompletion) Require(state.transferred == 1 && state.buffer[0] == 'x', "interrupted WAITALL lost progress"); + if (completion) Require(state.transferred == 1 && state.buffer[0] == 'x', "completion lost data"); + Require(state.cleanups.load() == 1 && state.destructors.load() == 1, "wrong cleanup or TSD count"); + Require(scePthreadKeyDelete(state.key) == 0, "delete TSD key"); + if (reuse) { + 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"); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d2d2c9872d8..75a94803a11 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -203,7 +203,7 @@ - [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) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg` are POSIX cancellation points, using FreeBSD libthr's entry check and failure-only exit check; SCE file reads, libSceNet calls, writes, connection operations and multiplexed I/O waits remain non-cancellable. Native file reads retain a descriptor alias and share its offset and options. Unix native I/O requires host `SIGUSR2` to be unused, temporarily unblocks it and restores the calling host signal mask; an existing handler makes the operation throw. Windows uses synchronous thread I/O cancellation for file reads and a distinct, drained overlapped request for each cancellable socket receive. Native drivers that do not support interruption may delay cancellation. Reading a kqueue descriptor still throws through its unsupported duplication path. Console cancellation behavior is unverified. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified @@ -230,8 +230,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, 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; `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. An enabled POSIX receive cancellation point 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 enabled POSIX cancellation, Windows overlapped receives bound the whole native request by `SO_RCVTIMEO`; the other paths bound only the wait for the first data. 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. 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. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). From 23dba78260e6554432cf3ca5507c7308070b69c5 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 02:29:33 -0300 Subject: [PATCH 08/61] fix(libkernel): preserve read validation and partial receive progress --- core/libs/prx/libkernel/CMakeLists.txt | 26 +++++- core/libs/prx/libkernel/File/src/Stdio.cpp | 16 +++- .../prx/libkernel/Pthread/src/NativeIo.cpp | 12 ++- core/libs/prx/libkernel/Socket/src/Udp.cpp | 90 +++++++++++-------- core/libs/tests/GuestIoReadCancel.cpp | 52 ++++++++--- docs/dev/debt/libkernel.md | 5 +- 6 files changed, 146 insertions(+), 55 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 026dd2f10a1..7439ead2d4d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -111,7 +111,7 @@ if(BUILD_TESTING) set(ioReadApis read alias readv recv recvfrom recvmsg) endif() foreach(ioReadApi IN LISTS ioReadApis) - foreach(ioReadScenario cancel complete disabled pending reuse reuse_complete partial) + 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) @@ -122,6 +122,30 @@ if(BUILD_TESTING) 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(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) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dbf06185d66..0ccb8e3e413 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1089,9 +1089,10 @@ 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) { return ThreadCancel::Io([&]() -> std::int64_t { + if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + if (IsKqueueDescriptor(d)) return PosixFailure(GUEST_ESPIPE); return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { - 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); @@ -1652,8 +1653,12 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcn int64_t APS5_VABI preadv_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { 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(d)) return PosixFailure(GUEST_ESPIPE); return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { - const auto result = sceKernelPreadv(d, iov, iovcnt, offset); + const auto result = sceKernelPreadv(d, vectors.data(), iovcnt, offset); return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; }); }); @@ -1833,8 +1838,11 @@ int APS5_VABI fdatasync_nid_postfix(int fd) { int64_t APS5_VABI readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { 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, iov, iovcnt); + const auto result = sceKernelReadv(d, vectors.data(), iovcnt); return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; }); }); diff --git a/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp index 83b9cad812f..2fec411705f 100644 --- a/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp +++ b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp @@ -58,10 +58,18 @@ struct NativeIo::State { 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 - wait.emplace(Interrupt, this); + try { + wait.emplace(Interrupt, this); + } catch (...) { + release(); + throw; + } } - ~State() { + ~State() { release(); } + +private: + void release() { const int saved = errno; wait.reset(); #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index fa2ddb87026..0d548d0dc96 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -781,48 +781,66 @@ int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* nonblocking = nonblocking || socket.options.nonblocking; timeout = socket.options.receiveTimeout; } - if (cancellable) { - OVERLAPPED operation{}; - operation.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (!operation.hEvent) { - WSASetLastError(WSAENOBUFS); + if (cancellable && (length != 0 || socket.type != 1)) { + const bool waitAll = socket.type == 1 && (flags & GuestWaitAll) != 0 && !nonblocking; + if (waitAll && (flags & GuestPeek) != 0) { + WSASetLastError(WSAEOPNOTSUPP); return -1; } - 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); + int transferred = 0; + for (;;) { + OVERLAPPED operation{}; + operation.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!operation.hEvent) { + WSASetLastError(WSAENOBUFS); + return transferred != 0 ? transferred : -1; } - } pending{socket, operation}; - DWORD done = 0; - DWORD nativeFlags = static_cast(NativeReceiveFlags(nonblocking ? flags & ~GuestWaitAll : flags, socket.type)); - int result = -1; - int error = WSAEINTR; - { - ThreadCancel::IoWait wait(Pending::Interrupt, &pending); - if (!ThreadCancel::Requested()) { - WSABUF destination{static_cast(length), static_cast(buffer)}; - 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; + 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::Requested()) { + 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 == 0) result = static_cast(done); } - 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::Requested()) return transferred; } - CloseHandle(operation.hEvent); - WSASetLastError(error); - return result; } return address ? ::recvfrom(socket.value, static_cast(buffer), length, NativeReceiveFlags(flags, socket.type), address, addressLength) diff --git a/core/libs/tests/GuestIoReadCancel.cpp b/core/libs/tests/GuestIoReadCancel.cpp index 6f576ccb95c..5cab851d83d 100644 --- a/core/libs/tests/GuestIoReadCancel.cpp +++ b/core/libs/tests/GuestIoReadCancel.cpp @@ -31,6 +31,8 @@ struct State { bool sce = false; bool partial = false; bool waitAll = false; + bool dontWait = false; + int error = 0; std::atomic ready{false}; std::atomic release{false}; std::atomic cleanups{0}; @@ -78,12 +80,16 @@ static void* APS5_VABI Reader(void* opaque) { 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 == "recv") state.transferred = recv_nid_postfix(state.input, state.buffer.data(), length, state.waitAll ? 0x40 : 0); - else if (state.operation == "recvfrom") state.transferred = recvfrom_nid_postfix(state.input, state.buffer.data(), length, state.waitAll ? 0x40 : 0, nullptr, nullptr); - else if (state.operation == "recvmsg") state.transferred = recvmsg_nid_postfix(state.input, &message, state.waitAll ? 0x40 : 0); + 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 == "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.disabled || state.sce) { Require(state.transferred == 1 && state.buffer[0] == 'x', "disabled read lost data"); @@ -110,7 +116,7 @@ static void Loopback(State& state) { Require(close_nid_postfix(listener) == 0, "close listener"); } -int main(int argc, char** argv) { +static void Run(int argc, char** argv) { Require(argc == 4, "expected transport, API, and scenario"); State state; state.operation = argv[2]; @@ -122,6 +128,9 @@ int main(int argc, char** argv) { state.waitAll = scenario == "waitall"; const bool completion = scenario == "complete" || 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"); @@ -133,12 +142,18 @@ int main(int argc, char** argv) { 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) { + if (!state.disabled && !state.pending && !state.sce && !wouldBlock) { const bool registered = Observe([&] { std::lock_guard lock(reader->cancelLock); return reader->cancelIo != nullptr; @@ -153,28 +168,40 @@ int main(int argc, char** argv) { Require(write_nid_postfix(replacement[1], "n", 1) == 1, "seed replacement pipe"); } bool prompt = true; - if (!completion) { + 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 Require(write_nid_postfix(state.output, "x", 1) == 1, "complete read"); + } 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}); - Require(result == (completion || partialCompletion ? reinterpret_cast(std::uintptr_t{7}) : GUEST_CANCELED), "wrong thread result"); - Require(state.returned.load() == (completion || partialCompletion || state.disabled || state.sce), "unexpected read return"); + 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"); + 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) Require(state.transferred == 1 && state.buffer[0] == 'x', "completion lost data"); + if (completion || racingCompletion) Require(state.transferred == 1 && state.buffer[0] == 'x', "completion lost 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) { char byte = 0; Require(read_nid_postfix(state.input, &byte, 1) == 1 && byte == 'n', "operation consumed the replacement descriptor"); @@ -182,3 +209,8 @@ int main(int argc, char** argv) { } 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/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 75a94803a11..49ccb772a0f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -203,7 +203,8 @@ - [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) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg` are POSIX cancellation points, using FreeBSD libthr's entry check and failure-only exit check; SCE file reads, libSceNet calls, writes, connection operations and multiplexed I/O waits remain non-cancellable. Native file reads retain a descriptor alias and share its offset and options. Unix native I/O requires host `SIGUSR2` to be unused, temporarily unblocks it and restores the calling host signal mask; an existing handler makes the operation throw. Windows uses synchronous thread I/O cancellation for file reads and a distinct, drained overlapped request for each cancellable socket receive. Native drivers that do not support interruption may delay cancellation. Reading a kqueue descriptor still throws through its unsupported duplication path. Console cancellation behavior is unverified. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. +- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation. Non-positioned reads of kqueue descriptors throw through their unsupported duplication path. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified @@ -231,7 +232,7 @@ - Guest sockets on macOS ([the equeue wait, 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; `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. An enabled POSIX receive cancellation point 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 enabled POSIX cancellation, Windows overlapped receives bound the whole native request by `SO_RCVTIMEO`; the other paths bound only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. +- [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 enabled POSIX cancellation, 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. 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. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). From 857c3e8b742907f720ac8ea1d53b79ca901006ca Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 03:04:45 -0300 Subject: [PATCH 09/61] fix(libkernel): preserve descriptor ownership during cancellable reads --- core/libs/prx/libSceNet/Export.cpp | 2 +- core/libs/prx/libc/CMakeLists.txt | 1 + .../prx/libc/include/DescriptorAccess.hpp | 30 ++ core/libs/prx/libc/include/FileStream.hpp | 11 + core/libs/prx/libc/src/DescriptorAccess.cpp | 107 ++++++ core/libs/prx/libkernel/CMakeLists.txt | 19 ++ core/libs/prx/libkernel/File/include/File.hpp | 5 +- core/libs/prx/libkernel/File/src/Open.cpp | 8 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 78 +++-- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 + core/libs/tests/GuestIoDescriptorAccess.cpp | 312 ++++++++++++++++++ core/libs/tests/GuestIoReadCancel.cpp | 11 +- docs/dev/debt/libkernel.md | 2 +- 13 files changed, 557 insertions(+), 31 deletions(-) create mode 100644 core/libs/prx/libc/include/DescriptorAccess.hpp create mode 100644 core/libs/prx/libc/src/DescriptorAccess.cpp create mode 100644 core/libs/tests/GuestIoDescriptorAccess.cpp diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 3ff9dad773b..ac1d8f3f699 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -979,7 +979,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); @@ -987,6 +986,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 68763d4139e..9c7685746ac 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -79,6 +79,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.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/DescriptorAccess.hpp b/core/libs/prx/libc/include/DescriptorAccess.hpp new file mode 100644 index 00000000000..fa36b78a714 --- /dev/null +++ b/core/libs/prx/libc/include/DescriptorAccess.hpp @@ -0,0 +1,30 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_DESCRIPTORACCESS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_DESCRIPTORACCESS_HPP + +#include +#include + +class GuestDescriptorAccess { +public: + enum class Mode { Read, Change }; + GuestDescriptorAccess(int descriptor, Mode mode); + GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t); + ~GuestDescriptorAccess(); + GuestDescriptorAccess(const GuestDescriptorAccess&) = delete; + GuestDescriptorAccess& operator=(const GuestDescriptorAccess&) = delete; + void Lock(); + bool TryLock(); + void Unlock(); + bool ReadOrWait(); + void Wake() noexcept; + static void LockTogether(GuestDescriptorAccess& first, GuestDescriptorAccess& second); + +private: + struct State; + 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 a97f626f497..9e6183927b3 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -12,6 +12,7 @@ #include #include #include +#include "prx/libc/include/DescriptorAccess.hpp" #ifdef _WIN32 #include #include "prx/libc/include/FileLock.hpp" @@ -148,6 +149,11 @@ class FileStream { bool Reopen(const std::filesystem::path& filename, const char* mode, [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); +#ifdef _WIN32 + const GuestDescriptorAccess descriptorAccess(previous ? ::_fileno(previous) : -1, GuestDescriptorAccess::Mode::Change); +#else + const GuestDescriptorAccess descriptorAccess(previous ? ::fileno(previous) : -1, GuestDescriptorAccess::Mode::Change); +#endif _guest = {}; guestError = false; randomDevice = false; @@ -299,6 +305,11 @@ class FileStream { int Close() { GetHandle(); +#ifdef _WIN32 + const GuestDescriptorAccess descriptorAccess(::_fileno(_handle), GuestDescriptorAccess::Mode::Change); +#else + const GuestDescriptorAccess descriptorAccess(::fileno(_handle), GuestDescriptorAccess::Mode::Change); +#endif #ifdef _WIN32 GuestFileLockForget_nid_no_patch(::_fileno(_handle)); if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(::_fileno(_handle)); diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp new file mode 100644 index 00000000000..619a1dceb3c --- /dev/null +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -0,0 +1,107 @@ +#include "prx/libc/include/DescriptorAccess.hpp" + +#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; + + 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) { + static std::mutex mutex; + static std::map> entries; + std::lock_guard lock(mutex); + if (auto state = entries[descriptor].lock()) return state; + auto state = std::make_shared(); + entries[descriptor] = state; + return state; +} + +GuestDescriptorAccess::GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t) + : state(Get(descriptor)), mode(mode) {} + +GuestDescriptorAccess::GuestDescriptorAccess(int descriptor, Mode mode) + : GuestDescriptorAccess(descriptor, mode, std::defer_lock) { Lock(); } + +GuestDescriptorAccess::~GuestDescriptorAccess() { if (held) Unlock(); } + +void GuestDescriptorAccess::Lock() { + std::unique_lock lock(state->mutex); + if (mode == Mode::Read) { + state->changed.wait(lock, [&] { return state->CanRead(); }); + ++state->readers; + } else { + ++state->changesWaiting; + try { + state->changed.wait(lock, [&] { return state->CanChange(); }); + } catch (...) { + --state->changesWaiting; + state->changed.notify_all(); + throw; + } + --state->changesWaiting; + state->writer = std::this_thread::get_id(); + ++state->depth; + } + held = true; +} + +bool GuestDescriptorAccess::TryLock() { + std::lock_guard lock(state->mutex); + if (mode == Mode::Read) { + if (!state->CanRead()) return false; + ++state->readers; + } else { + if (!state->CanChange()) return false; + state->writer = std::this_thread::get_id(); + ++state->depth; + } + held = true; + return true; +} + +void GuestDescriptorAccess::Unlock() { + { + std::lock_guard lock(state->mutex); + if (mode == Mode::Read) --state->readers; + else if (--state->depth == 0) state->writer = {}; + held = false; + } + state->changed.notify_all(); +} + +bool GuestDescriptorAccess::ReadOrWait() { + std::unique_lock lock(state->mutex); + if (state->CanRead()) { + ++state->readers; + held = true; + return true; + } + state->changed.wait(lock); + return false; +} + +void GuestDescriptorAccess::Wake() noexcept { state->changed.notify_all(); } + +void GuestDescriptorAccess::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); +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 7439ead2d4d..80ef1943325 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -100,6 +100,25 @@ endif() configure_windows_unwind(libkernel) if(BUILD_TESTING) + 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) + 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) + set_tests_properties(guest_io_descriptor_capacity guest_io_descriptor_no_aliases guest_io_descriptor_access_cancel 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) + set_tests_properties(guest_io_descriptor_close_progress guest_io_descriptor_position_cancel 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 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + else() + 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) + set_tests_properties(guest_io_descriptor_record_locks guest_io_descriptor_borrowed_net_close PROPERTIES TIMEOUT 15) + 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) diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index 91b0ef22b35..716e88a6c6c 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -12,6 +12,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/DescriptorAccess.hpp" extern "C" { @@ -32,8 +33,6 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ bool DescriptorIsOpen_nid_no_patch(int descriptor); namespace File { -std::recursive_mutex& DescriptorMutex(); - class IoDescriptor { public: explicit IoDescriptor(int descriptor); @@ -44,6 +43,7 @@ class IoDescriptor { private: int value; + GuestDescriptorAccess access; }; template @@ -55,6 +55,7 @@ std::int64_t WithIoDescriptor(int descriptor, TCall&& call) { #ifdef _WIN32 std::optional WindowsDescriptorPath_nid_no_patch(int descriptor); +std::optional> IoPositionedTransfer(int descriptor); #endif int HostStandardError(); [[nodiscard]] std::unique_lock LockPositionedTransfer(int descriptor); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0365061cdd4..cad05044191 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -72,8 +72,10 @@ static int NativeRead(int fd, void* buf, std::size_t n) { return -1; } std::optional> positioned; - if (GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) != FILE_TYPE_PIPE) - positioned.emplace(File::LockPositionedTransfer(fd)); + if (GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) != FILE_TYPE_PIPE) { + 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_NO_DATA) errno = EAGAIN; return result; @@ -322,7 +324,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { - std::lock_guard descriptorLock(File::DescriptorMutex()); + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Change); if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EPERM; int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) return socketError == 0 ? 0 : SceKernelError(socketError); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 0ccb8e3e413..6f9d9485296 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -246,7 +246,7 @@ struct PositionedLock { }; std::mutex g_positionedTableLock; std::unordered_map> g_positionedLocks; -std::unique_lock File::LockPositionedTransfer(int descriptor) { +static std::shared_ptr PositionedMutex(int descriptor) { std::shared_ptr entry; { std::lock_guard table(g_positionedTableLock); @@ -254,7 +254,29 @@ std::unique_lock File::LockPositionedTransfer(int descriptor) { if (!slot) slot = std::make_shared(); entry = slot; } - return std::unique_lock(entry->mutex); + return entry; +} +std::unique_lock File::LockPositionedTransfer(int descriptor) { + return std::unique_lock(PositionedMutex(descriptor)->mutex); +} +std::optional> File::IoPositionedTransfer(int descriptor) { + if (!ThreadCancel::IoEnabled()) 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::Requested()) { + errno = EINTR; + return std::nullopt; + } + std::unique_lock lock(entry->mutex, std::try_to_lock); + if (lock.owns_lock()) return 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); @@ -303,7 +325,8 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz 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; return NativePositionedTransfer(descriptor, buf, nbytes, offset, write); } std::int64_t File::WriteAtEnd(int descriptor, const void* buf, std::size_t nbytes) { @@ -457,6 +480,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; @@ -788,28 +812,32 @@ int APS5_VABI nmount_nid_postfix(const KernelIovec* iov, unsigned int niov, int } -std::recursive_mutex& File::DescriptorMutex() { - static std::recursive_mutex mutex; - return mutex; -} - -File::IoDescriptor::IoDescriptor(int descriptor) { - std::lock_guard lock(DescriptorMutex()); - value = dup_nid_postfix(descriptor); +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 (access.TryLock()) return; + ThreadCancel::IoWait wait([](void* argument) noexcept { + static_cast(argument)->Wake(); + }, &access); + for (;;) { + if (ThreadCancel::IoEnabled() && ThreadCancel::Requested()) { + value = -1; + *__error_nid_postfix() = GUEST_EINTR; + return; + } + if (access.ReadOrWait()) return; + } } -File::IoDescriptor::~IoDescriptor() { - const int saved = errno; - const int guestError = *__error_nid_postfix(); - if (value >= 0) close_nid_postfix(value); - *__error_nid_postfix() = guestError; - errno = saved; -} +File::IoDescriptor::~IoDescriptor() = default; extern "C" { int APS5_VABI close_nid_postfix(int d) { - std::lock_guard descriptorLock(File::DescriptorMutex()); + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Change); if (File::IsHostStream(d)) return PosixFailure(GUEST_EPERM); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { @@ -864,7 +892,7 @@ int File::HostStandardError() { extern "C" { int APS5_VABI dup_nid_postfix(int d) { - std::lock_guard descriptorLock(File::DescriptorMutex()); + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Read); if (GuestSockets::IsOpen(d)) return GuestSockets::Duplicate(d); if (d < 0) return PosixFailure(GUEST_EBADF); if (IsKqueueDescriptor(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); @@ -883,8 +911,11 @@ int APS5_VABI dup_nid_postfix(int d) { } int APS5_VABI dup2_nid_postfix(int from, int to) { - std::lock_guard descriptorLock(File::DescriptorMutex()); if (from < 0 || to < 0) 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"); @@ -1504,7 +1535,10 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, offset)) return *archived; } std::optional> positioned; - if (write || offset != nullptr || NativeIsDisk(d)) positioned.emplace(File::LockPositionedTransfer(d)); + if (write || offset != nullptr || NativeIsDisk(d)) { + 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); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 0d548d0dc96..b20c2cae0f6 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -404,6 +404,7 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l } int GuestSockets::Close(int descriptor) { + const GuestDescriptorAccess descriptorLock(descriptor, GuestDescriptorAccess::Mode::Change); std::shared_ptr released; { std::lock_guard lock(socketsMutex); @@ -541,6 +542,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); } diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp new file mode 100644 index 00000000000..1b42ecc4cf8 --- /dev/null +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -0,0 +1,312 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.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 + +#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); } +#else +#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); } +#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"); +} + +static bool Registered(Pthread reader) { + return Observe([&] { + std::lock_guard lock(reader->cancelLock); + return reader->cancelIo != nullptr; + }); +} + +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"); +} + +#ifdef _WIN32 +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() { + 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(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 && read.result == 0, "SCE 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 + +int main(int argc, char** argv) { + Require(argc == 2, "expected descriptor-access case"); + 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(); +#ifdef _WIN32 + else if (std::strcmp(argv[1], "close_progress") == 0) CloseProgress(); + else if (std::strcmp(argv[1], "position_cancel") == 0) AccessCancellation(true); +#endif +#ifndef _WIN32 + else if (std::strcmp(argv[1], "record_locks") == 0) RecordLocks(); + else if (std::strcmp(argv[1], "borrowed_net_close") == 0) BorrowedNetClose(); +#endif + else Require(false, "unknown descriptor-access case"); +} diff --git a/core/libs/tests/GuestIoReadCancel.cpp b/core/libs/tests/GuestIoReadCancel.cpp index 5cab851d83d..fa6712bce8c 100644 --- a/core/libs/tests/GuestIoReadCancel.cpp +++ b/core/libs/tests/GuestIoReadCancel.cpp @@ -161,11 +161,16 @@ static void Run(int argc, char** argv) { 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(dup2_nid_postfix(replacement[0], state.input) == state.input, "replace blocked descriptor"); - Require(close_nid_postfix(replacement[0]) == 0, "close replacement source"); 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) { @@ -203,6 +208,8 @@ static void Run(int argc, char** argv) { 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"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 49ccb772a0f..c4903297be8 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -204,7 +204,7 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. -- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation. Non-positioned reads of kqueue descriptors throw through their unsupported duplication path. +- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation. Concurrent close or replacement waits for active POSIX file reads; cancellation can interrupt a read waiting for descriptor access. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From c63fba65f35055bb371c3812cdc45b41d054be3a Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 03:54:33 -0300 Subject: [PATCH 10/61] fix(libkernel): preserve stream locking and bounded descriptor state --- core/libs/prx/libc/include/FileStream.hpp | 43 +++- core/libs/prx/libc/src/DescriptorAccess.cpp | 9 +- core/libs/prx/libkernel/CMakeLists.txt | 14 +- core/libs/prx/libkernel/File/src/Open.cpp | 9 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 25 ++- core/libs/tests/GuestIoDescriptorAccess.cpp | 208 ++++++++++++++++++++ core/libs/tests/GuestSocketReadWrite.cpp | 12 +- 7 files changed, 297 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 9e6183927b3..792f4540c1d 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -62,6 +62,39 @@ extern "C" int KeepHostStandardError_nid_no_patch(); #endif class FileStream { + class NativeLock { + std::FILE* handle; + public: + explicit NativeLock(std::FILE* handle) : handle(handle) { + if (!handle) 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{}; @@ -149,6 +182,7 @@ class FileStream { bool Reopen(const std::filesystem::path& filename, const char* mode, [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); + NativeLock streamLock(previous); #ifdef _WIN32 const GuestDescriptorAccess descriptorAccess(previous ? ::_fileno(previous) : -1, GuestDescriptorAccess::Mode::Change); #else @@ -167,7 +201,7 @@ class FileStream { if (open && !filename.empty()) { if (!IsHostStandard()) { _handle = nullptr; - std::fclose(previous); + streamLock.Close(); _handle = open(filename, mode); } else { std::wstring resetMode; @@ -207,9 +241,12 @@ class FileStream { KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); + const bool hostStandard = IsHostStandard(); 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 if (!_handle) return false; @@ -305,6 +342,7 @@ class FileStream { int Close() { GetHandle(); + NativeLock streamLock(_handle); #ifdef _WIN32 const GuestDescriptorAccess descriptorAccess(::_fileno(_handle), GuestDescriptorAccess::Mode::Change); #else @@ -314,7 +352,8 @@ class FileStream { GuestFileLockForget_nid_no_patch(::_fileno(_handle)); if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(::_fileno(_handle)); #endif - const int result = std::fclose(std::exchange(_handle, nullptr)); + _handle = nullptr; + const int result = streamLock.Close(); _guest.flags = 0; _guest.descriptor = -1; return result; diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp index 619a1dceb3c..aed6802502d 100644 --- a/core/libs/prx/libc/src/DescriptorAccess.cpp +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -24,6 +24,7 @@ std::shared_ptr GuestDescriptorAccess::Get(int des static std::mutex mutex; static std::map> entries; std::lock_guard lock(mutex); + std::erase_if(entries, [](const auto& entry) { return entry.second.expired(); }); if (auto state = entries[descriptor].lock()) return state; auto state = std::make_shared(); entries[descriptor] = state; @@ -31,7 +32,7 @@ std::shared_ptr GuestDescriptorAccess::Get(int des } GuestDescriptorAccess::GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t) - : state(Get(descriptor)), mode(mode) {} + : state(descriptor < 0 ? nullptr : Get(descriptor)), mode(mode) {} GuestDescriptorAccess::GuestDescriptorAccess(int descriptor, Mode mode) : GuestDescriptorAccess(descriptor, mode, std::defer_lock) { Lock(); } @@ -39,6 +40,7 @@ GuestDescriptorAccess::GuestDescriptorAccess(int descriptor, Mode mode) GuestDescriptorAccess::~GuestDescriptorAccess() { if (held) Unlock(); } void GuestDescriptorAccess::Lock() { + if (!state) { held = true; return; } std::unique_lock lock(state->mutex); if (mode == Mode::Read) { state->changed.wait(lock, [&] { return state->CanRead(); }); @@ -60,6 +62,7 @@ void GuestDescriptorAccess::Lock() { } bool GuestDescriptorAccess::TryLock() { + if (!state) { held = true; return true; } std::lock_guard lock(state->mutex); if (mode == Mode::Read) { if (!state->CanRead()) return false; @@ -74,6 +77,7 @@ bool GuestDescriptorAccess::TryLock() { } void GuestDescriptorAccess::Unlock() { + if (!state) { held = false; return; } { std::lock_guard lock(state->mutex); if (mode == Mode::Read) --state->readers; @@ -84,6 +88,7 @@ void GuestDescriptorAccess::Unlock() { } bool GuestDescriptorAccess::ReadOrWait() { + if (!state) { held = true; return true; } std::unique_lock lock(state->mutex); if (state->CanRead()) { ++state->readers; @@ -94,7 +99,7 @@ bool GuestDescriptorAccess::ReadOrWait() { return false; } -void GuestDescriptorAccess::Wake() noexcept { state->changed.notify_all(); } +void GuestDescriptorAccess::Wake() noexcept { if (state) state->changed.notify_all(); } void GuestDescriptorAccess::LockTogether(GuestDescriptorAccess& first, GuestDescriptorAccess& second) { struct Lockable { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 80ef1943325..8a03cf623c3 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -107,14 +107,22 @@ if(BUILD_TESTING) 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) - set_tests_properties(guest_io_descriptor_capacity guest_io_descriptor_no_aliases guest_io_descriptor_access_cancel PROPERTIES TIMEOUT 15) + add_test(NAME guest_io_descriptor_protected_close COMMAND guest_io_descriptor_access_tests protected_close) + 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) - set_tests_properties(guest_io_descriptor_close_progress guest_io_descriptor_position_cancel 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 PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME guest_io_descriptor_pipe_vectors COMMAND guest_io_descriptor_access_tests pipe_vectors) + 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) set_tests_properties(guest_io_descriptor_record_locks guest_io_descriptor_borrowed_net_close PROPERTIES TIMEOUT 15) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index cad05044191..a886f667586 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -71,11 +71,8 @@ static int NativeRead(int fd, void* buf, std::size_t n) { errno = EISDIR; return -1; } - std::optional> positioned; - if (GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) != FILE_TYPE_PIPE) { - positioned = File::IoPositionedTransfer(fd); - if (!positioned) return -1; - } + 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_NO_DATA) errno = EAGAIN; return result; @@ -324,8 +321,8 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { - const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Change); if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EPERM; + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Change); 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 6f9d9485296..e0d10b07afd 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -818,7 +818,13 @@ File::IoDescriptor::IoDescriptor(int descriptor) *__error_nid_postfix() = GUEST_EBADF; return; } - if (access.TryLock()) return; + if (access.TryLock()) { + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + value = -1; + *__error_nid_postfix() = GUEST_EBADF; + } + return; + } ThreadCancel::IoWait wait([](void* argument) noexcept { static_cast(argument)->Wake(); }, &access); @@ -828,7 +834,13 @@ File::IoDescriptor::IoDescriptor(int descriptor) *__error_nid_postfix() = GUEST_EINTR; return; } - if (access.ReadOrWait()) return; + if (access.ReadOrWait()) { + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + value = -1; + *__error_nid_postfix() = GUEST_EBADF; + } + return; + } } } @@ -837,8 +849,8 @@ File::IoDescriptor::~IoDescriptor() = default; extern "C" { int APS5_VABI close_nid_postfix(int d) { - const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Change); if (File::IsHostStream(d)) return PosixFailure(GUEST_EPERM); + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Change); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; @@ -1534,11 +1546,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; } - std::optional> positioned; - if (write || offset != nullptr || NativeIsDisk(d)) { - positioned = File::IoPositionedTransfer(d); - if (!positioned) return SceErrorFromErrno(errno); - } + 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); diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index 1b42ecc4cf8..1cae31ca447 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -15,6 +15,10 @@ #include #include #include +#include +#include +#include +#include "prx/libc/include/FileStream.hpp" #ifdef _WIN32 #include @@ -22,6 +26,7 @@ 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 @@ -30,6 +35,7 @@ static void NativeClose(int descriptor) { ::_close(descriptor); } 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) { @@ -187,7 +193,202 @@ static void AccessCancellation(bool positionedWait = false) { 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"); @@ -300,9 +501,16 @@ int main(int argc, char** argv) { 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], "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_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(); 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; From 3f3f32421af9b8d29059e11c0ee1e03472135a35 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 04:10:44 -0300 Subject: [PATCH 11/61] fix(libkernel): retain zero-length receives and append handles --- core/libs/prx/libc/src/FileAndHeap.cpp | 2 + core/libs/prx/libkernel/CMakeLists.txt | 15 +++++++ .../libkernel/File/src/DescriptorControl.cpp | 37 ++++++++++++++---- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- core/libs/tests/GuestIoDescriptorAccess.cpp | 39 +++++++++++++++++++ core/libs/tests/GuestIoReadCancel.cpp | 22 +++++++---- 6 files changed, 101 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index e023b0eaf44..b8327ba6141 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -139,6 +139,7 @@ static bool MoveAppendingHandle(int descriptor, HANDLE handle, DWORD access, DWO } static bool AppendThroughDescriptor(int descriptor) { + const GuestDescriptorAccess access(descriptor, GuestDescriptorAccess::Mode::Change); 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; @@ -146,6 +147,7 @@ static bool AppendThroughDescriptor(int descriptor) { } static bool AppendOnlyDescriptor(int descriptor) { + const GuestDescriptorAccess admission(descriptor, GuestDescriptorAccess::Mode::Change); 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/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9ea56d9fc29..59489242c14 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -115,6 +115,10 @@ if(BUILD_TESTING) 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:$") @@ -157,6 +161,17 @@ if(BUILD_TESTING) "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) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 337c823682c..bfdf356846a 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 @@ -374,14 +375,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::Change, 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/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b20c2cae0f6..6c05b815302 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -783,7 +783,7 @@ int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* nonblocking = nonblocking || socket.options.nonblocking; timeout = socket.options.receiveTimeout; } - if (cancellable && (length != 0 || socket.type != 1)) { + if (cancellable) { const bool waitAll = socket.type == 1 && (flags & GuestWaitAll) != 0 && !nonblocking; if (waitAll && (flags & GuestPeek) != 0) { WSASetLastError(WSAEOPNOTSUPP); diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index 1cae31ca447..df7357a2044 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -147,6 +147,43 @@ static void NoAliases() { 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); @@ -508,6 +545,8 @@ int main(int argc, char** argv) { else if (std::strcmp(argv[1], "invalid_registry") == 0) InvalidRegistry(); #endif #ifdef _WIN32 + 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(); diff --git a/core/libs/tests/GuestIoReadCancel.cpp b/core/libs/tests/GuestIoReadCancel.cpp index fa6712bce8c..79ee9b20165 100644 --- a/core/libs/tests/GuestIoReadCancel.cpp +++ b/core/libs/tests/GuestIoReadCancel.cpp @@ -32,6 +32,7 @@ struct State { bool partial = false; bool waitAll = false; bool dontWait = false; + bool zero = false; int error = 0; std::atomic ready{false}; std::atomic release{false}; @@ -72,11 +73,11 @@ static void* APS5_VABI Reader(void* opaque) { __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.partial || state.waitAll ? state.buffer.size() : 1; + 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 = 1; + 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); @@ -85,14 +86,14 @@ static void* APS5_VABI Reader(void* opaque) { 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 == "recvmsg") state.transferred = recvmsg_nid_postfix(state.input, &message, state.waitAll ? 0x40 : state.dontWait ? 0x80 : 0); + 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.disabled || state.sce) { - Require(state.transferred == 1 && state.buffer[0] == 'x', "disabled read lost data"); + 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(); } @@ -120,13 +121,14 @@ 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.disabled = scenario == "disabled"; state.pending = scenario == "pending"; state.partial = scenario == "partial"; state.waitAll = scenario == "waitall"; - const bool completion = scenario == "complete" || scenario == "reuse_complete" || state.partial; + 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"; @@ -192,7 +194,8 @@ static void Run(int argc, char** argv) { requester.join(); if (writer.joinable()) writer.join(); state.release.store(true); - } else if (!wouldBlock) Require(write_nid_postfix(state.output, "x", 1) == 1, "complete read"); + } 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"); @@ -202,7 +205,12 @@ static void Run(int argc, char** argv) { Require(result == (completion || partialCompletion || racingCompletion || wouldBlock ? reinterpret_cast(std::uintptr_t{7}) : GUEST_CANCELED), "wrong thread result"); 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.transferred == 1 && state.buffer[0] == 'x', "completion lost data"); + 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"); From 6a95e137de5d439708b338403a62d92a0fad6183 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 05:48:01 -0300 Subject: [PATCH 12/61] fix(libkernel): release read ownership before asynchronous signal handlers --- .../prx/libc/include/DescriptorAccess.hpp | 15 +- core/libs/prx/libc/include/JumpBuffer.hpp | 3 + core/libs/prx/libc/src/DescriptorAccess.cpp | 26 +- core/libs/prx/libc/src/FileAndHeap.cpp | 4 +- core/libs/prx/libc/src/JumpBuffer.cpp | 6 + core/libs/prx/libkernel/CMakeLists.txt | 46 +++ .../libkernel/File/src/DescriptorControl.cpp | 2 +- core/libs/prx/libkernel/File/src/Open.cpp | 1 + core/libs/prx/libkernel/File/src/Stdio.cpp | 29 +- .../prx/libkernel/Pthread/include/Cancel.hpp | 53 ++- .../prx/libkernel/Pthread/src/IoSignals.cpp | 384 ++++++++++++++++++ .../prx/libkernel/Pthread/src/NativeIo.cpp | 20 +- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 29 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 23 +- .../prx/libkernel/System/src/Exception.cpp | 27 ++ .../libs/prx/libkernel/System/src/Signals.cpp | 34 +- core/libs/tests/GuestIoReadCancel.cpp | 9 +- core/libs/tests/GuestIoSignalClose.cpp | 212 ++++++++++ docs/dev/debt/libkernel.md | 9 +- 19 files changed, 883 insertions(+), 49 deletions(-) create mode 100644 core/libs/prx/libkernel/Pthread/src/IoSignals.cpp create mode 100644 core/libs/tests/GuestIoSignalClose.cpp diff --git a/core/libs/prx/libc/include/DescriptorAccess.hpp b/core/libs/prx/libc/include/DescriptorAccess.hpp index fa36b78a714..43710455e53 100644 --- a/core/libs/prx/libc/include/DescriptorAccess.hpp +++ b/core/libs/prx/libc/include/DescriptorAccess.hpp @@ -3,10 +3,21 @@ #include #include +#include class GuestDescriptorAccess { + struct State; public: - enum class Mode { Read, Change }; + class Version { + public: + bool Unchanged() const; + private: + friend class GuestDescriptorAccess; + Version(std::shared_ptr state, std::uint64_t value); + std::shared_ptr state; + std::uint64_t value; + }; + enum class Mode { Read, Change, Update }; GuestDescriptorAccess(int descriptor, Mode mode); GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t); ~GuestDescriptorAccess(); @@ -17,10 +28,10 @@ class GuestDescriptorAccess { void Unlock(); bool ReadOrWait(); void Wake() noexcept; + Version Snapshot() const; static void LockTogether(GuestDescriptorAccess& first, GuestDescriptorAccess& second); private: - struct State; static std::shared_ptr Get(int descriptor); std::shared_ptr state; Mode mode; diff --git a/core/libs/prx/libc/include/JumpBuffer.hpp b/core/libs/prx/libc/include/JumpBuffer.hpp index 07e272b61e6..a97b538fc09 100644 --- a/core/libs/prx/libc/include/JumpBuffer.hpp +++ b/core/libs/prx/libc/include/JumpBuffer.hpp @@ -2,14 +2,17 @@ #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); extern "C" { void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask); +void GuestIoJumpRegister_nid_no_patch(StackRestore restore); } diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp index aed6802502d..b4061b26a8c 100644 --- a/core/libs/prx/libc/src/DescriptorAccess.cpp +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -3,6 +3,7 @@ #include #include #include +#include struct GuestDescriptorAccess::State { std::mutex mutex; @@ -11,6 +12,8 @@ struct GuestDescriptorAccess::State { unsigned changesWaiting = 0; std::thread::id writer; unsigned depth = 0; + std::uint64_t generation = 0; + bool replacement = false; bool CanRead() const { return writer == std::this_thread::get_id() || (writer == std::thread::id{} && changesWaiting == 0); @@ -57,6 +60,7 @@ void GuestDescriptorAccess::Lock() { --state->changesWaiting; state->writer = std::this_thread::get_id(); ++state->depth; + state->replacement = state->replacement || mode == Mode::Change; } held = true; } @@ -71,6 +75,7 @@ bool GuestDescriptorAccess::TryLock() { if (!state->CanChange()) return false; state->writer = std::this_thread::get_id(); ++state->depth; + state->replacement = state->replacement || mode == Mode::Change; } held = true; return true; @@ -81,7 +86,11 @@ void GuestDescriptorAccess::Unlock() { { std::lock_guard lock(state->mutex); if (mode == Mode::Read) --state->readers; - else if (--state->depth == 0) state->writer = {}; + else if (--state->depth == 0) { + state->writer = {}; + if (state->replacement && state->generation != std::numeric_limits::max()) ++state->generation; + state->replacement = false; + } held = false; } state->changed.notify_all(); @@ -101,6 +110,21 @@ bool GuestDescriptorAccess::ReadOrWait() { void GuestDescriptorAccess::Wake() noexcept { if (state) state->changed.notify_all(); } +GuestDescriptorAccess::Version::Version(std::shared_ptr state, std::uint64_t value) + : state(std::move(state)), value(value) {} + +bool GuestDescriptorAccess::Version::Unchanged() const { + if (!state) return false; + std::lock_guard lock(state->mutex); + return value != std::numeric_limits::max() && state->generation == value; +} + +GuestDescriptorAccess::Version GuestDescriptorAccess::Snapshot() const { + if (!state) return Version(nullptr, 0); + std::lock_guard lock(state->mutex); + return Version(state, state->generation); +} + void GuestDescriptorAccess::LockTogether(GuestDescriptorAccess& first, GuestDescriptorAccess& second) { struct Lockable { GuestDescriptorAccess& access; diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 3b0c9ec64c4..a01f57ff1ed 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -139,7 +139,7 @@ static bool MoveAppendingHandle(int descriptor, HANDLE handle, DWORD access, DWO } static bool AppendThroughDescriptor(int descriptor) { - const GuestDescriptorAccess access(descriptor, GuestDescriptorAccess::Mode::Change); + 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; @@ -147,7 +147,7 @@ static bool AppendThroughDescriptor(int descriptor) { } static bool AppendOnlyDescriptor(int descriptor) { - const GuestDescriptorAccess admission(descriptor, GuestDescriptorAccess::Mode::Change); + 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 17c34cf9d0f..c9a00a1e1ae 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -15,6 +15,7 @@ constexpr int SigBlock = 1; constexpr int SigSetmask = 3; std::atomic guestSigprocmask{nullptr}; +std::atomic ioStackRestore{nullptr}; } @@ -24,6 +25,8 @@ void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask) { guestSigprocmask.store(sigprocmask); } +void GuestIoJumpRegister_nid_no_patch(StackRestore restore) { ioStackRestore.store(restore); } + 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; @@ -31,6 +34,9 @@ void APS5_VABI JumpBufferSaveMask_nid_no_patch(unsigned char* buffer) { } void APS5_VABI JumpBufferRestoreMask_nid_no_patch(const unsigned char* buffer) { + std::uintptr_t stack = 0; + std::memcpy(&stack, buffer + 16, sizeof(stack)); + if (const auto restore = ioStackRestore.load()) restore(stack); std::int32_t saved = 0; std::memcpy(&saved, buffer + MaskSavedOffset, sizeof(saved)); const auto sigprocmask = guestSigprocmask.load(); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6e36dcd8790..9d5602c6ecd 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${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/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp @@ -100,6 +101,51 @@ endif() configure_windows_unwind(libkernel) if(BUILD_TESTING) + 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) + set_tests_properties(${ioSignalTest}_resume PROPERTIES TIMEOUT 15) + foreach(ioSignalJump jump local_jump) + 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 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) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index bfdf356846a..60e2b1169e4 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -386,7 +386,7 @@ int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { 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::Change, std::defer_lock)); + admissions.push_back(std::make_unique(descriptor, GuestDescriptorAccess::Mode::Update, std::defer_lock)); if (!admissions.back()->TryLock()) { blocked = true; break; } } if (blocked) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 425a656f05f..c2e9b1901ea 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -77,6 +77,7 @@ static int NativeRead(int fd, void* buf, std::size_t 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; } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index acfbe88d4fb..2833dab3195 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -261,7 +261,7 @@ File::PositionedTransferLock File::LockPositionedTransfer(int descriptor) { return PositionedTransferLock(std::shared_ptr(entry, &entry->mutex)); } std::optional File::IoPositionedTransfer(int descriptor) { - if (!ThreadCancel::IoEnabled()) return LockPositionedTransfer(descriptor); + if (!ThreadCancel::IoActive()) return LockPositionedTransfer(descriptor); const auto entry = PositionedMutex(descriptor); TimedWait::Condition changed; std::mutex mutex; @@ -269,7 +269,7 @@ std::optional File::IoPositionedTransfer(int descr static_cast(argument)->NotifyAll(); }, &changed); for (;;) { - if (ThreadCancel::Requested()) { + if (ThreadCancel::IoInterrupted()) { errno = EINTR; return std::nullopt; } @@ -320,7 +320,7 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz } 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; @@ -363,7 +363,11 @@ static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t if (appended != -2) return appended; } } - if (!write) return ThreadCancel::Native([&] { 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; @@ -819,7 +823,17 @@ File::IoDescriptor::IoDescriptor(int descriptor) *__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; @@ -830,12 +844,17 @@ File::IoDescriptor::IoDescriptor(int descriptor) static_cast(argument)->Wake(); }, &access); for (;;) { - if (ThreadCancel::IoEnabled() && ThreadCancel::Requested()) { + 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; diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index 0ddef8db0da..e30142ddc81 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -38,13 +38,40 @@ 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&); + State* state; +}; +void BeginIoSignals(Pthread thread, IoRestart& restart); +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); class IoPoint { public: - IoPoint(); + explicit IoPoint(IoRestart& restart); ~IoPoint(); IoPoint(const IoPoint&) = delete; IoPoint& operator=(const IoPoint&) = delete; + +private: + std::optional delivery{std::in_place}; }; class IoWait { @@ -57,10 +84,14 @@ class IoWait { 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; }; class NativeIo { @@ -78,7 +109,7 @@ class NativeIo { template auto Native(TCall&& call) { NativeIo operation; - if (IoEnabled() && Requested()) { + if (IoInterrupted()) { errno = EINTR; return static_cast(-1); } @@ -86,15 +117,19 @@ auto Native(TCall&& call) { } template -auto Io(TCall&& call) { +auto Io(TCall&& call, bool restartable = true) { Check(); - decltype(call()) result; - { - IoPoint point; - result = std::forward(call)(); + 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; } - if (result < 0) Check(); - return result; } template 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..2497bb91b33 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -0,0 +1,384 @@ +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libc/include/DescriptorAccess.hpp" +#include "prx/libc/include/JumpBuffer.hpp" + +#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; + bool allowed; + bool ready = false; + std::vector expected; + State(State* previous, std::uintptr_t stack, bool allowed) : previous(previous), stack(stack), allowed(allowed) {} +}; + +namespace { + +class SignalManager; + +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{}; +#endif + + bool Pending() const { return signals.load() != 0 || delivery.load(); } +}; + +static_assert(std::atomic::is_always_lock_free); +constinit thread_local SignalState* current = nullptr; +thread_local std::shared_ptr owner; +thread_local std::shared_ptr delivering; +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; + 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::yield(); + } while (pending); + } + } +}; + +SignalManager& Manager() { + static auto* manager = new SignalManager; + 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 = &(*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) { + if (current) { ++depth; return; } + const SetupBlock blocked; + auto& manager = Manager(); + auto state = std::make_shared(); + state->manager = &manager; + state->guest = thread; + state->replay = restart.state; + restart.state->ready = false; + { + std::lock_guard lock(manager.mutex); + manager.entries.push_back(state); + } + owner = std::move(state); + current = owner.get(); + depth = 1; +} + +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(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(delivering); + 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(); + delivering = std::move(state->previousDelivery); + } + errno = saved; +} + +bool IoInterrupted() { + 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::uint64_t signals = 0; + void (*callback)() = nullptr; +#ifndef _WIN32 + std::optional mask; +#endif + { + const SetupBlock blocked; + const bool exiting = stack == std::numeric_limits::max(); + while (delivering && delivering->replay->stack < stack) { + signals |= delivering->signals.load(); + if (const auto next = exiting ? delivering->retire : delivering->callback) callback = next; +#ifndef _WIN32 + if (!exiting) mask = delivering->deliveryMask; +#endif + delivering = std::move(delivering->previousDelivery); + } + while (restarts && restarts->stack < stack) { + auto* retired = restarts; + restarts = 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 = Manager(); + 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 index 2fec411705f..6f4746532d3 100644 --- a/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp +++ b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp @@ -70,6 +70,9 @@ struct NativeIo::State { private: void release() { +#ifdef _WIN32 + const DWORD error = GetLastError(); +#endif const int saved = errno; wait.reset(); #ifdef _WIN32 @@ -84,13 +87,26 @@ struct NativeIo::State { pthread_sigmask(SIG_SETMASK, &previous, nullptr); #endif errno = saved; +#ifdef _WIN32 + SetLastError(error); +#endif } }; NativeIo::NativeIo() { - if (IoEnabled()) state = new State; + if (IoActive()) state = new State; } -NativeIo::~NativeIo() { delete state; } +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 bdefb7f8cb0..f7662584b20 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -731,6 +731,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(); @@ -771,8 +772,11 @@ void ThreadCancel::EndWait() { static thread_local unsigned ioPoints = 0; -ThreadCancel::IoPoint::IoPoint() { ++ioPoints; } -ThreadCancel::IoPoint::~IoPoint() { --ioPoints; } +ThreadCancel::IoPoint::IoPoint(IoRestart& restart) { BeginIoSignals(CurrentGuestState().current, restart); ++ioPoints; } +ThreadCancel::IoPoint::~IoPoint() { --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 && @@ -780,17 +784,25 @@ bool ThreadCancel::IoEnabled() { } ThreadCancel::IoWait::IoWait(Interrupt interrupt, void* argument) : interrupt(interrupt), argument(argument) { + BeginSignal(); if (!IoEnabled()) return; thread = CurrentGuestState().current; - std::lock_guard lock(thread->cancelLock); - previous = thread->cancelIo; - thread->cancelIo = this; + try { + std::lock_guard lock(thread->cancelLock); + previous = thread->cancelIo; + thread->cancelIo = this; + } catch (...) { + EndSignal(); + throw; + } } ThreadCancel::IoWait::~IoWait() { - if (!thread) return; - std::lock_guard lock(thread->cancelLock); - thread->cancelIo = previous; + 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); @@ -1242,6 +1254,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; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 6c05b815302..3fd09e1aeff 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -775,7 +775,7 @@ int SocketSendFlags(Socket& socket, int flags) { } int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* address, socklen_t* addressLength) { #ifdef _WIN32 - const bool cancellable = ThreadCancel::IoEnabled(); + const bool cancellable = ThreadCancel::IoActive(); bool nonblocking = (flags & GuestDontWait) != 0; std::int64_t timeout = 0; { @@ -784,6 +784,7 @@ int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* 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); @@ -819,7 +820,7 @@ int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* int error = WSAEINTR; { ThreadCancel::IoWait wait(Pending::Interrupt, &pending); - if (!ThreadCancel::Requested()) { + if (!ThreadCancel::IoInterrupted()) { WSABUF destination{static_cast(length - transferred), transferred == 0 ? static_cast(buffer) : static_cast(buffer) + transferred}; const int started = address @@ -835,13 +836,14 @@ int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* 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::Requested()) return transferred; + if (!waitAll || result == 0 || transferred == length || ThreadCancel::IoInterrupted()) return transferred; } } return address @@ -855,7 +857,7 @@ int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* } int WaitForTransfer(Socket& socket, bool send, int flags) { #ifdef _WIN32 - if (!send && ThreadCancel::IoEnabled()) return 0; + if (!send && ThreadCancel::IoActive()) return 0; #endif if ((flags & GuestDontWait) != 0) { #ifdef _WIN32 @@ -874,10 +876,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::IoEnabled() && ThreadCancel::Requested()) return 4; + 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)); @@ -1782,6 +1785,8 @@ static std::int64_t ReceiveMessage(int descriptor, GuestMsghdr* message, int fla 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; if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); @@ -2116,11 +2121,15 @@ extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* } std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { - return ThreadCancel::Io([&] { return Receive(descriptor, buffer, length, 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 ReceiveFrom(descriptor, buffer, length, flags, address, 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); }); diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index b0ec9a9df89..51d2016987f 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.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 @@ -293,14 +294,24 @@ struct QueuedDelivery { std::atomic taken{false}; std::atomic waiting{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; } 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); @@ -311,6 +322,20 @@ void CALLBACK WaitingEntry(ULONG_PTR parameter) { Deliver(handler, signum, context); } +void DeliverIoCallbacks() { + while (auto* queued = ioDeliveries) { + 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"); @@ -719,6 +744,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); @@ -923,6 +949,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) { diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index f5493eb5706..d212eaca9af 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 @@ -209,22 +210,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, 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; } @@ -235,8 +243,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/tests/GuestIoReadCancel.cpp b/core/libs/tests/GuestIoReadCancel.cpp index 79ee9b20165..eaecad260c9 100644 --- a/core/libs/tests/GuestIoReadCancel.cpp +++ b/core/libs/tests/GuestIoReadCancel.cpp @@ -33,6 +33,7 @@ struct State { bool waitAll = false; bool dontWait = false; bool zero = false; + bool zeroCancel = false; int error = 0; std::atomic ready{false}; std::atomic release{false}; @@ -92,6 +93,11 @@ static void* APS5_VABI Reader(void* opaque) { 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"); @@ -124,6 +130,7 @@ static void Run(int argc, char** argv) { 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"; @@ -203,7 +210,7 @@ static void Run(int argc, char** argv) { 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"); - Require(state.returned.load() == (completion || partialCompletion || racingCompletion || wouldBlock || state.disabled || state.sce), "unexpected read return"); + 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)) { diff --git a/core/libs/tests/GuestIoSignalClose.cpp b/core/libs/tests/GuestIoSignalClose.cpp new file mode 100644 index 00000000000..86606f5cbcc --- /dev/null +++ b/core/libs/tests/GuestIoSignalClose.cpp @@ -0,0 +1,212 @@ +#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 socket = false; + std::intptr_t nativeInput = -1; + alignas(16) std::array jump{}; + std::atomic nativeId{0}; + 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->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 + 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 + +int main(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 resume = 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"); +#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); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 9a04b022fd1..c85ed3cdb3f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -191,7 +191,7 @@ - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection -- [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 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; POSIX read ownership also defers asynchronous delivery until its guards are released. Other emulator locks are not deferred. Restartable waits resume only while their descriptor generation is unchanged. 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 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 @@ -204,7 +204,8 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. -- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation. Concurrent close or replacement waits for active POSIX file reads; cancellation can interrupt a read waiting for descriptor access. +- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation. Concurrent close or replacement waits for active POSIX descriptor reads and receives; cancellation can interrupt a read waiting for descriptor access. +- [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 longjmp retires abandoned delivery/restart state and restores its host signal mask. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified @@ -231,8 +232,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, 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; `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. An enabled POSIX receive cancellation point 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 enabled POSIX cancellation, 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`. +- [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. 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. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). From cad3634b68445002d24c34646c97f4edc45c26c3 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 06:08:03 -0300 Subject: [PATCH 13/61] test(libkernel): bound signal delivery coverage and check I/O startup races --- core/libs/prx/libkernel/CMakeLists.txt | 30 ++++- .../prx/libkernel/Pthread/include/Cancel.hpp | 6 +- .../prx/libkernel/Pthread/src/IoSignals.cpp | 28 +++-- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 8 +- core/libs/tests/GuestIoSignalClose.cpp | 15 ++- core/libs/tests/GuestRaiseException.cpp | 105 ++++++++++++++---- docs/dev/debt/libkernel.md | 14 +-- 7 files changed, 159 insertions(+), 47 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index d00f341ccae..c9f478a8cce 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -123,7 +123,7 @@ if(BUILD_TESTING) set_tests_properties(${ioSignalTest} PROPERTIES TIMEOUT 15) add_test(NAME ${ioSignalTest}_resume COMMAND guest_io_signal_close_tests ${ioSignalSource} ${ioSignalMode} resume) set_tests_properties(${ioSignalTest}_resume PROPERTIES TIMEOUT 15) - foreach(ioSignalJump jump local_jump) + 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) @@ -1372,12 +1372,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) + 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/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index e30142ddc81..10d8b066fef 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -51,10 +51,10 @@ class IoRestart { IoRestart(const IoRestart&) = delete; IoRestart& operator=(const IoRestart&) = delete; private: - friend void BeginIoSignals(Pthread, IoRestart&); + friend void BeginIoSignals(Pthread, IoRestart&, std::optional&); State* state; }; -void BeginIoSignals(Pthread thread, IoRestart& restart); +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)()); @@ -71,7 +71,7 @@ class IoPoint { IoPoint& operator=(const IoPoint&) = delete; private: - std::optional delivery{std::in_place}; + std::optional delivery; }; class IoWait { diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp index 2497bb91b33..7491a79b2e8 100644 --- a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -43,6 +43,7 @@ struct IoRestart::State { namespace { class SignalManager; +std::atomic published{nullptr}; class SetupBlock { public: @@ -188,7 +189,11 @@ class SignalManager { }; SignalManager& Manager() { - static auto* manager = new SignalManager; + static auto* manager = [] { + auto* value = new SignalManager; + published.store(value, std::memory_order_release); + return value; + }(); return *manager; } @@ -221,8 +226,12 @@ void IoRestart::Clear() { bool IoRestart::Ready() const { return state->ready && IoRestartError(); } -void BeginIoSignals(Pthread thread, IoRestart& restart) { - if (current) { ++depth; return; } +void BeginIoSignals(Pthread thread, IoRestart& restart, std::optional& deferral) { + if (current) { + if (!deferral) deferral.emplace(); + ++depth; + return; + } const SetupBlock blocked; auto& manager = Manager(); auto state = std::make_shared(); @@ -230,6 +239,7 @@ void BeginIoSignals(Pthread thread, IoRestart& restart) { 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); @@ -237,6 +247,9 @@ void BeginIoSignals(Pthread thread, IoRestart& restart) { owner = std::move(state); current = owner.get(); depth = 1; +#ifdef _WIN32 + TimedWait::DeliverDeferred(); +#endif } void EndIoSignals(std::optional& deferral) { @@ -357,12 +370,13 @@ void RetireIoStack(std::uintptr_t stack) { void WakeIoDelivery(Pthread thread) { if (!thread) return; - auto& manager = Manager(); - std::lock_guard registered(manager.mutex); - for (const auto& state : manager.entries) { + 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(); + manager->Notify(); } } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index f7662584b20..1770d970d04 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -772,7 +772,13 @@ void ThreadCancel::EndWait() { static thread_local unsigned ioPoints = 0; -ThreadCancel::IoPoint::IoPoint(IoRestart& restart) { BeginIoSignals(CurrentGuestState().current, restart); ++ioPoints; } +ThreadCancel::IoPoint::IoPoint(IoRestart& restart) { +#ifdef _WIN32 + delivery.emplace(); +#endif + BeginIoSignals(CurrentGuestState().current, restart, delivery); + ++ioPoints; +} ThreadCancel::IoPoint::~IoPoint() { --ioPoints; EndIoSignals(delivery); } bool ThreadCancel::IoActive() { return ioPoints != 0; } diff --git a/core/libs/tests/GuestIoSignalClose.cpp b/core/libs/tests/GuestIoSignalClose.cpp index 86606f5cbcc..72a2ec19953 100644 --- a/core/libs/tests/GuestIoSignalClose.cpp +++ b/core/libs/tests/GuestIoSignalClose.cpp @@ -50,6 +50,7 @@ struct State { 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; @@ -93,6 +94,7 @@ static void* APS5_VABI Reader(void* opaque) { 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(); @@ -122,7 +124,7 @@ static bool NativeReadBlocked(const State& reading) { } #endif -int main(int argc, char** argv) { +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"); @@ -131,7 +133,8 @@ int main(int argc, char** argv) { state = &reading; reading.disabled = std::strcmp(argv[2], "disabled") == 0; reading.socket = source.ends_with("_socket"); - const bool resume = argc == 4 && std::strcmp(argv[3], "resume") == 0; + const bool starting = argc == 4 && std::strcmp(argv[3], "starting") == 0; + const bool resume = starting || (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; @@ -161,6 +164,8 @@ int main(int argc, char** argv) { 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; @@ -169,6 +174,7 @@ int main(int argc, char** argv) { #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"); @@ -210,3 +216,8 @@ int main(int argc, char** argv) { 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 439e8086134..6e7a0ee3bdd 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -382,7 +382,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}; @@ -552,7 +552,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 { @@ -567,17 +567,24 @@ int main() { Require(sceKernelRaiseException(scePthreadSelf(), 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, 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; @@ -586,13 +593,16 @@ int main() { std::this_thread::sleep_for(std::chrono::milliseconds(50)); for (int raised = 0; raised < Repeats; ++raised) { Require(sceKernelRaiseException(waitingThread, 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(); @@ -602,7 +612,7 @@ int main() { std::this_thread::sleep_for(std::chrono::milliseconds(20)); Require(sceKernelRaiseException(blockedThread, 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); @@ -610,7 +620,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; @@ -623,13 +636,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, 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; @@ -638,24 +654,29 @@ int main() { while (!continuing.started.load() || continued.load() == 0) std::this_thread::yield(); for (int raised = 0; raised < ContinuingRounds; ++raised) { Require(sceKernelRaiseException(continuingThread, 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, 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); @@ -683,7 +704,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); @@ -694,15 +718,18 @@ int main() { std::this_thread::sleep_for(std::chrono::milliseconds(20)); for (int raised = 0; raised < VectorRounds; ++raised) { Require(sceKernelRaiseException(vectorsThread, 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); } +} + #endif +static void CaseContext() { SpinState spin; Pthread spinningThread = nullptr; Require(scePthreadCreate(&spinningThread, nullptr, Spinning, &spin, "spinning") == 0); @@ -716,7 +743,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); @@ -729,7 +758,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); @@ -738,24 +769,56 @@ 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, SIGUSR1) == SCE_KERNEL_ERROR_ESRCH); Require(scePthreadJoin(finishedThread, nullptr) == 0); +} - Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); - - Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&NestedHandler)) == 0); - NestedWaitKeepsLaterWaiters(); - NestedWaitKeepsOuterWake(); - Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); - - Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&HoldingHandler)) == 0); - HandlerLeavesAddressLock(AddressWaking, "address waking"); - HandlerLeavesAddressLock(AddressWaiting, "address waiting"); - HandlerRunsInUnboundedAddressWait(); - Require(sceKernelRemoveExceptionHandler(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}, +#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(SIGUSR1, reinterpret_cast(selected.handler)) == 0); + selected.run(); + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + return 0; + } + Require(false); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index bd5d11e4bed..0dac44303ac 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -190,15 +190,15 @@ - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection -- [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; POSIX read ownership also defers asynchronous delivery until its guards are released. Other emulator locks are not deferred. Restartable waits resume only while their descriptor generation is unchanged. 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 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 +- [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 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 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 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. - [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. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs a queued delivery (an APC) in the first alertable wait of libkernel's TimedWait the target enters, or where libkernel drains APCs after releasing its address-wait or park queue lock (both are taken under a delivery deferral). Host code that holds a lock of its own across such a wait keeps it held while a waiting handler runs -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows throws for a target thread that was created suspended and is not resumed yet. 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 +- [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 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 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. - [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 From 6de6c5bd0ea3cf8b814437b6a55865b94e65523a Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 06:46:46 -0300 Subject: [PATCH 14/61] fix(libkernel): preserve I/O delivery across user contexts --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/include/GuestStacks.hpp | 7 + core/libs/prx/libc/include/JumpBuffer.hpp | 1 + core/libs/prx/libc/src/GuestStacks.cpp | 56 +++-- core/libs/prx/libc/src/JumpBuffer.cpp | 6 +- core/libs/prx/libkernel/CMakeLists.txt | 12 + .../prx/libkernel/Pthread/include/Cancel.hpp | 2 +- .../prx/libkernel/Pthread/src/IoSignals.cpp | 69 ++++-- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 2 +- .../prx/libkernel/System/src/SignalSets.cpp | 8 + .../prx/libkernel/System/src/UserContext.cpp | 1 + core/libs/tests/GuestIoContext.cpp | 215 ++++++++++++++++++ docs/dev/debt/libkernel.md | 12 +- 13 files changed, 356 insertions(+), 36 deletions(-) create mode 100644 core/libs/tests/GuestIoContext.cpp diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index cf0eb2256a1..f35c52bb18d 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -7,6 +7,7 @@ #include "prx/libc/include/GuestLocale.hpp" #include "prx/libc/include/GuestDirectory.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 "SceTypes.hpp" diff --git a/core/libs/prx/libc/include/GuestStacks.hpp b/core/libs/prx/libc/include/GuestStacks.hpp index 2b729a9bd57..f8a280bcb15 100644 --- a/core/libs/prx/libc/include/GuestStacks.hpp +++ b/core/libs/prx/libc/include/GuestStacks.hpp @@ -7,9 +7,16 @@ namespace GuestStacks { +struct Bounds { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + bool Contains(std::uintptr_t address) const { return address >= low && address < high; } +}; + extern "C" { void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes); +Bounds GuestStackBounds_nid_no_patch(std::uintptr_t stackPointer); void APS5_VABI GuestStackSwitch_nid_no_patch(std::uintptr_t stackPointer); } diff --git a/core/libs/prx/libc/include/JumpBuffer.hpp b/core/libs/prx/libc/include/JumpBuffer.hpp index a97b538fc09..c6dedc6e781 100644 --- a/core/libs/prx/libc/include/JumpBuffer.hpp +++ b/core/libs/prx/libc/include/JumpBuffer.hpp @@ -13,6 +13,7 @@ extern "C" { void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask); void GuestIoJumpRegister_nid_no_patch(StackRestore restore); +void APS5_VABI GuestIoStackRestore_nid_no_patch(std::uintptr_t stack); } diff --git a/core/libs/prx/libc/src/GuestStacks.cpp b/core/libs/prx/libc/src/GuestStacks.cpp index 9b24019938c..b76dac4a4dc 100644 --- a/core/libs/prx/libc/src/GuestStacks.cpp +++ b/core/libs/prx/libc/src/GuestStacks.cpp @@ -1,16 +1,29 @@ #include "prx/libc/include/GuestStacks.hpp" -#ifdef _WIN32 -#include "prx/libc/include/GuestArena.hpp" #include #include #include +#ifdef _WIN32 +#include "prx/libc/include/GuestArena.hpp" #include +#else +#include #endif namespace GuestStacks { namespace { +std::mutex rangesLock; +std::map ranges; + +Bounds RegisteredRange(std::uintptr_t pointer) { + std::lock_guard lock(rangesLock); + const auto next = ranges.upper_bound(pointer); + if (next == ranges.begin()) return {}; + const auto& [low, high] = *std::prev(next); + return pointer < high ? Bounds{low, high} : Bounds{}; +} + #ifdef _WIN32 struct StackBounds { std::uintptr_t base; @@ -20,9 +33,6 @@ struct StackBounds { constexpr std::size_t DeallocationStackOffset = 0x1478; -std::mutex rangesLock; -std::map ranges; - StackBounds CurrentBounds() { auto* tib = reinterpret_cast(NtCurrentTeb()); auto* teb = reinterpret_cast(tib); @@ -39,12 +49,9 @@ void ApplyBounds(const StackBounds& bounds) { } bool RegisteredBounds(std::uintptr_t stackPointer, StackBounds& bounds) { - std::lock_guard lock(rangesLock); - const auto next = ranges.upper_bound(stackPointer); - if (next == ranges.begin()) return false; - const auto& [low, high] = *std::prev(next); - if (stackPointer >= high) return false; - bounds = {high, low, low}; + const auto range = RegisteredRange(stackPointer); + if (!range.Contains(stackPointer)) return false; + bounds = {range.high, range.low, range.low}; return true; } @@ -60,7 +67,6 @@ bool HostBounds(std::uintptr_t stackPointer, StackBounds& bounds) { } void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes) { -#ifdef _WIN32 const auto low = reinterpret_cast(pointer); const auto high = low + bytes; std::lock_guard lock(rangesLock); @@ -69,14 +75,36 @@ void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes) { auto overlap = ranges.upper_bound(low); if (overlap != ranges.begin() && std::prev(overlap)->second > low) --overlap; while (overlap != ranges.end() && overlap->first < high) { +#ifdef _WIN32 GuestArena::GuestArenaUnpinWritable_nid_postfix(reinterpret_cast(overlap->first), overlap->second - overlap->first); +#endif overlap = ranges.erase(overlap); } +#ifdef _WIN32 GuestArena::GuestArenaPinWritable_nid_postfix(pointer, bytes); +#endif ranges.emplace(low, high); +} + +Bounds GuestStackBounds_nid_no_patch(std::uintptr_t stackPointer) { + const auto registered = RegisteredRange(stackPointer); + if (registered.Contains(stackPointer)) return registered; +#ifdef _WIN32 + const auto native = CurrentBounds(); + return {native.deallocation, native.base}; +#elif defined(__APPLE__) + const auto high = reinterpret_cast(pthread_get_stackaddr_np(pthread_self())); + return {high - pthread_get_stacksize_np(pthread_self()), high}; #else - (void)pointer; - (void)bytes; + pthread_attr_t attributes; + if (pthread_getattr_np(pthread_self(), &attributes) != 0) return {}; + void* pointer = nullptr; + std::size_t bytes = 0; + const int result = pthread_attr_getstack(&attributes, &pointer, &bytes); + pthread_attr_destroy(&attributes); + if (result != 0) return {}; + const auto low = reinterpret_cast(pointer); + return {low, low + bytes}; #endif } diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index c9a00a1e1ae..4aab6cd0e75 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -27,6 +27,10 @@ void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask) { void GuestIoJumpRegister_nid_no_patch(StackRestore restore) { ioStackRestore.store(restore); } +void APS5_VABI GuestIoStackRestore_nid_no_patch(std::uintptr_t stack) { + if (const auto restore = ioStackRestore.load()) restore(stack); +} + 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; @@ -36,7 +40,7 @@ void APS5_VABI JumpBufferSaveMask_nid_no_patch(unsigned char* buffer) { void APS5_VABI JumpBufferRestoreMask_nid_no_patch(const unsigned char* buffer) { std::uintptr_t stack = 0; std::memcpy(&stack, buffer + 16, sizeof(stack)); - if (const auto restore = ioStackRestore.load()) restore(stack); + GuestIoStackRestore_nid_no_patch(stack); std::int32_t saved = 0; std::memcpy(&saved, buffer + MaskSavedOffset, sizeof(saved)); const auto sigprocmask = guestSigprocmask.load(); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index c9f478a8cce..42687971f01 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -105,6 +105,18 @@ 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) + foreach(ioContext adopted adopted_exit swap jump sigjump) + 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(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}) diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index 10d8b066fef..76b5ec0f9bd 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -66,7 +66,7 @@ void RetireIoStack(std::uintptr_t stack); class IoPoint { public: explicit IoPoint(IoRestart& restart); - ~IoPoint(); + ~IoPoint() noexcept(false); IoPoint(const IoPoint&) = delete; IoPoint& operator=(const IoPoint&) = delete; diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp index 7491a79b2e8..e9195ca0986 100644 --- a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/DescriptorAccess.hpp" #include "prx/libc/include/JumpBuffer.hpp" +#include "prx/libc/include/GuestStacks.hpp" #include #include @@ -34,10 +35,13 @@ struct IoRestart::State { }; State* previous; std::uintptr_t stack; + GuestStacks::Bounds bounds; bool allowed; bool ready = false; std::vector expected; - State(State* previous, std::uintptr_t stack, bool allowed) : previous(previous), stack(stack), allowed(allowed) {} + State(State* previous, std::uintptr_t stack, bool allowed) + : previous(previous), stack(stack), bounds(GuestStacks::GuestStackBounds_nid_no_patch(stack)), allowed(allowed) {} + bool Abandoned(std::uintptr_t target) const { return bounds.Contains(target) && stack < target; } }; namespace { @@ -85,6 +89,8 @@ struct SignalState { 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(); } @@ -210,9 +216,11 @@ IoRestart::~IoRestart() { const int saved = errno; const SetupBlock blocked; auto** cursor = &restarts; - while (*cursor && *cursor != state) cursor = &(*cursor)->previous; - if (*cursor) *cursor = state->previous; - delete state; + while (*cursor && (*cursor != state || (*cursor)->stack != reinterpret_cast(this))) cursor = &(*cursor)->previous; + if (*cursor) { + *cursor = state->previous; + delete state; + } errno = saved; } @@ -290,7 +298,9 @@ void EndIoSignals(std::optional& deferral) { 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(); - delivering = std::move(state->previousDelivery); + auto* cursor = &delivering; + while (*cursor && cursor->get() != state) cursor = &(*cursor)->previousDelivery; + if (*cursor) *cursor = std::move(state->previousDelivery); } errno = saved; } @@ -345,17 +355,50 @@ void RetireIoStack(std::uintptr_t stack) { { const SetupBlock blocked; const bool exiting = stack == std::numeric_limits::max(); - while (delivering && delivering->replay->stack < stack) { - signals |= delivering->signals.load(); - if (const auto next = exiting ? delivering->retire : delivering->callback) callback = next; + auto* deliveryCursor = &delivering; +#ifndef _WIN32 + const auto here = reinterpret_cast(&blocked); + bool savedCurrent = false; +#endif + while (*deliveryCursor) { + auto& delivery = **deliveryCursor; +#ifndef _WIN32 + if (!exiting && !savedCurrent && delivery.replay->bounds.Contains(here) && here < delivery.replay->stack) { + delivery.suspendedMask = blocked.Previous(); + delivery.suspended = true; + savedCurrent = true; + mask = delivery.deliveryMask; + } +#endif + if (!exiting && !delivery.replay->Abandoned(stack)) { + deliveryCursor = &delivery.previousDelivery; + continue; + } + signals |= delivery.signals.load(); + if (const auto next = exiting ? delivery.retire : delivery.callback) callback = next; #ifndef _WIN32 - if (!exiting) mask = delivering->deliveryMask; + if (!exiting) mask = delivery.deliveryMask; #endif - delivering = std::move(delivering->previousDelivery); + *deliveryCursor = std::move(delivery.previousDelivery); } - while (restarts && restarts->stack < stack) { - auto* retired = restarts; - restarts = retired->previous; +#ifndef _WIN32 + if (!exiting) { + for (auto delivery = delivering; delivery; delivery = delivery->previousDelivery) { + if (delivery->suspended && delivery->replay->bounds.Contains(stack) && stack < delivery->replay->stack) { + mask = delivery->suspendedMask; + break; + } + } + } +#endif + auto** restartCursor = &restarts; + while (*restartCursor) { + if (!exiting && !(*restartCursor)->Abandoned(stack)) { + restartCursor = &(*restartCursor)->previous; + continue; + } + auto* retired = *restartCursor; + *restartCursor = retired->previous; delete retired; } if (exiting) signals = 0; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 1770d970d04..1575c939d66 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -779,7 +779,7 @@ ThreadCancel::IoPoint::IoPoint(IoRestart& restart) { BeginIoSignals(CurrentGuestState().current, restart, delivery); ++ioPoints; } -ThreadCancel::IoPoint::~IoPoint() { --ioPoints; EndIoSignals(delivery); } +ThreadCancel::IoPoint::~IoPoint() noexcept(false) { --ioPoints; EndIoSignals(delivery); } bool ThreadCancel::IoActive() { return ioPoints != 0; } bool ThreadCancel::IoRestartError() { return *__error_nid_postfix() == 4; } diff --git a/core/libs/prx/libkernel/System/src/SignalSets.cpp b/core/libs/prx/libkernel/System/src/SignalSets.cpp index 8444422f5dd..8096735354a 100644 --- a/core/libs/prx/libkernel/System/src/SignalSets.cpp +++ b/core/libs/prx/libkernel/System/src/SignalSets.cpp @@ -92,6 +92,14 @@ asm(".text\n" " xorl %eax, %eax\n" " ret\n" APS5_SIGNAL_ASM_FUNCTION("siglongjmp_nid_postfix") + " pushq %rdi\n" + " pushq %rsi\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" " cmpl $0, 88(%rdi)\n" " jz 2f\n" " movq %rdi, %rdx\n" diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 0d3d7f5937c..c18f42318fa 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -223,6 +223,7 @@ int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { std::memcpy(&mxcsr, frame.fxsave + FxsaveMxcsr, sizeof(mxcsr)); mxcsr &= HostMxcsrMask(); std::memcpy(frame.fxsave + FxsaveMxcsr, &mxcsr, sizeof(mxcsr)); + JumpBuffer::GuestIoStackRestore_nid_no_patch(machine.rsp); sigprocmask_nid_postfix(GuestSigSetmask, context->sigmask, nullptr); 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..417a48cc1f1 --- /dev/null +++ b/core/libs/tests/GuestIoContext.cpp @@ -0,0 +1,215 @@ +#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 +#ifndef _WIN32 +#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]{}; + 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 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) { + Require(swapcontext_nid_postfix(&from.context, &to.context) == 0, "swap I/O context"); + return; + } + from.started = true; + if (state->signalJumps) { + if (sigsetjmp_nid_postfix(from.jump, 1) == 0) { + if (to.started) siglongjmp_nid_postfix(to.jump, 1); + setcontext_nid_postfix(&to.context); + Require(false, "signal context restore returned"); + } + return; + } + if (setjmp_nid_postfix(from.jump) == 0) { + if (to.started) longjmp_nid_postfix(to.jump, 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; +} + +static void APS5_VABI FibreBody() { + auto& fibre = *state->active; + char byte = 0; + const auto result = read_nid_postfix(fibre.descriptors[0], &byte, 1); + Require(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; + 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); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected adopted, adopted_exit, swap, jump or sigjump"); + State reading; + state = &reading; + 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; + } + reading.signalJumps = std::strcmp(argv[1], "sigjump") == 0; + reading.jumps = reading.signalJumps || std::strcmp(argv[1], "jump") == 0; + 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/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0dac44303ac..63a35a5c9f3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -191,14 +191,14 @@ - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [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 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 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 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) 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 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 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) 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 +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs a queued delivery (an APC) in the first alertable wait of libkernel's TimedWait the target enters, or where libkernel drains APCs after releasing its address-wait or park queue lock (both are taken under a delivery deferral). Host code that holds a lock of its own across such a wait keeps it held while a waiting handler runs +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows throws for a target thread that was created suspended and is not resumed yet. 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 From 5cfba81590f75f2334ba553ae271f77d17f37410 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 07:18:36 -0300 Subject: [PATCH 15/61] fix(libkernel): identify suspended I/O contexts without stack registries --- .../prx/libSceLibcInternal/UserContext.cpp | 4 + core/libs/prx/libc/include/GuestStacks.hpp | 7 -- core/libs/prx/libc/include/JumpBuffer.hpp | 20 ++++- core/libs/prx/libc/src/GuestStacks.cpp | 56 ++++--------- core/libs/prx/libc/src/JumpBuffer.cpp | 52 +++++++++++- core/libs/prx/libkernel/CMakeLists.txt | 12 ++- core/libs/prx/libkernel/File/src/Stdio.cpp | 9 ++ .../prx/libkernel/Pthread/include/Cancel.hpp | 2 +- .../prx/libkernel/Pthread/src/IoSignals.cpp | 19 +++-- .../prx/libkernel/System/src/SignalSets.cpp | 24 ++++-- .../prx/libkernel/System/src/UserContext.cpp | 6 +- core/libs/tests/GuestIoContext.cpp | 84 +++++++++++++++++-- core/libs/tests/GuestIoDescriptorAccess.cpp | 24 ++++++ core/libs/tests/GuestUserContext.cpp | 1 - docs/dev/debt/libc.md | 2 +- docs/dev/debt/libkernel.md | 4 +- 16 files changed, 240 insertions(+), 86 deletions(-) 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/libc/include/GuestStacks.hpp b/core/libs/prx/libc/include/GuestStacks.hpp index f8a280bcb15..2b729a9bd57 100644 --- a/core/libs/prx/libc/include/GuestStacks.hpp +++ b/core/libs/prx/libc/include/GuestStacks.hpp @@ -7,16 +7,9 @@ namespace GuestStacks { -struct Bounds { - std::uintptr_t low = 0; - std::uintptr_t high = 0; - bool Contains(std::uintptr_t address) const { return address >= low && address < high; } -}; - extern "C" { void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes); -Bounds GuestStackBounds_nid_no_patch(std::uintptr_t stackPointer); void APS5_VABI GuestStackSwitch_nid_no_patch(std::uintptr_t stackPointer); } diff --git a/core/libs/prx/libc/include/JumpBuffer.hpp b/core/libs/prx/libc/include/JumpBuffer.hpp index c6dedc6e781..fb14ae729f7 100644 --- a/core/libs/prx/libc/include/JumpBuffer.hpp +++ b/core/libs/prx/libc/include/JumpBuffer.hpp @@ -7,13 +7,29 @@ namespace JumpBuffer { using Sigprocmask = int (APS5_VABI*)(int how, const void* set, void* previousSet); -using StackRestore = void (*)(std::uintptr_t stack); +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); -void APS5_VABI GuestIoStackRestore_nid_no_patch(std::uintptr_t stack); +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/GuestStacks.cpp b/core/libs/prx/libc/src/GuestStacks.cpp index b76dac4a4dc..9b24019938c 100644 --- a/core/libs/prx/libc/src/GuestStacks.cpp +++ b/core/libs/prx/libc/src/GuestStacks.cpp @@ -1,29 +1,16 @@ #include "prx/libc/include/GuestStacks.hpp" +#ifdef _WIN32 +#include "prx/libc/include/GuestArena.hpp" #include #include #include -#ifdef _WIN32 -#include "prx/libc/include/GuestArena.hpp" #include -#else -#include #endif namespace GuestStacks { namespace { -std::mutex rangesLock; -std::map ranges; - -Bounds RegisteredRange(std::uintptr_t pointer) { - std::lock_guard lock(rangesLock); - const auto next = ranges.upper_bound(pointer); - if (next == ranges.begin()) return {}; - const auto& [low, high] = *std::prev(next); - return pointer < high ? Bounds{low, high} : Bounds{}; -} - #ifdef _WIN32 struct StackBounds { std::uintptr_t base; @@ -33,6 +20,9 @@ struct StackBounds { constexpr std::size_t DeallocationStackOffset = 0x1478; +std::mutex rangesLock; +std::map ranges; + StackBounds CurrentBounds() { auto* tib = reinterpret_cast(NtCurrentTeb()); auto* teb = reinterpret_cast(tib); @@ -49,9 +39,12 @@ void ApplyBounds(const StackBounds& bounds) { } bool RegisteredBounds(std::uintptr_t stackPointer, StackBounds& bounds) { - const auto range = RegisteredRange(stackPointer); - if (!range.Contains(stackPointer)) return false; - bounds = {range.high, range.low, range.low}; + std::lock_guard lock(rangesLock); + const auto next = ranges.upper_bound(stackPointer); + if (next == ranges.begin()) return false; + const auto& [low, high] = *std::prev(next); + if (stackPointer >= high) return false; + bounds = {high, low, low}; return true; } @@ -67,6 +60,7 @@ bool HostBounds(std::uintptr_t stackPointer, StackBounds& bounds) { } void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes) { +#ifdef _WIN32 const auto low = reinterpret_cast(pointer); const auto high = low + bytes; std::lock_guard lock(rangesLock); @@ -75,36 +69,14 @@ void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes) { auto overlap = ranges.upper_bound(low); if (overlap != ranges.begin() && std::prev(overlap)->second > low) --overlap; while (overlap != ranges.end() && overlap->first < high) { -#ifdef _WIN32 GuestArena::GuestArenaUnpinWritable_nid_postfix(reinterpret_cast(overlap->first), overlap->second - overlap->first); -#endif overlap = ranges.erase(overlap); } -#ifdef _WIN32 GuestArena::GuestArenaPinWritable_nid_postfix(pointer, bytes); -#endif ranges.emplace(low, high); -} - -Bounds GuestStackBounds_nid_no_patch(std::uintptr_t stackPointer) { - const auto registered = RegisteredRange(stackPointer); - if (registered.Contains(stackPointer)) return registered; -#ifdef _WIN32 - const auto native = CurrentBounds(); - return {native.deallocation, native.base}; -#elif defined(__APPLE__) - const auto high = reinterpret_cast(pthread_get_stackaddr_np(pthread_self())); - return {high - pthread_get_stacksize_np(pthread_self()), high}; #else - pthread_attr_t attributes; - if (pthread_getattr_np(pthread_self(), &attributes) != 0) return {}; - void* pointer = nullptr; - std::size_t bytes = 0; - const int result = pthread_attr_getstack(&attributes, &pointer, &bytes); - pthread_attr_destroy(&attributes); - if (result != 0) return {}; - const auto low = reinterpret_cast(pointer); - return {low, low + bytes}; + (void)pointer; + (void)bytes; #endif } diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index 4aab6cd0e75..02b3d14d06c 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,9 +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; + } +} } @@ -27,24 +41,56 @@ void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask) { void GuestIoJumpRegister_nid_no_patch(StackRestore restore) { ioStackRestore.store(restore); } -void APS5_VABI GuestIoStackRestore_nid_no_patch(std::uintptr_t stack) { - if (const auto restore = ioStackRestore.load()) restore(stack); +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)); - GuestIoStackRestore_nid_no_patch(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); } } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 42687971f01..9096f404292 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -109,7 +109,7 @@ if(BUILD_TESTING) 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) - foreach(ioContext adopted adopted_exit swap jump sigjump) + foreach(ioContext 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) @@ -117,6 +117,10 @@ if(BUILD_TESTING) "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}) @@ -170,6 +174,12 @@ if(BUILD_TESTING) 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) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 8302ef671c8..269a6ae1e23 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -940,6 +940,15 @@ 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(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(); diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index 76b5ec0f9bd..b1dcf604668 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -61,7 +61,7 @@ bool DeferIoCallback(void (*callback)(), void (*retire)()); void WakeIoDelivery(Pthread thread); bool TrackIoDescriptor(int descriptor); void IoNoRestart(); -void RetireIoStack(std::uintptr_t stack); +void RetireIoStack(std::uintptr_t stack, std::uint32_t context = 0); class IoPoint { public: diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp index e9195ca0986..acdeb5bdceb 100644 --- a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -1,7 +1,6 @@ #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/DescriptorAccess.hpp" #include "prx/libc/include/JumpBuffer.hpp" -#include "prx/libc/include/GuestStacks.hpp" #include #include @@ -35,13 +34,13 @@ struct IoRestart::State { }; State* previous; std::uintptr_t stack; - GuestStacks::Bounds bounds; + 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), bounds(GuestStacks::GuestStackBounds_nid_no_patch(stack)), allowed(allowed) {} - bool Abandoned(std::uintptr_t target) const { return bounds.Contains(target) && stack < target; } + : 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 { @@ -346,7 +345,7 @@ bool TrackIoDescriptor(int descriptor) { return true; } -void RetireIoStack(std::uintptr_t stack) { +void RetireIoStack(std::uintptr_t stack, std::uint32_t context) { std::uint64_t signals = 0; void (*callback)() = nullptr; #ifndef _WIN32 @@ -355,6 +354,8 @@ void RetireIoStack(std::uintptr_t stack) { { const SetupBlock blocked; const bool exiting = stack == std::numeric_limits::max(); + const auto previousContext = JumpBuffer::GuestContextCurrent_nid_no_patch(); + if (!exiting) JumpBuffer::GuestContextSet_nid_no_patch(context); auto* deliveryCursor = &delivering; #ifndef _WIN32 const auto here = reinterpret_cast(&blocked); @@ -363,14 +364,14 @@ void RetireIoStack(std::uintptr_t stack) { while (*deliveryCursor) { auto& delivery = **deliveryCursor; #ifndef _WIN32 - if (!exiting && !savedCurrent && delivery.replay->bounds.Contains(here) && here < delivery.replay->stack) { + 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)) { + if (!exiting && !delivery.replay->Abandoned(stack, context)) { deliveryCursor = &delivery.previousDelivery; continue; } @@ -384,7 +385,7 @@ void RetireIoStack(std::uintptr_t stack) { #ifndef _WIN32 if (!exiting) { for (auto delivery = delivering; delivery; delivery = delivery->previousDelivery) { - if (delivery->suspended && delivery->replay->bounds.Contains(stack) && stack < delivery->replay->stack) { + if (delivery->suspended && delivery->replay->context == context && stack < delivery->replay->stack) { mask = delivery->suspendedMask; break; } @@ -393,7 +394,7 @@ void RetireIoStack(std::uintptr_t stack) { #endif auto** restartCursor = &restarts; while (*restartCursor) { - if (!exiting && !(*restartCursor)->Abandoned(stack)) { + if (!exiting && !(*restartCursor)->Abandoned(stack, context)) { restartCursor = &(*restartCursor)->previous; continue; } diff --git a/core/libs/prx/libkernel/System/src/SignalSets.cpp b/core/libs/prx/libkernel/System/src/SignalSets.cpp index 8096735354a..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" @@ -92,14 +99,6 @@ asm(".text\n" " xorl %eax, %eax\n" " ret\n" APS5_SIGNAL_ASM_FUNCTION("siglongjmp_nid_postfix") - " pushq %rdi\n" - " pushq %rsi\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" " cmpl $0, 88(%rdi)\n" " jz 2f\n" " movq %rdi, %rdx\n" @@ -114,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/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index c18f42318fa..7cfe32ea720 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -190,6 +190,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)); std::memcpy(context, &captured, offsetof(UserUcontext, link)); return 0; @@ -223,8 +225,10 @@ int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { std::memcpy(&mxcsr, frame.fxsave + FxsaveMxcsr, sizeof(mxcsr)); mxcsr &= HostMxcsrMask(); std::memcpy(frame.fxsave + FxsaveMxcsr, &mxcsr, sizeof(mxcsr)); - JumpBuffer::GuestIoStackRestore_nid_no_patch(machine.rsp); 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 index 417a48cc1f1..602777dee93 100644 --- a/core/libs/tests/GuestIoContext.cpp +++ b/core/libs/tests/GuestIoContext.cpp @@ -12,10 +12,14 @@ #include #include #include +#include +#include +#include #include #ifndef _WIN32 #include #include +#include #endif static void Require(bool condition, const char* message) { @@ -37,6 +41,7 @@ static bool Observe(TPredicate predicate) { 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; @@ -50,6 +55,9 @@ struct State { Fibre* active = nullptr; bool jumps = false; bool signalJumps = false; + bool manual = false; + bool leave = false; + bool signalLeave = false; bool exitHandler = false; std::atomic phase{0}; std::atomic cleanups{0}; @@ -75,20 +83,29 @@ static void CheckMask(const sigset_t& before) { static void Switch(Fibre& from, Fibre& to) { if (!state->jumps) { - Require(swapcontext_nid_postfix(&from.context, &to.context) == 0, "swap I/O context"); + 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) siglongjmp_nid_postfix(to.jump, 1); + 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 (setjmp_nid_postfix(from.jump) == 0) { - if (to.started) longjmp_nid_postfix(to.jump, 1); + 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"); } @@ -107,13 +124,22 @@ static void APS5_VABI Handler(int, void*) { 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; - const auto result = read_nid_postfix(fibre.descriptors[0], &byte, 1); - Require(result == -1 && *__error_nid_postfix() == 4 && fibre.handled, "suspended handler did not resume safely"); + 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"); @@ -130,7 +156,13 @@ static void* APS5_VABI Scheduler(void*) { Require(getcontext_nid_postfix(&fibre.context) == 0, "capture fibre template"); fibre.context.stackPointer = fibre.stack.get(); fibre.context.stackSize = StackBytes; - makecontext_nid_postfix(&fibre.context, FibreBody, 0); + 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); @@ -158,10 +190,42 @@ static void APS5_VABI Destroy(void*) { 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 + int main(int argc, char** argv) { Require(argc == 2, "expected adopted, adopted_exit, swap, jump or sigjump"); State reading; state = &reading; +#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"); @@ -193,8 +257,12 @@ int main(int argc, char** argv) { if (reading.exitHandler) Require(sceKernelRemoveExceptionHandler(30) == 0, "remove adopted exit handler"); return 0; } - reading.signalJumps = std::strcmp(argv[1], "sigjump") == 0; - reading.jumps = reading.signalJumps || std::strcmp(argv[1], "jump") == 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.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; diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index d94f44da1f1..4e1cf9431e7 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -191,6 +191,29 @@ static bool Registered(Pthread 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"); @@ -539,6 +562,7 @@ int main(int argc, char** argv) { 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__ diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index 46fb68c1d53..7bae91bf9a0 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -282,7 +282,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/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c3a55642702..876a5b282c9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -21,7 +21,7 @@ - [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` - [_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. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow a payload process on a PS5 (firmware 13.60): the rounding modes are x87 control bits 10-11 (0, 0x400, 0x800, 0xc00), `fesetround` sets them and the MXCSR rounding bits and returns -1 for any other value, `fegetround` reads the x87 bits, and the exception flags are 1, 4, 8, 0x10 and 0x20 with a 2-byte `fexcept_t`. `fetestexcept` returns the x87 status word and MXCSR together, masked only by its argument, so a mask past 0x3f also returns their other bits. They access the current host thread's x87 and MXCSR state, which starts with flush-to-zero and denormals-are-zero set on the console. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. Null flag pointers throw instead of faulting. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 63a35a5c9f3..13b44d06254 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -184,7 +184,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`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call -- [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) +- [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`). The in-process opaque machine context uses 16 bytes of `mc_spare` for a context identity and saved stack pointer; it is retained by context copies and replaced when a context is manually retargeted to another stack. This metadata has no console evidence - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` @@ -205,7 +205,7 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. - [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation. Concurrent close or replacement waits for active POSIX descriptor reads and receives; cancellation can interrupt a read waiting for descriptor access. -- [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 longjmp retires abandoned delivery/restart state and restores its host signal mask. +- [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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From 4e5cd174295bccec02a7d2c8d1c057ca74c4ace2 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 07:39:48 -0300 Subject: [PATCH 16/61] test(libkernel): verify restarting handlers that change errno --- core/libs/prx/libkernel/CMakeLists.txt | 4 +++- core/libs/tests/GuestIoSignalClose.cpp | 5 ++++- core/libs/tests/GuestSignalSets.cpp | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9096f404292..85395dcbd68 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -138,6 +138,8 @@ if(BUILD_TESTING) 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}) @@ -160,7 +162,7 @@ if(BUILD_TESTING) endif() endif() if(WIN32) - set_tests_properties(${ioSignalTest} ${ioSignalTest}_resume PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(${ioSignalTest} ${ioSignalTest}_resume ${ioSignalTest}_resume_errno PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() diff --git a/core/libs/tests/GuestIoSignalClose.cpp b/core/libs/tests/GuestIoSignalClose.cpp index 72a2ec19953..2ccae928287 100644 --- a/core/libs/tests/GuestIoSignalClose.cpp +++ b/core/libs/tests/GuestIoSignalClose.cpp @@ -46,6 +46,7 @@ struct State { 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{}; @@ -71,6 +72,7 @@ static void APS5_VABI SignalHandler(int) { 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); @@ -134,7 +136,8 @@ static void RunCase(int argc, char** argv) { 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; - const bool resume = starting || (argc == 4 && std::strcmp(argv[3], "resume") == 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; 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); From 0403250579011f2870e5e72a57b1ceaf2aca6492 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 08:54:15 -0300 Subject: [PATCH 17/61] fix(libkernel): wake admitted POSIX reads before closing --- .../prx/libc/include/DescriptorAccess.hpp | 33 +++- core/libs/prx/libc/src/DescriptorAccess.cpp | 65 ++++++- core/libs/prx/libkernel/CMakeLists.txt | 30 ++++ core/libs/prx/libkernel/File/include/File.hpp | 1 + core/libs/prx/libkernel/File/src/Stdio.cpp | 10 +- .../prx/libkernel/Pthread/include/Cancel.hpp | 2 + .../prx/libkernel/Pthread/src/IoSignals.cpp | 1 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 3 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- core/libs/tests/GuestIoDescriptorAccess.cpp | 158 +++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 11 files changed, 291 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libc/include/DescriptorAccess.hpp b/core/libs/prx/libc/include/DescriptorAccess.hpp index 43710455e53..e13c6e1f271 100644 --- a/core/libs/prx/libc/include/DescriptorAccess.hpp +++ b/core/libs/prx/libc/include/DescriptorAccess.hpp @@ -4,6 +4,7 @@ #include #include #include +#include class GuestDescriptorAccess { struct State; @@ -17,7 +18,37 @@ class GuestDescriptorAccess { std::shared_ptr state; std::uint64_t value; }; - enum class Mode { Read, Change, Update }; + enum class Mode { Read, Change, Update, Close }; + class Operation { + public: + explicit Operation(GuestDescriptorAccess& access); + ~Operation(); + Operation(const Operation&) = delete; + Operation& operator=(const Operation&) = delete; + bool Interrupted() const { return interrupted.load(); } + static bool CurrentInterrupted(); + class WakeRegistration { + public: + WakeRegistration(void (*interrupt)(void*) noexcept, void* argument); + ~WakeRegistration(); + WakeRegistration(const WakeRegistration&) = delete; + WakeRegistration& operator=(const WakeRegistration&) = delete; + private: + friend class Operation; + WakeRegistration* previous = nullptr; + Operation* operation; + void (*interrupt)(void*) noexcept; + void* argument; + }; + private: + friend struct State; + static thread_local Operation* current; + std::shared_ptr state; + Operation* previous; + WakeRegistration* wake = nullptr; + std::atomic interrupted{false}; + void Interrupt() noexcept; + }; GuestDescriptorAccess(int descriptor, Mode mode); GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t); ~GuestDescriptorAccess(); diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp index b4061b26a8c..819f039cd56 100644 --- a/core/libs/prx/libc/src/DescriptorAccess.cpp +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include struct GuestDescriptorAccess::State { std::mutex mutex; @@ -14,6 +16,12 @@ struct GuestDescriptorAccess::State { 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); @@ -50,17 +58,25 @@ void GuestDescriptorAccess::Lock() { ++state->readers; } else { ++state->changesWaiting; + if (mode == Mode::Close) ++state->closesWaiting; try { - state->changed.wait(lock, [&] { return state->CanChange(); }); + if (mode == Mode::Close) { + while (!state->CanChange()) { + state->Interrupt(); + state->changed.wait_for(lock, std::chrono::milliseconds(1)); + } + } else state->changed.wait(lock, [&] { return state->CanChange(); }); } catch (...) { --state->changesWaiting; + if (mode == Mode::Close) --state->closesWaiting; state->changed.notify_all(); throw; } --state->changesWaiting; + if (mode == Mode::Close) --state->closesWaiting; state->writer = std::this_thread::get_id(); ++state->depth; - state->replacement = state->replacement || mode == Mode::Change; + state->replacement = state->replacement || mode == Mode::Change || mode == Mode::Close; } held = true; } @@ -75,7 +91,7 @@ bool GuestDescriptorAccess::TryLock() { if (!state->CanChange()) return false; state->writer = std::this_thread::get_id(); ++state->depth; - state->replacement = state->replacement || mode == Mode::Change; + state->replacement = state->replacement || mode == Mode::Change || mode == Mode::Close; } held = true; return true; @@ -116,7 +132,7 @@ GuestDescriptorAccess::Version::Version(std::shared_ptr state, std::uint6 bool GuestDescriptorAccess::Version::Unchanged() const { if (!state) return false; std::lock_guard lock(state->mutex); - return value != std::numeric_limits::max() && state->generation == value; + return value != std::numeric_limits::max() && state->generation == value && state->closesWaiting == 0; } GuestDescriptorAccess::Version GuestDescriptorAccess::Snapshot() const { @@ -134,3 +150,44 @@ void GuestDescriptorAccess::LockTogether(GuestDescriptorAccess& first, GuestDesc } firstLock{first}, secondLock{second}; std::lock(firstLock, secondLock); } + +thread_local GuestDescriptorAccess::Operation* GuestDescriptorAccess::Operation::current = nullptr; + +GuestDescriptorAccess::Operation::Operation(GuestDescriptorAccess& access) : state(access.state), previous(current) { + if (state) { + std::lock_guard lock(state->mutex); + state->operations.push_back(this); + interrupted.store(state->closesWaiting != 0); + } + current = this; +} + +GuestDescriptorAccess::Operation::~Operation() { + current = previous; + if (state) { + std::lock_guard lock(state->mutex); + std::erase(state->operations, this); + } +} + +bool GuestDescriptorAccess::Operation::CurrentInterrupted() { return current && current->Interrupted(); } + +void GuestDescriptorAccess::Operation::Interrupt() noexcept { + interrupted.store(true); + for (auto* registration = wake; registration; registration = registration->previous) + registration->interrupt(registration->argument); +} + +GuestDescriptorAccess::Operation::WakeRegistration::WakeRegistration(void (*interrupt)(void*) noexcept, void* argument) + : operation(Operation::current), interrupt(interrupt), argument(argument) { + if (!operation || !operation->state) { operation = nullptr; return; } + std::lock_guard lock(operation->state->mutex); + previous = operation->wake; + operation->wake = this; +} + +GuestDescriptorAccess::Operation::WakeRegistration::~WakeRegistration() { + if (!operation) return; + std::lock_guard lock(operation->state->mutex); + operation->wake = previous; +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 85395dcbd68..771a56d79fc 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -172,6 +172,34 @@ if(BUILD_TESTING) 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) + 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:$") + endif() 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) @@ -203,6 +231,8 @@ if(BUILD_TESTING) 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_read_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoReadCancel.cpp) diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index d4bdefef96c..5e61c3f4bff 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -45,6 +45,7 @@ class IoDescriptor { private: int value; GuestDescriptorAccess access; + std::optional operation; }; template diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 269a6ae1e23..61033b91c73 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -837,7 +837,7 @@ File::IoDescriptor::IoDescriptor(int descriptor) if (!DescriptorIsOpen_nid_no_patch(descriptor)) { value = -1; *__error_nid_postfix() = GUEST_EBADF; - } + } else operation.emplace(access); return; } ThreadCancel::IoWait wait([](void* argument) noexcept { @@ -858,19 +858,21 @@ File::IoDescriptor::IoDescriptor(int descriptor) if (!DescriptorIsOpen_nid_no_patch(descriptor)) { value = -1; *__error_nid_postfix() = GUEST_EBADF; - } + } else operation.emplace(access); return; } } } -File::IoDescriptor::~IoDescriptor() = default; +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::Change); + const GuestDescriptorAccess descriptorLock(d, GuestDescriptorAccess::Mode::Close); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index b1dcf604668..247dc37b505 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libc/include/DescriptorAccess.hpp" #include #include #include @@ -92,6 +93,7 @@ class IoWait { Pthread thread = nullptr; void* signalState = nullptr; IoWait* signalPrevious = nullptr; + GuestDescriptorAccess::Operation::WakeRegistration descriptorWake; }; class NativeIo { diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp index acdeb5bdceb..36e22b01686 100644 --- a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -305,6 +305,7 @@ void EndIoSignals(std::optional& deferral) { } bool IoInterrupted() { + if (GuestDescriptorAccess::Operation::CurrentInterrupted()) { IoNoRestart(); return true; } return (current && current->Pending()) || (IoEnabled() && Requested()); } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 1575c939d66..277c852a9c3 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -789,7 +789,8 @@ bool ThreadCancel::IoEnabled() { CurrentGuestState().cancelState == GUEST_CANCEL_ENABLE; } -ThreadCancel::IoWait::IoWait(Interrupt interrupt, void* argument) : interrupt(interrupt), argument(argument) { +ThreadCancel::IoWait::IoWait(Interrupt interrupt, void* argument) + : interrupt(interrupt), argument(argument), descriptorWake(interrupt, argument) { BeginSignal(); if (!IoEnabled()) return; thread = CurrentGuestState().current; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 3fd09e1aeff..8f0a95b69a3 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -404,7 +404,7 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l } int GuestSockets::Close(int descriptor) { - const GuestDescriptorAccess descriptorLock(descriptor, GuestDescriptorAccess::Mode::Change); + const GuestDescriptorAccess descriptorLock(descriptor, GuestDescriptorAccess::Mode::Close); std::shared_ptr released; { std::lock_guard lock(socketsMutex); diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index 4e1cf9431e7..2d2cfd8d452 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -1,5 +1,6 @@ #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" @@ -191,6 +192,143 @@ static bool Registered(Pthread reader) { }); } +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, bool direct = false) { + 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((direct ? GuestSockets::Close(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"); +} + +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"); @@ -504,7 +642,7 @@ static bool ChildBlocked(int descriptor) { return WEXITSTATUS(status) == 7; } -static void BorrowedNetClose() { +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}; @@ -522,7 +660,7 @@ static void BorrowedNetClose() { const bool registered = Registered(reader); std::atomic closed{false}; std::thread closer([&] { - Require(sceNetSocketClose(accepted) == 0, "close borrowed socket"); + Require((posix ? close_nid_postfix(accepted) : sceNetSocketClose(accepted)) == 0, "close borrowed socket"); closed.store(true); }); const bool prompt = Observe([&] { return closed.load(); }); @@ -530,7 +668,7 @@ static void BorrowedNetClose() { closer.join(); Require(Observe([&] { return reader->_finished.load(); }), "borrowed reader did not finish"); Require(scePthreadJoin(reader, nullptr) == 0, "join borrowed reader"); - Require(registered && prompt && read.result == 0, "SCE socket close did not wake the borrowed POSIX read"); + 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"); } @@ -557,7 +695,18 @@ static void RecordLocks() { #endif int main(int argc, char** argv) { - Require(argc == 2, "expected descriptor-access case"); + 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]) != 0); + 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], "capacity") == 0) Capacity(); else if (std::strcmp(argv[1], "no_aliases") == 0) NoAliases(); else if (std::strcmp(argv[1], "access_cancel") == 0) AccessCancellation(); @@ -578,6 +727,7 @@ int main(int argc, char** argv) { #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/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 13b44d06254..f0b188ced9d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -204,7 +204,7 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. -- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation. Concurrent close or replacement waits for active POSIX descriptor reads and receives; cancellation can interrupt a read waiting for descriptor access. +- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation and close indefinitely. POSIX `close` interrupts admitted POSIX reads and receives, including when cancellation is disabled, then waits for their native operations to drain before releasing the descriptor; a completed or partial result can win, otherwise the read returns `EINTR`. This differs from the reported console behavior where close returns while the read remains blocked; it has no console verification. Descriptor replacement and stdio closure can still wait indefinitely for active reads. Cancellation can interrupt a read waiting for descriptor access. - [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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From fdd4ba317c3b62dedbcf5b941dae8914030932b7 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 08:59:30 -0300 Subject: [PATCH 18/61] fix(libkernel): handle mixed close and pre-call interruption errors --- core/libs/prx/libkernel/CMakeLists.txt | 8 +++++ core/libs/prx/libkernel/File/src/Open.cpp | 2 +- .../prx/libkernel/Pthread/include/Cancel.hpp | 3 +- .../prx/libkernel/Pthread/src/NativeIo.cpp | 9 +++++ core/libs/tests/GuestIoDescriptorAccess.cpp | 33 +++++++++++++++++-- docs/dev/debt/libkernel.md | 2 +- 6 files changed, 51 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 771a56d79fc..d9e2c6bab13 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -173,6 +173,12 @@ if(BUILD_TESTING) 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}) @@ -199,6 +205,8 @@ if(BUILD_TESTING) 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() 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) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index c2e9b1901ea..9be9a517e75 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -378,7 +378,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::Change); + 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/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index 247dc37b505..aa58475bc8d 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -100,6 +100,7 @@ class NativeIo { public: NativeIo(); ~NativeIo(); + static void InterruptError(); NativeIo(const NativeIo&) = delete; NativeIo& operator=(const NativeIo&) = delete; @@ -112,7 +113,7 @@ template auto Native(TCall&& call) { NativeIo operation; if (IoInterrupted()) { - errno = EINTR; + NativeIo::InterruptError(); return static_cast(-1); } return std::forward(call)(); diff --git a/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp index 6f4746532d3..a512c9da436 100644 --- a/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp +++ b/core/libs/prx/libkernel/Pthread/src/NativeIo.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/Pthread/include/Cancel.hpp" #include +#include #include #include #include @@ -97,6 +98,14 @@ 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(); diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index 2d2cfd8d452..e7731a82f82 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -233,7 +233,7 @@ static void* APS5_VABI ReadUntilClose(void* argument) { return nullptr; } -static void PosixClose(bool socket, int api, bool disabled, bool direct = false) { +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]); @@ -263,7 +263,7 @@ static void PosixClose(bool socket, int api, bool disabled, bool direct = false) if (disabled) Require(pthread_cancel_nid_postfix(reader) == 0, "leave cancellation pending in close reader"); std::atomic closed{false}; std::thread closer([&] { - Require((direct ? GuestSockets::Close(descriptors[0]) : close_nid_postfix(descriptors[0])) == 0, "close active read descriptor"); + 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(); }); @@ -283,6 +283,32 @@ static void PosixClose(bool socket, int api, bool disabled, bool direct = false) 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"); @@ -698,7 +724,7 @@ 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]) != 0); + 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) { @@ -718,6 +744,7 @@ int main(int argc, char** argv) { 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(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f0b188ced9d..df3760432c0 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -204,7 +204,7 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. -- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation and close indefinitely. POSIX `close` interrupts admitted POSIX reads and receives, including when cancellation is disabled, then waits for their native operations to drain before releasing the descriptor; a completed or partial result can win, otherwise the read returns `EINTR`. This differs from the reported console behavior where close returns while the read remains blocked; it has no console verification. Descriptor replacement and stdio closure can still wait indefinitely for active reads. Cancellation can interrupt a read waiting for descriptor access. +- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation and close indefinitely. POSIX `close`, `sceKernelClose` and the guest-socket close backend interrupt admitted POSIX reads and receives, including when cancellation is disabled, then wait for their native operations to drain before releasing the descriptor; a completed or partial result can win, otherwise the read returns `EINTR`. This differs from the reported console behavior where close returns while the read remains blocked; it has no console verification. Descriptor replacement and stdio closure can still wait indefinitely for active reads. Cancellation can interrupt a read waiting for descriptor access. - [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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From bcf5df1c7756b848fce3c3baf16fe1eb07d36e96 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 09:03:52 -0300 Subject: [PATCH 19/61] docs(libkernel): clarify remaining close limitations concisely --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index df3760432c0..dad182199bd 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -204,7 +204,7 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. -- [POSIX read cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) has no console verification. It covers `read`, `_read`, `readv`, `_readv`, `pread`, `preadv`, `recv`, `recvfrom`, `recvmsg`, `_recvfrom` and `_recvmsg`, following FreeBSD libthr's entry check and failure-only exit check; SCE read and libSceNet cancellation behavior is unchanged. Unix native I/O requires host `SIGUSR2` to be unused and throws if a handler is installed. Native drivers that do not support interruption may delay cancellation and close indefinitely. POSIX `close`, `sceKernelClose` and the guest-socket close backend interrupt admitted POSIX reads and receives, including when cancellation is disabled, then wait for their native operations to drain before releasing the descriptor; a completed or partial result can win, otherwise the read returns `EINTR`. This differs from the reported console behavior where close returns while the read remains blocked; it has no console verification. Descriptor replacement and stdio closure can still wait indefinitely for active reads. Cancellation can interrupt a read waiting for descriptor access. +- [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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From eebaac5b2483ca7bbda3b3f87f1f165abb3dae35 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 13:57:43 -0300 Subject: [PATCH 20/61] fix(libkernel): retain I/O signal state through native thread teardown --- .../libs/prx/libc/include/HostThreadLocal.hpp | 30 +++- core/libs/prx/libkernel/CMakeLists.txt | 17 ++ .../prx/libkernel/Pthread/src/IoSignals.cpp | 35 ++-- core/libs/tests/GuestIoNativeThreads.cpp | 152 ++++++++++++++++++ core/libs/tests/HostThreadLocalLibrary.cpp | 7 + 5 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 core/libs/tests/GuestIoNativeThreads.cpp 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/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 85d96a67d46..bcbab2e9570 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -244,6 +244,23 @@ if(BUILD_TESTING) 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) diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp index 36e22b01686..2908ba7de7b 100644 --- a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -1,6 +1,7 @@ #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 @@ -97,8 +98,11 @@ struct SignalState { static_assert(std::atomic::is_always_lock_free); constinit thread_local SignalState* current = nullptr; -thread_local std::shared_ptr owner; -thread_local std::shared_ptr delivering; +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; @@ -240,6 +244,7 @@ void BeginIoSignals(Pthread thread, IoRestart& restart, std::optional(); state->manager = &manager; @@ -251,8 +256,8 @@ void BeginIoSignals(Pthread thread, IoRestart& restart, std::optional& deferral) { bool pending; { const SetupBlock blocked; - auto finishing = std::move(owner); + auto finishing = std::move(Ownership().owner); state = finishing.get(); #ifndef _WIN32 state->deliveryMask = blocked.Previous(); @@ -281,8 +286,8 @@ void EndIoSignals(std::optional& deferral) { } current = nullptr; pending = state->Pending(); - state->previousDelivery = std::move(delivering); - delivering = std::move(finishing); + state->previousDelivery = std::move(Ownership().delivering); + Ownership().delivering = std::move(finishing); deferral.reset(); } for (int signal = 1; signal < 64; ++signal) @@ -297,7 +302,7 @@ void EndIoSignals(std::optional& deferral) { 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 = &delivering; + auto* cursor = &Ownership().delivering; while (*cursor && cursor->get() != state) cursor = &(*cursor)->previousDelivery; if (*cursor) *cursor = std::move(state->previousDelivery); } @@ -355,9 +360,19 @@ void RetireIoStack(std::uintptr_t stack, std::uint32_t context) { { 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); - auto* deliveryCursor = &delivering; +#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; @@ -385,7 +400,7 @@ void RetireIoStack(std::uintptr_t stack, std::uint32_t context) { } #ifndef _WIN32 if (!exiting) { - for (auto delivery = delivering; delivery; delivery = delivery->previousDelivery) { + for (auto delivery = ownership->delivering; delivery; delivery = delivery->previousDelivery) { if (delivery->suspended && delivery->replay->context == context && stack < delivery->replay->stack) { mask = delivery->suspendedMask; break; 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/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(); } From 45272246838fa21709c48de50e2208f8820e9019 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 14:26:09 -0300 Subject: [PATCH 21/61] fix(libc): preserve descriptor coordination across patched libraries --- .../prx/libc/include/DescriptorAccess.hpp | 88 ++++++-- core/libs/prx/libc/src/DescriptorAccess.cpp | 204 +++++++++--------- 2 files changed, 172 insertions(+), 120 deletions(-) diff --git a/core/libs/prx/libc/include/DescriptorAccess.hpp b/core/libs/prx/libc/include/DescriptorAccess.hpp index e13c6e1f271..693c990f6fb 100644 --- a/core/libs/prx/libc/include/DescriptorAccess.hpp +++ b/core/libs/prx/libc/include/DescriptorAccess.hpp @@ -6,63 +6,111 @@ #include #include +extern "C" { +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; + bool Unchanged() const { return DescriptorVersionUnchanged_nid_no_patch(this); } private: friend class GuestDescriptorAccess; - Version(std::shared_ptr state, std::uint64_t value); + 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; + std::uint64_t value = 0; }; enum class Mode { Read, Change, Update, Close }; class Operation { public: - explicit Operation(GuestDescriptorAccess& access); - ~Operation(); + 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(); + static bool CurrentInterrupted() { return DescriptorOperationInterrupted_nid_no_patch(); } class WakeRegistration { public: - WakeRegistration(void (*interrupt)(void*) noexcept, void* argument); - ~WakeRegistration(); + 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; + 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; + Operation* previous = nullptr; WakeRegistration* wake = nullptr; std::atomic interrupted{false}; void Interrupt() noexcept; }; - GuestDescriptorAccess(int descriptor, Mode mode); - GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t); - ~GuestDescriptorAccess(); + 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(); - bool TryLock(); - void Unlock(); - bool ReadOrWait(); - void Wake() noexcept; - Version Snapshot() const; - static void LockTogether(GuestDescriptorAccess& first, GuestDescriptorAccess& second); + 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; diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp index 819f039cd56..607d6b40c7f 100644 --- a/core/libs/prx/libc/src/DescriptorAccess.cpp +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -42,135 +42,137 @@ std::shared_ptr GuestDescriptorAccess::Get(int des return state; } -GuestDescriptorAccess::GuestDescriptorAccess(int descriptor, Mode mode, std::defer_lock_t) - : state(descriptor < 0 ? nullptr : Get(descriptor)), mode(mode) {} - -GuestDescriptorAccess::GuestDescriptorAccess(int descriptor, Mode mode) - : GuestDescriptorAccess(descriptor, mode, std::defer_lock) { Lock(); } - -GuestDescriptorAccess::~GuestDescriptorAccess() { if (held) Unlock(); } +void DescriptorAccessInitialize_nid_no_patch(void* access, int descriptor) { + auto& self = *static_cast(access); + self.state = descriptor < 0 ? nullptr : GuestDescriptorAccess::Get(descriptor); +} -void GuestDescriptorAccess::Lock() { - if (!state) { held = true; return; } - std::unique_lock lock(state->mutex); - if (mode == Mode::Read) { - state->changed.wait(lock, [&] { return state->CanRead(); }); - ++state->readers; +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 { - ++state->changesWaiting; - if (mode == Mode::Close) ++state->closesWaiting; + ++self.state->changesWaiting; + if (self.mode == GuestDescriptorAccess::Mode::Close) ++self.state->closesWaiting; try { - if (mode == Mode::Close) { - while (!state->CanChange()) { - state->Interrupt(); - state->changed.wait_for(lock, std::chrono::milliseconds(1)); + if (self.mode == GuestDescriptorAccess::Mode::Close) { + while (!self.state->CanChange()) { + self.state->Interrupt(); + self.state->changed.wait_for(lock, std::chrono::milliseconds(1)); } - } else state->changed.wait(lock, [&] { return state->CanChange(); }); + } else self.state->changed.wait(lock, [&] { return self.state->CanChange(); }); } catch (...) { - --state->changesWaiting; - if (mode == Mode::Close) --state->closesWaiting; - state->changed.notify_all(); + --self.state->changesWaiting; + if (self.mode == GuestDescriptorAccess::Mode::Close) --self.state->closesWaiting; + self.state->changed.notify_all(); throw; } - --state->changesWaiting; - if (mode == Mode::Close) --state->closesWaiting; - state->writer = std::this_thread::get_id(); - ++state->depth; - state->replacement = state->replacement || mode == Mode::Change || mode == Mode::Close; + --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; } - held = true; + self.held = true; } -bool GuestDescriptorAccess::TryLock() { - if (!state) { held = true; return true; } - std::lock_guard lock(state->mutex); - if (mode == Mode::Read) { - if (!state->CanRead()) return false; - ++state->readers; +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 (!state->CanChange()) return false; - state->writer = std::this_thread::get_id(); - ++state->depth; - state->replacement = state->replacement || mode == Mode::Change || mode == Mode::Close; + 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; } - held = true; + self.held = true; return true; } -void GuestDescriptorAccess::Unlock() { - if (!state) { held = false; return; } +void DescriptorAccessUnlock_nid_no_patch(void* access) { + auto& self = *static_cast(access); + if (!self.state) { self.held = false; return; } { - std::lock_guard lock(state->mutex); - if (mode == Mode::Read) --state->readers; - else if (--state->depth == 0) { - state->writer = {}; - if (state->replacement && state->generation != std::numeric_limits::max()) ++state->generation; - state->replacement = false; + 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; } - held = false; + self.held = false; } - state->changed.notify_all(); + self.state->changed.notify_all(); } -bool GuestDescriptorAccess::ReadOrWait() { - if (!state) { held = true; return true; } - std::unique_lock lock(state->mutex); - if (state->CanRead()) { - ++state->readers; - held = true; +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; } - state->changed.wait(lock); + self.state->changed.wait(lock); return false; } -void GuestDescriptorAccess::Wake() noexcept { if (state) state->changed.notify_all(); } - -GuestDescriptorAccess::Version::Version(std::shared_ptr state, std::uint64_t value) - : state(std::move(state)), value(value) {} - -bool GuestDescriptorAccess::Version::Unchanged() const { - if (!state) return false; - std::lock_guard lock(state->mutex); - return value != std::numeric_limits::max() && state->generation == value && state->closesWaiting == 0; +void DescriptorAccessWake_nid_no_patch(void* access) noexcept { + auto& self = *static_cast(access); + if (self.state) self.state->changed.notify_all(); } -GuestDescriptorAccess::Version GuestDescriptorAccess::Snapshot() const { - if (!state) return Version(nullptr, 0); - std::lock_guard lock(state->mutex); - return Version(state, state->generation); +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 GuestDescriptorAccess::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); +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; -GuestDescriptorAccess::Operation::Operation(GuestDescriptorAccess& access) : state(access.state), previous(current) { - if (state) { - std::lock_guard lock(state->mutex); - state->operations.push_back(this); - interrupted.store(state->closesWaiting != 0); +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); } - current = this; + GuestDescriptorAccess::Operation::current = &self; } -GuestDescriptorAccess::Operation::~Operation() { - current = previous; - if (state) { - std::lock_guard lock(state->mutex); - std::erase(state->operations, this); +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 GuestDescriptorAccess::Operation::CurrentInterrupted() { return current && current->Interrupted(); } +bool DescriptorOperationInterrupted_nid_no_patch() { + auto* current = GuestDescriptorAccess::Operation::current; + return current && current->Interrupted(); +} void GuestDescriptorAccess::Operation::Interrupt() noexcept { interrupted.store(true); @@ -178,16 +180,18 @@ void GuestDescriptorAccess::Operation::Interrupt() noexcept { registration->interrupt(registration->argument); } -GuestDescriptorAccess::Operation::WakeRegistration::WakeRegistration(void (*interrupt)(void*) noexcept, void* argument) - : operation(Operation::current), interrupt(interrupt), argument(argument) { - if (!operation || !operation->state) { operation = nullptr; return; } - std::lock_guard lock(operation->state->mutex); - previous = operation->wake; - operation->wake = this; +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; } -GuestDescriptorAccess::Operation::WakeRegistration::~WakeRegistration() { - if (!operation) return; - std::lock_guard lock(operation->state->mutex); - operation->wake = previous; +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; } From ee9bca6d8a6a82b4f020155361eb8b57d0a62eda Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 13:21:40 -0300 Subject: [PATCH 22/61] fix(libkernel): retain read cancellation across native socket transport waits --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 122 ++++++++------------- docs/dev/debt/libkernel.md | 2 +- 2 files changed, 45 insertions(+), 79 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 6060b7b5bd1..8a5469eb6d7 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -145,6 +145,10 @@ struct Socket : KernelSocketPoll::NativeOwner { } }; +#ifdef _WIN32 +int WaitTransferReady(Socket& socket, bool send, int milliseconds); +#endif + template int Transfer(Socket& socket, bool send, int flags, int length, TOperation operation, bool timed = true) { #ifdef _WIN32 @@ -167,8 +171,13 @@ int Transfer(Socket& socket, bool send, int flags, int length, TOperation operat (static_cast(static_cast(timeout)) * 1000000) >> 32); }; auto deadline = nextDeadline(); + if (timeout != 0) ThreadCancel::IoNoRestart(); int transferred = 0; for (;;) { + if (ThreadCancel::IoInterrupted()) { + WSASetLastError(WSAEINTR); + return transferred == 0 ? -1 : transferred; + } const int result = operation(transferred); if (result >= 0) { transferred += result; @@ -187,12 +196,7 @@ int Transfer(Socket& socket, bool send, int flags, int length, TOperation operat milliseconds = static_cast(std::min( std::chrono::ceil(remaining).count(), INT_MAX)); } - fd_set ready; - FD_ZERO(&ready); - FD_SET(socket.value, &ready); - timeval wait{milliseconds < 0 ? 0 : milliseconds / 1000, milliseconds < 0 ? 0 : milliseconds % 1000 * 1000}; - if (::select(0, send ? nullptr : &ready, send ? &ready : nullptr, nullptr, milliseconds < 0 ? nullptr : &wait) < 0) - return transferred == 0 ? -1 : transferred; + if (WaitTransferReady(socket, send, milliseconds) < 0) return transferred == 0 ? -1 : transferred; } #else static_cast(socket); @@ -200,7 +204,7 @@ int Transfer(Socket& socket, bool send, int flags, int length, TOperation operat static_cast(flags); static_cast(length); static_cast(timed); - return operation(0); + return ThreadCancel::Native([&] { return operation(0); }); #endif } @@ -290,6 +294,39 @@ std::mutex socketsMutex; std::map> sockets; std::set closeOnExecDescriptors; #ifdef _WIN32 +int WaitTransferReady(Socket& socket, bool send, int milliseconds) { + int error = 0; + struct ErrorRestore { + int& error; + ~ErrorRestore() { WSASetLastError(error); } + } restore{error}; + fd_set ready; + fd_set interrupted; + FD_ZERO(&ready); + FD_ZERO(&interrupted); + FD_SET(socket.value, &ready); + timeval timeout{milliseconds < 0 ? 0 : milliseconds / 1000, milliseconds < 0 ? 0 : milliseconds % 1000 * 1000}; + if (!ThreadCancel::IoActive()) { + const int result = ::select(0, send ? nullptr : &ready, send ? &ready : nullptr, nullptr, milliseconds < 0 ? nullptr : &timeout); + error = result < 0 ? WSAGetLastError() : 0; + return result; + } + Waker waker; + ThreadCancel::IoWait operation([](void* opaque) noexcept { + auto& waker = *static_cast(opaque); + const char byte = 0; + ::send(waker.value, &byte, 1, 0); + }, &waker); + FD_SET(waker.value, &interrupted); + if (!send) FD_SET(socket.value, &interrupted); + if (ThreadCancel::IoInterrupted()) { error = WSAEINTR; return -1; } + const int result = ::select(0, &interrupted, send ? &ready : nullptr, nullptr, milliseconds < 0 ? nullptr : &timeout); + error = result < 0 ? WSAGetLastError() : 0; + if (ThreadCancel::IoInterrupted()) { error = WSAEINTR; return -1; } + return result; +} +#endif +#ifdef _WIN32 int ReserveNumber(NativeSocket) { const HANDLE placeholder = ::CreateFileW(L"NUL", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); if (placeholder == INVALID_HANDLE_VALUE) @@ -888,77 +925,6 @@ int SocketSendFlags(Socket& socket, int 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 Transfer(socket, false, flags, length, [&](int offset) { auto* destination = offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset; return address ? ::recvfrom(socket.value, destination, length - offset, NativeReceiveFlags(flags, socket.type), address, addressLength) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ea3adb9c67a..faa381c11a4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -234,7 +234,7 @@ - [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, 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; `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. 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_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) has no console verification of the Windows transport emulation. Exposed Windows native sockets stay nonblocking; guest blocking mode and timeouts are implemented by directional readiness and retries. Unix uses native per-call flags. Windows blocking `MSG_PEEK | MSG_WAITALL` remains unsupported with `EOPNOTSUPP`; this combination needs a separate buffering contract. - [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. 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. From c9917f64367f0e1bf6111c136cabdeaa5494e368 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 14:52:57 -0300 Subject: [PATCH 23/61] docs(libkernel): correct retained socket wait limitations --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index faa381c11a4..f4f7fc2cfff 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -235,7 +235,7 @@ - Guest sockets on macOS ([the equeue wait, 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; `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) has no console verification of the Windows transport emulation. Exposed Windows native sockets stay nonblocking; guest blocking mode and timeouts are implemented by directional readiness and retries. Unix uses native per-call flags. Windows blocking `MSG_PEEK | MSG_WAITALL` remains unsupported with `EOPNOTSUPP`; this combination needs a separate buffering contract. -- [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`. +- [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) has no console measurement of stream completion or timeout behavior. Windows completes stream receives through readiness retries and restarts the `SO_RCVTIMEO` inactivity deadline after positive progress; Unix uses native flags. Datagram sockets ignore `MSG_WAITALL`. Windows blocking `MSG_PEEK | MSG_WAITALL` remains unsupported (`EOPNOTSUPP`); the other platform combinations follow host rules. - [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. 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. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). From f21940e8376ceaa434343d2a5ec6e92bd4884f17 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 10:29:57 -0300 Subject: [PATCH 24/61] feat(libkernel): cancel blocked POSIX writes safely --- core/libs/prx/libkernel/CMakeLists.txt | 47 +++ core/libs/prx/libkernel/File/include/File.hpp | 1 + core/libs/prx/libkernel/File/src/Open.cpp | 22 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 153 ++++++-- .../prx/libkernel/Pthread/include/Cancel.hpp | 9 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 51 ++- core/libs/tests/GuestIoDescriptorAccess.cpp | 4 +- core/libs/tests/GuestIoWriteCancel.cpp | 364 ++++++++++++++++++ docs/dev/debt/libkernel.md | 5 +- 9 files changed, 597 insertions(+), 59 deletions(-) create mode 100644 core/libs/tests/GuestIoWriteCancel.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index bcd240b4dab..58ede1a9d50 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -261,6 +261,53 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_io_native_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_io_write_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoWriteCancel.cpp) + target_include_directories(guest_io_write_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_write_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_io_write_cancel_tests) + foreach(writeTransport pipe loopback regular datagram) + set(writeApis write alias writev writev_alias) + set(writeScenarios cancel disabled complete zero partial pending nonblocking close reuse alias_close peer_close race) + if(writeTransport STREQUAL "pipe") + list(APPEND writeScenarios atomic signal_close) + elseif(writeTransport STREQUAL "loopback") + list(APPEND writeApis send sendto sendto_alias sendmsg sendmsg_alias) + list(APPEND writeScenarios timeout) + elseif(writeTransport STREQUAL "datagram") + list(APPEND writeApis send sendto sendto_alias sendmsg sendmsg_alias) + set(writeScenarios complete zero pending pending_zero) + elseif(writeTransport STREQUAL "regular") + list(APPEND writeApis pwrite pwritev) + set(writeScenarios complete zero pending access append) + if(WIN32) + list(APPEND writeScenarios position) + endif() + endif() + foreach(writeApi IN LISTS writeApis) + set(writeCases ${writeScenarios}) + if(writeTransport STREQUAL "loopback" AND writeApi MATCHES "^send") + list(APPEND writeCases dontwait) + endif() + foreach(writeScenario IN LISTS writeCases) + set(writeTest guest_io_write_${writeTransport}__${writeApi}__${writeScenario}) + add_test(NAME ${writeTest} COMMAND guest_io_write_cancel_tests ${writeTransport} ${writeApi} ${writeScenario}) + set_tests_properties(${writeTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${writeTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + endforeach() + foreach(writeApi sce_write sce_writev) + foreach(writeTransport pipe loopback) + set(writeTest guest_io_write_${writeTransport}_${writeApi}) + add_test(NAME ${writeTest} COMMAND guest_io_write_cancel_tests ${writeTransport} ${writeApi} disabled) + set_tests_properties(${writeTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${writeTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() 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) diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index 5e61c3f4bff..59bc7ff36bf 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -77,6 +77,7 @@ class PositionedTransferLock { void SharePositionedTransfer(int from, int to); #ifdef _WIN32 std::optional IoPositionedTransfer(int descriptor); +std::int64_t IoWriteNative(int descriptor, const void* buffer, std::size_t bytes); 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/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 9be9a517e75..976c73b6b70 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -81,20 +81,23 @@ static int NativeRead(int fd, void* buf, std::size_t n) { if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; return result; } -static int NativeWrite(int fd, const void* buf, std::size_t n) { +static std::int64_t NativeWrite(int fd, const void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } - const auto positioned = File::LockPositionedTransfer(fd); + const auto positioned = File::IoPositionedTransfer(fd); + if (!positioned) return -1; const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); - if (shared != -2) return static_cast(shared); + if (shared != -2) return shared; if (File::Appends(fd)) { const auto appended = File::WriteAtEnd(fd, buf, n); - if (appended != -2) return static_cast(appended); + if (appended != -2) return appended; + } + const auto result = File::IoWriteNative(fd, buf, n); + if (!ThreadCancel::IoActive()) { + if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; + if (result < 0 && (_doserrno == ERROR_NO_DATA || _doserrno == ERROR_BROKEN_PIPE) && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EPIPE; } - const int result = ::_write(fd, buf, static_cast(n)); - if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; - if (result < 0 && (_doserrno == ERROR_NO_DATA || _doserrno == ERROR_BROKEN_PIPE) && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EPIPE; return result; } extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); @@ -164,7 +167,7 @@ static std::int64_t NativeRead(int fd, void* buf, std::size_t 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); - return result == -2 ? ::write(fd, buf, n) : result; + return result == -2 ? ThreadCancel::Native([&] { return ::write(fd, buf, n); }) : result; } static int NativeClose(int fd) { return ::close(fd); } static int NativeUnlink(const std::filesystem::path& p) { @@ -384,7 +387,8 @@ int APS5_VABI sceKernelClose(int d) { if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); EqueueDescriptorClosed(d); #ifdef _WIN32 - const auto positioned = File::LockPositionedTransfer(d); + std::optional positioned; + if (GetFileType(WindowsDescriptorHandle_nid_no_patch(d)) != FILE_TYPE_PIPE) positioned.emplace(File::LockPositionedTransfer(d)); #endif File::ForgetClosedDescriptor(d); if (NativeClose(d) != 0) { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 643fab4f7b4..7aa81a319c8 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -279,6 +279,57 @@ std::optional File::IoPositionedTransfer(int descr changed.WaitUntil(sleep, TimedWait::DeadlineNanos(1000)); } } +std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t bytes) { + const auto handle = WindowsDescriptorHandle_nid_no_patch(descriptor); + const auto type = GetFileType(handle); + if (!ThreadCancel::IoActive() || (type != FILE_TYPE_PIPE && type != FILE_TYPE_DISK)) { + const auto result = ThreadCancel::Native([&] { return ::_write(descriptor, buffer, static_cast(bytes)); }); + if (result < 0 && ThreadCancel::IoActive() && _doserrno == ERROR_OPERATION_ABORTED) errno = EINTR; + return result; + } + const bool pipe = type == FILE_TYPE_PIPE; + DWORD mode = 0; + if (pipe && !GetNamedPipeHandleStateW(handle, &mode, nullptr, nullptr, nullptr, nullptr, 0)) { + const auto error = GetLastError(); + errno = error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? EPIPE : EIO; + return -1; + } + const bool nonblocking = pipe && (mode & PIPE_NOWAIT) != 0; + std::size_t transferred = 0; + TimedWait::Condition changed; + std::mutex mutex; + ThreadCancel::IoWait wait([](void* opaque) noexcept { static_cast(opaque)->NotifyAll(); }, &changed); + do { + if (ThreadCancel::IoInterrupted()) { errno = EINTR; return transferred == 0 ? -1 : transferred; } + auto amount = std::min(bytes - transferred, std::numeric_limits::max()); + if (pipe && amount != 0) { + std::int64_t available = 0; + bool eof = false, writable = false; + if (!GuestSockets::PipeWriteState(descriptor, &available, &eof, &writable)) { errno = EBADF; return transferred == 0 ? -1 : transferred; } + if (eof) { errno = EPIPE; return transferred == 0 ? -1 : transferred; } + if (available == 0 || (bytes <= 512 && available < static_cast(amount))) { + if (nonblocking) { errno = EAGAIN; return transferred == 0 ? -1 : transferred; } + std::unique_lock lock(mutex); + changed.WaitUntil(lock, TimedWait::DeadlineNanos(1000)); + continue; + } + amount = std::min(amount, static_cast(available)); + } + DWORD done = 0; + const auto* source = transferred == 0 ? static_cast(buffer) : static_cast(buffer) + transferred; + const auto result = ThreadCancel::Native([&] { return WriteFile(handle, source, static_cast(amount), &done, nullptr) ? 0 : -1; }); + const DWORD error = result < 0 ? GetLastError() : ERROR_SUCCESS; + transferred += done; + if (result < 0) { + errno = error == ERROR_OPERATION_ABORTED ? EINTR : error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? EPIPE : + error == ERROR_NOACCESS || error == ERROR_INVALID_USER_BUFFER ? EFAULT : error == ERROR_ACCESS_DENIED ? EBADF : + error == ERROR_DISK_FULL || error == ERROR_HANDLE_DISK_FULL ? ENOSPC : EIO; + return transferred == 0 ? -1 : transferred; + } + if (nonblocking || done == 0) break; + } while (transferred < bytes); + return static_cast(transferred); +} void File::SharePositionedTransfer(int from, int to) { std::lock_guard table(g_positionedTableLock); auto& source = g_positionedLocks[from]; @@ -312,13 +363,16 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz overlapped.Offset = static_cast(at); overlapped.OffsetHigh = static_cast(at >> 32u); DWORD done = 0; - const BOOL ok = write ? ::WriteFile(handle, buf, static_cast(nbytes), &done, &overlapped) - : ThreadCancel::Native([&] { return ::ReadFile(handle, buf, static_cast(nbytes), &done, &overlapped) ? 0 : -1; }) == 0; + const BOOL ok = ThreadCancel::Native([&] { + return (write ? ::WriteFile(handle, buf, static_cast(nbytes), &done, &overlapped) + : ::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 (done != 0) return done; if (!write && error == ERROR_HANDLE_EOF) return 0; errno = ThreadCancel::IoInterrupted() ? EINTR : error == ERROR_ACCESS_DENIED ? EBADF : EIO; return -1; @@ -344,9 +398,10 @@ std::int64_t File::WriteAtEnd(int descriptor, const void* buf, std::size_t nbyte end.Offset = 0xFFFFFFFF; end.OffsetHigh = 0xFFFFFFFF; DWORD done = 0; - if (!::WriteFile(handle, buf, static_cast(nbytes), &done, &end)) { + if (ThreadCancel::Native([&] { return ::WriteFile(handle, buf, static_cast(nbytes), &done, &end) ? 0 : -1; }) < 0) { const DWORD error = ::GetLastError(); - errno = error == ERROR_ACCESS_DENIED ? EBADF : error == ERROR_DISK_FULL || error == ERROR_HANDLE_DISK_FULL ? ENOSPC : EIO; + if (done != 0) return done; + errno = error == ERROR_OPERATION_ABORTED ? EINTR : error == ERROR_ACCESS_DENIED ? EBADF : error == ERROR_DISK_FULL || error == ERROR_HANDLE_DISK_FULL ? ENOSPC : EIO; return -1; } return done; @@ -368,8 +423,8 @@ static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t 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; + const auto result = File::IoWriteNative(descriptor, buf, nbytes); + if (!ThreadCancel::IoActive() && result < 0 && (_doserrno == ERROR_NO_DATA || _doserrno == ERROR_BROKEN_PIPE) && ::GetFileType(reinterpret_cast(::_get_osfhandle(descriptor))) == FILE_TYPE_PIPE) errno = EPIPE; return result; } static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes, bool write) { @@ -448,7 +503,7 @@ std::int64_t File::WritevAtEnd(int descriptor, const iovec* iov, int iovcnt) { if (!File::Appends(descriptor) || ::fstat(descriptor, &status) != 0 || !S_ISREG(status.st_mode)) return -2; const off_t position = ::lseek(descriptor, 0, SEEK_CUR); if (position < 0) return -1; - const auto result = static_cast(::writev(descriptor, iov, iovcnt)); + const auto result = ThreadCancel::Native([&] { return static_cast(::writev(descriptor, iov, iovcnt)); }); const int error = errno; if (::lseek(descriptor, position, SEEK_SET) < 0) throw std::runtime_error("WritevAtEnd: cannot restore the file offset"); errno = error; @@ -462,7 +517,7 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb const iovec whole{const_cast(buf), nbytes}; if (const auto appended = File::WritevAtEnd(descriptor, &whole, 1); appended != -2) return appended; #endif - return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); + return ThreadCancel::Native([&] { return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); }); } static_assert(sizeof(KernelIovec) == sizeof(struct iovec)); static_assert(offsetof(KernelIovec, base) == offsetof(struct iovec, iov_base)); @@ -882,7 +937,8 @@ int APS5_VABI close_nid_postfix(int d) { if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d); EqueueDescriptorClosed(d); #ifdef _WIN32 - const auto positioned = File::LockPositionedTransfer(d); + std::optional positioned; + if (GetFileType(WindowsDescriptorHandle_nid_no_patch(d)) != FILE_TYPE_PIPE) positioned.emplace(File::LockPositionedTransfer(d)); #endif File::ForgetClosedDescriptor(d); #ifdef _WIN32 @@ -1177,15 +1233,19 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { - if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); - if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (const int error = PositionedArgumentError(d, true, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); - if (FailPosixPositionedGuestSocket(d)) return -1; - const File::WriteSync sync(d); - GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); - char empty = 0; - const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset); - return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; + return ThreadCancel::IoWrite([&]() -> std::int64_t { + if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + if (const int error = PositionedArgumentError(d, true, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); + if (FailPosixPositionedGuestSocket(d)) return -1; + const File::WriteSync sync(d); + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); + char empty = 0; + const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset); + return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; + }); + }); } int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { @@ -1213,19 +1273,27 @@ std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size } int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { - if (size < 0) return PosixFailure(GUEST_EINVAL); - if (GuestSockets::IsOpen(d)) return GuestSockets::Write(d, str, static_cast(size)); - if (const int error = TransferArgumentError(d, str == nullptr && size != 0)) return PosixFailure(error); - const char emptyBuffer = 0; - return PosixResult(sceKernelWrite(d, str == nullptr ? &emptyBuffer : str, static_cast(size))); + return ThreadCancel::IoWrite([&]() -> std::int64_t { + if (size < 0) return PosixFailure(GUEST_EINVAL); + return File::WithIoDescriptor(d, [&](int d) -> std::int64_t { + if (GuestSockets::IsOpen(d)) return GuestSockets::Write(d, str, static_cast(size)); + if (const int error = TransferArgumentError(d, str == nullptr && size != 0)) return PosixFailure(error); + const char emptyBuffer = 0; + return PosixResult(sceKernelWrite(d, str == nullptr ? &emptyBuffer : str, static_cast(size))); + }); + }); } std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { - if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); - if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Write(descriptor, buffer, count); - if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); - const char emptyBuffer = 0; - return PosixResult(sceKernelWrite(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); + return ThreadCancel::IoWrite([&]() -> 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::Write(descriptor, buffer, count); + if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); + const char emptyBuffer = 0; + return PosixResult(sceKernelWrite(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); + }); + }); } int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { @@ -1581,7 +1649,8 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn return result < 0 ? SceErrorFromErrno(errno) : 0; } if (!write && offset == nullptr && File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); - if (write && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && File::Appends(d)) { + if (write && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && + (File::Appends(d) || GetFileType(WindowsDescriptorHandle_nid_no_patch(d)) == FILE_TYPE_PIPE)) { std::vector gathered(total); std::size_t gatheredOffset = 0; for (int i = 0; i < iovcnt; ++i) { @@ -1686,7 +1755,7 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt PrepareWriteIovecs(iov, iovcnt); const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; - const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); + const auto result = ThreadCancel::Native([&] { return static_cast(::writev(d, NativeIovecs(iov), iovcnt)); }); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1719,7 +1788,7 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcn #ifdef __APPLE__ if (const auto appended = File::WritevAtEnd(d, NativeIovecs(iov), iovcnt); appended != -2) return appended < 0 ? SceErrorFromErrno(errno) : appended; #endif - const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + const auto result = ThreadCancel::Native([&] { return static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); }); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1739,8 +1808,15 @@ int64_t APS5_VABI preadv_nid_postfix(int d, const KernelIovec* iov, int iovcnt, } int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { - const auto result = sceKernelPwritev(d, iov, iovcnt, offset); - return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + return ThreadCancel::IoWrite([&]() -> std::int64_t { + if (File::IsVirtualDirectoryDescriptor(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 = sceKernelPwritev(d, vectors.data(), iovcnt, offset); + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + }); + }); } int APS5_VABI sceKernelRename(const char* from, const char* to) { @@ -1923,8 +1999,15 @@ int64_t APS5_VABI readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { } int64_t APS5_VABI writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { - const auto result = sceKernelWritev(d, iov, iovcnt); - return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + return ThreadCancel::IoWrite([&]() -> std::int64_t { + if (File::IsVirtualDirectoryDescriptor(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 = sceKernelWritev(d, vectors.data(), iovcnt); + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; + }); + }); } int64_t APS5_VABI _readv_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 aa58475bc8d..25d9c1ad439 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -120,7 +120,7 @@ auto Native(TCall&& call) { } template -auto Io(TCall&& call, bool restartable = true) { +auto Io(TCall&& call, bool restartable = true, bool cancelZero = false) { Check(); IoRestart restart(restartable); for (;;) { @@ -129,12 +129,15 @@ auto Io(TCall&& call, bool restartable = true) { IoPoint point(restart); result = call(); } - if (result >= 0) return result; + if (result > 0 || (result == 0 && !cancelZero)) return result; if (Requested()) { restart.Clear(); Check(); } - if (!restart.Ready()) return result; + if (result == 0 || !restart.Ready()) return result; } } +template +auto IoWrite(TCall&& call) { return Io(std::forward(call), true, true); } + template struct WaitLock { TLock& inner; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 8a5469eb6d7..c726887bf41 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -923,6 +923,18 @@ int SocketSendFlags(Socket& socket, int flags) { std::lock_guard lock(socket.modeMutex); return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); } +int SendBytes(Socket& socket, int remaining, int flags) { +#ifdef _WIN32 + if (ThreadCancel::IoActive() && socket.type == 1 && (flags & GuestDontWait) == 0) { + std::lock_guard lock(socket.modeMutex); + if (!socket.options.nonblocking) return std::min(remaining, std::max(1, socket.options.sendBuffer)); + } +#else + static_cast(socket); + static_cast(flags); +#endif + return remaining; +} int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* address, socklen_t* addressLength) { #ifdef _WIN32 return Transfer(socket, false, flags, length, [&](int offset) { @@ -1563,16 +1575,17 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l return Fail(12); } } -std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags) { +static std::int64_t Send(int descriptor, const void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, length); const auto result = Transfer(*socket, true, flags, static_cast(length), [&](int offset) { return ::send(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, - static_cast(length) - offset, SocketSendFlags(*socket, flags)); + SendBytes(*socket, static_cast(length) - offset, flags), SocketSendFlags(*socket, flags)); }); return result < 0 ? Fail(SendError(socket->type)) : result; } @@ -1699,7 +1712,7 @@ int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* arg int APS5_VABI _ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { return ioctl_nid_postfix(descriptor, request, argument); } -std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, +static std::int64_t SendTo(int descriptor, const void* buffer, std::uint64_t length, int flags, const void* address, std::uint32_t addressLength) { if (address) { if (const int error = AddressLengthError(address, addressLength)) return Fail(error); @@ -1709,13 +1722,14 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, length); if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); int result; if (!address || (socket->type == 1 && PeerConnected(*socket))) { if (!address && addressLength != 0) return Fail(22); result = Transfer(*socket, true, flags, static_cast(length), [&](int offset) { return ::send(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, - static_cast(length) - offset, SocketSendFlags(*socket, flags)); + SendBytes(*socket, static_cast(length) - offset, flags), SocketSendFlags(*socket, flags)); }); } else { sockaddr_storage native{}; @@ -1724,7 +1738,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; result = Transfer(*socket, true, flags, static_cast(length), [&](int offset) { return ::sendto(socket->value, offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset, - static_cast(length) - offset, SocketSendFlags(*socket, flags), reinterpret_cast(&native), size); + SendBytes(*socket, static_cast(length) - offset, flags), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size); }); } return result < 0 ? Fail(SendError(socket->type)) : result; @@ -1776,10 +1790,12 @@ static std::int64_t MessageLength(const GuestMsghdr* message) { } return static_cast(total); } -std::int64_t APS5_VABI sendmsg_nid_postfix(int descriptor, const GuestMsghdr* message, int flags) { +static std::int64_t SendMessageInternal(int descriptor, const GuestMsghdr* message, int flags) { const auto total = MessageLength(message); if (total < 0) return -1; if (message->control && message->controlLength) throw std::runtime_error("sendmsg: control data is not supported"); + const File::IoDescriptor retained(descriptor); + if (retained.Value() < 0) return -1; std::vector buffer; try { buffer.resize(static_cast(total)); @@ -1791,7 +1807,7 @@ std::int64_t APS5_VABI sendmsg_nid_postfix(int descriptor, const GuestMsghdr* me if (message->iov[i].length) std::memcpy(buffer.data() + offset, message->iov[i].base, message->iov[i].length); offset += message->iov[i].length; } - return sendto_nid_postfix(descriptor, buffer.data(), buffer.size(), flags, message->name, message->name ? message->nameLength : 0u); + return SendTo(descriptor, buffer.data(), buffer.size(), flags, message->name, message->name ? message->nameLength : 0u); } #ifdef _WIN32 static bool GuestPagesAllow(std::uintptr_t start, std::size_t bytes, DWORD allowed) { @@ -2236,6 +2252,25 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 return Receive(descriptor, buffer, length, flags); }); }); } +std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags) { + return ThreadCancel::IoWrite([&] { return File::WithIoDescriptor(descriptor, [&](int descriptor) { + return Send(descriptor, buffer, length, flags); + }); }); +} +std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, + int flags, const void* address, std::uint32_t addressLength) { + return ThreadCancel::IoWrite([&]() -> std::int64_t { + if (address) { + if (const int error = AddressLengthError(address, addressLength)) return Fail(error); + } + return File::WithIoDescriptor(descriptor, [&](int descriptor) { + return SendTo(descriptor, buffer, length, flags, address, addressLength); + }); + }); +} +std::int64_t APS5_VABI sendmsg_nid_postfix(int descriptor, const GuestMsghdr* message, int flags) { + return ThreadCancel::IoWrite([&] { return SendMessageInternal(descriptor, message, 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) { @@ -2251,7 +2286,7 @@ std::int64_t GuestSockets::Read(int descriptor, void* buffer, std::size_t length } std::int64_t GuestSockets::Write(int descriptor, const void* buffer, std::size_t length) { - return send_nid_postfix(descriptor, buffer, length, 0); + return Send(descriptor, buffer, length, 0); } std::int64_t GuestSockets::Readv(int descriptor, const Iovec* iov, int iovcnt) { diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index e7731a82f82..284f9861c48 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -617,7 +617,7 @@ 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]); + GuestDescriptorAccess admission(pipe[1], GuestDescriptorAccess::Mode::Read); std::atomic closed{false}; std::thread closer([&] { Require(close_nid_postfix(pipe[1]) == 0, "close positioned descriptor"); @@ -632,7 +632,7 @@ static void CloseProgress() { 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(); + admission.Unlock(); closer.join(); Require(Observe([&] { return reader->_finished.load(); }), "drain reader did not finish"); Require(scePthreadJoin(reader, nullptr) == 0, "join drain reader"); diff --git a/core/libs/tests/GuestIoWriteCancel.cpp b/core/libs/tests/GuestIoWriteCancel.cpp new file mode 100644 index 00000000000..42cbe37e609 --- /dev/null +++ b/core/libs/tests/GuestIoWriteCancel.cpp @@ -0,0 +1,364 @@ +#include "prx/libkernel/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 "prx/libc/include/Api.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#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); +} + +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 output = -1; + int input = -1; + int replacement = -1; + std::atomic signals{0}; + PthreadKey key = -1; + std::string api; + std::string scenario; + bool socket = false; + bool regular = false; + bool datagram = false; + std::vector bytes; + std::int64_t result = -2; + int error = 0; + std::atomic ready{false}, release{false}, returned{false}; + std::atomic cleanups{0}, destructors{0}; +}; + +static State* signalState = nullptr; +static void APS5_VABI SignalHandler(int) { + Require(close_nid_postfix(signalState->output) == 0, "signal handler closes writer"); + Require(dup2_nid_postfix(signalState->replacement, signalState->output) == signalState->output, "signal handler replaces writer"); + ++signalState->signals; +} +static void APS5_VABI ExceptionHandler(int signal, void*) { SignalHandler(signal); } + +static void APS5_VABI Cleanup(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.fetch_add(1) == 0 && state.destructors.load() == 0, "repeated or reordered cleanup"); +} +static void APS5_VABI Destroy(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.load() == 1 && state.destructors.fetch_add(1) == 0, "repeated or reordered TSD"); +} + +static void* APS5_VABI Writer(void* opaque) { + auto& state = *static_cast(opaque); + const bool disabled = state.scenario == "disabled"; + if (disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable writer cancellation"); + Require(scePthreadSetspecific(state.key, &state) == 0, "set writer TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); + state.ready.store(true); + if (state.scenario.starts_with("pending")) Require(Observe([&] { return state.release.load(); }), "release pending write"); + const auto size = state.bytes.size(); + KernelIovec vectors[2]{{state.bytes.data(), size / 2}, {size == 0 ? nullptr : state.bytes.data() + size / 2, size - size / 2}}; + GuestMsghdr message{}; + message.iov = vectors; + message.iovLength = 2; + const int flags = state.scenario == "dontwait" ? 0x20080 : 0x20000; + if (state.api == "write") state.result = write_nid_postfix(state.output, state.bytes.data(), size); + else if (state.api == "alias") state.result = _write_nid_postfix(state.output, state.bytes.data(), size); + else if (state.api == "writev") state.result = writev_nid_postfix(state.output, vectors, 2); + else if (state.api == "writev_alias") state.result = _writev_nid_postfix(state.output, vectors, 2); + else if (state.api == "pwrite") state.result = pwrite_nid_disambig1_nid_postfix(state.output, state.bytes.data(), size, 1); + else if (state.api == "pwritev") state.result = pwritev_nid_postfix(state.output, vectors, 2, 1); + else if (state.api == "send") state.result = send_nid_postfix(state.output, state.bytes.data(), size, flags); + else if (state.api == "sendto") state.result = sendto_nid_postfix(state.output, state.bytes.data(), size, flags, nullptr, 0); + else if (state.api == "sendto_alias") state.result = _sendto_nid_postfix(state.output, state.bytes.data(), size, flags, nullptr, 0); + else if (state.api == "sendmsg") state.result = sendmsg_nid_postfix(state.output, &message, flags); + else if (state.api == "sendmsg_alias") state.result = _sendmsg_nid_postfix(state.output, &message, flags); + else if (state.api == "sce_write") state.result = sceKernelWrite(state.output, state.bytes.data(), size); + else if (state.api == "sce_writev") state.result = sceKernelWritev(state.output, vectors, 2); + else Require(false, "unknown write API"); + state.error = *__error_nid_postfix(); + state.returned.store(true); + if (disabled || state.api.starts_with("sce_")) { + Require(state.result == static_cast(size), "disabled/SCE write lost data"); + Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable writer cancellation"); + scePthreadTestcancel(); + } + __pthread_cleanup_pop_imp_nid_postfix(1); + return reinterpret_cast(std::uintptr_t{7}); +} + +static std::size_t Fill(State& state) { + Require(fcntl_nid_postfix(state.output, 4, 6) == 0, "set fill nonblocking"); + std::array bytes{}; + bytes.fill('f'); + std::size_t filled = 0; + while (filled < 16 * 1024 * 1024) { + const auto sent = write_nid_postfix(state.output, bytes.data(), bytes.size()); + if (sent < 0) { + Require(*__error_nid_postfix() == 35, "fill did not stop with EAGAIN"); + if (state.scenario != "nonblocking") Require(fcntl_nid_postfix(state.output, 4, 2) == 0, "restore fill blocking"); + return filled; + } + Require(sent > 0, "fill made no progress"); + filled += static_cast(sent); + } + Require(false, "transport did not fill within its bounded budget"); + return 0; +} + +static void Drain(State& state, std::size_t count, char expected) { + std::array buffer{}; + while (count != 0) { + int available = 0; + Require(Observe([&] { + Require(ioctl_nid_postfix(state.input, 0x4004667f, &available) == 0, "query input bytes"); + return available > 0; + }), "peer did not receive expected bytes"); + const auto amount = std::min({count, buffer.size(), static_cast(available)}); + const auto received = read_nid_postfix(state.input, buffer.data(), amount); + Require(received > 0, "drain did not progress"); + Require(std::all_of(buffer.begin(), buffer.begin() + received, [&](char byte) { return byte == expected; }), "write lost byte order or ownership"); + count -= static_cast(received); + } +} + +static void Setup(State& state) { + if (state.datagram) { + int descriptors[2]{}; + Require(socketpair_nid_postfix(1, 2, 0, descriptors) == 0, "create write datagram pair"); + state.input = descriptors[0]; state.output = descriptors[1]; + return; + } + if (!state.socket) { + int descriptors[2]{}; + Require(pipe_nid_postfix(descriptors) == 0, "create blocked write pipe"); + state.input = descriptors[0]; state.output = descriptors[1]; + return; + } + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0, "create write listener"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0, "listen write loopback"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0, "get write listener address"); + state.output = socket_nid_postfix(2, 1, 0); + const int capacity = 4096; + Require(state.output >= 0 && setsockopt_nid_postfix(state.output, 0xffff, 0x1001, &capacity, sizeof(capacity)) == 0, "bound socket send buffer"); + Require(connect_nid_postfix(state.output, address.data(), address.size()) == 0, "connect write loopback"); + state.input = accept_nid_postfix(listener, nullptr, nullptr); + Require(state.input >= 0 && setsockopt_nid_postfix(state.input, 0xffff, 0x1002, &capacity, sizeof(capacity)) == 0, "bound socket receive buffer"); + Require(close_nid_postfix(listener) == 0, "close write listener"); +} + +static void Run(const char* transport, const char* api, const char* scenario) { + State state; + state.api = api; state.scenario = scenario; state.socket = std::strcmp(transport, "loopback") == 0; + state.datagram = std::strcmp(transport, "datagram") == 0; + state.socket = state.socket || state.datagram; + state.regular = std::strcmp(transport, "regular") == 0; + const bool partial = state.scenario == "partial"; + const bool disabled = state.scenario == "disabled"; + const bool sce = state.api.starts_with("sce_"); + const bool pending = state.scenario.starts_with("pending"); + const bool zero = state.scenario == "zero" || state.scenario == "pending_zero"; + const auto expectedCount = zero ? 0 : 2; + const bool append = state.scenario == "append"; + const bool race = state.scenario == "race"; + const bool peerClose = state.scenario == "peer_close"; + const bool aliasClose = state.scenario == "alias_close"; + const bool completion = state.scenario == "complete" || state.scenario == "zero" || append || disabled || sce; + const bool wouldBlock = state.scenario == "nonblocking" || state.scenario == "dontwait" || state.scenario == "timeout"; + const bool close = state.scenario == "close"; + const bool reuse = state.scenario == "reuse"; + const bool signal = state.scenario == "signal_close"; + std::FILE* file = nullptr; + if (state.regular) { + file = std::tmpfile(); Require(file != nullptr, "create write file"); + Require(std::fwrite("xyz", 1, 3, file) == 3 && std::fflush(file) == 0 && std::fseek(file, 0, SEEK_SET) == 0, "seed write file"); +#ifdef _WIN32 + state.output = ::_fileno(file); +#else + state.output = ::fileno(file); +#endif + } else Setup(state); + state.bytes.assign(zero ? 0 : partial ? 8 * 1024 * 1024 : 2, 'w'); + auto filled = state.regular || state.datagram || zero || partial || pending ? 0 : Fill(state); + if (state.scenario == "atomic") { Require(filled > 0, "empty atomic-write fill"); Drain(state, 1, 'f'); --filled; } + const int alias = state.regular ? -1 : dup_nid_postfix(state.output); + if (!state.regular) Require(alias >= 0, "duplicate writer"); + if (state.scenario == "timeout") { + const std::array timeout{0, 60000}; + Require(setsockopt_nid_postfix(state.output, 0xffff, 0x1005, timeout.data(), sizeof(timeout)) == 0, "set send timeout"); + } + if (append) Require(fcntl_nid_postfix(state.output, 4, 10) == 0, "set append writes"); + std::optional exclusion; + if (state.scenario == "access") exclusion.emplace(state.output, GuestDescriptorAccess::Mode::Update); +#ifdef _WIN32 + std::optional positioned; + if (state.scenario == "position") positioned.emplace(File::LockPositionedTransfer(state.output)); +#endif + Require(scePthreadKeyCreate(&state.key, reinterpret_cast(Destroy)) == 0, "create write TSD key"); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, &state, nullptr) == 0, "create blocked writer"); + Require(Observe([&] { return state.ready.load(); }), "writer did not start"); + if (!state.regular && !partial && !disabled && !sce && !wouldBlock && !pending) Require(Observe([&] { + std::lock_guard lock(writer->cancelLock); return writer->cancelIo != nullptr || writer->_finished.load(); + }), "writer did not register its blocked operation"); + if (partial) Require(Observe([&] { + int available = 0; Require(ioctl_nid_postfix(state.input, 0x4004667f, &available) == 0, "query partial write"); return available > 0; + }), "partial write did not transfer initial bytes"); + int replacement[2]{-1, -1}; + GuestSigaction previous{}; + if (signal) { + signalState = &state; + Require(pipe_nid_postfix(replacement) == 0, "create signal replacement pipe"); + state.replacement = replacement[1]; +#ifdef _WIN32 + Require(sceKernelInstallExceptionHandler(30, reinterpret_cast(ExceptionHandler)) == 0, "install write exception handler"); +#else + const GuestSigaction action{reinterpret_cast(SignalHandler), 0, {}}; + Require(sigaction_nid_postfix(2, &action, &previous) == 0, "install write signal handler"); +#endif + } + std::atomic changed{false}; + std::thread changer; + if (reuse) { + Require(pipe_nid_postfix(replacement) == 0, "create replacement write pipe"); + changer = std::thread([&] { Require(dup2_nid_postfix(replacement[1], state.output) == state.output, "replace blocked writer"); changed.store(true); }); + } + if (close) changer = std::thread([&] { Require(close_nid_postfix(state.output) == 0, "close blocked writer"); changed.store(true); }); + if (aliasClose) Require(close_nid_postfix(alias) == 0, "close independent writer alias"); + if (peerClose) Require(close_nid_postfix(state.input) == 0, "close write peer"); + std::thread drainer; + if (race) drainer = std::thread([&] { Drain(state, filled, 'f'); }); + bool prompt = true; + if (signal) { +#ifdef _WIN32 + Require(sceKernelRaiseException(writer, 30) == 0, "raise exception during write"); +#else + Require(pthread_kill(writer->hostThread, SIGINT) == 0, "raise native signal during write"); +#endif + } else if (!completion && !wouldBlock && !close && !peerClose) { + std::atomic requested{false}; + std::thread cancel([&] { Require(pthread_cancel_nid_postfix(writer) == 0, "cancel blocked writer"); requested.store(true); }); + prompt = Observe([&] { return requested.load(); }); + if (!prompt && !state.regular) Drain(state, partial ? 65536 : filled, partial ? 'w' : 'f'); + state.release.store(true); + cancel.join(); + } else if (disabled || sce) Require(pthread_cancel_nid_postfix(writer) == 0, "pend disabled/SCE write cancellation"); + if (completion && !state.regular) Drain(state, filled, 'f'); + Require(Observe([&] { return writer->_finished.load(); }), "writer did not exit"); + void* result = nullptr; + Require(scePthreadJoin(writer, &result) == 0, "join writer"); + exclusion.reset(); +#ifdef _WIN32 + positioned.reset(); +#endif + if (drainer.joinable()) drainer.join(); + if (changer.joinable()) { Require(Observe([&] { return changed.load(); }), "descriptor change did not finish"); changer.join(); } + Require(prompt, "cancellation required external write completion"); + Require(state.cleanups.load() == 1 && state.destructors.load() == 1, "write cleanup/TSD did not run exactly once"); + if (completion) { + Require(state.result == expectedCount && state.returned.load(), "completed write lost its result"); + Require(result == (disabled || sce ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "completion/disabled result wrong"); + if (state.datagram) { + Require(write_nid_postfix(state.output, "t", 1) == 1, "send following datagram"); + Require(fcntl_nid_postfix(state.input, 4, 6) == 0, "set datagram receive nonblocking"); + std::array record{}; + Require(recv_nid_postfix(state.input, record.data(), record.size(), 0) == expectedCount, "write changed a datagram boundary"); + if (!zero) Require(record[0] == 'w' && record[1] == 'w', "datagram payload changed"); + Require(recv_nid_postfix(state.input, record.data(), record.size(), 0) == 1 && record[0] == 't', "write consumed following datagram"); + } else if (!state.regular) Drain(state, expectedCount, 'w'); + } else if (signal) Require(state.signals.load() == 1 && result == reinterpret_cast(std::uintptr_t{7}) && state.returned.load() && + ((state.result == -1 && state.error == 4) || (state.socket && state.result > 0 && state.result <= 2)), "write signal close lost result or retained a guard"); + else if (wouldBlock) Require(state.returned.load() && (state.result > 0 || (state.result == -1 && state.error == 35)), "would-block write result wrong"); + else if (close) Require(state.returned.load() && ((state.result == -1 && state.error == 4) || (state.socket && state.result > 0 && state.result <= 2)), "close did not preserve completion or interrupt with EINTR"); + else if (peerClose) Require(state.returned.load() && ((state.result == -1 && (state.error == 32 || state.error == 54 || state.error == 57)) || (state.socket && state.result > 0 && state.result <= 2)), "peer close returned wrong write error"); + else if (race && result == reinterpret_cast(std::uintptr_t{7})) { + Require(state.returned.load() && state.result > 0 && state.result <= 2, "racing write lost progress"); + Drain(state, static_cast(state.result), 'w'); + } + else if (!partial) Require((result == GUEST_CANCELED && !state.returned.load()) || + (state.socket && result == reinterpret_cast(std::uintptr_t{7}) && state.returned.load() && state.result > 0 && state.result <= 2), "blocked write neither canceled nor preserved completed bytes"); + if (state.socket && !completion && !partial && !peerClose && !race && state.result > 0) { + Drain(state, filled, 'f'); + Drain(state, static_cast(state.result), 'w'); + } + if (state.regular) { + const bool positionedWrite = state.api.starts_with("pwrite"); + const auto expectedOffset = !completion || zero || positionedWrite ? 0 : append ? 5 : 2; + Require(lseek_nid_postfix(state.output, 0, 1) == expectedOffset, "write changed the wrong shared offset"); + Require(std::fseek(file, 0, SEEK_SET) == 0, "rewind write file"); + std::array contents{}; + Require(std::fread(contents.data(), 1, contents.size(), file) == contents.size(), "read write-file result"); + const char* expected = append || zero ? "xyz" : completion ? (positionedWrite ? "xww" : "wwz") : "xyz"; + Require(std::memcmp(contents.data(), expected, contents.size()) == 0, "write file modified unexpectedly"); + if (append) Require(std::fgetc(file) == 'w' && std::fgetc(file) == 'w' && std::fgetc(file) == EOF, "append write overwrote existing bytes"); + Require(std::fclose(file) == 0, "close write file"); + } else { + if (reuse || signal) { + Require(write_nid_postfix(state.output, "r", 1) == 1, "write reused descriptor"); + char byte = 0; Require(read_nid_postfix(replacement[0], &byte, 1) == 1 && byte == 'r', "old writer acted on replacement"); + Require(close_nid_postfix(replacement[0]) == 0 && close_nid_postfix(replacement[1]) == 0, "close replacement pipe"); + } + if (!close) Require(close_nid_postfix(state.output) == 0, "close original writer"); + if (!aliasClose) Require(close_nid_postfix(alias) == 0, "close duplicate writer"); + if (partial) { + Require(result == reinterpret_cast(std::uintptr_t{7}) && state.result > 0 && state.result < static_cast(state.bytes.size()), "partial write progress was canceled or lost"); + Drain(state, static_cast(state.result), 'w'); + char extra = 0; Require(read_nid_postfix(state.input, &extra, 1) == 0, "write reported fewer bytes than it transferred"); + } + if (state.scenario == "atomic") { + Drain(state, filled, 'f'); + char extra = 0; Require(read_nid_postfix(state.input, &extra, 1) == 0, "atomic pipe write exposed a prefix"); + } + if (race) { char extra = 0; Require(read_nid_postfix(state.input, &extra, 1) == 0, "racing cancellation hid transferred bytes"); } + if (!peerClose) Require(close_nid_postfix(state.input) == 0, "close read peer"); + } + if (signal) { +#ifdef _WIN32 + Require(sceKernelRemoveExceptionHandler(30) == 0, "remove write exception handler"); +#else + Require(sigaction_nid_postfix(2, &previous, nullptr) == 0, "restore write signal handler"); +#endif + signalState = nullptr; + } + Require(scePthreadKeyDelete(state.key) == 0, "delete writer TSD key"); +} + +int main(int argc, char** argv) { +#ifndef _WIN32 + std::signal(SIGPIPE, SIG_IGN); +#endif + Require(argc == 4, "expected write transport, API and scenario"); + const int rounds = std::strcmp(argv[3], "race") == 0 ? 8 : 1; + for (int round = 0; round < rounds; ++round) Run(argv[1], argv[2], argv[3]); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f4f7fc2cfff..eb4ff39f9f5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -205,9 +205,10 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. File and socket writes, connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation except at the two set calls. +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. Connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation 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. +- [POSIX write cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) is not console-verified. It covers `write`, `_write`, `writev`, `_writev`, `pwrite`, `pwritev`, `send`, `sendto`, `sendmsg`, `_sendto` and `_sendmsg` on accepted file, pipe and socket descriptors; positioned socket and pipe writes retain their existing errors. Positive partial counts win over pending cancellation; zero and failed writes check cancellation after guards release, following FreeBSD libthr. SCE writes and libSceNet gain no cancellation points. Non-interruptible drivers can delay cancellation and close indefinitely. Windows pipe quota waits retry readiness at 1 ms intervals. Descriptor replacement and stdio closure can still wait indefinitely; stream-socket writes share the read admission and close/drain machinery. +- [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 and write waits and run after operation guards are released, including disabled cancellation and unregistered callers. Restartable operations retry only if descriptor generations remain unchanged; socket transfer 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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From 10dbaaa297de320abe3cee84160c9da396aaa0df Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 10:43:23 -0300 Subject: [PATCH 25/61] fix(libkernel): cancel contended shared-memory writes --- core/libs/prx/libkernel/CMakeLists.txt | 16 ++ .../prx/libkernel/File/src/SharedMemory.cpp | 50 ++++-- core/libs/tests/GuestIoSharedWriteCancel.cpp | 169 ++++++++++++++++++ .../tests/GuestNamedShmIoDescriptorReuse.cpp | 2 +- docs/dev/debt/libkernel.md | 2 +- 5 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 core/libs/tests/GuestIoSharedWriteCancel.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 58ede1a9d50..9f9e936e9fe 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -308,6 +308,22 @@ if(BUILD_TESTING) endif() endforeach() endforeach() + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_io_shared_write_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoSharedWriteCancel.cpp) + target_include_directories(guest_io_shared_write_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_io_shared_write_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_io_shared_write_cancel_tests) + foreach(sharedWriteApi write alias writev writev_alias pwrite pwritev) + foreach(sharedWriteScenario cancel complete disabled close disabled_close) + set(sharedWriteTest guest_io_write_shared_${sharedWriteApi}__${sharedWriteScenario}) + add_test(NAME ${sharedWriteTest} COMMAND guest_io_shared_write_cancel_tests ${sharedWriteApi} ${sharedWriteScenario}) + set_tests_properties(${sharedWriteTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${sharedWriteTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + 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) diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 8e18c4bcb68..ac9c044a18a 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -5,6 +5,7 @@ #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include #include @@ -129,7 +130,13 @@ class HostShmLock { HostShmLock() { #ifdef __linux__ descriptor = ::open("/dev/shm", O_RDONLY | O_DIRECTORY | O_CLOEXEC); - if (descriptor < 0 || ::flock(descriptor, LOCK_EX) != 0) error = errno; + try { + if (descriptor < 0 || ThreadCancel::Native([&] { return ::flock(descriptor, LOCK_EX); }) != 0) error = errno; + } catch (...) { + if (descriptor >= 0) ::close(descriptor); + descriptor = -1; + throw; + } #endif } @@ -227,6 +234,7 @@ int WindowsNamedError(DWORD error) { switch (error) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: return ENOENT; + case ERROR_OPERATION_ABORTED: return EINTR; case ERROR_ACCESS_DENIED: case ERROR_SHARING_VIOLATION: return EACCES; case ERROR_FILE_EXISTS: @@ -363,9 +371,27 @@ class WindowsNamedLock { } handle = ::CreateMutexW(nullptr, FALSE, lockName.c_str()); if (handle == nullptr) { error = ::GetLastError(); return; } - const DWORD result = ::WaitForSingleObject(handle, INFINITE); - if (result == WAIT_OBJECT_0 || result == WAIT_ABANDONED) locked = true; - else error = result == WAIT_FAILED ? ::GetLastError() : ERROR_GEN_FAILURE; + try { Wait(); } + catch (...) { ::CloseHandle(handle); handle = nullptr; throw; } + } + void Wait() { + if (!ThreadCancel::IoActive()) { + const DWORD result = ::WaitForSingleObject(handle, INFINITE); + if (result == WAIT_OBJECT_0 || result == WAIT_ABANDONED) locked = true; + else error = result == WAIT_FAILED ? ::GetLastError() : ERROR_GEN_FAILURE; + return; + } + const std::unique_ptr interrupted(::CreateEventW(nullptr, TRUE, FALSE, nullptr), &::CloseHandle); + if (!interrupted) { error = ::GetLastError(); return; } + ThreadCancel::IoWait wait([](void* event) noexcept { ::SetEvent(event); }, interrupted.get()); + if (ThreadCancel::IoInterrupted()) { error = ERROR_OPERATION_ABORTED; return; } + const HANDLE handles[]{interrupted.get(), handle}; + const DWORD result = ::WaitForMultipleObjects(2, handles, FALSE, INFINITE); + if (result == WAIT_OBJECT_0) { error = ERROR_OPERATION_ABORTED; return; } + if (result == WAIT_OBJECT_0 + 1 || result == WAIT_ABANDONED_0 + 1) { + locked = true; + if (ThreadCancel::IoInterrupted()) { ::ReleaseMutex(handle); locked = false; error = ERROR_OPERATION_ABORTED; } + } else error = result == WAIT_FAILED ? ::GetLastError() : ERROR_GEN_FAILURE; } HANDLE handle = nullptr; DWORD error = ERROR_SUCCESS; @@ -831,9 +857,9 @@ std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, if (position >= status.st_size) return 0; const auto count = std::min(size, static_cast(status.st_size - position)); #ifdef _WIN32 - const auto result = ::_write(fd, buffer, static_cast(std::min(count, std::numeric_limits::max()))); + const auto result = File::IoWriteNative(fd, buffer, static_cast(std::min(count, std::numeric_limits::max()))); #else - const auto result = ::write(fd, buffer, static_cast(count)); + const auto result = ThreadCancel::Native([&] { return ::write(fd, buffer, static_cast(count)); }); #endif return static_cast(result); } @@ -864,13 +890,13 @@ std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, overlapped.Offset = static_cast(offset); overlapped.OffsetHigh = static_cast(static_cast(offset) >> 32u); DWORD written = 0; - const BOOL succeeded = ::WriteFile(handle, buffer, static_cast(std::min(count, std::numeric_limits::max())), &written, &overlapped); + const BOOL succeeded = ThreadCancel::Native([&] { return ::WriteFile(handle, buffer, static_cast(std::min(count, std::numeric_limits::max())), &written, &overlapped) ? 0 : -1; }) == 0; const DWORD writeError = succeeded ? ERROR_SUCCESS : ::GetLastError(); if (!::SetFilePointerEx(handle, previous, nullptr, FILE_BEGIN)) throw std::runtime_error("pwrite: restoring the host file position failed"); - if (!succeeded) { errno = writeError == ERROR_ACCESS_DENIED ? EBADF : EIO; return -1; } + if (!succeeded) { if (written != 0) return written; errno = writeError == ERROR_OPERATION_ABORTED ? EINTR : writeError == ERROR_ACCESS_DENIED ? EBADF : EIO; return -1; } const auto result = written; #else - const auto result = ::pwrite(fd, buffer, static_cast(count), static_cast(offset)); + const auto result = ThreadCancel::Native([&] { return ::pwrite(fd, buffer, static_cast(count), static_cast(offset)); }); #endif return static_cast(result); } @@ -949,8 +975,10 @@ static std::int64_t TransferSharedMemoryVectors(int descriptor, const struct iov available -= length; } if (bounded.empty()) return 0; - return positioned ? ::pwritev(fd, bounded.data(), static_cast(bounded.size()), static_cast(offset)) - : ::writev(fd, bounded.data(), static_cast(bounded.size())); + return ThreadCancel::Native([&] { + return positioned ? ::pwritev(fd, bounded.data(), static_cast(bounded.size()), static_cast(offset)) + : ::writev(fd, bounded.data(), static_cast(bounded.size())); + }); } std::int64_t WritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count) { diff --git a/core/libs/tests/GuestIoSharedWriteCancel.cpp b/core/libs/tests/GuestIoSharedWriteCancel.cpp new file mode 100644 index 00000000000..7584ee7a62c --- /dev/null +++ b/core/libs/tests/GuestIoSharedWriteCancel.cpp @@ -0,0 +1,169 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libc/include/Api.hpp" +#include "nid/Sha1.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s, guest errno %d\n", message, *__error_nid_postfix()); + std::exit(1); +} +template +static bool Observe(TPredicate predicate, int milliseconds = 2000) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +class NativeLock { +public: + explicit NativeLock(int descriptor) { +#ifdef _WIN32 + BY_HANDLE_FILE_INFORMATION info{}; + Require(GetFileInformationByHandle(reinterpret_cast(::_get_osfhandle(descriptor)), &info), "identify shared object"); + const auto identity = L"object-" + std::to_wstring(info.dwVolumeSerialNumber) + L"-" + + std::to_wstring(info.nFileIndexHigh) + L"-" + std::to_wstring(info.nFileIndexLow); + const auto* begin = reinterpret_cast(identity.data()); + const auto digest = Nid::Sha1(std::vector(begin, begin + identity.size() * sizeof(wchar_t))); + std::wstring name = L"Global\\AnyPS5NamedShm-"; + for (const auto byte : digest) { name += L"0123456789abcdef"[byte >> 4]; name += L"0123456789abcdef"[byte & 15]; } + handle = CreateMutexW(nullptr, FALSE, name.c_str()); + Require(handle && WaitForSingleObject(handle, 2000) == WAIT_OBJECT_0, "hold shared object lock"); +#else + static_cast(descriptor); + handle = ::open("/dev/shm", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + Require(handle >= 0 && ::flock(handle, LOCK_EX) == 0, "hold shared namespace lock"); +#endif + } + ~NativeLock() { Release(); } + void Release() { +#ifdef _WIN32 + if (!handle) return; + Require(ReleaseMutex(handle), "release shared object lock"); + CloseHandle(handle); handle = nullptr; +#else + if (handle < 0) return; + Require(::flock(handle, LOCK_UN) == 0, "release shared namespace lock"); + ::close(handle); handle = -1; +#endif + } +private: +#ifdef _WIN32 + HANDLE handle = nullptr; +#else + int handle = -1; +#endif +}; + +struct State { + int descriptor; + std::string api; + bool disabled; + PthreadKey key = -1; + std::atomic ready{false}, returned{false}; + std::atomic cleanups{0}, destructors{0}; + std::int64_t result = -2; + int error = 0; +}; +static void APS5_VABI Cleanup(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.fetch_add(1) == 0 && state.destructors.load() == 0, "shared write cleanup order"); +} +static void APS5_VABI Destroy(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.load() == 1 && state.destructors.fetch_add(1) == 0, "shared write TSD order"); +} +static void* APS5_VABI Writer(void* opaque) { + auto& state = *static_cast(opaque); + if (state.disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable shared writer cancellation"); + Require(scePthreadSetspecific(state.key, &state) == 0, "set shared write TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); + KernelIovec vectors[2]{{const_cast("w"), 1}, {const_cast("w"), 1}}; + state.ready.store(true); + if (state.api == "write") state.result = write_nid_postfix(state.descriptor, "ww", 2); + else if (state.api == "alias") state.result = _write_nid_postfix(state.descriptor, "ww", 2); + else if (state.api == "pwrite") state.result = pwrite_nid_disambig1_nid_postfix(state.descriptor, "ww", 2, 0); + else if (state.api == "writev") state.result = writev_nid_postfix(state.descriptor, vectors, 2); + else if (state.api == "writev_alias") state.result = _writev_nid_postfix(state.descriptor, vectors, 2); + else if (state.api == "pwritev") state.result = pwritev_nid_postfix(state.descriptor, vectors, 2, 0); + else Require(false, "unknown shared write API"); + state.error = *__error_nid_postfix(); + state.returned.store(true); + if (state.disabled) { + Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable shared writer cancellation"); + scePthreadTestcancel(); + } + __pthread_cleanup_pop_imp_nid_postfix(1); + return reinterpret_cast(std::uintptr_t{7}); +} + +int main(int argc, char** argv) { + Require(argc == 3, "expected shared write API and scenario"); + const std::string scenario = argv[2]; + const bool close = scenario == "close" || scenario == "disabled_close"; + const bool disabled = scenario.starts_with("disabled"); + const bool cancel = scenario == "cancel" || disabled; +#ifdef _WIN32 + const auto process = GetCurrentProcessId(); +#else + const auto process = getpid(); +#endif + const auto name = "/anyps5-write-cancel-" + std::to_string(process) + "-" + argv[1] + "-" + scenario; + const int descriptor = shm_open_nid_postfix(name.c_str(), 2 | 0x200 | 0x800, 0600); + Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, 0x4000) == 0, "create shared write object"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, "safe", 4, 0) == 4, "seed shared write object"); + const int alias = dup_nid_postfix(descriptor); + Require(alias >= 0, "retain shared write alias"); + NativeLock held(descriptor); + State state{descriptor, argv[1], disabled}; + Require(scePthreadKeyCreate(&state.key, reinterpret_cast(Destroy)) == 0, "create shared write TSD key"); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, &state, nullptr) == 0, "create shared writer"); + Require(Observe([&] { return state.ready.load(); }), "shared writer did not start"); + if (!disabled) Observe([&] { std::lock_guard lock(writer->cancelLock); return writer->cancelIo != nullptr; }, 250); + if (cancel) Require(pthread_cancel_nid_postfix(writer) == 0, "request shared write cancellation"); + std::atomic closed{false}; + std::thread closer; + if (close) closer = std::thread([&] { Require(close_nid_postfix(descriptor) == 0, "close contended shared writer"); closed.store(true); }); + bool prompt = true; + if (scenario == "cancel" || close) prompt = Observe([&] { return writer->_finished.load() && (!close || closed.load()); }, 500); + else Require(!state.returned.load(), "shared write escaped held metadata lock"); + held.Release(); + Require(Observe([&] { return writer->_finished.load(); }), "shared writer did not finish after release"); + void* result = nullptr; + Require(scePthreadJoin(writer, &result) == 0, "join shared writer"); + if (closer.joinable()) closer.join(); + Require(prompt, "cancellation/close required releasing shared metadata lock"); + Require(state.cleanups.load() == 1 && state.destructors.load() == 1, "shared write cleanup or TSD repeated"); + if (scenario == "cancel") Require(result == GUEST_CANCELED && !state.returned.load(), "contended shared write escaped cancellation"); + else if (close) Require(result == (disabled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})) && state.returned.load() && state.result == -1 && state.error == 4, "shared close did not interrupt safely"); + else Require(result == (disabled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})) && state.returned.load() && state.result == 2, "shared completion lost bytes or pending cancellation"); + std::array bytes{}; + Require(pread_nid_postfix(alias, bytes.data(), bytes.size(), 0) == 4 && std::memcmp(bytes.data(), scenario == "cancel" || close ? "safe" : "wwfe", 4) == 0, "shared write touched wrong bytes"); + Require(lseek_nid_postfix(alias, 0, 1) == (scenario == "cancel" || close || state.api.starts_with("pwrite") ? 0 : 2), "shared write offset changed unexpectedly"); + Require(close_nid_postfix(alias) == 0 && (close || close_nid_postfix(descriptor) == 0), "close shared write handles"); + Require(shm_unlink_nid_postfix(name.c_str()) == 0 && scePthreadKeyDelete(state.key) == 0, "remove shared write resources"); +} diff --git a/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp index 088119df948..70d0f02c1d6 100644 --- a/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp +++ b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp @@ -89,7 +89,7 @@ static bool ReplaceDescriptor() { swapped = true; bool replaced = false; std::thread reuse([&] { - replaced = close_nid_postfix(original) == 0 && dup2_nid_postfix(replacement, original) == original; + replaced = ::syscall(SYS_close, original) == 0 && ::syscall(SYS_dup2, replacement, original) == original; }); reuse.join(); return replaced; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index eb4ff39f9f5..b8dc4c8603a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -207,7 +207,7 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. Connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation 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 write cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) is not console-verified. It covers `write`, `_write`, `writev`, `_writev`, `pwrite`, `pwritev`, `send`, `sendto`, `sendmsg`, `_sendto` and `_sendmsg` on accepted file, pipe and socket descriptors; positioned socket and pipe writes retain their existing errors. Positive partial counts win over pending cancellation; zero and failed writes check cancellation after guards release, following FreeBSD libthr. SCE writes and libSceNet gain no cancellation points. Non-interruptible drivers can delay cancellation and close indefinitely. Windows pipe quota waits retry readiness at 1 ms intervals. Descriptor replacement and stdio closure can still wait indefinitely; stream-socket writes share the read admission and close/drain machinery. +- [POSIX write cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) is not console-verified. It covers `write`, `_write`, `writev`, `_writev`, `pwrite`, `pwritev`, `send`, `sendto`, `sendmsg`, `_sendto` and `_sendmsg` on accepted file, shared-memory, pipe and socket descriptors; positioned socket and pipe writes retain their existing errors. Positive partial counts win over pending cancellation; zero and failed writes check cancellation after guards release, following FreeBSD libthr. SCE writes and libSceNet gain no cancellation points. Non-interruptible drivers can delay cancellation and close indefinitely. Windows pipe quota waits retry readiness at 1 ms intervals. Descriptor replacement and stdio closure can still wait indefinitely; stream-socket writes share the read admission and close/drain machinery. - [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 and write waits and run after operation guards are released, including disabled cancellation and unregistered callers. Restartable operations retry only if descriptor generations remain unchanged; socket transfer 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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From beb483798d61b2411d6e938cc19ce44a53c734f1 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 10:46:52 -0300 Subject: [PATCH 26/61] test(libkernel): tighten blocked-write lifecycle handshakes --- core/libs/tests/GuestIoSharedWriteCancel.cpp | 6 +++++- core/libs/tests/GuestIoWriteCancel.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestIoSharedWriteCancel.cpp b/core/libs/tests/GuestIoSharedWriteCancel.cpp index 7584ee7a62c..9927336469a 100644 --- a/core/libs/tests/GuestIoSharedWriteCancel.cpp +++ b/core/libs/tests/GuestIoSharedWriteCancel.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/DescriptorAccess.hpp" #include "nid/Sha1.hpp" #include @@ -143,7 +144,10 @@ int main(int argc, char** argv) { Pthread writer = nullptr; Require(scePthreadCreate(&writer, nullptr, Writer, &state, nullptr) == 0, "create shared writer"); Require(Observe([&] { return state.ready.load(); }), "shared writer did not start"); - if (!disabled) Observe([&] { std::lock_guard lock(writer->cancelLock); return writer->cancelIo != nullptr; }, 250); + Require(Observe([&] { + GuestDescriptorAccess probe(descriptor, GuestDescriptorAccess::Mode::Update, std::defer_lock); + return !probe.TryLock(); + }), "shared writer did not acquire descriptor admission"); if (cancel) Require(pthread_cancel_nid_postfix(writer) == 0, "request shared write cancellation"); std::atomic closed{false}; std::thread closer; diff --git a/core/libs/tests/GuestIoWriteCancel.cpp b/core/libs/tests/GuestIoWriteCancel.cpp index 42cbe37e609..49e5ad3e62f 100644 --- a/core/libs/tests/GuestIoWriteCancel.cpp +++ b/core/libs/tests/GuestIoWriteCancel.cpp @@ -307,7 +307,7 @@ static void Run(const char* transport, const char* api, const char* scenario) { Drain(state, static_cast(state.result), 'w'); } else if (!partial) Require((result == GUEST_CANCELED && !state.returned.load()) || - (state.socket && result == reinterpret_cast(std::uintptr_t{7}) && state.returned.load() && state.result > 0 && state.result <= 2), "blocked write neither canceled nor preserved completed bytes"); + (!pending && state.socket && result == reinterpret_cast(std::uintptr_t{7}) && state.returned.load() && state.result > 0 && state.result <= 2), "blocked write neither canceled nor preserved completed bytes"); if (state.socket && !completion && !partial && !peerClose && !race && state.result > 0) { Drain(state, filled, 'f'); Drain(state, static_cast(state.result), 'w'); From 3c7e9ec0a263292045b950c1efd68b567b7c9248 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 15:28:07 -0300 Subject: [PATCH 27/61] test(libkernel): cover synchronous-write cancellation boundaries --- core/libs/prx/libkernel/CMakeLists.txt | 19 ++ .../File/include/DescriptorControl.hpp | 2 + .../libkernel/File/src/DescriptorControl.cpp | 9 +- core/libs/tests/GuestIoWriteSyncCancel.cpp | 199 ++++++++++++++++++ 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestIoWriteSyncCancel.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9f9e936e9fe..9df6df9e225 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -261,6 +261,25 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_io_native_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_io_write_sync_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoWriteSyncCancel.cpp) + target_include_directories(guest_io_write_sync_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_write_sync_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_io_write_sync_cancel_tests) + foreach(syncWriteApi write writev pwrite pwritev) + foreach(syncWriteCase cancel disabled complete zero error close signal) + set(syncWriteTest guest_io_write_sync_${syncWriteApi}__${syncWriteCase}) + add_test(NAME ${syncWriteTest} COMMAND guest_io_write_sync_cancel_tests ${syncWriteApi} ${syncWriteCase}) + set_tests_properties(${syncWriteTest} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(${syncWriteTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + add_test(NAME guest_io_write_sync_sce COMMAND guest_io_write_sync_cancel_tests sce cancel) + set_tests_properties(guest_io_write_sync_sce PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_io_write_sync_sce PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test_executable(guest_io_write_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoWriteCancel.cpp) target_include_directories(guest_io_write_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_io_write_cancel_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index f2555a2dae9..ccf95c73ed6 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -19,6 +19,8 @@ void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); void SyncWrites(int fd); +using SynchronousWriteHook = int (*)(int); +extern "C" void FileSetSynchronousWriteHookForTest_nid_no_patch(SynchronousWriteHook hook); class WriteSync { public: diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 60e2b1169e4..425418e12fd 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -11,6 +11,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -640,6 +641,11 @@ void ForgetDescriptor(int fd) { } std::uint64_t g_synchronousWrites = 0; +std::atomic g_synchronousWriteHook{nullptr}; + +extern "C" void FileSetSynchronousWriteHookForTest_nid_no_patch(SynchronousWriteHook hook) { + g_synchronousWriteHook.store(hook); +} void SyncWrites(int fd) { { @@ -648,7 +654,8 @@ void SyncWrites(int fd) { if (found == g_entries.end() || !found->second.status || (*found->second.status & Synchronous) == 0) return; } if (Classify(fd, "write") != Kind::File) return; - if (const int result = sceKernelFsync(fd); result != 0) + const auto hook = g_synchronousWriteHook.load(); + if (const int result = hook ? hook(fd) : sceKernelFsync(fd); result != 0) throw std::runtime_error("write: syncing an O_FSYNC file failed, fd=" + std::to_string(fd) + ", error=" + std::to_string(result & 0xffff)); std::lock_guard lock(g_mutex); ++g_synchronousWrites; diff --git a/core/libs/tests/GuestIoWriteSyncCancel.cpp b/core/libs/tests/GuestIoWriteSyncCancel.cpp new file mode 100644 index 00000000000..a0058e1d7d1 --- /dev/null +++ b/core/libs/tests/GuestIoWriteSyncCancel.cpp @@ -0,0 +1,199 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +static void Require(bool condition, const char* text) { + if (condition) return; + std::fprintf(stderr, "%s; guest errno %d\n", text, *__error_nid_postfix()); + 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 file = -1; + PthreadKey key = -1; + std::string api, scenario; + std::atomic calls{0}, cleanups{0}, destructors{0}, signals{0}; + std::atomic entered{false}, finished{false}, returned{false}; + std::int64_t result = -2; + int error = 0; +#ifdef _WIN32 + HANDLE input = INVALID_HANDLE_VALUE, output = INVALID_HANDLE_VALUE; +#else + int input = -1, output = -1; +#endif +}; + +static State* state = nullptr; + +static int Flush(int file) { + Require(file == state->file, "flush used a different descriptor"); + ++state->calls; + if (state->scenario == "zero" || state->scenario == "error") return 0; + state->entered.store(true); + char token = 0; +#ifdef _WIN32 + DWORD count = 0; + const bool completed = ReadFile(state->input, &token, 1, &count, nullptr) && count == 1; +#else + const bool completed = ::read(state->input, &token, 1) == 1; +#endif + if (!completed) return static_cast(0x80020004u); +#ifdef _WIN32 + Require(::_commit(file) == 0, "native file flush failed"); +#else + Require(::fsync(file) == 0, "native file flush failed"); +#endif + return 0; +} + +static void Release() { + const char token = 'x'; +#ifdef _WIN32 + DWORD count = 0; + Require(WriteFile(state->output, &token, 1, &count, nullptr) && count == 1, "release native flush wait"); +#else + Require(::write(state->output, &token, 1) == 1, "release native flush wait"); +#endif +} + +static void APS5_VABI Cleanup(void*) { + Require(state->cleanups.fetch_add(1) == 0 && state->destructors.load() == 0, "cleanup order"); + if (state->scenario != "close") Require(fcntl_nid_postfix(state->file, 4, 0) == 0, "cleanup did not release descriptor admission"); +} + +static void APS5_VABI Destroy(void*) { + Require(state->cleanups.load() == 1 && state->destructors.fetch_add(1) == 0, "TSD order"); + state->finished.store(true); +} + +static void APS5_VABI Handler(int) { ++state->signals; } + +static std::int64_t Write(std::size_t size) { + const char bytes[] = {'a', 'b'}; + const KernelIovec vectors[]{{const_cast(bytes), size / 2}, {const_cast(bytes) + size / 2, size - size / 2}}; + if (state->api == "write") return write_nid_postfix(state->file, bytes, size); + if (state->api == "writev") return writev_nid_postfix(state->file, vectors, 2); + if (state->api == "pwrite") return pwrite_nid_disambig1_nid_postfix(state->file, bytes, size, 0); + if (state->api == "pwritev") return pwritev_nid_postfix(state->file, vectors, 2, 0); + return sceKernelWrite(state->file, bytes, size); +} + +static void* APS5_VABI Writer(void*) { + if (state->scenario == "disabled") Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable cancellation"); + Require(scePthreadSetspecific(state->key, state) == 0, "set flush TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, state, &cleanup); + state->result = Write(2); + state->error = *__error_nid_postfix(); + state->returned.store(true); + if (state->scenario == "disabled" || state->api == "sce") { + Require(state->result == 2, "disabled or SCE flush lost completion"); + Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable cancellation"); + scePthreadTestcancel(); + } + __pthread_cleanup_pop_imp_nid_postfix(1); + return reinterpret_cast(std::uintptr_t{7}); +} + +int main(int argc, char** argv) { + Require(argc == 3, "expected API and scenario"); + State storage; + state = &storage; + state->api = argv[1]; state->scenario = argv[2]; + const auto path = "anyps5-sync-cancel-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + state->file = open_nid_postfix(path.c_str(), 2 | 0x200, 0600); + Require(state->file >= 0 && fcntl_nid_postfix(state->file, 4, 0x80) == 0, "create synchronous file"); + if (state->scenario == "error") { + Require(close_nid_postfix(state->file) == 0, "close writable file"); + state->file = open_nid_postfix(path.c_str(), 0x80, 0600); + Require(state->file >= 0, "reopen read-only synchronous file"); + } + File::FileSetSynchronousWriteHookForTest_nid_no_patch(Flush); + if (state->scenario == "zero" || state->scenario == "error") { + const auto result = Write(state->scenario == "zero" ? 0 : 2); + Require(result == (state->scenario == "zero" ? 0 : -1), "zero or failed write result"); + Require(state->calls.load() == 0, "zero-progress write entered a flush"); + } else { +#ifdef _WIN32 + Require(CreatePipe(&state->input, &state->output, nullptr, 4096), "create native flush wait pipe"); +#else + int pipe[2]{}; + Require(::pipe(pipe) == 0, "create native flush wait pipe"); + state->input = pipe[0]; state->output = pipe[1]; +#endif + if (state->scenario == "signal") { + GuestSigaction action{}; + action.handler = reinterpret_cast(&Handler); + action.flags = 2; + Require(sigaction_nid_postfix(30, &action, nullptr) == 0, "install restartable handler"); + } + Require(scePthreadKeyCreate(&state->key, reinterpret_cast(Destroy)) == 0, "create flush TSD key"); + Pthread writer{}; + Require(scePthreadCreate(&writer, nullptr, Writer, state, "sync-write") == 0, "start synchronous writer"); + Require(Observe([&] { return state->entered.load(); }), "writer did not enter native flush"); + std::atomic closed{false}; + std::thread closer; + if (state->scenario == "close") { + closer = std::thread([&] { Require(close_nid_postfix(state->file) == 0, "close flushing descriptor"); closed.store(true); }); + } else if (state->scenario == "signal") { + Require(pthread_kill_nid_postfix(writer, 30) == 0, "interrupt flushing writer"); + } else if (state->scenario != "complete") { + Require(scePthreadCancel(writer) == 0, "cancel flushing writer"); + } + const bool waiting = state->scenario == "disabled" || state->scenario == "complete" || state->api == "sce"; + bool woke = false; + if (waiting) Release(); + else { + woke = Observe([&] { return state->finished.load(); }); + if (!woke) Release(); + } + Require(Observe([&] { return state->finished.load(); }), "writer did not finish after native flush release"); + void* joined = nullptr; + Require(scePthreadJoin(writer, &joined) == 0, "join flushing writer"); + if (closer.joinable()) { Require(Observe([&] { return closed.load(); }), "close did not drain flush"); closer.join(); } + Require(waiting || woke, "cancellation or close did not wake native flush"); + const bool canceled = state->scenario == "cancel" || state->scenario == "disabled" || state->api == "sce"; + Require(joined == (canceled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "unexpected flush join result"); + Require(state->cleanups.load() == 1 && state->destructors.load() == 1 && state->calls.load() == 1, "flush repeated or cleanup lost"); + if (canceled) Require(state->returned.load() == waiting, "canceled flush completion state"); + else if (state->scenario == "complete") Require(state->result == 2, "completed flush lost positive result"); + else Require(state->result == -1 && state->error == 4, "interrupted flush did not return EINTR"); + if (state->scenario == "signal") Require(state->signals.load() == 1, "restartable signal lost or write repeated"); + Require(scePthreadKeyDelete(state->key) == 0, "delete flush TSD key"); +#ifdef _WIN32 + CloseHandle(state->input); CloseHandle(state->output); +#else + ::close(state->input); ::close(state->output); +#endif + } + File::FileSetSynchronousWriteHookForTest_nid_no_patch(nullptr); + if (state->scenario != "close") Require(close_nid_postfix(state->file) == 0, "close synchronous file"); + std::filesystem::remove(path); + return 0; +} From 7480e2fa726171dbae0874ed2ac8671dfc1046aa Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 16:02:09 -0300 Subject: [PATCH 28/61] fix(libkernel): cancel synchronous flushes and restricted pipe writes --- core/libs/prx/libkernel/CMakeLists.txt | 16 ++ .../File/include/DescriptorControl.hpp | 13 +- .../libkernel/File/src/DescriptorControl.cpp | 16 +- core/libs/prx/libkernel/File/src/Open.cpp | 2 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 64 +++++--- core/libs/tests/GuestIoWriteOnlyPipe.cpp | 145 ++++++++++++++++++ core/libs/tests/GuestIoWriteSyncCancel.cpp | 35 +++-- docs/dev/debt/libkernel.md | 3 +- 8 files changed, 256 insertions(+), 38 deletions(-) create mode 100644 core/libs/tests/GuestIoWriteOnlyPipe.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9df6df9e225..a1dd13300a8 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -262,6 +262,19 @@ if(BUILD_TESTING) set_tests_properties(guest_io_native_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_io_write_sync_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoWriteSyncCancel.cpp) + if(WIN32) + add_test_executable(guest_io_write_only_pipe_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoWriteOnlyPipe.cpp) + target_include_directories(guest_io_write_only_pipe_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_io_write_only_pipe_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_io_write_only_pipe_tests) + foreach(minimalPipeApi write writev) + foreach(minimalPipeCase complete cancel disabled close nonblocking) + set(minimalPipeTest guest_io_write_minimal_pipe_${minimalPipeApi}__${minimalPipeCase}) + add_test(NAME ${minimalPipeTest} COMMAND guest_io_write_only_pipe_tests ${minimalPipeApi} ${minimalPipeCase}) + set_tests_properties(${minimalPipeTest} PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + endforeach() + endif() target_include_directories(guest_io_write_sync_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_io_write_sync_cancel_tests PRIVATE libkernel libc) configure_windows_unwind(guest_io_write_sync_cancel_tests) @@ -276,9 +289,12 @@ if(BUILD_TESTING) endforeach() endforeach() add_test(NAME guest_io_write_sync_sce COMMAND guest_io_write_sync_cancel_tests sce cancel) + add_test(NAME guest_io_write_sync_sce_legacy COMMAND guest_io_write_sync_cancel_tests sce legacy) set_tests_properties(guest_io_write_sync_sce PROPERTIES TIMEOUT 15) + set_tests_properties(guest_io_write_sync_sce_legacy PROPERTIES TIMEOUT 15) if(WIN32) set_tests_properties(guest_io_write_sync_sce PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_io_write_sync_sce_legacy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_io_write_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoWriteCancel.cpp) target_include_directories(guest_io_write_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index ccf95c73ed6..2f7133c4cb4 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -3,6 +3,7 @@ #include #include +#include "prx/libkernel/Pthread/include/Cancel.hpp" namespace File { @@ -18,20 +19,26 @@ void RememberPipe(int fd); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); -void SyncWrites(int fd); +bool SyncWrites(int fd); using SynchronousWriteHook = int (*)(int); extern "C" void FileSetSynchronousWriteHookForTest_nid_no_patch(SynchronousWriteHook hook); class WriteSync { public: - explicit WriteSync(int fd) : _fd(fd), _exceptions(std::uncaught_exceptions()) {} + explicit WriteSync(int fd) : _fd(fd), _exceptions(std::uncaught_exceptions()), posix(ThreadCancel::IoActive()) {} ~WriteSync() noexcept(false) { - if (std::uncaught_exceptions() == _exceptions) SyncWrites(_fd); + if (!posix && std::uncaught_exceptions() == _exceptions) SyncWrites(_fd); + } + template + TResult Complete(TResult result) const { + if (posix && result > 0 && _fd >= 0 && !SyncWrites(_fd)) return static_cast(-1); + return result; } private: int _fd; int _exceptions; + bool posix; }; bool IsVnode(int fd); bool IsCharacterDevice(int fd); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 425418e12fd..6c49c307333 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -5,6 +5,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/FileLock.hpp" @@ -647,18 +648,25 @@ extern "C" void FileSetSynchronousWriteHookForTest_nid_no_patch(SynchronousWrite g_synchronousWriteHook.store(hook); } -void SyncWrites(int fd) { +bool SyncWrites(int fd) { { std::lock_guard lock(g_mutex); const auto found = g_entries.find(fd); - if (found == g_entries.end() || !found->second.status || (*found->second.status & Synchronous) == 0) return; + if (found == g_entries.end() || !found->second.status || (*found->second.status & Synchronous) == 0) return true; } - if (Classify(fd, "write") != Kind::File) return; + if (Classify(fd, "write") != Kind::File) return true; const auto hook = g_synchronousWriteHook.load(); - if (const int result = hook ? hook(fd) : sceKernelFsync(fd); result != 0) + if (const int result = ThreadCancel::Native([&] { return hook ? hook(fd) : sceKernelFsync(fd); }); result != 0) { + if (ThreadCancel::IoActive() && (ThreadCancel::IoInterrupted() || (static_cast(result) & 0xffff) == EINTR)) { + ThreadCancel::IoNoRestart(); + errno = EINTR; + return false; + } throw std::runtime_error("write: syncing an O_FSYNC file failed, fd=" + std::to_string(fd) + ", error=" + std::to_string(result & 0xffff)); + } std::lock_guard lock(g_mutex); ++g_synchronousWrites; + return true; } bool Appends(int fd) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 976c73b6b70..c294b47b329 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -431,7 +431,7 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes } const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); - auto n = NativeWrite(d, buf, nbytes); + auto n = sync.Complete(NativeWrite(d, buf, nbytes)); if (n < 0) return SceIoErrorFromErrno(errno); return static_cast(n); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 7aa81a319c8..f482a8f125a 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -228,6 +228,10 @@ static int NativeSync(int descriptor) { const auto pipe = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); if (pipe != INVALID_HANDLE_VALUE && GetFileType(pipe) == FILE_TYPE_PIPE) { errno = EINVAL; return -1; } if (::_commit(descriptor) == 0) return 0; + if (ThreadCancel::IoActive() && (ThreadCancel::IoInterrupted() || _doserrno == ERROR_OPERATION_ABORTED)) { + errno = EINTR; + return -1; + } const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); if (original == INVALID_HANDLE_VALUE || GetFileType(original) != FILE_TYPE_DISK) { errno = EBADF; return -1; } handle = ReOpenFile(original, GENERIC_WRITE, share, 0); @@ -282,16 +286,22 @@ std::optional File::IoPositionedTransfer(int descr std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t bytes) { const auto handle = WindowsDescriptorHandle_nid_no_patch(descriptor); const auto type = GetFileType(handle); - if (!ThreadCancel::IoActive() || (type != FILE_TYPE_PIPE && type != FILE_TYPE_DISK)) { + const auto nativeWrite = [&] { const auto result = ThreadCancel::Native([&] { return ::_write(descriptor, buffer, static_cast(bytes)); }); - if (result < 0 && ThreadCancel::IoActive() && _doserrno == ERROR_OPERATION_ABORTED) errno = EINTR; + if (result < 0 && ThreadCancel::IoActive()) { + if (_doserrno == ERROR_OPERATION_ABORTED) errno = EINTR; + else if (type == FILE_TYPE_PIPE && errno == ENOSPC && _doserrno == 0) errno = EAGAIN; + else if (type == FILE_TYPE_PIPE && (_doserrno == ERROR_BROKEN_PIPE || _doserrno == ERROR_NO_DATA)) errno = EPIPE; + } return result; - } + }; + if (!ThreadCancel::IoActive() || (type != FILE_TYPE_PIPE && type != FILE_TYPE_DISK)) return nativeWrite(); const bool pipe = type == FILE_TYPE_PIPE; DWORD mode = 0; - if (pipe && !GetNamedPipeHandleStateW(handle, &mode, nullptr, nullptr, nullptr, nullptr, 0)) { + if (pipe && ThreadCancel::Native([&] { return GetNamedPipeHandleStateW(handle, &mode, nullptr, nullptr, nullptr, nullptr, 0) ? 0 : -1; }) < 0) { const auto error = GetLastError(); - errno = error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? EPIPE : EIO; + if (error == ERROR_ACCESS_DENIED) return nativeWrite(); + errno = ThreadCancel::IoInterrupted() ? EINTR : error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? EPIPE : EIO; return -1; } const bool nonblocking = pipe && (mode & PIPE_NOWAIT) != 0; @@ -305,7 +315,14 @@ std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t if (pipe && amount != 0) { std::int64_t available = 0; bool eof = false, writable = false; - if (!GuestSockets::PipeWriteState(descriptor, &available, &eof, &writable)) { errno = EBADF; return transferred == 0 ? -1 : transferred; } + int checked; + try { + checked = ThreadCancel::Native([&] { return GuestSockets::PipeWriteState(descriptor, &available, &eof, &writable) ? 0 : -1; }); + } catch (const std::runtime_error&) { + if (!ThreadCancel::IoInterrupted()) throw; + checked = -1; + } + if (checked < 0) { errno = ThreadCancel::IoInterrupted() ? EINTR : EBADF; return transferred == 0 ? -1 : transferred; } if (eof) { errno = EPIPE; return transferred == 0 ? -1 : transferred; } if (available == 0 || (bytes <= 512 && available < static_cast(amount))) { if (nonblocking) { errno = EAGAIN; return transferred == 0 ? -1 : transferred; } @@ -326,6 +343,7 @@ std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t error == ERROR_DISK_FULL || error == ERROR_HANDLE_DISK_FULL ? ENOSPC : EIO; return transferred == 0 ? -1 : transferred; } + if (nonblocking && transferred == 0 && amount != 0) { errno = EAGAIN; return -1; } if (nonblocking || done == 0) break; } while (transferred < bytes); return static_cast(transferred); @@ -1242,7 +1260,7 @@ int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_ const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; - const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset); + const auto result = sync.Complete(NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset)); return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; }); }); @@ -1607,7 +1625,7 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; - const auto result = NativePwrite(d, nbytes == 0 && buf == nullptr ? &empty : buf, nbytes, offset); + const auto result = sync.Complete(NativePwrite(d, nbytes == 0 && buf == nullptr ? &empty : buf, nbytes, offset)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1641,8 +1659,13 @@ 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::IoPositionedTransfer(d); + auto positioned = File::IoPositionedTransfer(d); if (!positioned) return SceErrorFromErrno(errno); + const auto finish = [&](std::int64_t result) { + positioned->unlock(); + const auto completed = sync.Complete(result); + return result > 0 && completed < 0 ? SceErrorFromErrno(errno) : completed; + }; if (total == 0) { char none = 0; const auto result = offset != nullptr ? NativePositionedTransfer(d, &none, 0, *offset, write) : NativeTransferBuffer(d, &none, 0, write); @@ -1663,7 +1686,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn } const auto result = offset != nullptr ? NativePositionedTransfer(d, gathered.data(), gathered.size(), *offset, true) : NativeTransferBuffer(d, gathered.data(), gathered.size(), true); - return result < 0 ? SceErrorFromErrno(errno) : result; + return finish(result < 0 ? SceErrorFromErrno(errno) : result); } const bool whole = write || offset != nullptr || NativeIsDisk(d); std::int64_t done = 0; @@ -1672,13 +1695,13 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn for (std::size_t position = 0; position < iov[i].length;) { const auto chunk = std::min(iov[i].length - position, std::numeric_limits::max()); const auto result = offset != nullptr ? NativePositionedTransfer(d, base + position, chunk, *offset + done, write) : NativeTransferBuffer(d, base + position, chunk, write); - if (result < 0) return done != 0 ? done : SceErrorFromErrno(errno); + if (result < 0) return finish(done != 0 ? done : SceErrorFromErrno(errno)); done += result; position += static_cast(result); - if (static_cast(result) < chunk || (!whole && !NativePipeHasData(d))) return done; + if (static_cast(result) < chunk || (!whole && !NativePipeHasData(d))) return finish(done); } } - return done; + return finish(done); } int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { @@ -1721,7 +1744,7 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t if (const auto error = PositionedGuestSocketError(d); error != 0) return error; const File::WriteSync sync(d); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); - const auto result = NativePwrite(d, buf, nbytes, offset); + const auto result = sync.Complete(NativePwrite(d, buf, nbytes, offset)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1753,9 +1776,9 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt } const File::WriteSync sync(d); PrepareWriteIovecs(iov, iovcnt); - const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt); + const auto sharedResult = sync.Complete(File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt)); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; - const auto result = ThreadCancel::Native([&] { return static_cast(::writev(d, NativeIovecs(iov), iovcnt)); }); + const auto result = sync.Complete(ThreadCancel::Native([&] { return static_cast(::writev(d, NativeIovecs(iov), iovcnt)); })); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1783,12 +1806,15 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcn if (const auto error = PositionedGuestSocketError(d); error != 0) return error; const File::WriteSync sync(d); PrepareWriteIovecs(iov, iovcnt); - const auto sharedResult = File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset); + const auto sharedResult = sync.Complete(File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset)); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; #ifdef __APPLE__ - if (const auto appended = File::WritevAtEnd(d, NativeIovecs(iov), iovcnt); appended != -2) return appended < 0 ? SceErrorFromErrno(errno) : appended; + if (const auto appended = File::WritevAtEnd(d, NativeIovecs(iov), iovcnt); appended != -2) { + const auto result = sync.Complete(appended); + return result < 0 ? SceErrorFromErrno(errno) : result; + } #endif - const auto result = ThreadCancel::Native([&] { return static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); }); + const auto result = sync.Complete(ThreadCancel::Native([&] { return static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); })); return result < 0 ? SceErrorFromErrno(errno) : result; } diff --git a/core/libs/tests/GuestIoWriteOnlyPipe.cpp b/core/libs/tests/GuestIoWriteOnlyPipe.cpp new file mode 100644 index 00000000000..deae8be827c --- /dev/null +++ b/core/libs/tests/GuestIoWriteOnlyPipe.cpp @@ -0,0 +1,145 @@ +#include "prx/libkernel/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 "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libc/include/Api.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; guest errno %d, Windows error %lu\n", message, *__error_nid_postfix(), GetLastError()); + 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 file = -1; + bool vector = false, disabled = false; + std::atomic ready{false}, returned{false}, done{false}; + std::atomic cleanups{0}; + std::int64_t result = -2; + int error = 0; +}; + +static void APS5_VABI Cleanup(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.fetch_add(1) == 0, "repeated pipe cleanup"); + state.done.store(true); +} + +static void* APS5_VABI Writer(void* opaque) { + auto& state = *static_cast(opaque); + if (state.disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable pipe cancellation"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); + state.ready.store(true); + char byte = 'w'; + const KernelIovec vectors[]{{nullptr, 0}, {&byte, 1}}; + state.result = state.vector ? writev_nid_postfix(state.file, vectors, 2) : write_nid_postfix(state.file, &byte, 1); + state.error = *__error_nid_postfix(); + state.returned.store(true); + if (state.disabled) { + Require(state.result == 1, "disabled pipe writer lost completion"); + Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable pipe cancellation"); + scePthreadTestcancel(); + } + __pthread_cleanup_pop_imp_nid_postfix(1); + return reinterpret_cast(std::uintptr_t{7}); +} + +int main(int argc, char** argv) { + Require(argc == 3, "expected pipe API and scenario"); + const std::string scenario = argv[2]; + State state; + state.vector = std::string(argv[1]) == "writev"; + state.disabled = scenario == "disabled"; + const auto name = L"\\\\.\\pipe\\anyps5-minimal-writer-" + std::to_wstring(GetCurrentProcessId()); + const auto server = CreateNamedPipeW(name.c_str(), PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, nullptr); + Require(server != INVALID_HANDLE_VALUE, "create inbound pipe server"); + HANDLE client = CreateFileW(name.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + Require(client != INVALID_HANDLE_VALUE, "open write-only client"); + Require(ConnectNamedPipe(server, nullptr) || GetLastError() == ERROR_PIPE_CONNECTED, "connect inbound pipe"); + if (scenario == "nonblocking") { + DWORD mode = PIPE_NOWAIT; + Require(SetNamedPipeHandleState(client, &mode, nullptr, nullptr), "set nonblocking client"); + } + const int queryDescriptor = ::_open_osfhandle(reinterpret_cast(client), _O_WRONLY | _O_BINARY); + Require(queryDescriptor >= 0, "adopt pipe for quota setup"); + std::int64_t available = 0; + bool eof = false, writable = false; + Require(GuestSockets::PipeWriteState(queryDescriptor, &available, &eof, &writable) && available > 0 && available < 1024 * 1024, "query finite pipe quota"); + HANDLE limited = INVALID_HANDLE_VALUE; + Require(DuplicateHandle(GetCurrentProcess(), client, GetCurrentProcess(), &limited, FILE_WRITE_DATA | SYNCHRONIZE, FALSE, 0), "retain minimal-access pipe client"); + Require(::_close(queryDescriptor) == 0, "release pipe setup descriptor"); + client = limited; + using Query = LONG (NTAPI*)(HANDLE, void*, void*, ULONG, int); + const auto query = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationFile")); + std::uintptr_t status[2]{}; + ACCESS_MASK access = 0; + Require(query && query(client, status, &access, sizeof(access), 8) >= 0 && (access & FILE_READ_ATTRIBUTES) == 0, "client unexpectedly has read-attribute access"); + state.file = ::_open_osfhandle(reinterpret_cast(client), _O_WRONLY | _O_BINARY); + Require(state.file >= 0, "adopt minimal pipe client"); + std::vector fill(static_cast(available), 'f'); + if (scenario != "complete") { + DWORD count = 0; + Require(WriteFile(client, fill.data(), static_cast(fill.size()), &count, nullptr) && count == fill.size(), "fill minimal pipe quota"); + } + Pthread writer{}; + Require(scePthreadCreate(&writer, nullptr, Writer, &state, "minimal-pipe") == 0, "start minimal pipe writer"); + Require(Observe([&] { return state.ready.load(); }), "minimal writer did not start"); + const bool blocked = scenario == "cancel" || state.disabled || scenario == "close"; + if (blocked && !state.disabled) Require(Observe([&] { std::lock_guard lock(writer->cancelLock); return writer->cancelIo != nullptr || state.done.load(); }), "minimal writer did not register"); + std::atomic closed{false}; + std::thread closer; + if (scenario == "close") closer = std::thread([&] { Require(close_nid_postfix(state.file) == 0, "close minimal pipe"); closed.store(true); }); + else if (scenario == "cancel" || state.disabled) Require(scePthreadCancel(writer) == 0, "cancel minimal writer"); + bool woke = true; + if (state.disabled) { + DWORD count = 0; + Require(ReadFile(server, fill.data(), static_cast(fill.size()), &count, nullptr) && count == fill.size(), "drain disabled writer quota"); + } else if (blocked) { + woke = Observe([&] { return state.done.load(); }); + if (!woke) { + DWORD count = 0; + Require(ReadFile(server, fill.data(), static_cast(fill.size()), &count, nullptr), "release failed minimal writer"); + } + } + Require(Observe([&] { return state.done.load(); }), "minimal writer did not complete"); + void* result = nullptr; + Require(scePthreadJoin(writer, &result) == 0, "join minimal writer"); + if (closer.joinable()) { Require(Observe([&] { return closed.load(); }), "minimal close did not drain"); closer.join(); } + Require(woke && state.cleanups.load() == 1, "minimal pipe did not wake safely"); + std::printf("minimal pipe writer result %lld, error %d\n", static_cast(state.result), state.error); + const bool canceled = scenario == "cancel" || state.disabled; + Require(result == (canceled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "minimal pipe join result"); + if (scenario == "cancel") Require(!state.returned.load(), "blocked minimal writer returned before cancellation"); + else if (scenario == "close") Require(state.result == -1 && state.error == 4, "minimal pipe close result"); + else if (scenario == "nonblocking") Require(state.result == -1 && state.error == 35, "minimal pipe nonblocking result"); + else { + Require(state.result == 1, "minimal pipe write lost count"); + char byte = 0; DWORD count = 0; + Require(ReadFile(server, &byte, 1, &count, nullptr) && count == 1 && byte == 'w', "minimal pipe write lost payload"); + } + if (scenario != "close") Require(close_nid_postfix(state.file) == 0, "close minimal client"); + CloseHandle(server); + return 0; +} diff --git a/core/libs/tests/GuestIoWriteSyncCancel.cpp b/core/libs/tests/GuestIoWriteSyncCancel.cpp index a0058e1d7d1..258afd858af 100644 --- a/core/libs/tests/GuestIoWriteSyncCancel.cpp +++ b/core/libs/tests/GuestIoWriteSyncCancel.cpp @@ -15,6 +15,8 @@ #include #include #else +#include +#include #include #endif @@ -54,7 +56,7 @@ static State* state = nullptr; static int Flush(int file) { Require(file == state->file, "flush used a different descriptor"); ++state->calls; - if (state->scenario == "zero" || state->scenario == "error") return 0; + if (state->scenario == "zero" || state->scenario == "error" || state->scenario == "legacy") return 0; state->entered.store(true); char token = 0; #ifdef _WIN32 @@ -82,17 +84,20 @@ static void Release() { #endif } -static void APS5_VABI Cleanup(void*) { +static void APS5_VABI Cleanup(void* opaque) { + Require(opaque == state, "cleanup argument ABI"); Require(state->cleanups.fetch_add(1) == 0 && state->destructors.load() == 0, "cleanup order"); if (state->scenario != "close") Require(fcntl_nid_postfix(state->file, 4, 0) == 0, "cleanup did not release descriptor admission"); } -static void APS5_VABI Destroy(void*) { +static void APS5_VABI Destroy(void* opaque) { + Require(opaque == state, "TSD argument ABI"); Require(state->cleanups.load() == 1 && state->destructors.fetch_add(1) == 0, "TSD order"); state->finished.store(true); } static void APS5_VABI Handler(int) { ++state->signals; } +static void APS5_VABI ExceptionHandler(int signal, void*) { Handler(signal); } static std::int64_t Write(std::size_t size) { const char bytes[] = {'a', 'b'}; @@ -104,7 +109,8 @@ static std::int64_t Write(std::size_t size) { return sceKernelWrite(state->file, bytes, size); } -static void* APS5_VABI Writer(void*) { +static void* APS5_VABI Writer(void* opaque) { + Require(opaque == state, "writer argument ABI"); if (state->scenario == "disabled") Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable cancellation"); Require(scePthreadSetspecific(state->key, state) == 0, "set flush TSD"); CleanupHandler cleanup{}; @@ -135,10 +141,10 @@ int main(int argc, char** argv) { Require(state->file >= 0, "reopen read-only synchronous file"); } File::FileSetSynchronousWriteHookForTest_nid_no_patch(Flush); - if (state->scenario == "zero" || state->scenario == "error") { - const auto result = Write(state->scenario == "zero" ? 0 : 2); - Require(result == (state->scenario == "zero" ? 0 : -1), "zero or failed write result"); - Require(state->calls.load() == 0, "zero-progress write entered a flush"); + if (state->scenario == "zero" || state->scenario == "error" || state->scenario == "legacy") { + const auto result = Write(state->scenario == "error" ? 2 : 0); + Require(result == (state->scenario == "error" ? -1 : 0), "zero or failed write result"); + Require(state->calls.load() == (state->scenario == "legacy" ? 1u : 0u), "zero-progress flush contract changed"); } else { #ifdef _WIN32 Require(CreatePipe(&state->input, &state->output, nullptr, 4096), "create native flush wait pipe"); @@ -148,10 +154,14 @@ int main(int argc, char** argv) { state->input = pipe[0]; state->output = pipe[1]; #endif if (state->scenario == "signal") { +#ifdef _WIN32 + Require(sceKernelInstallExceptionHandler(30, reinterpret_cast(ExceptionHandler)) == 0, "install flush exception handler"); +#else GuestSigaction action{}; action.handler = reinterpret_cast(&Handler); action.flags = 2; - Require(sigaction_nid_postfix(30, &action, nullptr) == 0, "install restartable handler"); + Require(sigaction_nid_postfix(2, &action, nullptr) == 0, "install restartable handler"); +#endif } Require(scePthreadKeyCreate(&state->key, reinterpret_cast(Destroy)) == 0, "create flush TSD key"); Pthread writer{}; @@ -162,7 +172,11 @@ int main(int argc, char** argv) { if (state->scenario == "close") { closer = std::thread([&] { Require(close_nid_postfix(state->file) == 0, "close flushing descriptor"); closed.store(true); }); } else if (state->scenario == "signal") { - Require(pthread_kill_nid_postfix(writer, 30) == 0, "interrupt flushing writer"); +#ifdef _WIN32 + Require(sceKernelRaiseException(writer, 30) == 0, "raise flush exception"); +#else + Require(::pthread_kill(writer->hostThread, SIGINT) == 0, "interrupt flushing writer"); +#endif } else if (state->scenario != "complete") { Require(scePthreadCancel(writer) == 0, "cancel flushing writer"); } @@ -181,6 +195,7 @@ int main(int argc, char** argv) { const bool canceled = state->scenario == "cancel" || state->scenario == "disabled" || state->api == "sce"; Require(joined == (canceled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "unexpected flush join result"); Require(state->cleanups.load() == 1 && state->destructors.load() == 1 && state->calls.load() == 1, "flush repeated or cleanup lost"); + Require(std::filesystem::file_size(path) == 2, "flush interruption changed write progress"); if (canceled) Require(state->returned.load() == waiting, "canceled flush completion state"); else if (state->scenario == "complete") Require(state->result == 2, "completed flush lost positive result"); else Require(state->result == -1 && state->error == 4, "interrupted flush did not return EINTR"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b8dc4c8603a..2ce4e3b97ef 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -207,7 +207,8 @@ - [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) follows FreeBSD libthr's states, types, `PTHREAD_CANCELED` value and cleanup-node layout, not console measurements. Connection operations and multiplexed I/O waits remain non-cancellable. POSIX `sleep` wakes for an enabled request; `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check only before and after the host sleep. The asynchronous type acts like deferred cancellation 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 write cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) is not console-verified. It covers `write`, `_write`, `writev`, `_writev`, `pwrite`, `pwritev`, `send`, `sendto`, `sendmsg`, `_sendto` and `_sendmsg` on accepted file, shared-memory, pipe and socket descriptors; positioned socket and pipe writes retain their existing errors. Positive partial counts win over pending cancellation; zero and failed writes check cancellation after guards release, following FreeBSD libthr. SCE writes and libSceNet gain no cancellation points. Non-interruptible drivers can delay cancellation and close indefinitely. Windows pipe quota waits retry readiness at 1 ms intervals. Descriptor replacement and stdio closure can still wait indefinitely; stream-socket writes share the read admission and close/drain machinery. +- [POSIX write cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) is not console-verified. It covers `write`, `_write`, `writev`, `_writev`, `pwrite`, `pwritev`, `send`, `sendto`, `sendmsg`, `_sendto` and `_sendmsg` on accepted file, shared-memory, pipe and socket descriptors; positioned socket and pipe writes retain their existing errors. Completed positive results, including partial counts, win over pending cancellation; zero and failed writes check cancellation after guards release, following FreeBSD libthr. SCE writes and libSceNet gain no cancellation points. Non-interruptible drivers can delay cancellation and close indefinitely. Windows pipe quota waits retry readiness at 1 ms intervals. Descriptor replacement and stdio closure can still wait indefinitely; stream-socket writes share the read admission and close/drain machinery. +- [Synchronous POSIX write cancellation](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) has no console measurement of interrupted `O_FSYNC` writes. Synchronization is a separate native flush after buffered progress; interruption can leave buffered bytes visible without durable completion. The operation reports success only after that flush succeeds and does not replay buffered writes after an interrupted flush. Direct SCE writes retain their existing flush and cancellation behavior. - [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 and write waits and run after operation guards are released, including disabled cancellation and unregistered callers. Restartable operations retry only if descriptor generations remain unchanged; socket transfer 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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From e87a337fee86c770faed40ee9c8c2269db913f07 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 16:27:48 -0300 Subject: [PATCH 29/61] fix(libkernel): guard synchronous-write metadata waits --- core/libs/prx/libkernel/CMakeLists.txt | 4 ++-- .../File/include/DescriptorControl.hpp | 1 + .../libkernel/File/src/DescriptorControl.cpp | 23 +++++++++++++++++-- core/libs/tests/GuestIoWriteOnlyPipe.cpp | 18 ++++++++++----- core/libs/tests/GuestIoWriteSyncCancel.cpp | 10 +++++++- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a1dd13300a8..78504150586 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -268,7 +268,7 @@ if(BUILD_TESTING) target_link_libraries(guest_io_write_only_pipe_tests PRIVATE libkernel libc) configure_windows_unwind(guest_io_write_only_pipe_tests) foreach(minimalPipeApi write writev) - foreach(minimalPipeCase complete cancel disabled close nonblocking) + foreach(minimalPipeCase complete cancel disabled close nonblocking nonblocking_partial) set(minimalPipeTest guest_io_write_minimal_pipe_${minimalPipeApi}__${minimalPipeCase}) add_test(NAME ${minimalPipeTest} COMMAND guest_io_write_only_pipe_tests ${minimalPipeApi} ${minimalPipeCase}) set_tests_properties(${minimalPipeTest} PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -279,7 +279,7 @@ if(BUILD_TESTING) target_link_libraries(guest_io_write_sync_cancel_tests PRIVATE libkernel libc) configure_windows_unwind(guest_io_write_sync_cancel_tests) foreach(syncWriteApi write writev pwrite pwritev) - foreach(syncWriteCase cancel disabled complete zero error close signal) + foreach(syncWriteCase cancel disabled complete zero error close signal metadata_cancel metadata_disabled metadata_complete metadata_close metadata_signal) set(syncWriteTest guest_io_write_sync_${syncWriteApi}__${syncWriteCase}) add_test(NAME ${syncWriteTest} COMMAND guest_io_write_sync_cancel_tests ${syncWriteApi} ${syncWriteCase}) set_tests_properties(${syncWriteTest} PROPERTIES TIMEOUT 15) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 2f7133c4cb4..727dcd0df54 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -22,6 +22,7 @@ bool Appends(int fd); bool SyncWrites(int fd); using SynchronousWriteHook = int (*)(int); extern "C" void FileSetSynchronousWriteHookForTest_nid_no_patch(SynchronousWriteHook hook); +extern "C" void FileSetSynchronousWriteMetadataHookForTest_nid_no_patch(SynchronousWriteHook hook); class WriteSync { public: diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 6c49c307333..5980c2a783e 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -643,20 +643,38 @@ void ForgetDescriptor(int fd) { std::uint64_t g_synchronousWrites = 0; std::atomic g_synchronousWriteHook{nullptr}; +std::atomic g_synchronousWriteMetadataHook{nullptr}; extern "C" void FileSetSynchronousWriteHookForTest_nid_no_patch(SynchronousWriteHook hook) { g_synchronousWriteHook.store(hook); } +extern "C" void FileSetSynchronousWriteMetadataHookForTest_nid_no_patch(SynchronousWriteHook hook) { + g_synchronousWriteMetadataHook.store(hook); +} + bool SyncWrites(int fd) { { std::lock_guard lock(g_mutex); const auto found = g_entries.find(fd); if (found == g_entries.end() || !found->second.status || (*found->second.status & Synchronous) == 0) return true; } - if (Classify(fd, "write") != Kind::File) return true; const auto hook = g_synchronousWriteHook.load(); - if (const int result = ThreadCancel::Native([&] { return hook ? hook(fd) : sceKernelFsync(fd); }); result != 0) { + const auto metadataHook = g_synchronousWriteMetadataHook.load(); + bool synchronized = false; + int result; + try { + result = ThreadCancel::Native([&] { + if (metadataHook && metadataHook(fd) != 0) throw std::runtime_error("write: synchronous metadata query failed"); + if (Classify(fd, "write") != Kind::File) return 0; + synchronized = true; + return hook ? hook(fd) : sceKernelFsync(fd); + }); + } catch (const std::runtime_error&) { + if (!ThreadCancel::IoActive() || !ThreadCancel::IoInterrupted()) throw; + result = -1; + } + if (result != 0) { if (ThreadCancel::IoActive() && (ThreadCancel::IoInterrupted() || (static_cast(result) & 0xffff) == EINTR)) { ThreadCancel::IoNoRestart(); errno = EINTR; @@ -664,6 +682,7 @@ bool SyncWrites(int fd) { } throw std::runtime_error("write: syncing an O_FSYNC file failed, fd=" + std::to_string(fd) + ", error=" + std::to_string(result & 0xffff)); } + if (!synchronized) return true; std::lock_guard lock(g_mutex); ++g_synchronousWrites; return true; diff --git a/core/libs/tests/GuestIoWriteOnlyPipe.cpp b/core/libs/tests/GuestIoWriteOnlyPipe.cpp index deae8be827c..c5d910d20ec 100644 --- a/core/libs/tests/GuestIoWriteOnlyPipe.cpp +++ b/core/libs/tests/GuestIoWriteOnlyPipe.cpp @@ -34,6 +34,7 @@ static bool Observe(TPredicate predicate) { struct State { int file = -1; bool vector = false, disabled = false; + std::size_t bytes = 1; std::atomic ready{false}, returned{false}, done{false}; std::atomic cleanups{0}; std::int64_t result = -2; @@ -52,9 +53,9 @@ static void* APS5_VABI Writer(void* opaque) { CleanupHandler cleanup{}; __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); state.ready.store(true); - char byte = 'w'; - const KernelIovec vectors[]{{nullptr, 0}, {&byte, 1}}; - state.result = state.vector ? writev_nid_postfix(state.file, vectors, 2) : write_nid_postfix(state.file, &byte, 1); + char bytes[] = {'w', 'w'}; + const KernelIovec vectors[]{{bytes, state.bytes / 2}, {bytes + state.bytes / 2, state.bytes - state.bytes / 2}}; + state.result = state.vector ? writev_nid_postfix(state.file, vectors, 2) : write_nid_postfix(state.file, bytes, state.bytes); state.error = *__error_nid_postfix(); state.returned.store(true); if (state.disabled) { @@ -72,13 +73,14 @@ int main(int argc, char** argv) { State state; state.vector = std::string(argv[1]) == "writev"; state.disabled = scenario == "disabled"; + state.bytes = scenario == "nonblocking_partial" ? 2 : 1; const auto name = L"\\\\.\\pipe\\anyps5-minimal-writer-" + std::to_wstring(GetCurrentProcessId()); const auto server = CreateNamedPipeW(name.c_str(), PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, nullptr); Require(server != INVALID_HANDLE_VALUE, "create inbound pipe server"); HANDLE client = CreateFileW(name.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); Require(client != INVALID_HANDLE_VALUE, "open write-only client"); Require(ConnectNamedPipe(server, nullptr) || GetLastError() == ERROR_PIPE_CONNECTED, "connect inbound pipe"); - if (scenario == "nonblocking") { + if (scenario.starts_with("nonblocking")) { DWORD mode = PIPE_NOWAIT; Require(SetNamedPipeHandleState(client, &mode, nullptr, nullptr), "set nonblocking client"); } @@ -98,7 +100,7 @@ int main(int argc, char** argv) { Require(query && query(client, status, &access, sizeof(access), 8) >= 0 && (access & FILE_READ_ATTRIBUTES) == 0, "client unexpectedly has read-attribute access"); state.file = ::_open_osfhandle(reinterpret_cast(client), _O_WRONLY | _O_BINARY); Require(state.file >= 0, "adopt minimal pipe client"); - std::vector fill(static_cast(available), 'f'); + std::vector fill(static_cast(available) - (scenario == "nonblocking_partial" ? 1 : 0), 'f'); if (scenario != "complete") { DWORD count = 0; Require(WriteFile(client, fill.data(), static_cast(fill.size()), &count, nullptr) && count == fill.size(), "fill minimal pipe quota"); @@ -133,7 +135,11 @@ int main(int argc, char** argv) { Require(result == (canceled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "minimal pipe join result"); if (scenario == "cancel") Require(!state.returned.load(), "blocked minimal writer returned before cancellation"); else if (scenario == "close") Require(state.result == -1 && state.error == 4, "minimal pipe close result"); - else if (scenario == "nonblocking") Require(state.result == -1 && state.error == 35, "minimal pipe nonblocking result"); + else if (scenario.starts_with("nonblocking")) { + Require(state.result == -1 && state.error == 35, "minimal pipe nonblocking result"); + DWORD queued = 0; + Require(PeekNamedPipe(server, nullptr, 0, nullptr, &queued, nullptr) && queued == fill.size(), "small nonblocking write changed the pipe"); + } else { Require(state.result == 1, "minimal pipe write lost count"); char byte = 0; DWORD count = 0; diff --git a/core/libs/tests/GuestIoWriteSyncCancel.cpp b/core/libs/tests/GuestIoWriteSyncCancel.cpp index 258afd858af..d3df9667edc 100644 --- a/core/libs/tests/GuestIoWriteSyncCancel.cpp +++ b/core/libs/tests/GuestIoWriteSyncCancel.cpp @@ -40,6 +40,7 @@ struct State { int file = -1; PthreadKey key = -1; std::string api, scenario; + bool metadata = false; std::atomic calls{0}, cleanups{0}, destructors{0}, signals{0}; std::atomic entered{false}, finished{false}, returned{false}; std::int64_t result = -2; @@ -66,6 +67,7 @@ static int Flush(int file) { const bool completed = ::read(state->input, &token, 1) == 1; #endif if (!completed) return static_cast(0x80020004u); + if (state->metadata) return 0; #ifdef _WIN32 Require(::_commit(file) == 0, "native file flush failed"); #else @@ -132,6 +134,10 @@ int main(int argc, char** argv) { State storage; state = &storage; state->api = argv[1]; state->scenario = argv[2]; + if (state->scenario.starts_with("metadata_")) { + state->metadata = true; + state->scenario.erase(0, 9); + } const auto path = "anyps5-sync-cancel-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); state->file = open_nid_postfix(path.c_str(), 2 | 0x200, 0600); Require(state->file >= 0 && fcntl_nid_postfix(state->file, 4, 0x80) == 0, "create synchronous file"); @@ -140,7 +146,8 @@ int main(int argc, char** argv) { state->file = open_nid_postfix(path.c_str(), 0x80, 0600); Require(state->file >= 0, "reopen read-only synchronous file"); } - File::FileSetSynchronousWriteHookForTest_nid_no_patch(Flush); + if (state->metadata) File::FileSetSynchronousWriteMetadataHookForTest_nid_no_patch(Flush); + else File::FileSetSynchronousWriteHookForTest_nid_no_patch(Flush); if (state->scenario == "zero" || state->scenario == "error" || state->scenario == "legacy") { const auto result = Write(state->scenario == "error" ? 2 : 0); Require(result == (state->scenario == "error" ? -1 : 0), "zero or failed write result"); @@ -208,6 +215,7 @@ int main(int argc, char** argv) { #endif } File::FileSetSynchronousWriteHookForTest_nid_no_patch(nullptr); + File::FileSetSynchronousWriteMetadataHookForTest_nid_no_patch(nullptr); if (state->scenario != "close") Require(close_nid_postfix(state->file) == 0, "close synchronous file"); std::filesystem::remove(path); return 0; From dc7772e6e18b612ede5b926496cf955251bfca1f Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 16:34:32 -0300 Subject: [PATCH 30/61] fix(libkernel): cancel write preflight metadata queries --- core/libs/prx/libkernel/CMakeLists.txt | 11 ++++++++ .../libkernel/File/include/SharedMemory.hpp | 3 +++ .../libkernel/File/src/DescriptorControl.cpp | 4 +-- .../prx/libkernel/File/src/SharedMemory.cpp | 26 +++++++++++++++---- core/libs/prx/libkernel/File/src/Stdio.cpp | 6 +++-- core/libs/tests/GuestIoWriteSyncCancel.cpp | 23 +++++++++++----- 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 78504150586..30f73a08c4a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -288,6 +288,17 @@ if(BUILD_TESTING) endif() endforeach() endforeach() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + foreach(preflightApi write writev pwrite pwritev) + foreach(preflightPhase 1 2) + foreach(preflightCase cancel disabled complete close signal) + set(preflightTest guest_io_write_preflight_${preflightApi}__${preflightPhase}_${preflightCase}) + add_test(NAME ${preflightTest} COMMAND guest_io_write_sync_cancel_tests ${preflightApi} preflight${preflightPhase}_${preflightCase}) + set_tests_properties(${preflightTest} PROPERTIES TIMEOUT 15) + endforeach() + endforeach() + endforeach() + endif() add_test(NAME guest_io_write_sync_sce COMMAND guest_io_write_sync_cancel_tests sce cancel) add_test(NAME guest_io_write_sync_sce_legacy COMMAND guest_io_write_sync_cancel_tests sce legacy) set_tests_properties(guest_io_write_sync_sce PROPERTIES TIMEOUT 15) diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp index 7d9be97385a..0273df1d422 100644 --- a/core/libs/prx/libkernel/File/include/SharedMemory.hpp +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -9,6 +9,9 @@ namespace File { +using SharedMemoryMetadataHook = int (*)(int); +extern "C" void FileSetSharedMemoryMetadataHookForTest_nid_no_patch(SharedMemoryMetadataHook hook); + bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size); std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 5980c2a783e..a5c450a7712 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -700,7 +700,7 @@ bool IsCharacterDevice(int fd) { return DescriptorIsOpen_nid_no_patch(fd) && !GuestSockets::IsOpen(fd) && !File::DirectoryDescriptorPath(fd) && ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_CHAR; #else struct stat status{}; - return ::fstat(fd, &status) == 0 && S_ISCHR(status.st_mode); + return ThreadCancel::Native([&] { return ::fstat(fd, &status); }) == 0 && S_ISCHR(status.st_mode); #endif } @@ -717,7 +717,7 @@ bool IsVnode(int fd) { return File::DirectoryDescriptorPath(fd) || ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_DISK; #else struct stat status{}; - return ::fstat(fd, &status) == 0 && (S_ISREG(status.st_mode) || S_ISDIR(status.st_mode)); + return ThreadCancel::Native([&] { return ::fstat(fd, &status); }) == 0 && (S_ISREG(status.st_mode) || S_ISDIR(status.st_mode)); #endif } diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index ac9c044a18a..5c06769a584 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -45,6 +45,17 @@ extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handle namespace { +std::atomic sharedMemoryMetadataHook{nullptr}; + +template +int SharedMemoryMetadata(int descriptor, TCall&& call) { + return ThreadCancel::Native([&] { + const auto hook = sharedMemoryMetadataHook.load(); + if (hook && hook(descriptor) != 0) { errno = EINTR; return -1; } + return call(); + }); +} + #ifdef _WIN32 std::mutex sharedDescriptorAccessMutex; std::unordered_map sharedDescriptorAccess; @@ -692,6 +703,11 @@ int CreateAnonymousFile() { namespace File { +extern "C" void FileSetSharedMemoryMetadataHookForTest_nid_no_patch(SharedMemoryMetadataHook hook) { + sharedMemoryMetadataHook.store(hook); +} + + bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) { return IsHostSharedMemoryDescriptor(descriptor); } @@ -741,7 +757,7 @@ class SharedMemoryLock { explicit SharedMemoryLock(int value) { #ifdef __linux__ struct stat originalStatus{}; - if (::fstat(value, &originalStatus) != 0) { shared = true; error = errno; return; } + if (SharedMemoryMetadata(value, [&] { return ::fstat(value, &originalStatus); }) != 0) { shared = true; error = errno; return; } #elif defined(_WIN32) if (!IsHostSharedMemoryDescriptor(value)) return; const auto originalValue = ::_get_osfhandle(value); @@ -758,7 +774,7 @@ class SharedMemoryLock { if (!IsHostSharedMemoryDescriptor(value)) { #ifdef __linux__ struct stat currentStatus{}; - if (::fstat(value, ¤tStatus) != 0) { shared = true; error = errno; return; } + if (SharedMemoryMetadata(value, [&] { return ::fstat(value, ¤tStatus); }) != 0) { shared = true; error = errno; return; } if (currentStatus.st_dev != originalStatus.st_dev || currentStatus.st_ino != originalStatus.st_ino) { shared = true; error = EBADF; @@ -806,11 +822,11 @@ class SharedMemoryLock { bool Refresh() { #ifdef _WIN32 struct _stat64 nativeStatus{}; - if (::_fstat64(descriptor, &nativeStatus) != 0) return false; + if (SharedMemoryMetadata(descriptor, [&] { return ::_fstat64(descriptor, &nativeStatus); }) != 0) return false; status.st_size = nativeStatus.st_size; return true; #else - return ::fstat(descriptor, &status) == 0; + return SharedMemoryMetadata(descriptor, [&] { return ::fstat(descriptor, &status); }) == 0; #endif } struct stat status{}; @@ -951,7 +967,7 @@ static std::int64_t TransferSharedMemoryVectors(int descriptor, const struct iov if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } if (positioned && offset < 0) { errno = EINVAL; return -1; } struct stat status{}; - if (::fstat(fd, &status) != 0) return -1; + if (SharedMemoryMetadata(fd, [&] { return ::fstat(fd, &status); }) != 0) return -1; std::uint64_t total = 0; constexpr auto maxTransfer = static_cast(std::numeric_limits::max()); for (int i = 0; i < count; ++i) { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index f482a8f125a..296a64f4f06 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -518,7 +518,9 @@ static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, s #ifdef __APPLE__ std::int64_t File::WritevAtEnd(int descriptor, const iovec* iov, int iovcnt) { struct stat status{}; - if (!File::Appends(descriptor) || ::fstat(descriptor, &status) != 0 || !S_ISREG(status.st_mode)) return -2; + if (!File::Appends(descriptor)) return -2; + if (ThreadCancel::Native([&] { return ::fstat(descriptor, &status); }) != 0) return ThreadCancel::IoActive() && ThreadCancel::IoInterrupted() ? -1 : -2; + if (!S_ISREG(status.st_mode)) return -2; const off_t position = ::lseek(descriptor, 0, SEEK_CUR); if (position < 0) return -1; const auto result = ThreadCancel::Native([&] { return static_cast(::writev(descriptor, iov, iovcnt)); }); @@ -596,7 +598,7 @@ static int PositionedArgumentError(int descriptor, bool write, bool nullBuffer, if (IsKqueueDescriptor(descriptor)) return GUEST_ESPIPE; if (!nullBuffer && offset >= 0) return 0; if (!GuestSockets::IsOpen(descriptor) && !DescriptorIsOpen_nid_no_patch(descriptor)) return GUEST_EBADF; - if (!IsSeekableDescriptor(descriptor)) return GUEST_ESPIPE; + if (!IsSeekableDescriptor(descriptor)) return ThreadCancel::IoActive() && ThreadCancel::IoInterrupted() ? EINTR : GUEST_ESPIPE; if (!File::GrantsAccess(descriptor, write)) return GUEST_EBADF; return offset < 0 ? GUEST_EINVAL : GUEST_EFAULT; } diff --git a/core/libs/tests/GuestIoWriteSyncCancel.cpp b/core/libs/tests/GuestIoWriteSyncCancel.cpp index d3df9667edc..e2830b87472 100644 --- a/core/libs/tests/GuestIoWriteSyncCancel.cpp +++ b/core/libs/tests/GuestIoWriteSyncCancel.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/Api.hpp" @@ -41,6 +42,7 @@ struct State { PthreadKey key = -1; std::string api, scenario; bool metadata = false; + unsigned preflight = 0; std::atomic calls{0}, cleanups{0}, destructors{0}, signals{0}; std::atomic entered{false}, finished{false}, returned{false}; std::int64_t result = -2; @@ -56,7 +58,8 @@ static State* state = nullptr; static int Flush(int file) { Require(file == state->file, "flush used a different descriptor"); - ++state->calls; + const auto call = state->calls.fetch_add(1) + 1; + if (state->preflight != 0 && call != state->preflight) return 0; if (state->scenario == "zero" || state->scenario == "error" || state->scenario == "legacy") return 0; state->entered.store(true); char token = 0; @@ -67,7 +70,7 @@ static int Flush(int file) { const bool completed = ::read(state->input, &token, 1) == 1; #endif if (!completed) return static_cast(0x80020004u); - if (state->metadata) return 0; + if (state->metadata || state->preflight != 0) return 0; #ifdef _WIN32 Require(::_commit(file) == 0, "native file flush failed"); #else @@ -134,19 +137,24 @@ int main(int argc, char** argv) { State storage; state = &storage; state->api = argv[1]; state->scenario = argv[2]; + if (state->scenario.starts_with("preflight")) { + state->preflight = state->scenario[9] == '1' ? 1 : 2; + state->scenario.erase(0, 11); + } if (state->scenario.starts_with("metadata_")) { state->metadata = true; state->scenario.erase(0, 9); } const auto path = "anyps5-sync-cancel-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); state->file = open_nid_postfix(path.c_str(), 2 | 0x200, 0600); - Require(state->file >= 0 && fcntl_nid_postfix(state->file, 4, 0x80) == 0, "create synchronous file"); + Require(state->file >= 0 && (state->preflight != 0 || fcntl_nid_postfix(state->file, 4, 0x80) == 0), "create synchronous file"); if (state->scenario == "error") { Require(close_nid_postfix(state->file) == 0, "close writable file"); state->file = open_nid_postfix(path.c_str(), 0x80, 0600); Require(state->file >= 0, "reopen read-only synchronous file"); } - if (state->metadata) File::FileSetSynchronousWriteMetadataHookForTest_nid_no_patch(Flush); + if (state->preflight != 0) File::FileSetSharedMemoryMetadataHookForTest_nid_no_patch(Flush); + else if (state->metadata) File::FileSetSynchronousWriteMetadataHookForTest_nid_no_patch(Flush); else File::FileSetSynchronousWriteHookForTest_nid_no_patch(Flush); if (state->scenario == "zero" || state->scenario == "error" || state->scenario == "legacy") { const auto result = Write(state->scenario == "error" ? 2 : 0); @@ -166,7 +174,7 @@ int main(int argc, char** argv) { #else GuestSigaction action{}; action.handler = reinterpret_cast(&Handler); - action.flags = 2; + action.flags = state->preflight != 0 ? 0 : 2; Require(sigaction_nid_postfix(2, &action, nullptr) == 0, "install restartable handler"); #endif } @@ -201,8 +209,8 @@ int main(int argc, char** argv) { Require(waiting || woke, "cancellation or close did not wake native flush"); const bool canceled = state->scenario == "cancel" || state->scenario == "disabled" || state->api == "sce"; Require(joined == (canceled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "unexpected flush join result"); - Require(state->cleanups.load() == 1 && state->destructors.load() == 1 && state->calls.load() == 1, "flush repeated or cleanup lost"); - Require(std::filesystem::file_size(path) == 2, "flush interruption changed write progress"); + Require(state->cleanups.load() == 1 && state->destructors.load() == 1 && state->calls.load() == (state->preflight != 0 ? (waiting ? 2u : state->preflight) : 1u), "flush repeated or cleanup lost"); + Require(std::filesystem::file_size(path) == (state->preflight != 0 && !waiting ? 0u : 2u), "flush interruption changed write progress"); if (canceled) Require(state->returned.load() == waiting, "canceled flush completion state"); else if (state->scenario == "complete") Require(state->result == 2, "completed flush lost positive result"); else Require(state->result == -1 && state->error == 4, "interrupted flush did not return EINTR"); @@ -214,6 +222,7 @@ int main(int argc, char** argv) { ::close(state->input); ::close(state->output); #endif } + File::FileSetSharedMemoryMetadataHookForTest_nid_no_patch(nullptr); File::FileSetSynchronousWriteHookForTest_nid_no_patch(nullptr); File::FileSetSynchronousWriteMetadataHookForTest_nid_no_patch(nullptr); if (state->scenario != "close") Require(close_nid_postfix(state->file) == 0, "close synchronous file"); From 3b0d2c6feb8bc25ff5b5ebec8784e03fad507a69 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 22:40:23 -0300 Subject: [PATCH 31/61] fix(libkernel): retain renamed kqueue checks in read wrappers --- core/libs/prx/libkernel/File/src/Stdio.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 838fcde5240..58e6cac356e 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -953,7 +953,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { 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(from)) + 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); } @@ -1167,7 +1167,7 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off return ThreadCancel::Io([&]() -> std::int64_t { if (File::IsDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (IsKqueueDescriptor(d)) return PosixFailure(GUEST_ESPIPE); + 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; @@ -1736,7 +1736,7 @@ int64_t APS5_VABI preadv_nid_postfix(int d, const KernelIovec* iov, int iovcnt, 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(d)) return PosixFailure(GUEST_ESPIPE); + 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; From 5764c776cb5d574c64292f6a31c13eba27ad9020 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 22:57:07 -0300 Subject: [PATCH 32/61] fix(libkernel): reuse the thread socket transfer waker --- .../libkernel/Socket/include/SocketRuntime.hpp | 2 ++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 12 +++++++++++- core/libs/tests/GuestSocketTimeouts.cpp | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index c5343daede2..9194a5a8514 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -42,3 +42,5 @@ std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor); extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* source, std::size_t bytes); + +extern "C" std::uint64_t GuestSocketWakerCreationsForTest_nid_no_patch(); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 559f1f8c912..ae22e76259e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -249,6 +249,8 @@ int TransferForNativeOwner(KernelSocketPoll::NativeOwner* owner, bool send, void #endif } +std::atomic wakerCreations{0}; + struct Waker { NativeSocket value = Invalid; Waker() { @@ -274,6 +276,7 @@ struct Waker { Close(); throw std::runtime_error("equeue waker: loopback socket setup failed, guest errno " + std::to_string(error)); } + ++wakerCreations; } ~Waker() { Close(); } void Close() { @@ -311,7 +314,10 @@ int WaitTransferReady(Socket& socket, bool send, int milliseconds) { error = result < 0 ? WSAGetLastError() : 0; return result; } - Waker waker; + struct TransferWakerTag {}; + auto& waker = HostThreadLocal(); + char discarded[64]; + while (::recv(waker.value, discarded, sizeof(discarded), 0) > 0) {} ThreadCancel::IoWait operation([](void* opaque) noexcept { auto& waker = *static_cast(opaque); const char byte = 0; @@ -2354,3 +2360,7 @@ std::int64_t APS5_VABI _recvmsg_nid_postfix(int descriptor, GuestMsghdr* message return recvmsg_nid_postfix(descriptor, message, flags); } } + +extern "C" std::uint64_t GuestSocketWakerCreationsForTest_nid_no_patch() { + return wakerCreations.load(); +} diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp index 949f24cf4db..e988dfd0c76 100644 --- a/core/libs/tests/GuestSocketTimeouts.cpp +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -102,7 +103,24 @@ static void RetainedTimeoutWithInterrupts() { } #endif +#ifdef _WIN32 +static void TransferWakerReuse() { + const auto before = GuestSocketWakerCreationsForTest_nid_no_patch(); + const int descriptor = socket_nid_postfix(2, 2, 0); + Require(descriptor >= 0 && Set(descriptor, ReceiveTimeout, 0, 2000) == 0); + char byte = '?'; + for (int attempt = 0; attempt != 2; ++attempt) { + Require(Fails(recv_nid_postfix(descriptor, &byte, 1, 0), WouldBlock)); + Require(byte == '?' && GuestSocketWakerCreationsForTest_nid_no_patch() == before + 1); + } + Require(close_nid_postfix(descriptor) == 0); +} +#endif + int main() { +#ifdef _WIN32 + TransferWakerReuse(); +#endif const int datagram = socket_nid_postfix(2, 2, 0); std::array own{16, 2, 0, 0, 127, 0, 0, 1}; Require(datagram >= 0 && bind_nid_postfix(datagram, own.data(), own.size()) == 0); From 3c20c17d804958a405e2168bbe6bed3e80e3d319 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 23:02:31 -0300 Subject: [PATCH 33/61] test(libkernel): bind the transfer waker reuse endpoint --- core/libs/tests/GuestSocketTimeouts.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp index e988dfd0c76..3ddc5c2948a 100644 --- a/core/libs/tests/GuestSocketTimeouts.cpp +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -107,7 +107,9 @@ static void RetainedTimeoutWithInterrupts() { static void TransferWakerReuse() { const auto before = GuestSocketWakerCreationsForTest_nid_no_patch(); const int descriptor = socket_nid_postfix(2, 2, 0); - Require(descriptor >= 0 && Set(descriptor, ReceiveTimeout, 0, 2000) == 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(descriptor >= 0 && bind_nid_postfix(descriptor, address.data(), address.size()) == 0); + Require(Set(descriptor, ReceiveTimeout, 0, 2000) == 0); char byte = '?'; for (int attempt = 0; attempt != 2; ++attempt) { Require(Fails(recv_nid_postfix(descriptor, &byte, 1, 0), WouldBlock)); From 7b3caa041c14768910e5eba81baca4d2e0495b7b Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 23:05:57 -0300 Subject: [PATCH 34/61] test(libkernel): handshake socket transfer waker reuse --- core/libs/tests/GuestSocketTimeouts.cpp | 47 +++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp index 3ddc5c2948a..082e2a06c3b 100644 --- a/core/libs/tests/GuestSocketTimeouts.cpp +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -1,6 +1,10 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include +#include #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -104,18 +108,49 @@ static void RetainedTimeoutWithInterrupts() { #endif #ifdef _WIN32 +struct ReuseReader { + int descriptor; + std::atomic phase{-1}; +}; + +static void* APS5_VABI ReadTwice(void* opaque) { + auto& reader = *static_cast(opaque); + for (int attempt = 0; attempt != 2; ++attempt) { + reader.phase.store(attempt); + char byte = '?'; + Require(recv_nid_postfix(reader.descriptor, &byte, 1, 0) == 1 && byte == 'x'); + } + return nullptr; +} + static void TransferWakerReuse() { const auto before = GuestSocketWakerCreationsForTest_nid_no_patch(); const int descriptor = socket_nid_postfix(2, 2, 0); + const int peer = socket_nid_postfix(2, 2, 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; - Require(descriptor >= 0 && bind_nid_postfix(descriptor, address.data(), address.size()) == 0); - Require(Set(descriptor, ReceiveTimeout, 0, 2000) == 0); - char byte = '?'; + Require(descriptor >= 0 && peer >= 0 && bind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(descriptor, address.data(), &size) == 0); + ReuseReader reader{descriptor}; + Pthread thread{}; + Require(scePthreadCreate(&thread, nullptr, ReadTwice, &reader, "waker-reuse") == 0); for (int attempt = 0; attempt != 2; ++attempt) { - Require(Fails(recv_nid_postfix(descriptor, &byte, 1, 0), WouldBlock)); - Require(byte == '?' && GuestSocketWakerCreationsForTest_nid_no_patch() == before + 1); + const auto deadline = Clock::now() + std::chrono::seconds(2); + for (;;) { + bool waiting; + { + std::lock_guard lock(thread->cancelLock); + waiting = reader.phase.load() == attempt && thread->cancelIo != nullptr; + } + if (waiting) break; + Require(Clock::now() < deadline); + std::this_thread::yield(); + } + Require(GuestSocketWakerCreationsForTest_nid_no_patch() == before + 1); + Require(sendto_nid_postfix(peer, "x", 1, 0, address.data(), address.size()) == 1); } - Require(close_nid_postfix(descriptor) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(close_nid_postfix(peer) == 0 && close_nid_postfix(descriptor) == 0); } #endif From 7644baec1e1b1410512c58a0a2e8bbc20cf836c2 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 23:21:15 -0300 Subject: [PATCH 35/61] test(libkernel): preserve synchronous-write progress across interruption --- core/libs/tests/GuestIoWriteSyncCancel.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestIoWriteSyncCancel.cpp b/core/libs/tests/GuestIoWriteSyncCancel.cpp index e2830b87472..e997bdf3e92 100644 --- a/core/libs/tests/GuestIoWriteSyncCancel.cpp +++ b/core/libs/tests/GuestIoWriteSyncCancel.cpp @@ -60,6 +60,7 @@ static int Flush(int file) { Require(file == state->file, "flush used a different descriptor"); const auto call = state->calls.fetch_add(1) + 1; if (state->preflight != 0 && call != state->preflight) return 0; + if (state->scenario == "signal" && state->preflight == 0 && call > 1) return 0; if (state->scenario == "zero" || state->scenario == "error" || state->scenario == "legacy") return 0; state->entered.store(true); char token = 0; @@ -122,7 +123,12 @@ static void* APS5_VABI Writer(void* opaque) { __pthread_cleanup_push_imp_nid_postfix(Cleanup, state, &cleanup); state->result = Write(2); state->error = *__error_nid_postfix(); + if (state->scenario == "signal" && state->preflight == 0 && state->result == -1 && state->error == 4) { + state->result = Write(2); + state->error = *__error_nid_postfix(); + } state->returned.store(true); + if (state->scenario == "cancel") scePthreadTestcancel(); if (state->scenario == "disabled" || state->api == "sce") { Require(state->result == 2, "disabled or SCE flush lost completion"); Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable cancellation"); @@ -211,8 +217,8 @@ int main(int argc, char** argv) { Require(joined == (canceled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "unexpected flush join result"); Require(state->cleanups.load() == 1 && state->destructors.load() == 1 && state->calls.load() == (state->preflight != 0 ? (waiting ? 2u : state->preflight) : 1u), "flush repeated or cleanup lost"); Require(std::filesystem::file_size(path) == (state->preflight != 0 && !waiting ? 0u : 2u), "flush interruption changed write progress"); - if (canceled) Require(state->returned.load() == waiting, "canceled flush completion state"); - else if (state->scenario == "complete") Require(state->result == 2, "completed flush lost positive result"); + if (canceled) Require(state->returned.load() == (waiting || state->preflight == 0), "canceled flush completion state"); + else if (state->scenario == "complete" || state->preflight == 0) Require(state->result == 2, "completed flush lost positive result"); else Require(state->result == -1 && state->error == 4, "interrupted flush did not return EINTR"); if (state->scenario == "signal") Require(state->signals.load() == 1, "restartable signal lost or write repeated"); Require(scePthreadKeyDelete(state->key) == 0, "delete flush TSD key"); From d0d6abc423c385addeba724757401eebdb42ef51 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 23:26:05 -0300 Subject: [PATCH 36/61] fix(libkernel): retain written bytes when synchronous flushing is interrupted --- core/libs/prx/libkernel/File/include/DescriptorControl.hpp | 2 +- docs/dev/debt/libkernel.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 727dcd0df54..ee76884ae06 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -32,7 +32,7 @@ class WriteSync { } template TResult Complete(TResult result) const { - if (posix && result > 0 && _fd >= 0 && !SyncWrites(_fd)) return static_cast(-1); + if (posix && result > 0 && _fd >= 0) SyncWrites(_fd); return result; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5a3e105a5cd..78501ef2fa4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -209,10 +209,9 @@ - [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. 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. - [POSIX write cancellation](../../../core/libs/prx/libkernel/Pthread/src/NativeIo.cpp) (libkernel) is not console-verified. It covers `write`, `_write`, `writev`, `_writev`, `pwrite`, `pwritev`, `send`, `sendto`, `sendmsg`, `_sendto` and `_sendmsg` on accepted file, shared-memory, pipe and socket descriptors; positioned socket and pipe writes retain their existing errors. Completed positive results, including partial counts, win over pending cancellation; zero and failed writes check cancellation after guards release, following FreeBSD libthr. SCE writes and libSceNet gain no cancellation points. Non-interruptible drivers can delay cancellation and close indefinitely. Windows pipe quota waits retry readiness at 1 ms intervals. Descriptor replacement and stdio closure can still wait indefinitely; stream-socket writes share the read admission and close/drain machinery. -- [Synchronous POSIX write cancellation](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) has no console measurement of interrupted `O_FSYNC` writes. Synchronization is a separate native flush after buffered progress; interruption can leave buffered bytes visible without durable completion. The operation reports success only after that flush succeeds and does not replay buffered writes after an interrupted flush. Direct SCE writes retain their existing flush and cancellation behavior. +- [Synchronous POSIX write cancellation](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) has no console measurement of interrupted `O_FSYNC` writes. Synchronization is a separate native flush after buffered progress. If that flush is interrupted, the write retains its positive byte count, and cancellation remains pending for the next cancellation point; buffered bytes may be visible without durable completion. A guest signal does not turn those bytes into `EINTR` or replay them. Direct SCE writes retain their existing flush and cancellation behavior. - [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 and write waits and run after operation guards are released, including disabled cancellation and unregistered callers. Restartable operations retry only if descriptor generations remain unchanged; socket transfer 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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From 8ef7f8551ca22ee5f3dc7c4c166a9bc320b54d7c Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 23:36:46 -0300 Subject: [PATCH 37/61] test(libkernel): cover disconnected Windows pipe writers --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- core/libs/tests/GuestIoWriteOnlyPipe.cpp | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 141edd070e4..e21d5074c15 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -268,7 +268,7 @@ if(BUILD_TESTING) target_link_libraries(guest_io_write_only_pipe_tests PRIVATE libkernel libc) configure_windows_unwind(guest_io_write_only_pipe_tests) foreach(minimalPipeApi write writev) - foreach(minimalPipeCase complete cancel disabled close nonblocking nonblocking_partial) + foreach(minimalPipeCase complete cancel disabled close nonblocking nonblocking_partial disconnected disconnected_attributes disconnected_wait) set(minimalPipeTest guest_io_write_minimal_pipe_${minimalPipeApi}__${minimalPipeCase}) add_test(NAME ${minimalPipeTest} COMMAND guest_io_write_only_pipe_tests ${minimalPipeApi} ${minimalPipeCase}) set_tests_properties(${minimalPipeTest} PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/tests/GuestIoWriteOnlyPipe.cpp b/core/libs/tests/GuestIoWriteOnlyPipe.cpp index c5d910d20ec..0f6d58ceb39 100644 --- a/core/libs/tests/GuestIoWriteOnlyPipe.cpp +++ b/core/libs/tests/GuestIoWriteOnlyPipe.cpp @@ -89,30 +89,33 @@ int main(int argc, char** argv) { std::int64_t available = 0; bool eof = false, writable = false; Require(GuestSockets::PipeWriteState(queryDescriptor, &available, &eof, &writable) && available > 0 && available < 1024 * 1024, "query finite pipe quota"); + const bool attributes = scenario == "disconnected_attributes" || scenario == "disconnected_wait"; HANDLE limited = INVALID_HANDLE_VALUE; - Require(DuplicateHandle(GetCurrentProcess(), client, GetCurrentProcess(), &limited, FILE_WRITE_DATA | SYNCHRONIZE, FALSE, 0), "retain minimal-access pipe client"); + Require(DuplicateHandle(GetCurrentProcess(), client, GetCurrentProcess(), &limited, attributes ? 0 : FILE_WRITE_DATA | SYNCHRONIZE, FALSE, attributes ? DUPLICATE_SAME_ACCESS : 0), "retain pipe client"); Require(::_close(queryDescriptor) == 0, "release pipe setup descriptor"); client = limited; using Query = LONG (NTAPI*)(HANDLE, void*, void*, ULONG, int); const auto query = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationFile")); std::uintptr_t status[2]{}; ACCESS_MASK access = 0; - Require(query && query(client, status, &access, sizeof(access), 8) >= 0 && (access & FILE_READ_ATTRIBUTES) == 0, "client unexpectedly has read-attribute access"); + Require(query && query(client, status, &access, sizeof(access), 8) >= 0 && ((access & FILE_READ_ATTRIBUTES) != 0) == attributes, "client attribute rights differ"); state.file = ::_open_osfhandle(reinterpret_cast(client), _O_WRONLY | _O_BINARY); Require(state.file >= 0, "adopt minimal pipe client"); std::vector fill(static_cast(available) - (scenario == "nonblocking_partial" ? 1 : 0), 'f'); - if (scenario != "complete") { + if (scenario != "complete" && scenario != "disconnected" && scenario != "disconnected_attributes") { DWORD count = 0; Require(WriteFile(client, fill.data(), static_cast(fill.size()), &count, nullptr) && count == fill.size(), "fill minimal pipe quota"); } + if (scenario == "disconnected" || scenario == "disconnected_attributes") Require(DisconnectNamedPipe(server), "disconnect before write"); Pthread writer{}; Require(scePthreadCreate(&writer, nullptr, Writer, &state, "minimal-pipe") == 0, "start minimal pipe writer"); Require(Observe([&] { return state.ready.load(); }), "minimal writer did not start"); - const bool blocked = scenario == "cancel" || state.disabled || scenario == "close"; + const bool blocked = scenario == "cancel" || state.disabled || scenario == "close" || scenario == "disconnected_wait"; if (blocked && !state.disabled) Require(Observe([&] { std::lock_guard lock(writer->cancelLock); return writer->cancelIo != nullptr || state.done.load(); }), "minimal writer did not register"); std::atomic closed{false}; std::thread closer; - if (scenario == "close") closer = std::thread([&] { Require(close_nid_postfix(state.file) == 0, "close minimal pipe"); closed.store(true); }); + if (scenario == "disconnected_wait") Require(DisconnectNamedPipe(server), "disconnect registered writer"); + else if (scenario == "close") closer = std::thread([&] { Require(close_nid_postfix(state.file) == 0, "close minimal pipe"); closed.store(true); }); else if (scenario == "cancel" || state.disabled) Require(scePthreadCancel(writer) == 0, "cancel minimal writer"); bool woke = true; if (state.disabled) { @@ -133,7 +136,8 @@ int main(int argc, char** argv) { std::printf("minimal pipe writer result %lld, error %d\n", static_cast(state.result), state.error); const bool canceled = scenario == "cancel" || state.disabled; Require(result == (canceled ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "minimal pipe join result"); - if (scenario == "cancel") Require(!state.returned.load(), "blocked minimal writer returned before cancellation"); + if (scenario.starts_with("disconnected")) Require(state.result == -1 && state.error == 32, "disconnected pipe did not report EPIPE"); + else if (scenario == "cancel") Require(!state.returned.load(), "blocked minimal writer returned before cancellation"); else if (scenario == "close") Require(state.result == -1 && state.error == 4, "minimal pipe close result"); else if (scenario.starts_with("nonblocking")) { Require(state.result == -1 && state.error == 35, "minimal pipe nonblocking result"); From 7a2c024aca0d8baef8a42a885fef268d63fdf77c Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 28 Oct 2026 23:39:25 -0300 Subject: [PATCH 38/61] fix(libkernel): report broken peers for disconnected pipe writes --- core/libs/prx/libkernel/File/src/Stdio.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 66bba37d1ea..bfdff61477b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -283,6 +283,10 @@ std::optional File::IoPositionedTransfer(int descr changed.WaitUntil(sleep, TimedWait::DeadlineNanos(1000)); } } +static bool BrokenPipeError(DWORD error) { + return error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA || error == ERROR_PIPE_NOT_CONNECTED; +} + std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t bytes) { const auto handle = WindowsDescriptorHandle_nid_no_patch(descriptor); const auto type = GetFileType(handle); @@ -291,7 +295,7 @@ std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t if (result < 0 && ThreadCancel::IoActive()) { if (_doserrno == ERROR_OPERATION_ABORTED) errno = EINTR; else if (type == FILE_TYPE_PIPE && errno == ENOSPC && _doserrno == 0) errno = EAGAIN; - else if (type == FILE_TYPE_PIPE && (_doserrno == ERROR_BROKEN_PIPE || _doserrno == ERROR_NO_DATA)) errno = EPIPE; + else if (type == FILE_TYPE_PIPE && BrokenPipeError(_doserrno)) errno = EPIPE; } return result; }; @@ -301,7 +305,7 @@ std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t if (pipe && ThreadCancel::Native([&] { return GetNamedPipeHandleStateW(handle, &mode, nullptr, nullptr, nullptr, nullptr, 0) ? 0 : -1; }) < 0) { const auto error = GetLastError(); if (error == ERROR_ACCESS_DENIED) return nativeWrite(); - errno = ThreadCancel::IoInterrupted() ? EINTR : error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? EPIPE : EIO; + errno = ThreadCancel::IoInterrupted() ? EINTR : BrokenPipeError(error) ? EPIPE : EIO; return -1; } const bool nonblocking = pipe && (mode & PIPE_NOWAIT) != 0; @@ -319,7 +323,12 @@ std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t try { checked = ThreadCancel::Native([&] { return GuestSockets::PipeWriteState(descriptor, &available, &eof, &writable) ? 0 : -1; }); } catch (const std::runtime_error&) { - if (!ThreadCancel::IoInterrupted()) throw; + if (!ThreadCancel::IoInterrupted()) { + const auto query = ThreadCancel::Native([&] { return GetNamedPipeHandleStateW(handle, &mode, nullptr, nullptr, nullptr, nullptr, 0) ? 0 : -1; }); + const auto error = GetLastError(); + if (query < 0 && BrokenPipeError(error)) { errno = EPIPE; return transferred == 0 ? -1 : transferred; } + if (!ThreadCancel::IoInterrupted()) throw; + } checked = -1; } if (checked < 0) { errno = ThreadCancel::IoInterrupted() ? EINTR : EBADF; return transferred == 0 ? -1 : transferred; } @@ -338,7 +347,7 @@ std::int64_t File::IoWriteNative(int descriptor, const void* buffer, std::size_t const DWORD error = result < 0 ? GetLastError() : ERROR_SUCCESS; transferred += done; if (result < 0) { - errno = error == ERROR_OPERATION_ABORTED ? EINTR : error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? EPIPE : + errno = error == ERROR_OPERATION_ABORTED ? EINTR : BrokenPipeError(error) ? EPIPE : error == ERROR_NOACCESS || error == ERROR_INVALID_USER_BUFFER ? EFAULT : error == ERROR_ACCESS_DENIED ? EBADF : error == ERROR_DISK_FULL || error == ERROR_HANDLE_DISK_FULL ? ENOSPC : EIO; return transferred == 0 ? -1 : transferred; From cb4335cce6d7c704c6900271456d22b751e4719e Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Thu, 29 Oct 2026 14:05:23 -0300 Subject: [PATCH 39/61] test(libkernel): force stream close lock interleavings --- .../prx/libc/include/DescriptorAccess.hpp | 3 + core/libs/prx/libc/include/FileStream.hpp | 19 ++++- core/libs/prx/libc/src/DescriptorAccess.cpp | 16 +++++ core/libs/prx/libc/src/RuntimeSupport.cpp | 12 ++-- core/libs/prx/libkernel/File/src/Stdio.cpp | 9 +++ core/libs/tests/GuestStreamClose.cpp | 71 +++++++++++++------ 6 files changed, 100 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libc/include/DescriptorAccess.hpp b/core/libs/prx/libc/include/DescriptorAccess.hpp index 693c990f6fb..be2e20cbc1e 100644 --- a/core/libs/prx/libc/include/DescriptorAccess.hpp +++ b/core/libs/prx/libc/include/DescriptorAccess.hpp @@ -7,6 +7,9 @@ #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); diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 2df1e0915bb..65ac8182205 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -91,12 +91,25 @@ 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) : handle(handle) { + 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 @@ -243,7 +256,7 @@ class FileStream { return Adopt(open(filename, mode)); } auto* previous = GetHandle(); - NativeLock streamLock(previous); + NativeLock streamLock(previous, true); _guest = {}; guestError = false; randomDevice = false; @@ -450,7 +463,7 @@ class FileStream { int Close() { GetHandle(); - NativeLock streamLock(_handle); + NativeLock streamLock(_handle, true); #ifdef _WIN32 GuestFileLockForget_nid_no_patch(::_fileno(_handle)); if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(::_fileno(_handle)); diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp index 607d6b40c7f..a2c796a3cd2 100644 --- a/core/libs/prx/libc/src/DescriptorAccess.cpp +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -195,3 +195,19 @@ void DescriptorWakeDestroy_nid_no_patch(void* registration) { 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/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 0b95021e58c..c013ebe783c 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -179,15 +179,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/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4d9eb40035a..ea48823751b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -259,6 +259,14 @@ static std::shared_ptr PositionedMutex(int descriptor) { } 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) { @@ -329,6 +337,7 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz 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::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) { diff --git a/core/libs/tests/GuestStreamClose.cpp b/core/libs/tests/GuestStreamClose.cpp index bfd1f21311b..b83ff19bff3 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,16 @@ 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); + funlockfile_nid_postfix(stream); + closer.join(); + FileStreamSetCloseHookForTest_nid_no_patch(nullptr); std::remove(Racing); } From bc9022d81452767176061b999a7cd4a9703d52ae Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Thu, 29 Oct 2026 14:17:51 -0300 Subject: [PATCH 40/61] test(libkernel): check POSIX admission while a stream close waits --- core/libs/tests/GuestStreamClose.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestStreamClose.cpp b/core/libs/tests/GuestStreamClose.cpp index b83ff19bff3..a414f363fbb 100644 --- a/core/libs/tests/GuestStreamClose.cpp +++ b/core/libs/tests/GuestStreamClose.cpp @@ -118,6 +118,8 @@ static void CloseWaitsForALockedStream() { 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); From d7e75a0f77df47db3354bd9367d7c542460bc266 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Thu, 29 Oct 2026 14:20:22 -0300 Subject: [PATCH 41/61] feat(kernel): make POSIX accept cancellation-safe --- core/libs/prx/libkernel/CMakeLists.txt | 12 ++ .../prx/libkernel/Pthread/include/Cancel.hpp | 3 + .../prx/libkernel/Pthread/src/IoSignals.cpp | 4 + .../Socket/include/SocketRuntime.hpp | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 34 ++- core/libs/tests/GuestAcceptCancel.cpp | 197 ++++++++++++++++++ docs/dev/debt/libkernel.md | 5 +- 7 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 core/libs/tests/GuestAcceptCancel.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 5a5f15bb777..8e595bb1aeb 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -261,6 +261,18 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_io_native_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_accept_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestAcceptCancel.cpp) + target_include_directories(guest_accept_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_accept_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_accept_cancel_tests) + foreach(acceptScenario cancel alias pending disabled complete close reuse nonblocking flags handoff_cancel handoff_jump handoff_pending handoff_reuse) + add_test(NAME guest_accept_${acceptScenario} COMMAND guest_accept_cancel_tests ${acceptScenario}) + set_tests_properties(guest_accept_${acceptScenario} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_accept_${acceptScenario} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() 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) diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index aa58475bc8d..a824e3314cb 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -6,6 +6,7 @@ #include "prx/libc/include/DescriptorAccess.hpp" #include #include +#include #include #include @@ -49,6 +50,8 @@ class IoRestart { ~IoRestart(); bool Ready() const; void Clear(); + void RetainResult(std::shared_ptr result); + std::shared_ptr ReleaseResult(); IoRestart(const IoRestart&) = delete; IoRestart& operator=(const IoRestart&) = delete; private: diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp index 2908ba7de7b..e45b1b2b73a 100644 --- a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -38,6 +38,7 @@ struct IoRestart::State { std::uint32_t context; bool allowed; bool ready = false; + std::shared_ptr result; std::vector expected; State(State* previous, std::uintptr_t stack, bool allowed) : previous(previous), stack(stack), context(JumpBuffer::GuestContextCurrent_nid_no_patch()), allowed(allowed) {} @@ -237,6 +238,9 @@ void IoRestart::Clear() { bool IoRestart::Ready() const { return state->ready && IoRestartError(); } +void IoRestart::RetainResult(std::shared_ptr result) { state->result = std::move(result); } +std::shared_ptr IoRestart::ReleaseResult() { return std::move(state->result); } + void BeginIoSignals(Pthread thread, IoRestart& restart, std::optional& deferral) { if (current) { if (!deferral) deferral.emplace(); diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 9194a5a8514..b56ef948185 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -44,3 +44,4 @@ extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor); extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* source, std::size_t bytes); extern "C" std::uint64_t GuestSocketWakerCreationsForTest_nid_no_patch(); +extern "C" void GuestSocketAcceptObserverForTest_nid_no_patch(void (*observer)()); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index ae22e76259e..5bc15235437 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -74,6 +74,7 @@ constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } constexpr int GuestSocketDescriptorLimit = 1024; +thread_local void (*acceptObserver)() = nullptr; constexpr int GuestPeek = 0x2; constexpr int GuestWaitAll = 0x40; constexpr int GuestDontWait = 0x80; @@ -1536,7 +1537,7 @@ int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const int error = NativeError(); return Fail(error == 56 ? 22 : error); } -int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* length) { +static int Accept(int descriptor, void* address, std::uint32_t* length, std::shared_ptr& accepted) { const auto listener = Lookup(descriptor); if (!listener) return -1; if (listener->type != 1 || (address && !length)) return Fail(22); @@ -1551,7 +1552,7 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l if (result < 0) return Fail(NativeError()); Socket guard(native, listener->family, listener->type); try { - auto accepted = std::make_shared(native, listener->family, listener->type); + accepted = std::make_shared(native, listener->family, listener->type); guard.value = Invalid; if (listener->family == 28) accepted->options.tcpMaximumSegment = 1220; bool inheritNonblocking; @@ -1574,14 +1575,33 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l accepted->options.tcpFlags = listener->options.tcpFlags & 0x1000; } if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; - const int acceptedDescriptor = Install(std::move(accepted)); - if (acceptedDescriptor < 0) return -1; if (address) GuestAddress(peer, address, length); - return acceptedDescriptor; + return 0; } catch (const std::bad_alloc&) { return Fail(12); } } +int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* length) { + ThreadCancel::Check(); + ThreadCancel::IoRestart restart(true); + for (;;) { + std::int64_t result; + { + ThreadCancel::IoPoint point(restart); + std::shared_ptr accepted; + result = File::WithIoDescriptor(descriptor, [&](int descriptor) { + return Accept(descriptor, address, length, accepted); + }); + if (result >= 0) { + restart.RetainResult(std::move(accepted)); + if (acceptObserver) acceptObserver(); + } + } + if (result >= 0) return Install(std::static_pointer_cast(restart.ReleaseResult())); + if (ThreadCancel::Requested()) { restart.Clear(); ThreadCancel::Check(); } + if (!restart.Ready()) return static_cast(result); + } +} std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; @@ -2364,3 +2384,7 @@ std::int64_t APS5_VABI _recvmsg_nid_postfix(int descriptor, GuestMsghdr* message extern "C" std::uint64_t GuestSocketWakerCreationsForTest_nid_no_patch() { return wakerCreations.load(); } + +extern "C" void GuestSocketAcceptObserverForTest_nid_no_patch(void (*observer)()) { + acceptObserver = observer; +} diff --git a/core/libs/tests/GuestAcceptCancel.cpp b/core/libs/tests/GuestAcceptCancel.cpp new file mode 100644 index 00000000000..d09fc1cde76 --- /dev/null +++ b/core/libs/tests/GuestAcceptCancel.cpp @@ -0,0 +1,197 @@ +#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 + +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(3); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +struct State { + int listener = -1; + int accepted = -1; + int error = 0; + PthreadKey key = -1; + bool disabled = false; + bool pending = false; + bool alias = false; + bool handoff = false; + bool jump = false; + bool flags = false; + bool handoffPending = false; + bool handoffReuse = false; + alignas(16) std::array jumpBuffer{}; + std::array peer{}; + std::uint32_t peerSize = peer.size(); + int acceptedFlags = -1; + int acceptedDescriptorFlags = -1; + std::atomic observed{false}; + std::atomic ready{false}; + std::atomic release{false}; + std::atomic returned{false}; + std::atomic cleanups{0}; + std::atomic destructors{0}; +}; + +static void APS5_VABI Cleanup(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.fetch_add(1) == 0 && state.destructors.load() == 0, "cleanup ordering/count"); + if (state.accepted >= 0) Require(close_nid_postfix(state.accepted) == 0, "close accepted socket"); +} + +static void APS5_VABI Destroy(void* opaque) { + auto& state = *static_cast(opaque); + Require(state.cleanups.load() == 1 && state.destructors.fetch_add(1) == 0, "TSD ordering/count"); +} + +static thread_local State* accepting; + +static void LeaveHandoff() { + if (accepting->jump) longjmp_nid_postfix(accepting->jumpBuffer.data(), 1); + if (accepting->handoffReuse) { + Require(close_nid_postfix(accepting->listener) == 0, "close listener in delivery"); + const int replacement = socket_nid_postfix(2, 1, 0); + Require(replacement >= 0, "delivery replacement"); + if (replacement != accepting->listener) { + Require(dup2_nid_postfix(replacement, accepting->listener) == accepting->listener, "delivery descriptor reuse"); + Require(close_nid_postfix(replacement) == 0, "release delivery duplicate"); + } + } + Require(pthread_cancel_nid_postfix(pthread_self_nid_postfix()) == 0, "self cancel accepted handoff"); + if (accepting->handoffPending) return; + scePthreadTestcancel(); + Require(false, "handoff cancellation returned"); +} + +static void ObserveHandoff() { + accepting->observed.store(true); + Require(ThreadCancel::DeferIoCallback(LeaveHandoff, nullptr), "defer accepted handoff"); +} + +static void* APS5_VABI Acceptor(void* opaque) { + auto& state = *static_cast(opaque); + if (state.disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable"); + Require(scePthreadSetspecific(state.key, &state) == 0, "set TSD"); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); + accepting = &state; + if (state.handoff) GuestSocketAcceptObserverForTest_nid_no_patch(ObserveHandoff); + state.ready.store(true); + if (state.pending) Require(Observe([&] { return state.release.load(); }), "pending release"); + if (!state.jump || setjmp_nid_postfix(state.jumpBuffer.data()) == 0) + state.accepted = state.alias ? _accept_nid_postfix(state.listener, nullptr, nullptr) : + accept_nid_postfix(state.listener, state.peer.data(), &state.peerSize); + GuestSocketAcceptObserverForTest_nid_no_patch(nullptr); + if (state.accepted >= 0) { + state.acceptedFlags = fcntl_nid_postfix(state.accepted, 3); + state.acceptedDescriptorFlags = fcntl_nid_postfix(state.accepted, 1); + } + state.error = *__error_nid_postfix(); + state.returned.store(true); + if (state.disabled) { + Require(state.accepted >= 0, "disabled accept lost connection"); + Require(scePthreadSetcancelstate(GUEST_CANCEL_ENABLE, nullptr) == 0, "enable"); + scePthreadTestcancel(); + } + __pthread_cleanup_pop_imp_nid_postfix(1); + return reinterpret_cast(std::uintptr_t{7}); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected scenario"); + const std::string scenario = argv[1]; + State state; + state.disabled = scenario == "disabled"; + state.pending = scenario == "pending"; + state.alias = scenario == "alias"; + state.handoff = scenario.starts_with("handoff_"); + state.handoffPending = scenario == "handoff_pending"; + state.handoffReuse = scenario == "handoff_reuse"; + state.jump = scenario == "handoff_jump"; + state.flags = scenario == "flags"; + state.listener = socket_nid_postfix(2, 1, 0); + Require(state.listener >= 0, "create listener"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(state.listener, address.data(), address.size()) == 0, "bind"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(state.listener, address.data(), &size) == 0, "address"); + Require(listen_nid_postfix(state.listener, 4) == 0, "listen"); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0, "client"); + if (scenario == "nonblocking" || state.flags) Require(fcntl_nid_postfix(state.listener, 4, 6) == 0, "nonblocking"); + Require(fcntl_nid_postfix(state.listener, 2, 1) == 0, "listener close-on-exec"); + if (state.flags) Require(connect_nid_postfix(client, address.data(), address.size()) == 0, "queue connection"); + Require(scePthreadKeyCreate(&state.key, reinterpret_cast(Destroy)) == 0, "TSD key"); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Acceptor, &state, nullptr) == 0, "create acceptor"); + Require(Observe([&] { return state.ready.load(); }), "acceptor admission"); + if (!state.pending && !state.disabled && scenario != "nonblocking" && !state.flags) { + Require(Observe([&] { + std::lock_guard lock(thread->cancelLock); + return thread->cancelIo != nullptr; + }), "accept wait registration"); + } + const bool cancel = scenario == "cancel" || scenario == "alias" || state.disabled || state.pending; + if (cancel) Require(pthread_cancel_nid_postfix(thread) == 0, "cancel"); + state.release.store(true); + if (scenario == "complete" || state.disabled || state.handoff) Require(connect_nid_postfix(client, address.data(), address.size()) == 0, "complete accept"); + int replacement[2]{-1, -1}; + if (scenario == "reuse") Require(pipe_nid_postfix(replacement) == 0, "replacement pipe"); + if (scenario == "close" || scenario == "reuse") Require(close_nid_postfix(state.listener) == 0, "close waiting listener"); + if (scenario == "reuse") { + if (replacement[0] != state.listener) Require(dup2_nid_postfix(replacement[0], state.listener) == state.listener, "reuse listener slot"); + Require(write_nid_postfix(replacement[1], "r", 1) == 1, "seed replacement"); + } + Require(Observe([&] { return state.destructors.load() == 1; }), "accept did not finish"); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == 0, "join"); + Require(result == ((cancel || (state.handoff && !state.jump && !state.handoffPending)) ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "thread result"); + Require(state.cleanups.load() == 1 && state.destructors.load() == 1, "cleanup counts"); + if (scenario == "nonblocking") Require(state.accepted == -1 && state.error == 35, "nonblocking errno"); + if (scenario == "complete" || state.flags) { + Require(state.accepted >= 0 && state.peerSize == 16 && state.peer[0] == 16 && state.peer[1] == 2, "completed accept peer"); + Require(state.acceptedDescriptorFlags == 0, "accepted close-on-exec inherited"); + Require((state.acceptedFlags & 4) == (state.flags ? 4 : 0), "accepted nonblocking flags"); + } + if (state.handoff) { + Require(state.observed.load() && (state.handoffPending ? state.accepted >= 0 : state.accepted == -1), "accepted ownership handoff not reached"); + if (state.handoffReuse) Require(fcntl_nid_postfix(state.listener, 3) >= 0, "disposal closed replacement"); + char byte = 0; + Require(Observe([&] { return recv_nid_postfix(client, &byte, 1, 0x80) == 0; }), "unpublished accepted socket leaked"); + } + if (scenario == "reuse") { + char byte = 0; + Require(read_nid_postfix(state.listener, &byte, 1) == 1 && byte == 'r', "retired accept damaged replacement"); + if (replacement[0] != state.listener) Require(close_nid_postfix(replacement[0]) == 0, "close replacement read end"); + Require(close_nid_postfix(replacement[1]) == 0, "close replacement write end"); + } + if (scenario == "close" || scenario == "reuse") Require(state.accepted == -1 && state.error == 4, "closed operation errno"); + if (scenario == "cancel" || scenario == "alias" || state.pending) { + Require(connect_nid_postfix(client, address.data(), address.size()) == 0, "listener survives cancellation"); + const int accepted = accept_nid_postfix(state.listener, nullptr, nullptr); + Require(accepted >= 0 && close_nid_postfix(accepted) == 0, "accept after cancellation"); + } + if (scenario != "close") Require(close_nid_postfix(state.listener) == 0, "close listener"); + Require(close_nid_postfix(client) == 0 && scePthreadKeyDelete(state.key) == 0, "release resources"); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 53fca01b21e..fdcaf7f7066 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -207,8 +207,9 @@ - [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. 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. +- [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, connect 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 accept cancellation](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) is not console-verified. `accept` and `_accept` wake on enabled deferred cancellation; libSceNet has no new cancellation point. Close interrupts and drains an admitted accept, returning `EINTR` unless acceptance completed; this close behavior is not console-measured. A completed accept wins a pending cancellation request, but guest exit or longjmp during deferred delivery disposes the unpublished connection. Unix uses the read cancellation backend and its host signal limitations. macOS has not been run. - [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) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured @@ -288,7 +289,7 @@ - [F_DUPFD_CLOEXEC and F_DUP2FD_CLOEXEC](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) - `F_DUPFD_CLOEXEC` on a shared memory descriptor fails with `EOPNOTSUPP`, where PS5 firmware 13.60 returns 0 for any start without setting `errno`; whether it duplicates anything is not known, since descriptor 0 was already open. `F_DUP2FD_CLOEXEC` returns the lowest free descriptor, where the console picks scattered free numbers below 1024 (17 to 1012 measured). Otherwise both follow the console: `F_DUPFD_CLOEXEC` fails with `EOPNOTSUPP` for any start on files, sockets, pipes and kqueues, after `EBADF` for a bad descriptor; `F_DUP2FD_CLOEXEC` ignores its target, which it neither opens nor closes (-1, -2, `INT_MAX`, the source and an open descriptor alike), and returns a new descriptor for the same open file without `FD_CLOEXEC`, or `EBADF` for a bad source - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) do not pass a `sceNetSocket` socket to libSceNet as `fcntl` does. On Linux its number duplicates the host socket, so `FIONBIO` switches the host `O_NONBLOCK` but not the library's `SO_NBIO` state; on Windows it is a `NUL` placeholder, so `FIONREAD` and enabling `FIONBIO` throw as on a character device. `sceNetIoctl` handles both requests on such a socket. -- [_accept, _bind, _connect, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [_getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [_sigaction, _sigsuspend](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) are not measured on the console. They forward to the calls without the underscore, which have no libthr layer here, as FreeBSD 11.4 makes the underscore names the plain system calls. `_sigaction` and `sigaction` share one handler table, where FreeBSD's `__thr_sigaction` hands its own wrapper to the system call `_sigaction` reads back, and rejects `SIGCANCEL` (32). A null set throws in `_sigsuspend` as in `sigsuspend`, while the system call FreeBSD 11.4 exports as `_sigsuspend` ([`Symbol.map`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/sys/Symbol.map#L961)) returns `EFAULT` +- [_accept, _bind, _connect, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [_getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [_sigaction, _sigsuspend](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) are not measured on the console. They forward to the calls without the underscore; `_accept`, `_recvfrom` and `_recvmsg` therefore share their cancellation points, unlike the plain system calls FreeBSD 11.4 assigns to these underscore names. The console contract is unverified. `_sigaction` and `sigaction` share one handler table, where FreeBSD's `__thr_sigaction` hands its own wrapper to the system call `_sigaction` reads back, and rejects `SIGCANCEL` (32). A null set throws in `_sigsuspend` as in `sigsuspend`, while the system call FreeBSD 11.4 exports as `_sigsuspend` ([`Symbol.map`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/sys/Symbol.map#L961)) returns `EFAULT` - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as does a write-only descriptor; FreeBSD 11.4's `vn_mmap` maps a write-only descriptor without `PROT_READ`, and Linux refuses it with `EACCES`. A `PROT_EXEC` mapping is a view of a section created on a second handle that `ReOpenFile` opens with execute access, so a file whose ACL does not grant the user execute fails with `EACCES`, where FreeBSD maps it unless the file system is mounted `noexec`. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A file that is not a regular file or shared memory, such as a socket, `/dev/urandom` or a kqueue, fails with `EINVAL` and `*sbytes` 0, as FreeBSD's [`sendfile_getobj`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_sendfile.c#L421-L463) refuses it. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it From 096b544fd5c678da2a365883dc00530dd2ed4b52 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Thu, 29 Oct 2026 14:36:02 -0300 Subject: [PATCH 42/61] docs(kernel): shorten socket alias cancellation debt --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 71eef0f442d..88eac77134c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -292,7 +292,7 @@ - [F_DUPFD_CLOEXEC and F_DUP2FD_CLOEXEC](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) - `F_DUPFD_CLOEXEC` on a shared memory descriptor fails with `EOPNOTSUPP`, where PS5 firmware 13.60 returns 0 for any start without setting `errno`; whether it duplicates anything is not known, since descriptor 0 was already open. `F_DUP2FD_CLOEXEC` returns the lowest free descriptor, where the console picks scattered free numbers below 1024 (17 to 1012 measured). Otherwise both follow the console: `F_DUPFD_CLOEXEC` fails with `EOPNOTSUPP` for any start on files, sockets, pipes and kqueues, after `EBADF` for a bad descriptor; `F_DUP2FD_CLOEXEC` ignores its target, which it neither opens nor closes (-1, -2, `INT_MAX`, the source and an open descriptor alike), and returns a new descriptor for the same open file without `FD_CLOEXEC`, or `EBADF` for a bad source - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) do not pass a `sceNetSocket` socket to libSceNet as `fcntl` does. On Linux its number duplicates the host socket, so `FIONBIO` switches the host `O_NONBLOCK` but not the library's `SO_NBIO` state; on Windows it is a `NUL` placeholder, so `FIONREAD` and enabling `FIONBIO` throw as on a character device. `sceNetIoctl` handles both requests on such a socket. -- [_accept, _bind, _connect, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [_getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [_sigaction, _sigsuspend](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) are not measured on the console. They forward to the calls without the underscore; `_accept`, `_recvfrom` and `_recvmsg` therefore share their cancellation points, unlike the plain system calls FreeBSD 11.4 assigns to these underscore names. The console contract is unverified. `_sigaction` and `sigaction` share one handler table, where FreeBSD's `__thr_sigaction` hands its own wrapper to the system call `_sigaction` reads back, and rejects `SIGCANCEL` (32). A null set throws in `_sigsuspend` as in `sigsuspend`, while the system call FreeBSD 11.4 exports as `_sigsuspend` ([`Symbol.map`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/sys/Symbol.map#L961)) returns `EFAULT` +- [_accept, _bind, _connect, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [_getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [_sigaction, _sigsuspend](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) are not measured on the console. They forward to the unprefixed calls, including cancellation in `_accept`, `_recvfrom` and `_recvmsg`; FreeBSD 11.4 exports plain system calls. `_sigaction` and `sigaction` share one handler table, where FreeBSD's `__thr_sigaction` hands its own wrapper to the system call `_sigaction` reads back, and rejects `SIGCANCEL` (32). A null set throws in `_sigsuspend` as in `sigsuspend`, while the system call FreeBSD 11.4 exports as `_sigsuspend` ([`Symbol.map`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/sys/Symbol.map#L961)) returns `EFAULT` - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as does a write-only descriptor; FreeBSD 11.4's `vn_mmap` maps a write-only descriptor without `PROT_READ`, and Linux refuses it with `EACCES`. A `PROT_EXEC` mapping is a view of a section created on a second handle that `ReOpenFile` opens with execute access, so a file whose ACL does not grant the user execute fails with `EACCES`, where FreeBSD maps it unless the file system is mounted `noexec`. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A file that is not a regular file or shared memory, such as a socket, `/dev/urandom` or a kqueue, fails with `EINVAL` and `*sbytes` 0, as FreeBSD's [`sendfile_getobj`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_sendfile.c#L421-L463) refuses it. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it From 55161f97476a8bd413c279829cabb6390307e4d5 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Fri, 30 Oct 2026 00:10:03 -0300 Subject: [PATCH 43/61] test(libkernel): await readable data before nonblocking receive races --- core/libs/tests/NetDontWait.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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) From daa3a810cbb43e399e3912df6bf3edb4221ef98c Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Fri, 30 Oct 2026 00:23:13 -0300 Subject: [PATCH 44/61] fix(libkernel): initialize default blocking send limits --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index bf46ce85105..72ca00e1d7d 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -946,7 +946,16 @@ int SendBytes(Socket& socket, int remaining, int flags) { #ifdef _WIN32 if (ThreadCancel::IoActive() && socket.type == 1 && (flags & GuestDontWait) == 0) { std::lock_guard lock(socket.modeMutex); - if (!socket.options.nonblocking) return std::min(remaining, std::max(1, socket.options.sendBuffer)); + if (!socket.options.nonblocking) { + if (socket.options.sendBuffer == 0) { + int buffer = 0; + std::uint32_t length = sizeof(buffer); + const int error = GuestSockets::GetSocketLevelOption(static_cast(socket.value), socket.options, + socket.family, socket.type, 0x1001, &buffer, &length); + if (error != 0) throw std::runtime_error("send: initializing the send-buffer limit failed, error=" + std::to_string(error)); + } + return std::min(remaining, std::max(1, socket.options.sendBuffer)); + } } #else static_cast(socket); From 75147ee78de7d93bbb88e217888e9c3e52c8382b Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sat, 31 Oct 2026 00:41:41 -0300 Subject: [PATCH 45/61] fix(libc): restore I/O contexts through underscore jumps --- core/libs/prx/libc/src/JumpBuffer.cpp | 10 ++++++++-- core/libs/prx/libkernel/CMakeLists.txt | 2 +- core/libs/tests/GuestIoContext.cpp | 14 ++++++++++++++ core/libs/tests/GuestUnderscoreJumps.cpp | 11 +++++++++-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index d7914d75f9a..afd31e97256 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -154,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" @@ -170,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/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index e8d28428fc5..574b322f391 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -118,7 +118,7 @@ if(BUILD_TESTING) 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) - foreach(ioContext adopted adopted_exit swap jump sigjump manual_swap manual_jump manual_sigjump manual_leave manual_sigleave) + 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) diff --git a/core/libs/tests/GuestIoContext.cpp b/core/libs/tests/GuestIoContext.cpp index 602777dee93..1bcbaab9fb0 100644 --- a/core/libs/tests/GuestIoContext.cpp +++ b/core/libs/tests/GuestIoContext.cpp @@ -55,6 +55,7 @@ struct State { Fibre* active = nullptr; bool jumps = false; bool signalJumps = false; + bool underscoreJumps = false; bool manual = false; bool leave = false; bool signalLeave = false; @@ -100,6 +101,18 @@ static void Switch(Fibre& from, Fibre& to) { } 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]; @@ -262,6 +275,7 @@ int main(int argc, char** argv) { 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"); 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; } From 592b32ad989eab9d85dfea2dcc521b49e8c5d60a Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sat, 31 Oct 2026 00:42:48 -0300 Subject: [PATCH 46/61] test(libkernel): bound and diagnose context delivery hangs --- core/libs/tests/GuestIoContext.cpp | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/core/libs/tests/GuestIoContext.cpp b/core/libs/tests/GuestIoContext.cpp index 1bcbaab9fb0..312649d7b0f 100644 --- a/core/libs/tests/GuestIoContext.cpp +++ b/core/libs/tests/GuestIoContext.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -66,6 +68,31 @@ struct State { std::atomic adopted{nullptr}; }; +class Watchdog { +public: + std::atomic stage{"setup"}; + explicit Watchdog(State& state) : monitor([this, &state] { + std::unique_lock lock(mutex); + if (condition.wait_for(lock, std::chrono::seconds(10), [this] { return done; })) return; + std::fprintf(stderr, "I/O context stalled: stage=%s phase=%d\n", stage.load(), state.phase.load()); + std::fflush(stderr); + std::abort(); + }) {} + ~Watchdog() { + { + std::lock_guard lock(mutex); + done = true; + } + condition.notify_one(); + monitor.join(); + } +private: + std::mutex mutex; + std::condition_variable condition; + bool done = false; + std::thread monitor; +}; + static State* state; #ifndef _WIN32 @@ -236,6 +263,7 @@ int main(int argc, char** argv) { Require(argc == 2, "expected adopted, adopted_exit, swap, jump or sigjump"); State reading; state = &reading; + Watchdog watchdog(reading); #ifdef __linux__ if (std::strcmp(argv[1], "churn") == 0) { ContextChurn(); return 0; } #endif @@ -282,13 +310,16 @@ int main(int argc, char** argv) { Pthread worker = nullptr; Require(scePthreadCreate(&worker, nullptr, Scheduler, nullptr, nullptr) == 0, "create fibre scheduler"); for (int phase = 1; phase <= 2; ++phase) { + watchdog.stage.store("observe read admission"); Require(Observe([&] { if (reading.phase.load() != phase) return false; std::lock_guard lock(worker->cancelLock); return worker->cancelIo != nullptr; }), "fibre read did not block"); + watchdog.stage.store("raise exception"); Require(sceKernelRaiseException(worker, 30) == 0, "interrupt fibre read"); } + watchdog.stage.store("observe scheduler completion"); 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"); From 8af4225e98b96e800a4ab3c1e7c7456391db1f02 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 20:31:15 -0300 Subject: [PATCH 47/61] fix(libkernel): bound I/O retries and shard descriptor ownership --- core/libs/prx/libc/src/DescriptorAccess.cpp | 44 ++++++++++--- core/libs/prx/libkernel/CMakeLists.txt | 13 ++++ .../prx/libkernel/Pthread/include/Cancel.hpp | 4 ++ .../prx/libkernel/Pthread/src/IoSignals.cpp | 7 ++- .../prx/libkernel/System/src/Exception.cpp | 6 ++ core/libs/tests/GuestIoContext.cpp | 18 ++++++ core/libs/tests/GuestIoDescriptorAccess.cpp | 63 ++++++++++++++++++- 7 files changed, 145 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/src/DescriptorAccess.cpp b/core/libs/prx/libc/src/DescriptorAccess.cpp index a2c796a3cd2..db81a3d0ee2 100644 --- a/core/libs/prx/libc/src/DescriptorAccess.cpp +++ b/core/libs/prx/libc/src/DescriptorAccess.cpp @@ -1,6 +1,8 @@ #include "prx/libc/include/DescriptorAccess.hpp" #include +#include +#include #include #include #include @@ -32,13 +34,37 @@ struct GuestDescriptorAccess::State { }; std::shared_ptr GuestDescriptorAccess::Get(int descriptor) { - static std::mutex mutex; - static std::map> entries; - std::lock_guard lock(mutex); - std::erase_if(entries, [](const auto& entry) { return entry.second.expired(); }); - if (auto state = entries[descriptor].lock()) return state; - auto state = std::make_shared(); - entries[descriptor] = state; + 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; } @@ -59,9 +85,11 @@ void DescriptorAccessLock_nid_no_patch(void* access) { 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, std::chrono::milliseconds(1)); + 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 (...) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1c5d53490f0..53e13a48c88 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -118,6 +118,11 @@ if(BUILD_TESTING) 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) @@ -217,6 +222,14 @@ if(BUILD_TESTING) 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) diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index aa58475bc8d..be3cbfa25fe 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -29,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(); diff --git a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp index 2908ba7de7b..fbf75a32807 100644 --- a/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp +++ b/core/libs/prx/libkernel/Pthread/src/IoSignals.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -180,6 +181,7 @@ class SignalManager { while (sem_wait(Semaphore()) != 0 && errno == EINTR) {} #endif bool pending; + auto retry = std::chrono::milliseconds(1); do { pending = false; { @@ -191,7 +193,10 @@ class SignalManager { if (state->wait) state->wait->Wake(); } } - if (pending) std::this_thread::yield(); + if (pending) { + std::this_thread::sleep_for(retry); + retry = std::min(retry * 2, std::chrono::milliseconds(16)); + } } while (pending); } } diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index e7bdea05316..3337cd6f2fd 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -324,6 +324,7 @@ void CALLBACK WaitingEntry(ULONG_PTR parameter) { void DeliverIoCallbacks() { while (auto* queued = ioDeliveries) { + if (ThreadCancel::DeferIoCallback(DeliverIoCallbacks, RetireIoCallbacks)) return; ioDeliveries = queued->next; WaitingEntry(reinterpret_cast(queued)); } @@ -1034,6 +1035,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/tests/GuestIoContext.cpp b/core/libs/tests/GuestIoContext.cpp index 1bcbaab9fb0..9f1d2d80920 100644 --- a/core/libs/tests/GuestIoContext.cpp +++ b/core/libs/tests/GuestIoContext.cpp @@ -232,10 +232,28 @@ static void ContextChurn() { } #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 diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index e7731a82f82..343ac536538 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -720,6 +720,65 @@ static void RecordLocks() { } #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([&] { + entered.store(true); + Require(Observe([&] { return GuestDescriptorAccess::Operation::CurrentInterrupted(); }), "close did not interrupt before native entry"); + interrupted.store(true); + Require(Observe([&] { return release.load(); }), "release native entry"); + 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) { @@ -733,7 +792,9 @@ int main(int argc, char** argv) { else CloseReaders(std::atoi(argv[2]) != 0); return 0; } - if (std::strcmp(argv[1], "capacity") == 0) Capacity(); + 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(); From ee8a3b975dd4433220e55c55adafa0faa0f81769 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 20:33:44 -0300 Subject: [PATCH 48/61] test(libkernel): fault inject accepted descriptor installation and disposal --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- .../Socket/include/SocketRuntime.hpp | 2 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 6 +++ core/libs/tests/GuestAcceptCancel.cpp | 44 ++++++++++++++++++- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a5c48e56b79..f6906dafcc9 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -395,7 +395,7 @@ if(BUILD_TESTING) target_include_directories(guest_accept_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_accept_cancel_tests PRIVATE libkernel libc) configure_windows_unwind(guest_accept_cancel_tests) - foreach(acceptScenario cancel alias pending disabled complete close reuse nonblocking flags handoff_cancel handoff_jump handoff_pending handoff_reuse) + foreach(acceptScenario install_reserve_failure install_map_failure cancel alias pending disabled complete close reuse nonblocking flags handoff_cancel handoff_jump handoff_pending handoff_reuse) add_test(NAME guest_accept_${acceptScenario} COMMAND guest_accept_cancel_tests ${acceptScenario}) set_tests_properties(guest_accept_${acceptScenario} PROPERTIES TIMEOUT 15) if(WIN32) diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index b56ef948185..5f7c47e449f 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -45,3 +45,5 @@ extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* extern "C" std::uint64_t GuestSocketWakerCreationsForTest_nid_no_patch(); extern "C" void GuestSocketAcceptObserverForTest_nid_no_patch(void (*observer)()); + +extern "C" void GuestSocketAcceptInstallFaultForTest_nid_no_patch(int stage); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index e33e29a0798..a68ec9c3130 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -79,6 +79,7 @@ constexpr auto Invalid = -1; int Fail(int error) { *__error_nid_postfix() = error; return -1; } constexpr int GuestSocketDescriptorLimit = 1024; thread_local void (*acceptObserver)() = nullptr; +thread_local int acceptInstallFault = 0; constexpr int GuestPeek = 0x2; constexpr int GuestWaitAll = 0x40; constexpr int GuestDontWait = 0x80; @@ -420,6 +421,7 @@ int Install(std::shared_ptr socket) { const int number = ReserveNumber(socket->value); if (number < 0) return -1; try { + if (acceptInstallFault == 2) { acceptInstallFault = 0; throw std::bad_alloc(); } std::lock_guard lock(socketsMutex); sockets.insert_or_assign(number, std::move(socket)); } catch (const std::bad_alloc&) { @@ -2864,3 +2866,7 @@ extern "C" std::uint64_t GuestSocketWakerCreationsForTest_nid_no_patch() { extern "C" void GuestSocketAcceptObserverForTest_nid_no_patch(void (*observer)()) { acceptObserver = observer; } + +extern "C" void GuestSocketAcceptInstallFaultForTest_nid_no_patch(int stage) { + acceptInstallFault = stage; +} diff --git a/core/libs/tests/GuestAcceptCancel.cpp b/core/libs/tests/GuestAcceptCancel.cpp index d09fc1cde76..3044afed2e4 100644 --- a/core/libs/tests/GuestAcceptCancel.cpp +++ b/core/libs/tests/GuestAcceptCancel.cpp @@ -11,6 +11,12 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#include +#endif static void Require(bool condition, const char* message) { if (condition) return; @@ -28,10 +34,28 @@ static bool Observe(TPredicate predicate) { return true; } +static std::size_t NativeDescriptorCount() { +#ifdef _WIN32 + DWORD count = 0; + Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0, "count native handles"); + return count; +#else + std::size_t count = 0; + const auto limit = ::sysconf(_SC_OPEN_MAX); + Require(limit > 0, "native descriptor limit"); + for (int descriptor = 0; descriptor < limit; ++descriptor) + if (::fcntl(descriptor, F_GETFD) >= 0) ++count; + return count; +#endif +} + struct State { int listener = -1; int accepted = -1; int error = 0; + int installFault = 0; + std::size_t nativeBefore = 0; + bool countDisposal = false; PthreadKey key = -1; bool disabled = false; bool pending = false; @@ -56,12 +80,14 @@ struct State { static void APS5_VABI Cleanup(void* opaque) { auto& state = *static_cast(opaque); + if (state.countDisposal) Require(NativeDescriptorCount() == state.nativeBefore, "accepted native resource survived until cleanup"); Require(state.cleanups.fetch_add(1) == 0 && state.destructors.load() == 0, "cleanup ordering/count"); if (state.accepted >= 0) Require(close_nid_postfix(state.accepted) == 0, "close accepted socket"); } static void APS5_VABI Destroy(void* opaque) { auto& state = *static_cast(opaque); + if (state.countDisposal) Require(NativeDescriptorCount() == state.nativeBefore, "accepted native resource survived until TSD"); Require(state.cleanups.load() == 1 && state.destructors.fetch_add(1) == 0, "TSD ordering/count"); } @@ -86,6 +112,10 @@ static void LeaveHandoff() { static void ObserveHandoff() { accepting->observed.store(true); + if (accepting->installFault) { + GuestSocketAcceptInstallFaultForTest_nid_no_patch(accepting->installFault); + return; + } Require(ThreadCancel::DeferIoCallback(LeaveHandoff, nullptr), "defer accepted handoff"); } @@ -97,6 +127,14 @@ static void* APS5_VABI Acceptor(void* opaque) { __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); accepting = &state; if (state.handoff) GuestSocketAcceptObserverForTest_nid_no_patch(ObserveHandoff); + if (state.countDisposal) { + GuestSockets::CurrentWaker(); + { + ThreadCancel::IoRestart warmup(false); + ThreadCancel::IoPoint point(warmup); + } + state.nativeBefore = NativeDescriptorCount(); + } state.ready.store(true); if (state.pending) Require(Observe([&] { return state.release.load(); }), "pending release"); if (!state.jump || setjmp_nid_postfix(state.jumpBuffer.data()) == 0) @@ -130,6 +168,9 @@ int main(int argc, char** argv) { state.handoffReuse = scenario == "handoff_reuse"; state.jump = scenario == "handoff_jump"; state.flags = scenario == "flags"; + state.installFault = scenario == "install_reserve_failure" ? 1 : scenario == "install_map_failure" ? 2 : 0; + state.countDisposal = state.installFault || scenario == "handoff_cancel" || scenario == "handoff_jump"; + state.handoff = state.handoff || state.installFault; state.listener = socket_nid_postfix(2, 1, 0); Require(state.listener >= 0, "create listener"); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -166,7 +207,7 @@ int main(int argc, char** argv) { Require(Observe([&] { return state.destructors.load() == 1; }), "accept did not finish"); void* result = nullptr; Require(scePthreadJoin(thread, &result) == 0, "join"); - Require(result == ((cancel || (state.handoff && !state.jump && !state.handoffPending)) ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "thread result"); + Require(result == ((cancel || (state.handoff && !state.installFault && !state.jump && !state.handoffPending)) ? GUEST_CANCELED : reinterpret_cast(std::uintptr_t{7})), "thread result"); Require(state.cleanups.load() == 1 && state.destructors.load() == 1, "cleanup counts"); if (scenario == "nonblocking") Require(state.accepted == -1 && state.error == 35, "nonblocking errno"); if (scenario == "complete" || state.flags) { @@ -174,6 +215,7 @@ int main(int argc, char** argv) { Require(state.acceptedDescriptorFlags == 0, "accepted close-on-exec inherited"); Require((state.acceptedFlags & 4) == (state.flags ? 4 : 0), "accepted nonblocking flags"); } + if (state.installFault) Require(state.accepted == -1 && state.error == (state.installFault == 1 ? 24 : 12), "accepted installation failure errno"); if (state.handoff) { Require(state.observed.load() && (state.handoffPending ? state.accepted >= 0 : state.accepted == -1), "accepted ownership handoff not reached"); if (state.handoffReuse) Require(fcntl_nid_postfix(state.listener, 3) >= 0, "disposal closed replacement"); From f5d67c6ed8ddda3d6563f28d2c81a3c9529f702e Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 20:35:05 -0300 Subject: [PATCH 49/61] fix(libkernel): retain cancellation for main descriptor-passing sends --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index a62b2e8e12a..b3b79cd4a14 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -2050,7 +2050,7 @@ std::int64_t SendPassing(Socket& socket, const GuestMsghdr& message, std::vector header->cmsg_len = CMSG_LEN(descriptors.size() * sizeof(int)); std::memcpy(CMSG_DATA(header), descriptors.data(), descriptors.size() * sizeof(int)); } - const auto result = ::sendmsg(socket.value, &host, SocketSendFlags(socket, flags)); + const auto result = ThreadCancel::Native([&] { return ::sendmsg(socket.value, &host, SocketSendFlags(socket, flags)); }); return result < 0 ? Fail(SendError(socket.type)) : result; } int GuestFromHostSocket(int domain) { From 17f4e57432be1ee4521a9b5d5225e1607756fa96 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 20:35:59 -0300 Subject: [PATCH 50/61] test(libkernel): count Linux native descriptors through procfs --- core/libs/tests/GuestAcceptCancel.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestAcceptCancel.cpp b/core/libs/tests/GuestAcceptCancel.cpp index 3044afed2e4..ea3d9aa5de4 100644 --- a/core/libs/tests/GuestAcceptCancel.cpp +++ b/core/libs/tests/GuestAcceptCancel.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef _WIN32 #include #else @@ -39,6 +40,13 @@ static std::size_t NativeDescriptorCount() { DWORD count = 0; Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0, "count native handles"); return count; +#elif defined(__linux__) + std::size_t count = 0; + for (const auto& entry : std::filesystem::directory_iterator("/proc/self/fd")) { + static_cast(entry); + ++count; + } + return count; #else std::size_t count = 0; const auto limit = ::sysconf(_SC_OPEN_MAX); From 5d01e01118503aa0a96d47f205d2e843498e0597 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 20:38:28 -0300 Subject: [PATCH 51/61] test(libkernel): drain the interrupted native-entry control before release --- core/libs/tests/GuestIoDescriptorAccess.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/libs/tests/GuestIoDescriptorAccess.cpp b/core/libs/tests/GuestIoDescriptorAccess.cpp index 343ac536538..54db4159cc9 100644 --- a/core/libs/tests/GuestIoDescriptorAccess.cpp +++ b/core/libs/tests/GuestIoDescriptorAccess.cpp @@ -31,6 +31,8 @@ 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); } @@ -751,10 +753,28 @@ static void CloseBeforeNative() { 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); From d682ea71365098a3a0410cc544b1337ab8064b1b Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 20:46:23 -0300 Subject: [PATCH 52/61] test(libkernel): wire reservation failure before accepted socket installation --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 879391cf00e..af7362777d0 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -412,6 +412,7 @@ void ReleaseNumber(int number) { } #endif int Install(std::shared_ptr socket) { + if (acceptInstallFault == 1) { acceptInstallFault = 0; return Fail(24); } #ifdef _WIN32 unsigned long nonblocking = 1; if (ioctlsocket(socket->value, FIONBIO, &nonblocking)) return Fail(NativeError()); From 0376e15be13cdd338bdf9b6662dbcf5454581a28 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 21:10:42 -0300 Subject: [PATCH 53/61] fix(libkernel): retain datagram source-fault error precedence --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- core/libs/tests/GuestUdp.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 226f67288d2..702c62d52a1 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1883,7 +1883,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st } else { sockaddr_storage native{}; socklen_t size; - if (!Address(socket->family, address, addressLength, native, size)) return -1; + if (!Address(socket->family, address, addressLength, native, size)) return socket->type == 2 && SourceFaults(buffer, length) ? Fail(14) : -1; if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return socket->type == 2 && SourceFaults(buffer, length) ? Fail(14) : answered; #ifdef _WIN32 diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 06dd3392135..a1d26f0e3d3 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -18,7 +19,12 @@ #include #endif -static void Require(bool value) { if (!value) std::abort(); } +static void Check(bool value, int line) { + if (value) return; + std::fprintf(stderr, "UDP check failed at line %d, guest errno %d\n", line, *__error_nid_postfix()); + std::abort(); +} +#define Require(value) Check((value), __LINE__) static void CheckUnreachablePortIgnored() { const int closing = socket_nid_postfix(2, 2, 0); From 258548aec44d0e9c6fd525041ad3f8f8d5ecaa35 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Mon, 2 Nov 2026 13:53:05 -0300 Subject: [PATCH 54/61] test(libkernel): warm transfer waker before accept leak snapshots --- core/libs/tests/GuestAcceptCancel.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestAcceptCancel.cpp b/core/libs/tests/GuestAcceptCancel.cpp index ea3d9aa5de4..f6da66a8057 100644 --- a/core/libs/tests/GuestAcceptCancel.cpp +++ b/core/libs/tests/GuestAcceptCancel.cpp @@ -57,6 +57,25 @@ static std::size_t NativeDescriptorCount() { #endif } +static void CheckNativeDescriptors(std::size_t expected, const char* phase) { + const auto actual = NativeDescriptorCount(); + if (actual == expected) return; + std::fprintf(stderr, "%s: native descriptors expected %zu, observed %zu\n", phase, expected, actual); + std::exit(1); +} + +static void WarmTransferWait() { + const int socket = socket_nid_postfix(2, 2, 0); + Require(socket >= 0, "warmup socket"); + const std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(socket, address.data(), address.size()) == 0, "bind warmup socket"); + const std::array timeout{0, 10000}; + Require(setsockopt_nid_postfix(socket, 0xffff, 0x1006, timeout.data(), sizeof(timeout)) == 0, "warmup timeout"); + char byte = 0; + Require(recv_nid_postfix(socket, &byte, 1, 0) == -1 && *__error_nid_postfix() == 35, "warmup transfer wait"); + Require(close_nid_postfix(socket) == 0, "close warmup socket"); +} + struct State { int listener = -1; int accepted = -1; @@ -88,14 +107,14 @@ struct State { static void APS5_VABI Cleanup(void* opaque) { auto& state = *static_cast(opaque); - if (state.countDisposal) Require(NativeDescriptorCount() == state.nativeBefore, "accepted native resource survived until cleanup"); + if (state.countDisposal) CheckNativeDescriptors(state.nativeBefore, "cleanup"); Require(state.cleanups.fetch_add(1) == 0 && state.destructors.load() == 0, "cleanup ordering/count"); if (state.accepted >= 0) Require(close_nid_postfix(state.accepted) == 0, "close accepted socket"); } static void APS5_VABI Destroy(void* opaque) { auto& state = *static_cast(opaque); - if (state.countDisposal) Require(NativeDescriptorCount() == state.nativeBefore, "accepted native resource survived until TSD"); + if (state.countDisposal) CheckNativeDescriptors(state.nativeBefore, "TSD"); Require(state.cleanups.load() == 1 && state.destructors.fetch_add(1) == 0, "TSD ordering/count"); } @@ -141,6 +160,7 @@ static void* APS5_VABI Acceptor(void* opaque) { ThreadCancel::IoRestart warmup(false); ThreadCancel::IoPoint point(warmup); } + WarmTransferWait(); state.nativeBefore = NativeDescriptorCount(); } state.ready.store(true); From db093790df20c82996d9a7b75b43daa4e8764c6e Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Mon, 2 Nov 2026 15:16:25 -0300 Subject: [PATCH 55/61] test(libkernel): measure accept disposal after connection setup --- core/libs/tests/GuestAcceptCancel.cpp | 61 +++++++++++++++++---------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/core/libs/tests/GuestAcceptCancel.cpp b/core/libs/tests/GuestAcceptCancel.cpp index f6da66a8057..11bb4fe8ae2 100644 --- a/core/libs/tests/GuestAcceptCancel.cpp +++ b/core/libs/tests/GuestAcceptCancel.cpp @@ -64,24 +64,31 @@ static void CheckNativeDescriptors(std::size_t expected, const char* phase) { std::exit(1); } -static void WarmTransferWait() { - const int socket = socket_nid_postfix(2, 2, 0); - Require(socket >= 0, "warmup socket"); - const std::array address{16, 2, 0, 0, 127, 0, 0, 1}; - Require(bind_nid_postfix(socket, address.data(), address.size()) == 0, "bind warmup socket"); - const std::array timeout{0, 10000}; - Require(setsockopt_nid_postfix(socket, 0xffff, 0x1006, timeout.data(), sizeof(timeout)) == 0, "warmup timeout"); - char byte = 0; - Require(recv_nid_postfix(socket, &byte, 1, 0) == -1 && *__error_nid_postfix() == 35, "warmup transfer wait"); - Require(close_nid_postfix(socket) == 0, "close warmup socket"); -} +struct ControllerResource { +#ifdef _WIN32 + HANDLE value = nullptr; + void Open() { + value = CreateEventW(nullptr, FALSE, FALSE, nullptr); + Require(value != nullptr, "create late controller resource"); + } + ~ControllerResource() { if (value) Require(CloseHandle(value) != 0, "close controller resource"); } +#else + int descriptors[2]{-1, -1}; + void Open() { Require(::pipe(descriptors) == 0, "create late controller resources"); } + ~ControllerResource() { + for (int descriptor : descriptors) + if (descriptor >= 0) Require(::close(descriptor) == 0, "close controller resource"); + } +#endif +}; struct State { int listener = -1; int accepted = -1; int error = 0; int installFault = 0; - std::size_t nativeBefore = 0; + std::size_t nativeExpected = 0; + bool counted = false; bool countDisposal = false; PthreadKey key = -1; bool disabled = false; @@ -99,6 +106,7 @@ struct State { int acceptedDescriptorFlags = -1; std::atomic observed{false}; std::atomic ready{false}; + std::atomic connectionReady{false}; std::atomic release{false}; std::atomic returned{false}; std::atomic cleanups{0}; @@ -107,14 +115,20 @@ struct State { static void APS5_VABI Cleanup(void* opaque) { auto& state = *static_cast(opaque); - if (state.countDisposal) CheckNativeDescriptors(state.nativeBefore, "cleanup"); + if (state.countDisposal) { + Require(state.counted, "missing accepted resource snapshot"); + CheckNativeDescriptors(state.nativeExpected, "cleanup"); + } Require(state.cleanups.fetch_add(1) == 0 && state.destructors.load() == 0, "cleanup ordering/count"); if (state.accepted >= 0) Require(close_nid_postfix(state.accepted) == 0, "close accepted socket"); } static void APS5_VABI Destroy(void* opaque) { auto& state = *static_cast(opaque); - if (state.countDisposal) CheckNativeDescriptors(state.nativeBefore, "TSD"); + if (state.countDisposal) { + Require(state.counted, "missing accepted resource snapshot at TSD"); + CheckNativeDescriptors(state.nativeExpected, "TSD"); + } Require(state.cleanups.load() == 1 && state.destructors.fetch_add(1) == 0, "TSD ordering/count"); } @@ -139,6 +153,13 @@ static void LeaveHandoff() { static void ObserveHandoff() { accepting->observed.store(true); + if (accepting->countDisposal) { + Require(Observe([] { return accepting->connectionReady.load(); }), "controller connection completion"); + const auto native = NativeDescriptorCount(); + Require(native != 0, "missing accepted native resource"); + accepting->nativeExpected = native - 1; + accepting->counted = true; + } if (accepting->installFault) { GuestSocketAcceptInstallFaultForTest_nid_no_patch(accepting->installFault); return; @@ -154,15 +175,6 @@ static void* APS5_VABI Acceptor(void* opaque) { __pthread_cleanup_push_imp_nid_postfix(Cleanup, &state, &cleanup); accepting = &state; if (state.handoff) GuestSocketAcceptObserverForTest_nid_no_patch(ObserveHandoff); - if (state.countDisposal) { - GuestSockets::CurrentWaker(); - { - ThreadCancel::IoRestart warmup(false); - ThreadCancel::IoPoint point(warmup); - } - WarmTransferWait(); - state.nativeBefore = NativeDescriptorCount(); - } state.ready.store(true); if (state.pending) Require(Observe([&] { return state.release.load(); }), "pending release"); if (!state.jump || setjmp_nid_postfix(state.jumpBuffer.data()) == 0) @@ -215,6 +227,8 @@ int main(int argc, char** argv) { Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Acceptor, &state, nullptr) == 0, "create acceptor"); Require(Observe([&] { return state.ready.load(); }), "acceptor admission"); + ControllerResource controllerResource; + if (state.countDisposal) controllerResource.Open(); if (!state.pending && !state.disabled && scenario != "nonblocking" && !state.flags) { Require(Observe([&] { std::lock_guard lock(thread->cancelLock); @@ -225,6 +239,7 @@ int main(int argc, char** argv) { if (cancel) Require(pthread_cancel_nid_postfix(thread) == 0, "cancel"); state.release.store(true); if (scenario == "complete" || state.disabled || state.handoff) Require(connect_nid_postfix(client, address.data(), address.size()) == 0, "complete accept"); + state.connectionReady.store(true); int replacement[2]{-1, -1}; if (scenario == "reuse") Require(pipe_nid_postfix(replacement) == 0, "replacement pipe"); if (scenario == "close" || scenario == "reuse") Require(close_nid_postfix(state.listener) == 0, "close waiting listener"); From 51a30734ed3738e2e394e0619b8f66f83db0f981 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Mon, 2 Nov 2026 20:33:21 -0300 Subject: [PATCH 56/61] test(libkernel): exercise cancellation through the sys pwrite provider --- core/libs/prx/libkernel/CMakeLists.txt | 14 +++++++------- core/libs/tests/GuestIoSharedWriteCancel.cpp | 2 ++ core/libs/tests/GuestIoWriteCancel.cpp | 2 ++ core/libs/tests/GuestIoWriteSyncCancel.cpp | 2 ++ 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a7236af442b..c2c0fd33f2e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -298,9 +298,9 @@ if(BUILD_TESTING) endforeach() endif() target_include_directories(guest_io_write_sync_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_io_write_sync_cancel_tests PRIVATE libkernel libc) + target_link_libraries(guest_io_write_sync_cancel_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_io_write_sync_cancel_tests) - foreach(syncWriteApi write writev pwrite pwritev) + foreach(syncWriteApi write writev pwrite pwrite_sys pwritev) foreach(syncWriteCase cancel disabled complete zero error close signal metadata_cancel metadata_disabled metadata_complete metadata_close metadata_signal) set(syncWriteTest guest_io_write_sync_${syncWriteApi}__${syncWriteCase}) add_test(NAME ${syncWriteTest} COMMAND guest_io_write_sync_cancel_tests ${syncWriteApi} ${syncWriteCase}) @@ -311,7 +311,7 @@ if(BUILD_TESTING) endforeach() endforeach() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - foreach(preflightApi write writev pwrite pwritev) + foreach(preflightApi write writev pwrite pwrite_sys pwritev) foreach(preflightPhase 1 2) foreach(preflightCase cancel disabled complete close signal) set(preflightTest guest_io_write_preflight_${preflightApi}__${preflightPhase}_${preflightCase}) @@ -331,7 +331,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_io_write_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoWriteCancel.cpp) target_include_directories(guest_io_write_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_io_write_cancel_tests PRIVATE libkernel libc) + target_link_libraries(guest_io_write_cancel_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_io_write_cancel_tests) foreach(writeTransport pipe loopback regular datagram) set(writeApis write alias writev writev_alias) @@ -345,7 +345,7 @@ if(BUILD_TESTING) list(APPEND writeApis send sendto sendto_alias sendmsg sendmsg_alias) set(writeScenarios complete zero pending pending_zero) elseif(writeTransport STREQUAL "regular") - list(APPEND writeApis pwrite pwritev) + list(APPEND writeApis pwrite pwrite_sys pwritev) set(writeScenarios complete zero pending access append) if(WIN32) list(APPEND writeScenarios position) @@ -379,9 +379,9 @@ if(BUILD_TESTING) if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(guest_io_shared_write_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoSharedWriteCancel.cpp) target_include_directories(guest_io_shared_write_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) - target_link_libraries(guest_io_shared_write_cancel_tests PRIVATE libkernel libc) + target_link_libraries(guest_io_shared_write_cancel_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_io_shared_write_cancel_tests) - foreach(sharedWriteApi write alias writev writev_alias pwrite pwritev) + foreach(sharedWriteApi write alias writev writev_alias pwrite pwrite_sys pwritev) foreach(sharedWriteScenario cancel complete disabled close disabled_close) set(sharedWriteTest guest_io_write_shared_${sharedWriteApi}__${sharedWriteScenario}) add_test(NAME ${sharedWriteTest} COMMAND guest_io_shared_write_cancel_tests ${sharedWriteApi} ${sharedWriteScenario}) diff --git a/core/libs/tests/GuestIoSharedWriteCancel.cpp b/core/libs/tests/GuestIoSharedWriteCancel.cpp index 9927336469a..3027b620e48 100644 --- a/core/libs/tests/GuestIoSharedWriteCancel.cpp +++ b/core/libs/tests/GuestIoSharedWriteCancel.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel_sys/include/Api.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/Api.hpp" @@ -107,6 +108,7 @@ static void* APS5_VABI Writer(void* opaque) { if (state.api == "write") state.result = write_nid_postfix(state.descriptor, "ww", 2); else if (state.api == "alias") state.result = _write_nid_postfix(state.descriptor, "ww", 2); else if (state.api == "pwrite") state.result = pwrite_nid_disambig1_nid_postfix(state.descriptor, "ww", 2, 0); + else if (state.api == "pwrite_sys") state.result = pwrite_nid_postfix(state.descriptor, "ww", 2, 0); else if (state.api == "writev") state.result = writev_nid_postfix(state.descriptor, vectors, 2); else if (state.api == "writev_alias") state.result = _writev_nid_postfix(state.descriptor, vectors, 2); else if (state.api == "pwritev") state.result = pwritev_nid_postfix(state.descriptor, vectors, 2, 0); diff --git a/core/libs/tests/GuestIoWriteCancel.cpp b/core/libs/tests/GuestIoWriteCancel.cpp index 49e5ad3e62f..94a5318ea9a 100644 --- a/core/libs/tests/GuestIoWriteCancel.cpp +++ b/core/libs/tests/GuestIoWriteCancel.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel_sys/include/Api.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" @@ -95,6 +96,7 @@ static void* APS5_VABI Writer(void* opaque) { else if (state.api == "writev") state.result = writev_nid_postfix(state.output, vectors, 2); else if (state.api == "writev_alias") state.result = _writev_nid_postfix(state.output, vectors, 2); else if (state.api == "pwrite") state.result = pwrite_nid_disambig1_nid_postfix(state.output, state.bytes.data(), size, 1); + else if (state.api == "pwrite_sys") state.result = pwrite_nid_postfix(state.output, state.bytes.data(), size, 1); else if (state.api == "pwritev") state.result = pwritev_nid_postfix(state.output, vectors, 2, 1); else if (state.api == "send") state.result = send_nid_postfix(state.output, state.bytes.data(), size, flags); else if (state.api == "sendto") state.result = sendto_nid_postfix(state.output, state.bytes.data(), size, flags, nullptr, 0); diff --git a/core/libs/tests/GuestIoWriteSyncCancel.cpp b/core/libs/tests/GuestIoWriteSyncCancel.cpp index e997bdf3e92..9a2e93ae324 100644 --- a/core/libs/tests/GuestIoWriteSyncCancel.cpp +++ b/core/libs/tests/GuestIoWriteSyncCancel.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel_sys/include/Api.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" @@ -111,6 +112,7 @@ static std::int64_t Write(std::size_t size) { if (state->api == "write") return write_nid_postfix(state->file, bytes, size); if (state->api == "writev") return writev_nid_postfix(state->file, vectors, 2); if (state->api == "pwrite") return pwrite_nid_disambig1_nid_postfix(state->file, bytes, size, 0); + if (state->api == "pwrite_sys") return pwrite_nid_postfix(state->file, bytes, size, 0); if (state->api == "pwritev") return pwritev_nid_postfix(state->file, vectors, 2, 0); return sceKernelWrite(state->file, bytes, size); } From 32169af1dc395b182370cdf973687d313d33bfc4 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 3 Nov 2026 13:03:35 -0300 Subject: [PATCH 57/61] docs(libkernel): keep composed context debt concise --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e1d03b51e44..a38680635fa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -219,7 +219,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`). The in-process opaque machine context uses 16 bytes of `mc_spare` for a context identity and saved stack pointer; copies retain it and a manually retargeted stack replaces it. This metadata has no console evidence. +- [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 - [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 From 085458a70c43b3a5c8b03f86f4faa2b975f107e0 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 3 Nov 2026 14:51:23 -0300 Subject: [PATCH 58/61] fix(libkernel): retry partial-buffer socket probes within transport waits --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 26 ++++---- .../tests/GuestSocketTransferProgress.cpp | 27 +++++++- core/libs/tests/GuestUdp.cpp | 63 +++++++++++++++++++ docs/dev/debt/libSceSsl.md | 2 + docs/dev/debt/libkernel.md | 6 +- 5 files changed, 109 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 0c5f07047ac..16e2ae8b87f 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -165,8 +165,7 @@ int Transfer(Socket& socket, bool send, int flags, int length, TOperation operat timeout = timed ? (send ? socket.options.sendTimeout : socket.options.receiveTimeout) : 0; } if (!send && socket.type == 1 && !nonblocking && (flags & (GuestWaitAll | GuestPeek)) == (GuestWaitAll | GuestPeek)) { - WSASetLastError(WSAEOPNOTSUPP); - return -1; + NotImplemented_nid_no_patch("recv: MSG_PEEK | MSG_WAITALL on a blocking stream socket"); } const bool complete = socket.type == 1 && !nonblocking && (send || ((flags & GuestWaitAll) != 0 && (flags & GuestPeek) == 0)); @@ -1071,14 +1070,24 @@ 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) { +int ReceiveNative(Socket& socket, void* buffer, int length, int flags, sockaddr* address, socklen_t* addressLength, bool partialBuffer = false) { #ifdef _WIN32 return Transfer(socket, false, flags, length, [&](int offset) { + if (partialBuffer && offset == 0) { + int error = 0; + if (!PendingFits(socket, buffer, length, flags, error)) { + DropDatagram(socket, flags); + WSASetLastError(WSAEFAULT); + return -1; + } + if (error != 0) return -1; + } auto* destination = offset == 0 ? static_cast(buffer) : static_cast(buffer) + offset; return address ? ::recvfrom(socket.value, destination, length - offset, NativeReceiveFlags(flags, socket.type), address, addressLength) : ::recv(socket.value, destination, length - offset, NativeReceiveFlags(flags, socket.type)); }); #else + static_cast(partialBuffer); 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))); }); @@ -1998,16 +2007,9 @@ static std::int64_t ReceiveFrom(int descriptor, void* buffer, std::uint64_t leng if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); std::uint64_t limit = length; #ifdef _WIN32 - if (length != 0 && (limit = GuestPrefix(buffer, length, WritableProtection)) < length) { - int error = 0; - if (!PendingFits(*socket, buffer, limit, flags, error)) { - DropDatagram(*socket, flags); - return Fail(14); - } - if (error) return Fail(error); - } + if (length != 0) limit = GuestPrefix(buffer, length, WritableProtection); #endif - auto result = ReceiveNative(*socket, buffer, static_cast(limit), flags, reinterpret_cast(&native), &size); + auto result = ReceiveNative(*socket, buffer, static_cast(limit), flags, reinterpret_cast(&native), &size, limit < length); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(limit); diff --git a/core/libs/tests/GuestSocketTransferProgress.cpp b/core/libs/tests/GuestSocketTransferProgress.cpp index f6e38b27360..ad05c90893f 100644 --- a/core/libs/tests/GuestSocketTransferProgress.cpp +++ b/core/libs/tests/GuestSocketTransferProgress.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -171,6 +172,28 @@ void ReceiveNoProgress() { Require(std::all_of(call.bytes.begin(), call.bytes.end(), [](auto value) { return value == 0xe7; }), "empty WAITALL modified its buffer"); } +void ReceivePeekWaitAll() { + auto pair = TcpPair(); + SetTimeout(pair->Receiver(), false, 300ms); + std::array buffer{}; + for (const bool from : {false, true}) { + const auto calls = NotImplementedCalls_nid_no_patch(); + bool threw = false; + try { + if (from) recvfrom_nid_postfix(pair->Receiver(), buffer.data(), buffer.size(), WaitAll | 2, nullptr, nullptr); + else recv_nid_postfix(pair->Receiver(), buffer.data(), buffer.size(), WaitAll | 2); + } catch (const std::runtime_error& error) { + threw = std::strstr(error.what(), "MSG_PEEK | MSG_WAITALL on a blocking stream socket") != nullptr; + } + Require(threw && NotImplementedCalls_nid_no_patch() == calls + 1, "blocking PEEK/WAITALL did not report its unimplemented contract"); + } + Require(recv_nid_postfix(pair->Receiver(), buffer.data(), buffer.size(), WaitAll | 2 | DontWait) == -1 && + *__error_nid_postfix() == WouldBlock, "DONTWAIT PEEK/WAITALL did not preserve EAGAIN"); + Require(fcntl_nid_postfix(pair->Receiver(), 4, std::uint64_t{4}) == 0, "cannot make the peek receiver nonblocking"); + Require(recv_nid_postfix(pair->Receiver(), buffer.data(), buffer.size(), WaitAll | 2) == -1 && + *__error_nid_postfix() == WouldBlock, "nonblocking PEEK/WAITALL did not preserve EAGAIN"); +} + void ReceiveProgress(bool complete) { auto pair = TcpPair(); SetTimeout(pair->Receiver(), false, 300ms); @@ -269,8 +292,10 @@ int main(int argc, char** argv) { if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "receive-empty") == 0) ReceiveNoProgress(); if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "receive-partial") == 0) ReceiveProgress(false); if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "receive-complete") == 0) ReceiveProgress(true); + if (std::strcmp(mode, "all") == 0 || std::strcmp(mode, "receive-peek-waitall") == 0) ReceivePeekWaitAll(); Require(std::strcmp(mode, "all") == 0 || std::strcmp(mode, "send") == 0 || std::strcmp(mode, "receive-empty") == 0 || - std::strcmp(mode, "receive-partial") == 0 || std::strcmp(mode, "receive-complete") == 0, "unknown transfer progress mode"); + std::strcmp(mode, "receive-partial") == 0 || std::strcmp(mode, "receive-complete") == 0 || + std::strcmp(mode, "receive-peek-waitall") == 0, "unknown transfer progress mode"); std::puts("socket transfer progress preserves payloads and inactivity timeouts"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index a1d26f0e3d3..4c9ab428a1d 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -387,6 +388,67 @@ class GuardedPage { char* memory = nullptr; }; +static void CheckDelayedPartialReceive() { + using namespace std::chrono_literals; + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t addressSize = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &addressSize) == 0); + GuardedPage page; + const char payload[] = "abcdefgh"; + for (const bool timed : {false, true}) { + const std::array timeout{timed ? 2 : 0, 0}; + Require(setsockopt_nid_postfix(receiver, 0xffff, 0x1006, timeout.data(), sizeof(timeout)) == 0); + for (const int flags : {0, 2}) { + for (const std::size_t bytes : {3u, 8u}) { + std::memset(page.Crossing(), '?', 4); + std::atomic entered{false}, finished{false}; + std::int64_t result = -2; + int error = -1; + std::thread call([&] { + entered.store(true, std::memory_order_release); + result = recvfrom_nid_postfix(receiver, page.Crossing(), 8, flags, nullptr, nullptr); + error = *__error_nid_postfix(); + finished.store(true, std::memory_order_release); + }); + const auto deadline = std::chrono::steady_clock::now() + 2s; + while (!entered.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) + std::this_thread::yield(); + Require(entered.load(std::memory_order_acquire)); + std::this_thread::sleep_for(100ms); + Require(!finished.load(std::memory_order_acquire)); + Require(sendto_nid_postfix(sender, payload, bytes, 0, address.data(), address.size()) == static_cast(bytes)); + call.join(); + if (bytes <= 4) { + Require(result == static_cast(bytes)); + Require(std::memcmp(page.Crossing(), payload, bytes) == 0 && page.Crossing()[3] == '?'); + } else Require(result == -1 && error == 14); + char remaining[8]{}; + if (flags == 2) { + Require(recvfrom_nid_postfix(receiver, remaining, sizeof(remaining), 0x80, nullptr, nullptr) == static_cast(bytes)); + Require(std::memcmp(remaining, payload, bytes) == 0); + } + Require(recvfrom_nid_postfix(receiver, remaining, sizeof(remaining), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + } + } + } + const std::array timeout{0, 300000}; + Require(setsockopt_nid_postfix(receiver, 0xffff, 0x1006, timeout.data(), sizeof(timeout)) == 0); + auto start = std::chrono::steady_clock::now(); + Require(recvfrom_nid_postfix(receiver, page.Crossing(), 8, 0, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(std::chrono::steady_clock::now() - start >= 250ms); + for (const bool nonblocking : {false, true}) { + Require(fcntl_nid_postfix(receiver, 4, std::uint64_t{nonblocking ? 4u : 0u}) == 0); + start = std::chrono::steady_clock::now(); + Require(recvfrom_nid_postfix(receiver, page.Crossing(), 8, nonblocking ? 0 : 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(std::chrono::steady_clock::now() - start < 250ms); + } + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); +} + static void CheckPointerFaults() { constexpr int Fault = 14; GuardedPage page; @@ -497,6 +559,7 @@ static void CheckPointerFaults() { } int main() { + CheckDelayedPartialReceive(); CheckPointerFaults(); CheckAddressLengths(2); CheckAddressLengths(28); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 5a7f86a3bfa..6623c7ffa4f 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -19,3 +19,5 @@ ### Functional - [System roots for HTTP](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - `SslTrustSystemRoots_nid_no_patch` makes one connection also trust the system CA list, read once per process from `//common/cert/CA_LIST.cer`. It is parsed into a separate allocation pool, not charged to the title's `sceSslInit` pool, and linked after the context's own authorities; `sceSslGetCaList` and `sceSslGetCaCerts` do not report it. A missing, empty or certificate-free list throws and names the path instead of trusting nothing. Only the HTTP libraries call it: whether a bare libSceSsl connection on the console trusts the system roots is not measured, so `sceSslCreateConnection` alone keeps trusting only the context's authorities + +- [Retained libkernel transport waits](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libSceSsl) on Unix use a bare host `poll` outside `ThreadCancel::Native`. A blocking SSL read or write without a socket timeout can retain its socket and wait indefinitely after another thread closes the guest descriptor or requests cancellation; peer progress or shutdown is needed to wake it. The transport wait is not a guest cancellation point. This limitation is not console-verified. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f00be127398..f6da20b3f09 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -275,8 +275,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) has no console verification of the Windows transport emulation. Exposed Windows native sockets stay nonblocking; guest blocking mode and timeouts are implemented by directional readiness and retries. Unix uses native per-call flags. Windows blocking `MSG_PEEK | MSG_WAITALL` remains unsupported with `EOPNOTSUPP`; this combination needs a separate buffering contract. -- [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) has no console measurement of stream completion or timeout behavior. Windows completes stream receives through readiness retries and restarts the `SO_RCVTIMEO` inactivity deadline after positive progress; Unix uses native flags. Datagram sockets ignore `MSG_WAITALL`. Windows blocking `MSG_PEEK | MSG_WAITALL` remains unsupported (`EOPNOTSUPP`); the other platform combinations follow host rules. +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) has no console verification of the Windows transport emulation. Exposed Windows native sockets stay nonblocking; guest blocking mode and timeouts are implemented by directional readiness and retries. Unix uses native per-call flags. Windows blocking stream `MSG_PEEK | MSG_WAITALL` throws through `NotImplemented_nid_no_patch`; this combination needs a separate buffering contract. +- [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) has no console measurement of stream completion or timeout behavior. Windows completes stream receives through readiness retries and restarts the `SO_RCVTIMEO` inactivity deadline after positive progress; Unix uses native flags. Datagram sockets ignore `MSG_WAITALL`. Windows blocking stream `MSG_PEEK | MSG_WAITALL` throws through `NotImplemented_nid_no_patch`; nonblocking combinations and other platforms follow host rules. - [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. 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. @@ -381,3 +381,5 @@ - [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. + +- [Retained native socket transport](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) used by libSceSsl holds the socket owner across guest descriptor close. On Unix its readiness wait is a bare host `poll`, outside `ThreadCancel::Native`; a blocking SSL transfer without a socket timeout can wait indefinitely after guest close or cancellation unless peer progress or shutdown wakes it. It is not a guest cancellation point. Console behavior is not verified. From 8957ad0b52a6400270f0129e09d2deb7f1a5503e Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 3 Nov 2026 14:55:47 -0300 Subject: [PATCH 59/61] docs(libSceSsl): locate retained transport debt with its owning library --- docs/dev/debt/libSceSsl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 6623c7ffa4f..7b455a28403 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -20,4 +20,4 @@ ### Functional - [System roots for HTTP](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - `SslTrustSystemRoots_nid_no_patch` makes one connection also trust the system CA list, read once per process from `//common/cert/CA_LIST.cer`. It is parsed into a separate allocation pool, not charged to the title's `sceSslInit` pool, and linked after the context's own authorities; `sceSslGetCaList` and `sceSslGetCaCerts` do not report it. A missing, empty or certificate-free list throws and names the path instead of trusting nothing. Only the HTTP libraries call it: whether a bare libSceSsl connection on the console trusts the system roots is not measured, so `sceSslCreateConnection` alone keeps trusting only the context's authorities -- [Retained libkernel transport waits](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libSceSsl) on Unix use a bare host `poll` outside `ThreadCancel::Native`. A blocking SSL read or write without a socket timeout can retain its socket and wait indefinitely after another thread closes the guest descriptor or requests cancellation; peer progress or shutdown is needed to wake it. The transport wait is not a guest cancellation point. This limitation is not console-verified. +- [Retained libkernel transport waits](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) on Unix use a bare host `poll` outside `ThreadCancel::Native`. A blocking SSL read or write without a socket timeout can retain its socket and wait indefinitely after another thread closes the guest descriptor or requests cancellation; peer progress or shutdown is needed to wake it. The transport wait is not a guest cancellation point. This limitation is not console-verified. From 4f897946d28deaa0197e55e5422cd932e9c765d6 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 3 Nov 2026 15:36:28 -0300 Subject: [PATCH 60/61] docs(libSceSsl): keep retained transport debt in its lists --- docs/dev/debt/libSceSsl.md | 3 +-- docs/dev/debt/libkernel.md | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 7b455a28403..c4ab7443529 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -19,5 +19,4 @@ ### Functional - [System roots for HTTP](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - `SslTrustSystemRoots_nid_no_patch` makes one connection also trust the system CA list, read once per process from `//common/cert/CA_LIST.cer`. It is parsed into a separate allocation pool, not charged to the title's `sceSslInit` pool, and linked after the context's own authorities; `sceSslGetCaList` and `sceSslGetCaCerts` do not report it. A missing, empty or certificate-free list throws and names the path instead of trusting nothing. Only the HTTP libraries call it: whether a bare libSceSsl connection on the console trusts the system roots is not measured, so `sceSslCreateConnection` alone keeps trusting only the context's authorities - -- [Retained libkernel transport waits](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) on Unix use a bare host `poll` outside `ThreadCancel::Native`. A blocking SSL read or write without a socket timeout can retain its socket and wait indefinitely after another thread closes the guest descriptor or requests cancellation; peer progress or shutdown is needed to wake it. The transport wait is not a guest cancellation point. This limitation is not console-verified. +- [Retained libkernel transport waits](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) use a retained socket owner; see [libkernel debt](libkernel.md) for the uncancellable Unix poll wait after guest close or cancellation. This limitation remains unverified on the console. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f6da20b3f09..9ee46162bed 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -381,5 +381,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. - - [Retained native socket transport](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) used by libSceSsl holds the socket owner across guest descriptor close. On Unix its readiness wait is a bare host `poll`, outside `ThreadCancel::Native`; a blocking SSL transfer without a socket timeout can wait indefinitely after guest close or cancellation unless peer progress or shutdown wakes it. It is not a guest cancellation point. Console behavior is not verified. From 70120f1b8604f9a5f4f96cb18fbf299204acdeec Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 3 Nov 2026 15:39:46 -0300 Subject: [PATCH 61/61] test(libkernel): expect unsupported blocking peek contract --- core/libs/tests/GuestSocketDontWait.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestSocketDontWait.cpp b/core/libs/tests/GuestSocketDontWait.cpp index 84a87c733eb..61f8821e4ed 100644 --- a/core/libs/tests/GuestSocketDontWait.cpp +++ b/core/libs/tests/GuestSocketDontWait.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include namespace { @@ -226,6 +228,19 @@ void OrdinaryDontWait(bool available) { Require(pair.Flags() == flags, "DONTWAIT changed the socket mode"); } +void UnsupportedPeekWaitAll(Pair& pair) { + std::array bytes{0xcc, 0xdd}; + const auto calls = NotImplementedCalls_nid_no_patch(); + bool threw = false; + try { + recv_nid_postfix(pair.Reader(), bytes.data(), bytes.size(), WaitAll | 2); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("MSG_PEEK | MSG_WAITALL on a blocking stream socket") != std::string::npos; + } + Require(threw && NotImplementedCalls_nid_no_patch() == calls + 1, "blocking Windows PEEK | WAITALL did not throw its unsupported contract"); + Require(bytes[0] == 0xcc && bytes[1] == 0xdd, "unsupported peek changed the receive buffer"); +} + void PeekWithWaitAll() { Pair pair; const auto flags = pair.Flags(); @@ -236,9 +251,7 @@ void PeekWithWaitAll() { Require(peek.result == 1 && peek.bytes[0] == FirstByte && peek.bytes[1] == 0xdd, "DONTWAIT peek did not return only the available byte"); pair.CheckReadable(true); #ifdef _WIN32 - Receive unsupported(pair, WaitAll | 2); - unsupported.Prompt("blocking PEEK | WAITALL did not preserve its unsupported result"); - Require(unsupported.result == -1 && unsupported.error == 45 && unsupported.bytes[0] == 0xcc && unsupported.bytes[1] == 0xdd, "blocking Windows PEEK | WAITALL did not report EOPNOTSUPP"); + UnsupportedPeekWaitAll(pair); pair.CheckReadable(true); #endif Receive consume(pair, DontWait);