At long last, I’ve gotten to the bottom of this. To be frank, I’m still not 100% sure why the crash was happening, but I’ll explain it as best I can. Hopefully, this will be useful to at least one other person out in the world.
It all came down to my building actors (BP_Mechanism) not handling destruction properly. But, I first need to explain how my moving platforms are handled.
In my game, I have a C++ Actor Component called MovingPlatform. This component handles tweening an actor around for basic moving platform behavior and has lots of customization. More importantly, it sets the component velocity for each component that is attached to itself, every tick. This is to make sure that as a character jumps off the platform, they receive the velocity that they are supposed to.
MovingPlatform has a TArray<USceneComponent*> that stores the scene components it is managing their velocities. At runtime, other actors can add their components through:
void UMovingPlatform::AddVelocityAffectedComponent(USceneComponent* Component)
void UMovingPlatform::RemoveVelocityAffectedComponent(USceneComponent* Component)
However, in my BP_Mechanism, RemoveVelocityAffectedComponent() was not always being called. A Detach bp node was used before the Remove Component node. This meant that in the TArray a USceneComponent pointer would go stale because of gc. Eventually, that would lead to a crash.
So to fix this, I first changed when the RemoveVelocityAffectedComponent() node was called so that it would always execute before DestroyActor() was called. And more importantly, I changed the TArray to TArray<TWeakObjectPtr<USceneComponent>> so that if the pointer went stale, it would not lead to crashes. There are also .IsValid() checks where relevant and a cleanup function if a pointer became invalid for some reason.
All in all, it seemed to be a basic mishandling of pointers. The reason that this was so challenging was that at no point did any crash stack trace I received point to either a Mechanism or a MovingPlatform. In fact, the crashes were often never even through the game thread. It would be some worker thread that would crash. MovingPlatform does use latent actions, but the component TArray was never used outside of Tick. Maybe someone with more experience could explain that lol.
TL;DR if you are receiving this crash in your project, you are likely not managing your pointers in some place you would not expect.