Skip to content

Server (Windows) 38435: memory corruption in the parallel per-client sync tick (5 crashes in 22 h: double free, corrupted object_pool free lists, corrupted control block) #4384

Open

Description

@yw1gara

What happened?

Our production server crashed 4 times in 16.5 hours on 38435 (2026-11-07 03:39 to 20:03 UTC), within the first 35 hours after we deployed it. All four crashes are inside the same function: the body of the per-client lambda passed to tbb::parallel_for_each in ServerGameState::Tick (ServerGameState.cpp L1409), which is citizen-server-state-fivesv.dll+0x1a240 (the function that references playerEnteredScope / playerLeftScope / onPlayerDropped). Two crashed on a TBB worker and two on UV loop: svSync. There are no script frames on any stack.

Time (UTC) Players Uptime Thread Fault Dump
A 2026-11-07 03:39 331 3h10m TBB worker 0xC0000374, double free (heap_failure_block_not_busy) c0c2d142-6f8c-4d7a-8159-12b3060d8e23
B 2026-11-07 05:01 417 1h22m svSync AV, object_pool<SyncCommandList> free-queue pop 01333ecd-efd0-450c-9633-d7170d4f48b9
C 2026-11-07 18:06 ~560 1h29m TBB worker AV, object_pool<SyncCommand> free-queue pop 01ce17fe-055b-4c5e-82de-084ba26852a2
D 2026-11-07 20:03 ~165 0h28m svSync AV, shared_reference<SyncCommandList>::reset() releasing its control block bdf34df9-2b23-4f8e-b7d6-75afc3a793dc

I don't have PDBs. I matched functions by disassembly against the source at 5c3065c. The struct offsets below line up with object_pool.h, shared_reference.h and detached_queue.h. Please confirm with symbols.

A: double free under frameStates.erase

ntdll!RtlFreeHeap (fail-fast, HEAP_FAILURE_INFORMATION FailureType 8 = block not busy)
ucrtbase!free+0x208b
citizen-server-state-fivesv.dll+0x35fe0
citizen-server-state-fivesv.dll+0x4971c
citizen-server-state-fivesv.dll+0x1c79b   <- per-client lambda
citizen-server-state-fivesv.dll+0x4eaf0   <- TBB task

I matched the free to clientDataUnlocked->frameStates.erase(firstFrameState); (L1993-L2012). The freed block was already free.

B and C: corrupted m_tail in a thread-local bucket_entry::free_queue

Both crashes fault on the same instruction, mov rax,[rdx], which is m_tail->next in detached_mpsc_queue::pop_until_empty (L112-L113). They are two instantiations of the same code: +0xed70 and +0xee50. Each one is reached from the thread-local bucket_proxy → bucket_entry::allocate() (TLS slots +0x238 and +0x250).

  • B (+0xed87 ← +0x41c40 ← lambda +0x1a322): the pool of shared_reference<sync::SyncCommandList, &syncPool>::Construct (L1440-L1441). m_tail pointed to unmapped memory.

  • C (+0xee67 ← +0x48c7e ← +0x1cb3b ← lambda +0x1c605). The function at +0x1cac0 builds a FixedFunction<…,128> (invoker pointers at +0x80/+0x88) and pushes the key at +0x90. That is SyncCommandList::EnqueueCommand → syncPool.construct → commands.push (ServerGameState.h L1181-L1184), so this is the SyncCommand pool.

    From the full dump, the bucket_entry at 0x21b692dab80 holds:

    • m_head (+0x00) = 0x21c4d3f7440
    • m_tail (+0x80) = 0x00d0021c4d827f68, non-canonical
    • m_stub.next (+0x88) = 0x21c4d1ea1a0

    The low 48 bits of m_tail do not point to a free_queue_key. Entries in that pool instance are 0xA8 bytes with the key at +0xA0, and 0x21c4d827f68 falls inside the callback storage of a SyncCommand entry. So a node's next was overwritten while the node was on the free queue: a write after free, or the same entry freed twice.

D: shared_reference<SyncCommandList>::reset() with a corrupted control block

citizen-server-state-fivesv.dll+0x3288b   cmp byte ptr [rdx+110h],0   ; rdx = bucket = nullptr
citizen-server-state-fivesv.dll+0x1c95b   per-client lambda, end of scope (destructor of `scl`)

