Re: [RFC PATCH 01/12] rcu_txn: Add single-writer RCU pseudo-transactions
From: Puranjay Mohan
Date: Fri Oct 23 2026 - 14:22:42 EST
On Fri, Oct 16, 2026 at 09:33:08AM -0400, Mathieu Desnoyers wrote:
[...]
> The records and the selector live in one descriptor, allocated with the
> gfp_t given to rcu_txn_init() and freed with kfree_rcu() after a commit
> that installed proxies. A descriptor that installed none, after a
> commit of one record or a cancel, owes no grace period: it is freed with
> kfree() or, on PREEMPT_RT, with kfree_rcu(): rcu_txn_commit() and
> rcu_txn_cancel() may then be called under a raw or a bit spinlock on
> every kernel, as a hash table with hlist_bl buckets does. An allocation
This isn't quite true on !PREEMPT_RT. As far as lockdep is concerned,
kfree() can't be called under a raw_spinlock_t on any kernel: once the
per-CPU sheaf is full, SLUB takes barn->lock, a spinlock_t, and
PROVE_RAW_LOCK_NESTING flags that whether or not PREEMPT_RT is set.
kfree_rcu() gets away with it because mm/slub.c annotates its sheaf
path for exactly this case (kfree_rcu_sheaf_map); kfree() has no such
annotation.
[...]
> +/*
> + * Free a descriptor that installed no proxy, which owes no grace period.
> + * commit() and cancel() may run under a raw or a bit spinlock, where
> + * PREEMPT_RT does not allow kfree(). It does allow kfree_rcu().
> + */
> +void __rcu_txn_free_desc(struct rcu_txn_desc *desc)
> +{
> + if (IS_ENABLED(CONFIG_PREEMPT_RT))
> + kfree_rcu(desc, rcu_head);
> + else
> + kfree(desc);
A single commit rarely trips it, since barn->lock is only taken once the
sheaf is full, and lockdep skips the wait-context check for the fast
path's trylock. 4096 reserved one-record commits under one raw spinlock
do trip it. This is the series as posted, on arm64 with
PROVE_LOCKING and PROVE_RAW_LOCK_NESTING:
[ BUG: Invalid wait context ]
kunit_try_catch/44 is trying to lock:
ffff000001804318 (&barn->lock){....}-{3:3}, at: __pcs_replace_full_main+0x4b4/0x514
locks held by kunit_try_catch/44: 2, last CPU#1:
#0: ffff80008080c120 (exp_raw_lock){....}-{2:2}, at: exp_bulk+0x1cc/0x2fc
#1: ffff00003fda37a0 ((local_lock_t *)&pcs->lock){+.+.}-{3:3}, at: local_trylock_acquire+0x14/0x8c
[...]
Call trace:
_raw_spin_lock_irqsave+0x60/0x88
__pcs_replace_full_main+0x4b4/0x514
kfree+0x400/0x43c
__rcu_txn_free_desc+0x10/0x1c
rcu_txn_commit+0x54/0x11c
exp_bulk+0x250/0x2fc
rcu_txn_test_exp_bulk_commit_raw+0x14/0x20
Cancels give the same splat, and so does a loop doing one commit per
raw-lock section once the sheaf fills up.
Using kfree_rcu() unconditionally fixes it, and the rest of the suite
still passes (against the top of the series):
--- a/lib/rcu_txn.c
+++ b/lib/rcu_txn.c
@@ -73,9 +73,6 @@ static struct rcu_txn_desc *rcu_txn_desc_alloc(unsigned int cap, gfp_t gfp)
*/
void __rcu_txn_free_desc(struct rcu_txn_desc *desc)
{
- if (IS_ENABLED(CONFIG_PREEMPT_RT))
- kfree_rcu(desc, rcu_head);
- else
- kfree(desc);
+ kfree_rcu(desc, rcu_head);
}
EXPORT_SYMBOL_GPL(__rcu_txn_free_desc);
The cost is a grace period of delay before freeing descriptors that
never installed a proxy. I'd take that over a wait-type override
around the !PREEMPT_RT kfree(), since it also removes the RT/!RT split.
This makes a few comments stale: the kerneldoc of rcu_txn_cancel(),
the comment above __rcu_txn_free_desc(), the two "no proxy: free now"
comments, the comment above rcu_txn_test_single_record() in patch 2
and the one above rcu_txn_test_commit_atomic() in patch 10, plus the
commit messages of patches 1 and 10. On the other hand, the "Readers
and reclaim" comment, which says commit reclaims with kfree_rcu(), and
the raw spinlock paragraph in rcu_txn.rst only become true with it.
Thanks,
Puranjay