On Mon, Aug 10, 2026 at 05:27:49AM -0700, Puranjay Mohan wrote:
> 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.

Queued for testing and further review, thank you!

                                                        Thanx, Paul

> 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.  The gate is bare irqs_disabled(), so callers that merely hold
> interrupts off are deferred too and pay one irq_work hop.
> 
> 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 every CPU's deferred list themselves
> before they wait.  They drain rather than wait the irq_work out because
> irq_work_sync() parks on an rcuwait, which holds a single waiter, so two
> concurrent barriers would clobber each other's wakeup.
> 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 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 catches that: a deferral that arrives while that CPU is
> inside its own irq_work drain, and is not from an NMI, is dropped rather
> than staged, with a WARN_ONCE() under CONFIG_PROVE_RCU whose backtrace
> names the instrumentation responsible.  Dropping leaks that callback, and
> can strand state its caller tied to it, 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.  Only the
> irq_work drain sets the flag: a direct drain from a barrier or from
> CPU-offline re-issues onto the current CPU, so anything staged during it
> is picked up by that CPU's own irq_work rather than feeding the drain in
> progress, and no legitimate callback is dropped. 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 in all four flavors.  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:
> v3: https://lore.kernel.org/rcu/[email protected]/
> Changes in v4:
> - rcu_barrier() and srcu_barrier() no longer skip a CPU whose deferred
>   list looks empty.  The lockless llist_empty() test added in v3 skipped
>   the lock that makes a drain conclusive: a concurrent drainer can already
>   have emptied the list without having re-issued yet, so the barrier could
>   return before those callbacks reached a callback list.
> - Tree SRCU: drain before cleanup_srcu_struct()'s "just leak it" early
>   returns, and in srcu_module_going() before it frees any ->sda.  Deferral
>   skips check_init_srcu_struct(), so srcu_module_going() could skip
>   cleanup_srcu_struct() and free ->sda with a staged srcu_data still
>   chained on a per-CPU list.
> - Dropped ->defer_exp: a deferred expedited call_srcu() now completes as a
>   normal grace period.  The flag lived on the srcu_data rather than on the
>   callback, so it expedited whole batches; only srcu_expedite_current()
>   with interrupts already disabled can reach this, so the downgrade costs
>   a delay and nothing else.
> - rcutorture: use a fixed perf sample period rather than .freq, which sets
>   TICK_DEP_BIT_PERF_EVENTS for every CPU and pinned the tick for whole
>   runs of TREE04 and TREE07.  Also document nmi_calls, and say on the
>   console when there is no PMU, since the issued==invoked check then
>   compares zero to zero.
> - kasan_record_aux_stack() moved to __call_rcu_common(), so a
>   use-after-free report names the caller rather than the irq_work.
> - Known gap, documented in patch 1: a callback deferred past the
>   CPUHP_AP_SMPCFD_DYING flush leaves ->defer_work claimed with its
>   self-IPI lost.  rcutree_migrate_callbacks() still re-issues the
>   callback, but the first deferral after that CPU comes back raises no IPI
>   and waits for the next irq_work there, or for rcu_barrier().
> 
> v2: https://lore.kernel.org/rcu/[email protected]/
> Changes in v3:
> - Barriers no longer call irq_work_sync() on an online CPU's ->defer_work.
>   irq_work_sync() waits on an rcuwait, which holds exactly one task, so two
>   concurrent rcu_barrier()s syncing the same per-CPU irq_work could lose a
>   wakeup and hang.  Both flushes now drain every CPU directly, guarded by a
>   lockless llist_empty() test so the no-deferrals case stays cheap.
> - The re-entry guard is now set only by the irq_work drain, which always
>   runs on the CPU owning the list it drains.  In v2 a barrier draining a
>   remote CPU set the flag on the draining CPU, so an unrelated irqs-off
>   call_rcu() there was dropped and leaked even though it could not have fed
>   the drain.
> - The drain clears ->next before re-issuing.  A double call_rcu() on a head
>   that is already debug-object-active makes llist_add() self-link it, and
>   rcu_do_enqueue()'s double-free path returns without clearing ->next, so
>   the drain span looped forever with interrupts disabled.
> - An expedited call_srcu() is no longer silently downgraded: ->defer_exp
>   records it per srcu_data and the batch is re-issued expedited.  Only
>   srcu_expedite_current() is affected, __synchronize_srcu() sleeps and so
>   is never deferred.
> - The drop is now WARN_ONCE() under CONFIG_PROVE_RCU rather than an
>   unconditional WARN, so instrumentation cannot reboot a panic_on_warn
>   kernel; the backtrace is what identifies the offending program.
> - Tiny RCU and Tiny SRCU use IRQ_WORK_INIT_HARD like the Tree flavors, and
>   READ_ONCE()/WRITE_ONCE() on their draining flags.  TINY_SRCU is
>   "default y if !SMP" with no PREEMPT_RT dependency, so it really can be
>   built on RT where a non-HARD irq_work waits on the irq_workd kthread.
> - Tiny SRCU: cleanup_srcu_struct() drains *and* irq_work_sync()s
>   ->defer_iw.  That irq_work is embedded in the srcu_struct the caller is
>   about to free, unlike the Tree flavors' static per-CPU ones.
> - rcutorture: drive the perf counters from CPU-hotplug callbacks.  The
>   one-shot for_each_online_cpu() loop lost them at the first CPU offline,
>   after which the end-of-test issued==invoked check compared 0 == 0.
> - rcutorture: per-CPU rcu_head instead of one global, so several CPUs can
>   race the drain; count the call before issuing it so mid-run stats cannot
>   show nmi-cbs > nmi-calls; release the perf events on the
>   torture_cleanup_begin() early-return path; print nmi_calls in the module
>   banner; report when nmi_calls is set but no NMI ->call() ever happened;
>   and drop sample_freq to 100, since 1000 made perf lower the system-wide
>   perf_event_max_sample_rate tenfold.
> - selftests/bpf: the old ASSERT_EQ(reentered, 1) could not fail, and an
>   atomic allocation failure in the nested task-storage delete made the test
>   pass without ever re-entering call_srcu().  It now records and asserts
>   the helper return values, probes for rcu_segcblist_enqueue() up front so
>   a Tiny kernel skips instead of failing, and restores the CPU affinity it
>   changes.
> 
> Testing: rcutorture rcu/srcu/srcud/tasks-tracing with nmi_calls, hotplug,
> barriers and nocb toggling issued ~81000 callbacks from NMI with none lost,
> under PROVE_LOCKING, PROVE_RAW_LOCK_NESTING, DEBUG_OBJECTS_RCU_HEAD and
> RCU_LAZY, with no lockdep reports; SRCU-T, SRCU-U, TINY01 and TINY02 pass;
> the BPF reproducer passes.
> 
> v1: https://lore.kernel.org/all/[email protected]/
> 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.
> 
> Testing: rcutorture rcu, srcu, srcud and tasks-tracing, each with
> nmi_calls, CPU hotplug and barriers, and nocb toggling for rcu, all
> End of test: SUCCESS with the count issued from NMI equal to the count
> invoked (17987, 2924, 2661 and 4412 respectively) and clean consoles,
> under PROVE_LOCKING, PROVE_RAW_LOCK_NESTING and DEBUG_OBJECTS_RCU_HEAD;
> SRCU-T, SRCU-U, TINY01 and TINY02 pass; the BPF reproducer passes.
> Build-tested x86_64 with TREE_RCU+TREE_SRCU, TINY_RCU+TINY_SRCU and
> TREE_RCU+TINY_SRCU, each with CONFIG_RCU_DEFER=y and =n, plus arm64
> TREE_RCU+TREE_SRCU with RCU_NOCB_CPU and RCU_LAZY.
> 
> 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
> 
>  .../admin-guide/kernel-parameters.txt         |   7 +
>  include/linux/srcutiny.h                      |  12 +-
>  include/linux/srcutree.h                      |   4 +
>  kernel/rcu/Kconfig                            |   6 +
>  kernel/rcu/rcu.h                              |  14 ++
>  kernel/rcu/rcutorture.c                       | 150 +++++++++++++++-
>  kernel/rcu/srcutiny.c                         |  93 +++++++++-
>  kernel/rcu/srcutree.c                         | 170 +++++++++++++++++-
>  kernel/rcu/tiny.c                             | 127 ++++++++++---
>  kernel/rcu/tree.c                             | 133 ++++++++++++--
>  kernel/rcu/tree.h                             |   6 +
>  .../selftests/bpf/prog_tests/rcu_reentry.c    |  93 ++++++++++
>  .../testing/selftests/bpf/progs/rcu_reentry.c |  51 ++++++
>  13 files changed, 818 insertions(+), 48 deletions(-)
>  create mode 100644 tools/testing/selftests/bpf/prog_tests/rcu_reentry.c
>  create mode 100644 tools/testing/selftests/bpf/progs/rcu_reentry.c
> 
> base-commit: 9dc303e69bcd49f9668ca090ae45325269531fbb
> -- 
> 2.53.0-Meta
> 

Reply via email to