+0x32810 is shared_reference::reset() (L97-L113) for SyncCommandList; the value's object_entry::pool is at value+0x18 = sizeof(SyncCommandList).

  1. Both counters reached zero, and Pool->destruct(value) succeeded.
  2. control_blocks.destruct(block) then read block's object_entry::pool → pool_instance::bucket → bucket->expired (object_pool.h L284, expired at bucket+0x110). The bucket pointer was null.

The full dump shows what went wrong. Control-block entries are 24 bytes (control_block 8, pool 8, free_queue_key 8):

block-0x18: 00000000'00000000  pool=00000184'b9e73048  key=00000184'b9e7c4e8
block     : 00000000'00000000  pool=00000183'b9e73048  key=00000184'b9ec5400   <- ours
block+0x18: 00000000'00000000  pool=00000184'b9e73048  key=00000000'00000000

0x184b9e73048 is a real pool_instance: bucket 0x183c4b30580, object_count 0xaaaa = 1 MiB / 24. Our block's pool is exactly 0x1'0000'0000 lower. Something did a 32-bit decrement on the upper half of block->pool (block+0xC). That matches dec_weak() called through a control-block pointer of block+8, or dec_ref() through block+0xC. In other words, a reference was released through a stale or corrupted control-block pointer. That last part is my inference.

Common pattern (inference, not confirmed)

All four crashes are use-after-free or double-free signatures in memory that the per-client lambda allocates or frees: frameStates, SyncCommandList, SyncCommand and control_blocks. The lambda carries the upstream comment at L1415:

// #TODO: imagine if this mutates state alongside but after OnDrop clears it. WHAT COULD GO WRONG?

At this population players join and drop every few seconds. A lifetime race between the per-client tick and client drop/cleanup is my best guess.

Expected result

FXServer keeps running; no memory corruption in the OneSync per-client tick.

Reproduction steps

Observed 4 times on production in 16.5 hours. I cannot trigger it on demand.

  1. Windows FXServer 38435, OneSync with sv_maxclients 2000 (big mode), onesync_population false, sv_enforceGameBuild 2944, default NAK sync (onesync_automaticResend off).
  2. 165-560 players connected and playing normally, with constant joins and drops.
  3. The crash comes at a random time: after 28 minutes to 3h10m of uptime.

Our anticheat logs of game events and state bag writes in the 60 s before each crash show normal traffic: no floods, no oversized values, nothing unusual on entity bags. In the minute before each crash, scripts did only ordinary things: joins, quits, timeouts, and one DropPlayer ban 16 s before A.

Importancy

Crash

Area(s)

FXServer, OneSync

Specific version(s)

Server 38435 windows (master 5c3065c), game build 2944

Additional information

  • Environment: Windows Server 2022 (20348), 32 logical CPUs, 128 GB RAM, txAdmin 8.1.2. Memory was healthy before each crash: working set under 4 GB, system commit far below the 143 GB limit, and no hitch warnings beforehand.

  • Previous artifact: feature/network-improvements-clean 37264. Its first crash came after ~9 days (a different heap corruption, in the ENet free path on svNetwork). On 38435 we had these 4 crashes in 16.5 hours.

  • Profiler: a resource ran profiler record / stop on svMain every ~20 s during A and B, but it was not running at all during C and D. No saveJSON thread was alive in any of these dumps.

  • Minidumps: fxserver-38435-onesync-crashes-sanitized-minidumps.zip (all 4) is attached in the first comment below. Sanitized because they are public:

    • Kept intact: thread contexts (GPRs, rip/rsp), thread stacks (return addresses), the module list with CodeView records, and the exception records.
    • Removed: all captured memory outside thread stacks is zeroed, and printable strings of 6 or more characters are zeroed inside stacks, vector/x87 register areas and the function-table stream.

    The raw minidumps (4-5 MB) and full dumps (3-8 GB) contain server secrets and player data. I can share them privately with Cfx staff.

  • Possibly related: [Urgent] No Viable Production Artifact After b25770 Degradation - Sync Network Hitches / Mass Connection Timeouts #4244.

