linux-rt-devel.lists.linux.dev archive mirror
 help / color / mirror / Atom feed
* [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context
@ 2026-08-03 13:48 Puranjay Mohan
  2026-08-03 13:53 ` [PATCH v2 1/6] rcu: Make call_rcu() safe to call " Puranjay Mohan
                   ` (5 more replies)
  0 siblings, 6 replies; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 13:48 UTC (permalink / raw)
  To: Lai Jiangshan, Paul E. McKenney, Josh Triplett, Onur Özkan,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes, Boqun Feng,
	Uladzislau Rezki, Davidlohr Bueso, Andrii Nakryiko,
	Eduard Zingerman, Alexei Starovoitov, Daniel Borkmann,
	Kumar Kartikeya Dwivedi
  Cc: Puranjay Mohan, Steven Rostedt, Mathieu Desnoyers, Zqiang,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Matt Fleming, Harry Yoo (Oracle), linux-kernel,
	rcu, bpf, linux-rt-devel

call_rcu() and call_srcu() only ever touch their per-CPU callback lists
with interrupts disabled: the enqueue runs under local_irq_save() (and the
nocb locks when offloaded), and so do callback invocation and grace-period
work.  That is fine as long as call_rcu() itself is invoked with
interrupts enabled, but it is not always.  An NMI handler can call
call_rcu(), and instrumentation can reenter it.  The case that prompted
this is a BPF program attached to rcu_segcblist_enqueue() that frees an
object: the free reaches call_rcu_tasks_trace(), which is call_srcu()
under the hood, back on the same CPU with the srcu_data lock already held,
and it deadlocks on that lock.  Either way, enqueuing directly can corrupt
the list or deadlock.

Rather than scatter context checks through the enqueue, make it defer
whenever interrupts are disabled: stage the callback on a per-CPU lockless
list and re-issue it from an irq_work once interrupts are back on, going
straight to the enqueue helper so the re-issue cannot defer again.  Only
the drain side takes a lock; the staging is a bare llist_add() and stays
safe from NMI.  This is behind a new hidden CONFIG_RCU_DEFER, which is set
wherever a reentrant enqueue is possible (HAVE_NMI, KPROBES,
FUNCTION_TRACER or TRACEPOINTS); without it call_rcu() enqueues exactly as
before.

CPU offline is the awkward part.  A callback can be deferred very late in
the outgoing CPU's teardown -- from do_idle() or cpuhp_ap_report_dead(),
past the CPUHP_AP_SMPCFD_DYING flush that would otherwise run the irq_work
-- so the irq_work can no longer run there to re-issue it.  rcu_barrier()
and srcu_barrier() therefore drain the deferred lists themselves before
they wait: for online CPUs they wait the irq_work out, and for offline
ones they drain the list directly, since that irq_work may never run
again.  rcutree_migrate_callbacks() drains the outgoing CPU's list too, so
a late deferral still lands on a callback list even when nobody calls a
barrier.  To keep those three drainers from stepping on each other, the
drain holds a per-CPU raw lock across the llist_del_all() and the
re-issue, so a drainer never returns having pulled callbacks off the
deferred list but not yet put them on a callback list.  Every lock the
re-issue touches (nocb, rcu_node, srcu_data) is already raw, so the
nesting is fine.

The drain re-issues with interrupts disabled, so instrumentation on the
enqueue path can re-enter call_rcu()/call_srcu() from inside it, stage
another callback, re-raise the irq_work, and the drain never finishes.  A
per-CPU flag set across the re-issue catches that: a deferral that arrives
while this CPU is draining, and is not from an NMI, is dropped with a
warning instead of staged.  Dropping leaks that callback, but the
alternative is a CPU that never leaves the drain, and the producer is a
BPF program that emits one callback per enqueue, so there is nothing
finite to wait for.  Instrumenting the irq_work machinery itself can still
loop, as it can for any irq_work user, and is not something this series
can fix.

The irq_work is IRQ_WORK_INIT_HARD.  It is not needed for correctness, but
a non-HARD irq_work runs from a kthread on PREEMPT_RT and can be delayed
under load, letting deferred callbacks pile up; running the re-issue in
hard-irq context keeps that from turning into an OOM.

Patches 1 and 2 do Tree and Tiny RCU, 3 and 4 Tree and Tiny SRCU.  Patch 5
teaches rcutorture to issue ->call() from a perf-overflow NMI -- the
nmi_calls parameter, on by default -- on the flavors that advertise it,
and checks that every callback issued from NMI is later invoked.  Patch 6
adds the BPF reentry reproducer described above.

Changelog:
v1: https://lore.kernel.org/all/20260729162207.1567770-1-puranjay@kernel.org/
Changes in v2:
- Fixed the re-entry livelock Zqiang spotted: a BPF program on the enqueue
  path re-enters call_srcu() from inside srcu_defer_drain(), stages another
  callback and re-raises the irq_work, so the drain never finishes.  A
  per-CPU flag now drops such a deferral, with a warning, unless it comes
  from an NMI.
- cleanup_srcu_struct(): drain the deferred callbacks before syncing
  ->irq_work rather than after, since re-issuing one can start a grace
  period and re-queue that irq_work (Zqiang).
- Tiny SRCU: sync ->defer_iw in cleanup_srcu_struct() as well, so a
  deferred callback is re-issued onto ->srcu_cb_head where the leak checks
  can see it instead of being stranded on a soon-to-be-freed srcu_struct.
- Added Kumar's ack to the BPF selftest patch.

Puranjay Mohan (6):
  rcu: Make call_rcu() safe to call from any context
  rcu: Make Tiny call_rcu() safe to call from any context
  srcu: Make call_srcu() safe to call from any context
  srcu: Make Tiny call_srcu() safe to call from any context
  rcutorture: Exercise ->call() from NMI context
  selftests/bpf: Add a call_srcu() re-entry reproducer

 include/linux/srcutiny.h                      |  11 +-
 include/linux/srcutree.h                      |   4 +
 kernel/rcu/Kconfig                            |   6 +
 kernel/rcu/rcu.h                              |  17 ++
 kernel/rcu/rcutorture.c                       | 112 +++++++++++++
 kernel/rcu/srcutiny.c                         |  63 +++++++-
 kernel/rcu/srcutree.c                         | 150 +++++++++++++++++-
 kernel/rcu/tiny.c                             | 104 +++++++++---
 kernel/rcu/tree.c                             | 132 +++++++++++++--
 kernel/rcu/tree.h                             |   5 +
 .../selftests/bpf/prog_tests/rcu_reentry.c    |  58 +++++++
 .../testing/selftests/bpf/progs/rcu_reentry.c |  45 ++++++
 12 files changed, 666 insertions(+), 41 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/rcu_reentry.c
 create mode 100644 tools/testing/selftests/bpf/progs/rcu_reentry.c
--
2.53.0-Meta


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH v2 1/6] rcu: Make call_rcu() safe to call from any context
  2026-08-03 13:48 [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context Puranjay Mohan
@ 2026-08-03 13:53 ` Puranjay Mohan
  2026-08-03 14:18   ` sashiko-bot
  2026-08-03 13:53 ` [PATCH v2 2/6] rcu: Make Tiny " Puranjay Mohan
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 13:53 UTC (permalink / raw)
  To: Lai Jiangshan, Paul E. McKenney, Josh Triplett, Onur Özkan,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes, Boqun Feng,
	Uladzislau Rezki, Davidlohr Bueso, Andrii Nakryiko,
	Eduard Zingerman, Alexei Starovoitov, Daniel Borkmann,
	Kumar Kartikeya Dwivedi
  Cc: Puranjay Mohan, Steven Rostedt, Mathieu Desnoyers, Zqiang,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Matt Fleming, Harry Yoo (Oracle), linux-kernel,
	rcu, bpf, linux-rt-devel

RCU's per-CPU callback list is only touched with interrupts disabled:
the enqueue runs under local_irq_save() (and the nocb locks when
offloaded), as do callback invocation and grace-period work.  A
call_rcu() that arrives with interrupts already disabled, whether from an
NMI or from instrumentation that re-enters RCU, can interrupt one of
those and corrupt the list or deadlock.

Handle it by deferring: stage the callback on a per-CPU llist and raise
an irq_work that re-issues it once interrupts are on.  The re-issue goes
straight to the enqueue so it cannot defer again.  Callers that only hold
interrupts off are deferred too, which is harmless.  Skip the gate while
the scheduler is down (RCU_SCHEDULER_INACTIVE), since irq_work is not
usable that early and rcu_init() already calls call_rcu().

rcu_barrier() flushes deferred callbacks before it scans the lists: it
waits out each online CPU's irq_work and drains an offline CPU's list
directly, since that irq_work may never run again.
rcutree_migrate_callbacks() drains an outgoing CPU's ->defer_head for the
same reason.  ->defer_lock is held across llist_del_all() and the
re-issue so these drainers serialize.

The re-issue runs with interrupts disabled, so instrumentation on the
enqueue path can re-enter call_rcu(), stage another callback, and
re-raise the irq_work, livelocking the drain.  A per-CPU flag guards it:
a call_rcu() that tries to defer while the drain is running, and is not
from an NMI, is dropped with WARN_ONCE() instead of re-queued.  Dropping
leaks the callback, but the alternative is an unbounded loop.

The irq_work is IRQ_WORK_INIT_HARD so the re-issue stays prompt on
PREEMPT_RT, where a non-HARD irq_work runs in a kthread that can be
delayed under load.  A hidden CONFIG_RCU_DEFER gates the code and its
IRQ_WORK dependency; without it call_rcu() enqueues directly as before.
Under CONFIG_PROVE_RCU, warn if the direct path is reached from an NMI.

Suggested-by: Paul E. McKenney <paulmck@kernel.org>
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
 kernel/rcu/Kconfig |   6 +++
 kernel/rcu/rcu.h   |  14 +++++
 kernel/rcu/tree.c  | 130 +++++++++++++++++++++++++++++++++++++++++----
 kernel/rcu/tree.h  |   5 ++
 4 files changed, 145 insertions(+), 10 deletions(-)

diff --git a/kernel/rcu/Kconfig b/kernel/rcu/Kconfig
index f15da8038d0ba..1a5fb3156c062 100644
--- a/kernel/rcu/Kconfig
+++ b/kernel/rcu/Kconfig
@@ -175,6 +175,12 @@ config RCU_STALL_COMMON
 config RCU_NEED_SEGCBLIST
 	def_bool ( TREE_RCU || TREE_SRCU || TASKS_RCU_GENERIC )
 
+# The deferral (and the IRQ_WORK it uses) is only needed where call_rcu() /
+# call_srcu() can be invoked while a callback-list operation is in flight.
+config RCU_DEFER
+	def_bool HAVE_NMI || KPROBES || FUNCTION_TRACER || TRACEPOINTS
+	select IRQ_WORK
+
 config RCU_FANOUT
 	int "Tree-based hierarchical RCU fanout value"
 	range 2 64 if 64BIT
diff --git a/kernel/rcu/rcu.h b/kernel/rcu/rcu.h
index 39a9f6fa9a7b2..f8add8f8eae15 100644
--- a/kernel/rcu/rcu.h
+++ b/kernel/rcu/rcu.h
@@ -572,6 +572,20 @@ static inline void tasks_cblist_init_generic(void) { }
 #define RCU_SCHEDULER_INIT	1
 #define RCU_SCHEDULER_RUNNING	2
 
+/*
+ * Defer a call_rcu()/call_srcu() callback rather than enqueue it now?  Defer
+ * whenever interrupts are disabled, since a callback-list operation may be in
+ * flight on this CPU.  Not while the scheduler is down, though: irq_work is
+ * unusable before init_IRQ(), yet rcu_init() already calls call_rcu().
+ */
+static inline bool should_rcu_defer(void)
+{
+	if (!IS_ENABLED(CONFIG_RCU_DEFER))
+		return false;
+
+	return irqs_disabled() && rcu_scheduler_active != RCU_SCHEDULER_INACTIVE;
+}
+
 enum rcutorture_type {
 	RCU_FLAVOR,
 	RCU_TASKS_FLAVOR,
diff --git a/kernel/rcu/tree.c b/kernel/rcu/tree.c
index 21b6ce1dffb63..744a7cb60db4a 100644
--- a/kernel/rcu/tree.c
+++ b/kernel/rcu/tree.c
@@ -24,6 +24,7 @@
 #include <linux/smp.h>
 #include <linux/rcupdate_wait.h>
 #include <linux/interrupt.h>
+#include <linux/llist.h>
 #include <linux/sched.h>
 #include <linux/sched/debug.h>
 #include <linux/nmi.h>
@@ -3148,21 +3149,28 @@ static void check_cb_ovld(struct rcu_data *rdp)
 	raw_spin_unlock_rcu_node(rnp);
 }
 
-static void
-__call_rcu_common(struct rcu_head *head, rcu_callback_t func, bool lazy_in)
+/*
+ * The callback list is only accessed with interrupts disabled, so a call_rcu()
+ * that arrives with interrupts off (see should_rcu_defer()) stages the callback
+ * on a per-CPU llist that an irq_work re-issues once interrupts are on.
+ */
+static void rcu_defer_drain(struct irq_work *iw);
+
+/* Set while rcu_defer_drain() re-issues, to catch a re-entrant call_rcu(). */
+static DEFINE_PER_CPU(bool, rcu_defer_draining);
+
+/*
+ * Enqueue @head on this CPU's rcu_segcblist.  Also called by rcu_defer_drain()
+ * to re-issue a deferred callback, so it must not re-check the deferral
+ * condition.  Either caller may have interrupts already disabled.
+ */
+static void rcu_do_enqueue(struct rcu_head *head, rcu_callback_t func, bool lazy_in)
 {
 	static atomic_t doublefrees;
 	unsigned long flags;
 	bool lazy;
 	struct rcu_data *rdp;
 
-	/* Misaligned rcu_head! */
-	WARN_ON_ONCE((unsigned long)head & (sizeof(void *) - 1));
-
-	/* Avoid NULL dereference if callback is NULL. */
-	if (WARN_ON_ONCE(!func))
-		return;
-
 	if (debug_rcu_head_queue(head)) {
 		/*
 		 * Probable double call_rcu(), so leak the callback.
@@ -3206,6 +3214,92 @@ __call_rcu_common(struct rcu_head *head, rcu_callback_t func, bool lazy_in)
 	local_irq_restore(flags);
 }
 
+/*
+ * Re-issue deferred callbacks straight to the enqueue so they cannot defer
+ * again.  ->defer_lock serializes the drainers: this CPU's irq_work,
+ * rcu_defer_flush() and rcutree_migrate_callbacks().
+ */
+static void rcu_defer_drain(struct irq_work *iw)
+{
+	struct rcu_data *rdp = container_of(iw, struct rcu_data, defer_work);
+	struct llist_node *node, *next;
+	unsigned long flags;
+
+	raw_spin_lock_irqsave(&rdp->defer_lock, flags);
+	this_cpu_write(rcu_defer_draining, true);
+	llist_for_each_safe(node, next, llist_del_all(&rdp->defer_head)) {
+		struct rcu_head *head = (struct rcu_head *)node;
+
+		rcu_do_enqueue(head, head->func, false);
+	}
+	this_cpu_write(rcu_defer_draining, false);
+	raw_spin_unlock_irqrestore(&rdp->defer_lock, flags);
+}
+
+/* Stage @head for this CPU's irq_work when call_rcu() cannot enqueue now. */
+static void call_rcu_defer(struct rcu_head *head, rcu_callback_t func)
+{
+	struct rcu_data *rdp = this_cpu_ptr(&rcu_data);
+
+	/*
+	 * Instrumentation on the enqueue path can re-enter here from inside
+	 * rcu_defer_drain().  Re-queuing would livelock the drain, so drop the
+	 * callback; an NMI is one-shot and cannot loop, so let it through.
+	 */
+	if (this_cpu_read(rcu_defer_draining) && !in_nmi()) {
+		WARN_ONCE(1, "call_rcu() re-entered during callback drain; leaking callback\n");
+		return;
+	}
+	head->func = func;
+	if (llist_add((struct llist_node *)head, &rdp->defer_head))
+		irq_work_queue(&rdp->defer_work);
+}
+
+/*
+ * Register pending deferred callbacks into the callback lists so a following
+ * rcu_barrier() waits for them.  This runs before rcu_barrier() scans the
+ * lists.  An online CPU's own irq_work re-issues its callbacks, so wait it out;
+ * an offline CPU's irq_work may never run again, so drain its list directly
+ * onto this CPU instead.
+ */
+static void rcu_defer_flush(void)
+{
+	int cpu;
+
+	if (!IS_ENABLED(CONFIG_RCU_DEFER))
+		return;
+
+	for_each_possible_cpu(cpu) {
+		struct rcu_data *rdp = per_cpu_ptr(&rcu_data, cpu);
+
+		if (cpu_online(cpu))
+			irq_work_sync(&rdp->defer_work);
+		else
+			rcu_defer_drain(&rdp->defer_work);
+	}
+}
+
+static void
+__call_rcu_common(struct rcu_head *head, rcu_callback_t func, bool lazy_in)
+{
+	/* Misaligned rcu_head! */
+	WARN_ON_ONCE((unsigned long)head & (sizeof(void *) - 1));
+
+	/* Avoid NULL dereference if callback is NULL. */
+	if (WARN_ON_ONCE(!func))
+		return;
+
+	if (should_rcu_defer()) {
+		call_rcu_defer(head, func);
+		return;
+	}
+
+	/* An NMI reaching here entered with irqs enabled, so the enqueue can race. */
+	WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi());
+
+	rcu_do_enqueue(head, func, lazy_in);
+}
+
 #ifdef CONFIG_RCU_LAZY
 static bool enable_rcu_lazy __read_mostly = !IS_ENABLED(CONFIG_RCU_LAZY_DEFAULT_OFF);
 module_param(enable_rcu_lazy, bool, 0444);
@@ -3896,8 +3990,12 @@ void rcu_barrier(void)
 	unsigned long flags;
 	unsigned long gseq;
 	struct rcu_data *rdp;
-	unsigned long s = rcu_seq_snap(&rcu_state.barrier_sequence);
+	unsigned long s;
 
+	/* Register any deferred callbacks before snapshotting the sequence. */
+	rcu_defer_flush();
+
+	s = rcu_seq_snap(&rcu_state.barrier_sequence);
 	rcu_barrier_trace(TPS("Begin"), -1, s);
 
 	/* Take mutex to serialize concurrent rcu_barrier() requests. */
@@ -4231,6 +4329,10 @@ rcu_boot_init_percpu_data(int cpu)
 	rdp->rcu_onl_gp_state = RCU_GP_CLEANED;
 	rdp->last_sched_clock = jiffies;
 	rdp->cpu = cpu;
+	init_llist_head(&rdp->defer_head);
+	raw_spin_lock_init(&rdp->defer_lock);
+	/* Hard irq_work so the re-issue runs promptly. */
+	rdp->defer_work = IRQ_WORK_INIT_HARD(rcu_defer_drain);
 	rcu_boot_init_nocb_percpu_data(rdp);
 }
 
@@ -4528,6 +4630,14 @@ void rcutree_migrate_callbacks(int cpu)
 	struct rcu_data *rdp = per_cpu_ptr(&rcu_data, cpu);
 	bool needwake;
 
+	/*
+	 * Callbacks the outgoing CPU deferred late in the offline path (past the
+	 * point its irq_work can run) sit on ->defer_head, which the ->cblist
+	 * migration below does not cover.  Drain them here, before the early
+	 * returns; the re-issue lands on this CPU.
+	 */
+	rcu_defer_drain(&rdp->defer_work);
+
 	if (rcu_rdp_is_offloaded(rdp))
 		return;
 
diff --git a/kernel/rcu/tree.h b/kernel/rcu/tree.h
index eedfa43059e80..3a8e17136c5a7 100644
--- a/kernel/rcu/tree.h
+++ b/kernel/rcu/tree.h
@@ -229,6 +229,11 @@ struct rcu_data {
 	struct rcu_head barrier_head;
 	int exp_watching_snap;		/* Double-check need for IPI. */
 
+	/* Deferral of an NMI/reentrant call_rcu(); see __call_rcu_common(). */
+	struct llist_head defer_head;
+	struct irq_work defer_work;
+	raw_spinlock_t defer_lock;
+
 	/* 5) Callback offloading. */
 #ifdef CONFIG_RCU_NOCB_CPU
 	struct swait_queue_head nocb_cb_wq; /* For nocb kthreads to sleep on. */
-- 
2.53.0-Meta


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH v2 2/6] rcu: Make Tiny call_rcu() safe to call from any context
  2026-08-03 13:48 [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context Puranjay Mohan
  2026-08-03 13:53 ` [PATCH v2 1/6] rcu: Make call_rcu() safe to call " Puranjay Mohan
@ 2026-08-03 13:53 ` Puranjay Mohan
  2026-08-03 14:35   ` sashiko-bot
  2026-08-03 13:53 ` [PATCH v2 3/6] srcu: Make call_srcu() " Puranjay Mohan
                   ` (3 subsequent siblings)
  5 siblings, 1 reply; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 13:53 UTC (permalink / raw)
  To: Lai Jiangshan, Paul E. McKenney, Josh Triplett, Onur Özkan,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes, Boqun Feng,
	Uladzislau Rezki, Davidlohr Bueso, Andrii Nakryiko,
	Eduard Zingerman, Alexei Starovoitov, Daniel Borkmann,
	Kumar Kartikeya Dwivedi
  Cc: Puranjay Mohan, Steven Rostedt, Mathieu Desnoyers, Zqiang,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Matt Fleming, Harry Yoo (Oracle), linux-kernel,
	rcu, bpf, linux-rt-devel

Give Tiny call_rcu() the same treatment as Tree RCU.  When interrupts are
disabled and the scheduler is up, stage the callback on a lockless list
that an irq_work re-issues later.  One global list and irq_work suffice
since Tiny RCU is uniprocessor, and there is no CPU-offline drain.

The re-issue runs with interrupts disabled and can be re-entered by
instrumentation, so a draining flag drops a deferring call_rcu() seen
mid-drain (unless from an NMI), as in Tree RCU.  Gated by CONFIG_RCU_DEFER.

Suggested-by: Paul E. McKenney <paulmck@kernel.org>
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
 kernel/rcu/tiny.c | 104 +++++++++++++++++++++++++++++++++++++---------
 1 file changed, 85 insertions(+), 19 deletions(-)

diff --git a/kernel/rcu/tiny.c b/kernel/rcu/tiny.c
index dccccd6be9411..5736b964d8ee1 100644
--- a/kernel/rcu/tiny.c
+++ b/kernel/rcu/tiny.c
@@ -11,6 +11,8 @@
  */
 #include <linux/completion.h>
 #include <linux/interrupt.h>
+#include <linux/irq_work.h>
+#include <linux/llist.h>
 #include <linux/notifier.h>
 #include <linux/rcupdate_wait.h>
 #include <linux/kernel.h>
@@ -42,8 +44,86 @@ static struct rcu_ctrlblk rcu_ctrlblk = {
 	.gp_seq		= 0 - 300UL,
 };
 
+/*
+ * The callback list is only accessed with interrupts disabled, so a call_rcu()
+ * that arrives with interrupts off stages the callback on a lockless list that
+ * an irq_work re-issues later.  One global list and irq_work suffice, as Tiny
+ * RCU is uniprocessor.
+ */
+static void rcu_defer_drain(struct irq_work *iw);
+static LLIST_HEAD(rcu_defer_list);
+static DEFINE_IRQ_WORK(rcu_defer_iw, rcu_defer_drain);
+static bool rcu_defer_draining;
+
+/*
+ * Enqueue @head on the callback list.  Also called by rcu_defer_drain() to
+ * re-issue a deferred callback, so it must not re-check the deferral condition.
+ */
+static void rcu_do_enqueue(struct rcu_head *head, rcu_callback_t func)
+{
+	static atomic_t doublefrees;
+	unsigned long flags;
+
+	if (debug_rcu_head_queue(head)) {
+		if (atomic_inc_return(&doublefrees) < 4) {
+			pr_err("%s(): Double-freed CB %p->%pS()!!!  ", __func__, head, head->func);
+			mem_dump_obj(head);
+		}
+		return;
+	}
+
+	head->func = func;
+	head->next = NULL;
+
+	local_irq_save(flags);
+	*rcu_ctrlblk.curtail = head;
+	rcu_ctrlblk.curtail = &head->next;
+	local_irq_restore(flags);
+
+	if (unlikely(is_idle_task(current))) {
+		/* force scheduling for rcu_qs() */
+		resched_cpu(0);
+	}
+}
+
+static void rcu_defer_drain(struct irq_work *iw)
+{
+	struct llist_node *node, *next;
+
+	/* Callbacks are unordered, so drain in llist order without reversing. */
+	rcu_defer_draining = true;
+	llist_for_each_safe(node, next, llist_del_all(&rcu_defer_list)) {
+		struct rcu_head *head = (struct rcu_head *)node;
+
+		rcu_do_enqueue(head, head->func);
+	}
+	rcu_defer_draining = false;
+}
+
+static void call_rcu_defer(struct rcu_head *head, rcu_callback_t func)
+{
+	/* A re-entrant call_rcu() during the drain would livelock it; drop it. */
+	if (rcu_defer_draining && !in_nmi()) {
+		WARN_ONCE(1, "call_rcu() re-entered during callback drain; leaking callback\n");
+		return;
+	}
+	head->func = func;
+	if (llist_add((struct llist_node *)head, &rcu_defer_list))
+		irq_work_queue(&rcu_defer_iw);
+}
+
+/* Register any deferred callbacks so a following rcu_barrier() waits for them. */
+static void rcu_defer_flush(void)
+{
+	if (!IS_ENABLED(CONFIG_RCU_DEFER))
+		return;
+	irq_work_sync(&rcu_defer_iw);
+}
+
 void rcu_barrier(void)
 {
+	/* Register any deferred callbacks first. */
+	rcu_defer_flush();
 	wait_rcu_gp(call_rcu_hurry);
 }
 EXPORT_SYMBOL(rcu_barrier);
@@ -157,29 +237,15 @@ EXPORT_SYMBOL_GPL(synchronize_rcu);
  */
 void call_rcu(struct rcu_head *head, rcu_callback_t func)
 {
-	static atomic_t doublefrees;
-	unsigned long flags;
-
-	if (debug_rcu_head_queue(head)) {
-		if (atomic_inc_return(&doublefrees) < 4) {
-			pr_err("%s(): Double-freed CB %p->%pS()!!!  ", __func__, head, head->func);
-			mem_dump_obj(head);
-		}
+	if (should_rcu_defer()) {
+		call_rcu_defer(head, func);
 		return;
 	}
 
-	head->func = func;
-	head->next = NULL;
-
-	local_irq_save(flags);
-	*rcu_ctrlblk.curtail = head;
-	rcu_ctrlblk.curtail = &head->next;
-	local_irq_restore(flags);
+	/* An NMI reaching here entered with irqs enabled, so the enqueue can race. */
+	WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi());
 
-	if (unlikely(is_idle_task(current))) {
-		/* force scheduling for rcu_qs() */
-		resched_cpu(0);
-	}
+	rcu_do_enqueue(head, func);
 }
 EXPORT_SYMBOL_GPL(call_rcu);
 
-- 
2.53.0-Meta


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH v2 3/6] srcu: Make call_srcu() safe to call from any context
  2026-08-03 13:48 [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context Puranjay Mohan
  2026-08-03 13:53 ` [PATCH v2 1/6] rcu: Make call_rcu() safe to call " Puranjay Mohan
  2026-08-03 13:53 ` [PATCH v2 2/6] rcu: Make Tiny " Puranjay Mohan
@ 2026-08-03 13:53 ` Puranjay Mohan
  2026-08-03 14:49   ` sashiko-bot
  2026-08-03 13:53 ` [PATCH v2 4/6] srcu: Make Tiny " Puranjay Mohan
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 13:53 UTC (permalink / raw)
  To: Lai Jiangshan, Paul E. McKenney, Josh Triplett, Onur Özkan,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes, Boqun Feng,
	Uladzislau Rezki, Davidlohr Bueso, Andrii Nakryiko,
	Eduard Zingerman, Alexei Starovoitov, Daniel Borkmann,
	Kumar Kartikeya Dwivedi
  Cc: Puranjay Mohan, Steven Rostedt, Mathieu Desnoyers, Zqiang,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Matt Fleming, Harry Yoo (Oracle), linux-kernel,
	rcu, bpf, linux-rt-devel

call_srcu() has the same constraint as call_rcu(): its callback list and
locks are only touched with interrupts disabled.  srcu_gp_start_if_needed()
enqueues under raw_spin_lock_irqsave() and may walk the srcu_node tree, as
do callback invocation and grace-period work.  A call_srcu() with
interrupts disabled can race a list operation in flight on this CPU and
corrupt the list or deadlock.  call_rcu_tasks_trace() is call_srcu() under
the hood, so a sleepable BPF program freeing an object can reach this.

Defer as call_rcu() does: stage the callback on the srcu_data's
->defer_cbs, chain that srcu_data onto a per-CPU list, and raise a per-CPU
irq_work that re-issues it straight to the enqueue helper, never back
through __call_srcu().  The common path is unchanged and keeps interrupts
enabled across srcu_gp_start_if_needed().

The irq_work is per-CPU rather than per-srcu_struct and statically
initialized, so deferral never runs check_init_srcu_struct(); it is
IRQ_WORK_INIT_HARD as for call_rcu().  srcu_barrier() and
cleanup_srcu_struct() flush it first, and rcutree_migrate_callbacks()
calls srcu_offline_drain() for an outgoing CPU.  ->lock is held across the
drain so the drainers serialize.

As in call_rcu(), the re-issue runs with interrupts disabled and can be
re-entered by instrumentation, so a per-CPU flag drops a deferring
call_srcu() seen mid-drain (unless from an NMI).  Gated by
CONFIG_RCU_DEFER.  Under CONFIG_PROVE_RCU, warn if the direct path is
reached from an NMI.

Suggested-by: Paul E. McKenney <paulmck@kernel.org>
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
 include/linux/srcutree.h |   4 ++
 kernel/rcu/rcu.h         |   3 +
 kernel/rcu/srcutree.c    | 150 +++++++++++++++++++++++++++++++++++++--
 kernel/rcu/tree.c        |   2 +
 4 files changed, 155 insertions(+), 4 deletions(-)

diff --git a/include/linux/srcutree.h b/include/linux/srcutree.h
index 75e54e4f963fa..1ce759fb70948 100644
--- a/include/linux/srcutree.h
+++ b/include/linux/srcutree.h
@@ -13,6 +13,8 @@
 
 #include <linux/rcu_node_tree.h>
 #include <linux/completion.h>
+#include <linux/irq_work_types.h>
+#include <linux/llist.h>
 
 struct srcu_node;
 struct srcu_struct;
@@ -41,6 +43,8 @@ struct srcu_data {
 	bool srcu_cblist_invoking;		/* Invoking these CBs? */
 	struct timer_list delay_work;		/* Delay for CB invoking */
 	struct work_struct work;		/* Context for CB invoking. */
+	struct llist_head defer_cbs;		/* Callbacks deferred on re-entry. */
+	struct llist_node defer_link;		/* Links onto the per-CPU deferral drain list */
 	struct rcu_head srcu_barrier_head;	/* For srcu_barrier() use. */
 	struct rcu_head srcu_ec_head;		/* For srcu_expedite_current() use. */
 	int srcu_ec_state;			/*  State for srcu_expedite_current(). */
diff --git a/kernel/rcu/rcu.h b/kernel/rcu/rcu.h
index f8add8f8eae15..ca05d48773c79 100644
--- a/kernel/rcu/rcu.h
+++ b/kernel/rcu/rcu.h
@@ -586,6 +586,9 @@ static inline bool should_rcu_defer(void)
 	return irqs_disabled() && rcu_scheduler_active != RCU_SCHEDULER_INACTIVE;
 }
 
+/* Drain an outgoing CPU's deferred SRCU callbacks; see rcutree_migrate_callbacks(). */
+void srcu_offline_drain(int cpu);
+
 enum rcutorture_type {
 	RCU_FLAVOR,
 	RCU_TASKS_FLAVOR,
diff --git a/kernel/rcu/srcutree.c b/kernel/rcu/srcutree.c
index 304112674e8a2..2669594a6402f 100644
--- a/kernel/rcu/srcutree.c
+++ b/kernel/rcu/srcutree.c
@@ -20,6 +20,7 @@
 #include <linux/percpu.h>
 #include <linux/preempt.h>
 #include <linux/irq_work.h>
+#include <linux/llist.h>
 #include <linux/rcupdate_wait.h>
 #include <linux/sched.h>
 #include <linux/smp.h>
@@ -79,6 +80,47 @@ static void process_srcu(struct work_struct *work);
 static void srcu_irq_work(struct irq_work *work);
 static void srcu_delay_timer(struct timer_list *t);
 
+static void srcu_defer_drain(struct irq_work *iw);
+
+/*
+ * Per-CPU call_srcu() deferral state, shared by every srcu_struct.  A deferred
+ * callback is staged on its srcu_data's ->defer_cbs; that srcu_data is chained
+ * via ->defer_link onto ->list, which the irq_work walks.
+ */
+struct srcu_defer {
+	struct llist_head	list;
+	struct irq_work		iw;
+	raw_spinlock_t		lock;
+};
+
+static DEFINE_PER_CPU(struct srcu_defer, srcu_defer) = {
+	.lock = __RAW_SPIN_LOCK_UNLOCKED(srcu_defer.lock),
+	.iw = IRQ_WORK_INIT_HARD(srcu_defer_drain),
+};
+
+/* Set while srcu_defer_drain() re-issues, to catch a re-entrant call_srcu(). */
+static DEFINE_PER_CPU(bool, srcu_defer_draining);
+
+/*
+ * Flush pending deferred callbacks so a following srcu_barrier() waits for them.
+ * Wait out an online CPU's irq_work; drain an offline CPU's list directly, as
+ * its irq_work may never run again.
+ */
+static void srcu_defer_flush(void)
+{
+	int cpu;
+
+	if (!IS_ENABLED(CONFIG_RCU_DEFER))
+		return;
+
+	for_each_possible_cpu(cpu) {
+		if (cpu_online(cpu))
+			irq_work_sync(&per_cpu(srcu_defer, cpu).iw);
+		else
+			srcu_defer_drain(&per_cpu(srcu_defer, cpu).iw);
+	}
+}
+
 /*
  * Initialize SRCU per-CPU data.  Note that statically allocated
  * srcu_struct structures might already have srcu_read_lock() and
@@ -107,6 +149,11 @@ static void init_srcu_struct_data(struct srcu_struct *ssp)
 		sdp->cpu = cpu;
 		INIT_WORK(&sdp->work, srcu_invoke_callbacks);
 		timer_setup(&sdp->delay_work, srcu_delay_timer, 0);
+		/*
+		 * ->defer_cbs and ->defer_link are valid when zeroed and are not
+		 * reinitialized here, lest we clobber callbacks a reentrant
+		 * call_srcu() already staged.  See __call_srcu().
+		 */
 		sdp->ssp = ssp;
 	}
 }
@@ -695,7 +742,12 @@ void cleanup_srcu_struct(struct srcu_struct *ssp)
 		return; /* Just leak it! */
 	if (WARN_ON(srcu_readers_active(ssp)))
 		return; /* Just leak it! */
-	/* Wait for irq_work to finish first as it may queue a new work. */
+	/*
+	 * Drain deferred callbacks before syncing ->irq_work: re-issuing one can
+	 * start a grace period and re-queue ->irq_work, which then schedules
+	 * ->work, so both must be waited out after the drain.
+	 */
+	srcu_defer_flush();
 	irq_work_sync(&sup->irq_work);
 	flush_delayed_work(&sup->work);
 	for_each_possible_cpu(cpu) {
@@ -1410,8 +1462,15 @@ static unsigned long srcu_gp_start_if_needed(struct srcu_struct *ssp,
  * srcu_read_lock(), and srcu_read_unlock() that are all passed the same
  * srcu_struct structure.
  */
-static void __call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
-			rcu_callback_t func, bool do_norm)
+/*
+ * The srcu_cblist and srcu_node tree are only accessed with interrupts disabled
+ * (srcu_gp_start_if_needed() enqueues under raw_spin_lock_irqsave() and may walk
+ * the tree).  Like call_rcu(), __call_srcu() defers when interrupts are already
+ * disabled, so a re-entrant call_srcu() -- e.g. call_rcu_tasks_trace() from a
+ * BPF program -- cannot corrupt the list or deadlock.
+ */
+static void srcu_do_enqueue(struct srcu_struct *ssp, struct rcu_head *rhp,
+			    rcu_callback_t func, bool do_norm)
 {
 	if (debug_rcu_head_queue(rhp)) {
 		/* Probable double call_srcu(), so leak the callback. */
@@ -1423,6 +1482,81 @@ static void __call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
 	(void)srcu_gp_start_if_needed(ssp, rhp, do_norm);
 }
 
+static void __call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
+			rcu_callback_t func, bool do_norm)
+{
+	if (should_rcu_defer()) {
+		struct srcu_data *sdp;
+
+		/*
+		 * Instrumentation on the enqueue path can re-enter here from
+		 * inside srcu_defer_drain().  Re-queuing would livelock the
+		 * drain, so drop the callback; an NMI cannot loop, so let it in.
+		 */
+		if (this_cpu_read(srcu_defer_draining) && !in_nmi()) {
+			WARN_ONCE(1, "call_srcu() re-entered during callback drain; leaking callback\n");
+			return;
+		}
+		sdp = this_cpu_ptr(ssp->sda);
+		rhp->func = func;
+		if (llist_add((struct llist_node *)rhp, &sdp->defer_cbs)) {
+			/* First deferral on this srcu_data: chain it for the drain. */
+			struct srcu_defer *sndp = this_cpu_ptr(&srcu_defer);
+
+			sdp->ssp = ssp;
+			if (llist_add(&sdp->defer_link, &sndp->list))
+				irq_work_queue(&sndp->iw);
+		}
+		return;
+	}
+
+	/* An NMI reaching here entered with irqs enabled, so the enqueue can race. */
+	WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi());
+
+	srcu_do_enqueue(ssp, rhp, func, do_norm);
+}
+
+/*
+ * Re-issue deferred callbacks straight to srcu_do_enqueue() so they cannot defer
+ * again.  ->lock serializes the drainers: the irq_work, srcu_defer_flush() and
+ * srcu_offline_drain().
+ */
+static void srcu_defer_drain(struct irq_work *iw)
+{
+	struct srcu_defer *sndp = container_of(iw, struct srcu_defer, iw);
+	struct llist_node *snode, *snext;
+	unsigned long flags;
+
+	raw_spin_lock_irqsave(&sndp->lock, flags);
+	this_cpu_write(srcu_defer_draining, true);
+	llist_for_each_safe(snode, snext, llist_del_all(&sndp->list)) {
+		struct srcu_data *sdp = container_of(snode, struct srcu_data, defer_link);
+		struct srcu_struct *ssp = sdp->ssp;
+		struct llist_node *cnode, *cnext;
+
+		cnode = llist_del_all(&sdp->defer_cbs);
+		llist_for_each_safe(cnode, cnext, cnode) {
+			struct rcu_head *rhp = (struct rcu_head *)cnode;
+
+			srcu_do_enqueue(ssp, rhp, rhp->func, true);
+		}
+	}
+	this_cpu_write(srcu_defer_draining, false);
+	raw_spin_unlock_irqrestore(&sndp->lock, flags);
+}
+
+/*
+ * Drain @cpu's deferred call_srcu() callbacks from rcutree_migrate_callbacks()
+ * once @cpu is dead.  One pass covers every srcu_struct, and the re-issue lands
+ * on the current CPU.
+ */
+void srcu_offline_drain(int cpu)
+{
+	if (!IS_ENABLED(CONFIG_RCU_DEFER))
+		return;
+	srcu_defer_drain(&per_cpu(srcu_defer, cpu).iw);
+}
+
 /**
  * call_srcu() - Queue a callback for invocation after an SRCU grace period
  * @ssp: srcu_struct in queue the callback
@@ -1677,9 +1811,17 @@ void srcu_barrier(struct srcu_struct *ssp)
 {
 	int cpu;
 	int idx;
-	unsigned long s = rcu_seq_snap(&ssp->srcu_sup->srcu_barrier_seq);
+	unsigned long s;
 
 	check_init_srcu_struct(ssp);
+
+	/*
+	 * Register any deferred callbacks before snapshotting the sequence.  The
+	 * shared irq_work may also drain other srcu_structs', which is harmless.
+	 */
+	srcu_defer_flush();
+
+	s = rcu_seq_snap(&ssp->srcu_sup->srcu_barrier_seq);
 	mutex_lock(&ssp->srcu_sup->srcu_barrier_mutex);
 	if (rcu_seq_done(&ssp->srcu_sup->srcu_barrier_seq, s)) {
 		smp_mb(); /* Force ordering following return. */
diff --git a/kernel/rcu/tree.c b/kernel/rcu/tree.c
index 744a7cb60db4a..3f95c941ed977 100644
--- a/kernel/rcu/tree.c
+++ b/kernel/rcu/tree.c
@@ -4637,6 +4637,8 @@ void rcutree_migrate_callbacks(int cpu)
 	 * returns; the re-issue lands on this CPU.
 	 */
 	rcu_defer_drain(&rdp->defer_work);
+	/* Likewise for the outgoing CPU's deferred call_srcu() callbacks. */
+	srcu_offline_drain(cpu);
 
 	if (rcu_rdp_is_offloaded(rdp))
 		return;
-- 
2.53.0-Meta


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH v2 4/6] srcu: Make Tiny call_srcu() safe to call from any context
  2026-08-03 13:48 [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context Puranjay Mohan
                   ` (2 preceding siblings ...)
  2026-08-03 13:53 ` [PATCH v2 3/6] srcu: Make call_srcu() " Puranjay Mohan
@ 2026-08-03 13:53 ` Puranjay Mohan
  2026-08-03 13:53 ` [PATCH v2 5/6] rcutorture: Exercise ->call() from NMI context Puranjay Mohan
  2026-08-03 13:53 ` [PATCH v2 6/6] selftests/bpf: Add a call_srcu() re-entry reproducer Puranjay Mohan
  5 siblings, 0 replies; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 13:53 UTC (permalink / raw)
  To: Lai Jiangshan, Paul E. McKenney, Josh Triplett, Onur Özkan,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes, Boqun Feng,
	Uladzislau Rezki, Davidlohr Bueso, Andrii Nakryiko,
	Eduard Zingerman, Alexei Starovoitov, Daniel Borkmann,
	Kumar Kartikeya Dwivedi
  Cc: Puranjay Mohan, Steven Rostedt, Mathieu Desnoyers, Zqiang,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Matt Fleming, Harry Yoo (Oracle), linux-kernel,
	rcu, bpf, linux-rt-devel

Give Tiny call_srcu() the same treatment as Tree SRCU.  When interrupts
are disabled and the scheduler is up, stage the callback on the
srcu_struct's lockless list for an irq_work to re-issue later.  Tiny SRCU
is uniprocessor, so there is no CPU-offline drain.  A draining flag drops
a deferring call_srcu() that re-enters mid-drain (unless from an NMI), as
in Tree SRCU.

srcu_barrier() (now out of line) and cleanup_srcu_struct() sync the
irq_work before checking for outstanding callbacks, so a deferred callback
is re-issued onto the callback list, where the leak checks can see it,
rather than stranded on a soon-to-be-freed srcu_struct.

Gated by CONFIG_RCU_DEFER, like Tree SRCU.

Suggested-by: Paul E. McKenney <paulmck@kernel.org>
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
 include/linux/srcutiny.h | 11 ++++---
 kernel/rcu/srcutiny.c    | 63 +++++++++++++++++++++++++++++++++++++---
 2 files changed, 66 insertions(+), 8 deletions(-)

diff --git a/include/linux/srcutiny.h b/include/linux/srcutiny.h
index fbcf13bc12d15..47275d182966c 100644
--- a/include/linux/srcutiny.h
+++ b/include/linux/srcutiny.h
@@ -12,6 +12,7 @@
 #define _LINUX_SRCU_TINY_H
 
 #include <linux/irq_work_types.h>
+#include <linux/llist.h>
 #include <linux/swait.h>
 
 struct srcu_struct {
@@ -26,6 +27,8 @@ struct srcu_struct {
 	struct rcu_head **srcu_cb_tail;	/* Pending callbacks: Tail. */
 	struct work_struct srcu_work;	/* For driving grace periods. */
 	struct irq_work srcu_irq_work;	/* Defer schedule_work() to irq work. */
+	struct llist_head defer_cbs;	/* Callbacks deferred on re-entry. */
+	struct irq_work defer_iw;		/* Registers defer_cbs later. */
 #ifdef CONFIG_DEBUG_LOCK_ALLOC
 	struct lockdep_map dep_map;
 #endif /* #ifdef CONFIG_DEBUG_LOCK_ALLOC */
@@ -33,6 +36,7 @@ struct srcu_struct {
 
 void srcu_drive_gp(struct work_struct *wp);
 void srcu_tiny_irq_work(struct irq_work *irq_work);
+void srcu_defer_drain(struct irq_work *irq_work);
 
 #define __SRCU_STRUCT_INIT(name, __ignored, ___ignored, ____ignored)	\
 {									\
@@ -40,6 +44,8 @@ void srcu_tiny_irq_work(struct irq_work *irq_work);
 	.srcu_cb_tail = &name.srcu_cb_head,				\
 	.srcu_work = __WORK_INITIALIZER(name.srcu_work, srcu_drive_gp),	\
 	.srcu_irq_work = { .func = srcu_tiny_irq_work },		\
+	.defer_cbs = LLIST_HEAD_INIT(name.defer_cbs),			\
+	.defer_iw = { .func = srcu_defer_drain },				\
 	__SRCU_DEP_MAP_INIT(name)					\
 }
 
@@ -131,10 +137,7 @@ static inline void synchronize_srcu_expedited(struct srcu_struct *ssp)
 	synchronize_srcu(ssp);
 }
 
-static inline void srcu_barrier(struct srcu_struct *ssp)
-{
-	synchronize_srcu(ssp);
-}
+void srcu_barrier(struct srcu_struct *ssp);
 
 static inline void srcu_expedite_current(struct srcu_struct *ssp) { }
 #define srcu_check_read_flavor(ssp, read_flavor) do { } while (0)
diff --git a/kernel/rcu/srcutiny.c b/kernel/rcu/srcutiny.c
index f9c498ae75df2..988819e6ddd4b 100644
--- a/kernel/rcu/srcutiny.c
+++ b/kernel/rcu/srcutiny.c
@@ -10,6 +10,7 @@
 
 #include <linux/export.h>
 #include <linux/irq_work.h>
+#include <linux/llist.h>
 #include <linux/mutex.h>
 #include <linux/preempt.h>
 #include <linux/rcupdate_wait.h>
@@ -43,6 +44,8 @@ static int init_srcu_struct_fields(struct srcu_struct *ssp)
 	INIT_WORK(&ssp->srcu_work, srcu_drive_gp);
 	INIT_LIST_HEAD(&ssp->srcu_work.entry);
 	init_irq_work(&ssp->srcu_irq_work, srcu_tiny_irq_work);
+	init_llist_head(&ssp->defer_cbs);
+	init_irq_work(&ssp->defer_iw, srcu_defer_drain);
 	return 0;
 }
 
@@ -86,6 +89,9 @@ EXPORT_SYMBOL_GPL(init_srcu_struct_generic);
 void cleanup_srcu_struct(struct srcu_struct *ssp)
 {
 	WARN_ON(srcu_readers_active(ssp));
+	/* Re-issue any deferred callbacks so ->srcu_cb_head sees them below. */
+	if (IS_ENABLED(CONFIG_RCU_DEFER))
+		irq_work_sync(&ssp->defer_iw);
 	irq_work_sync(&ssp->srcu_irq_work);
 	flush_work(&ssp->srcu_work);
 	WARN_ON(ssp->srcu_gp_running);
@@ -215,11 +221,11 @@ static void srcu_gp_start_if_needed(struct srcu_struct *ssp)
 }
 
 /*
- * Enqueue an SRCU callback on the specified srcu_struct structure,
- * initiating grace-period processing if it is not already running.
+ * Enqueue @rhp on the callback list.  Also called by srcu_defer_drain() to
+ * re-issue a deferred callback, so it must not re-check the deferral condition.
  */
-void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
-	       rcu_callback_t func)
+static void srcu_do_enqueue(struct srcu_struct *ssp, struct rcu_head *rhp,
+			    rcu_callback_t func)
 {
 	unsigned long flags;
 
@@ -233,6 +239,46 @@ void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
 	srcu_gp_start_if_needed(ssp);
 	preempt_enable();
 }
+
+/* Set while srcu_defer_drain() re-issues, to catch a re-entrant call_srcu(). */
+static bool srcu_defer_draining;
+
+void srcu_defer_drain(struct irq_work *iw)
+{
+	struct srcu_struct *ssp = container_of(iw, struct srcu_struct, defer_iw);
+	struct llist_node *node, *next;
+
+	/* Callbacks are unordered, so drain in llist order without reversing. */
+	srcu_defer_draining = true;
+	llist_for_each_safe(node, next, llist_del_all(&ssp->defer_cbs)) {
+		struct rcu_head *rhp = (struct rcu_head *)node;
+
+		srcu_do_enqueue(ssp, rhp, rhp->func);
+	}
+	srcu_defer_draining = false;
+}
+EXPORT_SYMBOL_GPL(srcu_defer_drain);
+
+void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
+	       rcu_callback_t func)
+{
+	if (should_rcu_defer()) {
+		/* A re-entrant call_srcu() during the drain would livelock it. */
+		if (srcu_defer_draining && !in_nmi()) {
+			WARN_ONCE(1, "call_srcu() re-entered during callback drain; leaking callback\n");
+			return;
+		}
+		rhp->func = func;
+		if (llist_add((struct llist_node *)rhp, &ssp->defer_cbs))
+			irq_work_queue(&ssp->defer_iw);
+		return;
+	}
+
+	/* An NMI reaching here entered with irqs enabled, so the enqueue can race. */
+	WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi());
+
+	srcu_do_enqueue(ssp, rhp, func);
+}
 EXPORT_SYMBOL_GPL(call_srcu);
 
 /*
@@ -262,6 +308,15 @@ void synchronize_srcu(struct srcu_struct *ssp)
 }
 EXPORT_SYMBOL_GPL(synchronize_srcu);
 
+/* Register any deferred callbacks, then wait for all in-flight ones. */
+void srcu_barrier(struct srcu_struct *ssp)
+{
+	if (IS_ENABLED(CONFIG_RCU_DEFER))
+		irq_work_sync(&ssp->defer_iw);
+	synchronize_srcu(ssp);
+}
+EXPORT_SYMBOL_GPL(srcu_barrier);
+
 /*
  * get_state_synchronize_srcu - Provide an end-of-grace-period cookie
  */
-- 
2.53.0-Meta


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH v2 5/6] rcutorture: Exercise ->call() from NMI context
  2026-08-03 13:48 [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context Puranjay Mohan
                   ` (3 preceding siblings ...)
  2026-08-03 13:53 ` [PATCH v2 4/6] srcu: Make Tiny " Puranjay Mohan
@ 2026-08-03 13:53 ` Puranjay Mohan
  2026-08-03 13:53 ` [PATCH v2 6/6] selftests/bpf: Add a call_srcu() re-entry reproducer Puranjay Mohan
  5 siblings, 0 replies; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 13:53 UTC (permalink / raw)
  To: Lai Jiangshan, Paul E. McKenney, Josh Triplett, Onur Özkan,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes, Boqun Feng,
	Uladzislau Rezki, Davidlohr Bueso, Andrii Nakryiko,
	Eduard Zingerman, Alexei Starovoitov, Daniel Borkmann,
	Kumar Kartikeya Dwivedi
  Cc: Puranjay Mohan, Steven Rostedt, Mathieu Desnoyers, Zqiang,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Matt Fleming, Harry Yoo (Oracle), linux-kernel,
	rcu, bpf, linux-rt-devel

call_rcu() and call_srcu() are now safe to invoke from NMI, but
rcutorture never does, leaving the deferral path untested.

Add an ->nmi_capable flag to rcu_torture_ops.  For flavors that set it,
arm a per-CPU hardware perf counter whose overflow handler submits a
callback via ->call().  The handler acts only when in_nmi(), so only a
genuine NMI exercises the deferral path.  One preallocated callback is
kept in flight (guarded by an atomic) to avoid allocating in NMI.

Report the count issued from NMI ("nmi-calls:") and the count invoked
("nmi-cbs:").  rcu_torture_cleanup() disables the counters and then calls
cb_barrier(), which drains every deferred callback, so the two counts must
then match; a mismatch means a lost callback and fails the test.  This
relies on srcu_barrier()/rcu_barrier() flushing deferred callbacks, as
added earlier in the series.

Set ->nmi_capable on the NMI-safe flavors: rcu, srcu, srcud, and
tasks-tracing (call_srcu() under the hood).  Tasks and Tasks Rude are left
alone, as call_rcu_tasks_generic() is not yet NMI-safe.

Enabled by default; the nmi_calls parameter disables it, which helps rule
NMI handling in or out when triaging a failure.  Requires
CONFIG_PERF_EVENTS and a hardware PMU, else silently skipped.

Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
 kernel/rcu/rcutorture.c | 112 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 112 insertions(+)

diff --git a/kernel/rcu/rcutorture.c b/kernel/rcu/rcutorture.c
index 39426a8718fe9..ae3bbe34809e1 100644
--- a/kernel/rcu/rcutorture.c
+++ b/kernel/rcu/rcutorture.c
@@ -48,6 +48,7 @@
 #include <linux/tick.h>
 #include <linux/rcupdate_trace.h>
 #include <linux/nmi.h>
+#include <linux/perf_event.h>
 
 #include "rcu.h"
 
@@ -115,6 +116,7 @@ torture_param(int, leakpointer, 0, "Leak pointer dereferences from readers");
 torture_param(int, n_barrier_cbs, 0, "# of callbacks/kthreads for barrier testing");
 torture_param(int, n_up_down, 32, "# of concurrent up/down hrtimer-based RCU readers");
 torture_param(int, nfakewriters, 4, "Number of RCU fake writer threads");
+torture_param(bool, nmi_calls, true, "Exercise ->call() from NMI on nmi_capable flavors");
 torture_param(int, nreaders, -1, "Number of RCU reader threads");
 torture_param(bool, nwriters, 1, "Number of RCU writer threads (0 or 1)");
 torture_param(int, object_debug, 0, "Enable debug-object double call_rcu() testing");
@@ -216,6 +218,8 @@ static long n_rcu_torture_boost_failure;
 static long n_rcu_torture_boosts;
 static atomic_long_t n_rcu_torture_timers;
 static atomic_long_t n_rcu_torture_irqs;
+static atomic_long_t n_rcu_torture_nmi_call;
+static atomic_long_t n_rcu_torture_nmi_cb;
 static long n_barrier_attempts;
 static long n_barrier_successes; /* did rcu_barrier test succeed? */
 static unsigned long n_read_exits;
@@ -433,6 +437,7 @@ struct rcu_torture_ops {
 	bool (*is_task_rcu_boosted)(void);
 	long cbflood_max;
 	int irq_capable;
+	int nmi_capable;
 	int can_boost;
 	int extendables;
 	int slow_gps;
@@ -648,6 +653,7 @@ static struct rcu_torture_ops rcu_ops = {
 	.extendables		= RCUTORTURE_MAX_EXTEND,
 	.debug_objects		= 1,
 	.start_poll_irqsoff	= 1,
+	.nmi_capable		= 1,
 	.name			= "rcu"
 };
 
@@ -942,6 +948,7 @@ static struct rcu_torture_ops srcu_ops = {
 	.debug_objects	= 1,
 	.have_up_down	= IS_ENABLED(CONFIG_TINY_SRCU)
 				? 0 : SRCU_READ_FLAVOR_NORMAL | SRCU_READ_FLAVOR_FAST_UPDOWN,
+	.nmi_capable	= 1,
 	.name		= "srcu"
 };
 
@@ -1005,6 +1012,7 @@ static struct rcu_torture_ops srcud_ops = {
 	.debug_objects	= 1,
 	.have_up_down	= IS_ENABLED(CONFIG_TINY_SRCU)
 				? 0 : SRCU_READ_FLAVOR_NORMAL | SRCU_READ_FLAVOR_FAST_UPDOWN,
+	.nmi_capable	= 1,
 	.name		= "srcud"
 };
 
@@ -1269,6 +1277,7 @@ static struct rcu_torture_ops tasks_tracing_ops = {
 	.cbflood_max	= 50000,
 	.irq_capable	= 1,
 	.slow_gps	= 1,
+	.nmi_capable	= 1,
 	.name		= "tasks-tracing"
 };
 
@@ -2659,12 +2668,94 @@ static bool rcu_torture_one_read(struct torture_random_state *trsp, long myid)
 
 static DEFINE_TORTURE_RANDOM_PERCPU(rcu_torture_timer_rand);
 
+/*
+ * Exercise ->call() from NMI context for flavors that set ->nmi_capable.  A
+ * per-CPU hardware perf counter overflows into an NMI, and its handler submits
+ * one preallocated callback via ->call().  One callback is in flight at a time
+ * (guarded by an atomic) to avoid allocating in NMI.  This mirrors how a BPF
+ * program reaches ->call() from NMI.
+ */
+#ifdef CONFIG_PERF_EVENTS
+static struct perf_event_attr rcu_torture_nmi_attr = {
+	.type		= PERF_TYPE_HARDWARE,
+	.config		= PERF_COUNT_HW_CPU_CYCLES,
+	.size		= sizeof(struct perf_event_attr),
+	.pinned		= 1,
+	.disabled	= 1,
+	.freq		= 1,
+	.sample_freq	= 1000,
+};
+
+static struct perf_event **rcu_torture_nmi_events;
+static struct rcu_head rcu_torture_nmi_rh;
+static atomic_t rcu_torture_nmi_rh_inuse;
+
+static void rcu_torture_nmi_cb(struct rcu_head *rhp)
+{
+	atomic_long_inc(&n_rcu_torture_nmi_cb);
+	atomic_set(&rcu_torture_nmi_rh_inuse, 0);
+}
+
+static void rcu_torture_nmi_overflow(struct perf_event *event,
+				     struct perf_sample_data *data,
+				     struct pt_regs *regs)
+{
+	if (!in_nmi())
+		return;
+	if (cur_ops->call && !atomic_xchg(&rcu_torture_nmi_rh_inuse, 1)) {
+		cur_ops->call(&rcu_torture_nmi_rh, rcu_torture_nmi_cb);
+		atomic_long_inc(&n_rcu_torture_nmi_call);
+	}
+}
+
+static void rcu_torture_nmi_init(void)
+{
+	struct perf_event *event;
+	int cpu;
+
+	if (!nmi_calls || !cur_ops->nmi_capable || !cur_ops->call)
+		return;
+	rcu_torture_nmi_events = kcalloc(nr_cpu_ids, sizeof(*rcu_torture_nmi_events),
+					 GFP_KERNEL);
+	if (!rcu_torture_nmi_events)
+		return;
+	for_each_online_cpu(cpu) {
+		event = perf_event_create_kernel_counter(&rcu_torture_nmi_attr, cpu,
+							 NULL, rcu_torture_nmi_overflow, NULL);
+		if (IS_ERR(event))
+			continue;
+		rcu_torture_nmi_events[cpu] = event;
+		perf_event_enable(event);
+	}
+}
+
+static void rcu_torture_nmi_cleanup(void)
+{
+	int cpu;
+
+	if (!rcu_torture_nmi_events)
+		return;
+	for_each_possible_cpu(cpu) {
+		if (!rcu_torture_nmi_events[cpu])
+			continue;
+		perf_event_disable(rcu_torture_nmi_events[cpu]);
+		perf_event_release_kernel(rcu_torture_nmi_events[cpu]);
+	}
+	kfree(rcu_torture_nmi_events);
+	rcu_torture_nmi_events = NULL;
+}
+#else /* #ifdef CONFIG_PERF_EVENTS */
+static void rcu_torture_nmi_init(void) { }
+static void rcu_torture_nmi_cleanup(void) { }
+#endif /* #else #ifdef CONFIG_PERF_EVENTS */
+
 /*
  * RCU torture reader from timer handler.  Dereferences rcu_torture_current,
  * incrementing the corresponding element of the pipeline array.  The
  * counter in the element should never be greater than 1, otherwise, the
  * RCU implementation is broken.
  */
+
 static void rcu_torture_timer(struct timer_list *unused)
 {
 	WARN_ON_ONCE(!in_serving_softirq());
@@ -3047,6 +3138,9 @@ rcu_torture_stats_print(void)
 		data_race(n_barrier_attempts),
 		data_race(n_rcu_torture_barrier_error));
 	pr_cont("read-exits: %ld ", data_race(n_read_exits)); // Statistic.
+	pr_cont("nmi-calls: %ld nmi-cbs: %ld ",
+		atomic_long_read(&n_rcu_torture_nmi_call),
+		atomic_long_read(&n_rcu_torture_nmi_cb));
 	pr_cont("nocb-toggles: %ld:%ld ",
 		atomic_long_read(&n_nocb_offload), atomic_long_read(&n_nocb_deoffload));
 	pr_cont("gpwraps: %ld\n", n_gpwraps);
@@ -4325,6 +4419,8 @@ rcu_torture_cleanup(void)
 		kfree(reader_tasks);
 		reader_tasks = NULL;
 	}
+	/* Disable the perf counters (and thus the NMI ->call() firing) now. */
+	rcu_torture_nmi_cleanup();
 	kfree(rcu_torture_reader_mbchk);
 	rcu_torture_reader_mbchk = NULL;
 
@@ -4354,6 +4450,20 @@ rcu_torture_cleanup(void)
 		pr_info("%s: Invoking %pS().\n", __func__, cur_ops->cb_barrier);
 		cur_ops->cb_barrier();
 	}
+
+	/*
+	 * cb_barrier() above drained every deferred NMI ->call() callback, so the
+	 * count issued from NMI must equal the count invoked; a mismatch means a
+	 * callback was lost.
+	 */
+	if (atomic_long_read(&n_rcu_torture_nmi_call) !=
+	    atomic_long_read(&n_rcu_torture_nmi_cb)) {
+		pr_alert("%s: NMI ->call() lost a callback: issued %ld invoked %ld\n",
+			 __func__, atomic_long_read(&n_rcu_torture_nmi_call),
+			 atomic_long_read(&n_rcu_torture_nmi_cb));
+		atomic_inc(&n_rcu_torture_error);
+	}
+
 	if (cur_ops->cleanup != NULL)
 		cur_ops->cleanup();
 
@@ -4786,6 +4896,8 @@ rcu_torture_init(void)
 		firsterr = -ENOMEM;
 		goto unwind;
 	}
+	/* Arm the per-CPU perf counters that drive ->call() from NMI. */
+	rcu_torture_nmi_init();
 	for (i = 0; i < nrealreaders; i++) {
 		rcu_torture_reader_mbchk[i].rtc_chkrdr = -1;
 		firsterr = torture_create_kthread(rcu_torture_reader, (void *)i,
-- 
2.53.0-Meta


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH v2 6/6] selftests/bpf: Add a call_srcu() re-entry reproducer
  2026-08-03 13:48 [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context Puranjay Mohan
                   ` (4 preceding siblings ...)
  2026-08-03 13:53 ` [PATCH v2 5/6] rcutorture: Exercise ->call() from NMI context Puranjay Mohan
@ 2026-08-03 13:53 ` Puranjay Mohan
  2026-08-03 15:15   ` sashiko-bot
  5 siblings, 1 reply; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 13:53 UTC (permalink / raw)
  To: Lai Jiangshan, Paul E. McKenney, Josh Triplett, Onur Özkan,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes, Boqun Feng,
	Uladzislau Rezki, Davidlohr Bueso, Andrii Nakryiko,
	Eduard Zingerman, Alexei Starovoitov, Daniel Borkmann,
	Kumar Kartikeya Dwivedi
  Cc: Puranjay Mohan, Steven Rostedt, Mathieu Desnoyers, Zqiang,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Matt Fleming, Harry Yoo (Oracle), linux-kernel,
	rcu, bpf, linux-rt-devel

Re-enter call_srcu() from a BPF program to exercise its any-context
safety (and thus call_rcu_tasks_trace(), which is call_srcu() on
rcu_tasks_trace_srcu_struct).

An fentry program on rcu_segcblist_enqueue() fires mid-enqueue: that
function is reached from srcu_gp_start_if_needed() with the srcu_data
->lock held.  The program does a task-storage delete, whose only deferred
work is call_rcu_tasks_trace(), re-entering the enqueue on the same CPU.
The triggering thread is pinned to one CPU and matched by TID, so the
program fires only for the test's own delete.

Without the fix the nested call re-takes the same sdp lock and
self-deadlocks; with it the nested __call_srcu() sees interrupts disabled
and defers via irq_work, so the delete returns and the test passes.  Since
it can hang an unfixed kernel, run it only against a kernel carrying the
fix.

Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Acked-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 .../selftests/bpf/prog_tests/rcu_reentry.c    | 58 +++++++++++++++++++
 .../testing/selftests/bpf/progs/rcu_reentry.c | 45 ++++++++++++++
 2 files changed, 103 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/rcu_reentry.c
 create mode 100644 tools/testing/selftests/bpf/progs/rcu_reentry.c

diff --git a/tools/testing/selftests/bpf/prog_tests/rcu_reentry.c b/tools/testing/selftests/bpf/prog_tests/rcu_reentry.c
new file mode 100644
index 0000000000000..f6ecd93be30f4
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/rcu_reentry.c
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Exercise re-entry into call_srcu() from BPF; see progs/rcu_reentry.c. */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <sys/syscall.h>
+#include <test_progs.h>
+#include "rcu_reentry.skel.h"
+
+static int sys_pidfd_open(pid_t pid, unsigned int flags)
+{
+	return syscall(__NR_pidfd_open, pid, flags);
+}
+
+void test_rcu_reentry(void)
+{
+	struct rcu_reentry *skel;
+	int err, pidfd = -1, map_fd;
+	__u64 val = 1;
+	cpu_set_t set;
+
+	skel = rcu_reentry__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
+		return;
+
+	err = rcu_reentry__attach(skel);
+	if (!ASSERT_OK(err, "skel_attach"))
+		goto out;
+
+	/* Keep the re-entry on a single CPU. */
+	CPU_ZERO(&set);
+	CPU_SET(0, &set);
+	if (sched_setaffinity(0, sizeof(set), &set))
+		perror("sched_setaffinity");
+
+	pidfd = sys_pidfd_open(getpid(), 0);
+	if (!ASSERT_GE(pidfd, 0, "pidfd_open"))
+		goto out;
+	map_fd = bpf_map__fd(skel->maps.task_stg);
+	err = bpf_map_update_elem(map_fd, &pidfd, &val, BPF_NOEXIST);
+	if (!ASSERT_OK(err, "boot_create"))
+		goto out;
+
+	/* Arm the handler for this thread, then trigger call_rcu_tasks_trace(). */
+	skel->bss->target_pid = syscall(__NR_gettid);
+	err = bpf_map_delete_elem(map_fd, &pidfd);
+	ASSERT_OK(err, "boot_delete");
+
+	/* Only Tree SRCU enqueues via rcu_segcblist_enqueue(); skip elsewhere. */
+	if (!skel->bss->hits) {
+		test__skip();
+		goto out;
+	}
+	ASSERT_EQ(skel->bss->reentered, 1, "reentry_deferred");
+out:
+	if (pidfd >= 0)
+		close(pidfd);
+	rcu_reentry__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/rcu_reentry.c b/tools/testing/selftests/bpf/progs/rcu_reentry.c
new file mode 100644
index 0000000000000..d92a927ff51c0
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/rcu_reentry.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Re-enter call_srcu() from a BPF program.  fentry on rcu_segcblist_enqueue()
+ * fires inside call_srcu()'s enqueue (reached from srcu_gp_start_if_needed()
+ * with the srcu_data ->lock held); the handler then calls call_rcu_tasks_trace()
+ * -- itself call_srcu() on rcu_tasks_trace_srcu_struct -- re-entering the same
+ * srcu_data on the same CPU.
+ */
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+	__uint(type, BPF_MAP_TYPE_TASK_STORAGE);
+	__uint(map_flags, BPF_F_NO_PREALLOC);
+	__type(key, int);
+	__type(value, __u64);
+} task_stg SEC(".maps");
+
+int target_pid;
+int hits;
+int reentered;
+
+SEC("fentry/rcu_segcblist_enqueue")
+int BPF_PROG(reenter)
+{
+	struct task_struct *cur;
+
+	if (reentered || !target_pid)
+		return 0;
+
+	cur = bpf_get_current_task_btf();
+	if (!cur || cur->pid != target_pid)
+		return 0;
+
+	/* Re-enter via a task-storage delete, which calls call_rcu_tasks_trace(). */
+	__sync_fetch_and_add(&hits, 1);
+	bpf_task_storage_get(&task_stg, cur, 0, BPF_LOCAL_STORAGE_GET_F_CREATE);
+	bpf_task_storage_delete(&task_stg, cur);
+
+	reentered = 1;
+	return 0;
+}
-- 
2.53.0-Meta


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 1/6] rcu: Make call_rcu() safe to call from any context
  2026-08-03 13:53 ` [PATCH v2 1/6] rcu: Make call_rcu() safe to call " Puranjay Mohan
@ 2026-08-03 14:18   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 14:18 UTC (permalink / raw)
  To: Puranjay Mohan; +Cc: bpf, linux-kernel, linux-rt-devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Concurrent `rcu_barrier()` calls can cause missed wakeups and system hangs in `irq_work_sync()` on architectures without IPI-based `irq_work`.
--

--- Patch [1]: [PATCH v2 1/6] rcu: Make call_rcu() safe to call from any context ---
commit f033f4bd0a671eccd668878bff133f083ff63d97
Author: Puranjay Mohan <puranjay@kernel.org>

rcu: Make call_rcu() safe to call from any context

This commit makes call_rcu() safe to call from any context by deferring
the callback on a per-CPU llist when interrupts are disabled, and raising
an irq_work to re-issue it once interrupts are enabled. It introduces
rcu_defer_flush() to flush these deferred callbacks during rcu_barrier().

> diff --git a/kernel/rcu/tree.c b/kernel/rcu/tree.c
> index 96848fc1f02b8..31e2d2207ac03 100644
> --- a/kernel/rcu/tree.c
> +++ b/kernel/rcu/tree.c

[ ... ]

> @@ -3896,8 +3990,12 @@ void rcu_barrier(void)
>  	unsigned long flags;
>  	unsigned long gseq;
>  	struct rcu_data *rdp;
> -	unsigned long s = rcu_seq_snap(&rcu_state.barrier_sequence);
> +	unsigned long s;
>  
> +	/* Register any deferred callbacks before snapshotting the sequence. */
> +	rcu_defer_flush();
> +

[Severity: High]
Because rcu_defer_flush() is called before taking the rcu_state.barrier_mutex,
can multiple threads execute it concurrently?

If multiple threads call rcu_defer_flush() concurrently, they will both
call irq_work_sync(&rdp->defer_work) on the same per-CPU irq_work:

kernel/rcu/tree.c:rcu_defer_flush() {
...
    if (cpu_online(cpu))
        irq_work_sync(&rdp->defer_work);
...
}

On architectures where arch_irq_work_has_interrupt() is false,
irq_work_sync() waits using rcuwait_wait_event(). Since rcuwait only
supports a single waiter, won't concurrent callers overwrite the internal
task pointer, causing one of the threads to miss the wakeup and hang
indefinitely?

> +	s = rcu_seq_snap(&rcu_state.barrier_sequence);
>  	rcu_barrier_trace(TPS("Begin"), -1, s);
>  
>  	/* Take mutex to serialize concurrent rcu_barrier() requests. */
>  	mutex_lock(&rcu_state.barrier_mutex);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260803134839.2103051-1-puranjay@kernel.org?part=1

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 2/6] rcu: Make Tiny call_rcu() safe to call from any context
  2026-08-03 13:53 ` [PATCH v2 2/6] rcu: Make Tiny " Puranjay Mohan
@ 2026-08-03 14:35   ` sashiko-bot
  2026-08-03 14:40     ` Puranjay Mohan
  0 siblings, 1 reply; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 14:35 UTC (permalink / raw)
  To: Puranjay Mohan; +Cc: bpf, linux-rt-devel, linux-kernel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Relocating `debug_rcu_head_queue()` into `rcu_do_enqueue()` bypasses double-free detection for deferred callbacks, silently corrupting the lockless list and inducing a permanent hard livelock during `rcu_defer_drain()`.
- [Medium] Invalid `WARN_ON_ONCE` in `call_rcu()` falsely flags safe NMI executions due to a flawed concurrency assumption.
--

--- Patch [2]: [PATCH v2 2/6] rcu: Make Tiny call_rcu() safe to call from any context ---
commit 866e1596ae5a7314657b9b0164dfabf922016c70
Author: Puranjay Mohan <puranjay@kernel.org>

rcu: Make Tiny call_rcu() safe to call from any context

Give Tiny call_rcu() the same treatment as Tree RCU.  When interrupts are
disabled and the scheduler is up, stage the callback on a lockless list
that an irq_work re-issues later.  One global list and irq_work suffice
since Tiny RCU is uniprocessor, and there is no CPU-offline drain.

> diff --git a/kernel/rcu/tiny.c b/kernel/rcu/tiny.c
> index dccccd6be9411..5736b964d8ee1 100644
> --- a/kernel/rcu/tiny.c
> +++ b/kernel/rcu/tiny.c

