Hi, there. I’ve encountered a seriously issue with UE5.7.4 engine in Multiplayer project. I’ll post a report below which is about the whole issue include phenomenon and underneath code.
Any suggestion or reply is appreciate, thank you.
Here it the report:
UE 5.7 Seamless Travel + Listen Server Crash Report (World.cpp:7116)
Engine Version: UE 5.7 (built from source, no local patches)
Project Environment: macOS / Windows, native C++ project,
standalone runtime (i.e. outside of Editor PIE)
1. Summary (TL;DR)
When a Listen Server initiates UWorld::ServerTravel(URL, /*bAbsolute=*/true /*bSeamless=*/true)
with at least one remote client connected, the Server reliably crashes 2~5 seconds after
SeamlessTravel completes. The crash window matches exactly the time it takes the remote
client to finish its own SeamlessTravel and send back the NMT_PCSwap control-channel message.
The fault is always:
Engine/Source/Runtime/Engine/Private/World.cpp:7116
UWorld::NotifyControlMessage(UNetConnection*, uint8, FInBunch&)
if (NetDriver->ServerConnection) // ← NetDriver == nullptr
EXCEPTION_ACCESS_VIOLATION reading address 0x0000000000000108
0x108 is the offset of UNetDriver::ServerConnection inside UNetDriver, so the
fault address corresponds to a null this->NetDriver being dereferenced.
In other words: a control message is being dispatched into a UWorld whose
NetDriver field has been silently zeroed out.
2. Repro Environment
| Item | Value |
|---|---|
| Engine version | UE 5.7 (release branch) |
| Platform | macOS / Windows (both reproduce) |
| Network mode | Listen Server (player-hosted, not Dedicated Server) |
| Launch mode | -game command line / Standalone packaged build → always crashes |
| Launch mode (control) | Editor PIE (GIsEditor==true) → does not reproduce (see §6) |
| Subsystems involved | OSS LAN session, control channel (NMT_PCSwap), FLevelCollection |
| Player Pawn | ADefaultPawn (irrelevant to crash, mentioned only to show minimality) |
| Custom NetDriver / ReplicationGraph | None |
Custom UEngine subclass |
None (default UGameEngine) |
GameMode::bUseSeamlessTravel |
true |
Override of GetSeamlessTravelActorList / PostSeamlessTravel |
None |
3. Repro Steps
- Minimal project: two maps,
LobbyandOrigin, twoAGameModesubclasses
(bUseSeamlessTravel = true). - A trivial
TransitionMap(justWorldSettings,GameModeBase). - Launch the host with
-game, host a LAN session via OSS,
ServerTravel("Lobby?listen"). - Launch a client with
-game, discover the LAN session and join. Lobby networking is fine. - On the host, call
World->ServerTravel("/Game/Map/Origin/Origin?GameExperience=...", true /*bAbsolute*/).
Note that?listenis not carried in the new URL (we expect the engine to retain
the listen-server context across SeamlessTravel). - Both ends complete SeamlessTravel; the host crashes when the remote client’s
NMT_PCSwaparrives.
4. Crash Stack & Live Inspection
Crash site:
[Callstack] UWorld::NotifyControlMessage()
[Engine/Source/Runtime/Engine/Private/World.cpp:7116]
void UWorld::NotifyControlMessage(UNetConnection* Connection, uint8 MessageType, FInBunch& Bunch)
{
if (NetDriver->ServerConnection) // 7116
{
check(Connection == NetDriver->ServerConnection);
...
}
Incoming message: MessageType == 15 → NMT_PCSwap
(Engine/Source/Runtime/Engine/Public/Net/DataChannel.h:
DEFINE_CONTROL_CHANNEL_MESSAGE(PCSwap, 15, int32);,
“client tells server it has completed a swap of its Connection->Actor”).
Watch values captured at the crash breakpoint (entry of UWorld::NotifyControlMessage,
prior to the null deref):
| Expression | Value |
|---|---|
this->GetName() |
Origin |
this->NetDriver |
nullptr (root cause of the deref) |
Connection->Driver |
valid pointer |
Connection->Driver->Notify == this |
true |
Connection->Driver->World |
== this (i.e. Origin) |
Connection->OwningActor->GetClass()->GetName() |
CBInGamePlayerController (the new PC, swap finished) |
Connection->OwningActor->GetWorld()->GetName() |
Origin |
((APlayerController*)Connection->OwningActor)->PendingSwapConnection |
nullptr |
There is a one-way break in the bidirectional pointer relationship:
NetDriver ──Notify/World──▶ Origin (intact)
NetDriver ◀──NetDriver──── Origin (nullptr — was zeroed in reverse)
UNetConnection::OwningActor, its world, and PendingSwapConnection are all
in their expected post-swap terminal state. The bug is not in the message
producer (the client) nor in the PC-swap protocol; it is that Origin->NetDriver
gets reverse-cleared to null by some engine-side path after the legitimate
SeamlessTravel migration has happened.
5. Root-Cause Analysis (engine-side)
5.1 CopyWorldData migrates only two collection types
Engine/Source/Runtime/Engine/Private/World.cpp:8395 CopyWorldData:
const FLevelCollection* CurrentCollection = CurrentWorld->FindCollectionByType(ELevelCollectionType::DynamicSourceLevels);
const FLevelCollection* CurrentStaticCollection= CurrentWorld->FindCollectionByType(ELevelCollectionType::StaticLevels);
const FLevelCollection* LoadedCollection = LoadedWorld->FindCollectionByType (ELevelCollectionType::DynamicSourceLevels);
const FLevelCollection* LoadedStaticCollection = LoadedWorld->FindCollectionByType (ELevelCollectionType::StaticLevels);
if (CurrentCollection && LoadedCollection) {
LoadedCollection->SetNetDriver(NetDriver);
CurrentCollection->SetNetDriver(nullptr);
}
if (CurrentStaticCollection && LoadedStaticCollection) {
LoadedStaticCollection->SetNetDriver(NetDriver);
CurrentStaticCollection->SetNetDriver(nullptr);
}
LoadedWorld->SetNetDriver(NetDriver);
NetDriver->SetWorld(LoadedWorld);
But ELevelCollectionType actually has three values
(Engine/Source/Runtime/Engine/Classes/Engine/EngineTypes.h:4239):
enum class ELevelCollectionType : uint8
{
DynamicSourceLevels, // CopyWorldData ✓
DynamicDuplicatedLevels, // CopyWorldData ✗ (skipped)
StaticLevels, // CopyWorldData ✓
};
DynamicDuplicatedLevels is never touched by CopyWorldData. Its NetDriver
slot stays at the default value (nullptr) it was initialized with.
5.2 SetActiveLevelCollection reverse-overwrites World->NetDriver
Engine/Source/Runtime/Engine/Private/World.cpp:9776 UWorld::SetActiveLevelCollection:
ActiveLevelCollectionIndex = LevelCollectionIndex;
const FLevelCollection* const ActiveLevelCollection = GetActiveLevelCollection();
...
GameState = ActiveLevelCollection->GetGameState();
NetDriver = ActiveLevelCollection->GetNetDriver(); // ← reverse overwrite
DemoNetDriver = ActiveLevelCollection->GetDemoNetDriver();
That is, World->NetDriver is forcibly overwritten every time the active
LevelCollection is switched, with the per-collection NetDriver — including
nullptr if that collection never received one.
5.3 LevelCollection context switching during UWorld::Tick
Engine/Source/Runtime/Engine/Private/LevelTick.cpp — UWorld::Tick iterates over
LevelCollections and uses FScopedLevelCollectionContextSwitch to temporarily
make each one the “active” collection:
for (int32 LevelCollectionIndex = 0; LevelCollectionIndex < LevelCollections.Num(); ++LevelCollectionIndex)
{
FScopedLevelCollectionContextSwitch LevelContext(LevelCollectionIndex, this);
...
}
The scope guard calls SetActiveLevelCollection on enter / exit. When the iteration
hits the DynamicDuplicatedLevels collection (whose NetDriver==nullptr),
World->NetDriver is overwritten to nullptr for the duration of that scope.
Nothing in the engine restores it before the following control-message dispatch.
Presumably the original design assumed that
World->NetDriverreads would only
happen against the “main” collection’s value. ButUWorld::NotifyControlMessage
is invoked from the NetDriver itself throughDriver->Notify, and it has no
way of knowing which LevelCollection context it lands inside; once it dereferences
this->NetDriver, it is at the mercy of whatever the most recent
SetActiveLevelCollectionwrote.
5.4 Conditions that produce the third collection
Engine/Source/Runtime/Engine/Private/World.cpp:8537 (post-SeamlessTravel):
if (!GIsEditor && !IsRunningDedicatedServer() && bSwitchedToDefaultMap)
{
LoadedWorld->DuplicateRequestedLevels(LoadedWorld->GetOuter()->GetFName());
}
These three predicates form exactly the Listen-Server -game / packaged
configuration:
!GIsEditor: standalone process!IsRunningDedicatedServer(): listen server (player-hosted)bSwitchedToDefaultMap: SeamlessTravel just finished
DuplicateRequestedLevels may create a DynamicDuplicatedLevels collection.
That collection is not included in CopyWorldData’s migration and therefore
retains a nullptr NetDriver indefinitely.
5.5 Full causal chain
1. ServerTravel(URL, bSeamless=true) → FSeamlessTravelHandler.
2. CopyWorldData(Lobby → Origin):
• Origin->NetDriver = NetDriver ✓
• Driver->World/Notify = Origin ✓
• migrates Dynamic + Static collection NetDrivers, **skips DynamicDuplicatedLevels**.
3. World.cpp:8537 — under -game listen server — calls DuplicateRequestedLevels.
• Origin acquires a DynamicDuplicatedLevels collection with NetDriver = nullptr.
4. UWorld::Tick iterates LevelCollections; FScopedLevelCollectionContextSwitch
activates the DynamicDuplicatedLevels collection.
• SetActiveLevelCollection sets Origin->NetDriver = nullptr.
5. The remote client finishes its own SeamlessTravel and sends NMT_PCSwap.
6. The server's NetDriver dispatches → Driver->Notify->NotifyControlMessage(this=Origin).
7. World.cpp:7116 reads this->NetDriver->ServerConnection.
• this->NetDriver == nullptr → read at offset 0x108 → CRASH.
6. Why Editor PIE Hides the Bug
| Launch mode | GIsEditor |
DuplicateRequestedLevels runs |
Crash |
|---|---|---|---|
| Editor PIE Host | true | No (short-circuited by !GIsEditor) |
✗ Does not crash |
-game Host |
false | Yes | ✓ Reproduces |
| Standalone packaged Host | false | Yes | ✓ Reproduces |
This matches §5.4 perfectly and is independent corroboration of the root cause.
7. Eliminated Hypotheses (project-side bisection)
| Hypothesis | Mitigation tested | Result |
|---|---|---|
Missing TransitionMap (engine falls back to /Temp/Untitled) |
Added explicit TransitionMap config |
Same crash, same address |
Missing net.AllowPIESeamlessTravel |
Added [ConsoleVariables] net.AllowPIESeamlessTravel=1 |
Only affects PIE — irrelevant for -game |
Custom UActorComponent::SetIsReplicatedByDefault(true) on PC subobject |
Disabled replication | Same crash, same address |
DOREPLIFETIME(FPrimaryAssetId) on GameState |
Removed | Same crash, same address |
Custom APlayerController subclass |
Inspected — has no business logic in path | Unrelated |
OwningActor / PendingSwapConnection mis-state |
Captured at break — all in expected terminal state | Unrelated |
The project’s own code is conclusively not the trigger.
8. NMT_PCSwap Is the Trigger, Not the Cause
NMT_PCSwap is simply the first post-SeamlessTravel control-channel message
that lands in UWorld::NotifyControlMessage on the server. The window during
which Origin->NetDriver is nullptr opens earlier than the NMT_PCSwap arrival,
but no other code path dereferences World->NetDriver during that window —
so the failure surfaces only when a control message arrives.
In other words, any control message reaching World::NotifyControlMessage
during the bad window will hit the same null deref. NMT_PCSwap just happens to
be the earliest one in practice.
9. Suggested Engine-side Fixes
We believe any one of the following would resolve the race. We list them in
order of how localized the change is. Selection should be left to Epic, since
Demo / Replay subsystems also share these data structures.
Fix A — Make CopyWorldData migrate every collection type
Replace the two hand-written (Dynamic, Static) migration blocks with an
exhaustive loop:
for (ELevelCollectionType Type : { ELevelCollectionType::DynamicSourceLevels,
ELevelCollectionType::DynamicDuplicatedLevels,
ELevelCollectionType::StaticLevels })
{
auto* Cur = CurrentWorld->FindCollectionByType(Type);
auto* Loaded = LoadedWorld ->FindCollectionByType(Type);
if (Cur && Loaded)
{
Loaded->SetNetDriver(NetDriver);
Cur ->SetNetDriver(nullptr);
}
}
Or use for (FLevelCollection& C : LoadedWorld->GetLevelCollections()) for
forward-compatibility with future collection types.
Fix B — Populate NetDriver inside DuplicateRequestedLevels
When DuplicateRequestedLevels creates a DynamicDuplicatedLevels collection,
inherit the NetDriver / DemoNetDriver from the source dynamic collection so
the new collection is born ready.
Fix C — SetActiveLevelCollection should not write nullptr
If the active collection has no NetDriver of its own, do not reverse-overwrite
World->NetDriver = nullptr; preserve the previous value:
if (UNetDriver* NewND = ActiveLevelCollection->GetNetDriver())
{
NetDriver = NewND;
}
// else: keep World->NetDriver as-is
This may interact with Demo recording semantics; please review.
Fix D — Defensive null check in UWorld::NotifyControlMessage
The most conservative back-stop: add a NetDriver != nullptr guard at line 7116
and either re-route through the live NetDriver of the current WorldContext,
or drop the message with a one-shot warning. This does not address the root
cause but it would prevent the hard crash for affected users.
10. Project-side Workaround
Until Epic ships a fix, our project has switched the relevant transitions to
hard travel:
LobbyGameMode::bUseSeamlessTravel = falsefor the Lobby→InGame transition;- Player data that previously rode on
APlayerStateacross SeamlessTravel is now
carried by URL options (?Name=,?GameExperience=) and aUGameInstanceSubsystem
cache keyed byFUniqueNetId; - Future InGame→InGame transitions (e.g. the “next floor” of a roguelike) will
also use hard travel + explicit data carrying.
We chose not to ship a local engine patch, because Fixes A/B/C all involve
cross-system semantics best decided by Epic.
11. Contact / Attachments
We can provide additional artifacts on request:
- Crash dumps (.dmp)
- Full server-side log
- Minimal repro project
Reference paths in our codebase:
Source/ChaosBilliards/GameFramework/GameMode/CBLobbyGameMode.cpp(ServerTravel call site)Source/ChaosBilliards/GameFramework/GameMode/CBInGameGameMode.cpp(target GameMode)Config/DefaultEngine.ini(GameMapsSettings.TransitionMap,[ConsoleVariables])
The full host-side log of the crash sequence is in compile error.txt,
lines 1170–1230 (from LobbyGameMode::OnStartGameExperienceLoaded to the final callstack).