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 Namex64 Valuex86 ValueDescription
PASSIVE_LEVEL / LOW_LEVEL00Normal thread execution; nothing masked
APC_LEVEL11APC delivery and page-fault handling
DISPATCH_LEVEL22Thread scheduler / DPC queue
CMC_LEVEL3Correctable Machine Check
Device IRQLs (DIRQL)4–113–26Hardware device interrupts
CLOCK_LEVEL1328System clock timer
IPI_LEVEL / DRS_LEVEL1429Inter-Processor Interrupt
POWER_LEVEL1530Power failure
PROFILE_LEVEL / HIGH_LEVEL1531Profiling / 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).


Hierarchical diagram showing Windows IRQL levels from HIGH_LEVEL at the top down to PASSIVE_LEVEL at the bottom, colour-coded by hardware versus software IRQLs
Windows x64 IRQL hierarchy: higher-numbered levels preempt all lower ones, with software IRQLs at the base and hardware interrupt levels at the top.

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.

FeaturePASSIVE_LEVELAPC_LEVELDISPATCH_LEVEL
Thread contextYesYesNot guaranteed
Scheduler activeYesYesNo
Paged pool accessYesYesNo
Blocking waits allowedYesYesNo

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 FunctionPurpose
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 oldIrql

A 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 PoolPASSIVE_LEVELAPC_LEVELDISPATCH_LEVEL+
Paged poolAccessibleAccessibleFatal page fault
Non-paged poolAccessibleAccessibleAccessible

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.


Flow diagram illustrating the handoff from a hardware interrupt through the ISR at DIRQL to a queued DPC callback executing at DISPATCH_LEVEL 2
ISRs acknowledge hardware and queue a DPC object; the kernel drains DPC queues at DISPATCH_LEVEL so heavy processing never blocks critical interrupt handling.

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 -v

On 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 cr8

The 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.


Flow diagram showing a BYOVD attack path from loading a vulnerable signed driver through raising IRQL to DISPATCH_LEVEL to bypass EDR hooks or trigger a denial-of-service blue screen
Attackers exploit IRQL semantics via BYOVD: owning the processor at DISPATCH_LEVEL lets them silently unhook defenses or weaponize paged-memory violations as a kernel-mode DoS.

11. Common Attacker Techniques

TechniqueDescription
BYOVD kernel executionLoad a signed-but-vulnerable driver (e.g. RTCore64.sys, dbutil_2_3.sys) to run code at kernel IRQL
EDR unhooking at DISPATCH_LEVELPatch SSDT entries or kernel callbacks while the scheduler is disabled, beating re-hook races
Rootkit concealmentHide processes, files, and connections from DIRQL/DISPATCH_LEVEL, below user-mode visibility
Spin-lock starvationHold a spin lock at DISPATCH_LEVEL to monopolize a processor — driver-stack DoS
Deliberate IRQL faultForce paged access above APC_LEVEL to bug-check the host (0x0000000A DoS)
DSE downgradeFlip 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: high

ISR/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.etl

Hardening 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

TechniqueMITRE IDDetection
RootkitT1014Sysmon EID 6 unsigned/anomalous drivers; HVCI logs
Create System Process: ServiceT1543.003EID 7045 / System 7036 driver-service install
Impair Defenses: Disable ToolsT1562.001EDR callback integrity, PatchGuard 0x109
Impair Defenses: DowngradeT1562.010CI\Config registry + bcdedit test-signing audit
Exploitation for Priv-EscT1068BYOVD load (EID 6) preceding kernel-write activity
Escape to HostT1611Kernel-IRQL execution from container context

13. Tools for IRQL Analysis

ToolDescriptionLink
WinDbg!irql, !pcr, !dpcs, !analyze -v on bug checksmicrosoft.com
Driver VerifierForces IRQL/pool/deadlock checks on a target drivermicrosoft.com
SysmonDriver-load (EID 6) and service (7045) telemetrymicrosoft.com
xperf / WPAETW interrupt and DPC tracingmicrosoft.com
Process HackerLive driver and kernel-module enumerationprocesshacker.sourceforge.io
VolatilityMemory-forensic driver and callback inspectionvolatilityfoundation.org
GhidraStatic analysis of suspect driver binariesghidra-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_LEVEL the scheduler is disabled and paged memory is off-limits — touching it triggers IRQL_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, and T1068.
  • Detect via Sysmon Event ID 6, the vulnerable-driver blocklist, HVCI/DSE enforcement, and SeLoadDriverPrivilege restriction.

Related Tutorials

References