On Thu, Jun 25, 2026 at 08:43:00PM -0400, Joel Fernandes wrote:
> Paul McKenney noted that a softirq (or irq_work) handler arming for a
> deferred QS can fire and find rcu_preempt_depth() > 0 -- the task is
> still inside its outer reader, so rcu_preempt_need_deferred_qs() bails
> without reporting the QS. At that point the queued mechanism has been
> consumed but ->defer_qs_pending stays in DEFER_QS_PENDING.
>
> In the meantime, the only remaining path back to a quiescent state on
> this CPU may be a local_irq_disable()/_enable() pair that does not
> call preempt_check_resched() (it is just `sti`/`cli`). patch 6's
> unconditional set_need_resched_current() makes need_resched true, but
> without an irq_work being raised the next outer rcu_read_unlock_special()
> hits the P-gate at the arming code:
>
> if (rdp->defer_qs_pending != DEFER_QS_PENDING) {
> rdp->defer_qs_pending = DEFER_QS_PENDING;
> irq_work_queue_on(...); // <-- skipped
> }
>
> so no irq_work is queued for the hardirq-exit preempt_schedule_irq()
> path either. The deferred QS now waits until the next timer tick (or
> similar preempt-safe boundary), needlessly extending expedited grace
> period latency.
>
> Clear ->defer_qs_pending in the bail-out path of rcu_preempt_deferred_qs()
> when rcu_preempt_depth() > 0. The recursion guard semantics introduced
> by commit b41642c87716 ("rcu: Fix rcu_read_unlock() deadloop due to IRQ
> work").
>
> The clear is also safe against fresh recursion at this exact program
> point: rcu_preempt_depth() > 0 guarantees we are still inside an outer
> reader, so any inner rcu_read_unlock() from tracing infrastructure
> brings nesting back to outer (>0), never to 0. The slow path of
> rcu_read_unlock_special() is structurally unreachable under that
> condition, so no recursive raise_softirq_irqoff()/irq_work_queue_on()
> can be triggered by the clear. Essentially, the mechanism will work to
> prevent the following recursion which Xiongfeng had previously reported:
>
> irq_exit() -> __irq_exit_rcu()
> -> tick_irq_exit() -> tick_nohz_irq_exit() -> tick_nohz_stop_sched_tick()
> -> trace_tick_stop() // BPF prog hooked here
> -> rcu_read_unlock_special()
> -> irq_work_queue_on(&rdp->defer_qs_iw, rdp->cpu) // self-IPI
> re-enters irq_exit
>
> Reported-by: Paul E. McKenney <[email protected]>
> Signed-off-by: Joel Fernandes <[email protected]>
I am a bit concerned about this one. We are clearing ->defer_qs_pending,
but there might still be something that will attempt to complete the
deferred quiescent state.
I am nevertheless tentatively pulling it in for further review and
testing.
Thanx, Paul
> ---
> kernel/rcu/tree_plugin.h | 28 +++++++++++++++++++++++++++-
> 1 file changed, 27 insertions(+), 1 deletion(-)
>
> diff --git a/kernel/rcu/tree_plugin.h b/kernel/rcu/tree_plugin.h
> index f58ae29acdef..6f5d31e3f1a3 100644
> --- a/kernel/rcu/tree_plugin.h
> +++ b/kernel/rcu/tree_plugin.h
> @@ -692,9 +692,35 @@ static notrace bool rcu_preempt_need_deferred_qs(struct
> task_struct *t)
> notrace void rcu_preempt_deferred_qs(struct task_struct *t)
> {
> unsigned long flags;
> + struct rcu_data *rdp;
>
> - if (!rcu_preempt_need_deferred_qs(t))
> + if (!rcu_preempt_need_deferred_qs(t)) {
> + /*
> + * If we got here from a softirq/irq_work that fired while
> + * rcu_preempt_depth() > 0, the deferred-QS mechanism has been
> + * consumed without doing any work:
> rcu_preempt_need_deferred_qs()
> + * just returned false because the task is still in a reader, so
> + * the actual QS report has to wait for the next
> + * rcu_read_unlock().
> + *
> + * Clear ->defer_qs_pending here so the next outer
> + * rcu_read_unlock_special() can re-arm a fresh mechanism (in
> + * particular the irq_work path, which the local_irq_enable()
> + * recovery boundary cannot itself reschedule from).
> + *
> + * Recursion safety: rcu_preempt_depth() > 0 means we are inside
> + * an outer reader, so any inner rcu_read_unlock() reached via
> + * tracing (bpf programs attached to trace points) brings
> + * nesting to outer (> 0), never to 0, so no recursive
> + * raise_softirq_irqoff()/irq_work_queue_on() can be triggered
> + * by this clear.
> + */
> + if (rcu_preempt_depth() > 0) {
> + rdp = this_cpu_ptr(&rcu_data);
> + rcu_defer_qs_clear(rdp);
> + }
> return;
> + }
> local_irq_save(flags);
> rcu_preempt_deferred_qs_irqrestore(t, flags);
> }
> --
> 2.34.1
>