[ ... ]

> @@ -42,8 +44,86 @@ static struct rcu_ctrlblk rcu_ctrlblk = {

[ ... ]

> +static void call_rcu_defer(struct rcu_head *head, rcu_callback_t func)
> +{
> +	/* A re-entrant call_rcu() during the drain would livelock it; drop it. */
> +	if (rcu_defer_draining && !in_nmi()) {
> +		WARN_ONCE(1, "call_rcu() re-entered during callback drain; leaking callback\n");
> +		return;
> +	}
> +	head->func = func;
> +	if (llist_add((struct llist_node *)head, &rcu_defer_list))

[Severity: High]
Does moving the debug_rcu_head_queue() check into rcu_do_enqueue() bypass
double-free detection for deferred callbacks?

If a double-free occurs, llist_add() will be called twice on the same node here,
corrupting the lockless list by creating a circular link (head->next = head).

When the irq_work executes rcu_defer_drain(), won't the llist_for_each_safe()
iteration infinitely hang on the circular list before the delayed debug check
can prevent the livelock?

> +		irq_work_queue(&rcu_defer_iw);
> +}

[ ... ]

> @@ -157,29 +237,15 @@ EXPORT_SYMBOL_GPL(synchronize_rcu);
>   */
>  void call_rcu(struct rcu_head *head, rcu_callback_t func)
>  {
> -	static atomic_t doublefrees;
> -	unsigned long flags;

[ ... ]

> +	if (should_rcu_defer()) {
> +		call_rcu_defer(head, func);
>  		return;
>  	}
>  
> -	head->func = func;
> -	head->next = NULL;
> -
> -	local_irq_save(flags);
> -	*rcu_ctrlblk.curtail = head;
> -	rcu_ctrlblk.curtail = &head->next;
> -	local_irq_restore(flags);
> +	/* An NMI reaching here entered with irqs enabled, so the enqueue can race. */
> +	WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi());

[Severity: Medium]
Is this concurrency assumption backwards? 

If an NMI interrupted a context where IRQs were enabled, that interrupted
context could not have been in the middle of rcu_do_enqueue() because
rcu_do_enqueue() disables IRQs via local_irq_save().

Since it's impossible for the NMI to race with rcu_do_enqueue() in this
scenario, will this falsely trigger kernel warnings during safe NMI
executions?

> -	if (unlikely(is_idle_task(current))) {
> -		/* force scheduling for rcu_qs() */
> -		resched_cpu(0);
> -	}
> +	rcu_do_enqueue(head, func);
>  }
>  EXPORT_SYMBOL_GPL(call_rcu);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260803134839.2103051-1-puranjay@kernel.org?part=2

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 2/6] rcu: Make Tiny call_rcu() safe to call from any context
  2026-08-03 14:35   ` sashiko-bot
@ 2026-08-03 14:40     ` Puranjay Mohan
  0 siblings, 0 replies; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 14:40 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: bpf, linux-rt-devel, linux-kernel

