Description

The GameFrameworkComponentManager has a system for registering callbacks to execute in response to event changes. This ends up calling UGameFrameworkComponentManager::CallFeatureStateDelegates which tries to call multiple delegates in a safe way. However, it does not make a copy of the delegate before calling which means that if the RegisteredDelegates array is resized from a callback, it may cause memory problems and crash. The function itself protects against this, but the delegate execution logic does not handle being destroyed in the middle of execution.

The code needs to be changed to either make copies of delegates, or stop it from modifying the raw delegate memory during execution by adding to some sort of list of pending changes that are applied after the delegate execution finishes

Steps to Reproduce

This is based on a licensee report, here is a Lyra test case that illustrates the problem:

  1. Open Lyra in visual studio
  2. Go to LyraPawnExtensionComponent.h and add this code at line 55 below SetPawn Data:
    UFUNCTION()
    void ChangeCallback1(const FActorInitStateChangedParams& Params) {  }
    
    UFUNCTION()
    void ChangeCallback2(const FActorInitStateChangedParams& Params) {  }
    
    UFUNCTION()
    void ChangeCallback3(const FActorInitStateChangedParams& Params) {  }
    
    UFUNCTION()
    void ChangeCallback4(const FActorInitStateChangedParams& Params) {  }
    
    UFUNCTION()
    void ChangeCallback5(const FActorInitStateChangedParams& Params) {  }
    
    UFUNCTION()
    void ChangeCallback6(const FActorInitStateChangedParams& Params) {  }
    
  3. Go to LyraPawnExtensionComponent.cpp and add this code at line 286 inside the if (Params.FeatureName != NAME_ActorFeatureName) block:
    FActorInitStateChangedBPDelegate BindDelegate;
    BindDelegate.FActorInitStateChangedBPDelegate::BindDynamic(this, &ULyraPawnExtensionComponent::ChangeCallback1);
    RegisterAndCallForInitStateChange(Params.FeatureState, BindDelegate);
    
    BindDelegate.FActorInitStateChangedBPDelegate::BindDynamic(this, &ULyraPawnExtensionComponent::ChangeCallback2);
    RegisterAndCallForInitStateChange(Params.FeatureState, BindDelegate);
    
    BindDelegate.FActorInitStateChangedBPDelegate::BindDynamic(this, &ULyraPawnExtensionComponent::ChangeCallback3);
    RegisterAndCallForInitStateChange(Params.FeatureState, BindDelegate);
    
    BindDelegate.FActorInitStateChangedBPDelegate::BindDynamic(this, &ULyraPawnExtensionComponent::ChangeCallback4);
    RegisterAndCallForInitStateChange(Params.FeatureState, BindDelegate);
    
    BindDelegate.FActorInitStateChangedBPDelegate::BindDynamic(this, &ULyraPawnExtensionComponent::ChangeCallback5);
    RegisterAndCallForInitStateChange(Params.FeatureState, BindDelegate);
    
    BindDelegate.FActorInitStateChangedBPDelegate::BindDynamic(this, &ULyraPawnExtensionComponent::ChangeCallback6);
    RegisterAndCallForInitStateChange(Params.FeatureState, BindDelegate);
    
  4. Compile and load Lyra in the editor
  5. Open the L_ShooterGym map
  6. Start play in editor, which may crash. If it does not crash stop PIE and try again. This is a random memory corruption issue
Callstack

This is the stack of where the memory corruption occurs, not the stack of an actual crash which will be random. It will not corrupt until the third delegate is bound which resizes the array:

