Skip to content

fix(session): free retired sessions at the start of a frame, not mid-frame - #73

Merged
notchyves merged 1 commit into
theselauraproject:mainfrom
NaySurGithub:fix/retired-session-lifetime
Nov 6, 2026
Merged

notchyves merged 1 commit into
theselauraproject:mainfrom
NaySurGithub:fix/retired-session-lifetime

Conversation

@NaySurGithub

Copy link
Copy Markdown
Contributor

Problem

Joining a server from Discord while already connected crashes the client:

libc++abi: terminating due to uncaught exception of type std::system_error: mutex lock failed: Invalid argument

The macOS crash report (EXC_CRASH / SIGABRT, render thread) stops in:

std::mutex::lock()
perlin::Perlin::FrameStep() + 18708
perlin::Perlin::RenderThreadMain()

The run log right before it shows [discord] joining nethernet.playhive.com:19132 from Discord.

Cause

In the release binary, FrameStep + 18708 is mutex::lock(live + 0xA940), right after PollJavaPack(live). That is the inlined UpdateChat → live->TakeChat(), where live is the frame's f.live session.

FrameStep runs UpdateSession(f) (which sets f.live), then UpdateDiscord(), then UpdateViewSettings and UpdateChat. A Discord join goes UpdateDiscord → HandleDiscordJoin → JoinServer → Connect(), in the middle of the frame. Connect() moved the current session into retiredSessions and then immediately ran:

if (connectsInFlight.load() == 0)
    std::erase_if(retiredSessions, [](const auto& s) { return !s->Transferring(); });

That freed the session it had just retired, while f.live still pointed at it. The rest of the frame then locked its destroyed chat mutex. The replay setup (PerlinReplay.cpp) pruned the same way. Any other mid-frame Connect() (a transfer, a menu button) has the same hazard.

Change

  • Connect() and the replay session setup no longer free retired sessions.
  • UpdateSession frees them at the start of a frame, before the frame takes its live session, with the same connectsInFlight / Transferring() conditions as before.

A retired session therefore always outlives the frame that retired it. The other raw session pointers are safe with this: hurtSession is only compared, never dereferenced, and modLive is reassigned every frame in PumpMods, after the prune.

Testing

  • macOS arm64 Release build.
  • Not tested yet: connect to a server, then join another one from Discord without leaving. That crashed before this change.

…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.
@notchyves
notchyves merged commit e54517a into theselauraproject:main Nov 6, 2026
4 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants