Fix use-after-free crash and misaligned protocol access

Stop active ScenePlayer animation in OnActorEnter/OnActorExit before
modifying ride animation state — the ScenePlayer may still hold a
reference to the ride vehicle ROI that ClearRideAnimation frees.
Deactivate() and OnWorldDisabled() already had this guard.

Add alignment padding to MessageHeader (13→14 bytes) so uint16_t fields
in packed protocol structs no longer sit at odd offsets (UBSan violation).
Breaking wire format change — all clients and relay must update together.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Semmler 2026-03-28 11:04:06 -07:00
parent f3c6d90cf1
commit e57164d345
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
5 changed files with 40 additions and 19 deletions

View File

@ -78,6 +78,7 @@ enum LightChangeType : uint8_t {
struct MessageHeader { struct MessageHeader {
uint8_t type; uint8_t type;
uint8_t _pad;
uint32_t peerId; uint32_t peerId;
uint32_t sequence; uint32_t sequence;
uint32_t target; uint32_t target;

View File

@ -175,7 +175,7 @@ MxResult NetworkManager::Tickle()
} }
else if (IsConnected()) { else if (IsConnected()) {
AnimCancelMsg cancelMsg{}; AnimCancelMsg cancelMsg{};
cancelMsg.header = {MSG_ANIM_CANCEL, m_localPeerId, m_sequence++, TARGET_HOST}; cancelMsg.header = {MSG_ANIM_CANCEL, 0, m_localPeerId, m_sequence++, TARGET_HOST};
SendMessage(cancelMsg); SendMessage(cancelMsg);
} }
m_localPendingAnimInterest = -1; m_localPendingAnimInterest = -1;
@ -326,7 +326,7 @@ void NetworkManager::CancelLocalAnimInterest()
} }
else if (IsConnected()) { else if (IsConnected()) {
AnimCancelMsg msg{}; AnimCancelMsg msg{};
msg.header = {MSG_ANIM_CANCEL, m_localPeerId, m_sequence++, TARGET_HOST}; msg.header = {MSG_ANIM_CANCEL, 0, m_localPeerId, m_sequence++, TARGET_HOST};
SendMessage(msg); SendMessage(msg);
} }
@ -638,7 +638,7 @@ void NetworkManager::ProcessPendingRequests()
} }
else if (IsConnected()) { else if (IsConnected()) {
AnimInterestMsg msg{}; AnimInterestMsg msg{};
msg.header = {MSG_ANIM_INTEREST, m_localPeerId, m_sequence++, TARGET_HOST}; msg.header = {MSG_ANIM_INTEREST, 0, m_localPeerId, m_sequence++, TARGET_HOST};
msg.animIndex = animIndex; msg.animIndex = animIndex;
ThirdPersonCamera::Controller* animCam = GetCamera(); ThirdPersonCamera::Controller* animCam = GetCamera();
msg.displayActorIndex = animCam ? animCam->GetDisplayActorIndex() : 0; msg.displayActorIndex = animCam ? animCam->GetDisplayActorIndex() : 0;
@ -715,7 +715,7 @@ void NetworkManager::BroadcastLocalState()
} }
PlayerStateMsg msg{}; PlayerStateMsg msg{};
msg.header = {MSG_STATE, m_localPeerId, m_sequence++, TARGET_BROADCAST}; msg.header = {MSG_STATE, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST};
msg.actorId = actorId; msg.actorId = actorId;
msg.worldId = inRestrictedArea ? WORLD_NOT_VISIBLE : (int8_t) currentWorld->GetWorldId(); msg.worldId = inRestrictedArea ? WORLD_NOT_VISIBLE : (int8_t) currentWorld->GetWorldId();
msg.vehicleType = DetectVehicleType(userActor); msg.vehicleType = DetectVehicleType(userActor);
@ -1066,7 +1066,7 @@ void NetworkManager::SendEmote(uint8_t p_emoteId)
cam->TriggerEmote(p_emoteId); cam->TriggerEmote(p_emoteId);
EmoteMsg msg{}; EmoteMsg msg{};
msg.header = {MSG_EMOTE, m_localPeerId, m_sequence++, TARGET_BROADCAST}; msg.header = {MSG_EMOTE, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST};
msg.emoteId = p_emoteId; msg.emoteId = p_emoteId;
SendMessage(msg); SendMessage(msg);
} }
@ -1201,7 +1201,7 @@ bool NetworkManager::IsClonedCharacter(const char* p_name) const
void NetworkManager::SendCustomize(uint32_t p_targetPeerId, uint8_t p_changeType, uint8_t p_partIndex) void NetworkManager::SendCustomize(uint32_t p_targetPeerId, uint8_t p_changeType, uint8_t p_partIndex)
{ {
CustomizeMsg msg{}; CustomizeMsg msg{};
msg.header = {MSG_CUSTOMIZE, m_localPeerId, m_sequence++, TARGET_BROADCAST_ALL}; msg.header = {MSG_CUSTOMIZE, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST_ALL};
msg.targetPeerId = p_targetPeerId; msg.targetPeerId = p_targetPeerId;
msg.changeType = p_changeType; msg.changeType = p_changeType;
msg.partIndex = p_partIndex; msg.partIndex = p_partIndex;
@ -1664,7 +1664,7 @@ void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localIn
AnimUpdateMsg NetworkManager::BuildAnimUpdateMsg(uint16_t p_animIndex, uint32_t p_target) AnimUpdateMsg NetworkManager::BuildAnimUpdateMsg(uint16_t p_animIndex, uint32_t p_target)
{ {
AnimUpdateMsg msg{}; AnimUpdateMsg msg{};
msg.header = {MSG_ANIM_UPDATE, m_localPeerId, m_sequence++, p_target}; msg.header = {MSG_ANIM_UPDATE, 0, m_localPeerId, m_sequence++, p_target};
msg.animIndex = p_animIndex; msg.animIndex = p_animIndex;
const Animation::AnimSession* session = m_animSessionHost.FindSession(p_animIndex); const Animation::AnimSession* session = m_animSessionHost.FindSession(p_animIndex);
@ -1699,7 +1699,7 @@ void NetworkManager::SendAnimUpdateToPlayer(uint16_t p_animIndex, uint32_t p_tar
void NetworkManager::BroadcastAnimStart(uint16_t p_animIndex) void NetworkManager::BroadcastAnimStart(uint16_t p_animIndex)
{ {
AnimStartMsg msg{}; AnimStartMsg msg{};
msg.header = {MSG_ANIM_START, m_localPeerId, m_sequence++, TARGET_BROADCAST}; msg.header = {MSG_ANIM_START, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST};
msg.animIndex = p_animIndex; msg.animIndex = p_animIndex;
SendMessage(msg); SendMessage(msg);
@ -1720,7 +1720,7 @@ void NetworkManager::BroadcastAnimComplete(uint16_t p_animIndex)
} }
AnimCompleteMsg msg{}; AnimCompleteMsg msg{};
msg.header = {MSG_ANIM_COMPLETE, m_localPeerId, m_sequence++, TARGET_BROADCAST}; msg.header = {MSG_ANIM_COMPLETE, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST};
msg.eventId = (static_cast<uint64_t>(SDL_rand_bits()) << 32) | static_cast<uint64_t>(SDL_rand_bits()); msg.eventId = (static_cast<uint64_t>(SDL_rand_bits()) << 32) | static_cast<uint64_t>(SDL_rand_bits());
msg.objectId = animInfo->m_objectId; msg.objectId = animInfo->m_objectId;
msg.participantCount = 0; msg.participantCount = 0;

View File

@ -1,10 +1,10 @@
// Protocol constants — must stay in sync with protocol.h // Protocol constants — must stay in sync with protocol.h
// MessageHeader binary layout: type(1) + peerId(4) + sequence(4) + target(4) = 13 bytes // MessageHeader binary layout: type(1) + _pad(1) + peerId(4) + sequence(4) + target(4) = 14 bytes
export const HEADER_SIZE = 13; export const HEADER_SIZE = 14;
export const PEER_ID_OFFSET = 1; export const PEER_ID_OFFSET = 2;
export const SEQUENCE_OFFSET = 5; export const SEQUENCE_OFFSET = 6;
export const TARGET_OFFSET = 9; export const TARGET_OFFSET = 10;
// Routing target constants // Routing target constants
export const TARGET_BROADCAST = 0; export const TARGET_BROADCAST = 0;
@ -19,7 +19,7 @@ export const MSG_ASSIGN_ID = 0xff;
// AssignIdMsg: compact server-only message — type(1) + peerId(4) // AssignIdMsg: compact server-only message — type(1) + peerId(4)
const ASSIGN_ID_SIZE = 1 + 4; const ASSIGN_ID_SIZE = 1 + 4;
// HostAssignMsg: header(13) + hostPeerId(4) // HostAssignMsg: header(14) + hostPeerId(4)
const HOST_ASSIGN_SIZE = HEADER_SIZE + 4; const HOST_ASSIGN_SIZE = HEADER_SIZE + 4;
export function createAssignIdMsg(peerId: number): ArrayBuffer { export function createAssignIdMsg(peerId: number): ArrayBuffer {

View File

@ -237,7 +237,7 @@ MxBool WorldStateSync::HandleSkyLightMutation(uint8_t p_entityType, uint8_t p_ch
void WorldStateSync::SendSnapshotRequest() void WorldStateSync::SendSnapshotRequest()
{ {
RequestSnapshotMsg msg{}; RequestSnapshotMsg msg{};
msg.header = {MSG_REQUEST_SNAPSHOT, m_localPeerId, m_sequence++, TARGET_HOST}; msg.header = {MSG_REQUEST_SNAPSHOT, 0, m_localPeerId, m_sequence++, TARGET_HOST};
SendMessage(msg); SendMessage(msg);
m_snapshotRequested = true; m_snapshotRequested = true;
@ -275,7 +275,7 @@ void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId)
stateBuffer[dataLength++] = (uint8_t) lightPos; stateBuffer[dataLength++] = (uint8_t) lightPos;
WorldSnapshotMsg msg{}; WorldSnapshotMsg msg{};
msg.header = {MSG_WORLD_SNAPSHOT, m_localPeerId, m_sequence++, p_targetPeerId}; msg.header = {MSG_WORLD_SNAPSHOT, 0, m_localPeerId, m_sequence++, p_targetPeerId};
msg.dataLength = (uint16_t) dataLength; msg.dataLength = (uint16_t) dataLength;
std::vector<uint8_t> msgBuf(sizeof(WorldSnapshotMsg) + dataLength); std::vector<uint8_t> msgBuf(sizeof(WorldSnapshotMsg) + dataLength);
@ -288,7 +288,7 @@ void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId)
void WorldStateSync::BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex) void WorldStateSync::BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{ {
WorldEventMsg msg{}; WorldEventMsg msg{};
msg.header = {MSG_WORLD_EVENT, m_localPeerId, m_sequence++, TARGET_BROADCAST}; msg.header = {MSG_WORLD_EVENT, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST};
msg.entityType = p_entityType; msg.entityType = p_entityType;
msg.changeType = p_changeType; msg.changeType = p_changeType;
msg.entityIndex = p_entityIndex; msg.entityIndex = p_entityIndex;
@ -298,7 +298,7 @@ void WorldStateSync::BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeT
void WorldStateSync::SendWorldEventRequest(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex) void WorldStateSync::SendWorldEventRequest(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{ {
WorldEventRequestMsg msg{}; WorldEventRequestMsg msg{};
msg.header = {MSG_WORLD_EVENT_REQUEST, m_localPeerId, m_sequence++, TARGET_HOST}; msg.header = {MSG_WORLD_EVENT_REQUEST, 0, m_localPeerId, m_sequence++, TARGET_HOST};
msg.entityType = p_entityType; msg.entityType = p_entityType;
msg.changeType = p_changeType; msg.changeType = p_changeType;
msg.entityIndex = p_entityIndex; msg.entityIndex = p_entityIndex;

View File

@ -105,6 +105,16 @@ void Controller::OnActorEnter(IslePathActor* p_actor)
return; return;
} }
// Stop external animation before modifying ride/display state —
// the ScenePlayer may hold a reference to the ride vehicle ROI.
if (m_animPlaying) {
if (m_animStopCallback) {
m_animStopCallback();
}
m_animPlaying = false;
m_animStopCallback = nullptr;
}
LegoROI* newROI = userActor->GetROI(); LegoROI* newROI = userActor->GetROI();
if (!newROI) { if (!newROI) {
return; return;
@ -157,6 +167,16 @@ void Controller::OnActorExit(IslePathActor* p_actor)
return; return;
} }
// Stop external animation before clearing ride animation state —
// the ScenePlayer may hold a reference to the ride vehicle ROI.
if (m_animPlaying) {
if (m_animStopCallback) {
m_animStopCallback();
}
m_animPlaying = false;
m_animStopCallback = nullptr;
}
if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) {
m_animator.ClearRideAnimation(); m_animator.ClearRideAnimation();
m_animator.ClearAll(); m_animator.ClearAll();