From 46338a981c68af0018a4abc7fd5f2e8a21ef6c01 Mon Sep 17 00:00:00 2001 From: bubblegg <86165363+bubblegg@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:30:03 +0000 Subject: [PATCH 1/4] feat(libSceNetCtl): add sceNetCtlGetIfStat Co-Authored-By: Claude Opus 5.5 --- core/libs/SceTypes.hpp | 7 +++++ core/libs/prx/libSceNetCtl/Export.cpp | 38 +++++++++++++++++++++++++++ core/libs/tests/GuestSceNet.cpp | 13 +++++++++ 3 files changed, 58 insertions(+) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 95c4f4207..374b75270 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1259,6 +1259,13 @@ struct NetCtlNatInfo { std::uint8_t opaque[128]; }; union NetCtlInfo { std::uint8_t opaque[256]; }; +struct NetCtlIfStat { + std::uint32_t device; + std::uint64_t tx_bytes; + std::uint64_t rx_bytes; + std::uint32_t reserved[8]; +}; + using NetCtlCallback = void (*)(int, void*); struct HttpEpoll; diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 4bae50760..8962e4f6f 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -5,14 +5,17 @@ #include #else #include +#include #include #include +#include #endif #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #ifdef _WIN32 #include #endif @@ -78,12 +81,47 @@ static bool HostHasAddress(int family) { #endif } +static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { + sent = 0; + received = 0; +#ifdef _WIN32 + MIB_IF_TABLE2* table = nullptr; + if (GetIfTable2(&table) != NO_ERROR) throw std::runtime_error("sceNetCtlGetIfStat: the host interface table is unavailable"); + for (ULONG index = 0; index < table->NumEntries; ++index) { + const auto& row = table->Table[index]; + if (row.OperStatus != IfOperStatusUp || row.Type == IF_TYPE_SOFTWARE_LOOPBACK || !row.InterfaceAndOperStatusFlags.HardwareInterface) continue; + sent += row.OutOctets; + received += row.InOctets; + } + FreeMibTable(table); +#else + ifaddrs* interfaces = nullptr; + if (getifaddrs(&interfaces) != 0) throw std::runtime_error("sceNetCtlGetIfStat: the host interface list is unavailable"); + for (auto* item = interfaces; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_PACKET || !item->ifa_data) continue; + if (!(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + const auto* stats = static_cast(item->ifa_data); + sent += stats->tx_bytes; + received += stats->rx_bytes; + } + freeifaddrs(interfaces); +#endif +} + extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { return 0; } +int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { + if (!stat) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (!HostHasAddress(AF_UNSPEC)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + *stat = {}; + HostTraffic(stat->tx_bytes, stat->rx_bytes); + return 0; +} + int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { (void)code; (void)info; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 2ad9a8a60..eab87d58b 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -57,6 +57,7 @@ int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); @@ -764,6 +765,18 @@ int main() { int state = -1; Require(sceNetCtlGetState(&state) == 0); Require(state == 0 || state == 3); + Require(sceNetCtlGetIfStat(nullptr) == static_cast(0x80412107)); + NetCtlIfStat first{}; + first.device = 7; + first.reserved[7] = 7; + const int statResult = sceNetCtlGetIfStat(&first); + Require(statResult == (state == 3 ? 0 : static_cast(0x80412108))); + if (statResult == 0) { + Require(first.device == 0 && first.reserved[7] == 0); + NetCtlIfStat second{}; + Require(sceNetCtlGetIfStat(&second) == 0); + Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); + } if (ipv6Unavailable) { std::puts("skipped the IPv6 socket checks, the host has no usable IPv6 loopback"); return 77; From ec07f5a5ecb6eff976d3bb34ad878672407bb02f Mon Sep 17 00:00:00 2001 From: bubblegg <86165363+bubblegg@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:21:33 +0000 Subject: [PATCH 2/4] fix(libSceNetCtl): keep the Linux interface counters to Linux Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNetCtl/Export.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 8962e4f6f..592ab29a8 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -5,11 +5,13 @@ #include #else #include -#include #include #include +#ifdef __linux__ +#include #include #endif +#endif #include #include #include "SceTypes.hpp" @@ -94,7 +96,7 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { received += row.InOctets; } FreeMibTable(table); -#else +#elif defined(__linux__) ifaddrs* interfaces = nullptr; if (getifaddrs(&interfaces) != 0) throw std::runtime_error("sceNetCtlGetIfStat: the host interface list is unavailable"); for (auto* item = interfaces; item; item = item->ifa_next) { @@ -105,6 +107,8 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { received += stats->rx_bytes; } freeifaddrs(interfaces); +#else + throw std::runtime_error("sceNetCtlGetIfStat: host interface byte counters are not implemented on this platform"); #endif } From a2e97d25a6082a5db9f7bd6c3bb7c1e66fc82692 Mon Sep 17 00:00:00 2001 From: bubblegg <86165363+bubblegg@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:25:54 +0000 Subject: [PATCH 3/4] fix(libSceNetCtl): read 64-bit interface byte counters on Linux Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNetCtl/Export.cpp | 18 +++++++++++++----- core/libs/tests/GuestSceNet.cpp | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 592ab29a8..5f0ab11e2 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -8,7 +8,6 @@ #include #include #ifdef __linux__ -#include #include #endif #endif @@ -16,8 +15,10 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include #include #include +#include #ifdef _WIN32 #include #endif @@ -83,6 +84,13 @@ static bool HostHasAddress(int family) { #endif } +extern "C" std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char* path) { + std::ifstream file(path); + std::uint64_t value = 0; + if (!(file >> value)) throw std::runtime_error(std::string("sceNetCtlGetIfStat: no byte counter in ") + path); + return value; +} + static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { sent = 0; received = 0; @@ -100,11 +108,11 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { ifaddrs* interfaces = nullptr; if (getifaddrs(&interfaces) != 0) throw std::runtime_error("sceNetCtlGetIfStat: the host interface list is unavailable"); for (auto* item = interfaces; item; item = item->ifa_next) { - if (!item->ifa_addr || item->ifa_addr->sa_family != AF_PACKET || !item->ifa_data) continue; + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_PACKET) continue; if (!(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; - const auto* stats = static_cast(item->ifa_data); - sent += stats->tx_bytes; - received += stats->rx_bytes; + const std::string statistics = std::string("/sys/class/net/") + item->ifa_name + "/statistics/"; + sent += NetCtlReadByteCounter_nid_no_patch((statistics + "tx_bytes").c_str()); + received += NetCtlReadByteCounter_nid_no_patch((statistics + "rx_bytes").c_str()); } freeifaddrs(interfaces); #else diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index eab87d58b..712ef8d36 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -58,6 +58,7 @@ int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); +std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); @@ -766,6 +767,22 @@ int main() { Require(sceNetCtlGetState(&state) == 0); Require(state == 0 || state == 3); Require(sceNetCtlGetIfStat(nullptr) == static_cast(0x80412107)); + { + const char* counterPath = "guest_sce_net_byte_counter.txt"; + std::FILE* counter = std::fopen(counterPath, "w"); + Require(counter != nullptr); + Require(std::fputs("1099511627776", counter) >= 0); + Require(std::fclose(counter) == 0); + Require(NetCtlReadByteCounter_nid_no_patch(counterPath) == 1099511627776ull); + Require(std::remove(counterPath) == 0); + bool missing = false; + try { + NetCtlReadByteCounter_nid_no_patch(counterPath); + } catch (const std::runtime_error&) { + missing = true; + } + Require(missing); + } NetCtlIfStat first{}; first.device = 7; first.reserved[7] = 7; From 53fc059af70f81adcee267ae4507a677909bb198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 23:13:48 +0200 Subject: [PATCH 4/4] docs(technical-debt): record sceNetCtlGetIfStat assumptions Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3a1bae53f..0c250def6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -21,6 +21,7 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info - AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures +- `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures - Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters