All advisories
HXA-2026-003HighOpen

Out-of-bounds write in the NetGameEventV2 handler via a client-controlled index

Product
FiveM / FXServer
Component
citizen-server-impl (NetGameEventV2-Handler)
CVSS
8.1
Published
July 27, 2026

Summary

The handler for msgNetGameEventV2 in FXServer processes a client-supplied list of target players. Each of those values is used before any validation as an index into a thread_local bitset that is only sized for the maximum player count. Because the client may send arbitrary 16-bit values, this produces an out-of-bounds read and write at an attacker-chosen offset in the game-state sync thread's thread-local storage.

Any connected client can trigger it at any time — this is memory corruption, not a pure DoS.

Root cause

The processing loop tests and sets bits at the client-supplied position before the player index is validated at all:

thread_local eastl::bitset<roundToType<size_t>(MAX_CLIENTS)> processed;
processed.reset();

for (const uint16_t player : targetPlayersSpan)
{
    if (processed.test(player))   // client-controlled, NO bounds check → OOB read
    {
        continue;
    }
    processed.set(player);        // OOB write

    auto targetClient = clientRegistry->GetClientByNetID(player);  // first check — too late
}

The bitset is only sized for the maximum player count (a few hundred bytes). An access with a value near the 16-bit maximum lands several kilobytes past the object. eastl::bitset::test/set does not check the position in a release build — there the range check is only a debug assert. In addition, the V2 handler is the only one in its neighbourhood without a rate limiter.

Impact

  • DoS: a reliable crash of the sync thread or the process once the write hits an unmapped page or a critical structure.
  • Memory corruption: the write lands in the adjacent thread-local region, where other server components' objects live. Silent corruption of server state is conceivable; the concrete effect depends on the build's heap layout.

Fix

A single range check before the bitset access is enough:

for (const uint16_t player : targetPlayersSpan)
{
    if (player >= MAX_CLIENTS)   // was missing
    {
        continue;
    }
    if (processed.test(player)) { /* ... */ }
}

Additionally recommended: add a rate limiter in line with the neighbouring handlers.