diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index aa2b08ccd..bcf7a8ccf 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -3,6 +3,18 @@ target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) + target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_datagram_receive_tests) + foreach(api IN ITEMS recv recvfrom sceNetRecv sceNetRecvfrom) + add_test(NAME guest_datagram_${api} COMMAND guest_datagram_receive_tests ${api}) + set_tests_properties(guest_datagram_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_datagram_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0775ad06b..7e909a505 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -645,6 +645,9 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { } const int native_flags = flags == 2 ? MSG_PEEK : 0; const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); +#ifdef _WIN32 + if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); +#endif return result >= 0 ? result : fail(native_error()); } @@ -663,8 +666,11 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); const int native_flags = flags == 2 ? MSG_PEEK : 0; - const int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), + int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); +#ifdef _WIN32 + if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); +#endif if (result < 0) return fail(native_error()); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b5f852307..4d1866b4d 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -659,6 +659,9 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!buffer && length) return Fail(14); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0); +#ifdef _WIN32 + if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); +#endif return result < 0 ? Fail(NativeError()) : result; } int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { @@ -750,8 +753,11 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if ((!buffer && length) || (address && !addressLength)) return Fail(14); sockaddr_storage native{}; socklen_t size = sizeof(native); - const auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), + auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); +#ifdef _WIN32 + if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); +#endif if (result < 0) return Fail(NativeError()); if (address && socket->family == GuestUnix) { if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength); diff --git a/core/libs/tests/GuestDatagramReceive.cpp b/core/libs/tests/GuestDatagramReceive.cpp new file mode 100644 index 000000000..22cf4caaf --- /dev/null +++ b/core/libs/tests/GuestDatagramReceive.cpp @@ -0,0 +1,146 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +} + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +static void Check(bool net, bool from, int family) { + const auto create = [&] { return net ? sceNetSocket(nullptr, family, 2, 17) : socket_nid_postfix(family, 2, 17); }; + const int receiver = create(), sender = create(); + Require(receiver >= 0 && sender >= 0, "create UDP sockets"); + std::array destination{}; + const std::uint32_t addressSize = family == 2 ? 16 : 28; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const auto bind = [&](int descriptor) { + return net ? sceNetBind_nid_postfix(descriptor, destination.data(), addressSize) : + bind_nid_postfix(descriptor, destination.data(), addressSize); + }; + Require(bind(receiver) == 0 && bind(sender) == 0, "bind loopback sockets"); + auto senderAddress = destination; + std::uint32_t size = addressSize; + Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : + getsockname_nid_postfix(sender, senderAddress.data(), &size)) == 0, "get sender address"); + size = addressSize; + Require((net ? sceNetGetsockname(receiver, destination.data(), &size) : + getsockname_nid_postfix(receiver, destination.data(), &size)) == 0, "get receiver address"); + Require((net ? sceNetConnect(receiver, senderAddress.data(), addressSize) : + connect_nid_postfix(receiver, senderAddress.data(), addressSize)) == 0, "connect receiver"); + const int enabled = 1; + Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &enabled, sizeof(enabled)) : + fcntl_nid_postfix(receiver, 4, 6)) == 0, "make receiver nonblocking"); + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + const auto receive = [&](void* buffer, std::size_t length, int flags, void* address, std::uint32_t* addressLength) { + if (net) return from ? sceNetRecvfrom(receiver, buffer, length, flags, address, addressLength) : + sceNetRecv(receiver, buffer, length, flags); + return from ? recvfrom_nid_postfix(receiver, buffer, length, flags, address, addressLength) : + recv_nid_postfix(receiver, buffer, length, flags); + }; + const auto send = [&](const char* data, std::size_t length) { + Require((net ? sceNetSendto(sender, data, length, 0, destination.data(), addressSize) : + sendto_nid_postfix(sender, data, length, 0, destination.data(), addressSize)) == + static_cast(length), "send datagram"); + }; + const char message[] = "UDP header/body"; + for (const std::size_t capacity : {std::size_t{4}, sizeof(message), sizeof(message) + 4, std::size_t{0}}) { + send(message, sizeof(message)); + std::array buffer{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(buffer.data(), buffer.size(), 2, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == sizeof(message), "wait for complete datagram"); + for (const int flags : {2, 0}) { + buffer.fill('!'); + std::array address{}; + address.fill(0xa5); + size = address.size(); + *error = 123; + result = receive(buffer.data() + 1, capacity, flags, address.data(), &size); + const auto expected = std::min(capacity, sizeof(message)); + if (result != static_cast(expected)) { + std::fprintf(stderr, "%s%s IPv%d capacity=%zu flags=%d: expected %zu, got %lld errno=%d\n", + net ? "sceNetRecv" : "recv", from ? "from" : "", family == 2 ? 4 : 6, + capacity, flags, expected, static_cast(result), *error); + std::exit(1); + } + Require(*error == 123, "successful receive preserves errno"); + Require(buffer[0] == '!' && std::memcmp(buffer.data() + 1, message, expected) == 0 && + std::all_of(buffer.begin() + 1 + expected, buffer.end(), [](char value) { return value == '!'; }), + "copied prefix and buffer guards"); + if (from) { + Require(size == addressSize && std::memcmp(address.data(), senderAddress.data(), addressSize) == 0, + "source address on truncated receive"); + Require(std::all_of(address.begin() + addressSize, address.end(), [](unsigned char value) { return value == 0xa5; }), + "source address guards"); + } + if (flags == 2) { + Require(receive(buffer.data(), buffer.size(), 2, nullptr, nullptr) == sizeof(message) && + std::memcmp(buffer.data(), message, sizeof(message)) == 0, "short peek preserves complete packet"); + } + } + Require(receive(buffer.data(), buffer.size(), 0, nullptr, nullptr) < 0 && *error == 35, + "consuming receive discards remainder"); + } + send("", 0); + char byte = '!'; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(&byte, 1, 0, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == 0 && byte == '!', "empty datagram"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected receive API argument"); + const bool net = std::strncmp(argv[1], "sce", 3) == 0; + const bool from = std::strstr(argv[1], "from") != nullptr; + if (net) Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + Check(net, from, 2); + Check(net, from, 28); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..7b287afaa 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -554,6 +554,7 @@ Throughout the project, every function at every stage either **does exactly what - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. 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 a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `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 `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` +- [UDP recv/recvfrom](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [read, write and vector I/O](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [msync](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked