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.