Interrupts, IRQLs, DPCs, and APCs

Windows Internals
Time it takes to read this article 6 minutes.

A running Windows system is constantly interrupted. A network packet arrives, a disk finishes a read, the system clock ticks, another processor needs attention — each of these is a hardware event that must be serviced promptly, often in the middle of whatever thread happened to be running. At the same time, the kernel has to manipulate shared data structures (the scheduler database, driver queues, timers) without a higher-priority interrupt corrupting them halfway through. The mechanism that reconciles these competing demands is IRQL — the Interrupt Request Level — together with two deferred-work primitives, DPCs and APCs. Understanding them explains why some code can page-fault and some cannot, how drivers defer work, and how a whole family of code-injection techniques operates.

Interrupt Request Levels (IRQL)

IRQL is a per-processor priority value that governs which interrupts a processor will currently accept. It is not a thread priority — it is a property of the processor at a given instant. The rule is simple and absolute: raising a processor’s IRQL masks every interrupt at that level and below, so only a strictly higher-IRQL event can preempt the current work. Servicing an interrupt raises IRQL to that interrupt’s level for the duration; lowering it back re-enables the masked sources.

From lowest to highest, the levels that matter most are:

  • PASSIVE_LEVEL (0) — normal execution. All user-mode code and most kernel code (including driver dispatch routines) runs here. Nothing is masked; the thread scheduler is fully in control.
  • APC_LEVEL (1) — used when delivering kernel APCs and while certain locks are held. Only APC delivery is masked.
  • DISPATCH_LEVEL (2) — the level of the thread scheduler (dispatcher) and DPCs. This is the critical threshold: at or above it, the scheduler itself is masked, so the current code cannot be context-switched away.
  • Device IRQLs (DIRQL) — a band above DISPATCH_LEVEL assigned to hardware device interrupts; a device’s Interrupt Service Routine runs at its DIRQL.
  • High levels — CLOCK, IPI (inter-processor interrupt), HIGH_LEVEL — reserved for the clock tick, cross-processor signalling, and unrecoverable conditions.

The single most important consequence follows directly from what DISPATCH_LEVEL masks: code running at or above DISPATCH_LEVEL cannot page-fault and cannot block. Resolving a page fault requires the memory manager to run and possibly wait on I/O, and waiting requires the scheduler — but the scheduler is masked. Touch paged-out memory or call a blocking wait at DISPATCH_LEVEL and the system bugchecks (most famously IRQL_NOT_LESS_OR_EQUAL).

Hardware Interrupts and the IDT

When a device raises an interrupt, the processor consults the Interrupt Dispatch Table (IDT) — an array indexed by interrupt vector that maps each vector to a handler. Hardware interrupt vectors point (through Windows’ interrupt objects) at the driver’s registered Interrupt Service Routine (ISR), which runs at the device’s DIRQL. Because a running ISR masks its own device and everything below it, an ISR must be short: it acknowledges the hardware, captures the minimum state needed, and gets out. Any substantial processing is deferred to a lower IRQL so the device — and the rest of the system — is not stalled.

Deferred Procedure Calls (DPCs)

The mechanism an ISR uses to defer work is the Deferred Procedure Call (DPC). Before returning, the ISR queues a DPC — a routine plus context — onto the processor’s per-CPU DPC queue. Once all pending interrupts drop the processor back toward DISPATCH_LEVEL, the kernel drains the DPC queue, running each DPC routine at DISPATCH_LEVEL. Crucially, DPCs run before the dispatcher selects the next thread, so deferred device work is completed promptly without waiting for a thread to be scheduled.

Because DPCs execute at DISPATCH_LEVEL, they inherit its constraints: a DPC must not block, must not page-fault, and may only touch non-paged memory. It also has no specific thread context — it runs in whatever thread happened to be interrupted, so it cannot rely on the current process’s address space. DPCs are the workhorse of driver design: timers, I/O completion staging, and deferred device servicing all flow through them. A driver that spends too long in its DPCs raises system latency, which is why tools that measure “DPC latency” exist.

Asynchronous Procedure Calls (APCs)

