From 3e10d48548a285172ea7cb1f94c58783e1d97754 Mon Sep 17 00:00:00 2001 From: cirakg Date: Mon, 2 Nov 2026 23:40:30 +0100 Subject: network: fix room host LAN Play and room socket review issues The room host and room moderators are in the Moderator state, not Joined, so the airplane mode override for joined rooms did not apply to them and LAN Play still returned them to the menu. Accept both states. Share the room address override between the NIFM profile and IP config calls so they cannot drift apart. recv() on a room TCP socket now waited for the receive timeout, because Recv goes through RecvFrom. Room TCP sockets have no peer, so keep the previous immediate end-of-stream result when nothing is queued. Close() now drops queued packets and wakes a blocked recv, which then fails with EBADF. The socket state that the room thread reads when it queues a packet is now guarded by the packet mutex. Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/core/hle/service/nifm/nifm.cpp | 41 ++++++++++---------- src/core/internal_network/socket_proxy.cpp | 62 +++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 153b0f2193..e0a23f7a63 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -73,17 +73,17 @@ static u128 MakeUuidFromName(std::string_view name) { // A joined room is the emulated network, even in airplane mode. Its sockets only reach room // members, and LAN games (e.g. Mario Kart World LAN Play) need their connection request accepted. static bool IsNetworkAvailable() { - if (const auto room_member = Network::GetRoomMember().lock(); - room_member && room_member->GetState() == Network::RoomMember::State::Joined) { - return true; + if (const auto room_member = Network::GetRoomMember().lock()) { + // The room host and moderators are in the Moderator state instead of Joined + const auto state = room_member->GetState(); + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + return true; + } } return Network::GetHostIPv4Address().has_value() && !Settings::values.airplane_mode.GetValue(); } -// Room fake IPs are spread over 192.168.0.0/16, and some room servers give members different /24s. -// A host /24 mask would then put room members on different subnets, so LAN games ignore each other. -constexpr Network::IPv4Address RoomSubnetMask{255, 255, 0, 0}; - // This is nn::nifm::RequestState enum class RequestState : u32 { NotSubmitted = 1, @@ -124,6 +124,17 @@ struct IpAddressSetting { }; static_assert(sizeof(IpAddressSetting) == 0xD, "IpAddressSetting has incorrect size."); +// Room fake IPs are spread over 192.168.0.0/16, and some room servers give members different /24s. +// A host /24 mask would then put room members on different subnets, so LAN games ignore each other. +constexpr Network::IPv4Address RoomSubnetMask{255, 255, 0, 0}; + +static void ApplyRoomAddress(const Network::RoomMember& room_member, IpAddressSetting& setting) { + if (room_member.IsConnected()) { + setting.ip_address = room_member.GetFakeIpAddress(); + setting.subnet_mask = RoomSubnetMask; + } +} + // This is nn::nifm::DnsSetting struct DnsSetting { bool is_automatic{}; @@ -585,11 +596,7 @@ void IGeneralService::GetCurrentNetworkProfile(HLERequestContext& ctx) { std::strncpy(profile.network_name.data(), st.ssid, sizeof(profile.network_name) - 1); if (auto room_member = Network::GetRoomMember().lock()) { - if (room_member->IsConnected()) { - profile.ip_setting_data.ip_address_setting.ip_address = - room_member->GetFakeIpAddress(); - profile.ip_setting_data.ip_address_setting.subnet_mask = RoomSubnetMask; - } + ApplyRoomAddress(*room_member, profile.ip_setting_data.ip_address_setting); profile.ip_setting_data.dns_setting.secondary_dns = {8, 8, 8, 8}; } @@ -726,10 +733,7 @@ void IGeneralService::GetNetworkProfile(HLERequestContext& ctx) { }(); if (auto room_member = Network::GetRoomMember().lock()) { - if (room_member->IsConnected()) { - profile.ip_setting_data.ip_address_setting.ip_address = room_member->GetFakeIpAddress(); - profile.ip_setting_data.ip_address_setting.subnet_mask = RoomSubnetMask; - } + ApplyRoomAddress(*room_member, profile.ip_setting_data.ip_address_setting); } ctx.WriteBuffer(profile); @@ -917,10 +921,7 @@ void IGeneralService::GetCurrentIpConfigInfo(HLERequestContext& ctx) { } if (auto room_member = Network::GetRoomMember().lock()) { - if (room_member->IsConnected()) { - info.ip.ip_address = room_member->GetFakeIpAddress(); - info.ip.subnet_mask = RoomSubnetMask; - } + ApplyRoomAddress(*room_member, info.ip); info.dns.is_automatic = true; info.dns.primary_dns = {1, 1, 1, 1}; info.dns.secondary_dns = {8, 8, 8, 8}; diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp index f42ed61e21..83eb2ce9d0 100644 --- a/src/core/internal_network/socket_proxy.cpp +++ b/src/core/internal_network/socket_proxy.cpp @@ -31,21 +31,21 @@ ProxySocket::~ProxySocket() { } void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) { - if (protocol != packet.protocol || local_endpoint.portno != packet.remote_endpoint.portno || - closed) { - return; - } - - if (!broadcast && packet.broadcast) { - LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode"); - return; - } - - auto decompressed = packet; - decompressed.data = Common::Compression::DecompressDataZSTD(packet.data); - { + // Runs on the room thread, while the game thread may bind, close or reconfigure the socket std::lock_guard guard(packets_mutex); + if (protocol != packet.protocol || local_endpoint.portno != packet.remote_endpoint.portno || + closed) { + return; + } + + if (!broadcast && packet.broadcast) { + LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode"); + return; + } + + auto decompressed = packet; + decompressed.data = Common::Compression::DecompressDataZSTD(packet.data); received_packets.push(std::move(decompressed)); } packets_cv.notify_one(); @@ -58,7 +58,10 @@ Errno ProxySocket::SetSockOpt(SOCKET fd_, int option, T value) { } Errno ProxySocket::Initialize(Domain domain, Type type, Protocol socket_protocol) { - protocol = socket_protocol; + { + std::lock_guard guard(packets_mutex); + protocol = socket_protocol; + } SetSockOpt(fd, SO_TYPE, type); return Errno::SUCCESS; @@ -85,6 +88,7 @@ std::pair ProxySocket::GetSockName() { } Errno ProxySocket::Bind(SockAddrIn addr) { + std::lock_guard guard(packets_mutex); if (is_bound) { LOG_WARNING(Network, "Rebinding Socket is unimplemented!"); return Errno::SUCCESS; @@ -106,6 +110,14 @@ Errno ProxySocket::Shutdown(ShutdownHow how) { } std::pair ProxySocket::Recv(int flags, std::span message) { + if (protocol == Protocol::TCP) { + // Room TCP sockets have no peer (Connect and Send are stubs). Report an empty queue as the + // end of the stream, as before, instead of blocking the bsd service until the timeout. + std::lock_guard guard(packets_mutex); + if (received_packets.empty()) { + return {0, Errno::SUCCESS}; + } + } return RecvFrom(flags, message, nullptr); } @@ -114,6 +126,9 @@ std::pair ProxySocket::RecvFrom(int flags, std::span message, So ASSERT(message.size() < static_cast((std::numeric_limits::max)())); std::unique_lock lock(packets_mutex); + if (closed) { + return {-1, Errno::BADF}; + } if (received_packets.empty()) { if (!blocking) { return {-1, Errno::AGAIN}; @@ -124,9 +139,12 @@ std::pair ProxySocket::RecvFrom(int flags, std::span message, So // the timeout to 5s instead. An expired wait is reported like an SO_RCVTIMEO timeout. const u32 timeout = receive_timeout == 0 ? 5000 : receive_timeout; if (!packets_cv.wait_for(lock, std::chrono::milliseconds{timeout}, - [this] { return !received_packets.empty(); })) { + [this] { return closed || !received_packets.empty(); })) { return {-1, Errno::AGAIN}; } + if (closed) { + return {-1, Errno::BADF}; + } } return ReceivePacket(flags, message, addr, message.size()); @@ -217,8 +235,13 @@ std::pair ProxySocket::SendTo(u32 flags, std::span message } Errno ProxySocket::Close() { - fd = INVALID_SOCKET; - closed = true; + { + std::lock_guard guard(packets_mutex); + fd = INVALID_SOCKET; + closed = true; + received_packets = {}; + } + packets_cv.notify_all(); return Errno::SUCCESS; } @@ -239,7 +262,10 @@ Errno ProxySocket::SetReuseAddr(bool enable) { } Errno ProxySocket::SetBroadcast(bool enable) { - broadcast = enable; + { + std::lock_guard guard(packets_mutex); + broadcast = enable; + } return SetSockOpt(fd, SO_BROADCAST, enable ? 1 : 0); } -- cgit v1.2.3 From fd3e2fe18a101e2bbfd5935263183e25ab26497b Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 00:17:50 +0100 Subject: network: fix room socket races and share the room network check bsd:s, bsd:u and bsd:a share one file descriptor table, but each service bound its own proxy packet callback, so every room packet was queued three times on each socket. Bind the callback once. The room thread walked the descriptor table without a lock while bsd created and closed sockets, so it could reach a destroyed or not yet created socket. Guard the table with a mutex, and publish a socket only after it is created and initialized. Check airplane mode before an fd is taken, so a refused socket no longer leaks its slot. ProxySocket now initializes protocol, decompresses packets outside the packet lock without copying the compressed data first, and keeps a read offset for partial TCP reads instead of erasing from the front. Add Network::GetRoomIPv4Address(), set only in the Joined and Moderator states. NIFM uses it for network availability and for every reported address, including EnumerateNetworkInterfaces and GetCurrentIpAddress, so no call reports the room address while another rejects the network. Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/core/hle/service/nifm/nifm.cpp | 37 ++++++++-------- src/core/hle/service/sockets/bsd.cpp | 71 +++++++++++++++++++----------- src/core/hle/service/sockets/bsd.h | 12 +++-- src/core/internal_network/network.cpp | 11 +++++ src/core/internal_network/network.h | 4 ++ src/core/internal_network/socket_proxy.cpp | 29 +++++++++--- src/core/internal_network/socket_proxy.h | 3 +- 7 files changed, 111 insertions(+), 56 deletions(-) diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index e0a23f7a63..8714597f89 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -73,13 +73,8 @@ static u128 MakeUuidFromName(std::string_view name) { // A joined room is the emulated network, even in airplane mode. Its sockets only reach room // members, and LAN games (e.g. Mario Kart World LAN Play) need their connection request accepted. static bool IsNetworkAvailable() { - if (const auto room_member = Network::GetRoomMember().lock()) { - // The room host and moderators are in the Moderator state instead of Joined - const auto state = room_member->GetState(); - if (state == Network::RoomMember::State::Joined || - state == Network::RoomMember::State::Moderator) { - return true; - } + if (Network::GetRoomIPv4Address()) { + return true; } return Network::GetHostIPv4Address().has_value() && !Settings::values.airplane_mode.GetValue(); } @@ -128,9 +123,9 @@ static_assert(sizeof(IpAddressSetting) == 0xD, "IpAddressSetting has incorrect s // A host /24 mask would then put room members on different subnets, so LAN games ignore each other. constexpr Network::IPv4Address RoomSubnetMask{255, 255, 0, 0}; -static void ApplyRoomAddress(const Network::RoomMember& room_member, IpAddressSetting& setting) { - if (room_member.IsConnected()) { - setting.ip_address = room_member.GetFakeIpAddress(); +static void ApplyRoomAddress(IpAddressSetting& setting) { + if (const auto room_ip = Network::GetRoomIPv4Address()) { + setting.ip_address = *room_ip; setting.subnet_mask = RoomSubnetMask; } } @@ -596,7 +591,7 @@ void IGeneralService::GetCurrentNetworkProfile(HLERequestContext& ctx) { std::strncpy(profile.network_name.data(), st.ssid, sizeof(profile.network_name) - 1); if (auto room_member = Network::GetRoomMember().lock()) { - ApplyRoomAddress(*room_member, profile.ip_setting_data.ip_address_setting); + ApplyRoomAddress(profile.ip_setting_data.ip_address_setting); profile.ip_setting_data.dns_setting.secondary_dns = {8, 8, 8, 8}; } @@ -618,6 +613,8 @@ void IGeneralService::EnumerateNetworkInterfaces(HLERequestContext& ctx) { using Network::HostAdapterKind; const auto adapters = Network::GetAvailableNetworkInterfaces(); + const auto selected = Network::GetSelectedNetworkInterface(); + const auto room_ip = Network::GetRoomIPv4Address(); constexpr size_t kEntry = 0x3F0; std::vector blob(adapters.size() * kEntry, 0); @@ -633,6 +630,12 @@ void IGeneralService::EnumerateNetworkInterfaces(HLERequestContext& ctx) { *reinterpret_cast(base + 0x1C) = host.subnet_mask; *reinterpret_cast(base + 0x20) = host.gateway; + // The room traffic goes through the selected adapter, so report it like the other calls + if (room_ip && selected && host.name == selected->name) { + std::memcpy(base + 0x18, room_ip->data(), room_ip->size()); + std::memcpy(base + 0x1C, RoomSubnetMask.data(), RoomSubnetMask.size()); + } + std::string name_utf8 = host.name; name_utf8.resize(0x110, '\0'); std::memcpy(base + 0x2E0, name_utf8.data(), 0x110); @@ -732,9 +735,7 @@ void IGeneralService::GetNetworkProfile(HLERequestContext& ctx) { .wireless_setting_data{wifi}}; }(); - if (auto room_member = Network::GetRoomMember().lock()) { - ApplyRoomAddress(*room_member, profile.ip_setting_data.ip_address_setting); - } + ApplyRoomAddress(profile.ip_setting_data.ip_address_setting); ctx.WriteBuffer(profile); @@ -871,10 +872,8 @@ void IGeneralService::GetCurrentIpAddress(HLERequestContext& ctx) { ipv4.emplace(Network::IPv4Address{0, 0, 0, 0}); } - if (auto room_member = Network::GetRoomMember().lock()) { - if (room_member->IsConnected()) { - ipv4 = room_member->GetFakeIpAddress(); - } + if (const auto room_ip = Network::GetRoomIPv4Address()) { + ipv4 = room_ip; } IPC::ResponseBuilder rb{ctx, 3}; @@ -921,7 +920,7 @@ void IGeneralService::GetCurrentIpConfigInfo(HLERequestContext& ctx) { } if (auto room_member = Network::GetRoomMember().lock()) { - ApplyRoomAddress(*room_member, info.ip); + ApplyRoomAddress(info.ip); info.dns.is_automatic = true; info.dns.primary_dns = {1, 1, 1, 1}; info.dns.secondary_dns = {8, 8, 8, 8}; diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index 02c6bb7762..1b6b132769 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -535,33 +535,36 @@ std::pair BSD_USA::SocketImpl(Domain domain, Type type, Protocol pro UNIMPLEMENTED_IF_MSG(unk_flag, "Unknown flag in type"); type = static_cast(static_cast(type) & ~0x20000000); - const s32 fd = FindFreeFileDescriptorHandle(); - if (fd < 0) { - LOG_ERROR(Service, "No more file descriptors available"); - return {-1, Errno::MFILE}; + // Checked before taking an fd. A joined room does not lift it: room TCP is only stubbed. + if (Settings::values.airplane_mode.GetValue() && IsConnectionBased(type)) { + LOG_ERROR(Service, "Airplane mode is enabled, cannot create socket"); + return {-1, Errno::NOTCONN}; } - file_descriptors[fd] = FileDescriptor{}; - FileDescriptor& descriptor = *file_descriptors[fd]; - // ENONMEM might be thrown here - - LOG_INFO(Service, "New socket fd={}", fd); - + std::shared_ptr socket; auto room_member = Network::GetRoomMember().lock(); if (room_member && room_member->IsConnected()) { - descriptor.socket = std::make_shared(); + socket = std::make_shared(); } else { - descriptor.socket = std::make_shared(); + socket = std::make_shared(); } + socket->Initialize(Translate(domain), Translate(type), Translate(protocol)); - descriptor.socket->Initialize(Translate(domain), Translate(type), Translate(protocol)); - descriptor.is_connection_based = IsConnectionBased(type); - - if (Settings::values.airplane_mode.GetValue() && descriptor.is_connection_based) { - LOG_ERROR(Service, "Airplane mode is enabled, cannot create socket"); - return {-1, Errno::NOTCONN}; + // Publish the socket only once it is set up, since the room thread delivers packets to it + std::scoped_lock lock{file_descriptors_mutex}; + const s32 fd = FindFreeFileDescriptorHandle(); + if (fd < 0) { + LOG_ERROR(Service, "No more file descriptors available"); + return {-1, Errno::MFILE}; } + file_descriptors[fd] = FileDescriptor{ + .socket = std::move(socket), + .is_connection_based = IsConnectionBased(type), + }; + + LOG_INFO(Service, "New socket fd={}", fd); + return {fd, Errno::SUCCESS}; } @@ -649,10 +652,13 @@ std::pair BSD_USA::AcceptImpl(s32 fd, std::vector& write_buffer) return {-1, Translate(bsd_errno)}; } - file_descriptors[new_fd] = FileDescriptor{}; - FileDescriptor& new_descriptor = *file_descriptors[new_fd]; - new_descriptor.socket = std::move(result.socket); - new_descriptor.is_connection_based = descriptor.is_connection_based; + { + std::scoped_lock lock{file_descriptors_mutex}; + file_descriptors[new_fd] = FileDescriptor{ + .socket = std::move(result.socket), + .is_connection_based = descriptor.is_connection_based, + }; + } const SockAddrIn guest_addr_in = Translate(result.sockaddr_in); PutValue(write_buffer, guest_addr_in); @@ -1007,6 +1013,7 @@ Errno BSD_USA::CloseImpl(s32 fd) { LOG_INFO(Service, "Close socket fd={}", fd); + std::scoped_lock lock{file_descriptors_mutex}; file_descriptors[fd].reset(); return bsd_errno; } @@ -1016,6 +1023,7 @@ std::variant BSD_USA::DuplicateSocketImpl(s32 fd) { return Errno::BADF; } + std::scoped_lock lock{file_descriptors_mutex}; const s32 new_fd = FindFreeFileDescriptorHandle(); if (new_fd < 0) { LOG_ERROR(Service, "No more file descriptors available"); @@ -1071,12 +1079,14 @@ void BSD_USA::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const } void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) { + // Runs on the room thread. The lock keeps a socket alive while a bsd thread closes it. + std::scoped_lock lock{file_descriptors_mutex}; for (auto& optional_descriptor : file_descriptors) { - if (!optional_descriptor.has_value()) { + if (!optional_descriptor.has_value() || !optional_descriptor->socket) { continue; } FileDescriptor& descriptor = *optional_descriptor; - descriptor.socket.get()->HandleProxyPacket(packet); + descriptor.socket->HandleProxyPacket(packet); } } @@ -1140,18 +1150,27 @@ BSD_USA::BSD_USA(Core::System& system_, const char* name, bool is_user_) RegisterHandlers(functions); + std::scoped_lock lock{proxy_binding_mutex}; + if (proxy_binding_count++ != 0) { + return; + } if (auto room_member = Network::GetRoomMember().lock()) { - proxy_packet_received = room_member->BindOnProxyPacketReceived( - [this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); }); + proxy_packet_received = + room_member->BindOnProxyPacketReceived(&BSD_USA::OnProxyPacketReceived); } else { LOG_ERROR(Service, "Network isn't initialized"); } } BSD_USA::~BSD_USA() { + std::scoped_lock lock{proxy_binding_mutex}; + if (--proxy_binding_count != 0) { + return; + } if (auto room_member = Network::GetRoomMember().lock()) { room_member->Unbind(proxy_packet_received); } + proxy_packet_received.reset(); } std::unique_lock BSD_USA::LockService() noexcept { diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h index 5cc3eb5b81..0df5c58945 100644 --- a/src/core/hle/service/sockets/bsd.h +++ b/src/core/hle/service/sockets/bsd.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -181,12 +182,17 @@ private: void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept; static inline std::array, MAX_FD> file_descriptors{}; + /// Guards file_descriptors against the room thread, which delivers proxy packets to them + static inline std::mutex file_descriptors_mutex; /// Callback to parse and handle a received wifi packet. - void OnProxyPacketReceived(const Network::ProxyPacket& packet); + static void OnProxyPacketReceived(const Network::ProxyPacket& packet); - // Callback identifier for the OnProxyPacketReceived event. - Network::RoomMember::CallbackHandle proxy_packet_received; + // Callback identifier for the OnProxyPacketReceived event. bsd:s, bsd:u and bsd:a share + // file_descriptors, so they bind once; otherwise each packet is queued once per service. + static inline Network::RoomMember::CallbackHandle proxy_packet_received; + static inline std::mutex proxy_binding_mutex; + static inline size_t proxy_binding_count = 0; protected: std::unique_lock LockService() noexcept override; diff --git a/src/core/internal_network/network.cpp b/src/core/internal_network/network.cpp index 01d37fb35b..d1c436aa08 100644 --- a/src/core/internal_network/network.cpp +++ b/src/core/internal_network/network.cpp @@ -722,6 +722,17 @@ std::optional GetHostIPv4Address() { return TranslateIPv4(network_interface->ip_address); } +std::optional GetRoomIPv4Address() { + if (const auto room_member = GetRoomMember().lock()) { + // The room host and moderators are in the Moderator state instead of Joined + const auto state = room_member->GetState(); + if (state == RoomMember::State::Joined || state == RoomMember::State::Moderator) { + return room_member->GetFakeIpAddress(); + } + } + return std::nullopt; +} + std::string IPv4AddressToString(IPv4Address ip_addr) { std::array buf = {}; ASSERT(inet_ntop(AF_INET, &ip_addr, buf.data(), sizeof(buf)) == buf.data()); diff --git a/src/core/internal_network/network.h b/src/core/internal_network/network.h index 2c9ae523c5..a598f75348 100644 --- a/src/core/internal_network/network.h +++ b/src/core/internal_network/network.h @@ -121,6 +121,10 @@ constexpr IPv4Address TranslateIPv4(in_addr addr) { /// @return human ordered IPv4 address (e.g. 192.168.0.1) as an array std::optional GetHostIPv4Address(); +/// @brief Returns the fake IPv4 address of the joined room, the emulated network while in a room +/// @return nullopt outside a room and while still joining it +std::optional GetRoomIPv4Address(); + std::string IPv4AddressToString(IPv4Address ip_addr); u32 IPv4AddressToInteger(IPv4Address ip_addr); diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp index 83eb2ce9d0..6c2f79da6e 100644 --- a/src/core/internal_network/socket_proxy.cpp +++ b/src/core/internal_network/socket_proxy.cpp @@ -43,9 +43,21 @@ void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) { LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode"); return; } + } - auto decompressed = packet; - decompressed.data = Common::Compression::DecompressDataZSTD(packet.data); + // Decompress without the lock, so the game thread does not wait for it + ProxyPacket decompressed{ + .local_endpoint = packet.local_endpoint, + .remote_endpoint = packet.remote_endpoint, + .protocol = packet.protocol, + .broadcast = packet.broadcast, + .data = Common::Compression::DecompressDataZSTD(packet.data), + }; + { + std::lock_guard guard(packets_mutex); + if (closed) { + return; + } received_packets.push(std::move(decompressed)); } packets_cv.notify_one(); @@ -152,26 +164,28 @@ std::pair ProxySocket::RecvFrom(int flags, std::span message, So std::pair ProxySocket::ReceivePacket(int flags, std::span message, SockAddrIn* addr, std::size_t max_length) { - ProxyPacket& packet = received_packets.front(); + const ProxyPacket& packet = received_packets.front(); if (addr) { addr->family = Domain::INET; addr->ip = packet.local_endpoint.ip; // The senders ip address addr->portno = packet.local_endpoint.portno; // The senders port number } - const std::size_t read_bytes = (std::min)(packet.data.size(), max_length); - std::memcpy(message.data(), packet.data.data(), read_bytes); + const std::span unread = std::span{packet.data}.subspan(front_read_offset); + const std::size_t read_bytes = (std::min)(unread.size(), max_length); + std::memcpy(message.data(), unread.data(), read_bytes); if ((flags & FLAG_MSG_PEEK) != 0) { return {static_cast(read_bytes), Errno::SUCCESS}; } - if (protocol == Protocol::TCP && read_bytes < packet.data.size()) { + if (protocol == Protocol::TCP && read_bytes < unread.size()) { // Keep the unread part of the stream for the next call - packet.data.erase(packet.data.begin(), packet.data.begin() + read_bytes); + front_read_offset += read_bytes; } else { // Like BSD recv(), the rest of a datagram that does not fit is discarded received_packets.pop(); + front_read_offset = 0; } return {static_cast(read_bytes), Errno::SUCCESS}; @@ -240,6 +254,7 @@ Errno ProxySocket::Close() { fd = INVALID_SOCKET; closed = true; received_packets = {}; + front_read_offset = 0; } packets_cv.notify_all(); diff --git a/src/core/internal_network/socket_proxy.h b/src/core/internal_network/socket_proxy.h index 8343fcbae1..d2bd7aae77 100644 --- a/src/core/internal_network/socket_proxy.h +++ b/src/core/internal_network/socket_proxy.h @@ -90,7 +90,8 @@ private: SockAddrIn local_endpoint{}; bool blocking = true; std::queue received_packets; - Protocol protocol; + std::size_t front_read_offset = 0; ///< Bytes of the front TCP packet that were already read + Protocol protocol{}; std::mutex packets_mutex; std::condition_variable packets_cv; -- cgit v1.2.3 From af75958b0ed9a0e44a8c21afa768a63541d8b223 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 00:28:31 +0100 Subject: network: use the socket type for room stream reads and wake all readers socket(AF_INET, SOCK_STREAM, 0) leaves the protocol unspecified, so the TCP checks in ProxySocket::Recv and ReceivePacket missed such sockets. recv() then waited for the timeout and returned EAGAIN instead of end of stream, and partial reads dropped the rest of the packet. Keep the socket type and use it for these checks. The protocol stays as it is, because it is sent to other room members. A MSG_PEEK reader leaves the packet queued, so notify every waiter when a packet arrives instead of only one. Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/core/internal_network/socket_proxy.cpp | 8 +++++--- src/core/internal_network/socket_proxy.h | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp index 6c2f79da6e..1b93ce93e5 100644 --- a/src/core/internal_network/socket_proxy.cpp +++ b/src/core/internal_network/socket_proxy.cpp @@ -60,7 +60,8 @@ void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) { } received_packets.push(std::move(decompressed)); } - packets_cv.notify_one(); + // A MSG_PEEK reader leaves the packet queued, so every waiter must see it + packets_cv.notify_all(); } template @@ -72,6 +73,7 @@ Errno ProxySocket::SetSockOpt(SOCKET fd_, int option, T value) { Errno ProxySocket::Initialize(Domain domain, Type type, Protocol socket_protocol) { { std::lock_guard guard(packets_mutex); + socket_type = type; protocol = socket_protocol; } SetSockOpt(fd, SO_TYPE, type); @@ -122,7 +124,7 @@ Errno ProxySocket::Shutdown(ShutdownHow how) { } std::pair ProxySocket::Recv(int flags, std::span message) { - if (protocol == Protocol::TCP) { + if (socket_type == Type::STREAM) { // Room TCP sockets have no peer (Connect and Send are stubs). Report an empty queue as the // end of the stream, as before, instead of blocking the bsd service until the timeout. std::lock_guard guard(packets_mutex); @@ -179,7 +181,7 @@ std::pair ProxySocket::ReceivePacket(int flags, std::span messag return {static_cast(read_bytes), Errno::SUCCESS}; } - if (protocol == Protocol::TCP && read_bytes < unread.size()) { + if (socket_type == Type::STREAM && read_bytes < unread.size()) { // Keep the unread part of the stream for the next call front_read_offset += read_bytes; } else { diff --git a/src/core/internal_network/socket_proxy.h b/src/core/internal_network/socket_proxy.h index d2bd7aae77..5d6e35693c 100644 --- a/src/core/internal_network/socket_proxy.h +++ b/src/core/internal_network/socket_proxy.h @@ -92,6 +92,7 @@ private: std::queue received_packets; std::size_t front_read_offset = 0; ///< Bytes of the front TCP packet that were already read Protocol protocol{}; + Type socket_type{}; ///< Stream checks use the type, since protocol 0 means the default protocol std::mutex packets_mutex; std::condition_variable packets_cv; -- cgit v1.2.3 From a7e03702c10290dd46bc40e46599729510eda458 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 00:46:55 +0100 Subject: network: keep bsd sockets alive across concurrent close and fix room gaps bsd runs on three host threads, but its calls kept a raw reference into the shared descriptor table while they used a socket. A close() on another thread could then destroy the socket mid-call. Waking a blocked room recv() on close made this happen every time. Every call now works on a copy of the descriptor taken under the table lock, poll() keeps its sockets alive while it waits, and close() resets the slot only if it still holds the same socket. accept() picked a free fd before blocking and only locked the write, so another thread could publish a socket in the same slot. It now picks and writes the fd in one critical section after Accept() returns. dup() now re-checks the source fd under the lock. A duplicated fd shares its socket, so the room packet loop queued each packet once per fd. Deliver it once per socket. NIFM: when the selected host adapter is missing, the room still works and IsNetworkAvailable reports it, but GetCurrentNetworkProfile returned an empty profile. Report a wired room profile in that case, and put the room address on the first adapter in EnumerateNetworkInterfaces. Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/core/hle/service/nifm/nifm.cpp | 18 ++- src/core/hle/service/sockets/bsd.cpp | 257 +++++++++++++++++------------------ src/core/hle/service/sockets/bsd.h | 4 +- 3 files changed, 144 insertions(+), 135 deletions(-) diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 8714597f89..ea00e9f35c 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -600,6 +600,19 @@ void IGeneralService::GetCurrentNetworkProfile(HLERequestContext& ctx) { std::memcpy(profile.wireless_setting_data.ssid.data(), st.ssid, profile.wireless_setting_data.ssid_length); } + } else if (Network::GetRoomIPv4Address()) { + // The selected adapter is missing, but the room still works and IsNetworkAvailable + // reports it, so describe the room as a wired network + profile.ip_setting_data.ip_address_setting.is_automatic = true; + ApplyRoomAddress(profile.ip_setting_data.ip_address_setting); + + profile.ip_setting_data.dns_setting.is_automatic = true; + profile.ip_setting_data.dns_setting.primary_dns = {1, 1, 1, 1}; + profile.ip_setting_data.dns_setting.secondary_dns = {8, 8, 8, 8}; + + profile.uuid = MakeUuidFromName(st.ssid); + profile.profile_type = static_cast(NetworkProfileType::User); + profile.interface_type = static_cast(NetworkInterfaceType::Ethernet); } ctx.WriteBuffer(profile); @@ -630,8 +643,9 @@ void IGeneralService::EnumerateNetworkInterfaces(HLERequestContext& ctx) { *reinterpret_cast(base + 0x1C) = host.subnet_mask; *reinterpret_cast(base + 0x20) = host.gateway; - // The room traffic goes through the selected adapter, so report it like the other calls - if (room_ip && selected && host.name == selected->name) { + // The room traffic goes through the selected adapter, so report it like the other calls. + // Without a selected adapter, the first one stands for the room. + if (room_ip && (selected ? host.name == selected->name : i == 0)) { std::memcpy(base + 0x18, room_ip->data(), room_ip->size()); std::memcpy(base + 0x1C, RoomSubnetMask.data(), RoomSubnetMask.size()); } diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index 1b6b132769..477f32626d 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -598,31 +599,39 @@ std::pair BSD_USA::PollImpl(std::vector& write_buffer, std::span return {-1, Errno::INVAL}; } - for (PollFD& pollfd : fds) { - ASSERT(False(pollfd.revents)); - - if (pollfd.fd > static_cast(MAX_FD) || pollfd.fd < 0) { - LOG_ERROR(Service, "File descriptor handle={} is invalid", pollfd.fd); - pollfd.revents = PollEvents{}; - return {0, Errno::SUCCESS}; - } - - const std::optional& descriptor = file_descriptors[pollfd.fd]; - if (!descriptor) { - LOG_TRACE(Service, "File descriptor handle={} is not allocated", pollfd.fd); - pollfd.revents = PollEvents::Nval; - return {0, Errno::SUCCESS}; + // The copies keep the sockets alive while Network::Poll waits, even if another bsd thread + // closes one of them meanwhile + std::vector> sockets(fds.size()); + { + std::scoped_lock lock{file_descriptors_mutex}; + for (size_t i = 0; i < fds.size(); ++i) { + PollFD& pollfd = fds[i]; + ASSERT(False(pollfd.revents)); + + if (pollfd.fd > static_cast(MAX_FD) || pollfd.fd < 0) { + LOG_ERROR(Service, "File descriptor handle={} is invalid", pollfd.fd); + pollfd.revents = PollEvents{}; + return {0, Errno::SUCCESS}; + } + + const std::optional& descriptor = file_descriptors[pollfd.fd]; + if (!descriptor) { + LOG_TRACE(Service, "File descriptor handle={} is not allocated", pollfd.fd); + pollfd.revents = PollEvents::Nval; + return {0, Errno::SUCCESS}; + } + sockets[i] = descriptor->socket; } } std::vector host_pollfds(fds.size()); - std::transform(fds.begin(), fds.end(), host_pollfds.begin(), [](PollFD pollfd) { - Network::PollFD result; - result.socket = file_descriptors[pollfd.fd]->socket.get(); - result.events = Translate(pollfd.events); - result.revents = Network::PollEvents{}; - return result; - }); + for (size_t i = 0; i < fds.size(); ++i) { + host_pollfds[i] = Network::PollFD{ + .socket = sockets[i].get(), + .events = Translate(fds[i].events), + .revents = Network::PollEvents{}, + }; + } const auto result = Network::Poll(host_pollfds, timeout); @@ -636,27 +645,28 @@ std::pair BSD_USA::PollImpl(std::vector& write_buffer, std::span } std::pair BSD_USA::AcceptImpl(s32 fd, std::vector& write_buffer) { - if (!IsFileDescriptorValid(fd)) { + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return {-1, Errno::BADF}; } - const s32 new_fd = FindFreeFileDescriptorHandle(); - if (new_fd < 0) { - LOG_ERROR(Service, "No more file descriptors available"); - return {-1, Errno::MFILE}; - } - - FileDescriptor& descriptor = *file_descriptors[fd]; - auto [result, bsd_errno] = descriptor.socket->Accept(); + auto [result, bsd_errno] = descriptor->socket->Accept(); if (bsd_errno != Network::Errno::SUCCESS) { return {-1, Translate(bsd_errno)}; } + // Accept may block, so the fd is picked afterwards, in the same critical section as the write + s32 new_fd; { std::scoped_lock lock{file_descriptors_mutex}; + new_fd = FindFreeFileDescriptorHandle(); + if (new_fd < 0) { + LOG_ERROR(Service, "No more file descriptors available"); + return {-1, Errno::MFILE}; + } file_descriptors[new_fd] = FileDescriptor{ .socket = std::move(result.socket), - .is_connection_based = descriptor.is_connection_based, + .is_connection_based = descriptor->is_connection_based, }; } @@ -667,34 +677,28 @@ std::pair BSD_USA::AcceptImpl(s32 fd, std::vector& write_buffer) } Errno BSD_USA::BindImpl(s32 fd, std::span addr) { - if (!IsFileDescriptorValid(fd)) { + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } ASSERT(addr.size() >= 16); - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); - return Errno::BADF; - } auto addr_in = GetValue(addr); - return Translate(file_descriptors[fd]->socket->Bind(Translate(addr_in))); + return Translate(descriptor->socket->Bind(Translate(addr_in))); } Errno BSD_USA::ConnectImpl(s32 fd, std::span addr) { - if (!IsFileDescriptorValid(fd)) { + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } ASSERT(addr.size() >= 16); - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); - return Errno::BADF; - } auto addr_in = GetValue(addr); - const Errno result = Translate(file_descriptors[fd]->socket->Connect(Translate(addr_in))); + const Errno result = Translate(descriptor->socket->Connect(Translate(addr_in))); if (result == Errno::ISCONN) { LOG_DEBUG(Service, "returned ISCONN - socket already connected"); @@ -705,16 +709,12 @@ Errno BSD_USA::ConnectImpl(s32 fd, std::span addr) { } Errno BSD_USA::GetPeerNameImpl(s32 fd, std::vector& write_buffer) { - if (!IsFileDescriptorValid(fd)) { - return Errno::BADF; - } - - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } - const auto [addr_in, bsd_errno] = file_descriptors[fd]->socket->GetPeerName(); + const auto [addr_in, bsd_errno] = descriptor->socket->GetPeerName(); if (bsd_errno != Network::Errno::SUCCESS) { return Translate(bsd_errno); } @@ -727,16 +727,12 @@ Errno BSD_USA::GetPeerNameImpl(s32 fd, std::vector& write_buffer) { } Errno BSD_USA::GetSockNameImpl(s32 fd, std::vector& write_buffer) { - if (!IsFileDescriptorValid(fd)) { + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); - return Errno::BADF; - } - - const auto [addr_in, bsd_errno] = file_descriptors[fd]->socket->GetSockName(); + const auto [addr_in, bsd_errno] = descriptor->socket->GetSockName(); if (bsd_errno != Network::Errno::SUCCESS) { return Translate(bsd_errno); } @@ -749,38 +745,33 @@ Errno BSD_USA::GetSockNameImpl(s32 fd, std::vector& write_buffer) { } Errno BSD_USA::ListenImpl(s32 fd, s32 backlog) { - if (!IsFileDescriptorValid(fd)) { - return Errno::BADF; - } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } - return Translate(file_descriptors[fd]->socket->Listen(backlog)); + return Translate(descriptor->socket->Listen(backlog)); } std::pair BSD_USA::FcntlImpl(s32 fd, FcntlCmd cmd, s32 arg) { - if (!IsFileDescriptorValid(fd)) { - return {-1, Errno::BADF}; - } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return {-1, Errno::BADF}; } - FileDescriptor& descriptor = *file_descriptors[fd]; - switch (cmd) { case FcntlCmd::GETFL: ASSERT(arg == 0); - return {descriptor.flags, Errno::SUCCESS}; + return {descriptor->flags, Errno::SUCCESS}; case FcntlCmd::SETFL: { const bool enable = (arg & Network::FLAG_O_NONBLOCK) != 0; - const Errno bsd_errno = Translate(descriptor.socket->SetNonBlock(enable)); + const Errno bsd_errno = Translate(descriptor->socket->SetNonBlock(enable)); if (bsd_errno != Errno::SUCCESS) { return {-1, bsd_errno}; } - descriptor.flags = arg; + std::scoped_lock lock{file_descriptors_mutex}; + if (auto& entry = file_descriptors[fd]; entry && entry->socket == descriptor->socket) { + entry->flags = arg; + } return {0, Errno::SUCCESS}; } default: @@ -790,11 +781,8 @@ std::pair BSD_USA::FcntlImpl(s32 fd, FcntlCmd cmd, s32 arg) { } Errno BSD_USA::GetSockOptImpl(s32 fd, u32 level, OptName optname, std::vector& optval) { - if (!IsFileDescriptorValid(fd)) { - return Errno::BADF; - } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } @@ -803,7 +791,7 @@ Errno BSD_USA::GetSockOptImpl(s32 fd, u32 level, OptName optname, std::vectorsocket.get(); + Network::SocketBase* const socket = descriptor->socket.get(); switch (optname) { case OptName::ERROR_: { @@ -825,11 +813,8 @@ Errno BSD_USA::GetSockOptImpl(s32 fd, u32 level, OptName optname, std::vector optval) { - if (!IsFileDescriptorValid(fd)) { - return Errno::BADF; - } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } @@ -838,7 +823,7 @@ Errno BSD_USA::SetSockOptImpl(s32 fd, u32 level, OptName optname, std::spansocket.get(); + Network::SocketBase* const socket = descriptor->socket.get(); if (optname == OptName::LINGER) { ASSERT(optval.size() == sizeof(Linger)); @@ -879,39 +864,35 @@ Errno BSD_USA::SetSockOptImpl(s32 fd, u32 level, OptName optname, std::spansocket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } const Network::ShutdownHow host_how = Translate(static_cast(how)); - return Translate(file_descriptors[fd]->socket->Shutdown(host_how)); + return Translate(descriptor->socket->Shutdown(host_how)); } std::pair BSD_USA::RecvImpl(s32 fd, u32 flags, std::vector& message) { - if (!IsFileDescriptorValid(fd)) { + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return {-1, Errno::BADF}; } - FileDescriptor& descriptor = *file_descriptors[fd]; - // Apply flags using Network::FLAG_MSG_DONTWAIT; using Network::FLAG_O_NONBLOCK; if ((flags & FLAG_MSG_DONTWAIT) != 0) { flags &= ~FLAG_MSG_DONTWAIT; - if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) { - descriptor.socket->SetNonBlock(true); + if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { + descriptor->socket->SetNonBlock(true); } } - const auto [ret, bsd_errno] = Translate(descriptor.socket->Recv(flags, message)); + const auto [ret, bsd_errno] = Translate(descriptor->socket->Recv(flags, message)); // Restore original state - if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) { - descriptor.socket->SetNonBlock(false); + if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { + descriptor->socket->SetNonBlock(false); } return {ret, bsd_errno}; @@ -919,15 +900,14 @@ std::pair BSD_USA::RecvImpl(s32 fd, u32 flags, std::vector& mess std::pair BSD_USA::RecvFromImpl(s32 fd, u32 flags, std::vector& message, std::vector& addr) { - if (!IsFileDescriptorValid(fd)) { + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return {-1, Errno::BADF}; } - FileDescriptor& descriptor = *file_descriptors[fd]; - Network::SockAddrIn addr_in{}; Network::SockAddrIn* p_addr_in = nullptr; - if (descriptor.is_connection_based) { + if (descriptor->is_connection_based) { // Connection based file descriptors (e.g. TCP) zero addr addr.clear(); } else { @@ -939,16 +919,17 @@ std::pair BSD_USA::RecvFromImpl(s32 fd, u32 flags, std::vector& using Network::FLAG_O_NONBLOCK; if ((flags & FLAG_MSG_DONTWAIT) != 0) { flags &= ~FLAG_MSG_DONTWAIT; - if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) { - descriptor.socket->SetNonBlock(true); + if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { + descriptor->socket->SetNonBlock(true); } } - const auto [ret, bsd_errno] = Translate(descriptor.socket->RecvFrom(flags, message, p_addr_in)); + const auto [ret, bsd_errno] = + Translate(descriptor->socket->RecvFrom(flags, message, p_addr_in)); // Restore original state - if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) { - descriptor.socket->SetNonBlock(false); + if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { + descriptor->socket->SetNonBlock(false); } if (p_addr_in) { @@ -965,23 +946,17 @@ std::pair BSD_USA::RecvFromImpl(s32 fd, u32 flags, std::vector& } std::pair BSD_USA::SendImpl(s32 fd, u32 flags, std::span message) { - if (!IsFileDescriptorValid(fd)) { + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return {-1, Errno::BADF}; } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); - return {-1, Errno::BADF}; - } - return Translate(file_descriptors[fd]->socket->Send(message, flags)); + return Translate(descriptor->socket->Send(message, flags)); } std::pair BSD_USA::SendToImpl(s32 fd, u32 flags, std::span message, std::span addr) { - if (!IsFileDescriptorValid(fd)) { - return {-1, Errno::BADF}; - } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return {-1, Errno::BADF}; } @@ -994,36 +969,36 @@ std::pair BSD_USA::SendToImpl(s32 fd, u32 flags, std::span p_addr_in = &addr_in; } - return Translate(file_descriptors[fd]->socket->SendTo(flags, message, p_addr_in)); + return Translate(descriptor->socket->SendTo(flags, message, p_addr_in)); } Errno BSD_USA::CloseImpl(s32 fd) { - if (!IsFileDescriptorValid(fd)) { - return Errno::BADF; - } - if (!file_descriptors[fd]->socket) { - LOG_WARNING(Service, "Uninitialized socket"); + const auto descriptor = CopyDescriptor(fd); + if (!descriptor) { return Errno::BADF; } - const Errno bsd_errno = Translate(file_descriptors[fd]->socket->Close()); + const Errno bsd_errno = Translate(descriptor->socket->Close()); if (bsd_errno != Errno::SUCCESS) { return bsd_errno; } LOG_INFO(Service, "Close socket fd={}", fd); + // Another thread may have closed this fd and reused the slot meanwhile std::scoped_lock lock{file_descriptors_mutex}; - file_descriptors[fd].reset(); + if (file_descriptors[fd] && file_descriptors[fd]->socket == descriptor->socket) { + file_descriptors[fd].reset(); + } return bsd_errno; } std::variant BSD_USA::DuplicateSocketImpl(s32 fd) { - if (!IsFileDescriptorValid(fd)) { + std::scoped_lock lock{file_descriptors_mutex}; + if (!IsFileDescriptorValid(fd) || !file_descriptors[fd]->socket) { return Errno::BADF; } - std::scoped_lock lock{file_descriptors_mutex}; const s32 new_fd = FindFreeFileDescriptorHandle(); if (new_fd < 0) { LOG_ERROR(Service, "No more file descriptors available"); @@ -1039,6 +1014,16 @@ std::variant BSD_USA::DuplicateSocketImpl(s32 fd) { } std::optional> BSD_USA::GetSocket(s32 fd) { + if (auto descriptor = CopyDescriptor(fd)) { + return std::move(descriptor->socket); + } + return std::nullopt; +} + +std::optional BSD_USA::CopyDescriptor(s32 fd) { + // The copy holds a reference to the socket, so a close on another bsd thread cannot destroy + // it while the caller uses it + std::scoped_lock lock{file_descriptors_mutex}; if (!IsFileDescriptorValid(fd)) { return std::nullopt; } @@ -1046,7 +1031,7 @@ std::optional> BSD_USA::GetSocket(s32 fd) { LOG_WARNING(Service, "Uninitialized socket"); return std::nullopt; } - return file_descriptors[fd]->socket; + return file_descriptors[fd]; } s32 BSD_USA::FindFreeFileDescriptorHandle() noexcept { @@ -1081,12 +1066,20 @@ void BSD_USA::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) { // Runs on the room thread. The lock keeps a socket alive while a bsd thread closes it. std::scoped_lock lock{file_descriptors_mutex}; + // A duplicated fd shares its socket, which must still get each packet only once + std::array delivered{}; + size_t num_delivered = 0; for (auto& optional_descriptor : file_descriptors) { if (!optional_descriptor.has_value() || !optional_descriptor->socket) { continue; } - FileDescriptor& descriptor = *optional_descriptor; - descriptor.socket->HandleProxyPacket(packet); + Network::SocketBase* const socket = optional_descriptor->socket.get(); + const auto delivered_end = delivered.begin() + num_delivered; + if (std::find(delivered.begin(), delivered_end, socket) != delivered_end) { + continue; + } + delivered[num_delivered++] = socket; + socket->HandleProxyPacket(packet); } } diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h index 0df5c58945..f908edc7d2 100644 --- a/src/core/hle/service/sockets/bsd.h +++ b/src/core/hle/service/sockets/bsd.h @@ -178,11 +178,13 @@ private: s32 FindFreeFileDescriptorHandle() noexcept; bool IsFileDescriptorValid(s32 fd) const noexcept; + /// Copies a descriptor under file_descriptors_mutex; nullopt if fd has no socket + std::optional CopyDescriptor(s32 fd); void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept; static inline std::array, MAX_FD> file_descriptors{}; - /// Guards file_descriptors against the room thread, which delivers proxy packets to them + /// Guards file_descriptors. Calls use CopyDescriptor, so sockets outlive a concurrent close. static inline std::mutex file_descriptors_mutex; /// Callback to parse and handle a received wifi packet. -- cgit v1.2.3 From 01090e8e44d8d64bb5839f8d842bdd3028447608 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 01:08:49 +0100 Subject: tests: cover room ProxySocket receives and the LDN join handshake Add unit tests for the room LAN fixes in this branch. The socket tests check datagram truncation, TCP remainders and peeks, timeouts, wakeups on close and on a new packet, and stream behavior with the default protocol. The LDN tests check that a join waits for the host's reply, rejects bad or duplicate joins, ignores late replies and uses fresh identities. Assisted-by: Sol (LDN join tests) Assisted-by: Claude Opus 5.5 (room socket tests) --- src/tests/CMakeLists.txt | 2 + src/tests/core/internal_network/socket_proxy.cpp | 231 +++++++++++++++++++++++ src/tests/core/ldn.cpp | 202 ++++++++++++++++++++ 3 files changed, 435 insertions(+) create mode 100644 src/tests/core/internal_network/socket_proxy.cpp create mode 100644 src/tests/core/ldn.cpp diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 1eb1040e34..a31c073765 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -19,6 +19,8 @@ add_executable(tests common/undefined_fix.cpp common/unique_function.cpp core/core_timing.cpp + core/ldn.cpp + core/internal_network/socket_proxy.cpp core/nup_writer.cpp core/priority_queue.cpp core/internal_network/network.cpp diff --git a/src/tests/core/internal_network/socket_proxy.cpp b/src/tests/core/internal_network/socket_proxy.cpp new file mode 100644 index 0000000000..d97d7d5102 --- /dev/null +++ b/src/tests/core/internal_network/socket_proxy.cpp @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#include + +#include "common/socket_types.h" +#include "common/zstd_compression.h" +#include "core/internal_network/socket_proxy.h" + +namespace { + +void BindSocket(Network::ProxySocket& socket, Network::Protocol protocol) { + const auto type = + protocol == Network::Protocol::TCP ? Network::Type::STREAM : Network::Type::DGRAM; + REQUIRE(socket.Initialize(Network::Domain::INET, type, protocol) == Network::Errno::SUCCESS); + REQUIRE(socket.Bind({.family = Network::Domain::INET, + .ip = {192, 168, 1, 1}, + .portno = 35000}) == Network::Errno::SUCCESS); +} + +template +void QueuePacket(Network::ProxySocket& socket, Network::Protocol protocol, + const std::array& payload) { + Network::ProxyPacket packet{}; + packet.local_endpoint = {.family = Network::Domain::INET, + .ip = {192, 168, 1, 2}, + .portno = 35000}; + packet.remote_endpoint = {.family = Network::Domain::INET, + .ip = {192, 168, 1, 1}, + .portno = 35000}; + packet.protocol = protocol; + packet.data = Common::Compression::CompressDataZSTDDefault(payload.data(), payload.size()); + socket.HandleProxyPacket(packet); +} + +} // namespace + +TEST_CASE("Room UDP sockets receive queued datagrams with recv", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + const std::array payload{1, 2, 3, 4}; + QueuePacket(socket, Network::Protocol::UDP, payload); + + std::array received{}; + const auto [count, error] = socket.Recv(0, received); + REQUIRE(error == Network::Errno::SUCCESS); + REQUIRE(count == payload.size()); + REQUIRE(received == payload); + + const auto [empty_count, empty_error] = socket.Recv(0, received); + REQUIRE(empty_count == -1); + REQUIRE(empty_error == Network::Errno::AGAIN); +} + +TEST_CASE("Room sockets peek without consuming", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + const std::array payload{1, 2, 3, 4}; + QueuePacket(socket, Network::Protocol::UDP, payload); + + std::array received{}; + REQUIRE(socket.Recv(Network::FLAG_MSG_PEEK, received) == + std::pair{4, Network::Errno::SUCCESS}); + REQUIRE(socket.Recv(0, received) == std::pair{4, Network::Errno::SUCCESS}); + REQUIRE(received == payload); + REQUIRE(socket.Recv(0, received).second == Network::Errno::AGAIN); +} + +TEST_CASE("Room UDP sockets truncate oversized datagrams", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + QueuePacket(socket, Network::Protocol::UDP, std::array{1, 2, 3, 4}); + + std::array received{}; + REQUIRE(socket.Recv(0, received) == std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{1, 2}); + REQUIRE(socket.Recv(0, received).second == Network::Errno::AGAIN); +} + +TEST_CASE("Room TCP sockets keep the unread part of a packet", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::TCP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + QueuePacket(socket, Network::Protocol::TCP, std::array{1, 2, 3, 4, 5, 6}); + + std::array received{}; + REQUIRE(socket.Recv(0, received) == std::pair{4, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{1, 2, 3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received[0] == 5); + REQUIRE(received[1] == 6); + REQUIRE(socket.Recv(0, received) == std::pair{0, Network::Errno::SUCCESS}); +} + +TEST_CASE("Room TCP sockets read a packet in small parts and peek at the unread part", + "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::TCP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + QueuePacket(socket, Network::Protocol::TCP, std::array{1, 2, 3, 4, 5}); + QueuePacket(socket, Network::Protocol::TCP, std::array{6, 7}); + + std::array received{}; + REQUIRE(socket.Recv(0, received) == std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{1, 2}); + REQUIRE(socket.Recv(Network::FLAG_MSG_PEEK, received) == + std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair{1, Network::Errno::SUCCESS}); + REQUIRE(received[0] == 5); + REQUIRE(socket.Recv(0, received) == std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{6, 7}); +} + +TEST_CASE("Room TCP sockets report an empty queue as end of stream", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::TCP); + + std::array received{}; + const auto start = std::chrono::steady_clock::now(); + REQUIRE(socket.Recv(0, received) == std::pair{0, Network::Errno::SUCCESS}); + REQUIRE(std::chrono::steady_clock::now() - start < std::chrono::seconds{1}); +} + +TEST_CASE("Blocking room sockets wait for packets and time out with EAGAIN", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetRcvTimeo(50) == Network::Errno::SUCCESS); + + std::array received{}; + const auto start = std::chrono::steady_clock::now(); + REQUIRE(socket.Recv(0, received) == std::pair{-1, Network::Errno::AGAIN}); + REQUIRE(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds{50}); + + REQUIRE(socket.SetRcvTimeo(5000) == Network::Errno::SUCCESS); + const std::array payload{1, 2, 3, 4}; + std::jthread sender([&socket, &payload] { + std::this_thread::sleep_for(std::chrono::milliseconds{20}); + QueuePacket(socket, Network::Protocol::UDP, payload); + }); + REQUIRE(socket.Recv(0, received) == std::pair{4, Network::Errno::SUCCESS}); + REQUIRE(received == payload); +} + +TEST_CASE("Closing a room socket wakes a blocked recv and drops queued packets", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetRcvTimeo(5000) == Network::Errno::SUCCESS); + + std::array received{}; + const auto start = std::chrono::steady_clock::now(); + std::jthread closer([&socket] { + std::this_thread::sleep_for(std::chrono::milliseconds{20}); + socket.Close(); + }); + REQUIRE(socket.Recv(0, received) == std::pair{-1, Network::Errno::BADF}); + REQUIRE(std::chrono::steady_clock::now() - start < std::chrono::seconds{1}); + closer.join(); + + Network::ProxySocket queued; + BindSocket(queued, Network::Protocol::UDP); + QueuePacket(queued, Network::Protocol::UDP, std::array{1, 2, 3, 4}); + REQUIRE(queued.Close() == Network::Errno::SUCCESS); + QueuePacket(queued, Network::Protocol::UDP, std::array{5, 6, 7, 8}); + REQUIRE(queued.Recv(0, received) == std::pair{-1, Network::Errno::BADF}); +} + +TEST_CASE("Room stream sockets with the default protocol keep stream behavior", "[core][sockets]") { + // socket(AF_INET, SOCK_STREAM, 0) leaves the protocol unspecified + Network::ProxySocket socket; + REQUIRE(socket.Initialize(Network::Domain::INET, Network::Type::STREAM, + Network::Protocol::Unspecified) == Network::Errno::SUCCESS); + REQUIRE(socket.Bind({.family = Network::Domain::INET, + .ip = {192, 168, 1, 1}, + .portno = 35000}) == Network::Errno::SUCCESS); + REQUIRE(socket.SetRcvTimeo(1000) == Network::Errno::SUCCESS); + + std::array received{}; + const auto start = std::chrono::steady_clock::now(); + REQUIRE(socket.Recv(0, received) == std::pair{0, Network::Errno::SUCCESS}); + REQUIRE(std::chrono::steady_clock::now() - start < std::chrono::milliseconds{500}); + + QueuePacket(socket, Network::Protocol::Unspecified, std::array{1, 2, 3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{1, 2}); + REQUIRE(socket.Recv(0, received) == std::pair{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array{3, 4}); +} + +TEST_CASE("One room packet wakes every blocked peek reader", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetRcvTimeo(2000) == Network::Errno::SUCCESS); + + // Catch2 assertions are not thread-safe here, so the readers only record their results + std::array, 2> results{}; + std::array finished{}; + std::chrono::steady_clock::time_point queued{}; + { + std::array readers; + for (std::size_t i = 0; i < readers.size(); ++i) { + readers[i] = std::jthread([&socket, &results, &finished, i] { + std::array peeked{}; + results[i] = socket.Recv(Network::FLAG_MSG_PEEK, peeked); + finished[i] = std::chrono::steady_clock::now(); + }); + } + std::this_thread::sleep_for(std::chrono::milliseconds{50}); + queued = std::chrono::steady_clock::now(); + QueuePacket(socket, Network::Protocol::UDP, std::array{1, 2, 3, 4}); + } + + for (std::size_t i = 0; i < results.size(); ++i) { + REQUIRE(results[i] == std::pair{4, Network::Errno::SUCCESS}); + REQUIRE(finished[i] - queued < std::chrono::milliseconds{1000}); + } +} diff --git a/src/tests/core/ldn.cpp b/src/tests/core/ldn.cpp new file mode 100644 index 0000000000..4e6da2d52e --- /dev/null +++ b/src/tests/core/ldn.cpp @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include + +#include "core/hle/service/ldn/lan_discovery.h" +#include "core/internal_network/network_interface.h" + +using namespace std::chrono_literals; + +namespace { + +class TestLANDiscovery : public Service::LDN::LANDiscovery { +public: + void SetConnectionTimeout(std::chrono::nanoseconds timeout) { + connection_timeout = timeout; + } + + std::chrono::nanoseconds GetConnectionTimeout() const { + return connection_timeout; + } + + bool WaitUntilConnecting() { + const auto deadline = std::chrono::steady_clock::now() + 5s; + do { + { + std::scoped_lock lock{packet_mutex}; + if (host_ip) { + // Connect has published its target and released this lock to wait for a reply. + return true; + } + } + std::this_thread::sleep_for(1ms); + } while (std::chrono::steady_clock::now() < deadline); + return false; + } +}; + +} // namespace + +TEST_CASE("LDN waits for the selected host's connection reply", "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + TestLANDiscovery station; + REQUIRE(station.Initialize([] {}).IsSuccess()); + REQUIRE(station.OpenStation().IsSuccess()); + + NetworkInfo info{}; + info.network_id.session_id = {1, 2}; + info.ldn.node_count = 1; + info.ldn.node_count_max = 8; + info.ldn.nodes[0].ipv4_address = {1, 0, 168, 192}; + + Network::LDNPacket reply{}; + reply.type = Network::LDNPacketType::SyncNetwork; + reply.local_ip = {192, 168, 0, 1}; + reply.data.resize(sizeof(info)); + std::memcpy(reply.data.data(), &info, sizeof(info)); + + // A scan result or unsolicited sync must not connect an idle station. + station.ReceivePacket(reply); + REQUIRE(station.GetState() == State::StationOpened); + + auto connecting = std::async(std::launch::async, [&] { return station.Connect(info, {}, 1); }); + REQUIRE(station.WaitUntilConnecting()); + + auto unrelated = reply; + unrelated.local_ip[3] = 2; + station.ReceivePacket(unrelated); + REQUIRE(connecting.wait_for(0ms) == std::future_status::timeout); + + auto stale = info; + stale.network_id.session_id.low++; + std::memcpy(unrelated.data.data(), &stale, sizeof(stale)); + unrelated.local_ip = reply.local_ip; + station.ReceivePacket(unrelated); + REQUIRE(connecting.wait_for(0ms) == std::future_status::timeout); + + unrelated.data.resize(1); + station.ReceivePacket(unrelated); + REQUIRE(connecting.wait_for(0ms) == std::future_status::timeout); + + station.ReceivePacket(reply); + REQUIRE(connecting.wait_for(5s) == std::future_status::ready); + REQUIRE(connecting.get().IsSuccess()); + REQUIRE(station.GetState() == State::StationConnected); + REQUIRE(station.Finalize().IsSuccess()); +} + +TEST_CASE("LDN host validates join packets and does not allocate duplicate stations", + "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + LANDiscovery host; + REQUIRE(host.Initialize([] {}).IsSuccess()); + REQUIRE(host.OpenAccessPoint().IsSuccess()); + NetworkConfig config{}; + config.node_count_max = 2; + REQUIRE(host.CreateNetwork({}, {}, config).IsSuccess()); + + NodeInfo node{}; + node.is_connected = 1; + node.mac_address.raw[5] = 1; + Network::LDNPacket request{}; + request.type = Network::LDNPacketType::Connect; + request.local_ip = {192, 168, 0, 2}; + request.data.resize(sizeof(node)); + std::memcpy(request.data.data(), &node, sizeof(node)); + host.ReceivePacket(request); + NetworkInfo info{}; + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + + host.ReceivePacket(request); + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + + // A third console cannot enter a two-console network. + request.local_ip[3] = 3; + host.ReceivePacket(request); + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + + request.type = Network::LDNPacketType::Disconnect; + request.data.resize(1); + host.ReceivePacket(request); + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + REQUIRE(host.Finalize().IsSuccess()); +} + +TEST_CASE("LDN reports a missing connection reply and ignores late replies", "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + TestLANDiscovery station; + REQUIRE(station.GetConnectionTimeout() == 5s); + station.SetConnectionTimeout(25ms); + REQUIRE(station.Initialize([] {}).IsSuccess()); + REQUIRE(station.OpenStation().IsSuccess()); + NetworkInfo info{}; + info.ldn.node_count = 1; + info.ldn.nodes[0].ipv4_address = {1, 0, 168, 192}; + REQUIRE(station.Connect(info, {}, 1) == ResultAuthenticationTimeout); + REQUIRE(station.GetState() == State::StationOpened); + + Network::LDNPacket reply{}; + reply.type = Network::LDNPacketType::SyncNetwork; + reply.local_ip = {192, 168, 0, 1}; + reply.data.resize(sizeof(info)); + std::memcpy(reply.data.data(), &info, sizeof(info)); + station.ReceivePacket(reply); + REQUIRE(station.GetState() == State::StationOpened); + REQUIRE(station.Finalize().IsSuccess()); +} + +TEST_CASE("LDN creates fresh identities for recreated networks and separate hosts", "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + LANDiscovery host; + LANDiscovery other_host; + REQUIRE(host.Initialize([] {}).IsSuccess()); + REQUIRE(other_host.Initialize([] {}).IsSuccess()); + REQUIRE(host.OpenAccessPoint().IsSuccess()); + REQUIRE(other_host.OpenAccessPoint().IsSuccess()); + NetworkConfig config{}; + config.node_count_max = 2; + REQUIRE(host.CreateNetwork({}, {}, config).IsSuccess()); + NetworkInfo first{}; + REQUIRE(host.GetNetworkInfo(first).IsSuccess()); + + REQUIRE(host.DestroyNetwork().IsSuccess()); + REQUIRE(host.CreateNetwork({}, {}, config).IsSuccess()); + NetworkInfo recreated{}; + REQUIRE(host.GetNetworkInfo(recreated).IsSuccess()); + REQUIRE(recreated.network_id.session_id != first.network_id.session_id); + + REQUIRE(other_host.CreateNetwork({}, {}, config).IsSuccess()); + NetworkInfo other{}; + REQUIRE(other_host.GetNetworkInfo(other).IsSuccess()); + REQUIRE(other.network_id.session_id != first.network_id.session_id); + REQUIRE(other.network_id.session_id != recreated.network_id.session_id); + REQUIRE(host.Finalize().IsSuccess()); + REQUIRE(other_host.Finalize().IsSuccess()); +} -- cgit v1.2.3 From 9b56bf38cb92c42abd776d95bdd61f96bf9d1e9a Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 01:15:30 +0100 Subject: network: keep fcntl modes across MSG_DONTWAIT and decompress room packets once A receive with MSG_DONTWAIT restored the blocking mode from the descriptor copy taken before the call. When fcntl(SETFL, O_NONBLOCK) ran during the receive, the fd kept reporting O_NONBLOCK while its socket blocked again. Receives now switch the mode only for MSG_DONTWAIT and read the current flags under file_descriptors_mutex at both ends, and SETFL changes the mode and the stored flags in the same critical section. OnProxyPacketReceived decompressed each room packet once per matching socket while holding file_descriptors_mutex, so every bsd call waited for it. It now copies the distinct sockets under the lock, decompresses once without it and delivers the result. Also reject fd == MAX_FD in IsFileDescriptorValid and PollImpl, which read past the table, and guard the ProxySocket blocking and receive timeout setters with packets_mutex, matching their reader. Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/core/hle/service/sockets/bsd.cpp | 92 ++++++++++++++++++------------ src/core/hle/service/sockets/bsd.h | 2 + src/core/internal_network/socket_proxy.cpp | 23 ++------ src/core/internal_network/sockets.h | 1 + 4 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index 477f32626d..e6d23b7bf6 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -14,6 +14,7 @@ #include "common/logging.h" #include "common/socket_types.h" +#include "common/zstd_compression.h" #include "core/core.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/service/ipc_helpers.h" @@ -608,7 +609,7 @@ std::pair BSD_USA::PollImpl(std::vector& write_buffer, std::span PollFD& pollfd = fds[i]; ASSERT(False(pollfd.revents)); - if (pollfd.fd > static_cast(MAX_FD) || pollfd.fd < 0) { + if (pollfd.fd >= static_cast(MAX_FD) || pollfd.fd < 0) { LOG_ERROR(Service, "File descriptor handle={} is invalid", pollfd.fd); pollfd.revents = PollEvents{}; return {0, Errno::SUCCESS}; @@ -764,11 +765,12 @@ std::pair BSD_USA::FcntlImpl(s32 fd, FcntlCmd cmd, s32 arg) { return {descriptor->flags, Errno::SUCCESS}; case FcntlCmd::SETFL: { const bool enable = (arg & Network::FLAG_O_NONBLOCK) != 0; + // Set the mode and the flags together, so SetDontWait never sees one without the other + std::scoped_lock lock{file_descriptors_mutex}; const Errno bsd_errno = Translate(descriptor->socket->SetNonBlock(enable)); if (bsd_errno != Errno::SUCCESS) { return {-1, bsd_errno}; } - std::scoped_lock lock{file_descriptors_mutex}; if (auto& entry = file_descriptors[fd]; entry && entry->socket == descriptor->socket) { entry->flags = arg; } @@ -880,19 +882,14 @@ std::pair BSD_USA::RecvImpl(s32 fd, u32 flags, std::vector& mess // Apply flags using Network::FLAG_MSG_DONTWAIT; - using Network::FLAG_O_NONBLOCK; - if ((flags & FLAG_MSG_DONTWAIT) != 0) { - flags &= ~FLAG_MSG_DONTWAIT; - if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { - descriptor->socket->SetNonBlock(true); - } - } + const bool dont_wait = (flags & FLAG_MSG_DONTWAIT) != 0 && SetDontWait(fd, *descriptor, true); + flags &= ~FLAG_MSG_DONTWAIT; const auto [ret, bsd_errno] = Translate(descriptor->socket->Recv(flags, message)); // Restore original state - if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { - descriptor->socket->SetNonBlock(false); + if (dont_wait) { + SetDontWait(fd, *descriptor, false); } return {ret, bsd_errno}; @@ -916,20 +913,15 @@ std::pair BSD_USA::RecvFromImpl(s32 fd, u32 flags, std::vector& // Apply flags using Network::FLAG_MSG_DONTWAIT; - using Network::FLAG_O_NONBLOCK; - if ((flags & FLAG_MSG_DONTWAIT) != 0) { - flags &= ~FLAG_MSG_DONTWAIT; - if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { - descriptor->socket->SetNonBlock(true); - } - } + const bool dont_wait = (flags & FLAG_MSG_DONTWAIT) != 0 && SetDontWait(fd, *descriptor, true); + flags &= ~FLAG_MSG_DONTWAIT; const auto [ret, bsd_errno] = Translate(descriptor->socket->RecvFrom(flags, message, p_addr_in)); // Restore original state - if ((descriptor->flags & FLAG_O_NONBLOCK) == 0) { - descriptor->socket->SetNonBlock(false); + if (dont_wait) { + SetDontWait(fd, *descriptor, false); } if (p_addr_in) { @@ -1043,8 +1035,21 @@ s32 BSD_USA::FindFreeFileDescriptorHandle() noexcept { return -1; } +bool BSD_USA::SetDontWait(s32 fd, const FileDescriptor& descriptor, bool enable) { + // Read the current flags, not the copy: fcntl may have changed them during the receive + std::scoped_lock lock{file_descriptors_mutex}; + const auto& entry = file_descriptors[fd]; + const s32 current_flags = + entry && entry->socket == descriptor.socket ? entry->flags : descriptor.flags; + if ((current_flags & Network::FLAG_O_NONBLOCK) != 0) { + return false; + } + descriptor.socket->SetNonBlock(enable); + return true; +} + bool BSD_USA::IsFileDescriptorValid(s32 fd) const noexcept { - if (fd > static_cast(MAX_FD) || fd < 0) { + if (fd >= static_cast(MAX_FD) || fd < 0) { LOG_ERROR(Service, "Invalid file descriptor handle={}", fd); return false; } @@ -1064,22 +1069,37 @@ void BSD_USA::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const } void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) { - // Runs on the room thread. The lock keeps a socket alive while a bsd thread closes it. - std::scoped_lock lock{file_descriptors_mutex}; - // A duplicated fd shares its socket, which must still get each packet only once - std::array delivered{}; - size_t num_delivered = 0; - for (auto& optional_descriptor : file_descriptors) { - if (!optional_descriptor.has_value() || !optional_descriptor->socket) { - continue; - } - Network::SocketBase* const socket = optional_descriptor->socket.get(); - const auto delivered_end = delivered.begin() + num_delivered; - if (std::find(delivered.begin(), delivered_end, socket) != delivered_end) { - continue; + // Runs on the room thread. The copies keep each socket alive if a bsd thread closes it, so + // the bsd threads do not wait for the decompression below. + std::array, MAX_FD> sockets{}; + size_t num_sockets = 0; + { + std::scoped_lock lock{file_descriptors_mutex}; + for (const auto& descriptor : file_descriptors) { + if (!descriptor.has_value() || !descriptor->socket) { + continue; + } + // A duplicated fd shares its socket, which must still get each packet only once + const auto sockets_end = sockets.begin() + num_sockets; + if (std::find(sockets.begin(), sockets_end, descriptor->socket) == sockets_end) { + sockets[num_sockets++] = descriptor->socket; + } } - delivered[num_delivered++] = socket; - socket->HandleProxyPacket(packet); + } + if (num_sockets == 0) { + return; + } + + // Decompress once for all sockets that share the port + const Network::ProxyPacket decompressed{ + .local_endpoint = packet.local_endpoint, + .remote_endpoint = packet.remote_endpoint, + .protocol = packet.protocol, + .broadcast = packet.broadcast, + .data = Common::Compression::DecompressDataZSTD(packet.data), + }; + for (size_t i = 0; i < num_sockets; ++i) { + sockets[i]->HandleProxyPacket(decompressed); } } diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h index f908edc7d2..559830d7a1 100644 --- a/src/core/hle/service/sockets/bsd.h +++ b/src/core/hle/service/sockets/bsd.h @@ -180,6 +180,8 @@ private: bool IsFileDescriptorValid(s32 fd) const noexcept; /// Copies a descriptor under file_descriptors_mutex; nullopt if fd has no socket std::optional CopyDescriptor(s32 fd); + /// Switches a blocking socket for one MSG_DONTWAIT call; false if fd is already nonblocking + bool SetDontWait(s32 fd, const FileDescriptor& descriptor, bool enable); void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept; diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp index 1b93ce93e5..95f64b9bd0 100644 --- a/src/core/internal_network/socket_proxy.cpp +++ b/src/core/internal_network/socket_proxy.cpp @@ -43,22 +43,7 @@ void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) { LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode"); return; } - } - - // Decompress without the lock, so the game thread does not wait for it - ProxyPacket decompressed{ - .local_endpoint = packet.local_endpoint, - .remote_endpoint = packet.remote_endpoint, - .protocol = packet.protocol, - .broadcast = packet.broadcast, - .data = Common::Compression::DecompressDataZSTD(packet.data), - }; - { - std::lock_guard guard(packets_mutex); - if (closed) { - return; - } - received_packets.push(std::move(decompressed)); + received_packets.push(packet); } // A MSG_PEEK reader leaves the packet queued, so every waiter must see it packets_cv.notify_all(); @@ -304,11 +289,15 @@ Errno ProxySocket::SetSndTimeo(u32 value) { } Errno ProxySocket::SetRcvTimeo(u32 value) { - receive_timeout = value; + { + std::lock_guard guard(packets_mutex); + receive_timeout = value; + } return SetSockOpt(fd, SO_RCVTIMEO, static_cast(value)); } Errno ProxySocket::SetNonBlock(bool enable) { + std::lock_guard guard(packets_mutex); blocking = !enable; return Errno::SUCCESS; } diff --git a/src/core/internal_network/sockets.h b/src/core/internal_network/sockets.h index 0b751b6622..37cffd4651 100644 --- a/src/core/internal_network/sockets.h +++ b/src/core/internal_network/sockets.h @@ -89,6 +89,7 @@ public: virtual bool IsOpened() const = 0; + /// Queues a room packet whose data is already decompressed virtual void HandleProxyPacket(const ProxyPacket& packet) = 0; [[nodiscard]] SOCKET GetFD() const { -- cgit v1.2.3 From fdeecea52df593d6d2a1ce6e1977e3aab1bb9135 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 01:15:30 +0100 Subject: tests: queue decompressed packets in the room socket tests HandleProxyPacket now receives data that bsd has already decompressed. Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/tests/core/internal_network/socket_proxy.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/core/internal_network/socket_proxy.cpp b/src/tests/core/internal_network/socket_proxy.cpp index d97d7d5102..0ba5f2aa2b 100644 --- a/src/tests/core/internal_network/socket_proxy.cpp +++ b/src/tests/core/internal_network/socket_proxy.cpp @@ -8,7 +8,6 @@ #include #include "common/socket_types.h" -#include "common/zstd_compression.h" #include "core/internal_network/socket_proxy.h" namespace { @@ -33,7 +32,7 @@ void QueuePacket(Network::ProxySocket& socket, Network::Protocol protocol, .ip = {192, 168, 1, 1}, .portno = 35000}; packet.protocol = protocol; - packet.data = Common::Compression::CompressDataZSTDDefault(payload.data(), payload.size()); + packet.data.assign(payload.begin(), payload.end()); socket.HandleProxyPacket(packet); } -- cgit v1.2.3