On Mon, Aug 3, 2026 at 3:35 PM <sashiko-bot@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [High] Relocating `debug_rcu_head_queue()` into `rcu_do_enqueue()` bypasses double-free detection for deferred callbacks, silently corrupting the lockless list and inducing a permanent hard livelock during `rcu_defer_drain()`.
> - [Medium] Invalid `WARN_ON_ONCE` in `call_rcu()` falsely flags safe NMI executions due to a flawed concurrency assumption.
> --
>
> --- Patch [2]: [PATCH v2 2/6] rcu: Make Tiny call_rcu() safe to call from any context ---
> commit 866e1596ae5a7314657b9b0164dfabf922016c70
> Author: Puranjay Mohan <puranjay@kernel.org>
>
> rcu: Make Tiny call_rcu() safe to call from any context
>
> Give Tiny call_rcu() the same treatment as Tree RCU.  When interrupts are
> disabled and the scheduler is up, stage the callback on a lockless list
> that an irq_work re-issues later.  One global list and irq_work suffice
> since Tiny RCU is uniprocessor, and there is no CPU-offline drain.
>
> > diff --git a/kernel/rcu/tiny.c b/kernel/rcu/tiny.c
> > index dccccd6be9411..5736b964d8ee1 100644
> > --- a/kernel/rcu/tiny.c
> > +++ b/kernel/rcu/tiny.c
>
> [ ... ]
>
> > @@ -42,8 +44,86 @@ static struct rcu_ctrlblk rcu_ctrlblk = {
>
> [ ... ]
>
> > +static void call_rcu_defer(struct rcu_head *head, rcu_callback_t func)
> > +{
> > +     /* A re-entrant call_rcu() during the drain would livelock it; drop it. */
> > +     if (rcu_defer_draining && !in_nmi()) {
> > +             WARN_ONCE(1, "call_rcu() re-entered during callback drain; leaking callback\n");
> > +             return;
> > +     }
> > +     head->func = func;
> > +     if (llist_add((struct llist_node *)head, &rcu_defer_list))
>
> [Severity: High]
> Does moving the debug_rcu_head_queue() check into rcu_do_enqueue() bypass
> double-free detection for deferred callbacks?
>
> If a double-free occurs, llist_add() will be called twice on the same node here,
> corrupting the lockless list by creating a circular link (head->next = head).
>
> When the irq_work executes rcu_defer_drain(), won't the llist_for_each_safe()
> iteration infinitely hang on the circular list before the delayed debug check
> can prevent the livelock?
>
> > +             irq_work_queue(&rcu_defer_iw);
> > +}
>
> [ ... ]
>
> > @@ -157,29 +237,15 @@ EXPORT_SYMBOL_GPL(synchronize_rcu);
> >   */
> >  void call_rcu(struct rcu_head *head, rcu_callback_t func)
> >  {
> > -     static atomic_t doublefrees;
> > -     unsigned long flags;
>
> [ ... ]
>
> > +     if (should_rcu_defer()) {
> > +             call_rcu_defer(head, func);
> >               return;
> >       }
> >
> > -     head->func = func;
> > -     head->next = NULL;
> > -
> > -     local_irq_save(flags);
> > -     *rcu_ctrlblk.curtail = head;
> > -     rcu_ctrlblk.curtail = &head->next;
> > -     local_irq_restore(flags);
> > +     /* An NMI reaching here entered with irqs enabled, so the enqueue can race. */
> > +     WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi());
>
> [Severity: Medium]
> Is this concurrency assumption backwards?
>
> If an NMI interrupted a context where IRQs were enabled, that interrupted
> context could not have been in the middle of rcu_do_enqueue() because
> rcu_do_enqueue() disables IRQs via local_irq_save().
>
> Since it's impossible for the NMI to race with rcu_do_enqueue() in this
> scenario, will this falsely trigger kernel warnings during safe NMI
> executions?

This code assumes that irqs_disabled() returns true in NMI handlers,
so the execution should not reach this warning in NMI handlers.

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 3/6] srcu: Make call_srcu() safe to call from any context
  2026-08-03 13:53 ` [PATCH v2 3/6] srcu: Make call_srcu() " Puranjay Mohan
@ 2026-08-03 14:49   ` sashiko-bot
  2026-08-03 14:52     ` Puranjay Mohan
  0 siblings, 1 reply; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 14:49 UTC (permalink / raw)
  To: Puranjay Mohan; +Cc: bpf, linux-rt-devel, linux-kernel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Tiny SRCU's call_srcu() remains unsafe to call from NMI context, leaving it vulnerable to list corruption.
--

--- Patch [3]: [PATCH v2 3/6] srcu: Make call_srcu() safe to call from any context ---
commit e3be0303432c7c7afdd4ed014dae0987e25bf7d3
Author: Puranjay Mohan <puranjay@kernel.org>

srcu: Make call_srcu() safe to call from any context

This commit updates Tree SRCU to make call_srcu() safe to call from any
context, including NMIs. It defers callbacks via a per-CPU irq_work when
interrupts are already disabled to prevent list corruption and deadlocks.

> srcu: Make call_srcu() safe to call from any context
> 
> call_srcu() has the same constraint as call_rcu(): its callback list and
> locks are only touched with interrupts disabled.  srcu_gp_start_if_needed()
> enqueues under raw_spin_lock_irqsave() and may walk the srcu_node tree, as
> do callback invocation and grace-period work.  A call_srcu() with
> interrupts disabled can race a list operation in flight on this CPU and
> corrupt the list or deadlock.  call_rcu_tasks_trace() is call_srcu() under
> the hood, so a sleepable BPF program freeing an object can reach this.

[Severity: High]
This isn't a bug introduced by this patch, but does Tiny SRCU also need a
similar fix to safely handle NMI contexts?

In kernel/rcu/srcutiny.c, call_srcu() relies solely on local_irq_save(),
which does not mask NMIs:

	local_irq_save(flags);
	*ssp->srcu_cb_tail = rhp;
	ssp->srcu_cb_tail = &rhp->next;
	local_irq_restore(flags);

If an NMI interrupts call_srcu() exactly between assigning the tail and
updating the tail pointer, and the NMI handler invokes call_srcu()
(e.g., via call_rcu_tasks_trace()) on the same srcu_struct, could this
overwrite the tail pointer and permanently detach the list?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260803134839.2103051-1-puranjay@kernel.org?part=3

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 3/6] srcu: Make call_srcu() safe to call from any context
  2026-08-03 14:49   ` sashiko-bot
