Description

Summary

When CreateShapes_AssumesLocked fails during async body creation, the failed FBodyInstance is left in Bodies[] with a released actor handle. InitDynamicProperties_AssumesLocked is then called on it and dereferences the null handle.

Regression from 5.7. Reported against 5.8 (project upgrade from 5.7, CL 54667078) and confirmed present in //UE5/Main. Requires p.Chaos.EnableAsyncInitBody=true. The same content ran without incident in 5.7.

Callstack

FChaosEngineInterface::SetSleepThresholdMultiplier_AssumesLocked   ChaosEngineInterface.cpp:1261
FBodyInstance::InitDynamicProperties_AssumesLocked                BodyInstance.cpp:4710
TInitBodiesHelperBase<>::InitBodies                               BodyInstance.cpp:1704

(Line numbers are from the reporting CL; the code is unchanged in //UE5/Main — current locations given below.)

Cause

Three things combine.

1. The failure path leaves the instance in `Bodies[]`.

BodyInstance.cpp:1577-1596 — on bInitFail, the instance releases its actor handle and nulls BodySetup, then continue}}s without removing itself from {{Bodies[]/Transforms[]:

Instance->ReleasePhysicsActor();

Instance->OwnerComponent = nullptr;
Instance->BodyInstanceOwner = nullptr;
Instance->SourceObject = nullptr;
Instance->BodySetup = nullptr;
SetBodyInstanceExternalCollisionProfileBodySetup(Instance, nullptr);

continue;

Contrast the "already got a body" branch at :1550-1559, which does Bodies.RemoveAt(...) / Transforms.RemoveAt(...). InitBodies() subsequently iterates Bodies[] and calls InitDynamicProperties_AssumesLocked() on the failed instance.

2. The `BodySetup` guard no longer fires in 5.8.

5.7 guarded with if (!BodySetup.IsValid()) return;, which caught the nulled member. 5.8 guards with if (!GetBodySetup()) return;, and GetBodySetup() (BodyInstance.cpp:1380) now prefers the async creation inputs:

UBodySetup* FBodyInstance::GetBodySetup() const
{
    if (const FBodyInstancePhysicsCreationInputs* Cached = GetAsyncPhysicsCreationInputs())
    {
        return Cached->ResolvedBodySetup;
    }
    ...
}

FBodyInstancePhysicsCreationInputs::ResolvedBodySetup is never reset on the failure path, so GetBodySetup() returns a live UBodySetup and the early-out is skipped. Instance->BodySetup == nullptr is no longer a reliable signal that physics creation failed.

3. `IsDynamic()` returns true for a null handle.

FPhysicsInterface::IsDynamic() is !IsStatic(), and FChaosEngineInterface::IsStatic (ChaosEngineInterface.cpp:415) returns false for an invalid handle:

bool FChaosEngineInterface::IsStatic(const FPhysicsActorHandle& InActorReference)
{
    if(FChaosEngineInterface::IsValid(InActorReference))
    {
        return InActorReference->GetGameThreadAPI().ObjectState() == Chaos::EObjectStateType::Static;
    }

    return false;
}

So the if (FPhysicsInterface::IsDynamic(GetPhysicsActor())) block is entered with a null handle, and SetSleepThresholdMultiplier_AssumesLocked dereferences it.

Repro conditions

  1. p.Chaos.EnableAsyncInitBody=true — body initialized on a worker thread rather than the game thread.
  2. CreateShapes_AssumesLocked returns failure (NumShapes == 0) — e.g. a component requesting collision from a mesh that has no collision geometry.

Suggested fix

Early-out on an invalid actor handle at the top of InitDynamicProperties_AssumesLocked. The handle is the reliable failure signal now that BodySetup is not.

 void FBodyInstance::InitDynamicProperties_AssumesLocked()
 {
+    // Shape creation may have failed and released the actor handle, leaving this
+    // instance in Bodies[]. IsDynamic() returns true for a null handle.
+    if (!FPhysicsInterface::IsValid(GetPhysicsActor()))
+    {
+        return;
+    }
+
     UBodySetup* ResolvedSetup = GetBodySetup();
     if (!ResolvedSetup)
     {
         // This may be invalid following an undo if the BodySetup was a transient object
         return;
     }

Worth considering alongside this: clearing ResolvedBodySetup on the failure path, and/or removing the failed instance from Bodies[]/Transforms[] the way the :1550 branch does — either would also close the hole, and the current asymmetry between the two bail-out paths is itself suspect.

Note on content

The triggering content is also wrong — a component requesting collision from a mesh with no collision geometry is meaningless, and that is being fixed separately. But the engine has already decided to fail that body and released its handle, so it should not then walk into a null dereference. 5.7 tolerated the same content silently.

Affected code

  • Engine/Source/Runtime/Engine/Private/PhysicsEngine/BodyInstance.cpp:1577 — failure path
  • Engine/Source/Runtime/Engine/Private/PhysicsEngine/BodyInstance.cpp:1380GetBodySetup()
  • Engine/Source/Runtime/Engine/Private/PhysicsEngine/BodyInstance.cpp:4660InitDynamicProperties_AssumesLocked()
  • Engine/Source/Runtime/PhysicsCore/Private/ChaosEngineInterface.cpp:415IsStatic()

Have Comments or More Details?

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

0
Login to Vote

Unresolved
CreatedAug 2, 2026
UpdatedAug 2, 2026
View Jira Issue