Vector Allocation
July 16, 2026 ยท View on GitHub
The fixed vectors, the exceptions, the legacy IRQs, the syscall gate, are assigned at build
time. The rest of the vector space is a pool the kernel hands out at runtime, for example when
the hardware broker routes a claimed device's line to a fresh vector. This page documents that
pool. The code is under src/interrupts/allocation/.
The registry
The allocator's state is one global registry (src/interrupts/allocation/registry.rs:21),
two parallel arrays indexed by vector:
struct Registry {
reserved: [bool; 256], // is this vector taken
handlers: [Option<NoErrorHandler>; 256], // its handler, if registered
}
A vector is available only if it is neither reserved nor has a handler. The registry is behind
an RwLock, so allocation takes the write lock and the availability query takes the read lock.
What is reserved
init (allocation/init.rs:20) marks the fixed part of the space as reserved before any
dynamic allocation can happen:
reserve vectors 0 .. RESERVED_VECTORS_END (32) // all CPU exceptions
reserve TIMER_VECTOR (32)
reserve KEYBOARD_VECTOR (33)
reserve SYSCALL_VECTOR (0x80)
The first thirty-two vectors are the CPU exceptions and are never allocatable. The timer, keyboard, and syscall vectors are individually reserved on top, because they have fixed handlers installed in the IDT and must not be handed to a dynamic requester.
Allocating and freeing
allocate_vector (allocation/allocator.rs:20) walks upward from the first non-reserved
vector and claims the first free slot:
allocate_vector():
for vector in RESERVED_VECTORS_END ..= 255:
if not reserved[vector] and handlers[vector] is None:
reserved[vector] = true
return Some(vector)
None // pool exhausted
free_vector refuses to free anything below RESERVED_VECTORS_END and refuses to free a
vector that was not allocated, then clears both the reserved flag and the handler. The
allocation starts at 32 rather than at the user-allocatable range's nominal start, so the
legacy IRQ vectors are claimable by a requester that owns the corresponding line, while the
individually-reserved timer, keyboard, and syscall vectors within that range stay off limits.
The failure modes are explicit, None when the pool is exhausted and an error string when a
free is invalid, rather than silent wraparound.
Security analysis
The vector pool is small and its safety story is mostly about not handing out something that is already in use, but the fixed-vector guarantee it enforces is load bearing. Three properties.
The exceptions are never allocatable. init reserves vectors 0 through 31 before any dynamic
allocation can run (init.rs), and both allocate_vector and free_vector refuse to cross
RESERVED_VECTORS_END (32): the allocator scans upward from 32 (allocator.rs:23) and free_vector
returns "cannot free reserved vector" for anything below it (allocator.rs:35). So no runtime
requester can ever be handed a CPU exception vector or free one out from under the fixed handlers, which
means a dynamic allocation can never shadow the page-fault or double-fault gate.
Availability is a two-array conjunction. A vector is available only if it is neither reserved nor
holds a registered handler (allocator.rs:25). Allocation takes the RwLock write lock and flips the
reserved bit atomically with the check, so two racing allocators cannot both claim the same vector; the
availability query takes the read lock. The timer, keyboard, and syscall vectors are individually
reserved on top of the exception range (init.rs) because they carry fixed handlers in the IDT
and must not be dispensed even though they sit inside the otherwise-claimable legacy IRQ span.
Failure is explicit, not silent. allocate_vector returns None when the pool is exhausted rather
than wrapping around, and free_vector returns "vector not allocated" for a double free
(allocator.rs:42), so a bug in a caller surfaces as a handled error rather than as a reused vector. The
honest boundary is that this registry is the kernel's own bookkeeping, not a capability check: it tracks
which vectors are taken, but the authority to route a device's line to a vector lives in the
broker IRQ bind path, which is what ties a vector to a claimed device. A
caller that allocates a vector still has to be trusted kernel code; the registry stops collisions, not
privilege escalation.
Debugging vector allocation
The allocator is deterministic, so its two failure returns tell you exactly what went wrong.
allocate_vector() -> None the pool 32..=255 is exhausted (every slot reserved or handled)
free_vector(v) -> "cannot free reserved vector" v is below 32 (an exception or fixed vector)
free_vector(v) -> "vector not allocated" v was never reserved (double free or stale id)
A None from allocate_vector under a growing device count means the broker vector pool or the dynamic
range genuinely filled, and the fix is capacity, not a bug; because allocation scans from 32 upward, the
first free slot is always returned, so the allocated vectors cluster low and a None really does mean
full. The two free_vector strings separate a caller that computed the wrong vector (below 32) from a
caller that freed twice or freed a vector it never held, which is usually a lifecycle bug where a grant
was released and then released again. Because the registry is behind an RwLock, a hang taking the write
lock during allocation points at a handler that is holding the read lock across a blocking call rather
than at the allocator itself.
Source map
src/interrupts/allocation/registry.rs the reserved and handler arrays
src/interrupts/allocation/init.rs reserving the fixed vectors
src/interrupts/allocation/allocator.rs allocate_vector, free_vector, is_vector_available
src/interrupts/allocation/handlers.rs register_handler, get_handler, unregister_handler
src/interrupts/allocation/types.rs RESERVED_VECTORS_END and the fixed vector constants
Every reference above is verified against those trees. The fixed handlers this pool reserves around are installed on the IDT page, and the broker bind path that consumes dynamically-routed vectors is on the broker IRQ page.