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.
Contents
- 1 1. What Is an IRQL?
- 2 2. The IRQL Hierarchy
- 3 3. Software IRQLs: PASSIVE, APC, and DISPATCH
- 4 4. Hardware IRQLs: DIRQL and Above
- 5 5. Kernel APIs for IRQL Management
- 6 6. Memory Access Rules at Each IRQL
- 7 7. DPCs: The DISPATCH_LEVEL Workhorses
- 8 8. APCs: The APC_LEVEL Mechanism
- 9 9. Debugging IRQL With WinDbg
- 10 10. IRQL in a Security Context
- 11 11. Common Attacker Techniques
- 12 12. Defensive Strategies & Detection
- 13 13. Tools for IRQL Analysis
- 14 Summary
- 15 Related Tutorials
- 16 References
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
Get new drops in your inbox
Windows internals, exploit dev, and red-team write-ups — no spam, unsubscribe anytime.