You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TransportContain::isSpecificRiderFreeToExit() dereferences specificObject->getContainedBy() without a null check, so any call where the rider is not (or no longer) contained crashes the game with SIGSEGV on the main thread.
The null check exists only as a DEBUG_ASSERTCRASH, which compiles out of release builds — the release build then dereferences the same pointer on the very next line.
Hit three times in a row on the macOS arm64 build, in three consecutive multiplayer network games across two maps — every time with an identical stack, identical crashing instruction and identical fault address. This is not a rare race; it is reliably reachable in normal early-game play.
A replay of the third crashing game is available (see below) — it is the auto-saved 00000000.rep, written 1 second before the crash, so it covers the run right up to the faulting frame.
Steps to reproduce
Start a 2-player LAN/network game, China Infantry General vs. USA Superweapon General
Play normally through the opening
Crash occurs in the first ~5 minutes of the match
All three crashes, back to back:
#
Incident ID
Launched
Crashed
Time in process
1
2EFFB021-A070-49A3-9DA0-7CC816B82D12
22:08:16
22:15:46
7.5 min
2
6165BDB3-5F40-4564-9FAC-F71CBD213DED
22:18:40
22:24:14
5.5 min
3
6B6925A6-61A2-4B48-B588-21077F298C92
22:24:59
22:28:34
3.5 min
Crash #3 was on the official map Twilight Flame; the earlier ones were on a different map, so this is not map-specific. Early-game China Infantry play means Troop Crawlers are in use, which is the obvious TransportContain candidate on that side.
Note this is a network game: the crash is inside GameLogic::update(), so the affected client is dropped mid-match.
All three incidents share the same binary (slice_uuid 5182a493-e4a8-318c-856d-579b258920e1), the same crashing instruction (__TEXT+0x1A0064, i.e. isSpecificRiderFreeToExit + 56), and the same faulting address 0x158.
Register state at the fault is consistent across all three:
Incident
x20 (specificObject)
x8 (getContainedBy())
far
2EFFB021-…
0x7f4451fa0
0
0x158
6165BDB3-…
0x87f8fd660
0
0x158
6B6925A6-…
0x794042400
0
0x158
specificObject itself is always a valid heap pointer — it is specifically its container back-pointer that is null. So this is a genuinely uncontained rider reaching the check, not a wild/garbage Object*.
Root cause
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TransportContain.cpp:582-587
(identical code in Generals/.../TransportContain.cpp:471-476):
#if !RETAIL_COMPATIBLE_CRC
// TheSuperHackers @bugfix Stubbjax 02/03/2026 If our parent container is held, then we// are not free to exit.DEBUG_ASSERTCRASH(specificObject->getContainedBy(), ("rider must be contained"));
if (specificObject->getContainedBy()->isDisabledByType(DISABLED_HELD))
return FALSE;
#endif
DEBUG_ASSERTCRASH is a no-op in release, so line 585 dereferences a NULLObject*.
Disassembly confirmation
Instruction stream around the fault (from the crash report's instructionByteStream, arm64):
At the fault: x8 = 0, far = 0x158, and tbnz w8, #3 matches DISABLED_HELD == 3 in Common/DisabledTypes.h. The two neighbouring pointer loads (ai on line 578, getAIUpdateInterface() on line 594) are null-checked; the getContainedBy() load between them is not.
Why the pointer is null
The call arrives from AIExitState::update() → ExitInterface::reserveDoorForExit() → isSpecificRiderFreeToExit(). AIExitState asks the exit interface whether a unit may leave; nothing on that path guarantees the unit is still inside the container at that moment — e.g. it was already removed/killed/transferred on an earlier frame while the exit state was still pending. The DEBUG_ASSERTCRASH on line 584 shows the author was already aware getContainedBy() can be null here.
Suggested fix
Either null-guard the pointer:
DEBUG_ASSERTCRASH(specificObject->getContainedBy(), ("rider must be contained"));
const Object* riderContainer = specificObject->getContainedBy();
if (riderContainer != nullptr && riderContainer->isDisabledByType(DISABLED_HELD))
return FALSE;
…or, since a rider inside this transport has getContainedBy() == me, test the container directly and drop the indirection entirely:
if (me->isDisabledByType(DISABLED_HELD))
return FALSE;
The first is the minimal, behaviour-preserving change; the second matches the comment's intent ("if our parent container is held") and removes the dependence on the rider's containment state.
Summary
TransportContain::isSpecificRiderFreeToExit()dereferencesspecificObject->getContainedBy()without a null check, so any call where the rider is not (or no longer) contained crashes the game withSIGSEGVon the main thread.The null check exists only as a
DEBUG_ASSERTCRASH, which compiles out of release builds — the release build then dereferences the same pointer on the very next line.Hit three times in a row on the macOS arm64 build, in three consecutive multiplayer network games across two maps — every time with an identical stack, identical crashing instruction and identical fault address. This is not a rare race; it is reliably reachable in normal early-game play.
A replay of the third crashing game is available (see below) — it is the auto-saved
00000000.rep, written 1 second before the crash, so it covers the run right up to the faulting frame.Steps to reproduce
All three crashes, back to back:
2EFFB021-A070-49A3-9DA0-7CC816B82D126165BDB3-5F40-4564-9FAC-F71CBD213DED6B6925A6-61A2-4B48-B588-21077F298C92Crash #3 was on the official map Twilight Flame; the earlier ones were on a different map, so this is not map-specific. Early-game China Infantry play means Troop Crawlers are in use, which is the obvious
TransportContaincandidate on that side.Note this is a network game: the crash is inside
GameLogic::update(), so the affected client is dropped mid-match.Crash signature
All three incidents share the same binary (
slice_uuid 5182a493-e4a8-318c-856d-579b258920e1), the same crashing instruction (__TEXT+0x1A0064, i.e.isSpecificRiderFreeToExit + 56), and the same faulting address0x158.Register state at the fault is consistent across all three:
x20(specificObject)x8(getContainedBy())far2EFFB021-…0x7f4451fa000x1586165BDB3-…0x87f8fd66000x1586B6925A6-…0x79404240000x158specificObjectitself is always a valid heap pointer — it is specifically its container back-pointer that is null. So this is a genuinely uncontained rider reaching the check, not a wild/garbageObject*.Root cause
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TransportContain.cpp:582-587(identical code in
Generals/.../TransportContain.cpp:471-476):DEBUG_ASSERTCRASHis a no-op in release, so line 585 dereferences aNULLObject*.Disassembly confirmation
Instruction stream around the fault (from the crash report's
instructionByteStream, arm64):At the fault:
x8 = 0,far = 0x158, andtbnz w8, #3matchesDISABLED_HELD == 3inCommon/DisabledTypes.h. The two neighbouring pointer loads (aion line 578,getAIUpdateInterface()on line 594) are null-checked; thegetContainedBy()load between them is not.Why the pointer is null
The call arrives from
AIExitState::update()→ExitInterface::reserveDoorForExit()→isSpecificRiderFreeToExit().AIExitStateasks the exit interface whether a unit may leave; nothing on that path guarantees the unit is still inside the container at that moment — e.g. it was already removed/killed/transferred on an earlier frame while the exit state was still pending. TheDEBUG_ASSERTCRASHon line 584 shows the author was already awaregetContainedBy()can be null here.Suggested fix
Either null-guard the pointer:
…or, since a rider inside this transport has
getContainedBy() == me, test the container directly and drop the indirection entirely:The first is the minimal, behaviour-preserving change; the second matches the comment's intent ("if our parent container is held") and removes the dependence on the rider's containment state.
Both
GeneralsMDandGeneralscopies need the fix.Related
f334383ec— "Prevent riders from being added to destroyed container object when Reinforcement Pad is destroyed before Troop Crawler drop" (bugfix(contain): Prevent riders from being added to destroyed container object when Reinforcement Pad is destroyed before Troop Crawler drop TheSuperHackers/GeneralsGameCode#2746). Same bug class: a rider whose container relationship is broken. That fix addressed the add path; this report is the exit path.DEBUG_ASSERTCRASHon line 584 was added alongside theDISABLED_HELDcheck itself, so the null case was anticipated but never guarded in release.Attachments
crash-222834.rep— auto-saved replay of crashing game Macos build #3 (Twilight Flame, 2-player LAN), written 1 s before the crash.ipscrash reports for all three incidents available on requestEnvironment
Mac16,1, arm64 native, not translated)libdxvk_d3d9.0.dylib) over MoltenVK / Metal (AGXMetalG16G)GeneralsXZH.app, binary UUID5182a493-e4a8-318c-856d-579b258920e1(no version string embedded in the binary)