The aarch64 IRQ-exit path takes the global SCHEDULER spin lock when
do_softirq overflows its iteration limit and wakes the local ksoftirqd.
The path, with the lines it is made of
4 of the 4 lines are on main today, and 0 of the 4 come from an open
branch (git blame against origin/main, commits below):
file:line on main |
text |
introduced |
kernel/src/arch_impl/aarch64/exception.rs:2212 |
crate::task::softirqd::do_softirq(); |
0668208d7, 2026-02-06 |
kernel/src/task/softirqd.rs:220 |
wakeup_ksoftirqd(); (the restart_count >= MAX_SOFTIRQ_RESTART arm) |
edf9186fe, 2026-01-21 |
kernel/src/task/kthread.rs:227 |
`scheduler::with_scheduler( |
sched |
kernel/src/task/scheduler.rs:4609 |
let mut scheduler_lock = lock_scheduler(); inside with_scheduler |
af73c0d45, 2026-08-06 |
exception.rs:2212 runs after irq_exit() on the aarch64 IRQ path -- 1 of
the 1 post-irq_exit do_softirq call site compiled on that arch. When the
dispatch loop hits MAX_SOFTIRQ_RESTART with work still pending it calls
wakeup_ksoftirqd(), which calls kthread_unpark, which calls
with_scheduler, which blocks in lock_scheduler() on the one global
SCHEDULER mutex. So an interrupt handler can spin on a lock a thread on
another CPU is holding.
Reproduce the shape by reading the four spans:
nl -ba kernel/src/arch_impl/aarch64/exception.rs | sed -n '2205,2216p'
nl -ba kernel/src/task/softirqd.rs | sed -n '214,224p'
nl -ba kernel/src/task/kthread.rs | sed -n '223,234p'
nl -ba kernel/src/task/scheduler.rs | sed -n '4603,4614p'
Linux, for contrast
Linux takes no global runqueue lock here. wakeup_softirqd() is called from
__do_softirq's overflow arm and from irq_exit(), and it wakes the current
CPU's ksoftirqd through wake_up_process, which takes that task's own
runqueue lock with raw_spin_lock_irqsave (rq_lock_irqsave /
task_rq_lock). The lock is per-runqueue, not global, and it is
interrupt-safe by construction: an IRQ that arrives while a thread on the same
CPU holds it cannot deadlock, and a peer CPU's runqueue is a different lock.
claim-lint:ok: the contrast is a source reading of Linux's
kernel/softirq.c::wakeup_softirqd and kernel/sched/core.c::try_to_wake_up,
not a measurement of this tree.
claim-lint:ok: the count is a source count -- static SCHEDULER has 1
declaration in kernel/src/task/scheduler.rs.
This kernel has one SCHEDULER mutex shared by every CPU, and it is a bare
spin::Mutex with no irqsave discipline of its own, so an IRQ-exit acquisition
contends with every thread-context acquisition on every CPU.
Scope
An IRQ-exit wakeup in this kernel shares this property whichever thread it
wakes, not just the ksoftirqd one: a wake reached from exception.rs's
post-irq_exit work ends in Scheduler::unblock, which is a method on the
locked scheduler: it takes &mut Scheduler, and that reference exists only
inside the lock_scheduler() critical section.
claim-lint:ok: source counts -- 1 fn unblock definition and 34 .unblock(
call sites in kernel/src, 34 of 34 typed on &mut Scheduler.
No deadlock has been observed from it. It is filed because it is a real
structural property that the aarch64 testing-profile work (#562/#761) kept
running into while removing the other two locks on that path -- the softirq
handler's identity read and the KSOFTIRQD handle mutex, both of which are
gone. This one is left deliberately: the fix is per-CPU (or irqsave-typed)
runqueue locking, which is a scheduler change, not a testing-profile change.
What would close it
A wake that reaches only the woken thread's own CPU-local scheduling state,
under an interrupt-safe lock -- the #609 irqsave-typed lock work is the
nearest existing precedent in this tree.
The aarch64 IRQ-exit path takes the global
SCHEDULERspin lock whendo_softirqoverflows its iteration limit and wakes the local ksoftirqd.The path, with the lines it is made of
4 of the 4 lines are on
maintoday, and 0 of the 4 come from an openbranch (
git blameagainstorigin/main, commits below):mainkernel/src/arch_impl/aarch64/exception.rs:2212crate::task::softirqd::do_softirq();0668208d7, 2026-02-06kernel/src/task/softirqd.rs:220wakeup_ksoftirqd();(therestart_count >= MAX_SOFTIRQ_RESTARTarm)edf9186fe, 2026-01-21kernel/src/task/kthread.rs:227kernel/src/task/scheduler.rs:4609let mut scheduler_lock = lock_scheduler();insidewith_scheduleraf73c0d45, 2026-08-06exception.rs:2212runs afterirq_exit()on the aarch64 IRQ path -- 1 ofthe 1 post-
irq_exitdo_softirqcall site compiled on that arch. When thedispatch loop hits
MAX_SOFTIRQ_RESTARTwith work still pending it callswakeup_ksoftirqd(), which callskthread_unpark, which callswith_scheduler, which blocks inlock_scheduler()on the one globalSCHEDULERmutex. So an interrupt handler can spin on a lock a thread onanother CPU is holding.
Reproduce the shape by reading the four spans:
Linux, for contrast
Linux takes no global runqueue lock here.
wakeup_softirqd()is called from__do_softirq's overflow arm and fromirq_exit(), and it wakes the currentCPU's
ksoftirqdthroughwake_up_process, which takes that task's ownrunqueue lock with
raw_spin_lock_irqsave(rq_lock_irqsave/task_rq_lock). The lock is per-runqueue, not global, and it isinterrupt-safe by construction: an IRQ that arrives while a thread on the same
CPU holds it cannot deadlock, and a peer CPU's runqueue is a different lock.
claim-lint:ok: the contrast is a source reading of Linux's
kernel/softirq.c::wakeup_softirqdandkernel/sched/core.c::try_to_wake_up,not a measurement of this tree.
claim-lint:ok: the count is a source count --
static SCHEDULERhas 1declaration in kernel/src/task/scheduler.rs.
This kernel has one
SCHEDULERmutex shared by every CPU, and it is a barespin::Mutexwith no irqsave discipline of its own, so an IRQ-exit acquisitioncontends with every thread-context acquisition on every CPU.
Scope
An IRQ-exit wakeup in this kernel shares this property whichever thread it
wakes, not just the ksoftirqd one: a wake reached from
exception.rs'spost-
irq_exitwork ends inScheduler::unblock, which is a method on thelocked scheduler: it takes
&mut Scheduler, and that reference exists onlyinside the
lock_scheduler()critical section.claim-lint:ok: source counts -- 1
fn unblockdefinition and 34.unblock(call sites in kernel/src, 34 of 34 typed on
&mut Scheduler.No deadlock has been observed from it. It is filed because it is a real
structural property that the aarch64
testing-profile work (#562/#761) keptrunning into while removing the other two locks on that path -- the softirq
handler's identity read and the
KSOFTIRQDhandle mutex, both of which aregone. This one is left deliberately: the fix is per-CPU (or irqsave-typed)
runqueue locking, which is a scheduler change, not a testing-profile change.
What would close it
A wake that reaches only the woken thread's own CPU-local scheduling state,
under an interrupt-safe lock -- the
#609irqsave-typed lock work is thenearest existing precedent in this tree.