ControlRig crash due to array used in foreach loop being reallocated within loop - FModularRigModel::TraverseModules:~541

If execution is within UpdateModuleHierarchyFromCDO -> ForEachModule and LoadStrongReference is called, a PostLoad call may cause the ModularRigModel to re-cache the CachedChildren property on ModuleReferences. The problem is these arrays are being iterated on in FModularRigModel::TraverseModules when they are modified. My solution is to preload all modules like so:

// the CDO owns the model - when we ask for the model we'll always
		// get the model from the CDO. we'll now add UObject module instances
		// for each module (data only) reference in the model.
		// Note: The CDO does not contain any UObject module instances itself.
		const FModularRigModel& Model = GetModularRigModel();
// Begin #ControlRig_ArrayReallocCrash
		// preload as this can cause traversal transient begin iterator pointers to be invalid ( see ForEachModule -> TraverseModules )
		TMap<FName, TStrongObjectPtr<UObject>> LoadedReferences;
		for (const FRigModuleReference& ModuleReference : Model.Modules)
		{
			FControlRigAssetStrongReference AssetReference = ModuleReference.ControlRigAssetReference.Get();
			if (IsInGameThread() && !AssetReference.IsValid())
			{
				AssetReference = ModuleReference.ControlRigAssetReference.LoadStrongReference(/*bForceLoad=*/true);
			}
			LoadedReferences.Add(ModuleReference.Name, TStrongObjectPtr(AssetReference.Get()));
		}
 
		Model.ForEachModule([this, &Model, &LoadedReferences](const FRigModuleReference* InModuleReference) -> bool
// End #ControlRig_ArrayReallocCrash
		{
			check(InModuleReference);
			FControlRigAssetStrongReference AssetReference = InModuleReference->ControlRigAssetReference.Get();
// Begin #ControlRig_ArrayReallocCrash
			TStrongObjectPtr<UObject>* ModuleObject = LoadedReferences.Find(InModuleReference->Name);
			const bool bModuleWasPreloaded = ModuleObject != nullptr && ModuleObject->Get() == AssetReference.Get();
			checkf(bModuleWasPreloaded, TEXT("ControlRig Preload Failure - Name Found (%s)"), *LexToString(ModuleObject != nullptr));
			// if (IsInGameThread() && !AssetReference.IsValid())
			// {
			// 	AssetReference = InModuleReference->ControlRigAssetReference.LoadStrongReference(/*bForceLoad=*/true);
			// }
// End #ControlRig_ArrayReallocCrash

Hopefully, that’s enough to go on? I considered instead of preloading, just iterating on Model.Modules instead of using ForEachModule. That might be the cleaner solution but I’m not familiar enough with this code.

Looking to see if this is on Epic’s radar or a fix is incoming? I could not find anything in perforce.

[Attachment Removed]

Steps to Reproduce
Unclear how to repro.

[Attachment Removed]

Hi, thanks for reporting this. I think that the change you’ve made looks sensible, generally it seems safer to load all of the rig modules up front. Having said that, from digging through the code, I’m not sure how the crash you’re seeing would actually happen in practice. The ControlRigAssetReference should be a rig module, not a modular rig. So if the LoadStrongReference call was to result in a PostLoad it should be UControRig::PostLoad, not UModularRig::PostLoad. My guess is that either the rig module is referencing a modular rig somehow, or the current modular rig somehow hasn’t had PostLoad called on it, and the call to LoadStrongReference forces any pending loads to complete.

Does this happen for you in the editor, or a packaged build? And would you be able to provide a callstack for the crash? The other thing would be whether you can debug the calls to UpdateModuleHierarchyFromCDO and FModularRigModel::UpdateCachedChildren. The thing we’d be looking for is a callstack when the model’s children are modified. So it’d be when UpdateCachedChildren is called on the same model that the ForEachModule was just called on in UpdateModuleHierarchyFromCDO.

[Attachment Removed]