README.md

February 17, 2026 · View on GitHub

irql

IRQL — Compile-Time IRQL Safety for Windows Kernel Drivers

Crates.io Documentation CI License: MIT License: Apache 2.0

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

FormMeaning
#[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

ValueTypeDescription
0PassiveNormal thread; paged memory OK
1ApcAPC delivery
2DispatchDPC / spinlock
3–26DirqlDevice interrupts
27ProfileProfiling timer
28ClockClock interrupt
29IpiInter-processor interrupt
30PowerPower failure
31HighHighest — 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

PoolAllocable atAccessible at
PagedPoolPassive, ApcPassive, Apc
NonPagedPoolPassive, Apc, DispatchAny 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.