Activity

  1. added
    triageNeeds a preliminary assessment to determine the urgency and required action
    on Nov 8, 2026
  2. yw1gara commented on Nov 8, 2026

    @yw1gara
    Author
  3. yw1gara commented on Nov 8, 2026

    @yw1gara
    Author

    Fifth crash (E), same lambda, same path as A

    Time (UTC) Players Uptime Thread Dump
    E 2026-11-08 01:45 ~144 1h17m TBB worker 337edd0b-eb04-4ace-9880-b1e00695034a

    That makes 5 OneSync crashes in 22 hours on 38435.

    citizen-server-state-fivesv.dll+0xe9a8   lock xadd dword ptr [rax+4],edx   ; rax = block = 0x00ff020f32abb250 (non-canonical)
    citizen-server-state-fivesv.dll+0x35f99  loop over 40-byte elements, then frees the storage (crash A died in this function at +0x35fe0, on that free)
    citizen-server-state-fivesv.dll+0x4971c  destroys the value at node+0x110, then frees the node
    citizen-server-state-fivesv.dll+0x1c79b  per-client lambda (frameStates.erase), same return address as A
    citizen-server-state-fivesv.dll+0x4eaf0  TBB task
    

    +0xe990 is weak_reference::reset(): value at +0, block at +8, then dec_weak() = lock xadd [block+4], -1. So this is the destruction of ClientEntityState::syncedEntities (pair<uint16_t, ClientEntityData>, 40 bytes, SyncEntityWeakPtr at element+8). It fails on element 10 of 16.

    The vector in the full dump: begin 0x20f38b98eb0, end 0x20f38b99130, keys sorted (…0x420, 0x422, 0x448, 0x44a, 0x44b, 0x50e, 0x50f, 0x530…). The contents are intact except for one pattern: in exactly two 64-byte lines of the vector, byte 6 (bits 48-55) of every qword has been overwritten. Every other line is clean.

    line 0x20f38b99000  byte 6 of the 8 qwords: 00 00 00 00 00 00 00 00
    line 0x20f38b99040                          80 b6 ff 6e 85 e5 b7 4f   <- element 10: value 0x00b6020f4eb4b338, block 0x00ff020f32abb250
    line 0x20f38b99080                          00 00 00 00 00 00 00 00
    line 0x20f38b990c0                          22 12 5d ee 27 47 05 ef
    line 0x20f38b99100                          00 00 00 00 00 00 00 00
    

    The low 48 bits still hold the original values: 0x0000020f32abb250 is a plausible control block, and the frame indices look right. Crash C's m_tail (0x00d0021c4d827f68) has the same signature, a valid-looking heap pointer with garbage only in byte 6. Something writes into memory owned by these per-client sync structures. A and E (double free / corrupted vector in the same frameStates.erase) point at the same ClientEntityState. Without symbols I can't tell which code writes those bytes.

    Nothing unusual happened before E: normal joins and quits, one timeout, and our entity cleanup (paced DeleteEntity) had run about 14 minutes earlier. 38435 is still both recommended and latest, and master has no newer commits.

    The sanitized minidump for E is attached below. It was sanitized the same way as the others.

  4. changed the title [-]Server (Windows) 38435: memory corruption in the parallel per-client sync tick (4 crashes in 16 h: double free, corrupted object_pool free lists, corrupted control block)[/-] [+]Server (Windows) 38435: memory corruption in the parallel per-client sync tick (5 crashes in 22 h: double free, corrupted object_pool free lists, corrupted control block)[/+] on Nov 8, 2026
  5. Gogsi commented on Nov 8, 2026

    @Gogsi
    Contributor

    I would try reinstalling the server artifacts, in case of file corruption, and running a memory test to check if the RAM on the server is good. There have been no other reports of such crashes and 5 in a day is highly unusual. Especially when you also had corruption issues with the network-improvements-clean artifact.

    Looking into the first crash, I don't see any possibility of a race condition around GameStateClientData, every write or erase is behind the selfMutex, and the clientRef cannot be deleted by a client drop while processing that client in ServerGameState::Tick's tbb::parallel_for_each, because the shared pointer holds a reference on the client. This to me implies some larger corruption than just the state data. It's also worth noting that none of this code has changed recently, beside simple changes in bd84727 and d34c31a both of which have been used by many large servers.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    crashtriageNeeds a preliminary assessment to determine the urgency and required action

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions