NtRemoveIoCompletionEx
Dequeues up to a caller-specified number of completion packets from an I/O completion port in a single syscall.
Prototype
NTSTATUS NtRemoveIoCompletionEx( HANDLE IoCompletionHandle, PFILE_IO_COMPLETION_INFORMATION IoCompletionInformation, ULONG Count, PULONG NumEntriesRemoved, PLARGE_INTEGER Timeout, BOOLEAN Alertable );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| IoCompletionHandle | HANDLE | in | Handle to the IoCompletion object opened with IO_COMPLETION_MODIFY_STATE. |
| IoCompletionInformation | PFILE_IO_COMPLETION_INFORMATION | out | Caller-allocated array of Count entries that receives (KeyContext, ApcContext, IoStatusBlock) tuples. |
| Count | ULONG | in | Maximum number of packets to dequeue in this call. |
| NumEntriesRemoved | PULONG | out | Receives the actual number of packets returned (1..Count). |
| Timeout | PLARGE_INTEGER | in | Optional 100ns-unit relative (negative) or absolute (positive) timeout. NULL waits forever. |
| Alertable | BOOLEAN | in | TRUE allows the wait to be interrupted by a user-mode APC (returns STATUS_USER_APC). |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x156 | win10-1507 |
| Win10 1607 | 0x15D | win10-1607 |
| Win10 1703 | 0x163 | win10-1703 |
| Win10 1709 | 0x166 | win10-1709 |
| Win10 1803 | 0x168 | win10-1803 |
| Win10 1809 | 0x169 | win10-1809 |
| Win10 1903 | 0x16A | win10-1903 |
| Win10 1909 | 0x16A | win10-1909 |
| Win10 2004 | 0x170 | win10-2004 |
| Win10 20H2 | 0x170 | win10-20h2 |
| Win10 21H1 | 0x170 | win10-21h1 |
| Win10 21H2 | 0x172 | win10-21h2 |
| Win10 22H2 | 0x172 | win10-22h2 |
| Win11 21H2 | 0x17A | win11-21h2 |
| Win11 22H2 | 0x17D | win11-22h2 |
| Win11 23H2 | 0x17D | win11-23h2 |
| Win11 24H2 | 0x17F | win11-24h2 |
| Server 2016 | 0x15D | winserver-2016 |
| Server 2019 | 0x169 | winserver-2019 |
| Server 2022 | 0x178 | winserver-2022 |
| Server 2025 | 0x17F | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 7F 01 00 00 mov eax, 0x17F 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
Batched cousin of `NtRemoveIoCompletion`. Where the legacy syscall returns at most one completion packet per syscall, `NtRemoveIoCompletionEx` returns up to `Count` packets in one round trip — a meaningful efficiency win for high-throughput servers (IIS, SQL Server, the .NET ThreadPool I/O completion thread, every Win32 thread-pool that sets `WT_EXECUTEINIOTHREAD`). The Win32 wrapper is `GetQueuedCompletionStatusEx`. Returns STATUS_TIMEOUT if Timeout elapses with no packet available, STATUS_USER_APC if Alertable and an APC fired, STATUS_ABANDONED_WAIT_0 if the IoCompletion object is closed mid-wait. Available since Windows Vista — fully stable API ever since, only the SSN has moved.
Common malware usage
**Weak malware signal**. This syscall is overwhelmingly a high-perf server / runtime primitive — the .NET ThreadPool, the Win32 thread-pool I/O worker, and every Windows server product call it constantly. The handful of malware mentions in the literature involve high-concurrency C2 *server-side* code (panels and bots that themselves act as servers), not implants. A small number of network-heavy implants (some Cobalt Strike external-C2 bridges, certain Sliver custom transports written on top of IO completion ports) use it because they reuse a server-style threading model — and there it just looks identical to legitimate WCF / ASP.NET Core traffic in telemetry. As an offensive primitive it adds zero stealth over the underlying ReadFile / WSARecv it batches.
Detection opportunities
Effectively no useful detection signal — `NtRemoveIoCompletionEx` is one of the highest-volume syscalls on any Windows server and a steady background hum on workstations (every browser tab, every cloud-storage agent, every IDE language server). There is no per-call telemetry pivot that meaningfully separates malicious from benign use. Defenders should focus on the *peers* of the IO completion port: which handles are queued onto it (sockets to suspicious endpoints, file handles into sensitive paths), and the threading pattern of the consumer (a worker thread that does completion-port reads followed immediately by NtCreateThreadEx into a remote process is the only weak red flag worth modeling). EDRs do not typically hook this syscall because of volume — direct-syscall variants gain nothing.
Direct syscall examples
asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtRemoveIoCompletionEx (SSN 0x17F on Win11 24H2)
NtRemoveIoCompletionEx PROC
mov r10, rcx ; syscall convention
mov eax, 17Fh ; SSN — varies per build
syscall
ret
NtRemoveIoCompletionEx ENDPcBatched dequeue via GetQueuedCompletionStatusEx
// Standard server-thread loop: pull up to 16 packets per syscall.
// This is what nearly every IOCP-based service does.
#include <windows.h>
VOID IocpWorker(HANDLE hPort) {
OVERLAPPED_ENTRY entries[16];
ULONG got = 0;
while (GetQueuedCompletionStatusEx(hPort, entries, 16, &got, INFINITE, FALSE)) {
for (ULONG i = 0; i < got; ++i) {
DispatchCompletion(&entries[i]);
}
}
}rustDirect-syscall wrapper
// Cargo: windows-sys = "0.59"
// Minimal wrapper around the Ex syscall — used by a custom server runtime.
use std::ptr::null_mut;
#[repr(C)]
pub struct IoCompletionInfo {
pub key_context: usize,
pub apc_context: usize,
pub io_status: [usize; 2], // status + information
}
extern "system" {
fn NtRemoveIoCompletionEx(h: isize, info: *mut IoCompletionInfo,
count: u32, removed: *mut u32, timeout: *mut i64, alertable: u8) -> i32;
}
pub unsafe fn drain(h: isize, batch: &mut [IoCompletionInfo]) -> u32 {
let mut n: u32 = 0;
NtRemoveIoCompletionEx(h, batch.as_mut_ptr(), batch.len() as u32,
&mut n, null_mut(), 0);
n
}MITRE ATT&CK mappings
Last verified: 2026-05-20