Skip to content

Fix push sync vehicle syncer hijack#4887

Open
Zephkek wants to merge 1 commit intomultitheftauto:masterfrom
Zephkek:fix-pushsync-syncer-hijack
Open

Fix push sync vehicle syncer hijack#4887
Zephkek wants to merge 1 commit intomultitheftauto:masterfrom
Zephkek:fix-pushsync-syncer-hijack

Conversation

@Zephkek
Copy link
Copy Markdown
Contributor

@Zephkek Zephkek commented May 4, 2026

Summary

Prevent vehicle push sync from replacing an existing unoccupied vehicle syncer.

Conditions for whether a player can claim syncer of a vehicle:

  • there is no existing syncer
  • there is no "persistent syncer" (set when setElementSyncer is used)
  • there is a contact element

Motivation

Pushsync accepted a nearby client's vehicle ID and could call OverrideSyncer even when another player already owned sync. This let forged pushsync packets hijack vehicle sync authority.

Test plan

  • Review the pushsync path to confirm it now only fills missing syncers.

Checklist

  • Your code should follow the coding guidelines.
  • Smaller pull requests are easier to review. If your pull request is beefy, your pull request should be reviewable commit-by-commit.

@Zephkek Zephkek force-pushed the fix-pushsync-syncer-hijack branch from 7b1c34a to 45d6c5e Compare May 4, 2026 11:46
Copy link
Copy Markdown
Member

@Lpsd Lpsd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, thanks!

if (pVehicle->GetSyncer() != pPlayer && pVehicle->GetTimeSinceLastPush() >= MIN_PUSH_ANTISPAM_RATE &&

CPlayer* pSyncer = pVehicle->GetSyncer();
const bool bCanClaimSync = !pSyncer || (!IsSyncerPersistent() && pPlayer->GetContactElement() == pVehicle);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the server side equivalent of this check?

// Sync Stuff
// if it's not a local vehicle + it collided with the local player
if (pVehicleClientEntity->IsLocalEntity() == false && pCollidedWithClientEntity == g_pClientGame->GetLocalPlayer())
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep -- 6df1094

Copy link
Copy Markdown
Member

@qaisjp qaisjp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This affects sync when the player ped is "pushing" a vehicle, by walking into it.

pPlayer->GetContactElement() == pVehicle is the main change here.

This could in theory cause 1.5s of desync for a player vehicle, if the "contact" packet arrives after the vehicle push packet.

If that happens, the server will reject the syncer change and the client will stop sending syncer packets for ~1.5s (because of the anti-span check client side)

This seems OK given that:

  • if there are other players in the vicinity, the other players will have their own simulation of the push
  • the player doing the pushing is likely pushing for more than 1.5s, so the next push packet is likely to succeed
  • if the player is pushing a vehicle carefully (where packets being lost for 1.5s would be bad), they have likely already set foot in the vehicle before

That said, is this change strictly necessary, given that we have the iVehicleContactSyncRadius check?

@qaisjp
Copy link
Copy Markdown
Member

qaisjp commented May 10, 2026

Question for you @Zephkek!

@Zephkek
Copy link
Copy Markdown
Contributor Author

Zephkek commented May 10, 2026

@qaisjp Thanks for the review, to answer your question yes I believe this check is necessary because the radius check alone doesn't prevent a cheating client from hijacking sync (default 30 units which is about 4 car lengths). This PR fixes the ability to forge or unintended push packet from a player who is near a vehicle but not actually pushing it so radius alone passes that. GetContactElement() == pVehicle is checking if the player is actually in contact with the vehicle and gate sourced from the independent puresync stream, which the push packet can't unilaterally fake.

So the two checks aren't redundant the contact check is what specifically prevents syncer hijack and it's only enforced when there's already a syncer to take over from so the no-syncer "first one to nudge it" remains unchanged as intended.

@Zephkek
Copy link
Copy Markdown
Contributor Author

Zephkek commented May 10, 2026

If that happens, the server will reject the syncer change and the client will stop sending syncer packets for ~1.5s (because of the anti-span check client side)

Also about the 1.5s desync concern, in theory yes client can lose 1.5s of push sync packets if the contact element update arrives after the first push packet but that window is bounded by exactly the case where it least matters:

  • It only triggers when another player is already the syncer of the same vehicles otherwise (!pSyncer bypasses the contact check) meaning someone elses simulation is already authoritative so visible drift is minimal

  • Persistent syncers bypass it via !IsSyncerPersistent()

  • For non persistent cases with an existing syncer, the legitimate displacing player will set foot on/ collide with the vehicle within a frame or two of starting to push and the next push packet after MIN_PUSH_ANTISPAM_RATE will go through normally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants