Skip to content

Commit df2224b

Browse files
authored
bugfix(object): Avoid crash with dangling contain module in Object::onDestroy() when Reinforcement Pad is destroyed before Troop Crawler drop (TheSuperHackers#2747)
1 parent 20f4254 commit df2224b

4 files changed

Lines changed: 112 additions & 20 deletions

File tree

Generals/Code/GameEngine/Include/GameLogic/Object.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ class Object : public Thing, public Snapshot
717717

718718
Object* m_containedBy; /**< an object can only be contained by at most one
719719
other object, this is that object (if present) */
720-
ObjectID m_xferContainedByID; ///< xfer uses IDs to store pointers and looks them up after
720+
ObjectID m_containedByID; ///< ID of the object we're contained by; only to be used when m_containedBy cannot be used
721721
UnsignedInt m_containedByFrame; ///< frame we were contained by m_containedBy
722722

723723
Real m_constructionPercent; ///< for objects being built ... this is the amount completed (0.0 to 100.0)

Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatu
173173
m_physics(nullptr),
174174
m_geometryInfo(tt->getTemplateGeometryInfo()),
175175
m_containedBy(nullptr),
176-
m_xferContainedByID(INVALID_ID),
176+
m_containedByID(INVALID_ID),
177177
m_containedByFrame(0),
178178
m_behaviors(nullptr),
179179
m_body(nullptr),
@@ -621,6 +621,22 @@ void Object::onContainedBy( Object *containedBy )
621621
clearStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_MASKED ) );
622622
m_containedBy = containedBy;
623623
m_containedByFrame = TheGameLogic->getFrame();
624+
625+
#if RETAIL_COMPATIBLE_CRC
626+
// TheSuperHackers @info Set INVALID_ID if the container object was destroyed
627+
// to indicate that the pointer will become a dangling pointer in the next frame.
628+
if (containedBy && !containedBy->isDestroyed())
629+
{
630+
m_containedByID = containedBy->getID();
631+
}
632+
else
633+
{
634+
m_containedByID = INVALID_ID;
635+
}
636+
#else
637+
DEBUG_ASSERTCRASH(containedBy == nullptr || !containedBy->isDestroyed(),
638+
("Object::onContainedBy - Adding into a destroyed container"));
639+
#endif
624640
}
625641

626642
//-------------------------------------------------------------------------------------------------
@@ -631,6 +647,10 @@ void Object::onRemovedFrom( Object *removedFrom )
631647
clearStatus( MAKE_OBJECT_STATUS_MASK2( OBJECT_STATUS_MASKED, OBJECT_STATUS_UNSELECTABLE ) );
632648
m_containedBy = nullptr;
633649
m_containedByFrame = 0;
650+
651+
#if RETAIL_COMPATIBLE_CRC
652+
m_containedByID = INVALID_ID;
653+
#endif
634654
}
635655

636656
//-------------------------------------------------------------------------------------------------
@@ -681,9 +701,33 @@ void Object::onDestroy()
681701
{
682702

683703
// This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up.
684-
if( m_containedBy && m_containedBy->getContain() )
704+
if (m_containedBy)
685705
{
686-
m_containedBy->getContain()->removeFromContain( this );
706+
#if RETAIL_COMPATIBLE_CRC
707+
if (m_containedByID == INVALID_ID)
708+
{
709+
// TheSuperHackers @bugfix Caball009 25/05/2026 Due to a potential use-after-free bug that cannot be fixed
710+
// with retail compatibility, the 'contained by' pointer of this object may point to an already destroyed object.
711+
// Avoid removing this object from the contain list, because it could crash the game,
712+
// as the begin / end iterator for STLPort and MSVC std::list implementations depends on dynamically allocated memory.
713+
DEBUG_CRASH(("container object must be valid; this looks like use-after-free"));
714+
}
715+
else
716+
{
717+
DEBUG_ASSERTCRASH(TheGameLogic->findObjectByID(m_containedByID) == m_containedBy,
718+
("contained by pointer is out of sync with contained by ID"));
719+
720+
if (ContainModuleInterface* contain = m_containedBy->getContain())
721+
{
722+
contain->removeFromContain(this);
723+
}
724+
}
725+
#else
726+
if (ContainModuleInterface* contain = m_containedBy->getContain())
727+
{
728+
contain->removeFromContain(this);
729+
}
730+
#endif
687731
}
688732

689733
//
@@ -3727,16 +3771,18 @@ void Object::xfer( Xfer *xfer )
37273771
// No, the contain module is just going to friend_ reach in and set this for us.
37283772
// Containers more complicated than Open (like Tunnel) can't do that. Our variable,
37293773
// our responsibility.
3774+
#if !RETAIL_COMPATIBLE_CRC
3775+
// TheSuperHackers @tweak Contained by ID is already set with retail compatibility; don't overwrite it.
37303776
if( xfer->getXferMode() == XFER_SAVE )
37313777
{
37323778
if( m_containedBy != nullptr )
3733-
m_xferContainedByID = m_containedBy->getID();
3779+
m_containedByID = m_containedBy->getID();
37343780
else
3735-
m_xferContainedByID = INVALID_ID;
3781+
m_containedByID = INVALID_ID;
37363782
}
3783+
#endif
37373784

3738-
3739-
xfer->xferObjectID( &m_xferContainedByID );
3785+
xfer->xferObjectID( &m_containedByID );
37403786
}
37413787

