DPCs: Deferred Procedure Calls and Interrupt Deferral
Objective: Understand how the Windows kernel uses Deferred Procedure Calls (DPCs) to move work out of high-IRQL interrupt service routines down to
DISPATCH_LEVEL, covering theKDPCstructure, IRQL mechanics, the full queue-to-callback lifecycle, threaded and timer DPCs, the DPC watchdog, and how defenders detect kernel-mode abuse of the DPC mechanism.
1. The Interrupt Deferral Problem
When a hardware device raises an interrupt, the kernel dispatches to an Interrupt Service Routine (ISR) running at DIRQL — a device IRQL higher than the scheduler itself. At that level the processor cannot wait, cannot touch pageable memory, and blocks all lower-priority interrupts on that CPU. An ISR that lingers degrades the entire system; the guidance is that ISRs should not run longer than 25 microseconds.
Windows therefore uses a two-phase interrupt model. The ISR does the minimum work needed to quiesce the device (acknowledge the interrupt, snapshot status), then schedules a Deferred Procedure Call to perform the heavier processing later, at a gentler IRQL. The DPC executes at DISPATCH_LEVEL, which is still too high for anything that touches pageable memory — but it is low enough to run the bulk of device servicing without starving other interrupts.
The essence of the DPC is deferring execution to gentler circumstances. It is the kernel’s primary tool for keeping ISRs short.
2. IRQL Levels: A Precise Map
The Interrupt Request Level (IRQL) is a per-processor priority that determines what code may run and what it may do. Any routine running at DISPATCH_LEVEL or above is not preemptable, runs to completion, and must reside in non-paged memory.
| IRQL Name | Value | Notes |
|---|---|---|
PASSIVE_LEVEL | 0 | Normal user/kernel thread execution; paging and waiting allowed |
APC_LEVEL | 1 | Asynchronous Procedure Calls |
DISPATCH_LEVEL | 2 | DPC execution, scheduler, spin locks; no paging, no waiting |
DIRQL | 3–11 (device-dependent) | Hardware ISRs run here |
An ISR at DIRQL cannot call functions that require PASSIVE_LEVEL. It instead schedules a DPC, which the kernel later runs at DISPATCH_LEVEL. Because DISPATCH_LEVEL still forbids page faults and blocking waits, a DPC routine and all data it touches must be non-paged.

3. The KDPC Structure Dissected
The KDPC is the structure in which the kernel keeps the state of a Deferred Procedure Call. It has always been explicitly undocumented — Microsoft labels it an opaque structure and warns drivers not to set members directly. The published layout from WDK/OSR headers is:
typedef struct _KDPC {
UCHAR Type; // DpcObject or ThreadedDpcObject
UCHAR Importance; // Low / Medium / High
USHORT Number; // target processor (directed DPCs)
LIST_ENTRY DpcListEntry; // links into per-processor DPC queue
PKDEFERRED_ROUTINE DeferredRoutine; // pointer to the callback function
PVOID DeferredContext; // driver-supplied context value
PVOID SystemArgument1; // extra arg passed to callback
PVOID SystemArgument2; // extra arg passed to callback
__volatile PVOID DpcData; // internal; pointer to KDPC_DATA
} KDPC, *PKDPC, *PRKDPC;| Field | Purpose |
|---|---|
Type | Distinguishes a normal DpcObject from a ThreadedDpcObject |
Importance | Controls queue insertion: MediumImportance = tail, HighImportance = head |
Number | Target logical processor, set via KeSetTargetProcessorDpc |
DeferredRoutine | Pointer to the KDEFERRED_ROUTINE callback |
DeferredContext | Opaque context the driver receives back in the callback |
SystemArgument1/2 | Caller-supplied arguments passed through to the callback |
DpcData | Volatile internal pointer to the per-processor KDPC_DATA; non-NULL while queued |
The DpcData field is the kernel’s bookkeeping anchor: before Windows 8.1 it pointed directly at a KDPC_DATA structure, and its non-NULL state indicates the DPC is currently queued. Because DeferredRoutine is a raw function pointer inside a writable structure, it is also a corruption target — covered in §10.
4. The DPC Lifecycle: From ISR to Callback
A DPC moves through four stages: allocate → initialize → queue → drain.
| API Function | Purpose |
|---|---|
KeInitializeDpc | Initializes a KDPC, binding a DeferredRoutine and DeferredContext |
KeInsertQueueDpc | Inserts the KDPC into the per-processor queue; returns FALSE if already queued |
IoRequestDpc | Convenience wrapper called from ISR context for the DpcForIsr pattern |
KeRemoveQueueDpc | Removes a pending (not-yet-fired) DPC from the queue |
Kernel code first allocates a KDPC in non-paged pool (or the device extension) so the object is resident when referenced from the ISR.
// C1 — allocate and initialize a DPC object
PKDPC pDpc = ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(KDPC), 'cpDD');
if (pDpc) {
KeInitializeDpc(pDpc, MyCustomDpc, DeviceContext); // routine + context
}The callback must match the KDEFERRED_ROUTINE signature and runs at DISPATCH_LEVEL:
// C2 — DPC callback stub
VOID MyCustomDpc(
_In_ PKDPC Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2)
{
UNREFERENCED_PARAMETER(Dpc);
ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL); // invariant
// Non-paged, bounded work only — no waits, no page faults.
}The ISR queues the DPC. The return value of KeInsertQueueDpc enforces the single-instantiation guarantee: only one instance of a given KDPC can be queued at a time, so queuing it twice before it fires runs the routine once.
// C3 — queue from a mock ISR
BOOLEAN queued = KeInsertQueueDpc(pDpc, Arg1, Arg2);
if (!queued) {
// Already pending on a queue — the earlier request still stands.
}Device drivers commonly use the wrapper from inside their InterruptService routine:
// C4 — DpcForIsr pattern
BOOLEAN MyIsr(_In_ PKINTERRUPT Interrupt, _In_ PVOID Context) {
PDEVICE_OBJECT devObj = (PDEVICE_OBJECT)Context;
// ...acknowledge hardware quickly...
IoRequestDpc(devObj, devObj->CurrentIrp, NULL); // schedules DpcForIsr
return TRUE;
}When the processor returns from the interrupt, it checks its DPC queue; if entries are pending, the kernel raises IRQL to DISPATCH_LEVEL, drains the queue by invoking each DeferredRoutine, then lowers IRQL back down.

