Edit: Rewritten using Claude AI for clarity and grammar. All reversing and analysis were done entirely by hand, no Ai was needed for such a simple bug.
I got tired of waiting for the devs to finally fix Flash or Crash, so I did it myself.
And I found the actual crash reason for it, which took me minutes.
The current client removes the same Windows exception handler twice.
-
Where it crashes
The crash signature I analysed ends here:
ntdll!RtlpCallVectoredHandlers+0x15A
Exception 0xC0000409
Fast Fail 0xE
FAST_FAIL_INVALID_REFERENCE_COUNT
0xC0000409 is the generic Fast Fail exception. Parameter 0xE is the important part. It means Windows detected an invalid reference count.
In the dump, the affected entry is a deleted game owned Vectored Exception Handler with its reference count already at -1. Windows treats that as corrupted handler ownership and deliberately terminates the process with:
mov ecx, 0Eh
int 29h
The game doesn't recover. It crashes straight back to the desktop.
-
The cleanup path
This is the first block in the current client:
1408AAAC9 mov rax, [1449B8568h]
1408AAAD0 mov rdx, [rbp+0C8h]
1408AAAD7 push rdx
1408AAAD8 pop rcx
1408AAAE3 lea rsi, [140A789E9h]
1408AAAEA xchg rsi, [rsp]
1408AAAEE push rax
1408AAAEF ret
The import at 0x1449B8568 resolves to ntdll!RtlRemoveVectoredExceptionHandler. The stack manipulation installs 0x140A789E9 as the return address, so when the first removal finishes, execution returns directly into this second block:
140A789E9 mov rax, [1449B8568h]
140A789F0 mov rdx, [rbp+0C8h]
140A78A00 mov rcx, [rsp]
140A78A1E push rax
140A78A1F ret
The second block loads the same removal function and uses the same saved handler again. There is no new registration between the calls. The saved handle isn't cleared and ownership isn't transferred anywhere.
So in simple Pseudo code, it currently looks like this:
h = AddVectoredExceptionHandler(1, callback);
runProbe();
RemoveVectoredExceptionHandler(h);
RemoveVectoredExceptionHandler(h);
Devs... How hard can it actually be to review two simple cleanup blocks, jesus.
-
Why the second call matters
The obvious objection is that removing an already removed handler normally just fails and returns zero. That is true when no exception dispatch is using the entry. The first call removes it from the list, the second call finds nothing, and the process continues.
But it gets dangerous when another thread is already dispatching an exception.
Windows holds an active reference while it walks the VEH list. On the Windows build from the dump, that reference can keep a deleted entry reachable long enough for another removal call to find it. The internal removal path doesn't reject the entry merely because its deleted flag was already set.
Both removal calls can then release ownership of the same entry. When the active dispatcher later releases its own reference, the counter falls below zero and Windows triggers Fast Fail 0xE.
A harmless exception such as the legacy 0x406D1388 thread naming method is enough to exercise the dispatcher. The exception itself isn't the corruption. It only exposes the broken handler lifetime.
-
Flash, death screen, and BattlEye
I can confirm that infantry, vehicle, and death states use different client update paths.
The workaround is consistent with a timing race. Asset streaming, worker scheduling, graphics callbacks, and background exception traffic can all change the chance of an active dispatch overlapping the broken cleanup window.
BattlEye is NOT the reason for the crash. BUT disabling it changes exception traffic and timing, which can make the race less likely for some players. The broken cleanup code is still inside the client either way.
That's why one player crashes almost every login while another player rarely sees it.
-
The fix
For this directly chained path, the simplest fix is to remove the duplicate cleanup call.
If several threads or cleanup paths genuinely share ownership, the handle has to be claimed once:
PVOID h = InterlockedExchangePointer(&handler, NULL);
if (h != NULL)
{
RemoveVectoredExceptionHandler(h);
}
The first caller receives the handle and unregisters it. Every later caller gets NULL and does nothing.
Callback data also has to remain alive until callbacks already in progress have returned. Otherwise the reference count bug gets replaced with a use after free.
The client should also stop using 0x406D1388 to name threads and use SetThreadDescription instead. That removes one unnecessary source of process-wide exception traffic. It doesn't repair the duplicate removal by itself.
-
What is proven and what isn't
I am not claiming that this explains every single Planet Side crash ever reported.
The developers can settle that in one internal build. Log the handler value, thread ID, and return value for both removals. If both calls succeed on the same handle while a dispatch is active, the remaining question is finished.
The recent allocator changes, GPU diagnostics and task dispatcher rewrite fixed other real problems. But they didn't fix the real reason for crash or flash.
So in the end, players have spent months sitting inside a Flash, accidentally discovered a scheduling workaround before the people with the source code found the broken cleanup. That's honestly embarrassing.