A #[global_allocator] for Windows kernel drivers written in #![no_std] Rust.
ntalloc serves the alloc crate (Vec, Box, String and the rest) from the
kernel pool through ExAllocatePool2, on stable Rust. With the allocator-api
feature (Rust 1.100 or newer) it also implements the Allocator trait, so a
single collection can use a pool of its own.
- Builds on stable Rust 1.85; the opt-in
AllocatorAPI needs 1.100, and only its marker traits need nightly - One zero-sized type, configured entirely at compile time: the pool flags, the pool tag, and whether freeing checks the tag
- Any alignment a
Layoutasks for, including#[repr(align(64))]types and alignments larger than a page POOL_FLAG_RAISE_ON_FAILUREand invalid pool tags are refused when the driver is built, not discovered when it runs- Testable in user mode with the
usermode-backendfeature, and checked under Miri
- Windows 10, version 2004 or later.
ExAllocatePool2does not exist on earlier versions. - Rust 1.85 or later.
panic = "abort"in the driver's own profiles. A kernel driver cannot unwind, and profile settings in a dependency never apply to the crate that uses it.
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"[dependencies]
ntalloc = "0.1"In the driver's lib.rs, next to #![no_std] and its panic handler:
extern crate alloc;
use alloc::vec::Vec;
use ntalloc::NonPagedAlloc;
#[global_allocator]
static GLOBAL: NonPagedAlloc = NonPagedAlloc {};
// Anywhere in the driver, the alloc types now draw on the non-paged pool.
let ids: Vec<u32> = (1..=4).collect();
assert_eq!(ids.len(), 4);Every allocation the driver makes through alloc now comes from the non-paged
pool under the tag Rust, and goes back to it when freed.
| Alias | Pool flag | Allocate at | Touch the memory at |
|---|---|---|---|
NonPagedAlloc |
POOL_FLAG_NON_PAGED |
IRQL ≤ DISPATCH_LEVEL |
any IRQL |
PagedAlloc |
POOL_FLAG_PAGED |
IRQL ≤ APC_LEVEL |
IRQL ≤ APC_LEVEL |
A global allocator serves every allocation in the driver, including those made
in a DPC or while a spin lock is held, so NonPagedAlloc is the safe choice for
#[global_allocator]. PagedAlloc suits code that never allocates or touches
the memory at DISPATCH_LEVEL, typically through the Allocator API below.
ExAllocatePool2 returns null when it cannot serve a request, and ntalloc
passes that on. Fallible APIs such as Vec::try_reserve report it as an error.
Infallible ones such as Vec::push and Box::new call handle_alloc_error,
which in a #![no_std] build panics and hands control to the driver's panic
handler. Where running out of pool must be survivable, reserve with the
fallible APIs first.
Both aliases are shorthand for
NtAlloc<ALLOC_POOL_FLAGS, MEMORY_TAG, TAGGED_DEALLOC, REALIGN>, which takes
any combination of pool flags:
use ntalloc::{NonPagedAlloc, NtAlloc, POOL_FLAG_NON_PAGED, POOL_FLAG_UNINITIALIZED};
// Shows up as "MyDr" in !poolused and !poolfind.
type DriverAlloc = NonPagedAlloc<{ u32::from_ne_bytes(*b"MyDr") }>;
// Non-paged pool without the zeroing ExAllocatePool2 does by default.
type ScratchAlloc = NtAlloc<{ POOL_FLAG_NON_PAGED | POOL_FLAG_UNINITIALIZED }>;
assert!(DriverAlloc::POOL_ZEROES);
assert!(!ScratchAlloc::POOL_ZEROES);MEMORY_TAGdefaults toDEFAULT_MEMORY_TAG, which the debugger shows asRust. Build your own withu32::from_ne_bytes, as above, and it appears exactly as written. A tag is one to four printable ASCII characters, padded with zero bytes at the end when shorter, and needs at least one letter or digit.TAGGED_DEALLOC,trueby default, frees withExFreePoolWithTag, which checks the tag;falsefrees withExFreePool.REALIGN,trueby default, serves aLayoutaligned more strictly than the pool guarantees by over-allocating and realigning, as described under Alignment;falseturns that off, for drivers that never need more than the pool's own alignment.ExAllocatePool2zeroes every block unlessPOOL_FLAG_UNINITIALIZEDis set, andPOOL_ZEROESsays which applies.alloc_zeroedandallocate_zeroedreturn zeroed memory either way. Microsoft advises against skipping the zeroing for memory that may be copied to user mode or over the network.- The flags exported with the type
POOL_FLAGSarePOOL_FLAG_PAGED,POOL_FLAG_NON_PAGED,POOL_FLAG_UNINITIALIZED,POOL_FLAG_CACHE_ALIGNED,POOL_FLAG_USE_QUOTAandPOOL_FLAG_SPECIAL_POOL. Other flags fromwdm.hcan be passed as raw values, with the one exception below. - Some configurations fail the build:
POOL_FLAG_RAISE_ON_FAILURE, which would make a failed allocation raise an SEH exception Rust cannot handle; a zero tag, which makes every allocation fail; any other tag that breaks the rules above; and"BIG ", a tag the kernel reserves. The error, E0080, comes fromcargo build;cargo checkdoes not report it.
ExAllocatePool2 aligns a block to MIN_ALIGNMENT, 16 bytes on 64-bit
Windows, or to the page size for a block of a page or more; kernel_alignment
gives the figure for a size. A Layout that asks for more is served by
allocating layout.align() extra bytes, rounding the pointer up, and storing
the pool's own pointer in the POINTER_SIZE bytes just before the one handed
out. GlobalAlloc and Allocator take care of both directions.
The same steps are available as functions. Memory from allocate_aligned must go
back through deallocate_aligned with the same Layout; allocate and
deallocate never realign, and pair only with each other:
use core::alloc::Layout;
use ntalloc::NonPagedAlloc;
// Call through an alias like this one: `NonPagedAlloc::allocate_aligned(..)`
// cannot infer the defaults of the alias's parameters (error E0284).
type Alloc = NonPagedAlloc;
let layout = Layout::from_size_align(256, 64).unwrap();
let ptr = Alloc::allocate_aligned(layout);
assert!(!ptr.is_null());
// SAFETY: `ptr` came from `allocate_aligned` with this layout, and is freed once.
unsafe { Alloc::deallocate_aligned(ptr, layout) };kernel_allocate is allocate without the test backend: it always reaches the
pool, with or without usermode-backend.
Realignment is on by default. A driver that never needs more than the pool's
own alignment can turn it off by setting REALIGN to false, as in
NonPagedAlloc<DEFAULT_MEMORY_TAG, true, false>: every request then goes to the
pool by size, and the alignment a Layout asks for is ignored. Only do so if
nothing allocated through it needs more, including types from other crates: a
misaligned Box or reference is undefined behaviour, which rules out
#[repr(align(32))] and larger types, 256- and 512-bit SIMD types, and
crossbeam's CachePadded.
With the allocator-api feature, NtAlloc implements core::alloc::Allocator,
so one collection can use a different pool or tag from the global allocator:
use ntalloc::PagedAlloc;
// A paged-pool buffer, for code that runs at PASSIVE_LEVEL.
let mut name: Vec<u16, PagedAlloc> = Vec::new_in(PagedAlloc::default());
name.extend("rule".encode_utf16());allocator-api-markers adds the StaticAllocator and AllocatorClone marker
traits, which Box::pin_in, cloning an Arc or Rc, and BTreeMap require.
They are still unstable, so it needs a nightly compiler from October 2026 or
later.
With the usermode-backend feature, code that allocates through NtAlloc runs
in ordinary tests, on the process heap instead of the kernel pool:
[dev-dependencies]
ntalloc = { version = "0.1", features = ["usermode-backend"] }Where the pool would bugcheck with BAD_POOL_CALLER, the test process prints
what went wrong and aborts instead: on a double free, on a pointer the allocator
never handed out, and, with TAGGED_DEALLOC, on the wrong tag. Windows reports
any abort as STATUS_STACK_BUFFER_OVERRUN, so run the tests with
-- --test-threads=1 to see which test it was.
Enable the feature only under [dev-dependencies]. It brings in std, so as a
normal dependency it reaches the driver itself, which then fails to build with a
duplicate panic_impl lang item.
| Feature | Toolchain | Adds |
|---|---|---|
allocator-api |
Rust 1.100 | the Allocator impl |
allocator-api-markers |
nightly | StaticAllocator and AllocatorClone; implies allocator-api |
usermode-backend |
stable | the process-heap backend, for [dev-dependencies] only |
There are no default features.
| Item | What it is |
|---|---|
NtAlloc |
the allocator: pool flags, tag, free routine, realignment |
PagedAlloc, NonPagedAlloc |
NtAlloc on the paged and non-paged pool |
POOL_FLAGS, POOL_FLAG_* |
the pool flags ExAllocatePool2 takes |
DEFAULT_MEMORY_TAG |
the default tag, Rust |
MIN_ALIGNMENT, POINTER_SIZE |
the pool's smallest alignment; a stashed pointer's size |
POOL_ZEROES |
whether a configuration's blocks arrive zeroed |
kernel_alignment |
the alignment the pool guarantees for a size |
allocate, deallocate |
a block of a given size, at the pool's own alignment |
allocate_aligned, deallocate_aligned |
a block for a Layout, realigned when needed |
kernel_allocate |
allocate, always from the pool |
GlobalAlloc, Allocator |
the trait impls; Allocator with the allocator-api feature |
The full reference is on docs.rs.
1.85, set by the 2024 edition. The allocator-api feature requires Rust
1.100, where the Allocator trait became stable, and allocator-api-markers a
nightly compiler; everything else builds on 1.85.
The MSRV is treated as a compatibility promise: raising it is a breaking change and will come with a minor version bump before 1.0.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.