Skip to content

Crash (SIGSEGV): null m_specialPowerModule in MissileLauncherBuildingUpdate::initiateIntentToDoSpecialPower when skirmish AI fires SCUD Storm (existing fix dead code: RETAIL_COMPATIBLE_CRC=0) #284

Description

@busybee13

Prerequisites

  • I have searched for similar issues and confirmed this is not a duplicate

Game Version

  • Command & Conquer Generals: Zero Hour

Operating System

  • macOS

Bug Description

During a skirmish against the AI (superweapons enabled), the game crashes with SIGSEGV (null pointer dereference) roughly 24 minutes in. The crash is triggered by the skirmish AI script action SKIRMISH_FIRE_SPECIAL_POWER_AT_MOST_COST firing a special power at a missile-launcher building (GLA SCUD Storm) that is still under construction.

Stack (macOS arm64, GeneralsXZH Beta 18):

0  MissileLauncherBuildingUpdate::initiateIntentToDoSpecialPower + 4   (MissileLauncherBuildingUpdate.cpp:213)
1  SpecialPowerModule::initiateIntentToDoSpecialPower + 184            (SpecialPowerModule.cpp:443)
2  SpecialPowerModule::doSpecialPowerAtLocation + 68                   (SpecialPowerModule.cpp:725)
3  OCLSpecialPower::doSpecialPowerAtLocation + 192                     (OCLSpecialPower.cpp:177)
4  ScriptActions::doSkirmishFireSpecialPowerAtMostCost + 556           (ScriptActions.cpp:4291)
5  ScriptEngine::executeActions + 724                                  (ScriptEngine.cpp:7633)
...

Faulting instruction in the shipped binary (registers: x0 = 0, far = 0):

10021d520: ldr x0, [x0, #0x8]   ; x0 = m_specialPowerModule  -> null
10021d524: ldr x8, [x0]         ; <-- crash: vtable load from null
10021d528: ldr x8, [x8, #0x28]
10021d52c: blr x8               ; virtual call getSpecialPowerTemplate()

Reproduction Steps

  1. Start a skirmish versus AI with Superweapons enabled.
  2. Play until the AI fires its SCUD Storm (GLA AI) while the building is still under construction or newly rebuilt.
  3. The game segfaults when initiateIntentToDoSpecialPower runs with m_specialPowerModule == nullptr.

The timing is nondeterministic because the AI script fires the power on its own schedule; the attached crash happened ~24 min into a game.

Root Cause

  1. MissileLauncherBuildingUpdate::update() skips the lazy init of m_specialPowerModule while the building is UNDER_CONSTRUCTION:
    if( getObject()->testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION) )
        return UPDATE_SLEEP_NONE;          // m_specialPowerModule stays null
  2. initiateIntentToDoSpecialPower() then dereferences m_specialPowerModule unconditionally in release builds.
  3. A null check exists, but only inside #if RETAIL_COMPATIBLE_CRC (added by TheSuperHackers/Mauller, 29/06/2025) — and Core/GameEngine/Include/Common/GameDefines.h:97 hardcodes #define RETAIL_COMPATIBLE_CRC (0), so the fix is dead code in every build and the crash still ships (confirmed by disassembling the Beta 18 binary, above).
  4. As a secondary issue, the existing guarded fix itself calls getSpecialPowerModule(...)->setReadyFrame(...) without checking the returned pointer, so it can crash the same way if the module lookup fails.

Proposed Fix

Make the null check unconditional and guard the getSpecialPowerModule result (also makes the game resilient if the module is absent for any other reason):

 Bool MissileLauncherBuildingUpdate::initiateIntentToDoSpecialPower( const SpecialPowerTemplate *specialPowerTemplate, const Object *targetObj, const Coord3D *targetPos, const Waypoint *way, UnsignedInt commandOptions )
 {
-#if RETAIL_COMPATIBLE_CRC
 	// TheSuperHackers @bugfix Mauller 29/06/2025 prevent a game crash when told to launch before ready to do so.
+	// Note: RETAIL_COMPATIBLE_CRC is hardcoded to 0 (GameDefines.h), so the guard was compiled out and the
+	// crash still occurs in release builds. The null check must be unconditional.
 	if (!m_specialPowerModule) {
 		Object* us = getObject();
-		us->getSpecialPowerModule(specialPowerTemplate)->setReadyFrame(0xFFFFFFFF);
+		SpecialPowerModuleInterface* spm = us->getSpecialPowerModule(specialPowerTemplate);
+		if (spm)
+			spm->setReadyFrame(0xFFFFFFFF);
 		return FALSE;
 	}
-#endif
 
 	if( m_specialPowerModule->getSpecialPowerTemplate() != specialPowerTemplate )

Testing

  • Fixed build compiled locally on macOS (Apple Silicon, macos-vulkan preset). Disassembly of the built binary shows the null check is now present where the shipped build had none:

    Shipped (Beta 18) — crashes:

    ldr x0, [x0, #0x8]   ; m_specialPowerModule
    ldr x8, [x0]         ; crash: no branch, vtable load from null
    

    Fixed build:

    ldr x0, [x0, #0x40]  ; m_specialPowerModule
    cbz x0, <null path>  ; null check
    ...                  ; getSpecialPowerModule() -> cbz -> setReadyFrame(0xFFFFFFFF) -> return FALSE
    
  • Boot smoke test passed: the fixed binary launches, renders (DXVK frames active), and exits cleanly (GameMain() returned with code 0). No regression.

  • The crash itself is AI-timing dependent, so there is no scripted repro; the fix eliminates the only unchecked null dereference on this code path.

Crash report

~/Library/Logs/DiagnosticReports/GeneralsXZH-2026-09-05-222444.ips (available on request)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions