From 010252ff2bf0e2a066c1a98c31612970b4b62071 Mon Sep 17 00:00:00 2001 From: NaySurGithub <96527285+NaySurGithub@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:56:50 +0100 Subject: [PATCH] fix(session): free retired sessions at the start of a frame, not mid-frame Connect() retired the current session and then freed every retired one right away. When it ran in the middle of a frame (a Discord join from UpdateDiscord, a transfer, a menu button), the rest of that frame still held the old session as f.live: UpdateChat locked its freed chat mutex and the client aborted with 'mutex lock failed'. Retired sessions are now freed in UpdateSession, before the frame takes its live session, so one always outlives the frame that retired it. --- src/client/Perlin.cpp | 3 --- src/client/PerlinFrame.cpp | 5 +++++ src/client/PerlinReplay.cpp | 3 --- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/client/Perlin.cpp b/src/client/Perlin.cpp index a102a8b5..db168ee7 100644 --- a/src/client/Perlin.cpp +++ b/src/client/Perlin.cpp @@ -242,9 +242,6 @@ namespace perlin { else if (!friendLease && versionOverride.protocol > 0 && versionOverride.server == options.server) session->SetProtocolOverride(versionOverride.protocol, versionOverride.version); session->SetRecordTap(replayCtl.NewSessionTap()); - if (connectsInFlight.load() == 0) - std::erase_if(retiredSessions, - [](const std::unique_ptr& s) { return !s->Transferring(); }); session->SetPacketHandler([this](uint32_t id, const std::vector& body) { if (!modWantPackets.load() || body.size() > 65536) return; diff --git a/src/client/PerlinFrame.cpp b/src/client/PerlinFrame.cpp index 813b5f8e..7427bee4 100644 --- a/src/client/PerlinFrame.cpp +++ b/src/client/PerlinFrame.cpp @@ -384,6 +384,11 @@ namespace perlin { camera.aspect = static_cast(width) / static_cast(height); PumpReplay(dt); + // retired sessions die here, before this frame takes `live`: Connect() can run mid-frame (a Discord + // join, a transfer, a button) while the rest of the frame still holds the old session + if (connectsInFlight.load() == 0) + std::erase_if(retiredSessions, + [](const std::unique_ptr& s) { return !s->Transferring(); }); live = sessionReady && session && !session->Transferring() ? session.get() : nullptr; if (live) { const physics::State& viewerState = player.State(); diff --git a/src/client/PerlinReplay.cpp b/src/client/PerlinReplay.cpp index 3c566671..faf9f58a 100644 --- a/src/client/PerlinReplay.cpp +++ b/src/client/PerlinReplay.cpp @@ -336,9 +336,6 @@ namespace perlin { : 1 << 20); session->SetPackDirectory(paths::DataDir() + "/server_packs"); sessionReady = true; - if (connectsInFlight.load() == 0) - std::erase_if(retiredSessions, - [](const std::unique_ptr& r) { return !r->Transferring(); }); } void Perlin::MountReplayPacks() {