A second race no longer takes the stack with it

Reported by a tester and reproduced here: finish a race, come back to the
lobby, start another, and the game dies a few seconds in.

It is a stack overflow, from CockpitShellProc calling itself. The cockpit
subclasses the game window to catch WM_SIZE and re-fit the canvas, and
kept SetWindowLongPtr's return as the proc to chain on to. But the game
window is not the cockpit's - it outlives it, and carries the console
screen from one race to the next - and nothing ever unsubclassed it. So
the second race subclassed an already-subclassed window, SetWindowLongPtr
handed back CockpitShellProc itself as the "original", and from the next
message onwards the proc chained to itself until the stack ran out.

Nothing in the log, because nothing in the game had gone wrong yet.

So the destructor puts the window's own proc back, and the install site
will not subclass the same window twice even if it could not.

While there: the destructor also left activeCockpit pointing at the
object it had just freed, so GetCockpit() handed CockpitShellProc a dead
cockpit to lay out. Harmless until someone resized or maximised the
window at the lobby between races, which is not a hard thing to do. Now
cleared.

This came in with the cockpit resize work in 6b43971, so every build
since has had it.

Verified under cdb: before, the crash is a c00000fd stack overflow with
CockpitShellProc / CallWindowProcA repeating the whole way down. After,
four consecutive races - launch, race, results, CONTINUE, lobby, launch
again - complete with no exception at all, and the process exits only
when asked to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-06 00:36:35 -05:00
co-authored by Claude Opus 5
parent cdccb16251
commit 943a15cef4
+50 -3
View File
@@ -3828,6 +3828,13 @@ static LRESULT CALLBACK
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static WNDPROC gCockpitBaseProc = NULL;
//
// Which window we subclassed, so the destructor can put its own proc
// back. The shell is the GAME window: it outlives the cockpit and
// carries the console screen from one race to the next.
//
static HWND gCockpitShellWindow = NULL;
static LRESULT CALLBACK
CockpitShellProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
@@ -4688,9 +4695,25 @@ SVGA16::SVGA16(
GetClientRect(cockpit, &inner);
LayoutCockpit(inner.right, inner.bottom);
// catch maximise / restore / drag-resize and re-fit
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
//
// Catch maximise / restore / drag-resize and re-fit - ONCE.
//
// The destructor puts the original proc back, so ordinarily
// this window is unsubclassed by the time a second race
// builds a new cockpit. The guard is for the case where it
// was not: subclassing an already-subclassed window makes
// SetWindowLongPtr hand back CockpitShellProc itself as the
// "original", and the proc below then chains to itself on
// every single message until the stack runs out. That is a
// stack overflow a few frames into the second race, with no
// hint of a cause in the log.
//
if (gCockpitShellWindow != cockpit)
{
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
gCockpitShellWindow = cockpit;
}
//
// Sticky placement for the shell. Position AND size: nothing
@@ -4926,6 +4949,30 @@ SVGA16::~SVGA16()
cockpitViewscreen = NULL;
}
//
// Give the game window its own proc back, and stop answering for a
// cockpit that is about to stop existing.
//
// The window survives us - it is the one that shows the console
// screen between races - so both of these outlived their subject.
// The subclass was the worse of the two: the next race re-subclassed
// the same window and CockpitShellProc ended up chained to itself.
// activeCockpit was the quieter one, left pointing at this object
// after it was freed, ready for the next WM_SIZE to lay out a
// cockpit that had already gone.
//
if (gCockpitShellWindow != NULL)
{
SetWindowLongPtrA(
gCockpitShellWindow, GWLP_WNDPROC, (LONG_PTR) gCockpitBaseProc);
gCockpitShellWindow = NULL;
gCockpitBaseProc = NULL;
}
if (activeCockpit == this)
{
activeCockpit = NULL;
}
Check_Fpu();
}