null ObjectItem during garbage collection when cooking game in unreal 5.6

We’re getting a crash when cooking our game when updating to unreal 5.6. We’re using a custom engine build, though with fairly minimal modifications, so I am not currently aware how to reproduce this issue with a vanilla build.

The crash occurs due to GUnreachableObjects containing null entries during garbage collection in the cook process. When the crash happens in the project there always appear to be two entries in GUnreachableObject, both containing null pointers. Could you help us in the right direction with what might trigger an issue like this, and how to go about debugging it?

[Image Removed]

These are the logged lines in the time just before the crash:

```

[2025.06.17-13.50.17:818][ 0]LogWorldPartition: Display: [Cook] Sending 186 packages to be generated.

[2025.06.17-13.50.17:819][ 0]LogWorldPartition: Display: [Cook] Debug(GetGenerateList) : OwnerObject=World /Game/Maps/PerformanceTests/Performance/MAP_Battlefield_FormationLODSwitching_1000.MAP_Battlefield_FormationLODSwitching_1000 bForceInitializedWorld=1 bInitializedPhysicsSceneForSave=1

[2025.06.17-13.50.17:845][ 0]LogCook: Display: Splitting Package /Game/Maps/PerformanceTests/Performance/MAP_Battlefield_FormationLODSwitching_1000 with splitter FWorldPartitionCookPackageSplitter acting on object World /Game/Maps/PerformanceTests/Performance/MAP_Battlefield_FormationLODSwitching_1000.MAP_Battlefield_FormationLODSwitching_1000.

[2025.06.17-13.50.17:857][ 0]LogWorldPartition: Display: [Cook][PopulateGeneratorPackage] Processing 186 packages

[2025.06.17-13.50.18:243][ 0]LogWorldPartition: Display: [Cook][PopulateGeneratorPackage] Gathered 18 modified packages

[2025.06.17-13.50.50:847][ 0]LogCook: Display: Garbage collection triggered (Full). Triggered by conditions:

CookSettings.MemoryMinFreeVirtual: Available virtual memory 1658MiB is less than 2048MiB.

[2025.06.17-13.50.50:847][ 0]LogCook: Display: Cooked packages 670 Packages Remain 11539 Total 12209

[2025.06.17-13.50.50:848][ 0]LogCook: Display: Cook Diagnostics: OpenFileHandles=3022, VirtualMemory=36160MiB, VirtualMemoryAvailable=1658MiB

[2025.06.17-13.50.50:848][ 0]LogCookCommandlet: Display: GarbageCollection… (Exceeded Max Memory)

[2025.06.17-13.50.51:756][ 0]LogUObjectHash: Compacting FUObjectHashTables data took 14.49ms

[2025.06.17-13.50.51:815][ 0]LogActorComponent: UnregisterComponent: (/Game/Maps/PlayableInstance/Battlefield/MAP_YmirBattlefieldTest_01.MAP_YmirBattlefieldTest_01:PersistentLevel.StaticMeshActor_UAID_D843AEDFB27FC67002_2128923619.RoadWaypointComponent0) Not registered. Aborting.

[2025.06.17-13.50.51:815][ 0]LogActorComponent: UnregisterComponent: (/Game/Maps/PlayableInstance/Battlefield/MAP_YmirBattlefieldTest_01.MAP_YmirBattlefieldTest_01:PersistentLevel.StaticMeshActor_UAID_E89C25C21D86446C02_1865376513.RoadWaypointComponent1) Not registered. Aborting.

[2025.06.17-13.50.51:818][ 0]LogOutputDevice: Warning:

Script Stack (0 frames) :

Assertion failed: ObjectItem [File:D:\p4\ue_integration\Engine\Source\Runtime\CoreUObject\Private\UObject\GarbageCollection.cpp] [Line: 4764]

A breakpoint instruction (__debugbreak() statement or a similar call) was executed in UnrealEditor-Win64-Debug.exe.

```

This is the call stack:

> UnrealEditor-CoreUObject-Win64-Debug.dll!IncrementalDestroyGarbage(bool bUseTimeLimit, double TimeLimit) Line 4764 C++

UnrealEditor-CoreUObject-Win64-Debug.dll!IncrementalPurgeGarbage(bool bUseTimeLimit, double TimeLimit) Line 4660 C++

UnrealEditor-CoreUObject-Win64-Debug.dll!UE::GC::PostCollectGarbageImpl<1>(EObjectFlags KeepFlags) Line 5713 C++

UnrealEditor-CoreUObject-Win64-Debug.dll!UE::GC::FReachabilityAnalysisState::PerformReachabilityAnalysisAndConditionallyPurgeGarbage(bool bReachabilityUsingTimeLimit) Line 5907 C++

UnrealEditor-CoreUObject-Win64-Debug.dll!UE::GC::FReachabilityAnalysisState::CollectGarbage(EObjectFlags KeepFlags, bool bFullPurge) Line 5775 C++

UnrealEditor-CoreUObject-Win64-Debug.dll!CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge) Line 6153 C++

UnrealEditor-UnrealEd-Win64-Debug.dll!UCookCommandlet::ConditionalCollectGarbage(unsigned int TickResults, UCookOnTheFlyServer & COTFS) Line 747 C++

