> Windows Syscalls
ntoskrnl.exeT1055T1027.011T1106

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

NameTypeDirDescription
WorkerFactoryHandleHANDLEinHandle to the worker factory the calling thread is bound to.
MiniPacketPFILE_IO_COMPLETION_INFORMATIONoutReceives the dequeued completion packet — KeyContext, ApcContext, IoStatusBlock.Status, IoStatusBlock.Information.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
Win10 15070x1B6win10-1507
Win10 16070x1BFwin10-1607
Win10 17030x1C5win10-1703
Win10 17090x1C9win10-1709
Win10 18030x1CBwin10-1803
Win10 18090x1CCwin10-1809
Win10 19030x1CDwin10-1903
Win10 19090x1CDwin10-1909
Win10 20040x1D3win10-2004
Win10 20H20x1D3win10-20h2
Win10 21H10x1D3win10-21h1
Win10 21H20x1D5win10-21h2
Win10 22H20x1D5win10-22h2
Win11 21H20x1DFwin11-21h2
Win11 22H20x1E3win11-22h2
Win11 23H20x1E3win11-23h2
Win11 24H20x1E6win11-24h2
Server 20160x1BFwinserver-2016
Server 20190x1CCwinserver-2019
Server 20220x1DBwinserver-2022
Server 20250x1E6winserver-2025

Kernel module

ntoskrnl.exeNtWaitForWorkViaWorkerFactory

Related APIs

GetQueuedCompletionStatusNtRemoveIoCompletionNtRemoveIoCompletionExNtWorkerFactoryWorkerReadyNtReleaseWorkerFactoryWorker

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 ENDP

rustObserve 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