From 835580ab91cb60aa42db55c6629c6a96926a0a67 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 13 Aug 2026 20:50:11 -0500 Subject: [PATCH] A pilot leaving mid-refresh no longer takes the room with them From the playtest dump: an access violation reading address zero inside strncpy, two minutes into a session, dispatched from a window message - WM_TIMER, wParam 7. Seven is kLobbyTimerId. That timer runs every 750ms while hosting or seated, and its work is RefreshRoster -> RPL4Lobby_RosterLines -> CollectMembers, which reads each member's row out of the lobby. CollectMembers took Steam's answers straight as strings: eleven strncpy and atoi calls fed directly from GetLobbyMemberData with nothing between. Steam answers NULL - not "" - for a member it no longer knows, and the roster is polled on a timer, so somebody leaving between GetNumLobbyMembers and the read of their row is ordinary rather than exotic. strncpy then walks off address zero, which is exactly the faulting instruction. The file already knew this: LobbyText() has guarded the lobby-wide reads from the start, and all six other direct readers test for NULL. Only the member rows went unguarded. So they get the matching accessor, MemberText(), and CollectMembers goes through it - which covers the two atoi calls as well, since atoi(NULL) reads address zero just as happily. Not confirmed frame by frame: the dump is a tester's own build (D:\Games\RP412, stamped 13 Aug 16:33) and no matching pdb came with it, so the frames resolve only as offsets. What is symbol-independent is the message number and the timer id, read straight off the stack; from there this is the one path in that handler that does an unguarded strncpy on a pointer Steam is allowed to return NULL for. The call depth after inlining matches too. Co-Authored-By: Claude Opus 5 (1M context) --- RP_L4/RPL4LOBBY.cpp | 49 ++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/RP_L4/RPL4LOBBY.cpp b/RP_L4/RPL4LOBBY.cpp index a5784f2..7dc0a48 100644 --- a/RP_L4/RPL4LOBBY.cpp +++ b/RP_L4/RPL4LOBBY.cpp @@ -293,6 +293,26 @@ namespace return (text != NULL) ? text : ""; } + //------------------------------------------------------------------- + // The same for one member's own row. + // + // Steam answers NULL - not "" - for a member it no longer knows, + // and the roster is polled on a 750ms timer, so somebody leaving + // between GetNumLobbyMembers and the read of their row is ordinary + // rather than exotic. Every caller treats the answer as a string: + // strncpy walks off address zero, and so does atoi. + //------------------------------------------------------------------- + const char *MemberText(CSteamID member, const char *key) + { + if (!gInLobby) + { + return ""; + } + const char *text = + SteamMatchmaking()->GetLobbyMemberData(gLobby, member, key); + return (text != NULL) ? text : ""; + } + //--------------------------------------------------------------- // Launch: roster into lobby data, peers into the transport, and // the hosted-race path primed on the owner @@ -328,39 +348,36 @@ namespace memset(member, 0, sizeof(*member)); member->id = SteamMatchmaking()->GetLobbyMemberByIndex(gLobby, i); strncpy(member->ip, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ip"), + MemberText(member->id, "ip"), sizeof(member->ip) - 1); - member->consolePort = atoi( - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "cp")); - member->gamePort = atoi( - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "gp")); + member->consolePort = atoi(MemberText(member->id, "cp")); + member->gamePort = atoi(MemberText(member->id, "gp")); strncpy(member->name, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "nm"), + MemberText(member->id, "nm"), sizeof(member->name) - 1); strncpy(member->vehicle, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "vh"), + MemberText(member->id, "vh"), sizeof(member->vehicle) - 1); strncpy(member->color, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "cl"), + MemberText(member->id, "cl"), sizeof(member->color) - 1); strncpy(member->badge, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"), + MemberText(member->id, "bd"), sizeof(member->badge) - 1); strncpy(member->team, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "tm"), + MemberText(member->id, "tm"), sizeof(member->team) - 1); strncpy(member->position, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"), + MemberText(member->id, "ps"), sizeof(member->position) - 1); strncpy(member->netRev, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kNetRevKey), + MemberText(member->id, kNetRevKey), sizeof(member->netRev) - 1); strncpy(member->build, - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kBuildKey), + MemberText(member->id, kBuildKey), sizeof(member->build) - 1); - member->camera = (atoi( - SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kCamKey)) != 0) - ? True : False; + member->camera = + (atoi(MemberText(member->id, kCamKey)) != 0) ? True : False; member->published = member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0; }