[UE 5.5] Race in UClass::CreateSparseClassData — Shipping crash during async loading (memcpy from null archetype SCD)

[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]

재현 방법
No deterministic repro case isolated. Reproduces statistically at scale in our live game (~6K devices/day on Android alone):

1. Shipping build (check() compiled out) on UE 5.5.

2. Play normally so that runtime async loading occurs repeatedly (level streaming, on-demand asset loads of Blueprint classes that carry sparse class data — AnimBP-related in our case).

3. The crash fires when the async loading thread serializes a CDO (SerializeSparseClassData -> CreateSparseClassData) while the same class — or its parent in the archetype chain — has its SparseClassData created/accessed concurrently from another thread.

Occurs sporadically from 40 minutes to 23+ hours into a session, foreground, ample free memory, no device/OS/version bias (constant rate across all our releases on 5.5). In a development build the race window can be widened for verification by inserting an artificial delay (e.g. FPlatformProcess::Sleep) in CreateSparseClassData between the Malloc/member assignment and the archetype CopyScriptStruct, then triggering async loads of SCD-bearing Blueprint classes while touching the same classes’ sparse data from the game thread.

[Attachment Removed]

Hello! Thanks for reporting this. I’d like to investigate this deeper, but I need some more information:

  • You mention the crash happens at a steady rate on every product version since 5.5. Which major/minor engine version are you currently on?
  • I understand the GameThread callstack is different per occurrence of the crash, while the Async Loading Thread’s callstack is similar to the one you shared already. Can you still provide 1 or 2 of the GameThread callstacks for reference?
    [Attachment Removed]

After searching our issue tracker a bit more, we had similar crash reports internally where the offender was FAnimSubsystem_Base. UE5 CL 37997548 a.k.a. GitHub commit f350a174e40cd603bd8c635deb32d41b938206ec addressed that. That bugfix is present in UE 5.6.1, but wasn’t in UE 5.5.

Can you see if that fix is already in your engine version?

[Attachment Removed]

Great. I hope cherrypicking that fixed your crash. I’ll close this case, but if the crash occurs again please respond here to let us know.

[Attachment Removed]

Thanks — that was exactly the pointer we needed.

Checked, and no: that fix was not in our engine. We are on a licensee source build of 5.5, and our

FAnimSubsystem_Base

had no copy constructor / copy-assignment override and no critical section at all, so UE5 CL 37997548 (f350a174) had indeed never landed in 5.5.

We have since integrated it. Our 5.5 branch picked up both related CLs on 17 Sep 2026:

  • CL 37997548 — Anim BP: fix race condition when copying sparse class data in base subsystem
  • CL 38418221 — fix race condition between parent and child class sparse class data patching/copying (ALT-enabled builds)

[Attachment Removed]