README.md
February 17, 2026 · View on GitHub
IRQL — Compile-Time IRQL Safety for Windows Kernel Drivers
IRQL violations cause blue screens. This crate catches them at compile time.
Zero runtime cost. Zero binary overhead.
[dependencies]
irql = "0.1.6"
use irql::{irql, Dispatch, Passive};
#[irql(max = Dispatch)]
fn acquire_spinlock() { /* … */ }
#[irql(max = Passive)]
fn driver_routine() {
call_irql!(acquire_spinlock()); // Passive can raise to Dispatch
}
#[irql(at = Passive)]
fn driver_entry() {
call_irql!(driver_routine());
}
If it compiles, your IRQL transitions are valid.
How it works
#[irql(max = Dispatch)] adds a hidden IRQL type parameter bounded by IrqlCanRaiseTo<Dispatch>. call_irql! threads it through every call as a turbofish argument. The compiler checks every transition — trying to lower IRQL is a compile error:
error[E0277]: IRQL violation: cannot reach `Passive` from `Dispatch`
-- would require lowering
The #[irql()] attribute
| Form | Meaning |
|---|---|
#[irql(at = Level)] | Fixed entry point — known IRQL, no generic |
#[irql(max = Level)] | Callable from Level or below |
#[irql(min = A, max = B)] | Callable in [A, B] |
Works on functions, impl blocks, and trait impl blocks.
IRQL levels
| Value | Type | Description |
|---|---|---|
| 0 | Passive | Normal thread; paged memory OK |
| 1 | Apc | APC delivery |
| 2 | Dispatch | DPC / spinlock |
| 3–26 | Dirql | Device interrupts |
| 27 | Profile | Profiling timer |
| 28 | Clock | Clock interrupt |
| 29 | Ipi | Inter-processor interrupt |
| 30 | Power | Power failure |
| 31 | High | Highest — machine check |
Impl blocks
Apply #[irql] to an entire impl block — every method gets the constraint:
struct Device { name: &'static str }
#[irql(max = Dispatch)]
impl Device {
fn new(name: &'static str) -> Self { Device { name } }
fn process(&self) { /* … */ }
}
Function traits
IrqlFn, IrqlFnMut, IrqlFnOnce — IRQL-aware analogues of Fn, FnMut, FnOnce:
#[irql(max = Passive)]
impl IrqlFn<()> for Reader {
type Output = u32;
fn call(&self, _: ()) -> u32 { self.value }
}
The macro rewrites IrqlFn<()> to IrqlFn<Passive, ()> automatically.
IRQL-aware allocation (nightly)
irql = { version = "0.1.6", features = ["alloc"] }
Requires nightly (allocator_api, vec_push_within_capacity, auto_traits, negative_impls).
Pool types
| Pool | Allocable at | Accessible at |
|---|---|---|
PagedPool | Passive, Apc | Passive, Apc |
NonPagedPool | Passive, Apc, Dispatch | Any IRQL |
IrqlBox::new and IrqlVec::new pick the cheapest legal pool automatically.
IrqlBox and IrqlVec
#[irql(max = Passive)]
fn example() -> Result<(), AllocError> {
let data = call_irql!(IrqlBox::new(42))?;
let val = call_irql!(data.get());
let v = irql_vec![1, 2, 3]?;
call_irql!(v.push(42))?;
// FFI: transfer ownership via raw pointer
let ptr = data.into_raw();
let data = unsafe { IrqlBox::<_, PagedPool>::from_raw(ptr) };
Ok(())
}
Drop safety
Paged-pool containers cannot be dropped at Dispatch or above. The #[irql] macro injects SafeToDropAt<Level> bounds on by-value parameters, so passing paged-pool memory into elevated-IRQL code is a compile error. References (&IrqlBox) are not gated. Use leak() or into_raw() to transfer ownership across IRQL boundaries.
Crate architecture
irql ← public facade (re-exports everything)
├── irql_core ← levels, hierarchy traits, function traits, SafeToDropAt
├── irql_macro ← #[irql] proc macro, call_irql! rewriter
└── irql_alloc ← IrqlBox, IrqlVec, pool allocator (optional, nightly)
Safety
All checks are compile-time only. You must ensure entry points (#[irql(at = …)]) match the actual runtime IRQL and that IRQL-raising operations are properly modelled.
License
MIT or Apache 2.0, at your option.