Re: [PATCH v2] irq_work: Fix use-after-free in irq_work_single on PREEMPT_RT

Jan Kiszka <[email protected]>
Newsgroups dev.linux.lists.linux-rt-devel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
On 30.03.26 09:32, Jiayuan Chen wrote:
> On PREEMPT_RT, non-HARD irq_work runs in per-CPU kthreads via
> run_irq_workd(), so irq_work_sync() uses rcuwait to wait for
> BUSY==0.
> 
> After irq_work_single() clears BUSY via atomic_cmpxchg(), it still
> dereferences @work for irq_work_is_hard() and rcuwait_wake_up().
> An irq_work_sync() caller on another CPU that enters after BUSY is
> cleared can observe BUSY==0 immediately, return, and free the work
> before those accesses complete — causing a use-after-free.
> 
> Fix this by wrapping run_irq_workd() in guard(rcu)() so that the
> entire irq_work_single() execution is within an RCU read-side
> critical section. Then add synchronize_rcu() in irq_work_sync()
> after rcuwait_wait_event() to ensure the caller waits for the RCU
> grace period before returning, preventing premature frees.
> 
> Fixes: 810979682ccc ("irq_work: Allow irq_work_sync() to sleep if irq_work() no IRQ support.")
> Suggested-by: Sebastian Andrzej Siewior <[email protected]>
> Suggested-by: Steven Rostedt <[email protected]>
> Signed-off-by: Jiayuan Chen <[email protected]>
> ---
>  kernel/irq_work.c | 7 +++++++
>  1 file changed, 7 insertions(+)
> 
> diff --git a/kernel/irq_work.c b/kernel/irq_work.c
> index 73f7e1fd4ab4..bf411656c316 100644
> --- a/kernel/irq_work.c
> +++ b/kernel/irq_work.c
> @@ -292,6 +292,12 @@ void irq_work_sync(struct irq_work *work)
>  	    !arch_irq_work_has_interrupt()) {
>  		rcuwait_wait_event(&work->irqwait, !irq_work_is_busy(work),
>  				   TASK_UNINTERRUPTIBLE);
> +		/*
> +		 * Ensure irq_work_single() does not access @work
> +		 * after removing IRQ_WORK_BUSY. It is always
> +		 * accessed within a RCU-read section.
> +		 */
> +		synchronize_rcu();
>  		return;
>  	}
>  
> @@ -302,6 +308,7 @@ EXPORT_SYMBOL_GPL(irq_work_sync);
>  
>  static void run_irq_workd(unsigned int cpu)
>  {
> +	guard(rcu)();
>  	irq_work_run_list(this_cpu_ptr(&lazy_list));
>  }
>  

With this patch, arm 32-bit, and a single-core SoC, I got a noticeable
slowdown of irq_work_sync. Reverting the patch resolves this. So I dug
deeper, found out that things were even worse before [1], but even with
that, I could still see up to 2 ticks delay per call. If you combine
that with an unfortunate loop of irq_work_sync calls (mine is
out-of-tree, but I see something even "worse" in bpf_mem_alloc_destroy),
there is this impact.

While this patch is motivated by PREEMPT_RT, the condition to enter the
modified branch are not limited to it:

... || !arch_irq_work_has_interrupt()

arch_irq_work_has_interrupt() is false on some archs, either always
(very rare) or under certain conditions. On arm, it's false when
is_smp() is false.

If we "only" need the synchronize_rcu() for PREEMPT_RT, should we limit
it to that configuration? Or do we actually need otherwise as well?

And what could be done to accelerate irq_work_sync loops? Practically, a
single synchronize_rcu() at the end could be enough, no?

Jan

[1]
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=78370df5c3574cdc5c6de7844481b4dc0ef4f172

-- 
Siemens AG, Foundational Technologies
Linux Expert Center
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.