aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-11-03 01:39:54 +0100
committercirakg <cirakg@cirakg.xyz>2026-11-03 01:39:54 +0100
commit0b927d171c6d918a675c25f57b2bc0121b7f889d (patch)
tree3f3e8ffd7106f05865ee1217f6cbaa68670e229a /src
parent70934ece3d2253c7579cf8d71b13e3a0547070e2 (diff)
network: close each bsd fd once and drop unwanted or malformed room packets
Two threads closing the same fd both copied the descriptor and both called Close(), so a host socket could be closed twice, and the second closesocket could hit an unrelated socket that had reused the host fd. CloseImpl now frees the slot under file_descriptors_mutex first and only that thread closes the socket; the other gets EBADF. OnProxyPacketReceived decompressed every room packet when any fd had a socket, including native sockets and proxy sockets on other ports. Sockets now report through AcceptsProxyPacket whether they want a packet, and only those are collected, so an unwanted packet is not decompressed. DecompressDataZSTD passed the frame's declared size straight to the vector constructor, so a malformed or unsized frame from a room member threw on the room thread. The new TryDecompressDataZSTD rejects invalid, unsized and oversized frames, and bsd drops such packets with a warning. DecompressDataZSTD now returns an empty vector for them instead of throwing. Assisted-by: Claude Opus 5.5 (investigation and implementation)
Diffstat (limited to 'src')
-rw-r--r--src/common/zstd_compression.cpp20
-rw-r--r--src/common/zstd_compression.h13
-rw-r--r--src/core/hle/service/sockets/bsd.cpp46
-rw-r--r--src/core/hle/service/sockets/bsd.h2
-rw-r--r--src/core/internal_network/network.cpp4
-rw-r--r--src/core/internal_network/socket_proxy.cpp28
-rw-r--r--src/core/internal_network/socket_proxy.h5
-rw-r--r--src/core/internal_network/sockets.h5
8 files changed, 91 insertions, 32 deletions
diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp
index 7665482ee6..9bb8d7b919 100644
--- a/src/common/zstd_compression.cpp
+++ b/src/common/zstd_compression.cpp
@@ -35,16 +35,26 @@ std::vector<u8> CompressDataZSTDDefault(const u8* source, std::size_t source_siz
}
std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed) {
- const std::size_t decompressed_size =
+ return TryDecompressDataZSTD(compressed, std::vector<u8>{}.max_size())
+ .value_or(std::vector<u8>{});
+}
+
+std::optional<std::vector<u8>> TryDecompressDataZSTD(std::span<const u8> compressed,
+ std::size_t max_size) {
+ // The size comes from the frame header, so check it before allocating
+ const unsigned long long decompressed_size =
ZSTD_getFrameContentSize(compressed.data(), compressed.size());
- std::vector<u8> decompressed(decompressed_size);
+ if (decompressed_size == ZSTD_CONTENTSIZE_ERROR ||
+ decompressed_size == ZSTD_CONTENTSIZE_UNKNOWN || decompressed_size > max_size) {
+ return std::nullopt;
+ }
+ std::vector<u8> decompressed(static_cast<std::size_t>(decompressed_size));
const std::size_t uncompressed_result_size = ZSTD_decompress(
decompressed.data(), decompressed.size(), compressed.data(), compressed.size());
- if (decompressed_size != uncompressed_result_size || ZSTD_isError(uncompressed_result_size)) {
- // Decompression failed
- return {};
+ if (ZSTD_isError(uncompressed_result_size) || uncompressed_result_size != decompressed_size) {
+ return std::nullopt;
}
return decompressed;
}
diff --git a/src/common/zstd_compression.h b/src/common/zstd_compression.h
index 819d7d201a..c20d96adb0 100644
--- a/src/common/zstd_compression.h
+++ b/src/common/zstd_compression.h
@@ -6,6 +6,7 @@
#pragma once
+#include <optional>
#include <span>
#include <vector>
@@ -46,6 +47,18 @@ namespace Common::Compression {
[[nodiscard]] std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed);
/**
+ * Decompresses a single Zstandard frame that declares its content size.
+ *
+ * @param compressed the compressed source memory region.
+ * @param max_size the largest content size to accept.
+ *
+ * @return the decompressed data, or nullopt if the frame is invalid, does not declare its size,
+ * or declares more than max_size bytes.
+ */
+[[nodiscard]] std::optional<std::vector<u8>> TryDecompressDataZSTD(std::span<const u8> compressed,
+ std::size_t max_size);
+
+/**
* Decompresses a single Zstandard frame into a caller-provided buffer of known size.
*
* @param compressed the compressed source memory region.
diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp
index e6d23b7bf6..730a7edcc1 100644
--- a/src/core/hle/service/sockets/bsd.cpp
+++ b/src/core/hle/service/sockets/bsd.cpp
@@ -965,24 +965,24 @@ std::pair<s32, Errno> BSD_USA::SendToImpl(s32 fd, u32 flags, std::span<const u8>
}
Errno BSD_USA::CloseImpl(s32 fd) {
- const auto descriptor = CopyDescriptor(fd);
- if (!descriptor) {
- return Errno::BADF;
- }
-
- const Errno bsd_errno = Translate(descriptor->socket->Close());
- if (bsd_errno != Errno::SUCCESS) {
- return bsd_errno;
+ std::shared_ptr<Network::SocketBase> socket;
+ {
+ // Free the slot first, so of two threads closing the same fd only one closes the socket.
+ // Like POSIX close(), the fd is released even if closing the socket fails.
+ std::scoped_lock lock{file_descriptors_mutex};
+ if (!IsFileDescriptorValid(fd)) {
+ return Errno::BADF;
+ }
+ if (!file_descriptors[fd]->socket) {
+ LOG_WARNING(Service, "Uninitialized socket");
+ return Errno::BADF;
+ }
+ socket = std::move(file_descriptors[fd]->socket);
+ file_descriptors[fd].reset();
}
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};
- if (file_descriptors[fd] && file_descriptors[fd]->socket == descriptor->socket) {
- file_descriptors[fd].reset();
- }
- return bsd_errno;
+ return Translate(socket->Close());
}
std::variant<s32, Errno> BSD_USA::DuplicateSocketImpl(s32 fd) {
@@ -1070,7 +1070,8 @@ void BSD_USA::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const
void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
// 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.
+ // the bsd threads do not wait for the decompression below. Only sockets that accept the
+ // packet are collected, so an unwanted packet is never decompressed.
std::array<std::shared_ptr<Network::SocketBase>, MAX_FD> sockets{};
size_t num_sockets = 0;
{
@@ -1081,7 +1082,8 @@ void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
}
// 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) {
+ if (std::find(sockets.begin(), sockets_end, descriptor->socket) == sockets_end &&
+ descriptor->socket->AcceptsProxyPacket(packet)) {
sockets[num_sockets++] = descriptor->socket;
}
}
@@ -1090,13 +1092,19 @@ void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
return;
}
- // Decompress once for all sockets that share the port
+ // Decompress once for all sockets that share the port. The room does not check the frame, so
+ // drop a malformed or oversized one instead of throwing on the room thread.
+ auto data = Common::Compression::TryDecompressDataZSTD(packet.data, MAX_ROOM_PACKET_SIZE);
+ if (!data) {
+ LOG_WARNING(Service, "Dropped a room packet with an invalid or oversized payload");
+ return;
+ }
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),
+ .data = std::move(*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 559830d7a1..bff80d7c00 100644
--- a/src/core/hle/service/sockets/bsd.h
+++ b/src/core/hle/service/sockets/bsd.h
@@ -44,6 +44,8 @@ public:
private:
/// Maximum number of file descriptors
static constexpr size_t MAX_FD = 128;
+ /// Largest decompressed room packet. A UDP datagram cannot be larger.
+ static constexpr size_t MAX_ROOM_PACKET_SIZE = 0x10000;
struct FileDescriptor {
std::shared_ptr<Network::SocketBase> socket;
diff --git a/src/core/internal_network/network.cpp b/src/core/internal_network/network.cpp
index d1c436aa08..806562076d 100644
--- a/src/core/internal_network/network.cpp
+++ b/src/core/internal_network/network.cpp
@@ -1095,6 +1095,10 @@ bool Socket::IsOpened() const {
return fd != INVALID_SOCKET;
}
+bool Socket::AcceptsProxyPacket(const ProxyPacket& packet) {
+ return false;
+}
+
void Socket::HandleProxyPacket(const ProxyPacket& packet) {
LOG_WARNING(Network, "ProxyPacket received, but not in Proxy mode!");
}
diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp
index 95f64b9bd0..f12891e8f5 100644
--- a/src/core/internal_network/socket_proxy.cpp
+++ b/src/core/internal_network/socket_proxy.cpp
@@ -30,17 +30,29 @@ ProxySocket::~ProxySocket() {
fd = INVALID_SOCKET;
}
+bool ProxySocket::MatchesPacket(const ProxyPacket& packet) const {
+ if (protocol != packet.protocol || local_endpoint.portno != packet.remote_endpoint.portno ||
+ closed) {
+ return false;
+ }
+ if (!broadcast && packet.broadcast) {
+ LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode");
+ return false;
+ }
+ return true;
+}
+
+bool ProxySocket::AcceptsProxyPacket(const ProxyPacket& packet) {
+ std::lock_guard guard(packets_mutex);
+ return MatchesPacket(packet);
+}
+
void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) {
{
- // Runs on the room thread, while the game thread may bind, close or reconfigure the socket
+ // Runs on the room thread, while the game thread may bind, close or reconfigure the socket.
+ // Check again: the socket may have changed since AcceptsProxyPacket.
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");
+ if (!MatchesPacket(packet)) {
return;
}
received_packets.push(packet);
diff --git a/src/core/internal_network/socket_proxy.h b/src/core/internal_network/socket_proxy.h
index 5d6e35693c..078baa7806 100644
--- a/src/core/internal_network/socket_proxy.h
+++ b/src/core/internal_network/socket_proxy.h
@@ -22,6 +22,8 @@ public:
explicit ProxySocket() noexcept;
~ProxySocket() override;
+ bool AcceptsProxyPacket(const ProxyPacket& packet) override;
+
void HandleProxyPacket(const ProxyPacket& packet) override;
Errno Initialize(Domain domain, Type type, Protocol socket_protocol) override;
@@ -82,6 +84,9 @@ public:
bool IsOpened() const override;
private:
+ /// Expects packets_mutex to be held
+ bool MatchesPacket(const ProxyPacket& packet) const;
+
bool broadcast = false;
bool closed = false;
u32 send_timeout = 0;
diff --git a/src/core/internal_network/sockets.h b/src/core/internal_network/sockets.h
index 37cffd4651..29b5b0f18a 100644
--- a/src/core/internal_network/sockets.h
+++ b/src/core/internal_network/sockets.h
@@ -89,6 +89,9 @@ public:
virtual bool IsOpened() const = 0;
+ /// Whether HandleProxyPacket would queue the packet; checked before decompressing it
+ virtual bool AcceptsProxyPacket(const ProxyPacket& packet) = 0;
+
/// Queues a room packet whose data is already decompressed
virtual void HandleProxyPacket(const ProxyPacket& packet) = 0;
@@ -164,6 +167,8 @@ public:
bool IsOpened() const override;
+ bool AcceptsProxyPacket(const ProxyPacket& packet) override;
+
void HandleProxyPacket(const ProxyPacket& packet) override;
private: