Description

In some occasions, engine code iterates over a certain subsystem type by calling FSubsystemCollection::GetSubsystemArray() and then calling some method on all subsystems of the returned array. This happens, for example, in UWorld::BeginPlay(), which iterates over subsystems of type UWorldSubsystem in an internal SubsystemCollection and calls OnWorldBeginPlay() on each one.

Now, subsystems in general have access to their owner and can call GetSubsystemArray() on it, which forwards the call to the owner's internal SubsystemCollection. If this is done while subsystems are being iterated on (for example, inside a World Subsystem's OnWorldBeginPlay() method), a crash can occur shortly after.

The crash can be explained as follows:

  • FSubsystemCollectionBase caches subsystems of each class and subclass in a TMap<UClass*, TArray<USubsystem*>>.
  • FSubsystemCollectionBase::GetSubsystemArrayInternal() checks if the map above contains the requested subsystem class. If it does not, it adds a new array entry to the map and populates the array with all subsystems of that class. At the end, the method returns a direct reference to the array contained in the map.
  • However, if adding a new array entry to the map causes it to reallocate its internal storage, all array references returned so far can be invalidated.
  • Engine code that iterates on subsystems often get the returned array reference and iterates over its elements. See UWorld::BeginPlay() iterating over UWorldSubsystems and calling OnWorldBeginPlay() on them.
  • During such an iteration, if a subsystem calls GetSubsystemArray() on its owner, the resulting call to FSubsystemCollectionBase::GetSubsystemArrayInternal() can cause the internal map storage to be reallocated, which invalidates the array reference currently being used by the outer iteration code. And this can lead to a crash on the next iteration.

A quick fix was suggested in the related UDN case: code that iterates over subsystems (such as UWorld::BeginPlay()) could make a copy of the subsystem array and iterate on that. Alternatively, FSubsystemCollectionBase::GetSubsystemArrayInternal() could return the array by copy to all of its clients. Another possibility would be to rework the mechanism used by FSubsystemCollectionBase to cache subsystems.

 

Steps to Reproduce

=== Repro Project ===

  • Download, compile and open the repro project available as an attachment of the linked UDN case
  • Start and Stop PIE. Open the Output Log and show only "LogRepro" category.
  • The log will show a list of pointers before and after repeated calls to GetSubsystemArray() from a UWorldSubsystem's OnWorldBeginPlay() method. Note that the first few pointers got invalidated, including the pointer to the array that is being iterated on by UWorld::BeginPlay() at that time. Check the project's "MyWorldSubsystem.cpp" file for details.

=== Blank Project ===

  • Create a blank C++ project
  • Add some trivial subclasses of "UWorldSubsystem" with only UCLASS() and GENERATED_BODY()
  • Add another class "UMyWorldSubsystem" derived from "UWorldSubsystem"
  • Override method OnWorldBeginPlay(). Implementation:
  • Call InWorld.GetSubsystemArray() or GetWorld()->GetSubsystemArray() with "UWorldSubsystem" and then several times with the various trivial subclasses created previously.
  • Repeat all of the calls above a second time.
  • Compile and open the project, then start PIE.
  • Using UE_LOG or breakpoints and watches, note that the address of the returned array is different for the first and second call to GetSubsystemArray<UWorldSubsystem>.
Callstack
[Inline Function] FMallocBinnedCommonBase::FBundle::PushHead(FMallocBinnedCommonBase::FBundleNode* Node) Line 251 C++
[Inline Function] TMallocBinnedCommon<FMallocBinned3,16,128,4,74,131072>::FFreeBlockList::PushToFront(void* InPtr) Line 333 C++
[Inline Function] TMallocBinnedCommon<FMallocBinned3,16,128,4,74,131072>::FPerThreadFreeBlockLists::Free(void* InPtr) Line 473 C++
FMallocBinned3::Realloc(void* Ptr, SIZE_T NewSize, uint32 Alignment) Line 392 C++
FMallocPoisonProxy::Realloc(void* Ptr, SIZE_T NewSize, uint32 Alignment) Line 64 + 0x14 bytes C++
FMemory::Realloc(void* Original, SIZE_T Count, uint32 Alignment) Line 426 + 0x22 bytes C++
TSizedHeapAllocator<32,FMemory>::ForAnyElementType::ResizeAllocation(TSizedHeapAllocator<32,FMemory>::SizeType PreviousNumElements, TSizedHeapAllocator<32,FMemory>::SizeType NumElements, SIZE_T NumBytesPerElement, uint32 AlignmentOfElement) Line 725 + 0xE bytes C++
TArray<TSparseArrayElementOrFreeListLink<TAlignedBytes<32,8u>>,TSizedDefaultAllocator<32>>::ResizeGrow(TArray<TSparseArrayElementOrFreeListLink<TAlignedBytes<32,8u>>,TSizedDefaultAllocator<32>>::SizeType OldNum) Line 3121 C++
[Inline Function] TSparseArray<TSetElement<TTuple<void*,TArray<UStaticMeshComponent*,TSizedDefaultAllocator<32>>>>,TSparseArrayAllocator<TSizedDefaultAllocator<32>,FDefaultBitArrayAllocator>>::AddUninitialized() Line 129 C++
TSet<TTuple<void*,TArray<UStaticMeshComponent*,TSizedDefaultAllocator<32>>>,TDefaultMapHashableKeyFuncs<void*,TArray<UStaticMeshComponent*,TSizedDefaultAllocator<32>>,false>,FDefaultSetAllocator>::Emplace<TKeyInitializer<void*&&>>(TKeyInitializer<void*&&>&& Args, bool* bIsAlreadyInSetPtr) Line 740 C++
[Inline Function] TMapBase<UClass*,TArray<USubsystem*,TSizedDefaultAllocator<32>>,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UClass*,TArray<USubsystem*,TSizedDefaultAllocator<32>>,false>>::Emplace<UClass*const&>(UClass* const& InKey) Line 458 C++
[Inline Function] TMapBase<UClass*,TArray<USubsystem*,TSizedDefaultAllocator<32>>,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UClass*,TArray<USubsystem*,TSizedDefaultAllocator<32>>,false>>::Add(UClass* const& InKey) Line 408 C++
FSubsystemCollectionBase::GetSubsystemArrayInternal(UClass* SubsystemClass) Line 81 C++
UPWGameLoopSubSystem::AddToTick(UObject* _Context, IPWTickableInterface* _Tickable, enum ETickingGroup _TickingGroup) Line 19 C++
UPWGraphSubSystem::OnWorldBeginPlay(UWorld& InWorld) Line 131 C++
UWorld::BeginPlay() Line 5323 + 0xC bytes C++
UEngine::LoadMap(FWorldContext& WorldContext, FURL URL, UPendingNetGame* Pending, FString& Error) Line 15532 C++
UEngine::Browse(FWorldContext& WorldContext, FURL URL, FString& Error) Line 14670 + 0x26 bytes C++
UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds) Line 14868 C++
UGameEngine::Tick(float DeltaSeconds, bool bIdleMode) Line 1775 C++
FEngineLoop::Tick() Line 5921 C++

Have Comments or More Details?

There's no existing public thread on this issue, so head over to Questions & Answers just mention UE-221062 in the post.

3
Login to Vote

Unresolved
ComponentUE - Gameplay
Affects Versions5.4.3
Target Fix5.6
CreatedAug 6, 2024
UpdatedSep 30, 2024
View Jira Issue