Fix glass panel dead-button crash: null-init MessageHandlerSet gap slots (Gitea #18)

Clicking a per-display glass Engineering panel button (0x21 on an Eng page)
hard-crashed via a wild call through an uninitialised pointer (eip=cdcdcdcd
debug / 0x01048748 release), zero btl4 frames above Receiver::Receive.

Root cause is the dense message-handler-table gap hazard (reconstruction-
gotchas #11), the message-side twin of the attribute-table one -- NOT a /FORCE
unresolved external (the glass link log carried only the known-benign mech3.obj
CreateStreamedSubsystem set). Receiver::MessageHandlerSet::Find(id) returns
messageHandlers[id-1].entryHandler for any id <= entryCount with no populated-
check, and Receive() calls it when != NullHandler. The streamed Eng-page .CTL
dispatches subsystem msg id 3/0xb to whatever subsystem is shown; a weapon
(Emitter) registers only PoweredSubsystem 4-8 + MechWeapon 9-10, so id 3 is a
gap below entryCount 10. Build did new HandlerEntry[entryCount] and left gaps
uninitialised -> (this->*garbage)(msg). The 1995 binary has the identical non-
zeroing new[] (part_002.c Build) and survived only on fresh-heap-zero luck: a
weapon receiving id 3 was always meant to IGNORE it (id 3 = a Condenser/
Reservoir action in a different subsystem branch; the uniform Eng-page button
template makes buttons for unimplemented actions authentically inert). The new
per-display windows just made the id reachable live.

Fix (engine, class-wide, faithful): Build now null-inits every slot
(entryID=0/entryName=""/entryHandler=NullHandler) before copying inherited /
placing supplied, so a gap is deterministically NullHandler -> Receive drops it
(the authentic "Receiver ignores unhandled messages"), with an empty never-NULL
name so the name-based Find() strcmp can't deref a gap. Correct dispatch
contract, not a glass-path guard -> protects the whole dead-button class (#14).

Also lands the [glasswin] CLICK crash-forensics log (names the button before
dispatch).

Verified under cdb: click-soaked every MFD bank (Engineering/L+R Weapons/Heat/
Comm, ~130 clicks through Quad<->Eng page cycles) -> zero AVs, reconstructed
mapper preset selects still fire ([mode] preset (1,1)/(2,1)...); pod build +
solo mission un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-20 15:59:37 -05:00
co-authored by Claude Fable 5
parent bc5ac3a0db
commit 5ba5697a08
4 changed files with 93 additions and 0 deletions
+11
View File
@@ -674,6 +674,14 @@ static LRESULT CALLBACK
int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam));
if (a >= 0)
{
// crash forensics (always-on, flushed BEFORE dispatch): if a
// click kills the process, the last log line names the button.
DEBUG_STREAM << "[glasswin] CLICK '" << (w->title ? w->title : "?")
<< "' addr=0x" << std::hex << a << std::dec
<< " at(" << (int)(short)LOWORD(lparam) << ","
<< (int)(short)HIWORD(lparam) << ")"
<< (latched[a & 0x7F] ? " unlatch" : " press")
<< "\n" << std::flush;
if (latched[a & 0x7F])
{
latched[a & 0x7F] = 0;
@@ -706,6 +714,9 @@ static LRESULT CALLBACK
int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam));
if (a >= 0)
{
DEBUG_STREAM << "[glasswin] CLICK '" << (w->title ? w->title : "?")
<< "' addr=0x" << std::hex << a << std::dec
<< " latch-toggle\n" << std::flush;
latched[a & 0x7F] = latched[a & 0x7F] ? 0 : 1;
PadRIO::SetScreenButton(a, latched[a & 0x7F]);
InvalidateRect(window, NULL, FALSE);