fix(netctl): restore initialization and callback validation

This commit is contained in:
boykopovar committed 2026-10-21 21:51:41 +03:00
1 parent cd175c3aad
commit ea086b2374
5 files changed
+79 -20

No files matched your search

+6 -1
View File
@@ -1334,7 +1334,12 @@ struct NetEpollEvent {
NetEpollData data;
};
struct NetCtlNatInfo { std::uint8_t opaque[128]; };
struct NetCtlNatInfo {
std::uint32_t size;
std::uint8_t opaque[12];
};
static_assert(sizeof(NetCtlNatInfo) == 16);
union NetCtlInfo { std::uint8_t opaque[256]; };
+23 -11
View File
@@ -19,6 +19,7 @@
#include <sys/sysctl.h>
#endif
#endif
#include <atomic>
#include <cstdint>
#include <cstddef>
#include <cstdio>
@@ -45,6 +46,9 @@ struct NetCtlNetEvConfiguration;
struct NetBweInfo;
struct NetCtlWifiInfo;
static constexpr int SCE_NET_CTL_ERROR_NOT_INITIALIZED = static_cast<int>(0x80412101);
static constexpr int SCE_NET_CTL_ERROR_ID_NOT_FOUND = static_cast<int>(0x80412104);
static constexpr int SCE_NET_CTL_ERROR_INVALID_SIZE = static_cast<int>(0x80412111);
static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast<int>(0x80412103);
static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast<int>(0x80412105);
static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast<int>(0x80412106);
@@ -91,11 +95,13 @@ struct CallbackSlot {
int lastEvent = 0;
};
static std::atomic_bool g_initialized{false};
static std::mutex g_callbackLock;
static CallbackSlot g_callbacks[MAX_CALLBACKS] = {};
static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {};
static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR;
std::lock_guard lock(g_callbackLock);
for (int index = 0; index < MAX_CALLBACKS; ++index) {
@@ -109,8 +115,10 @@ static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCall
}
static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID;
std::lock_guard lock(g_callbackLock);
if (!callbacks[cid].func) return SCE_NET_CTL_ERROR_ID_NOT_FOUND;
callbacks[cid] = {};
return 0;
}
@@ -420,6 +428,7 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) {
extern "C" {
int APS5_VABI sceNetCtlCheckCallback(void) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!HasIpv4Callback()) return 0;
const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED;
CallbackSlot due[MAX_CALLBACKS];
@@ -437,6 +446,7 @@ int APS5_VABI sceNetCtlCheckCallback(void) {
}
int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!stat) return SCE_NET_CTL_ERROR_INVALID_ADDR;
if (!HostHasAddress(AF_UNSPEC)) return SCE_NET_CTL_ERROR_NOT_CONNECTED;
*stat = {};
@@ -445,6 +455,7 @@ int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) {
}
int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR;
if (code < NET_CTL_INFO_DEVICE || code > NET_CTL_INFO_HTTP_PROXY_PORT) return SCE_NET_CTL_ERROR_INVALID_CODE;
HostIpv4Interface host;
@@ -476,31 +487,35 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) {
return 0;
}
int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) {
if (!nat_info) return SCE_NET_CTL_ERROR_INVALID_ADDR;
return SCE_NET_CTL_ERROR_NOT_CONNECTED;
int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* info) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR;
if (info->size != sizeof(NetCtlNatInfo)) return SCE_NET_CTL_ERROR_INVALID_SIZE;
throw std::logic_error("sceNetCtlGetNatInfo: NAT discovery is not implemented");
}
int APS5_VABI sceNetCtlGetResult(int event_type, int* error_code) {
(void)event_type;
if (!error_code) return SCE_NET_CTL_ERROR_INVALID_ADDR;
*error_code = 0;
return 0;
int APS5_VABI sceNetCtlGetResult(int event, int* result) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!result) return SCE_NET_CTL_ERROR_INVALID_ADDR;
throw std::logic_error("sceNetCtlGetResult: service event results are not implemented");
}
int APS5_VABI sceNetCtlGetState(int* state) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!state) return SCE_NET_CTL_ERROR_INVALID_ADDR;
*state = HostHasAddress(AF_UNSPEC) ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED;
return 0;
}
int APS5_VABI sceNetCtlGetStateV6(int* state) {
if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED;
if (!state) return SCE_NET_CTL_ERROR_INVALID_ADDR;
*state = HostHasAddress(AF_INET6) ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED;
return 0;
}
int APS5_VABI sceNetCtlInit(void) {
g_initialized.store(true);
return 0;
}
@@ -520,9 +535,6 @@ int APS5_VABI sceNetCtlGetInfoV6(int code, void* info) {
}
void APS5_VABI sceNetCtlTerm(void) {
std::lock_guard lock(g_callbackLock);
for (auto& slot : g_callbacks) slot = {};
for (auto& slot : g_callbacksV6) slot = {};
}
int APS5_VABI sceNetCtlUnregisterCallback(int cid) {
+45 -5
View File
@@ -32,6 +32,7 @@ struct NetCtlDefaultRoute {
#endif
extern "C" {
int APS5_VABI sceNetCtlInit(void);
int APS5_VABI sceNetCtlCheckCallback(void);
int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*);
int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*);
@@ -141,13 +142,19 @@ void CheckCallbacks() {
Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1);
sceNetCtlTerm();
Require(sceNetCtlCheckCallback() == 0);
Require(g_first.calls == 1 && g_second.calls == 1);
Require(g_first.calls == 1 && g_second.calls == 2);
Require(sceNetCtlInit() == 0);
Require(sceNetCtlUnregisterCallback(first) == 0);
Require(sceNetCtlUnregisterCallback(second) == 0);
Require(sceNetCtlUnregisterCallbackV6(v6) == 0);
Require(sceNetCtlUnregisterCallback(first) == static_cast<int>(0x80412104));
Require(sceNetCtlUnregisterCallbackV6(v6) == static_cast<int>(0x80412104));
Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0);
Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0);
Require(sceNetCtlCheckCallback() == 0);
Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe);
Require(g_second.calls == 1 && g_self.calls == 1 && g_v6.calls == 0);
Require(g_second.calls == 2 && g_self.calls == 1 && g_v6.calls == 0);
Require(sceNetCtlUnregisterCallback(first) == 0);
Require(sceNetCtlUnregisterCallbackV6(v6) == 0);
}
@@ -263,6 +270,20 @@ int main() {
#ifdef __APPLE__
CheckDefaultRoutes();
#endif
constexpr int notInitialized = static_cast<int>(0x80412101);
Require(sceNetCtlCheckCallback() == notInitialized);
Require(sceNetCtlGetState(nullptr) == notInitialized);
Require(sceNetCtlGetStateV6(nullptr) == notInitialized);
Require(sceNetCtlGetInfo(0, nullptr) == notInitialized);
Require(sceNetCtlGetResult(0, nullptr) == notInitialized);
Require(sceNetCtlGetNatInfo(nullptr) == notInitialized);
Require(sceNetCtlRegisterCallback(nullptr, nullptr, nullptr) == notInitialized);
Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == notInitialized);
Require(sceNetCtlUnregisterCallback(-1) == notInitialized);
Require(sceNetCtlUnregisterCallbackV6(-1) == notInitialized);
sceNetCtlTerm();
Require(sceNetCtlCheckCallback() == notInitialized);
Require(sceNetCtlInit() == 0);
CheckCallbacks();
Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr);
@@ -271,7 +292,13 @@ int main() {
Require(stateV6 == 0 || stateV6 == kStateIpObtained);
Require(sceNetCtlGetResult(0, nullptr) == kErrInvalidAddr);
int errorCode = -1;
Require(sceNetCtlGetResult(0, &errorCode) == 0 && errorCode == 0);
bool resultUnsupported = false;
try {
sceNetCtlGetResult(0, &errorCode);
} catch (const std::logic_error&) {
resultUnsupported = true;
}
Require(resultUnsupported && errorCode == -1);
Require(sceNetCtlGetResultV6(kEventIpObtained, nullptr) == kErrInvalidAddr);
for (const int eventType : {kEventDisconnected, 2, kEventIpObtained}) {
int errorCodeV6 = -1;
@@ -335,6 +362,19 @@ int main() {
}
Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr);
NetCtlNatInfo nat_info{};
Require(sceNetCtlGetNatInfo(&nat_info) == kErrNotConnected);
NetCtlNatInfo natInfo{};
for (const auto size : {0u, 15u, 17u, 128u, 0xffffffffu}) {
natInfo.size = size;
Require(sceNetCtlGetNatInfo(&natInfo) == static_cast<int>(0x80412111));
}
natInfo.size = sizeof(natInfo);
std::memset(natInfo.opaque, kFill, sizeof(natInfo.opaque));
bool natUnsupported = false;
try {
sceNetCtlGetNatInfo(&natInfo);
} catch (const std::logic_error&) {
natUnsupported = true;
}
Require(natUnsupported && natInfo.size == sizeof(natInfo));
for (const auto value : natInfo.opaque) Require(value == kFill);
}
+2
View File
@@ -75,6 +75,7 @@ int APS5_VABI sceNetResolverDestroy(int);
int APS5_VABI sceNetResolverGetError(int, int*);
int APS5_VABI sceNetClearDnsCache(int);
int APS5_VABI sceNetGetDnsInfo(std::uint32_t*, int);
int APS5_VABI sceNetCtlInit(void);
int APS5_VABI sceNetCtlGetState(int*);
int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*);
int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*);
@@ -1874,6 +1875,7 @@ int main() {
if (!ipv6Unavailable) CheckStreamSender(28);
int state = -1;
Require(sceNetCtlInit() == 0);
Require(sceNetCtlGetState(&state) == 0);
Require(state == 0 || state == 3);
Require(sceNetCtlGetIfStat(nullptr) == static_cast<int>(0x80412107));
+3 -3
View File
@@ -1,8 +1,8 @@
# libSceNetCtl technical debt
### Silent stubs
### Functional
- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `sceNetCtlGetNatInfo` validates null pointers and returns `SCE_NET_CTL_ERROR_NOT_CONNECTED` (`0x80412108`) without performing NAT discovery.
- [sceNetCtlGetNatInfo and sceNetCtlGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) - NAT discovery and service event results are not implemented and throw after native initialization and argument checks. NAT information is 16 bytes with a 32-bit size prefix; the remaining fields are unresolved.
### Unknown function info
@@ -25,6 +25,6 @@
### Functional
- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` or `sceNetCtlTerm` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event, because no source says which call delivers them
- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event: native `sceNetCtlCheckCallback` also drains the IPv6 service queue, which the host backend does not model
- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one
- [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB