NtWaitForWorkViaWorkerFactory
Blocks a threadpool worker until a work item is available on the factory's completion queue — the hot loop hijacked by PoolParty.
Prototype
NTSTATUS NtWaitForWorkViaWorkerFactory( HANDLE WorkerFactoryHandle, PFILE_IO_COMPLETION_INFORMATION MiniPacket );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| WorkerFactoryHandle | HANDLE | in | Handle to the worker factory the calling thread is bound to. |
| MiniPacket | PFILE_IO_COMPLETION_INFORMATION | out | Receives the dequeued completion packet — KeyContext, ApcContext, IoStatusBlock.Status, IoStatusBlock.Information. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x1B6 | win10-1507 |
| Win10 1607 | 0x1BF | win10-1607 |
| Win10 1703 | 0x1C5 | win10-1703 |
| Win10 1709 | 0x1C9 | win10-1709 |
| Win10 1803 | 0x1CB | win10-1803 |
| Win10 1809 | 0x1CC | win10-1809 |
| Win10 1903 | 0x1CD | win10-1903 |
| Win10 1909 | 0x1CD | win10-1909 |
| Win10 2004 | 0x1D3 | win10-2004 |
| Win10 20H2 | 0x1D3 | win10-20h2 |
| Win10 21H1 | 0x1D3 | win10-21h1 |
| Win10 21H2 | 0x1D5 | win10-21h2 |
| Win10 22H2 | 0x1D5 | win10-22h2 |
| Win11 21H2 | 0x1DF | win11-21h2 |
| Win11 22H2 | 0x1E3 | win11-22h2 |
| Win11 23H2 | 0x1E3 | win11-23h2 |
| Win11 24H2 | 0x1E6 | win11-24h2 |
| Server 2016 | 0x1BF | winserver-2016 |
| Server 2019 | 0x1CC | winserver-2019 |
| Server 2022 | 0x1DB | winserver-2022 |
| Server 2025 | 0x1E6 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 E6 01 00 00 mov eax, 0x1E6 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undocumented notes
Internally this is a fused dequeue of the underlying IoCompletion plus an update to the factory's `WaitingWorkerCount`. The packet returned matches the four-field `FILE_IO_COMPLETION_INFORMATION` layout that NtRemoveIoCompletion uses, but the wait is *bound* to the factory — only threads whose worker factory matches may consume from its queue, and the kernel uses this binding to enforce thread-count limits and idle timeouts. A return of `STATUS_TIMEOUT` triggers the threadpool to consider reaping the worker; a successful return jumps the worker into its dispatch loop.
Common malware usage
PoolParty's entire value proposition is that the kernel does *not* care what code happens to be running on a thread that calls this syscall — once a worker is blocked here, anything written into the upstream completion port (or anything pointed at by `KeyContext`/`ApcContext` if the user-mode dispatcher trusts them) becomes user-controlled execution flow. Most PoolParty variants do not actually issue NtWaitForWorkViaWorkerFactory themselves: they let the legitimate ntdll!TppWorkerThread keep calling it, then poison either the factory's StartRoutine or the items it dequeues. The 'worker via completion port' variant explicitly forges packets that, when consumed by this syscall and dispatched by TppWorkerThread, end up calling attacker-controlled code through the threadpool callback indirection.
Detection opportunities
Like NtWorkerFactoryWorkerReady, this is impossible to flag in isolation — every modern Windows process has dozens of threads sleeping here at all times. Detection has to focus on the *content* of the packets it returns. ETW IoCompletion providers can sample completion-port traffic; mismatches between expected callback contexts (which the threadpool internally tracks in `TP_DIRECT` / `TP_CALLBACK_INSTANCE` structures) and what was actually delivered are the highest-fidelity signal. Memory scanning of worker thread stacks for return addresses outside known ntdll/kernel32 ranges is a reasonable behavioural check after a suspicious cross-process Create/Release pair is observed.
Direct syscall examples
cMinimal worker loop
// Stripped-down clone of ntdll!TppWorkerThread for educational use.
VOID WINAPI MyWorker(PVOID context) {
HANDLE hWf = (HANDLE)context;
FILE_IO_COMPLETION_INFORMATION packet;
for (;;) {
NTSTATUS s = NtWaitForWorkViaWorkerFactory(hWf, &packet);
if (s == STATUS_TIMEOUT) break; // pool decided to reap us
if (!NT_SUCCESS(s)) break;
// packet.KeyContext is *not* trusted in legitimate ntdll —
// it dispatches through internal TP_CALLBACK_INSTANCE tables.
Dispatch(&packet);
NtWorkerFactoryWorkerReady(hWf);
}
}asmx64 direct stub (Win11 24H2 SSN 0x1E6)
NtWaitForWorkViaWorkerFactory PROC
mov r10, rcx
mov eax, 1E6h
syscall
ret
NtWaitForWorkViaWorkerFactory ENDPrustObserve a hijacked completion
// Diagnostic helper used in PoolParty research — drains one packet from a
// worker factory and prints the would-be dispatch context.
use ntapi::ntioapi::FILE_IO_COMPLETION_INFORMATION;
use ntapi::ntexapi::NtWaitForWorkViaWorkerFactory;
unsafe fn observe_one(h_wf: windows_sys::Win32::Foundation::HANDLE) {
let mut p: FILE_IO_COMPLETION_INFORMATION = core::mem::zeroed();
let s = NtWaitForWorkViaWorkerFactory(h_wf, &mut p);
if s == 0 {
eprintln!(
"KeyContext={:p} ApcContext={:p} Status={:#x} Info={:#x}",
p.KeyContext, p.ApcContext,
p.IoStatusBlock.u.Status, p.IoStatusBlock.Information
);
}
}MITRE ATT&CK mappings
Last verified: 2026-05-20