Playback reads its own header as the first packet

The heap corruption is explained, and it was never a memory bug - it was a
mis-parse that corrupted the heap downstream.

One line of instrumentation in DispatchPacket did it:

    Playback: packet client=0 message=3 length=1 -> the NETWORK MANAGER

Against the spool's header - appID 0, major 3, (remote 0, id 2),
(remote 1, id 3) - every field lines up:

    clientID      0   <- appID
    gameID        3   <- major version
    fromHost      0   <- host 1 remote
    timeStamp     2   <- host 1 id
    messageLength 1   <- host 2 remote
    messageID     3   <- host 2 id

So the spooler task is parsing the 24 byte header as packet one. Client 0
message 3 is ReceiveEggFile, which builds a Mission - a SECOND one, from a
packet - and that is the crash in Mission::Mission.

The cursor is at byte zero because L4PlaybackNetworkManager::StartConnecting,
which is what consumes the header, had not run yet. Its own log line for a
camera station never appeared, and Application::CreateMission shows up in
the stack UNDER SpoolerTask::Execute rather than under the egg message
posted from the constructor. The task begins dispatching before the mission
exists, and nothing stops it.

That also explains why the wrong-egg guard stayed silent: it inspects the
first packet in StartConnecting, which is correct when it runs, and it
simply never got the chance.

Left in: DispatchPacket names the client ID, message ID, length and the
client each packet resolved to, for the first two dozen packets. Client IDs
and message IDs are both small integers counted from the same base -
NetworkClient::NextMessageID is 3, so the interest manager's
NewDynamicEntity and the network manager's ReceiveEggFile are BOTH message
3 - which is exactly why a mis-routed packet looked valid to whoever
received it. A NULL client is now returned from rather than called through,
since Check() is a no-op in release.

Next: the spooler task must not dispatch until the mission is loaded and
the header consumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 14:58:30 -05:00
co-authored by Claude Opus 5
parent 48a595b244
commit d00e03ae20
+41
View File
@@ -419,6 +419,47 @@ void
NetworkManager *net_mgr = GetNetworkManager();
Check(net_mgr);
NetworkClient *client = net_mgr->GetNetworkClientPointer(packet->clientID);
//
// Say what is being routed where, for the first few.
//
// Playback dies inside Mission::Mission, reached through the egg file
// handler, while dispatching spooled packets - so a packet is arriving
// at a client that is not the one it was recorded for. Client IDs and
// message IDs are both small integers counted from the same base
// (NetworkClient::NextMessageID is 3, so the interest manager's
// NewDynamicEntity and the network manager's ReceiveEggFile are BOTH
// message 3), which makes a mis-routed packet look perfectly valid to
// whoever receives it. Naming the pair and the client it resolved to
// ends the guessing.
//
// A NULL client is the other candidate: Check() is a no-op in a
// release build, so a missing interest manager would be a call through
// nothing rather than a complaint.
//
{
static int said = 0;
if (said < 24)
{
++said;
DEBUG_STREAM << "Playback: packet client=" << (int) packet->clientID
<< " message=" << (int) packet->messageData.messageID
<< " length=" << (int) packet->messageData.messageLength
<< " -> client " << (void *) client
<< (client == NULL ? " (NULL!)" : "")
<< (client == (NetworkClient *) net_mgr ? " = the NETWORK MANAGER" : "")
<< (client == (NetworkClient *) this ? " = the application" : "")
<< (client == (NetworkClient *) GetInterestManager()
? " = the interest manager" : "")
<< "\n" << std::flush;
}
}
if (client == NULL)
{
return;
}
Check(client);
client->ReceiveNetworkPacket(packet, &packet->messageData);
Check_Fpu();