Callstack:
UE::CoreUObject::Private::ResolveObjectHandleNoRead (ObjectHandle.h:423)
ObjectPtr_Private::Friend::NoAccessTrackingGet (ObjectPtr.h:876)
ObjectPtr_Private::TNonAccessTrackedObjectPtr<T>::Get (ObjectPtr.h:910)
* (ObjectPtr.h:935)
UStruct::GetSuperStruct (Class.h:673)
UClass::GetSuperClass (Class.h:3490)
UAssetToolsImpl::GetAssetTypeActionsForClass (AssetTools.cpp:1577)
UAssetEditorSubsystem::CanOpenEditorForAsset (AssetEditorSubsystem.cpp:1548)
UAssetEditorSubsystem::OpenEditorForAsset (AssetEditorSubsystem.cpp:505)
FLevelEditorActionCallbacks::EditAsset_Clicked (LevelEditorActions.cpp:1527)
Invoke (Invoke.h:47)
UE::Core::Private::Tuple::TTupleBase<T>::ApplyAfter (Tuple.h:320)
TBaseStaticDelegateInstance<T>::ExecuteIfSafe (DelegateInstancesImpl.h:799)
TDelegate<T>::ExecuteIfBound (DelegateSignatureImpl.inl:635)
FUIAction::Execute (UIAction.h:139)
FUICommandList::ExecuteAction (UICommandList.cpp:117)
SMenuEntryBlock::OnClicked (SMenuEntryBlock.cpp:1188)
SMenuEntryBlock::OnMenuItemButtonClicked (SMenuEntryBlock.cpp:1147)
Invoke (Invoke.h:66)
UE::Core::Private::Tuple::TTupleBase<T>::ApplyAfter (Tuple.h:320)
TBaseSPMethodDelegateInstance<T>::Execute (DelegateInstancesImpl.h:292)
TDelegate<T>::Execute (DelegateSignatureImpl.inl:614)
SButton::ExecuteOnClick (SButton.cpp:495)
Analysis of root cause w/ Claude:
Steps 1, 2 and 5 are confirmed from the dump and the source. Steps 3 and 4 are inferred from the
control flow plus the repro, and are consistent with all the evidence above, but we did not capture
a GC in the act.
**1. The array is invisible to GC.** `EditAsset_Clicked` (stock 5.6.1):
```cpp
TArray< UObject* > ReferencedAssets;
const bool bIgnoreOtherAssetsIfBPReferenced = true;
GEditor->GetReferencedAssetsForEditorSelection( ReferencedAssets, bIgnoreOtherAssetsIfBPReferenced );
// … confirmation dialog …
for (auto Asset : ReferencedAssets)
{
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(Asset, ToolkitMode, LevelEditorSharedPtr);
}
```
Plain `UObject*`, stack-local, no root, no `TObjectPtr`, no `FGCObject`. Nothing in this function
keeps any entry alive.
**2. For a level instance, the entry is the actor itself — not an asset.**
`UEditorEngine::GetReferencedAssetsForEditorSelection` (`EditorEngine.cpp:3429`) walks the selection
calling `AActor::GetReferencedContentObjects`, and `ALevelInstance` overrides it as
(`LevelInstanceActor.cpp:353`):
```cpp
bool ALevelInstance::GetReferencedContentObjects(TArray<UObject*>& Objects) const
{
Objects.Add(const_cast<ALevelInstance*>(this));
return true;
}
```
So despite the variable being named `ReferencedAssets`, the entries are world actors whose lifetime
is bound to level streaming, not to asset loading. This is what makes the window wide enough to hit
in practice: an asset would normally survive the loop because something else has it loaded, whereas
these actors are destroyed by the very operation the loop is performing.
**3. Opening the first entry removes every root holding the rest.**
`OpenEditorForAsset(ALevelInstance*)` → `ALevelInstance::OpenAssetEditor()` → `EnterEdit()` →
`ULevelInstanceSubsystem::EditLevelInstanceInternal` (`LevelInstanceSubsystem.cpp:2456`), which does:
- `GEditor->SelectNone(true, true)` (`:2494`) — drops the `USelection` references. The selection was
the strong reference the array was silently relying on.
- `UnloadLevelInstance(EditLevelInstanceID)` (`:2533`, under the comment “Unload right away”) —
destroys the actors in the unloaded level, which is where the other selected nested level
instances live.
- `GEditor->ResetTransaction(…)` (`:2570`) — drops the undo buffer’s references.
**4. The same function pumps the message loop, so GC can actually run.**
- `FScopedSlowTask SlowTask(…); SlowTask.MakeDialog();` (`:2487-2488`).
- `PromptUserForCommit(…)` (`:2472`) — a modal dialog when a prior edit is dirty.
- `GEngine->BlockTillLevelStreamingCompleted(…)` via `ULevelStreamingLevelInstanceEditor::Load`
(`LevelInstanceEditorLevelStreaming.cpp:122`).
- On the property-override path, `CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true)` outright
(`LevelInstanceEditorPropertyOverrideLevelStreaming.cpp:477`).
With `Ctrl+E` there is an additional pump before the loop even starts, from the “open them all?”
`FMessageDialog`. `Ctrl+Shift+E` skips that one, and still crashes.
**5. The next iteration dereferences freed memory.** `CanOpenEditorForAsset` only null-checks:
```cpp
if (!Asset)
{
…
return false;
}
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT(“AssetTools”));
TWeakPtr<IAssetTypeActions> AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(Asset->GetClass());
```
`Asset->GetClass()` reads the reused block, returns `0x614D67646C425F6C`, and
`GetAssetTypeActionsForClass` walks it. Crash.
This is why a multi-selection is required to hit the bug: with a single entry the loop never returns
to the array after the pump.
[Attachment Removed]