SaveState: Fixes from a review of the network modules' savestates

- sceNet kept the current session's "inited" flags across a load, while
  sceNetAdhoc takes them from the state. A state saved after
  sceNetApctlInit loaded into a fresh boot had no apctl, and an earlier
  state loaded mid-session found apctl already inited with no thread to
  deliver its events. They now come from the state, and sceNetInet's host
  sockets are closed on load instead of leaking ports.
- Adhoc matching contexts aren't saved, but survived a load while their
  sockets were deleted under their host threads. They're deleted first now,
  leaving their fake PSP threads (ids from before the load) alone.
- sceHttp had no state at all. Its inited flags are saved now, and its
  objects, which hold host connections, are dropped on load.
- The achievements data size from the file was skipped unchecked, which can
  wrap the pointer on 32-bit, and a MemSlabMap slab count could size a huge
  allocation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-20 00:20:24 +02:00
1 parent 30cab94840
commit e3bb5f1ada
10 files changed
+80 -20

No files matched your search

+4 -2
View File
@@ -209,8 +209,10 @@ void MemSlabMap::DoState(PointerWrap &p) {
Slab *old = first_;
Slab *oldBulk = bulkStorage_;
Do(p, count);
// Every slab takes up at least one byte.
if (count < 1 || !p.CheckRead(count)) {
// Every slab takes up at least 30 bytes in the file (even the oldest version: its section
// version, fields and tag), so a count from a broken file can't size a huge allocation: each
// takes over 150 in memory.
if (count < 1 || !p.CheckRead((size_t)count * 30)) {
p.SetError(p.ERROR_FAILURE);
count = 1;
}
+10
View File
@@ -105,6 +105,16 @@ SOCKET SocketManager::GetHostSocketFromInetSocket(int sock) {
return inetSockets_[sock].sock;
}
void SocketManager::CloseAll(SocketState state) {
for (auto &sock : inetSockets_) {
if (sock.state == state) {
closesocket(sock.sock);
sock.state = SocketState::Unused;
sock.sock = 0;
}
}
}
void SocketManager::CloseAll() {
for (auto &sock : inetSockets_) {
if (sock.state != SocketState::Unused) {
+2
View File
@@ -42,6 +42,8 @@ public:
SOCKET GetHostSocketFromInetSocket(int sock);
bool Close(InetSocket *inetSocket);
void CloseAll();
// Only the sockets in this state (sceNetInet's, or proAdhoc's).
void CloseAll(SocketState state);
// For debugger
const InetSocket *Sockets() {
+25
View File
@@ -29,6 +29,8 @@
#include "Core/HLE/sceHttp.h"
#include "Core/HLE/sceNet.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/StringUtils.h"
#include "Common/LogReporting.h"
#include "Common/Net/URL.h"
@@ -308,6 +310,29 @@ void __HttpShutdown() {
httpObjects.clear();
}
void __HttpDoState(PointerWrap &p) {
// Older states don't have it.
auto s = p.Section("sceHttp", 0, 1);
if (!s)
return;
// The game remembers whether it initialized the library.
Do(p, httpInited);
Do(p, httpsInited);
Do(p, httpCacheInited);
if (p.mode == PointerWrap::MODE_READ) {
// Templates, connections and requests hold host connections, which can't be restored. The
// game's ids for them now fail as invalid, rather than reach objects from before the load.
std::lock_guard<std::mutex> guard(httpLock);
for (const auto &it : httpObjects) {
if (it->className() == name_HTTPRequest)
(static_cast<HTTPRequest *>(it.get()))->abortRequest();
}
httpObjects.clear();
httpObjects.reserve(1);
}
}
// id: ID of the template or connection
int sceHttpSetResolveRetry(int id, int retryCount) {
if (id <= 0 || id > (int)httpObjects.size())
+1
View File
@@ -287,6 +287,7 @@ public:
void __HttpInit();
void __HttpDoState(PointerWrap &p);
void __HttpShutdown();
void Register_sceHttp();
+1
View File
@@ -322,6 +322,7 @@ void __KernelDoState(PointerWrap &p)
__DmacDoState(p);
__UsbCamDoState(p);
__NpDrmDoState(p);
__HttpDoState(p);
// IMPORTANT! Add new sections last!
}
+7 -8
View File
@@ -56,6 +56,7 @@
#include "Core/HLE/sceNp.h"
#include "Core/HLE/sceNp2.h"
#include "Core/HLE/sceNetInet.h"
#include "Core/HLE/SocketManager.h"
#include "Core/HLE/sceNetResolver.h"
#include "Core/HLE/NetAdhocCommon.h"
@@ -712,10 +713,6 @@ void __NetDoState(PointerWrap &p) {
if (!s)
return;
auto cur_netInited = g_netInited;
auto cur_netInetInited = netInetInited;
auto cur_netApctlInited = g_netApctlInited;
Do(p, g_netInited);
Do(p, netInetInited);
Do(p, g_netApctlInited);
@@ -778,10 +775,12 @@ void __NetDoState(PointerWrap &p) {
}
if (p.mode == p.MODE_READ) {
// Let's not change "Inited" value when Loading SaveState in the middle of multiplayer to prevent memory & port leaks
g_netApctlInited = cur_netApctlInited;
netInetInited = cur_netInetInited;
g_netInited = cur_netInited;
// The game remembers which libraries it initialized, so the "Inited" flags come from the state
// (sceNetAdhoc does the same). Keeping the current ones left a state saved after
// sceNetApctlInit, loaded into a fresh boot, without apctl, and an earlier state loaded
// mid-session with it already inited but no apctl thread. Host sockets can't be restored:
// close them, rather than leak ports the loaded game may bind again.
g_socketManager.CloseAll(SocketState::UsedNetInet);
// Discard created resolvers for now (since i'm not sure whether the information in the struct is sufficient or not, and we don't support multi-threading yet anyway)
__NetResolverShutdown();
+7
View File
@@ -333,6 +333,7 @@ void discardMatchingEvents();
void DoNetAdhocMatchingInited(PointerWrap &p);
void DoNetAdhocMatchingThreads(PointerWrap &p);
void ZeroNetAdhocMatchingThreads();
void NetAdhocMatching_DeleteAllBeforeLoad();
bool __NetAdhocConnected() {
return netAdhocInited && netAdhocctlInited && (adhocctlState == ADHOCCTL_STATE_CONNECTED || adhocctlState == ADHOCCTL_STATE_GAMEMODE);
@@ -1868,6 +1869,12 @@ void __NetAdhocDoState(PointerWrap &p) {
auto cur_netAdhocInited = netAdhocInited;
auto cur_netAdhocctlInited = netAdhocctlInited;
if (p.mode == p.MODE_READ) {
// Matching contexts aren't saved, and their host threads use sockets deleted below. Before
// the matching thread ids are loaded over.
NetAdhocMatching_DeleteAllBeforeLoad();
}
Do(p, netAdhocInited);
Do(p, netAdhocctlInited);
DoNetAdhocMatchingInited(p);
+19 -10
View File
@@ -1708,24 +1708,33 @@ int sceNetAdhocMatchingInit(u32 memsize) {
return 0;
}
static void DeleteAllMatchingContexts() {
SceNetAdhocMatchingContext *next = NULL;
SceNetAdhocMatchingContext *context = contexts;
while (context != NULL) {
next = context->next;
NetAdhocMatching_Delete(context->id);
context = next;
}
contexts = NULL;
}
int NetAdhocMatching_Term() {
if (netAdhocMatchingInited) {
// Delete all Matching contexts
SceNetAdhocMatchingContext* next = NULL;
SceNetAdhocMatchingContext* context = contexts;
while (context != NULL) {
next = context->next;
//if (context->running) NetAdhocMatching_Stop(context->id);
NetAdhocMatching_Delete(context->id);
context = next;
}
contexts = NULL;
DeleteAllMatchingContexts();
matchingThreads.clear();
}
return 0;
}
void NetAdhocMatching_DeleteAllBeforeLoad() {
// Their fake PSP threads' ids are from the kernel before the load, and the kernel is the loaded
// one now: zeroed, stopping a context leaves the threads alone, and only the host side goes.
ZeroNetAdhocMatchingThreads();
DeleteAllMatchingContexts();
}
int sceNetAdhocMatchingTerm() {
WARN_LOG(Log::sceNet, "UNTESTED sceNetAdhocMatchingTerm() at %08x", currentMIPS->pc);
// Should we cleanup all created matching contexts first? just in case there are games that doesn't delete them before calling this
+4
View File
@@ -998,6 +998,10 @@ void DoState(PointerWrap &p) {
Do(p, data_size);
if (p.mode == PointerWrap::MODE_READ) {
WARN_LOG(Log::Achievements, "Save state contained achievement data, but achievements are not active. Ignore.");
// The size is straight from the file: skipping it unchecked can wrap the pointer on 32-bit.
if (!p.CheckRead(data_size)) {
return;
}
}
p.SkipBytes(data_size);
return;