Garbage Collection no longer calls out invalid object references and is difficult to debug when this occurs

Garbage collection has undergone a couple rewrites during 5.0s lifetime at this point. During one of those rewrites rather useful functionality was lost where Garbage Collection could call out UPROPERTYed object pointers that pointed to invalid objects (almost always caused by a gameplay bug of some kind- such as copying dangling raw pointers into a UPROPERTYed raw pointer, or USTRUCTs with UPROPERTY raw pointers that don’t initialize the pointers, etc). This code wasn’t perfect but it was generally good enough/incredibly useful when it did catch issues.

This output (as reproed in the attached UE4 project) would look like this:

Fatal error: [File:D:/Build/++UE4/Sync/Engine/Source/Runtime/CoreUObject/Private/UObject/GarbageCollection.cpp] [Line: 961] Invalid object in GC: 0x000001ff287a56c0, ReferencingObject: ActorWithBadGCRefs /Game/UEDPIE_0_Map.Map:PersistentLevel.ActorWithBadGCRefs_1, ReferencingObjectClass: Class /Script/UE4GCObjTest.ActorWithBadGCRefs, Property Name: Ref, Offset: 760, TokenIndex: 36

UE4Editor_CoreUObject!TFastReferenceCollector<FGCReferenceProcessor<1>,FGCCollector<1>,FGCArrayPool,1>::ProcessObjectArray() [D:\Build\++UE4\Sync\Engine\Source\Runtime\CoreUObject\Public\UObject\FastReferenceCollector.h:751]
UE4Editor_CoreUObject!TFastReferenceCollector<FGCReferenceProcessor<1>,FGCCollector<1>,FGCArrayPool,1>::FCollectorTaskQueue::DoTask() [D:\Build\++UE4\Sync\Engine\Source\Runtime\CoreUObject\Public\UObject\FastReferenceCollector.h:403]

Now, however, in more recent versions of the engine these issues are no longer called out- and not only are they no longer called out but the callstacks where this will cause a crash are incredibly cryptic and lack the context needed to debug the problem because references are queued without being checked and we don’t crash until the queue is processed- at which point the context of what object/property added the reference is long gone.

Assertion failed: IsValidIndex(Index) [File:D:\build\++UE5\Sync\Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectArray.h] [Line: 812] 
IsValidIndex(362619)


UnrealEditor_Core!FDebug::CheckVerifyFailedImpl2() [D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Private\Misc\AssertionMacros.cpp:728]
UnrealEditor_CoreUObject!UE::GC::TBatchDispatcher<UE::GC::TReachabilityProcessor<5> >::FlushQueuedReferences() [D:\build\++UE5\Sync\Engine\Source\Runtime\CoreUObject\Private\UObject\GarbageCollection.cpp:3142]
UnrealEditor_CoreUObject!UE::GC::TFastReferenceCollector<UE::GC::TReachabilityProcessor<5>,UE::GC::TReachabilityCollector<5> >::ProcessObjectArray() [D:\build\++UE5\Sync\Engine\Source\Runtime\CoreUObject\Public\UObject\FastReferenceCollector.h:945]
UnrealEditor_CoreUObject!`UE::GC::FRealtimeGC::CollectReferencesForGC<UE::GC::TReachabilityCollector<5>,UE::GC::TReachabilityProcessor<5> >'::`3'::<lambda_1>::<lambda_invoker_cdecl>() [D:\build\++UE5\Sync\Engine\Source\Runtime\CoreUObject\Private\UObject\GarbageCollection.cpp:4014]
UnrealEditor_CoreUObject!UE::Tasks::Private::TExecutableTaskBase<`UE::GC::FRealtimeGC::BeginInitialReferenceCollection'::`5'::<lambda_1>,void,void>::ExecuteTask() [D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Public\Tasks\TaskPrivate.h:904]

We have improved this locally by adding the following checks to the engine that objects are valid as they are added as unvalidated references to the reference batcher (which I’m sure is against a lot of what the reference batcher is trying to do for performance since this requires we read the uobject on push/queue)- but a more robust check for invalid objects like previous versions of GC used to have would be much appreciated (even if only in certain builds/development builds- this could even be done as a pre-GC verification step).

[Image Removed]

[Image Removed]

Any improvements here would be appreciated! Thanks!

Steps to Reproduce
Attached are 2 repro projects with the exact same code that introduce bad/questionable objects into garbage collection in the same way.

For the UE4 project:

  1. Build and launch the project
  2. Play in the default map
  3. Notice it will crash instantly with a nice error like: “Fatal error: [File:D:/Build/++UE4/Sync/Engine/Source/Runtime/CoreUObject/Private/UObject/GarbageCollection.cpp] [Line: 961] Invalid object in GC: 0x000002a93974bf70, ReferencingObject: ActorWithBadGCRefs /Game/UEDPIE_0_Map.Map:PersistentLevel.ActorWithBadGCRefs_1, ReferencingObjectClass: Class /Script/UE4GCObjTest.ActorWithBadGCRefs, Property Name: Ref, Offset: 760, TokenIndex: 36”

For the UE5 project:

  1. Build and launch the project
  2. Play in the default map
  3. If you step through the code in AActorWithBadGCRefs::Tick notice that many pieces of code that _really should crash_ don’t (collecting dangling object, collecting invalid objects that happen to have valid internal index values, etc).
  4. Notice it will eventually crash with a much more cryptic error like: “Assertion failed: IsValidIndex(Index) [File:D:\build\++UE5\Sync\Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectArray.h] [Line: 812] IsValidIndex(362619)”
  5. Notice if you attempt to debug the crash (even if you are attached in VS with full memory when the crash occurred) it’s pretty much impossible to find what introduced the bad object into the reference batcher.

Hi,

It looks like we might either not do those checks by default anymore or the reference batching is indeed a code path that is no longer covered by the checks we have in place.

Could you check if setting gc.ForceEnableGCProcessor 1 does make a difference and also whether ENABLE_GC_OBJECT_CHECKS is set for you (that should be on in debug/development builds by default)?

Thanks!

Kind Regards,

Sebastian