37423788
// contained by frame
@@ -3961,8 +4007,8 @@ void Object::xfer( Xfer *xfer )
39614007
//-------------------------------------------------------------------------------------------------
39624008
void Object::loadPostProcess()
39634009
{
3964-
if( m_xferContainedByID != INVALID_ID )
3965-
m_containedBy = TheGameLogic->findObjectByID(m_xferContainedByID);
4010+
if( m_containedByID != INVALID_ID )
4011+
m_containedBy = TheGameLogic->findObjectByID(m_containedByID);
39664012
else
39674013
m_containedBy = nullptr;
39684014

GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ class Object : public Thing, public Snapshot
761761

762762
Object* m_containedBy; /**< an object can only be contained by at most one
763763
other object, this is that object (if present) */
764-
ObjectID m_xferContainedByID; ///< xfer uses IDs to store pointers and looks them up after
764+
ObjectID m_containedByID; ///< ID of the object we're contained by; only to be used when m_containedBy cannot be used
765765
UnsignedInt m_containedByFrame; ///< frame we were contained by m_containedBy
766766

767767
Real m_constructionPercent; ///< for objects being built ... this is the amount completed (0.0 to 100.0)

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatu
182182
m_physics(nullptr),
183183
m_geometryInfo(tt->getTemplateGeometryInfo()),
184184
m_containedBy(nullptr),
185-
m_xferContainedByID(INVALID_ID),
185+
m_containedByID(INVALID_ID),
186186
m_containedByFrame(0),
187187
m_behaviors(nullptr),
188188
m_body(nullptr),
@@ -691,6 +691,22 @@ void Object::onContainedBy( Object *containedBy )
691691
m_containedBy = containedBy;
692692
m_containedByFrame = TheGameLogic->getFrame();
693693

694+
#if RETAIL_COMPATIBLE_CRC
695+
// TheSuperHackers @info Set INVALID_ID if the container object was destroyed
696+
// to indicate that the pointer will become a dangling pointer in the next frame.
697+
if (containedBy && !containedBy->isDestroyed())
698+
{
699+
m_containedByID = containedBy->getID();
700+
}
701+
else
702+
{
703+
m_containedByID = INVALID_ID;
704+
}
705+
#else
706+
DEBUG_ASSERTCRASH(containedBy == nullptr || !containedBy->isDestroyed(),
707+
("Object::onContainedBy - Adding into a destroyed container"));
708+
#endif
709+
694710
handlePartitionCellMaintenance(); // which should unlook me now that I am contained
695711

696712
}
@@ -704,6 +720,10 @@ void Object::onRemovedFrom( Object *removedFrom )
704720
m_containedBy = nullptr;
705721
m_containedByFrame = 0;
706722

723+
#if RETAIL_COMPATIBLE_CRC
724+
m_containedByID = INVALID_ID;
725+
#endif
726+
707727
handlePartitionCellMaintenance(); // get a clean look, now that I am outdoors, again
708728

709729
}
@@ -756,9 +776,33 @@ void Object::onDestroy()
756776
{
757777

758778
// This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up.
759-
if( m_containedBy && m_containedBy->getContain() )
779+
if (m_containedBy)
760780
{
761-
m_containedBy->getContain()->removeFromContain( this );
781+
#if RETAIL_COMPATIBLE_CRC
782+
if (m_containedByID == INVALID_ID)
783+
{
784+
// TheSuperHackers @bugfix Caball009 25/05/2026 Due to a potential use-after-free bug that cannot be fixed
785+
// with retail compatibility, the 'contained by' pointer of this object may point to an already destroyed object.
786+
// Avoid removing this object from the contain list, because it could crash the game,
787+
// as the begin / end iterator for STLPort and MSVC std::list implementations depends on dynamically allocated memory.
788+
DEBUG_CRASH(("container object must be valid; this looks like use-after-free"));
789+
}
790+
else
791+
{
792+
DEBUG_ASSERTCRASH(TheGameLogic->findObjectByID(m_containedByID) == m_containedBy,
793+
("contained by pointer is out of sync with contained by ID"));
794+
795+
if (ContainModuleInterface* contain = m_containedBy->getContain())
796+
{
797+
contain->removeFromContain(this);
798+
}
799+
}
800+
#else
801+
if (ContainModuleInterface* contain = m_containedBy->getContain())
802+
{
803+
contain->removeFromContain(this);
804+
}
805+
#endif
762806
}
763807

764808
//
@@ -4246,16 +4290,18 @@ void Object::xfer( Xfer *xfer )
42464290
// No, the contain module is just going to friend_ reach in and set this for us.
42474291
// Containers more complicated than Open (like Tunnel) can't do that. Our variable,
42484292
// our responsibility.
4293+
#if !RETAIL_COMPATIBLE_CRC
4294+
// TheSuperHackers @tweak Contained by ID is already set with retail compatibility; don't overwrite it.
42494295
if( xfer->getXferMode() == XFER_SAVE )
42504296
{
42514297
if( m_containedBy != nullptr )
4252-
m_xferContainedByID = m_containedBy->getID();
4298+
m_containedByID = m_containedBy->getID();
42534299
else
4254-
m_xferContainedByID = INVALID_ID;
4300+
m_containedByID = INVALID_ID;
42554301
}
4302+
#endif
42564303

4257-
4258-
xfer->xferObjectID( &m_xferContainedByID );
4304+
xfer->xferObjectID( &m_containedByID );
42594305
}
42604306

42614307
// contained by frame
@@ -4480,8 +4526,8 @@ void Object::xfer( Xfer *xfer )
44804526
//-------------------------------------------------------------------------------------------------
44814527
void Object::loadPostProcess()
44824528
{
4483-
if( m_xferContainedByID != INVALID_ID )
4484-
m_containedBy = TheGameLogic->findObjectByID(m_xferContainedByID);
4529+
if( m_containedByID != INVALID_ID )
4530+
m_containedBy = TheGameLogic->findObjectByID(m_containedByID);
44854531
else
44864532
m_containedBy = nullptr;
44874533

0 commit comments

Comments
 (0)