5. Per-Processor DPC Queues and KPRCB
Each logical processor owns a separate DPC queue, stored as a KDPC_DATA structure inside the processor’s KPRCB (Kernel Processor Control Block). This avoids cross-CPU locking on the common path.
KDPC_DATA carries the queue head, depth, count, and a spin lock:
typedef struct _KDPC_DATA {
LIST_ENTRY DpcListHead; // queued KDPC objects
ULONG DpcLock; // spin lock protecting the list
volatile ULONG DpcQueueDepth; // pending DPCs
ULONG DpcCount; // running total
} KDPC_DATA, *PKDPC_DATA;Exact
KDPC_DATAfield names vary by kernel build — confirm against a live PDB withdt nt!_KDPC_DATAbefore relying on offsets.
Because each queue is per-processor, the target processor of a DPC determines which CPU drains it. By default a DPC runs on the CPU that queued it, but it can be pinned elsewhere (§6) — a property attackers exploit to manipulate specific cores.

6. Controlling DPC Behaviour
| API Function | Purpose |
|---|---|
KeSetImportanceDpc | Sets Importance; HighImportance inserts at the queue head |
KeSetTargetProcessorDpc | Pins the DPC to a specific logical processor (directed DPC) |
KeRemoveQueueDpc | Dequeues a pending DPC; fails once the routine is already running |
DPCs have three priority levels — low, medium, high. Importance influences KeInsertQueueDpc: high-importance DPCs go to the head of the queue and are serviced first.
A directed DPC is created by binding it to a CPU before queuing. The pattern below — iterating over KeNumberProcessors and targeting each core — is the same primitive a rootkit weaponizes for CPU lockdown, so treat it as an educational illustration only:
// C5 — directed DPC setup (educational pattern)
for (CCHAR cpu = 0; cpu < KeNumberProcessors; cpu++) {
KeInitializeDpc(&pDpcArray[cpu], MyCustomDpc, NULL);
KeSetTargetProcessorDpc(&pDpcArray[cpu], cpu); // pin to logical CPU
KeSetImportanceDpc(&pDpcArray[cpu], HighImportance);
}Once a DPC begins executing it cannot be removed; KeRemoveQueueDpc only rescinds a still-pending entry.
7. Threaded DPCs
Since Windows Server 2003, a KDPC can represent either a normal DPC or a threaded DPC. In the threaded variant, the kernel — if it can arrange it — calls the routine back at PASSIVE_LEVEL from a highest-priority thread, allowing more flexible work. Support can be disabled, in which case the threaded DPC falls back to running at DISPATCH_LEVEL exactly like a normal DPC.
You initialize one with KeInitializeThreadedDpc and a CustomThreadedDpc routine. Because that routine can run at either PASSIVE_LEVEL or DISPATCH_LEVEL, it must synchronize correctly at both IRQLs:
// C7 — threaded DPC with dual-IRQL guard
KeInitializeThreadedDpc(&g_ThreadedDpc, MyThreadedDpc, NULL);
VOID MyThreadedDpc(_In_ PKDPC Dpc, _In_opt_ PVOID Ctx,
_In_opt_ PVOID A1, _In_opt_ PVOID A2) {
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); // may be PASSIVE or DISPATCH
// Use locks valid at both levels.
}Threaded DPCs should be preferred over ordinary DPCs unless a particular DPC must never be preempted — not even by another DPC.
8. Timer DPCs and KTIMER
A DPC is also the callback mechanism for kernel timers. You associate a KDPC with a KTIMER and arm it; on expiry the kernel queues the DPC. KeSetTimerEx supports both one-shot and periodic timers.
// C6 — periodic timer DPC
KeInitializeTimerEx(&g_Timer, NotificationTimer);
KeInitializeDpc(&g_TimerDpc, MyCustomDpc, NULL);
LARGE_INTEGER due;
due.QuadPart = -10LL * 1000 * 1000; // 1 second, relative
KeSetTimerEx(&g_Timer, due, 1000 /* ms period */, &g_TimerDpc);Windows uses special timer DPCs internally for timer expiration and context switching. The same primitive — a recurring timer pointed at a non-paged callback — is the cleanest way a driver schedules background work, and the cleanest way a malicious driver re-enters its payload (§10).
9. The DPC Watchdog and Debugging
The kernel runs a DPC watchdog. Bug Check 0x00000133 (DPC_WATCHDOG_VIOLATION) fires when the watchdog detects either a single long-running DPC or a prolonged time spent at DISPATCH_LEVEL or above. The timing budgets are 100 microseconds for a DPC and 25 microseconds for an ISR. A malicious DPC spin-loop can therefore inadvertently trip the watchdog and crash the host.
Inspect live DPC state in the kernel debugger:
kd> !dpcs ; list pending DPCs per processor
kd> dt nt!_KDPC ; KDPC layout for this build
kd> dt nt!_KDPC_DATA ; per-processor queue structure
kd> !prcb ; processor control block (contains DpcData)
kd> !pcr ; processor control region!dpcs reveals each queued DPC’s DeferredRoutine address — the single most useful artifact, since an unknown or non-image-backed routine address is a strong anomaly.
10. Common Attacker Techniques
DPCs give kernel-mode malware a high-IRQL execution surface. Because code at DISPATCH_LEVEL is non-preemptable and runs to completion, it is ideal cover for Direct Kernel Object Manipulation (DKOM).
| Technique | Description |
|---|---|
| CPU lockdown / freeze-other-CPUs | Queue a directed KDPC to every non-current CPU via KeSetTargetProcessorDpc and spin, raising all secondary cores to DISPATCH_LEVEL to block interruption during a DKOM patch |
| Timer DPC payload | Arm a KTIMER whose DeferredRoutine points at attacker-controlled non-paged code, for recurring stealth execution |
| KDPC hijacking | Overwrite DeferredRoutine in a legitimate queued KDPC to redirect execution to a payload |
| Driver-based persistence | Load a malicious signed/BYOVD driver that registers a recurring timer DPC at load time |
The CPU-lockdown pattern is especially relevant to defenders: by parking every other core at DISPATCH_LEVEL, the rootkit can unlink processes, patch EDR callbacks, or hide drivers while no scheduler or AV thread can run.

