[Repro / What was happening]
No deterministic repro — the crash fires sporadically during runtime async loading
(level streaming / on-demand asset loads) in normal mid-session play: samples show
40 min to 23+ hours of playtime, foreground, ample free memory, no device/OS bias.
It reproduces statistically at scale (~6K devices/day) but we have not isolated a
minimal case; frequency correlates with Blueprint classes carrying sparse class
data (AnimBP-related, e.g. FAnimSubsystem_Base in the failing copy).
Full analysis, root cause, and the fix we are considering follow below.
[Environment / Impact]
UE 5.5 licensee source build, Shipping, live RPG on Android/iOS/Windows.
This is our largest crash bucket: ~1.6M accumulated events (~6K devices/day on Android, ~40% of daily Android crashes), at a steady rate on every product version since 5.5 — a persistent engine-level race, not a content regression.
[Symptom]
All samples show the identical stack on FAsyncLoadingThread:
UBlueprintGeneratedClass::SerializeDefaultObject
-> UClass::SerializeSparseClassData
-> UClass::CreateSparseClassData
-> UScriptStruct::CopyScriptStruct (FAnimSubsystem_Base)
-> memcpy (SIGSEGV, src = 0x0, size = 0x10)
i.e. CopyScriptStruct copying from a null archetype SparseClassData pointer, mid-play during runtime async loading, with ample free memory.
[Our root-cause analysis]
In UClass::CreateSparseClassData() (CoreUObject/Private/UObject/Class.cpp):
1. Pointer published before init completes — “SparseClassData = FMemory::Malloc(…)” assigns the member first; InitializeStruct / CopyScriptStruct then run on that member. Lock-free readers (GetSparseClassData / GetOrCreateSparseClassData) can observe a non-null pointer to partially-initialized memory.
2. No synchronization between creators — the async loading thread (CDO serialization) and the game thread can both enter creation for the same class, or for a parent/child chain via GetArchetypeForSparseClassData().
3. In Shipping, check(SparseClassData == nullptr) compiles out, so double-entry proceeds silently. The memcpy(src=null) matches a child SCD creation copying from a parent archetype SCD that is concurrently mid-creation.
We searched the newest streams visible to us and found only a null-deferral mitigation around the copy site — it avoids the null deref but not the underlying publish/creation race.
[Fix we are considering — single .cpp, no ABI change]
Serialize the creation path with a static FCriticalSection (recursive, so the parent-chain creation inside the lock is fine); construct into a local buffer; publish last with a release barrier. Read path stays lock-free (dependent load, safe on ARM64).
FScopeLock Lock(&GSparseClassDataCreationLock);
if (SparseClassData) return SparseClassData; // lost the race — reuse
void* NewSCD = FMemory::Malloc(…);
SparseClassDataStruct->InitializeStruct(NewSCD);
/* unchanged archetype-copy logic, writing into NewSCD */
FPlatformMisc::MemoryBarrier(); // publish only after fully built
SparseClassData = NewSCD;
FCoreUObjectDelegates::OnPostInitSparseClassData.Broadcast(…);
[Questions]
1. Is this a known issue (UE-xxxxx)? Is a proper fix planned for 5.5/5.6, or an existing CL we should backport?
2. Any problems with our approach — e.g. OnPostInitSparseClassData now broadcast while holding the lock (deadlock risk from listeners?), or the single global lock serializing first-touch creation across all classes?
3. Is there a recommended official pattern (atomic CAS publish, or constraining SCD creation to the game thread during async loading)?
Given the volume we intend to ship a local fix in our next release unless there is an official one we can take. Any guidance appreciated.
[Attachment Removed]