From 3c9383b30f67c5849b803695016afcea7562e44d Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 18 Aug 2026 00:35:20 +0200 Subject: [PATCH 1/4] Revert "bugfix(contain): Restore retail compatibility after changes to Object::m_containedByID (#2868)" This reverts commit a20992d252f755dc074f6ae3469772caf24bc60a. --- .../GameEngine/Include/GameLogic/Object.h | 6 +-- .../Source/GameLogic/Object/Object.cpp | 12 +---- .../GameEngine/Include/GameLogic/Object.h | 6 +-- .../GameLogic/Object/Contain/HelixContain.cpp | 52 ++----------------- .../Source/GameLogic/Object/Object.cpp | 12 +---- 5 files changed, 9 insertions(+), 79 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Object.h b/Generals/Code/GameEngine/Include/GameLogic/Object.h index 7a1a25260a9..9bc97105fea 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Object.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Object.h @@ -424,14 +424,10 @@ class Object : public Thing, public Snapshot void onContainedBy( Object *containedBy ); void onRemovedFrom( Object *removedFrom ); Int getTransportSlotCount() const; - void friend_setContainedBy( Object *containedBy ); + void friend_setContainedBy( Object *containedBy ) { m_containedBy = containedBy; } const Object* getEnclosingContainedBy() const; ///< Find the first enclosing container in the containment chain. const Object* getOuterObject() const; ///< Get the top-level object -#if RTS_ZEROHOUR && RETAIL_COMPATIBLE_CRC - void friend_setContainedByID(ObjectID id) { m_containedByID = id; } -#endif - // Special Powers ------------------------------------------------------------------------------- SpecialPowerModuleInterface *getSpecialPowerModule( const SpecialPowerTemplate *specialPowerTemplate ) const; void doSpecialPower( const SpecialPowerTemplate *specialPowerTemplate, UnsignedInt commandOptions, Bool forced = false ); ///< execute power diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index c24af6a0175..8d91d86a70f 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -674,15 +674,6 @@ Int Object::getTransportSlotCount() const return count; } -void Object::friend_setContainedBy(Object* containedBy) -{ - m_containedBy = containedBy; - -#if !RETAIL_COMPATIBLE_CRC - m_containedByFrame = containedBy ? TheGameLogic->getFrame() : 0; -#endif -} - const Object* Object::getEnclosingContainedBy() const { for (const Object* child = this, *container = getContainedBy(); container; child = container, container = container->getContainedBy()) @@ -3781,9 +3772,8 @@ void Object::xfer( Xfer *xfer ) // No, the contain module is just going to friend_ reach in and set this for us. // Containers more complicated than Open (like Tunnel) can't do that. Our variable, // our responsibility. -#if RETAIL_COMPATIBLE_CRC +#if !RETAIL_COMPATIBLE_CRC // TheSuperHackers @tweak Contained by ID is already set with retail compatibility; don't overwrite it. -#else if( xfer->getXferMode() == XFER_SAVE ) { if( m_containedBy != nullptr ) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h index 90fc52b3960..2ffcb6c012a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h @@ -448,14 +448,10 @@ class Object : public Thing, public Snapshot void onContainedBy( Object *containedBy ); void onRemovedFrom( Object *removedFrom ); Int getTransportSlotCount() const; - void friend_setContainedBy( Object *containedBy ); + void friend_setContainedBy( Object *containedBy ) { m_containedBy = containedBy; } const Object* getEnclosingContainedBy() const; ///< Find the first enclosing container in the containment chain. const Object* getOuterObject() const; ///< Get the top-level object -#if RTS_ZEROHOUR && RETAIL_COMPATIBLE_CRC - void friend_setContainedByID(ObjectID id) { m_containedByID = id; } -#endif - // Special Powers ------------------------------------------------------------------------------- SpecialPowerModuleInterface *getSpecialPowerModule( const SpecialPowerTemplate *specialPowerTemplate ) const; void doSpecialPower( const SpecialPowerTemplate *specialPowerTemplate, UnsignedInt commandOptions, Bool forced = false ); ///< execute power diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp index c900e02d348..aff9d91941c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp @@ -250,28 +250,10 @@ void HelixContain::addToContainList( Object *obj ) if ( portable ) TheGameLogic->destroyObject( portable ); - portable = obj; + m_portableStructureID = obj->getID(); + obj->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container - m_portableStructureID = portable->getID(); - portable->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container -#if RETAIL_COMPATIBLE_CRC - Object* containedBy = getObject(); - - // TheSuperHackers @info Set INVALID_ID if the container object was destroyed - // to indicate that the pointer will become a dangling pointer in the next frame. - if (containedBy && !containedBy->isDestroyed()) - { - portable->friend_setContainedByID(containedBy->getID()); - } - else - { - portable->friend_setContainedByID(INVALID_ID); - } -#else - DEBUG_ASSERTCRASH(getObject() == nullptr || !getObject()->isDestroyed(), - ("HelixContain::addToContainList - Adding to a destroyed container")); -#endif } else TransportContain::addToContainList( obj ); @@ -286,28 +268,10 @@ void HelixContain::addToContain( Object *obj ) if ( portable ) TheGameLogic->destroyObject( portable ); - portable = obj; - - m_portableStructureID = portable->getID(); - portable->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container + m_portableStructureID = obj->getID(); + obj->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container -#if RETAIL_COMPATIBLE_CRC - Object* containedBy = getObject(); - // TheSuperHackers @info Set INVALID_ID if the container object was destroyed - // to indicate that the pointer will become a dangling pointer in the next frame. - if (containedBy && !containedBy->isDestroyed()) - { - portable->friend_setContainedByID(containedBy->getID()); - } - else - { - portable->friend_setContainedByID(INVALID_ID); - } -#else - DEBUG_ASSERTCRASH(getObject() == nullptr || !getObject()->isDestroyed(), - ("HelixContain::addToContain - Adding to a destroyed container")); -#endif } else TransportContain::addToContain( obj ); @@ -320,16 +284,10 @@ void HelixContain::removeFromContain( Object *obj, Bool exposeStealthUnits ) { Object *portable = getPortableStructure(); if ( portable ) - { -#if RETAIL_COMPATIBLE_CRC - portable->friend_setContainedByID(INVALID_ID); -#else - portable->friend_setContainedBy(nullptr); -#endif m_portableStructureID = INVALID_ID; //portable->kill(); - } + } else { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 800b5c20e4a..73d7913da3c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -749,15 +749,6 @@ Int Object::getTransportSlotCount() const return count; } -void Object::friend_setContainedBy(Object* containedBy) -{ - m_containedBy = containedBy; - -#if !RETAIL_COMPATIBLE_CRC - m_containedByFrame = containedBy ? TheGameLogic->getFrame() : 0; -#endif -} - const Object* Object::getEnclosingContainedBy() const { for (const Object* child = this, *container = getContainedBy(); container; child = container, container = container->getContainedBy()) @@ -4300,9 +4291,8 @@ void Object::xfer( Xfer *xfer ) // No, the contain module is just going to friend_ reach in and set this for us. // Containers more complicated than Open (like Tunnel) can't do that. Our variable, // our responsibility. -#if RETAIL_COMPATIBLE_CRC +#if !RETAIL_COMPATIBLE_CRC // TheSuperHackers @tweak Contained by ID is already set with retail compatibility; don't overwrite it. -#else if( xfer->getXferMode() == XFER_SAVE ) { if( m_containedBy != nullptr ) From c5c0b8e33530581319fa6bd13a5209010b1440e0 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 18 Aug 2026 00:35:41 +0200 Subject: [PATCH 2/4] Revert "bugfix(object): Avoid crash with dangling contain module in Object::onDestroy() when Reinforcement Pad is destroyed before Troop Crawler drop (#2747)" This reverts commit df2224bf17a69e88ead2e0b5429b6578534724ee. --- .../GameEngine/Include/GameLogic/Object.h | 2 +- .../Source/GameLogic/Object/Object.cpp | 64 +++---------------- .../GameEngine/Include/GameLogic/Object.h | 2 +- .../Source/GameLogic/Object/Object.cpp | 64 +++---------------- 4 files changed, 20 insertions(+), 112 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Object.h b/Generals/Code/GameEngine/Include/GameLogic/Object.h index 9bc97105fea..a2dcdd36606 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Object.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Object.h @@ -717,7 +717,7 @@ class Object : public Thing, public Snapshot Object* m_containedBy; /**< an object can only be contained by at most one other object, this is that object (if present) */ - ObjectID m_containedByID; ///< ID of the object we're contained by; only to be used when m_containedBy cannot be used + ObjectID m_xferContainedByID; ///< xfer uses IDs to store pointers and looks them up after UnsignedInt m_containedByFrame; ///< frame we were contained by m_containedBy Real m_constructionPercent; ///< for objects being built ... this is the amount completed (0.0 to 100.0) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 8d91d86a70f..448d1225825 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -173,7 +173,7 @@ Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatu m_physics(nullptr), m_geometryInfo(tt->getTemplateGeometryInfo()), m_containedBy(nullptr), - m_containedByID(INVALID_ID), + m_xferContainedByID(INVALID_ID), m_containedByFrame(0), m_behaviors(nullptr), m_body(nullptr), @@ -621,22 +621,6 @@ void Object::onContainedBy( Object *containedBy ) clearStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_MASKED ) ); m_containedBy = containedBy; m_containedByFrame = TheGameLogic->getFrame(); - -#if RETAIL_COMPATIBLE_CRC - // TheSuperHackers @info Set INVALID_ID if the container object was destroyed - // to indicate that the pointer will become a dangling pointer in the next frame. - if (containedBy && !containedBy->isDestroyed()) - { - m_containedByID = containedBy->getID(); - } - else - { - m_containedByID = INVALID_ID; - } -#else - DEBUG_ASSERTCRASH(containedBy == nullptr || !containedBy->isDestroyed(), - ("Object::onContainedBy - Adding into a destroyed container")); -#endif } //------------------------------------------------------------------------------------------------- @@ -647,10 +631,6 @@ void Object::onRemovedFrom( Object *removedFrom ) clearStatus( MAKE_OBJECT_STATUS_MASK2( OBJECT_STATUS_MASKED, OBJECT_STATUS_UNSELECTABLE ) ); m_containedBy = nullptr; m_containedByFrame = 0; - -#if RETAIL_COMPATIBLE_CRC - m_containedByID = INVALID_ID; -#endif } //------------------------------------------------------------------------------------------------- @@ -701,33 +681,9 @@ void Object::onDestroy() { // This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up. - if (m_containedBy) + if( m_containedBy && m_containedBy->getContain() ) { -#if RETAIL_COMPATIBLE_CRC - if (m_containedByID == INVALID_ID) - { - // TheSuperHackers @bugfix Caball009 25/05/2026 Due to a potential use-after-free bug that cannot be fixed - // with retail compatibility, the 'contained by' pointer of this object may point to an already destroyed object. - // Avoid removing this object from the contain list, because it could crash the game, - // as the begin / end iterator for STLPort and MSVC std::list implementations depends on dynamically allocated memory. - DEBUG_CRASH(("container object must be valid; this looks like use-after-free")); - } - else - { - DEBUG_ASSERTCRASH(TheGameLogic->findObjectByID(m_containedByID) == m_containedBy, - ("contained by pointer is out of sync with contained by ID")); - - if (ContainModuleInterface* contain = m_containedBy->getContain()) - { - contain->removeFromContain(this); - } - } -#else - if (ContainModuleInterface* contain = m_containedBy->getContain()) - { - contain->removeFromContain(this); - } -#endif + m_containedBy->getContain()->removeFromContain( this ); } // @@ -3772,18 +3728,16 @@ void Object::xfer( Xfer *xfer ) // No, the contain module is just going to friend_ reach in and set this for us. // Containers more complicated than Open (like Tunnel) can't do that. Our variable, // our responsibility. -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @tweak Contained by ID is already set with retail compatibility; don't overwrite it. if( xfer->getXferMode() == XFER_SAVE ) { if( m_containedBy != nullptr ) - m_containedByID = m_containedBy->getID(); + m_xferContainedByID = m_containedBy->getID(); else - m_containedByID = INVALID_ID; + m_xferContainedByID = INVALID_ID; } -#endif - xfer->xferObjectID( &m_containedByID ); + + xfer->xferObjectID( &m_xferContainedByID ); } // contained by frame @@ -4008,8 +3962,8 @@ void Object::xfer( Xfer *xfer ) //------------------------------------------------------------------------------------------------- void Object::loadPostProcess() { - if( m_containedByID != INVALID_ID ) - m_containedBy = TheGameLogic->findObjectByID(m_containedByID); + if( m_xferContainedByID != INVALID_ID ) + m_containedBy = TheGameLogic->findObjectByID(m_xferContainedByID); else m_containedBy = nullptr; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h index 2ffcb6c012a..14cdc982a25 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h @@ -760,7 +760,7 @@ class Object : public Thing, public Snapshot Object* m_containedBy; /**< an object can only be contained by at most one other object, this is that object (if present) */ - ObjectID m_containedByID; ///< ID of the object we're contained by; only to be used when m_containedBy cannot be used + ObjectID m_xferContainedByID; ///< xfer uses IDs to store pointers and looks them up after UnsignedInt m_containedByFrame; ///< frame we were contained by m_containedBy Real m_constructionPercent; ///< for objects being built ... this is the amount completed (0.0 to 100.0) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 73d7913da3c..6a8948fdf7d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -182,7 +182,7 @@ Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatu m_physics(nullptr), m_geometryInfo(tt->getTemplateGeometryInfo()), m_containedBy(nullptr), - m_containedByID(INVALID_ID), + m_xferContainedByID(INVALID_ID), m_containedByFrame(0), m_behaviors(nullptr), m_body(nullptr), @@ -691,22 +691,6 @@ void Object::onContainedBy( Object *containedBy ) m_containedBy = containedBy; m_containedByFrame = TheGameLogic->getFrame(); -#if RETAIL_COMPATIBLE_CRC - // TheSuperHackers @info Set INVALID_ID if the container object was destroyed - // to indicate that the pointer will become a dangling pointer in the next frame. - if (containedBy && !containedBy->isDestroyed()) - { - m_containedByID = containedBy->getID(); - } - else - { - m_containedByID = INVALID_ID; - } -#else - DEBUG_ASSERTCRASH(containedBy == nullptr || !containedBy->isDestroyed(), - ("Object::onContainedBy - Adding into a destroyed container")); -#endif - handlePartitionCellMaintenance(); // which should unlook me now that I am contained } @@ -720,10 +704,6 @@ void Object::onRemovedFrom( Object *removedFrom ) m_containedBy = nullptr; m_containedByFrame = 0; -#if RETAIL_COMPATIBLE_CRC - m_containedByID = INVALID_ID; -#endif - handlePartitionCellMaintenance(); // get a clean look, now that I am outdoors, again } @@ -776,33 +756,9 @@ void Object::onDestroy() { // This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up. - if (m_containedBy) + if( m_containedBy && m_containedBy->getContain() ) { -#if RETAIL_COMPATIBLE_CRC - if (m_containedByID == INVALID_ID) - { - // TheSuperHackers @bugfix Caball009 25/05/2026 Due to a potential use-after-free bug that cannot be fixed - // with retail compatibility, the 'contained by' pointer of this object may point to an already destroyed object. - // Avoid removing this object from the contain list, because it could crash the game, - // as the begin / end iterator for STLPort and MSVC std::list implementations depends on dynamically allocated memory. - DEBUG_CRASH(("container object must be valid; this looks like use-after-free")); - } - else - { - DEBUG_ASSERTCRASH(TheGameLogic->findObjectByID(m_containedByID) == m_containedBy, - ("contained by pointer is out of sync with contained by ID")); - - if (ContainModuleInterface* contain = m_containedBy->getContain()) - { - contain->removeFromContain(this); - } - } -#else - if (ContainModuleInterface* contain = m_containedBy->getContain()) - { - contain->removeFromContain(this); - } -#endif + m_containedBy->getContain()->removeFromContain( this ); } // @@ -4291,18 +4247,16 @@ void Object::xfer( Xfer *xfer ) // No, the contain module is just going to friend_ reach in and set this for us. // Containers more complicated than Open (like Tunnel) can't do that. Our variable, // our responsibility. -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @tweak Contained by ID is already set with retail compatibility; don't overwrite it. if( xfer->getXferMode() == XFER_SAVE ) { if( m_containedBy != nullptr ) - m_containedByID = m_containedBy->getID(); + m_xferContainedByID = m_containedBy->getID(); else - m_containedByID = INVALID_ID; + m_xferContainedByID = INVALID_ID; } -#endif - xfer->xferObjectID( &m_containedByID ); + + xfer->xferObjectID( &m_xferContainedByID ); } // contained by frame @@ -4527,8 +4481,8 @@ void Object::xfer( Xfer *xfer ) //------------------------------------------------------------------------------------------------- void Object::loadPostProcess() { - if( m_containedByID != INVALID_ID ) - m_containedBy = TheGameLogic->findObjectByID(m_containedByID); + if( m_xferContainedByID != INVALID_ID ) + m_containedBy = TheGameLogic->findObjectByID(m_xferContainedByID); else m_containedBy = nullptr; From 43a5e69190beef0fcd0dbe6d155412811b38e164 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:05:07 +0200 Subject: [PATCH 3/4] Retained relevant changes. --- .../GameEngine/Include/GameLogic/Object.h | 2 +- .../GameLogic/Object/Contain/HelixContain.cpp | 20 ++++++++++++++----- .../Source/GameLogic/Object/Object.cpp | 12 +++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h index 14cdc982a25..13019ab3fa7 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h @@ -448,7 +448,7 @@ class Object : public Thing, public Snapshot void onContainedBy( Object *containedBy ); void onRemovedFrom( Object *removedFrom ); Int getTransportSlotCount() const; - void friend_setContainedBy( Object *containedBy ) { m_containedBy = containedBy; } + void friend_setContainedBy( Object *containedBy ); const Object* getEnclosingContainedBy() const; ///< Find the first enclosing container in the containment chain. const Object* getOuterObject() const; ///< Get the top-level object diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp index aff9d91941c..c2c470bb3ba 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/HelixContain.cpp @@ -250,10 +250,13 @@ void HelixContain::addToContainList( Object *obj ) if ( portable ) TheGameLogic->destroyObject( portable ); - m_portableStructureID = obj->getID(); - obj->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container + portable = obj; + m_portableStructureID = portable->getID(); + portable->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container + DEBUG_ASSERTCRASH(getObject() == nullptr || !getObject()->isDestroyed(), + ("HelixContain::addToContainList - Adding to a destroyed container")); } else TransportContain::addToContainList( obj ); @@ -268,10 +271,13 @@ void HelixContain::addToContain( Object *obj ) if ( portable ) TheGameLogic->destroyObject( portable ); - m_portableStructureID = obj->getID(); - obj->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container + portable = obj; + m_portableStructureID = portable->getID(); + portable->friend_setContainedBy( getObject() );//fool portable into thinking my object is his container + DEBUG_ASSERTCRASH(getObject() == nullptr || !getObject()->isDestroyed(), + ("HelixContain::addToContain - Adding to a destroyed container")); } else TransportContain::addToContain( obj ); @@ -284,10 +290,14 @@ void HelixContain::removeFromContain( Object *obj, Bool exposeStealthUnits ) { Object *portable = getPortableStructure(); if ( portable ) + { +#if !RETAIL_COMPATIBLE_CRC + portable->friend_setContainedBy(nullptr); +#endif m_portableStructureID = INVALID_ID; //portable->kill(); - + } } else { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 6a8948fdf7d..a9a874cb13c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -691,6 +691,9 @@ void Object::onContainedBy( Object *containedBy ) m_containedBy = containedBy; m_containedByFrame = TheGameLogic->getFrame(); + DEBUG_ASSERTCRASH(containedBy == nullptr || !containedBy->isDestroyed(), + ("Object::onContainedBy - Adding to a destroyed container")); + handlePartitionCellMaintenance(); // which should unlook me now that I am contained } @@ -729,6 +732,15 @@ Int Object::getTransportSlotCount() const return count; } +void Object::friend_setContainedBy(Object* containedBy) +{ + m_containedBy = containedBy; + +#if !RETAIL_COMPATIBLE_CRC + m_containedByFrame = containedBy ? TheGameLogic->getFrame() : 0; +#endif +} + const Object* Object::getEnclosingContainedBy() const { for (const Object* child = this, *container = getContainedBy(); container; child = container, container = container->getContainedBy()) From 831ff73ffb9b529f12f764168c844706823ef37d Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:07:45 +0200 Subject: [PATCH 4/4] Added early container size check to avoid crash. --- .../GameLogic/Object/Contain/OpenContain.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp index baea90bf2c5..cabff786ad2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp @@ -177,6 +177,15 @@ OpenContain::~OpenContain() ("OpenContain %s: m_xferContainIDList is not empty but should be", getObject()->getTemplate()->getName().str() ) ); +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix Caball009 18/08/2026 Due to a potential use-after-free bug that cannot be fixed + // with retail compatibility, it's desirable to be able to check if the contain list is empty after its destruction. + // Empty the list explicitly to reset the list size. + while (!m_containList.empty()) + { + m_containList.pop_front(); + } +#endif } //------------------------------------------------------------------------------------------------- @@ -434,6 +443,15 @@ void OpenContain::removeFromContain( Object *rider, Bool exposeStealthUnits ) } +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix Caball009 18/08/2026 Due to a potential use-after-free bug that cannot be fixed + // with retail compatibility, the 'contained by' pointer of this object may point to an already destroyed object. + // Check the list size before executing the find operation below, otherwise the game crashes if the list + // was already destructed. + if (m_containList.empty()) + return; +#endif + ContainedItemsList::iterator it = std::find(m_containList.begin(), m_containList.end(), rider); if (it != m_containList.end()) {