11. Defensive Strategies & Detection
DPC objects live entirely in kernel memory and are not directly observable from user mode, so detection focuses on the driver that installs them and on kernel ETW timing telemetry.
Sysmon and Windows event telemetry:
| Event ID | Source | Relevance |
|---|---|---|
6 | Sysmon — Driver Loaded | Fires on every driver load; primary signal for kernel modules that register DPC routines |
7 | Sysmon — Image Loaded | Catches unsigned/anomalous modules entering kernel space |
7045 | Service Control Manager | New kernel-mode driver, especially from a non-standard path |
7040 | Service Control Manager | Service start-type change — driver persistence |
ETW providers: The NT Kernel Logger session with EVENT_TRACE_FLAG_DPC and EVENT_TRACE_FLAG_INTERRUPT records per-DPC timing and the routine address, exposing abnormally long-running or unknown-address DPC routines. Microsoft-Windows-Kernel-Processor-Power surfaces IRQL/watchdog events. Verify the exact flag constants against the current WDK evntrace.h.
Sigma anchor — unsigned/expired driver load:
title: Suspicious Kernel Driver Load (Unsigned or Expired)
logsource:
product: windows
service: sysmon
detection:
selection_unsigned:
EventID: 6
Signed: 'false'
selection_expired:
EventID: 6
SignatureStatus: 'Expired'
selection_path:
EventID: 6
ImageLoaded|contains: '\Temp\'
condition: selection_unsigned or selection_expired or selection_path
level: highHunt additionally for EventID 6 where ImageLoaded resolves outside \SystemRoot\System32\drivers\.
Hardening:
| Mitigation | Description |
|---|---|
| Driver Signature Enforcement (DSE) | Default on 64-bit Windows; blocks unsigned drivers that would install DPC routines |
| HVCI | Protects kernel code pages, raising the bar for DPC shellcode and DeferredRoutine overwrite |
| Kernel CET | Hardware shadow stack mitigates ROP-based DPC hijacking |
| DPC Watchdog | Built-in; Bug Check 0x133 catches long-running DPC loops, including malicious spin-locks |
| Vulnerable Driver Blocklist | HKLM\SYSTEM\CurrentControlSet\Control\CI\Config\VulnerableDriverBlocklistEnable blocks known BYOVD primitives |
| WDAC / Memory Integrity | Restrict which drivers may load, shrinking the DPC-abuse attack surface |
12. Tools for DPC Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | !dpcs, dt nt!_KDPC, !prcb, !pcr live queue inspection | microsoft.com |
| Process Hacker | Driver/service enumeration and kernel module listing | processhacker.sourceforge.io |
| Windows Performance Recorder / xperf | Captures DPC/ISR ETW timing and routine addresses | microsoft.com |
| Sysmon | Driver-load (EID 6) and image-load (EID 7) telemetry | sysinternals.com |
| Volatility | Memory-forensic enumeration of drivers and kernel callbacks | volatilityfoundation.org |
| Ghidra | Static analysis of suspect drivers for KeInsertQueueDpc usage | ghidra-sre.org |
13. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | ETW DPC routine-address anomalies; !dpcs unknown routines |
| Boot/Logon Autostart: Kernel Modules | T1547.006 | Sysmon EID 6 / Event 7045 driver loads |
| Exploitation for Privilege Escalation | T1068 | HVCI/CET violations; KDPC.DeferredRoutine corruption |
| Impair Defenses: Disable/Modify Tools | T1562.001 | CPU-freeze DPC pattern halting EDR threads; watchdog 0x133 |
| Native API | T1106 | Driver use of KeInitializeDpc / KeInsertQueueDpc |
No dedicated ATT&CK sub-technique exists for DPC abuse as of ATT&CK v15; the techniques above are the parents. Verify current IDs at attack.mitre.org before publishing.
Summary
- DPCs are the kernel’s mechanism for deferring interrupt work from high-IRQL ISRs down to
DISPATCH_LEVEL, keeping ISRs under their 25 µs budget. - The opaque
KDPCstructure carries theDeferredRoutine, context, arguments, and aDpcDatapointer that marks whether it is queued on a per-processorKDPC_DATAlist in theKPRCB. - The lifecycle runs allocate →
KeInitializeDpc→KeInsertQueueDpc/IoRequestDpc→ per-CPU drain atDISPATCH_LEVEL, with a single-instantiation guarantee per object. - Rootkits abuse directed DPCs for CPU lockdown, timer DPCs for stealth re-entry, and
DeferredRoutinecorruption for hijacking — mapping toT1014,T1547.006, andT1562.001. - Detect via Sysmon Event ID 6 driver loads, NT Kernel Logger DPC timing telemetry, and the DPC watchdog (
0x133); harden with DSE, HVCI, Kernel CET, and the vulnerable driver blocklist.
Related Tutorials
- APCs: Asynchronous Procedure Calls and Thread Hijacking Surface
- IRQL Levels: Interrupt Request Priorities Explained
- System Calls and SSDT: How User Mode Reaches the Kernel
- Access Tokens and Privileges: The Kernel’s Security Context
- SIDs and Security Descriptors: Identity in Windows Security
References
- Introduction to DPC Objects – Windows Kernel Drivers | Microsoft Learn
- Managing Hardware Priorities (IRQL) – Windows Kernel Drivers | Microsoft Learn
- Deferred Procedure Call (DPC) – Kernel API Reference | Geoff Chappell, Software Analyst
- Deferred Procedure Call Details (KDPC Internals & Queuing Mechanisms) | OSR Online NT Insider
- Understanding Deferred Procedure Calls (DPCs) for Windows Vulnerability Research & Exploit Development | Medium / WaterBucket
- IRQLs: Close Encounters of the Rootkit Kind (DPCs & IRQL Abuse) | OffSec Blog
IRQL Levels: Interrupt Request Priorities Explained
Objective: Understand the Windows kernel’s Interrupt Request Level (IRQL) priority system — what each level means numerically and symbolically, how the HAL arbitrates hardware and software interrupts, which APIs query and change the IRQL, what kernel operations are legal at each level, and how malicious kernel code abuses IRQL semantics to evade defenders.
1. What Is an IRQL?
An Interrupt Request Level (IRQL) is a per-processor priority value that determines which kernel-mode support routines the currently executing code may legally call. It is an integer in the range 0–31, stored as type KIRQL (a typedef for UCHAR). Three levels — PASSIVE_LEVEL, APC_LEVEL, and DISPATCH_LEVEL — are referred to symbolically; the rest are usually named by value.
IRQL is per-processor, not per-thread. On x86 it lives in the Irql field of the _KPCR (Kernel Processor Control Region); on x64 it is mapped to the CR8 register (Task Priority Register). When the processor raises its IRQL, all interrupts at or below that level are masked. Higher-numbered interrupts preempt all lower-IRQL processing; once handled, the processor returns to the previous level. Raising and lowering must follow strict stack discipline — you only lower back to a level you previously raised from.
2. The IRQL Hierarchy
The Hardware Abstraction Layer (HAL) maps physical interrupt vectors to software IRQLs. The count of levels is architecture-dependent: x64 and Itanium expose 16 IRQLs; x86 exposes 32, owing to differences in interrupt-controller hardware. The canonical wdm.h symbolic definitions differ across architectures.
| Symbolic Name | x64 Value | x86 Value | Description |
|---|---|---|---|
PASSIVE_LEVEL / LOW_LEVEL | 0 | 0 | Normal thread execution; nothing masked |
APC_LEVEL | 1 | 1 | APC delivery and page-fault handling |
DISPATCH_LEVEL | 2 | 2 | Thread scheduler / DPC queue |
CMC_LEVEL | 3 | — | Correctable Machine Check |
| Device IRQLs (DIRQL) | 4–11 | 3–26 | Hardware device interrupts |
CLOCK_LEVEL | 13 | 28 | System clock timer |
IPI_LEVEL / DRS_LEVEL | 14 | 29 | Inter-Processor Interrupt |
POWER_LEVEL | 15 | 30 | Power failure |
PROFILE_LEVEL / HIGH_LEVEL | 15 | 31 | Profiling / highest maskable |
Higher value = higher priority. A device interrupt at DIRQL 8 preempts a DPC at DISPATCH_LEVEL (2), which itself preempts ordinary thread code at PASSIVE_LEVEL (0).

