There seems to be a race condition introduced in 5.6 with multiple Mass processors concurrently writing to UWorld::SubsystemCollection::SubsystemArrayMap.
The call chain is
UMassProcessor::CallExecute ->
FMassExecutionContext::CacheSubsystemRequirements ->
FMassSubsystemAccess::CacheSubsystemRequirements ->
FMassSubsystemAccess::CacheSubsystem ->
FMassSubsystemAccess::FetchSubsystemInstance ->
UWorld::GetSubsystemBase ->
FObjectSubsystemCollection::GetSubsystem ->
FSubsystemCollectionBase::GetSubsystemInternal ->
FSubsystemCollectionBase::FindAndPopulateSubsystemArrayInternal
FSubsystemCollectionBase::GetSubsystemInternal calls Emplace on SubystemArrayMap (which is mutable), when first looking up a subsystem of a given type, and can cause a realloc to a occur.
Between UE 5.5 and 5.6, FMassProcessingPhase::bRunInParallelMode went from defaulting false and getting set true in FMassProcessingPhaseManager::OnPhaseEnd, to defaulting to true instead (and updating in FMassProcessingPhaseManager::OnPhaseStart). So, in 5.5, effectively all Mass processors using the phase processor would run on game thread on their first execution, which prevented this race condition from happening, since they would end up looking up their required subsystems on that first execution. However, with 5.6, the first execution of the Mass processors that may run in parallel do run in parallel (correctly) -- and that first execution can cause a lookup if the subsystem, and concurrent writes to SubystemArrayMap, and interleaved reallocs, which can lead to memory corruption and crashes (which we’ve seen with our upgrade to 5.6).
As a workaround, we’re locally making this change to FMassProcessingPhaseManager::OnPhaseStart, just before setting `GraphBuildState.bInitialized = true;` which ensures that the first lookup of these subsystems (and hence the Emplace call to SubystemArrayMap ) still occurs on the game thread.
// cache subsystem access requirements here, while single-threaded during initialization,
// so that any sid eeffect write access to World::SubsystemCollection's FSubsystemCollectionBase::SubsystemArrayMap is safe
TConstArrayView<UMassProcessor*> ChildProcessors = PhaseProcessor->GetChildProcessorsView();
for (UMassProcessor* ChildProcessor : ChildProcessors)
{
FMassSubsystemAccess SubsystemAccess;
SubsystemAccess.CacheSubsystemRequirements(ChildProcessor->GetProcessorRequirements());
};
This solution seems a bit fragile, though, since it seems like there is a deeper problem with the mutable SubystemArrayMap allowing const access to UWorld from concurrent threads to ultimately result in concurrent write access.
Thanks.