irql 0.1.6

Compile-time IRQL safety for Windows kernel drivers
Documentation

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