3. Software IRQLs: PASSIVE, APC, and DISPATCH
The lowest three levels are software IRQLs — the kernel raises and lowers them without involving the interrupt controller.
PASSIVE_LEVEL (0) masks nothing. This is where normal kernel-mode thread code runs: DriverEntry, AddDevice, Unload, most dispatch routines, and driver-created worker threads. All blocking, paging, and synchronization primitives are available.
APC_LEVEL (1) masks Asynchronous Procedure Call interrupts only. The sole functional difference from PASSIVE_LEVEL is that APCs cannot interrupt the running code. Both levels imply a valid thread context and both permit access to pageable memory. Page-fault handling itself runs at APC_LEVEL.
DISPATCH_LEVEL (2) masks DISPATCH_LEVEL and APC_LEVEL. Critically, the thread scheduler is disabled — code here owns the processor until it lowers IRQL. Routines such as StartIo, DpcForIsr, IoTimer, Cancel (holding the cancel spin lock), and all DPC callbacks run here. Two hard rules apply: no access to paged memory, and no blocking waits.
| Feature | PASSIVE_LEVEL | APC_LEVEL | DISPATCH_LEVEL |
|---|---|---|---|
| Thread context | Yes | Yes | Not guaranteed |
| Scheduler active | Yes | Yes | No |
| Paged pool access | Yes | Yes | No |
| Blocking waits allowed | Yes | Yes | No |
4. Hardware IRQLs: DIRQL and Above
Levels at or above the device range are hardware IRQLs driven by the interrupt controller. A driver’s Device IRQL (DIRQL) is the SynchronizeIrql stored in its _KINTERRUPT object. When a device fires, the processor raises to that DIRQL and invokes the Interrupt Service Routine (ISR), a KSERVICE_ROUTINE.
At DIRQL, all interrupts at or below the driver’s level are masked, but higher-DIRQL devices, the clock, and power-failure interrupts may still preempt. Because the scheduler and lower-priority interrupts are blocked, ISRs must be minimal — they acknowledge the hardware, capture volatile state, and queue a DPC for the heavy lifting at DISPATCH_LEVEL.
Above DIRQL sit CLOCK_LEVEL, IPI_LEVEL (used by one processor to interrupt another), POWER_LEVEL, and HIGH_LEVEL. The general principle: the higher the IRQL, the shorter the code must run. Sustained work at high IRQL starves the entire processor.
// KSERVICE_ROUTINE - runs at DIRQL; must be minimal
BOOLEAN MyInterruptServiceRoutine(
PKINTERRUPT Interrupt, PVOID ServiceContext) {
// Acknowledge hardware, then defer heavy work to a DPC.
// Do NOT touch paged memory here.
IoRequestDpc(MyDeviceObject, MyDeviceObject->CurrentIrp, ServiceContext);
return TRUE;
}5. Kernel APIs for IRQL Management
Drivers query and adjust IRQL through a small, exported API surface in wdm.h.
| API Function | Purpose |
|---|---|
KeGetCurrentIrql() | Returns the current processor IRQL; callable at any IRQL |
KeRaiseIrql(NewIrql, &OldIrql) | Raises to NewIrql; saves prior level. NewIrql must be ≥ current |
KeLowerIrql(OldIrql) | Restores a previously saved IRQL — only after a matching raise |
KeRaiseIrqlToDpcLevel() | Raises to DISPATCH_LEVEL, returns old IRQL |
KeAcquireSpinLock(&Lock, &OldIrql) | Acquires spin lock, raising to DISPATCH_LEVEL |
KeReleaseSpinLock(&Lock, OldIrql) | Releases lock, restoring saved IRQL |
KeAcquireSpinLockAtDpcLevel(&Lock) | Acquires lock without raising (caller already at DISPATCH_LEVEL) |
The exact signatures:
KIRQL KeGetCurrentIrql(void);
void KeRaiseIrql(
_In_ KIRQL NewIrql,
_Out_ PKIRQL OldIrql
);
void KeLowerIrql(_In_ KIRQL NewIrql); // restore saved old IRQL
KIRQL KeRaiseIrqlToDpcLevel(void);The raise/lower discipline is enforced: calling KeRaiseIrql with a value lower than the current IRQL is a fatal error, and KeLowerIrql may only restore the level a prior KeRaiseIrql saved.
// Demonstrates the raise/lower stack discipline
VOID MyFunctionNeedingDispatchLevel(VOID) {
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
// --- Critical section: no paged pool access here ---
KeLowerIrql(oldIrql);
}Spin locks couple mutual exclusion with IRQL: acquiring one raises to DISPATCH_LEVEL so the holder cannot be preempted by the scheduler on its processor.
KSPIN_LOCK MySpinLock;
KIRQL oldIrql;
KeInitializeSpinLock(&MySpinLock);
// KeAcquireSpinLock raises to DISPATCH_LEVEL internally
KeAcquireSpinLock(&MySpinLock, &oldIrql);
// ... protected shared-data access (non-paged only) ...
KeReleaseSpinLock(&MySpinLock, oldIrql); // restores oldIrqlA driver inspecting its own context queries the level directly:
// Demonstrates KeGetCurrentIrql() usage and KIRQL type
NTSTATUS DriverDispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
KIRQL currentIrql = KeGetCurrentIrql();
// Expected: PASSIVE_LEVEL (0) in a dispatch routine
DbgPrint("[MyDriver] Current IRQL: %u\n", (ULONG)currentIrql);
// ...complete IRP...
}6. Memory Access Rules at Each IRQL
The single most consequential IRQL rule concerns paged memory. Any routine running above APC_LEVEL that touches paged pool causes a fatal page fault. Resolving a page fault requires the file-system driver to read from disk — an operation that needs a context switch, which is impossible once the scheduler is disabled at DISPATCH_LEVEL.
| Memory Pool | PASSIVE_LEVEL | APC_LEVEL | DISPATCH_LEVEL+ |
|---|---|---|---|
| Paged pool | Accessible | Accessible | Fatal page fault |
| Non-paged pool | Accessible | Accessible | Accessible |
Code at or above DISPATCH_LEVEL must therefore allocate from non-paged pool and operate only on locked or non-pageable memory (for example, buffers locked with MmProbeAndLockPages). Violating this rule produces the most common driver bug check — IRQL_NOT_LESS_OR_EQUAL (0x0000000A), or its driver-attributed variant 0x000000D1.
7. DPCs: The DISPATCH_LEVEL Workhorses
A Deferred Procedure Call (DPC) moves work out of the time-critical ISR into DISPATCH_LEVEL. The ISR queues a _KDPC object (via IoRequestDpc or KeInsertQueueDpc); the kernel drains the DPC queue as IRQL drops below DISPATCH_LEVEL. DpcForIsr handles per-IRP completion; CustomDpc and CustomTimerDpc serve driver-specific needs.
// KDEFERRED_ROUTINE - runs at DISPATCH_LEVEL
VOID MyDpcRoutine(
PKDPC Dpc, PVOID DeferredContext,
PVOID SystemArgument1, PVOID SystemArgument2) {
// Safe: non-paged pool only.
// Do NOT call KeWaitForSingleObject with a nonzero timeout.
DbgPrint("[MyDpc] Running at DISPATCH_LEVEL\n");
}A DPC that runs too long throttles the whole system and triggers DPC_WATCHDOG_VIOLATION (0x00000133) once sustained execution exceeds the watchdog threshold.

