Merge pull request #3516 from Feuris/carry/windows-udp-short-receive
fix(net): preserve truncated UDP receives on Windows
This commit is contained in:
commit
9d9f2cf094
|
|
@ -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:$<TARGET_FILE_DIR:libSceNet>")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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<char*>(buf), static_cast<int>(len), native_flags);
|
||||
#ifdef _WIN32
|
||||
if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast<int64_t>(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<char*>(buf), static_cast<int>(len),
|
||||
int result = ::recvfrom(socket.native->value, static_cast<char*>(buf), static_cast<int>(len),
|
||||
native_flags, from ? reinterpret_cast<sockaddr*>(&peer) : nullptr, from ? &peer_length : nullptr);
|
||||
#ifdef _WIN32
|
||||
if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast<int>(len);
|
||||
#endif
|
||||
if (result < 0) return fail(native_error());
|
||||
if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT);
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -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<char*>(buffer), static_cast<int>(length),
|
||||
flags & 2 ? MSG_PEEK : 0);
|
||||
#ifdef _WIN32
|
||||
if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast<std::int64_t>(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<char*>(buffer), static_cast<int>(length),
|
||||
auto result = ::recvfrom(socket->value, static_cast<char*>(buffer), static_cast<int>(length),
|
||||
flags & 2 ? MSG_PEEK : 0, reinterpret_cast<sockaddr*>(&native), &size);
|
||||
#ifdef _WIN32
|
||||
if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast<int>(length);
|
||||
#endif
|
||||
if (result < 0) return Fail(NativeError());
|
||||
if (address && socket->family == GuestUnix) {
|
||||
if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
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<unsigned char, 28> destination{};
|
||||
const std::uint32_t addressSize = family == 2 ? 16 : 28;
|
||||
destination[0] = static_cast<unsigned char>(addressSize);
|
||||
destination[1] = static_cast<unsigned char>(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<std::int64_t>(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<char, 32> 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<unsigned char, 32> 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<std::int64_t>(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<long long>(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);
|
||||
}
|
||||
|
|
@ -556,6 +556,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
|
||||
|
|
|
|||
Loading…
Reference in New Issue