Where a DPC targets a processor, an Asynchronous Procedure Call (APC) targets a specific thread. An APC is a function queued to run in the context of a chosen thread — meaning in that thread’s address space, as that thread. There are two broad kinds:

  • Kernel APCs run at APC_LEVEL in kernel mode. A special kernel APC is used by the system for critical work such as delivering I/O completion into the initiating thread; normal kernel APCs can be disabled by raising to APC_LEVEL or entering a critical/guarded region.
  • User APCs run in user mode at PASSIVE_LEVEL, but only when the target thread is in an alertable wait — a wait entered via functions like SleepEx, WaitForSingleObjectEx, or MsgWaitForMultipleObjectsEx with the alertable flag set. Until the thread alertably waits, a queued user APC simply sits pending.

APCs are how a lot of asynchronous work is delivered back to the thread that asked for it. Overlapped I/O completion routines, for example, are delivered as user APCs when the issuing thread next enters an alertable wait — the completion runs “as” that thread, with access to its buffers and handles.

IRQL Rules for Code

The level a piece of code runs at dictates what it is allowed to do. The practical rules every driver author internalizes:

  • At PASSIVE_LEVEL / APC_LEVEL: you may access paged memory, block on dispatcher objects, and allocate from paged or non-paged pool. This is where normal work belongs.
  • At DISPATCH_LEVEL and above: no page faults (touch only non-paged memory and code), no waits that block (only acquire spin-locks and other non-blocking synchronization), and allocate only from non-paged pool. You may not call routines that could lower IRQL or wait.
  • Synchronization follows IRQL: a resource shared between a PASSIVE-level routine and a DISPATCH-level DPC must be protected by a mechanism that raises IRQL (a spin lock), because a plain mutex/blocking lock cannot be taken at DISPATCH_LEVEL.

Inspecting Interrupts, DPCs, and APCs in WinDbg

All of this state is observable in a kernel debugger:

# Current IRQL of the processor
!irql

# Dump the Interrupt Dispatch Table: vector -> ISR mapping
!idt

# Show the per-processor DPC queues and the pending DPC routines
!dpcs

# Inspect the APC queues (kernel and user) for a given thread
!apc <thread-address>
!thread <thread-address>    # the APC STATE line shows queued APCs

# What each processor is currently running
!running

!irql confirms the level you are debugging at; !idt resolves interrupt vectors to the driver ISRs behind them — invaluable when hunting a suspicious or hooked interrupt handler. !dpcs reveals what deferred work is queued (and which driver owns it), and !apc / !thread expose the APCs pending against a thread — the exact structures the injection techniques below manipulate.

Security Relevance

The APC mechanism is the basis of a mainstream code-injection family. In APC injection, an attacker with sufficient access to a target process writes a payload into its address space and then queues a user APC (via QueueUserAPC) pointing at that payload onto one of the target’s threads. The next time that thread enters an alertable wait, the APC fires and the payload runs in the target’s context — no new thread, no obvious CreateRemoteThread. The “early bird” variant queues the APC against a thread in a freshly created, suspended process before it begins executing, so the payload runs almost immediately as the process initializes. This ties directly back to the thread internals covered earlier in the series: the APC is delivered through the target thread’s queued-APC state.

On the kernel side, the same primitives appear in different abuses: malicious or buggy drivers can queue DPCs or timers to gain periodic execution at DISPATCH_LEVEL, and IRQL mismanagement — touching pageable memory or taking a bad lock at the wrong level — is a common root cause of driver crashes that, in a controlled exploit, can become a privilege-escalation primitive. For defenders, understanding these levels is what makes the telemetry legible: EDRs watch for the API sequences behind APC injection (OpenThreadQueueUserAPC against a remote thread, preceded by a remote write into executable memory), and kernel integrity mechanisms guard the IDT and DPC/timer structures against tampering. (All described for defensive understanding and authorized testing only.)

Conclusion

IRQL is the backbone of how Windows arbitrates between hardware events, deferred work, and normal thread execution. Interrupts arrive at high IRQL and are kept short; DPCs defer the bulk of device work to DISPATCH_LEVEL, where the scheduler is masked and paging is forbidden; and APCs deliver work into the context of a specific thread, at APC_LEVEL for the kernel or at PASSIVE_LEVEL for user-mode completion routines. These rules explain both why kernel code is written the way it is and how techniques like APC injection operate. In the next article we descend into how the memory a process actually allocates is organized — Windows heap internals, from the NT heap to the modern segment heap.

You Might Also Like

This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:

Comments

Copied title and URL