8. APCs: The APC_LEVEL Mechanism
An Asynchronous Procedure Call (APC) executes a function in the context of a specific thread. Kernel APCs run at APC_LEVEL; user APCs are delivered when a thread returns to PASSIVE_LEVEL in a user-mode alertable wait. Drivers initialize them with KeInitializeApc and queue them with KeInsertQueueApc. Because APC_LEVEL still implies a valid thread context and permits paged access, certain dispatch routines raise to APC_LEVEL to serialize against APC delivery while remaining able to page in data.
9. Debugging IRQL With WinDbg
WinDbg exposes IRQL state on both live kernels and crash dumps.
; Check current IRQL on each processor
!irql
; Examine the KPCR for processor 0
!pcr 0
; List pending DPCs
!dpcs
; Analyze a 0x0000000A bugcheck
!analyze -vOn x64 the IRQL is the CR8 register; you can read it and the _KPCR directly:
; dt = display type; shows _KPCR struct at GS base
dt nt!_KPCR @$pcr
; On x64, IRQL maps to CR8 (Task Priority Register)
r cr8The IRQL contract is also expressed statically through SAL annotations in wdm.h, which static-analysis tooling verifies at build time:
// Illustrates IRQL annotation macros from wdm.h
_IRQL_requires_max_(DISPATCH_LEVEL)
VOID MyRoutineSafeAtOrBelowDispatch(VOID);
_IRQL_requires_(PASSIVE_LEVEL)
VOID MyRoutineRequiresPassive(VOID);
_IRQL_raises_(DISPATCH_LEVEL)
_IRQL_saves_
KIRQL MyRaiseRoutine(VOID);10. IRQL in a Security Context
IRQL semantics become a security concern the moment attacker code reaches ring 0. Code running at DISPATCH_LEVEL owns its processor and is invisible to user-mode EDR hooks — an ideal vantage point for unhooking the SSDT, overwriting kernel callbacks, or hiding objects before defensive software can react. Because paged access above APC_LEVEL is fatal, IRQL violations also serve as a crude denial-of-service primitive: a single bad page touch produces an IRQL_NOT_LESS_OR_EQUAL blue screen.
The dominant delivery vector is Bring Your Own Vulnerable Driver (BYOVD) — loading a legitimately signed but exploitable driver to obtain kernel-IRQL execution without writing a new signed driver. Missing or incorrect IRQL SAL annotations frequently mask the very bugs these attacks exploit.