@ 2026-08-03 14:52     ` Puranjay Mohan
  0 siblings, 0 replies; 13+ messages in thread
From: Puranjay Mohan @ 2026-08-03 14:52 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: bpf, linux-rt-devel, linux-kernel

On Mon, Aug 3, 2026 at 3:49 PM <sashiko-bot@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
>
> Pre-existing issues:
> - [High] Tiny SRCU's call_srcu() remains unsafe to call from NMI context, leaving it vulnerable to list corruption.
> --
>
> --- Patch [3]: [PATCH v2 3/6] srcu: Make call_srcu() safe to call from any context ---
> commit e3be0303432c7c7afdd4ed014dae0987e25bf7d3
> Author: Puranjay Mohan <puranjay@kernel.org>
>
> srcu: Make call_srcu() safe to call from any context
>
> This commit updates Tree SRCU to make call_srcu() safe to call from any
> context, including NMIs. It defers callbacks via a per-CPU irq_work when
> interrupts are already disabled to prevent list corruption and deadlocks.
>
> > srcu: Make call_srcu() safe to call from any context
> >
> > call_srcu() has the same constraint as call_rcu(): its callback list and
> > locks are only touched with interrupts disabled.  srcu_gp_start_if_needed()
> > enqueues under raw_spin_lock_irqsave() and may walk the srcu_node tree, as
> > do callback invocation and grace-period work.  A call_srcu() with
> > interrupts disabled can race a list operation in flight on this CPU and
> > corrupt the list or deadlock.  call_rcu_tasks_trace() is call_srcu() under
> > the hood, so a sleepable BPF program freeing an object can reach this.
>
> [Severity: High]
> This isn't a bug introduced by this patch, but does Tiny SRCU also need a
> similar fix to safely handle NMI contexts?
>
> In kernel/rcu/srcutiny.c, call_srcu() relies solely on local_irq_save(),
> which does not mask NMIs:
>
>         local_irq_save(flags);
>         *ssp->srcu_cb_tail = rhp;
>         ssp->srcu_cb_tail = &rhp->next;
>         local_irq_restore(flags);
>
> If an NMI interrupts call_srcu() exactly between assigning the tail and
> updating the tail pointer, and the NMI handler invokes call_srcu()
> (e.g., via call_rcu_tasks_trace()) on the same srcu_struct, could this
> overwrite the tail pointer and permanently detach the list?

See next patch it fixed tiny srcu.

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 6/6] selftests/bpf: Add a call_srcu() re-entry reproducer
  2026-08-03 13:53 ` [PATCH v2 6/6] selftests/bpf: Add a call_srcu() re-entry reproducer Puranjay Mohan
