Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions Generals/Code/GameEngine/Include/GameLogic/Object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -721,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)
Expand Down
74 changes: 9 additions & 65 deletions Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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(),
Comment thread
Caball009 marked this conversation as resolved.
("Object::onContainedBy - Adding into a destroyed container"));
#endif
}

//-------------------------------------------------------------------------------------------------
Expand All @@ -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
}

//-------------------------------------------------------------------------------------------------
Expand All @@ -674,15 +654,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())
Expand Down Expand Up @@ -710,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 );
}

//
Expand Down Expand Up @@ -3781,19 +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.
#else
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
Expand Down Expand Up @@ -4018,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;

Expand Down
6 changes: 1 addition & 5 deletions GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,6 @@ class Object : public Thing, public Snapshot
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
Expand Down Expand Up @@ -764,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,23 +255,8 @@ void HelixContain::addToContainList( Object *obj )
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 );
Expand All @@ -291,23 +276,8 @@ void HelixContain::addToContain( Object *obj )
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::addToContain - Adding to a destroyed container"));
#endif
}
else
TransportContain::addToContain( obj );
Expand All @@ -321,9 +291,7 @@ void HelixContain::removeFromContain( Object *obj, Bool exposeStealthUnits )
Object *portable = getPortableStructure();
if ( portable )
{
#if RETAIL_COMPATIBLE_CRC
portable->friend_setContainedByID(INVALID_ID);
#else
#if !RETAIL_COMPATIBLE_CRC
portable->friend_setContainedBy(nullptr);
#endif
Comment thread
Caball009 marked this conversation as resolved.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just call m_containList.clear() ?

{
m_containList.pop_front();
}
#endif
}

//-------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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())
{
Expand Down
64 changes: 10 additions & 54 deletions GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -691,21 +691,8 @@ 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
("Object::onContainedBy - Adding to a destroyed container"));

handlePartitionCellMaintenance(); // which should unlook me now that I am contained

Expand All @@ -720,10 +707,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

}
Expand Down Expand Up @@ -785,33 +768,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 );
}
Comment thread
Caball009 marked this conversation as resolved.

//
Expand Down Expand Up @@ -4300,19 +4259,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.
#else
if( xfer->getXferMode() == XFER_SAVE )
{
if( m_containedBy != nullptr )
m_containedByID = m_containedBy->getID();
m_xferContainedByID = m_containedBy->getID();
Comment thread
Caball009 marked this conversation as resolved.
else
m_containedByID = INVALID_ID;
m_xferContainedByID = INVALID_ID;
}
#endif

xfer->xferObjectID( &m_containedByID );

xfer->xferObjectID( &m_xferContainedByID );
}

// contained by frame
Expand Down Expand Up @@ -4537,8 +4493,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;

Expand Down
Loading