Interrupt Safety
July 16, 2026 ยท View on GitHub
Two small mechanisms keep interrupt handling from tripping over itself: an RAII guard that
disables interrupts for a critical section and restores the prior state exactly, and a per-CPU
record of whether the CPU is currently inside an interrupt handler. This page documents both.
The code is under src/interrupts/safety/.
The interrupt guard
InterruptGuard (src/interrupts/safety/guard.rs:20) is a scoped critical section. On
creation it reads the current interrupt-enable flag, disables interrupts if they were enabled,
and remembers what it found; on drop it re-enables them only if they had been enabled:
InterruptGuard::new():
was_enabled = interrupts_enabled() // read IF from RFLAGS
if was_enabled: cli
Drop:
if was_enabled: sti
Restoring the prior state rather than unconditionally enabling is what makes the guard safe to
nest: a guard taken inside another guard's section finds interrupts already disabled, records
that, and leaves them disabled on drop, so the outer section is not cut short. The flag is read
straight from RFLAGS with pushfq, and the enable and disable are sti and cli; the guard
restores state even on an unwinding path because it lives in Drop.
Interrupt context
Separately, each handler records that its CPU is in interrupt context. set_interrupt_context
(src/interrupts/safety/context.rs:74) returns a guard that bumps a per-CPU nesting depth and
sets a per-CPU in-interrupt flag; the guard's Drop decrements the depth and clears the flag
only when the outermost handler exits:
set_interrupt_context():
depth[cpu] += 1
in_interrupt[cpu] = true
-> InterruptContext { cpu }
Drop:
if depth[cpu] drops to 0: in_interrupt[cpu] = false
in_interrupt_context() lets code elsewhere ask whether it is running inside a handler, which
matters for choosing between a path that may sleep and one that must not. The state is
per-CPU, indexed by a CPU id read from the per-CPU data block at gs:8 (the cpu_id field
sits just past the self-pointer at offset 0); the read requires the kernel GS base to be
loaded, which is exactly what the trampolines guarantee before any Rust
handler runs. The depth counter, rather than a bare boolean, is what makes the flag correct
under nesting: a higher-priority interrupt taken inside a handler increments the depth, and the
flag stays set until the last one unwinds.
Security analysis
These two mechanisms are internal correctness aids, not privilege boundaries, but their correctness is what keeps kernel critical sections from being cut short and keeps sleep-in-atomic bugs out of interrupt context. Three properties.
The guard restores, it does not force. InterruptGuard remembers whether interrupts were enabled at
entry and only re-enables on drop if they were (guard.rs:36), reading the flag straight from RFLAGS
with pushfq. That is what makes it safe to nest: a guard taken inside another guard's section finds
interrupts already off, records that, and leaves them off on drop, so the outer section is never
truncated. Because the restore lives in Drop, it holds on an early return or an unwinding path, so a
critical section cannot leak interrupts-disabled state to the code that follows it.
The depth counter, not a boolean, tracks nesting. set_interrupt_context bumps a per-CPU
INTERRUPT_DEPTH and sets the IN_INTERRUPT flag; the guard's Drop clears the flag only when the
depth returns to zero (context.rs:63). A higher-priority interrupt taken inside a handler increments
the depth and the flag stays set until the last one unwinds, so in_interrupt_context() stays honest
under nesting. That flag is the thing that lets code elsewhere refuse to sleep on a path that must not,
so a false negative there would be a real bug, which is why it is a counter and not a bare bool.
The per-CPU read depends on a loaded kernel GS. The CPU id comes from gs:8
(context.rs:41), the cpu_id field just past the self-pointer at offset 0 in the per-CPU block. The
read is only correct once the kernel GS base is loaded, which is exactly the guarantee the
trampolines provide before any Rust handler runs. The honest boundary is that this is a
hard dependency, not a checked one: if a handler ran before the swapgs, gs:8 would read the user GS
base and index the wrong CPU's counters. The % MAX_CPUS on the read keeps a garbage id from indexing
out of bounds, but it cannot make a wrong id right, so the safety of this whole module is conditional on
the trampoline discipline above it. Neither mechanism is reachable from ring 3; both are kernel-internal.
Debugging interrupt safety
Bugs here are timing bugs and they rarely print, so they are diagnosed by their shape rather than by a message.
Interrupts stuck off. If a critical section leaves interrupts disabled after it should have restored
them, the symptom is a dead CPU: no timer ticks, no preemption, the scheduler frozen. The cause is
almost always a guard that was mem::forget-ed or a manual cli without a matching sti, not
InterruptGuard itself, since the guard restores in Drop. The check is whether the RFLAGS IF bit is
set where it should be; a guard whose was_enabled captured the wrong prior state (for instance taken
with interrupts already off and expected to enable them) will correctly leave them off, which looks like
a hang but is the guard doing exactly what it promised.
A stuck in-interrupt flag. If in_interrupt_context() reports true outside any handler, an
InterruptContext was leaked (dropped depth never reached zero), and code that consults the flag to pick
a non-sleeping path will keep choosing it forever. Because the flag is per-CPU and indexed by gs:8, a
flag that is wrong on one core but right on others points at that core's GS base being wrong when
set_interrupt_context ran, which loops back to the trampoline: a handler that read gs:8 before its
swapgs would bump the wrong CPU's depth. So a per-CPU-asymmetric interrupt-context bug is a trampoline or
GS-base bug seen through this counter, and the fix is on the trampolines page, not
here.
Source map
src/interrupts/safety/guard.rs InterruptGuard, the cli/sti critical section
src/interrupts/safety/context.rs per-CPU interrupt-context depth and flag, cpu_id via gs:8
Every reference above is verified against those trees. The kernel GS base this module's gs:8 read
depends on is established by the trampolines, the handlers that take an interrupt
context at entry are on the handlers page, and the per-CPU block layout comes from the
SMP setup.