@ 2026-08-03 15:15   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 15:15 UTC (permalink / raw)
  To: Puranjay Mohan; +Cc: linux-rt-devel, linux-kernel, bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Missing control flow branch after ASSERT_OK failure for bpf_map_delete_elem in test_rcu_reentry.
- [Medium] The BPF test program unconditionally marks itself as successful even if it fails to trigger the nested RCU callback.
--

--- Patch [6]: [PATCH v2 6/6] selftests/bpf: Add a call_srcu() re-entry reproducer ---
commit 60310d36c31afce9ef7c76534688f89f2ffa0f29
Author: Puranjay Mohan <puranjay@kernel.org>

selftests/bpf: Add a call_srcu() re-entry reproducer

This commit adds a BPF selftest to exercise call_srcu() re-entry safety. It
uses an fentry program on rcu_segcblist_enqueue() to perform a task-storage
delete, which defers work via call_rcu_tasks_trace() and re-enters the
enqueue on the same CPU.

> diff --git a/tools/testing/selftests/bpf/prog_tests/rcu_reentry.c b/tools/testing/selftests/bpf/prog_tests/rcu_reentry.c
> new file mode 100644
> index 0000000000000..f6ecd93be30f4
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/rcu_reentry.c

[ ... ]

> @@ -0,0 +1,58 @@
> +// SPDX-License-Identifier: GPL-2.0

[ ... ]

> +	/* Arm the handler for this thread, then trigger call_rcu_tasks_trace(). */
> +	skel->bss->target_pid = syscall(__NR_gettid);
> +	err = bpf_map_delete_elem(map_fd, &pidfd);
> +	ASSERT_OK(err, "boot_delete");
> +
> +	/* Only Tree SRCU enqueues via rcu_segcblist_enqueue(); skip elsewhere. */
> +	if (!skel->bss->hits) {

[Severity: Medium]
Should this ASSERT_OK() call be placed inside an if statement?

If bpf_map_delete_elem() fails, the boolean return value of ASSERT_OK() is
ignored and the code falls through. The test will evaluate if (!skel->bss->hits)
and incorrectly skip instead of properly aborting.

> +		test__skip();
> +		goto out;
> +	}
> +	ASSERT_EQ(skel->bss->reentered, 1, "reentry_deferred");
> +out:
> +	if (pidfd >= 0)
> +		close(pidfd);
> +	rcu_reentry__destroy(skel);
> +}

> diff --git a/tools/testing/selftests/bpf/progs/rcu_reentry.c b/tools/testing/selftests/bpf/progs/rcu_reentry.c
> new file mode 100644
> index 0000000000000..d92a927ff51c0
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/rcu_reentry.c

[ ... ]

> @@ -0,0 +1,45 @@
> +// SPDX-License-Identifier: GPL-2.0

[ ... ]

> +SEC("fentry/rcu_segcblist_enqueue")
> +int BPF_PROG(reenter)
> +{
> +	struct task_struct *cur;
> +
> +	if (reentered || !target_pid)
> +		return 0;
> +
> +	cur = bpf_get_current_task_btf();
> +	if (!cur || cur->pid != target_pid)
> +		return 0;
> +
> +	/* Re-enter via a task-storage delete, which calls call_rcu_tasks_trace(). */
> +	__sync_fetch_and_add(&hits, 1);
> +	bpf_task_storage_get(&task_stg, cur, 0, BPF_LOCAL_STORAGE_GET_F_CREATE);
> +	bpf_task_storage_delete(&task_stg, cur);
> +
> +	reentered = 1;

[Severity: Medium]
Will this unconditionally mark the test as passed even if the BPF program
failed to trigger the nested callback?

If bpf_task_storage_get() fails (e.g., due to memory limits), the subsequent
bpf_task_storage_delete() will safely fail without triggering the nested
call_rcu_tasks_trace(). The tested deadlock path is never exercised, but
reentered is still set to 1, which will cause the userspace test to assert
success.

> +	return 0;
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260803134839.2103051-1-puranjay@kernel.org?part=6

^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2026-08-03 15:15 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-03 13:48 [PATCH v2 0/6] rcu,srcu: Make call_rcu()/call_srcu() safe from any context Puranjay Mohan
2026-08-03 13:53 ` [PATCH v2 1/6] rcu: Make call_rcu() safe to call " Puranjay Mohan
2026-08-03 14:18   ` sashiko-bot
2026-08-03 13:53 ` [PATCH v2 2/6] rcu: Make Tiny " Puranjay Mohan
2026-08-03 14:35   ` sashiko-bot
2026-08-03 14:40     ` Puranjay Mohan
2026-08-03 13:53 ` [PATCH v2 3/6] srcu: Make call_srcu() " Puranjay Mohan
2026-08-03 14:49   ` sashiko-bot
2026-08-03 14:52     ` Puranjay Mohan
2026-08-03 13:53 ` [PATCH v2 4/6] srcu: Make Tiny " Puranjay Mohan
2026-08-03 13:53 ` [PATCH v2 5/6] rcutorture: Exercise ->call() from NMI context Puranjay Mohan
2026-08-03 13:53 ` [PATCH v2 6/6] selftests/bpf: Add a call_srcu() re-entry reproducer Puranjay Mohan
2026-08-03 15:15   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).