UnrealEditor-UnrealEd-Win64-Debug.dll!UCookCommandlet::RunCookByTheBookCook(UCookOnTheFlyServer * CookOnTheFlyServer, void * StartupOptionsAsVoid, ECookByTheBookOptions CookOptions) Line 599 C++

UnrealEditor-UnrealEd-Win64-Debug.dll!UCookCommandlet::CookByTheBook(const TArray<ITargetPlatform *,TSizedDefaultAllocator<32>> & Platforms) Line 550 C++

UnrealEditor-UnrealEd-Win64-Debug.dll!UCookCommandlet::Main(const FString & CmdLineParams) Line 268 C++

UnrealEditor-Win64-Debug.exe!FEngineLoop::PreInitPostStartupScreen(const wchar_t * CmdLine) Line 3860 C++

UnrealEditor-Win64-Debug.exe!FEngineLoop::PreInit(const wchar_t * CmdLine) Line 4159 C++

UnrealEditor-Win64-Debug.exe!EnginePreInit(const wchar_t * CmdLine) Line 40 C++

UnrealEditor-Win64-Debug.exe!GuardedMain(const wchar_t * CmdLine) Line 143 C++

UnrealEditor-Win64-Debug.exe!LaunchWindowsStartup(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow, const wchar_t * CmdLine) Line 271 C++

UnrealEditor-Win64-Debug.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * pCmdLine, int nCmdShow) Line 339 C++

We haven’t hit this before, so you’ll need to try a few different debugging steps.

  • This problem could occur with Incremental gather enabled. But incremental gather is supposed to be hardcoded off. Add a check statement to make sure that GatherUnreachableObjects is always run
  • Possibly GIsIncrementalReachabilityPending is getting changed in between the call to GatherUnreachableObjects and the call to IncrementalPurgeGarbage. Save the value and assert it did not change.
  • Possibly nulls are being added into the GUnreachableObjecsArray during GatherUnreachableObjects. Assert that’s not the case.

To test those 3 possibilities, add this code around and inside PostCollectGarbageImpl:

`// NEWCODESTART
void GCCrash_ValidateGUnreachableObjects()
{
for (int32 Index = 0; Index < GUnreachableObjects.Num(); ++Index)
{
const UE::GC::FUnreachableObject& Element = GUnreachableObjects[Index];
if (!Element.ObjectItem)
{
checkf(false, TEXT(“GUnreachableObjects has invalid null ObjectItem at index %d.”), Index);
}
}
}
// NEWCODEEND

template
void PostCollectGarbageImpl(EObjectFlags KeepFlags)
{
const double PostCollectStartTime = FPlatformTime::Seconds();

using namespace UE::GC;
using namespace UE::GC::Private;

// NEWCODESTART
bool bGatherCalled = false;
// NEWCODEEND
if (!GIsIncrementalReachabilityPending)
{

if (bPerformFullPurge || !GAllowIncrementalGather || !FGCFlags::IsIncrementalGatherUnreachableSupported())
{
GatherUnreachableObjects(GatherOptions, /TimeLimit =/ 0.0);
// NEWCODESTART
bGatherCalled = true;
GCCrash_ValidateGUnreachableObjects();
// NEWCODEEND
}
// NEWCODESTART
else
{
// !IsIncrementalGatherUnreachableSupported should be true so we should never get here
check(false);
}
// NEWCODEEND
}

GIsGarbageCollectingAndLockingUObjectHashTables = false;
UnlockUObjectHashTables();

// The hash tables lock was released when reachability analysis was done.
// BeginDestroy, FinishDestroy, destructors and callbacks are allowed to call functions like StaticAllocateObject and StaticFindObject.
// Now release the GC lock to allow async loading and other threads to perform UObject operations under the FGCScopeGuard.
ReleaseGCLock();

if (!GIsIncrementalReachabilityPending)
{
// NEWCODESTART
check(bGatherCalled);
// NEWCODEEND
…`

Hey Matt,

Thank you for the help with debugging this!

Unfortunately, adding the additional code you supplied above does not trigger the crash, we still crash in the original location.

I did find out that it’s caused by a particular piece of code which in BeginDestroy() of a (non actor/component) UObject calls Destroy() on 2 actors. I’m not sure if this is unsupported, but I’m removing the offending code in the meanwhile which fixes this crash for us!

Okay, calling Destroy from BeginDestroy should be legal, so I think there is still a bug here. But we don’t need to spend your time trying to find out what the failing edge case is if you’ve already worked around it. Let me know if you want to pick this up and resume debugging.

The next idea I have for investigation is whether the timeslicing code in IncrementalPurgeGarbage, IncrementalDestroyGarbage, and DestroyObjects is at fault. Usually the timeslicing code is not even active, so it might not be possible. To investigate that

  • Add a log in IncrementalPurgeGarbage if bUseTimeLimit is true, to see if that is ever called
  • Add a log in DestroyObjects, after it sets UnreachableObject.Object = nullptr, if it encounters the break in the while loop in the block that has this comment: // Time slicing when running on the game thread