UnrealEditor-ModularGameplay.dll!UGameFrameworkComponentManager::RegisterAndCallForActorInitState(AActor * Actor, FName FeatureName, FGameplayTag RequiredState, FActorInitStateChangedBPDelegate Delegate, bool bCallImmediately) Line 819
UnrealEditor-ModularGameplay.dll!IGameFrameworkInitStateInterface::RegisterAndCallForInitStateChange(FGameplayTag RequiredState, FActorInitStateChangedBPDelegate Delegate, bool bCallImmediately) Line 239
UnrealEditor-LyraGame.dll!ULyraPawnExtensionComponent::OnActorInitStateChanged(const FActorInitStateChangedParams & Params) Line 296
[Inline Frame] UnrealEditor-ModularGameplay.dll!TDelegate<void __cdecl(FActorInitStateChangedParams const &),FDefaultDelegateUserPolicy>::Execute(const FActorInitStateChangedParams &) Line 549
UnrealEditor-ModularGameplay.dll!UGameFrameworkComponentManager::FActorFeatureRegisteredDelegate::Execute(AActor * OwningActor, FName FeatureName, UObject * Implementer, FGameplayTag FeatureState) Line 552
UnrealEditor-ModularGameplay.dll!UGameFrameworkComponentManager::CallFeatureStateDelegates(AActor * Actor, UGameFrameworkComponentManager::FActorFeatureState StateChange) Line 1044
UnrealEditor-ModularGameplay.dll!UGameFrameworkComponentManager::ProcessFeatureStateChange(AActor * Actor, const UGameFrameworkComponentManager::FActorFeatureState * StateChange) Line 1010
UnrealEditor-ModularGameplay.dll!UGameFrameworkComponentManager::ChangeFeatureInitState(AActor * Actor, FName FeatureName, UObject * Implementer, FGameplayTag FeatureState) Line 725
UnrealEditor-ModularGameplay.dll!IGameFrameworkInitStateInterface::TryToChangeInitState(FGameplayTag DesiredState) Line 98
UnrealEditor-LyraGame.dll!ULyraHeroComponent::BeginPlay() Line 215
UnrealEditor-Engine.dll!AActor::BeginPlay() Line 4231
UnrealEditor-Engine.dll!APawn::BeginPlay() Line 179
UnrealEditor-LyraGame.dll!ALyraCharacter::BeginPlay() Line 92
UnrealEditor-Engine.dll!AActor::DispatchBeginPlay(bool bFromLevelStreaming) Line 4193
UnrealEditor-Engine.dll!AActor::PostActorConstruction() Line 3979
UnrealEditor-Engine.dll!AActor::FinishSpawning(const UE::Math::TTransform<double> & UserTransform, bool bIsDefaultTransform, const FComponentInstanceDataCache * InstanceDataCache, ESpawnActorScaleMethod TransformScaleMethod) Line 3878
UnrealEditor-LyraGame.dll!ALyraGameMode::SpawnDefaultPawnAtTransform_Implementation(AController * NewPlayer, const UE::Math::TTransform<double> & SpawnTransform) Line 368
UnrealEditor-Engine.dll!AGameModeBase::execSpawnDefaultPawnAtTransform(UObject * Context, FFrame & Stack, void * const Z_Param__Result) Line 1511
UnrealEditor-CoreUObject.dll!UFunction::Invoke(UObject * Obj, FFrame & Stack, void * const Z_Param__Result) Line 6921
UnrealEditor-CoreUObject.dll!UObject::ProcessEvent(UFunction * Function, void * Parms) Line 2144
UnrealEditor-Engine.dll!AActor::ProcessEvent(UFunction * Function, void * Parameters) Line 1092
UnrealEditor-Engine.dll!AGameModeBase::SpawnDefaultPawnAtTransform(AController * NewPlayer, const UE::Math::TTransform<double> & SpawnTransform) Line 1464
UnrealEditor-Engine.dll!AGameModeBase::SpawnDefaultPawnFor_Implementation(AController * NewPlayer, AActor * StartSpot) Line 1226
UnrealEditor-Engine.dll!AGameModeBase::execSpawnDefaultPawnFor(UObject * Context, FFrame & Stack, void * const Z_Param__Result) Line 1582
UnrealEditor-CoreUObject.dll!UFunction::Invoke(UObject * Obj, FFrame & Stack, void * const Z_Param__Result) Line 6921
UnrealEditor-CoreUObject.dll!UObject::ProcessEvent(UFunction * Function, void * Parms) Line 2144
UnrealEditor-Engine.dll!AActor::ProcessEvent(UFunction * Function, void * Parameters) Line 1092
UnrealEditor-Engine.dll!AGameModeBase::SpawnDefaultPawnFor(AController * NewPlayer, AActor * StartSpot) Line 1536
UnrealEditor-Engine.dll!AGameModeBase::RestartPlayerAtPlayerStart(AController * NewPlayer, AActor * StartSpot) Line 1299
UnrealEditor-Engine.dll!AGameModeBase::RestartPlayer(AController * NewPlayer) Line 1265

Have Comments or More Details?

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

0
Login to Vote

Fixed
ComponentUE - Gameplay
Affects Versions5.25.5
Target Fix5.530.00
Fix Commit33049490
CreatedFeb 28, 2024
ResolvedApr 17, 2024
UpdatedApr 18, 2024