11. Common Attacker Techniques
| Technique | Description |
|---|---|
| BYOVD kernel execution | Load a signed-but-vulnerable driver (e.g. RTCore64.sys, dbutil_2_3.sys) to run code at kernel IRQL |
EDR unhooking at DISPATCH_LEVEL | Patch SSDT entries or kernel callbacks while the scheduler is disabled, beating re-hook races |
| Rootkit concealment | Hide processes, files, and connections from DIRQL/DISPATCH_LEVEL, below user-mode visibility |
| Spin-lock starvation | Hold a spin lock at DISPATCH_LEVEL to monopolize a processor — driver-stack DoS |
| Deliberate IRQL fault | Force paged access above APC_LEVEL to bug-check the host (0x0000000A DoS) |
| DSE downgrade | Flip test-signing or pre-release flags to load unsigned kernel code |
12. Defensive Strategies & Detection
Driver loads are the chokepoint. Sysmon Event ID 6 (Driver Loaded) records ImageLoaded, Hashes, Signed, Signature, and SignatureStatus — the fields that expose unsigned or anomalously signed drivers and known-vulnerable BYOVD payloads. Event ID 7045 (and System log 7036/7040) surface drivers registered as services. PatchGuard violations of _KPCR/IDT/SSDT raise bug check 0x00000109 (CRITICAL_STRUCTURE_CORRUPTION); HVCI/Code-Integrity blocks land in Microsoft-Windows-CodeIntegrity/Operational (Event IDs 3001–3089) and Security Event ID 5038.
A starting Sigma rule for vulnerable-driver loads:
title: Suspicious Vulnerable Driver Load (Possible BYOVD)
logsource:
product: windows
service: sysmon
detection:
selection_unsigned:
EventID: 6
Signed: 'false'
selection_known_vuln:
EventID: 6
ImageLoaded|endswith:
- '\RTCore64.sys'
- '\dbutil_2_3.sys'
condition: selection_unsigned or selection_known_vuln
level: highISR/DPC behavior can be traced through the NT Kernel Logger ETW provider with interrupt and DPC flags enabled:
xperf -on Base+Interrupt+DPC
xperf -d trace.etlHardening layers: enforce Driver Signature Enforcement and HVCI (M1048) so unsigned or tampered drivers cannot load even on a compromised kernel; enable the Microsoft Vulnerable Driver Blocklist (HKLM\SYSTEM\CurrentControlSet\Control\CI\Config\VulnerableDriverBlocklistEnable); restrict SeLoadDriverPrivilege to administrators (M1026); and run suspect drivers under Driver Verifier in a VM to force IRQL checks. Monitor bcdedit test-signing changes and the CI\Config registry path for downgrade attempts.
MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | Sysmon EID 6 unsigned/anomalous drivers; HVCI logs |
| Create System Process: Service | T1543.003 | EID 7045 / System 7036 driver-service install |
| Impair Defenses: Disable Tools | T1562.001 | EDR callback integrity, PatchGuard 0x109 |
| Impair Defenses: Downgrade | T1562.010 | CI\Config registry + bcdedit test-signing audit |
| Exploitation for Priv-Esc | T1068 | BYOVD load (EID 6) preceding kernel-write activity |
| Escape to Host | T1611 | Kernel-IRQL execution from container context |
13. Tools for IRQL Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | !irql, !pcr, !dpcs, !analyze -v on bug checks | microsoft.com |
| Driver Verifier | Forces IRQL/pool/deadlock checks on a target driver | microsoft.com |
| Sysmon | Driver-load (EID 6) and service (7045) telemetry | microsoft.com |
| xperf / WPA | ETW interrupt and DPC tracing | microsoft.com |
| Process Hacker | Live driver and kernel-module enumeration | processhacker.sourceforge.io |
| Volatility | Memory-forensic driver and callback inspection | volatilityfoundation.org |
| Ghidra | Static analysis of suspect driver binaries | ghidra-sre.org |
Summary
- IRQL is a per-processor priority register that gates which kernel routines code may legally call and which interrupts are masked.
- The HAL maps hardware vectors onto 16 IRQLs on x64 and 32 on x86; higher value preempts lower, and raising/lowering must follow strict stack discipline.
- Above
APC_LEVELthe scheduler is disabled and paged memory is off-limits — touching it triggersIRQL_NOT_LESS_OR_EQUAL(0x0000000A). - Attackers reach kernel IRQL through BYOVD to unhook EDR, conceal rootkits, or bug-check the host as a DoS — mapped to
T1014,T1543.003,T1562.001, andT1068. - Detect via Sysmon Event ID 6, the vulnerable-driver blocklist, HVCI/DSE enforcement, and
SeLoadDriverPrivilegerestriction.
Related Tutorials
- Windows Scheduler Internals: Priority Levels, Quantum, and Thread Selection
- DPCs: Deferred Procedure Calls and Interrupt Deferral
- Access Tokens and Privileges: The Kernel’s Security Context
- SIDs and Security Descriptors: Identity in Windows Security
- Fibers: User-Mode Cooperative Threads
References
- Managing Hardware Priorities (IRQL Levels) — Windows Kernel Driver Docs | Microsoft Learn
- Always Preemptible and Always Interruptible — Windows Kernel Driver Docs | Microsoft Learn
- IRQL Annotations for Drivers — Windows Driver Testing | Microsoft Learn
- !irql Extension Command (WinDbg Kernel Debugger) | Microsoft Learn
- Dispatch Routines and IRQLs — Windows Kernel Driver Docs | Microsoft Learn
- Guidelines for Writing DPC Routines (DISPATCH_LEVEL IRQL) | Microsoft Learn