Quick answer: 0xC000070C is a kernel-level bugcheck that fires when a threadpool callback releases a semaphore on an I/O completion it doesn't own — you fix it in the driver or app code, not in Windows settings.
What's actually happening here is a mismatch between who acquires the semaphore and who releases it during I/O completion. The Windows threadpool (the one in ntdll, not the .NET one) queues work items to complete IRPs. When your completion routine calls KeReleaseSemaphore or NtReleaseSemaphore on a semaphore that the threadpool associated with a completely different completion, the kernel notices the count would go above the maximum and bugchecks. This shows up most often in third-party filter drivers, antivirus mini-filters, backup agents (Veeam, Acronis), and older VPN clients that hook the network stack. On Windows 10 21H2 and Windows 11 22H2 I've seen it in klif.sys (Kaspersky), atc.sys (older Acronis), and wg.sys before WireGuard 0.5.3. If you're a developer seeing this under Driver Verifier, the verifier's "Threadpool" and "Deadlock detection" options are what caught it. If you're an end user, you've got a bad driver and the fix is removing or updating it.
Why the kernel throws this specific code
The threadpool in Windows (introduced properly in Vista, refined in Windows 7 and again in 10) maintains per-CPU worker threads and a set of synchronization primitives. When you queue a work item with IoQueueWorkItem or use TrySubmitThreadpoolCallback, the pool tracks a reference. If your callback releases a semaphore whose owning thread is not the current worker, or releases it more times than it acquires — the pool's internal accounting goes negative and the kernel bugchecks to prevent silent memory corruption.
The full name tells you everything: STATUS_THREADPOOL_RELEASE_SEMAPHORE_ON_COMPLETION_FA. "On completion" refers to the I/O completion path. "FA" is the fast-fail variant — the kernel chose to crash rather than let the count roll over. That's deliberate. A semaphore count going above max means somebody will eventually wait forever, and deadlocked kernel threads are much worse than a bugcheck.
Step-by-step fix
- Get the bugcheck parameters. Open WinDbg (or WinDbg Preview from the Store) and load the minidump from
C:\Windows\Minidump. Run!analyze -v. Parameter 1 is usually the semaphore object, parameter 2 the calling thread, parameter 3 the IRP. That IRP tells you which driver is lying. - Identify the offending driver. In the WinDbg output, look at the
STACK_TEXTandFAILURE_BUCKET_ID. If it says something like0xC000070C_nt!KiFastFailDispatch_klif!KliIfReleaseSemaphore, you've found it. Note the exact module name. - Check that driver's version. Right-click the .sys file, Properties, Details tab. Compare against the vendor's current release. Kaspersky fixed this in 21.3.10.391; older builds crash on Windows 11 22H2 after a specific Windows Update. Acronis fixed it in True Image 2021 build 39216.
- Remove or update the driver. If it's an AV filter, uninstall via the vendor's removal tool (Kaspersky has kavremover; Bitdefender has bdremover). Don't just disable the service — the filter driver still loads in some cases. For a VPN, uninstall the TAP adapter completely from Device Manager and reboot.
- Verify the fix. Reboot and, if you can, run Driver Verifier against the remaining third-party drivers:
verifier /standard /driver klif.sys. If the system boots clean for 24 hours of normal use, you're done. Turn Verifier off withverifier /resetwhen finished — leaving it on permanently slows things down measurably.
If the main fix doesn't work
Sometimes the driver is gone but the crash persists because a leftover filter remains registered. Check with fltmc filters in an admin command prompt. Legacy minifilters stay attached until the altitude registry entries are cleaned. Look under HKLM\SYSTEM\CurrentControlSet\Services\<drivername>\Instances and remove the stale subkeys if the vendor's uninstaller missed them.
If you're a developer and this fires inside your own code, the cause is almost always releasing a semaphore from a completion routine that didn't acquire it. The pattern to look for:
// WRONG — completion routine releases a semaphore acquired elsewhere
NTSTATUS Completion(PDEVICE_OBJECT dev, PIRP irp, PVOID ctx) {
KeReleaseSemaphore(&g_Semaphore, IO_NO_INCREMENT, 1, FALSE);
return STATUS_MORE_PROCESSING_REQUIRED;
}The reason this fails is the threadpool doesn't track the semaphore as belonging to that callback. Acquire it in the same routine, or use a KEVENT set with KeSetEvent and let the waiter handle its own state. Releasing from a different thread than the acquirer is legal for semaphores but only when the counts balance — if the pool's worker gets recycled mid-flight, the accounting breaks.
Also check that you're not double-releasing on error paths. A completion routine that returns STATUS_MORE_PROCESSING_REQUIRED and then queues another work item that also releases is a classic double-release. Use a single owner and a boolean flag.
Prevention
For end users: don't stack multiple AV products, and don't install a VPN client that ships its own NDIS filter on top of a Kaspersky/Bitdefender install. That combination is where I've seen this bugcheck most in the wild. Keep filter drivers current — vendors patch these fast once Windows changes the pool internals, and the 22H2 update did change them.
For developers: acquire and release within the same callback, never across the completion boundary. If you need cross-thread signaling, use an event, not a semaphore. And run with Driver Verifier's threadpool and handle-tracking options enabled during development. Catching this in the lab beats a customer's blue screen at 3am.