In URigHiearchyController::AddElement (and other functions), arbitrary delegate code is called while holding Hierarchy->ElementsLock, through the Notify function.
URigHierarchy::AddReferencedObjects takes this same lock during garbage collection.
Calling arbitrary code while holding a lock makes the code vulnerable to a deadlock, and in this case, because of the need for the lock during garbage collection, the deadlock is caused by the delegate itself.
URighHiearchy.ModifiedEvent is a delegate called by notify, and there exist some UObjects that subscribe to it via AddUObject, such as in ControlRig.cpp.
AddUObject uses a TWeakObjectPtr, and calls Internal_Pin on it during the delegate’s broadcast, which includes an FGCScopeGuard.
So on the task thread, we enter the locks in the order
And on the GameThread running garbage collection, we enter them in the opposite order
Which causes a deadlock.
The likely solution is to call Notify outside the lock.
But we call Notify inside the lock because it passes pointers to private data that might be deleted by other functions called on the URigHiearchy.
To call Notify outside the lock, we need either to pass a copy of the data instead of a pointer, or we need a separate locking structure that adds a readlock that prevents those pointers from being modified or deleted while the Notify is being called inside the readlock but outside of the ElementsLock.
Callstacks of the deadlock included in comments.
It should be possible to create a synthetic repro with custom c++ instrumentation:
There's no existing public thread on this issue, so head over to Questions & Answers just mention UE-374431 in the post.