Skip to contentSkip to conversation

rv/reactors: fix lockdep warning and add KUnit tests

PATCH Superseded [email protected] 4 participants 15 messages Aug 9 – Aug 25

Subsystems (2)

v3 of 6 Superseded Latest is v6 →

lore
Share & tools
lore
#0 lore raw

v3 cover rv/reactors: fix lockdep warning and add KUnit tests

From: Wen Yang <[email protected]>

We occasionally hit a lockdep "Invalid wait context" warning in production when a reactor callback is preempted by a timer interrupt. On interrupt exit the scheduler takes rq->__lock (LD_WAIT_SPIN) while rv_react() still holds its wait-type-override map, which declared LD_WAIT_FREE. On any kernel where the task context has preemption enabled (not just CONFIG_PREEMPT_RT) this triggers a spurious lockdep report:

[ BUG: Invalid wait context ]
1 lock held by kunit_try_catch/209:
#0: (rv_react_map-wait-type-override){+.+.}-{1:1}
kunit_try_catch/209 is trying to lock:
ffff8a743ed3e8a0 (&rq->__lock){-...}-{2:2}

Changes in v3:

Patch 1:
- Rewrite commit message to clarify that the bug is not
PREEMPT_RT-specific: any preemptible task context can be interrupted by a timer, and the scheduler acquires rq->__lock (LD_WAIT_SPIN) on interrupt exit while rv_react() still holds its LD_WAIT_FREE override map.
- Rewrite the inline comment in rv_react() to make clear.

Patch 3:
- Fix commit message: missing EXPORT_SYMBOL_GPL() is caught by modpost at link time, not at load time.

Patch 4:
- Drop test_reactor_registered bool and the unregister_test_reactor() wrapper entirely.
- Remove test_register_unregister(); the teardown already covers the unregister path, and test_double_register() exercises the full register->verify->unregister-via-teardown flow.
- Replace the runtime KUNIT_ASSERT_EQ(strlen(...)) guard in test_name_too_long() with a _Static_assert on the name array size.
- In test_react_no_callback(): reuse the existing react_call_count atomic to assert that the callback was never invoked.
This catches a broader class of regressions — e.g.
an inverted or missing NULL guard that does not cause a NULL dereference.

No functional change to patches 2-3; the series structure is unchanged.

Tested with CONFIG_PROVE_LOCKING=y and CONFIG_KUNIT=y.

v2: https://lore.kernel.org/lkml/[email protected]/
v1: https://lore.kernel.org/lkml/[email protected]/

Wen Yang (4):
rv/reactors: use context-sensitive lockdep wait type in rv_react() rv/reactors: propagate rv_register_reactor() error from reactor init rv/reactors: export rv_register_reactor() and rv_unregister_reactor() rv/reactors: add KUnit tests for reactor registration and dispatch

 kernel/trace/rv/Kconfig             |  12 +++
 kernel/trace/rv/Makefile            |   1 +
 kernel/trace/rv/reactor_panic.c     |   3 +-
 kernel/trace/rv/reactor_printk.c    |   3 +-
 kernel/trace/rv/rv_reactors.c       |  22 +++--
 kernel/trace/rv/rv_reactors_kunit.c | 119 ++++++++++++++++++++++++++++
 6 files changed, 151 insertions(+), 9 deletions(-)
 create mode 100644 kernel/trace/rv/rv_reactors_kunit.c

base-commit: 785095112f4198de49760552374f364043c8dbdf

Signature · 1 line
2.25.1
#1 lore raw

PATCH v3 1/4 rv/reactors: use context-sensitive lockdep wait type in rv_react()

From: Wen Yang <[email protected]>

Reactors must not explicitly take locks, so they should comply with LD_WAIT_FREE. However, reactor callbacks can run with preemption enabled on any kernel (not just PREEMPT_RT). If a timer interrupt fires during the callback, the interrupt exit path schedules and acquires rq->__lock (LD_WAIT_SPIN) while the lockdep override map that declared LD_WAIT_FREE is still held, triggering a spurious "Invalid wait context" warning:

[ BUG: Invalid wait context ] context-{5:5}
1 lock held by kunit_try_catch/209:
#0: (rv_react_map-wait-type-override){+.+.}-{1:1}
kunit_try_catch/209 is trying to lock:
ffff8a743ed3e8a0 (&rq->__lock){-...}-{2:2}

Fixes 69d8895cb9a9 ("rv: Add explicit lockdep context for reactors")
Cc Thomas Weißschuh <[email protected]>
---
 kernel/trace/rv/rv_reactors.c | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)
kernel/trace/rv/rv_reactors.c +15 -5
index 2f5fc8d18dea..5830229210e8 100644
@@ -465,18 +465,28 @@ int init_rv_reactors(struct dentry *root_dir)
 
 void rv_react(struct rv_monitor *monitor, const char *msg, ...)
 {
-	static DEFINE_WAIT_OVERRIDE_MAP(rv_react_map, LD_WAIT_FREE);
+	/*
+	 * Reactors must not explicitly take locks, so they should be
+	 * LD_WAIT_FREE.  However, reactor callbacks can run with preemption
+	 * enabled, meaning the preempting code (e.g. the scheduler taking
+	 * rq->__lock at LD_WAIT_SPIN) may violate that constraint.  Use
+	 * LD_WAIT_SPIN in preemptible contexts to avoid false-positive lockdep
+	 * reports; keep LD_WAIT_FREE in NMI/hardirq where preemption is
+	 * disabled and the scheduler cannot run.
+	 */
+	static DEFINE_WAIT_OVERRIDE_MAP(rv_react_map,        LD_WAIT_SPIN);
+	static DEFINE_WAIT_OVERRIDE_MAP(rv_react_map_atomic, LD_WAIT_FREE);
+	struct lockdep_map * __maybe_unused map;
 	va_list args;
 
 	if (!rv_reacting_on() || !monitor->react)
 		return;
 
+	map = (in_nmi() || in_hardirq()) ? &rv_react_map_atomic : &rv_react_map;
 	va_start(args, msg);
-
-	lock_map_acquire_try(&rv_react_map);
+	lock_map_acquire_try(map);
 	monitor->react(msg, args);
-	lock_map_release(&rv_react_map);
-
+	lock_map_release(map);
 	va_end(args);
 }
 EXPORT_SYMBOL_GPL(rv_react);
Signature · 1 line
2.25.1
maintainer Reviewed-by on v3 1/4
#2 lore raw

Re: [PATCH v3 1/4] rv/reactors: use context-sensitive lockdep wait type in rv_react()

Quoted 17 lines · On Mon, 2026-08-10 at 01:10 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Reactors must not explicitly take locks, so they should comply with LD_WAIT_FREE.  However, reactor callbacks can run with preemption enabled on any kernel (not just PREEMPT_RT).  If a timer interrupt fires during the callback, the interrupt exit path schedules and acquires rq->__lock (LD_WAIT_SPIN) while the lockdep override map that declared LD_WAIT_FREE is still held, triggering a spurious "Invalid wait context" warning:

    [ BUG: Invalid wait context ] context-{5:5}
    1 lock held by kunit_try_catch/209:
     #0: (rv_react_map-wait-type-override){+.+.}-{1:1} kunit_try_catch/209 is trying to lock:
    ffff8a743ed3e8a0 (&rq->__lock){-...}-{2:2}

Quoted 2 lines
Fixes 69d8895cb9a9 ("rv: Add explicit lockdep context for reactors")

You came up with the idea, I at most suggested how I'd like it worded in the comment.

Anyway, I'd appreciate comments/acks from the other folks in the loop, but from my side this looks good.

Thanks,
Gabriele

Quoted 46 lines
Cc Thomas Weißschuh <[email protected]>

---
 kernel/trace/rv/rv_reactors.c | 20 +++++++++++++++-----

 1 file changed, 15 insertions(+), 5 deletions(-)
kernel/trace/rv/rv_reactors.c +0 -0
index 2f5fc8d18dea..5830229210e8 100644
@@ -465,18 +465,28 @@ int init_rv_reactors(struct dentry *root_dir)


 void rv_react(struct rv_monitor *monitor, const char *msg, ...) {

kernel/trace/rv/rv_reactors.c +6 -1
-	static DEFINE_WAIT_OVERRIDE_MAP(rv_react_map, LD_WAIT_FREE);
+	/*
+	 * Reactors must not explicitly take locks, so they should be
+	 * LD_WAIT_FREE.  However, reactor callbacks can run with preemption
+	 * enabled, meaning the preempting code (e.g. the scheduler taking
+	 * rq->__lock at LD_WAIT_SPIN) may violate that constraint.  Use
+	 * LD_WAIT_SPIN in preemptible contexts to avoid false-positive

lockdep

kernel/trace/rv/rv_reactors.c +6 -0
+	 * reports; keep LD_WAIT_FREE in NMI/hardirq where preemption is
+	 * disabled and the scheduler cannot run.
+	 */
+	static DEFINE_WAIT_OVERRIDE_MAP(rv_react_map,        LD_WAIT_SPIN);
+	static DEFINE_WAIT_OVERRIDE_MAP(rv_react_map_atomic, LD_WAIT_FREE);
+	struct lockdep_map * __maybe_unused map;

  va_list args;

  if (!rv_reacting_on() || !monitor->react)
  return;

kernel/trace/rv/rv_reactors.c +1 -0
+	map = (in_nmi() || in_hardirq()) ? &rv_react_map_atomic :

&rv_react_map;
  va_start(args, msg);

kernel/trace/rv/rv_reactors.c +1 -2
-
-	lock_map_acquire_try(&rv_react_map);
+	lock_map_acquire_try(map);

  monitor->react(msg, args);

kernel/trace/rv/rv_reactors.c +1 -2
-	lock_map_release(&rv_react_map);
-
+	lock_map_release(map);

  va_end(args);
 }
 EXPORT_SYMBOL_GPL(rv_react);

#3 lore raw

Re: [PATCH v3 1/4] rv/reactors: use context-sensitive lockdep wait type in rv_react()

Quoted 10 lines · Gabriele Monaco
Quoted 9 lines · On Mon, 2026-08-10 at 01:10 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Reactors must not explicitly take locks, so they should comply with LD_WAIT_FREE.  However, reactor callbacks can run with preemption enabled on any kernel (not just PREEMPT_RT).  If a timer interrupt fires during the callback, the interrupt exit path schedules and acquires rq->__lock (LD_WAIT_SPIN) while the lockdep override map that declared LD_WAIT_FREE is still held, triggering a spurious "Invalid wait context" warning:

...

Quoted 1 line

Anyway, I'd appreciate comments/acks from the other folks in the loop

Sorry, I do not know enough about lockdep to comment on this.

FWIW, I would rather just use LD_WAIT_SPIN and keep things simple. Context-sensitive code paths "feels wrong" to me. Spinning should either be allowed or forbidden. Making it dynamic "feels like" it will bring further complications down the road.

But that's just my intuition.

Nam

maintainer
#4 lore raw

Re: [PATCH v3 1/4] rv/reactors: use context-sensitive lockdep wait type in rv_react()

Quoted 15 lines · Nam Cao
Quoted 10 lines · Gabriele Monaco
Quoted 9 lines · On Mon, 2026-08-10 at 01:10 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Reactors must not explicitly take locks, so they should comply with LD_WAIT_FREE.  However, reactor callbacks can run with preemption enabled on any kernel (not just PREEMPT_RT).  If a timer interrupt fires during the callback, the interrupt exit path schedules and acquires rq->__lock (LD_WAIT_SPIN) while the lockdep override map that declared LD_WAIT_FREE is still held, triggering a spurious "Invalid wait context" warning:

...

Quoted 1 line

Anyway, I'd appreciate comments/acks from the other folks in the loop

Quoted 8 lines

Sorry, I do not know enough about lockdep to comment on this.

FWIW, I would rather just use LD_WAIT_SPIN and keep things simple. Context-sensitive code paths "feels wrong" to me. Spinning should either be allowed or forbidden. Making it dynamic "feels like" it will bring further complications down the road.

But that's just my intuition.

I don't have a strong opinion on this, but since there's no one in the kernel using LD_WAIT_FREE as inner type, that feels like a hint to go down the simple route too and allow LD_WAIT_SPIN.

If a reactor ever uses spinlocks, lockdep would already complain on its own if that ends up being an issue, wouldn't it?

Gabriele

#5 lore raw

Re: [PATCH v3 1/4] rv/reactors: use context-sensitive lockdep wait type in rv_react()

Quoted 14 lines · Gabriele Monaco
Quoted 13 lines · Nam Cao
Quoted 10 lines · Gabriele Monaco
Quoted 9 lines · On Mon, 2026-08-10 at 01:10 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Reactors must not explicitly take locks, so they should comply with LD_WAIT_FREE.  However, reactor callbacks can run with preemption enabled on any kernel (not just PREEMPT_RT).  If a timer interrupt fires during the callback, the interrupt exit path schedules and acquires rq->__lock (LD_WAIT_SPIN) while the lockdep override map that declared LD_WAIT_FREE is still held, triggering a spurious "Invalid wait context" warning:

...

Quoted 8 lines
Quoted 8 lines
Quoted 1 line

Anyway, I'd appreciate comments/acks from the other folks in the loop

Sorry, I do not know enough about lockdep to comment on this.

FWIW, I would rather just use LD_WAIT_SPIN and keep things simple. Context-sensitive code paths "feels wrong" to me. Spinning should either be allowed or forbidden. Making it dynamic "feels like" it will bring further complications down the road.

To me the dynamic logic also feels quite complicated.
We could also disable preemption before overriding the lockdep context when lockdep is enabled to avoid the observed issue.

Quoted 8 lines
Quoted 1 line

But that's just my intuition.

I don't have a strong opinion on this, but since there's no one in the kernel using LD_WAIT_FREE as inner type, that feels like a hint to go down the simple route too and allow LD_WAIT_SPIN.

If a reactor ever uses spinlocks, lockdep would already complain on its own if that ends up being an issue, wouldn't it?

Only if that reactor is actually triggered by a tracepoint in the wrong context. This might not happen during testing. This happened to me in my signal reactor patch, which is why I added the lockdep override.

Thomas

author
#6 lore raw

Re: [PATCH v3 1/4] rv/reactors: use context-sensitive lockdep wait type in rv_react()

Quoted 19 lines · Thomas Weißschuh
Quoted 18 lines · Gabriele Monaco
Quoted 17 lines · Nam Cao
Quoted 10 lines · Gabriele Monaco
Quoted 9 lines · On Mon, 2026-08-10 at 01:10 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Reactors must not explicitly take locks, so they should comply with LD_WAIT_FREE.  However, reactor callbacks can run with preemption enabled on any kernel (not just PREEMPT_RT).  If a timer interrupt fires during the callback, the interrupt exit path schedules and acquires rq->__lock (LD_WAIT_SPIN) while the lockdep override map that declared LD_WAIT_FREE is still held, triggering a spurious "Invalid wait context" warning:

...

Quoted 1 line

Anyway, I'd appreciate comments/acks from the other folks in the loop

Sorry, I do not know enough about lockdep to comment on this.

Quoted 9 lines
Quoted 4 lines
Quoted 4 lines

FWIW, I would rather just use LD_WAIT_SPIN and keep things simple. Context-sensitive code paths "feels wrong" to me. Spinning should either be allowed or forbidden. Making it dynamic "feels like" it will bring further complications down the road.

To me the dynamic logic also feels quite complicated.
We could also disable preemption before overriding the lockdep context when lockdep is enabled to avoid the observed issue.

Thanks,

rv_react() already has two callers in this tree that land in very different places.
nrp attaches to local_timer_entry, which fires from inside the hardirq handler itself:

    DEFINE_IDTENTRY_SYSVEC(sysvec_apic_timer_interrupt)
      irq_enter_rcu()                  -> 
preempt_count_add(HARDIRQ_OFFSET)
      trace_local_timer_entry()
        handle_vector_irq_entry() -> da_handle_event() -> rv_react()

in_hardirq() is true for the whole of that rv_react() call.

While pagefault, on the other hand, attaches to page_fault_user, which only fires when the interrupted context was user mode -- i.e. preempt_count is guaranteed 0:

    exc_page_fault()
      trace_page_fault_user()
        handle_page_fault() -> ltl_atom_pulse() -> ... -> rv_react()

A timer tick can land inside that second one and there's nothing wrong with it doing so.

So I don't think LD_WAIT_SPIN for everything is safe, it's not just "less strict on paper".
If a reactor ever does raw_spin_lock() by mistake while running from nrp's path, check_wait_context() takes the wait type straight from the override map:

if (unlikely(class->lock_type == LD_LOCK_WAIT_OVERRIDE))
curr_inner = prev_inner; /* SPIN */
if (next_outer > curr_inner)
return print_lock_invalid_wait_context(...);

SPIN nested in SPIN is 2 > 2, which is false, so it just passes.
We'd be silently giving up the one case (hardirq/NMI) where the "no locks" rule for reactors actually matters, which is the same thing that bit you with the signal reactor.

And for what it's worth, checking context to decide the wait type isn't something we'd be introducing -- lockdep does the same thing to get the baseline before any override is applied, in the exact function that produced this splat:

    static inline short task_wait_context(struct task_struct *curr)
    {
            if (lockdep_hardirq_context()) {
                    if (curr->hardirq_threaded || curr->irq_config)
                            return LD_WAIT_CONFIG;
                    return LD_WAIT_SPIN;
            } else if (curr->softirq_context) {
                    return LD_WAIT_CONFIG;
            }
            return LD_WAIT_MAX;
    }

That's four cases. Our in_nmi() || in_hardirq() is a simplification of what's already there, not a new habit.

Thomas, on disabling preemption instead: it does fix the pagefault
case, and it's a no-op for nrp since preempt_count is already elevated there. But it changes what a reactor is allowed to do, for every reactor, not just the two above.cspin_lock() on RT checks might_resched() before it even looks at thevlock:

    static __always_inline void __rt_spin_lock(spinlock_t *lock)
    {
            rtlock_might_resched();   /* unconditional */
            rtlock_lock(&lock->lock);
    }

so any future reactor using a plain spinlock would hit that every single time, lock contended or not, on top of whatever it's already called for. That seems worse than the thing we're trying to fix.

Since neither struct rv_reactor nor rv_react() actually documents what context a callback may run in, maybe that's worth spelling out separately regardless of what we do here -- something close to what printk already does for the same reason (reactor_printk's vprintk_deferred() leans on this internally: is_printk_legacy_deferred() checks in_nmi() to decide whether it's safe to take console_lock or whether it has to go through the lock-free irq_work path instead).

A reactor that wants to do more than printk/panic would follow the same shape:

    static void some_reactor(const char *msg, va_list args)
    {
            if (atomic_cmpxchg(&pending, 0, 1) == 0)
                    irq_work_queue(&my_irq_work);   /* NMI-safe */
    }

    static void some_irq_work_fn(struct irq_work *work)
    {
            schedule_work(&my_work);   /* spin_lock()/mutex_lock() now 
fine */
    }

Please let me know if there are any concerns or if further changes are needed.

--
Best wishes,
Wen

Quoted 15 lines
Quoted 8 lines
Quoted 1 line

But that's just my intuition.

I don't have a strong opinion on this, but since there's no one in the kernel using LD_WAIT_FREE as inner type, that feels like a hint to go down the simple route too and allow LD_WAIT_SPIN.

If a reactor ever uses spinlocks, lockdep would already complain on its own if that ends up being an issue, wouldn't it?

Only if that reactor is actually triggered by a tracepoint in the wrong context. This might not happen during testing. This happened to me in my signal reactor patch, which is why I added the lockdep override.

Thomas

maintainer
#7 lore raw

Re: [PATCH v3 1/4] rv/reactors: use context-sensitive lockdep wait type in rv_react()

Quoted 2 lines · Wen Yang

So I don't think LD_WAIT_SPIN for everything is safe, it's not just "less strict on paper".

Yes it won't be safe, we could have wrong reactors not reported by lockdep, but so would your conditional check. We'd be catching everything in interrupts, but if a model never runs in interrupt context, we'd be demoted to LD_WAIT_SPIN nonetheless.

Your condition is kind of a best-effort to do a bit better than just LD_WAIT_SPIN. Again, I'm not completely against it, but it isn't a final solution (not that I can think of a better one) and if most people find it confusing, we're probably better off without it.

Or am I missing something?

Quoted 9 lines

If a reactor ever does raw_spin_lock() by mistake while running from nrp's path, check_wait_context() takes the wait type straight from the override map:

     if (unlikely(class->lock_type == LD_LOCK_WAIT_OVERRIDE)) curr_inner = prev_inner;        /* SPIN */ if (next_outer > curr_inner)
             return print_lock_invalid_wait_context(...);

Quoted 4 lines

SPIN nested in SPIN is 2 > 2, which is false, so it just passes. We'd be silently giving up the one case (hardirq/NMI) where the "no locks" rule for reactors actually matters, which is the same thing that bit you with the signal reactor.

True, that is one, but it isn't the only case where using locks can be an issue, namely sched_switch is particularly hard to please (interrupts are disabled for a reason).

Quoted 17 lines

And for what it's worth, checking context to decide the wait type isn't something we'd be introducing -- lockdep does the same thing to get the baseline before any override is applied, in the exact function that produced this splat:

     static inline short task_wait_context(struct task_struct *curr) {
             if (lockdep_hardirq_context()) {
                     if (curr->hardirq_threaded || curr->irq_config) return LD_WAIT_CONFIG; return LD_WAIT_SPIN;
             } else if (curr->softirq_context) {
                     return LD_WAIT_CONFIG;
             }
             return LD_WAIT_MAX;
     }

Quoted 2 lines

That's four cases. Our in_nmi() || in_hardirq() is a simplification of what's already there, not a new habit.

I'm not familiar with the lockdep code, so I may be missing something here, but isn't this function getting the current context? It isn't used to tune the lockdep logic for some specific function, it /is/ the lockdep logic itself.

Is this really comparable?

Quoted 12 lines

Thomas, on disabling preemption instead: it does fix the pagefault case, and it's a no-op for nrp since preempt_count is already elevated there. But it changes what a reactor is allowed to do, for every reactor, not just the two above.cspin_lock() on RT checks might_resched() before it even looks at thevlock:

     static __always_inline void __rt_spin_lock(spinlock_t *lock) {
             rtlock_might_resched();   /* unconditional */ rtlock_lock(&lock->lock);
     }

Quoted 3 lines

so any future reactor using a plain spinlock would hit that every single time, lock contended or not, on top of whatever it's already called for. That seems worse than the thing we're trying to fix.

I'm also against disabling preemption, we may even be able to avoid scheduling by also disabling interrupts in a convenient order, but in principle I'd avoid that like plague. RV was born as a tool for real-time and disabling preemption only to get better lockdep reporting doesn't look right to me.

Sure we could do it only with lockdep and assume lockdep's overhead is already enough, but we're again complicating things.

Quoted 7 lines

Since neither struct rv_reactor nor rv_react() actually documents what context a callback may run in, maybe that's worth spelling out separately regardless of what we do here -- something close to what printk already does for the same reason (reactor_printk's vprintk_deferred() leans on this internally: is_printk_legacy_deferred() checks in_nmi() to decide whether it's safe to take console_lock or whether it has to go through the lock-free irq_work path instead).

You raise a fair point though, we should document all this somewhere. I believe this is the best thing we can do at this point.
The peculiarity of reactors and RV in general is that we run from tracepoints, so we could run virtually from anywhere, that's why there's no one size fits all..

Overall I'm now leaning towards using only LD_WAIT_SPIN and carefully document why we had to do this.

What do you all think?

Thanks,
Gabriele

#8 lore raw

PATCH v3 2/4 rv/reactors: propagate rv_register_reactor() error from reactor init

In reply to [email protected]

From: Wen Yang <[email protected]>

Both register_react_printk() and register_react_panic() ignore the return value of rv_register_reactor() and always return 0. If the registration fails (e.g. a duplicate reactor name), the init functions silently report success even though the reactor was not registered.

Propagate the error from rv_register_reactor() so a failed registration is reported instead of being silently ignored.

---
 kernel/trace/rv/reactor_panic.c  | 3 +--
 kernel/trace/rv/reactor_printk.c | 3 +--
 2 files changed, 2 insertions(+), 4 deletions(-)
kernel/trace/rv/reactor_panic.c +1 -2
index 76537b8a4343..db7116ceafff 100644
@@ -26,8 +26,7 @@ static struct rv_reactor rv_panic = {
 
 static int __init register_react_panic(void)
 {
-	rv_register_reactor(&rv_panic);
-	return 0;
+	return rv_register_reactor(&rv_panic);
 }
 
 static void __exit unregister_react_panic(void)
kernel/trace/rv/reactor_printk.c +1 -2
index 48c934e315b3..002a10f6aa7b 100644
@@ -25,8 +25,7 @@ static struct rv_reactor rv_printk = {
 
 static int __init register_react_printk(void)
 {
-	rv_register_reactor(&rv_printk);
-	return 0;
+	return rv_register_reactor(&rv_printk);
 }
 
 static void __exit unregister_react_printk(void)
Signature · 1 line
2.25.1
Reviewed-by on v3 2/4
#9 lore raw

Re: [PATCH v3 2/4] rv/reactors: propagate rv_register_reactor() error from reactor init

Quoted 7 lines · [email protected] writes:

From: Wen Yang <[email protected]>

Both register_react_printk() and register_react_panic() ignore the return value of rv_register_reactor() and always return 0. If the registration fails (e.g. a duplicate reactor name), the init functions silently report success even though the reactor was not registered.

Quoted 6 lines

Propagate the error from rv_register_reactor() so a failed registration is reported instead of being silently ignored.

#10 lore raw

PATCH v3 3/4 rv/reactors: export rv_register_reactor() and rv_unregister_reactor()

In reply to [email protected]

From: Wen Yang <[email protected]>

rv_react() is exported to modules, but the reactor registration helpers are not. Export them with EXPORT_SYMBOL_GPL() so reactor modules and the tristate KUnit test module can register and unregister reactors without hitting undefined symbol errors at link time(modpost).

---
 kernel/trace/rv/rv_reactors.c | 2 ++
 1 file changed, 2 insertions(+)
kernel/trace/rv/rv_reactors.c +2 -0
index 5830229210e8..5c85e3efe42e 100644
@@ -314,6 +314,7 @@ int rv_register_reactor(struct rv_reactor *reactor)
 	guard(mutex)(&rv_interface_lock);
 	return __rv_register_reactor(reactor);
 }
+EXPORT_SYMBOL_GPL(rv_register_reactor);
 
 /**
  * rv_unregister_reactor - unregister a rv reactor.
@@ -327,6 +328,7 @@ int rv_unregister_reactor(struct rv_reactor *reactor)
 	list_del(&reactor->list);
 	return 0;
 }
+EXPORT_SYMBOL_GPL(rv_unregister_reactor);
 
 /*
  * reacting_on interface.
Signature · 1 line
2.25.1
#11 lore raw

PATCH v3 4/4 rv/reactors: add KUnit tests for reactor registration and dispatch

In reply to [email protected]

From: Wen Yang <[email protected]>

Add KUnit tests covering the reactor register/unregister lifecycle (including duplicate and name-length rejection) and rv_react() dispatch (a no-op without a callback, exactly one invocation with one; the mdelay callback keeps the CPU busy so a timer interrupt exercises the LD_WAIT_SPIN lockdep context). The Kconfig entry is tristate so the tests can be built as a module when CONFIG_KUNIT=m; only RV_REACTORS is required.

---
 kernel/trace/rv/Kconfig             |  12 +++
 kernel/trace/rv/Makefile            |   1 +
 kernel/trace/rv/rv_reactors_kunit.c | 119 ++++++++++++++++++++++++++++
 3 files changed, 132 insertions(+)
 create mode 100644 kernel/trace/rv/rv_reactors_kunit.c
kernel/trace/rv/Kconfig +12 -0
index efa930f94ea4..9bfd429ffdea 100644
@@ -113,6 +113,18 @@ config RV_REACT_PANIC
 	  Enables the panic reactor. The panic reactor emits a printk()
 	  message if an exception is found and panic()s the system.
 
+config RV_REACTORS_KUNIT
+	tristate "KUnit tests for RV reactors" if !KUNIT_ALL_TESTS
+	depends on KUNIT
+	depends on RV_REACTORS
+	default KUNIT_ALL_TESTS
+	help
+	  Enable KUnit tests for RV reactor registration and dispatch.
+	  These tests verify the register/unregister lifecycle, duplicate
+	  rejection, and that rv_react() correctly invokes callbacks.
+
+	  If unsure, say N.
+
 config RV_MONITORS_KUNIT_TEST
 	tristate "KUnit tests for RV monitors" if !KUNIT_ALL_TESTS
 	depends on KUNIT && RV && RV_REACTORS
kernel/trace/rv/Makefile +1 -0
index cdbf68c84f5a..c895d81dfdad 100644
@@ -25,4 +25,5 @@ obj-$(CONFIG_RV_MON_WAKEUP) += monitors/wakeup/wakeup.o
 obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
 obj-$(CONFIG_RV_REACT_PRINTK) += reactor_printk.o
 obj-$(CONFIG_RV_REACT_PANIC) += reactor_panic.o
+obj-$(CONFIG_RV_REACTORS_KUNIT) += rv_reactors_kunit.o
 obj-$(CONFIG_RV_MONITORS_KUNIT_TEST) += rv_monitors_test.o
kernel/trace/rv/rv_reactors_kunit.c new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
2.25.1
maintainer Reviewed-by on v3 4/4
#12 lore raw

Re: [PATCH v3 4/4] rv/reactors: add KUnit tests for reactor registration and dispatch

Quoted 9 lines · On Mon, 2026-08-10 at 01:10 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Add KUnit tests covering the reactor register/unregister lifecycle (including duplicate and name-length rejection) and rv_react() dispatch (a no-op without a callback, exactly one invocation with one; the mdelay callback keeps the CPU busy so a timer interrupt exercises the LD_WAIT_SPIN lockdep context).  The Kconfig entry is tristate so the tests can be built as a module when CONFIG_KUNIT=m; only RV_REACTORS is required.

Looks simple and good now.

Quoted 1 line

Same as before, you can leave only

Thanks
Gabriele

Quoted 169 lines

---
 kernel/trace/rv/Kconfig             |  12 +++ kernel/trace/rv/Makefile            |   1 + kernel/trace/rv/rv_reactors_kunit.c | 119 ++++++++++++++++++++++++++++

 3 files changed, 132 insertions(+)

 create mode 100644 kernel/trace/rv/rv_reactors_kunit.c

kernel/trace/rv/Kconfig +0 -0
index efa930f94ea4..9bfd429ffdea 100644
@@ -113,6 +113,18 @@ config RV_REACT_PANIC
  	  Enables the panic reactor. The panic reactor emits a printk()

    message if an exception is found and panic()s the system.

kernel/trace/rv/Kconfig +12 -0
+config RV_REACTORS_KUNIT
+	tristate "KUnit tests for RV reactors" if !KUNIT_ALL_TESTS
+	depends on KUNIT
+	depends on RV_REACTORS
+	default KUNIT_ALL_TESTS
+	help
+	  Enable KUnit tests for RV reactor registration and dispatch.
+	  These tests verify the register/unregister lifecycle, duplicate
+	  rejection, and that rv_react() correctly invokes callbacks.
+
+	  If unsure, say N.
+

 config RV_MONITORS_KUNIT_TEST
  tristate "KUnit tests for RV monitors" if !KUNIT_ALL_TESTS depends on KUNIT && RV && RV_REACTORS

kernel/trace/rv/Makefile +0 -0
index cdbf68c84f5a..c895d81dfdad 100644
@@ -25,4 +25,5 @@ obj-$(CONFIG_RV_MON_WAKEUP) += monitors/wakeup/wakeup.o

 obj-$(CONFIG_RV_REACTORS) += rv_reactors.o obj-$(CONFIG_RV_REACT_PRINTK) += reactor_printk.o obj-$(CONFIG_RV_REACT_PANIC) += reactor_panic.o +obj-$(CONFIG_RV_REACTORS_KUNIT) += rv_reactors_kunit.o obj-$(CONFIG_RV_MONITORS_KUNIT_TEST) += rv_monitors_test.o

kernel/trace/rv/rv_reactors_kunit.c new file +26 -0
new file mode 100644
index 000000000000..d5b731eb11f3
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for RV reactor registration and dispatch.
+ */
+
+#include <kunit/test.h>
+#include <linux/rv.h>
+#include <linux/delay.h>
+#include <linux/atomic.h>
+#include "rv.h"
+
+static struct rv_reactor test_reactor = {
+	.name		= "kunit_test_reactor",
+	.description	= "KUnit test reactor",
+};
+
+static void reactor_teardown(void *arg)
+{
+	rv_unregister_reactor(&test_reactor);
+}
+
+static void register_test_reactor(struct kunit *test)
+{
+	KUNIT_ASSERT_EQ(test, rv_register_reactor(&test_reactor), 0);
+	KUNIT_ASSERT_EQ(test,
+			kunit_add_action_or_reset(test, reactor_teardown,

NULL), 0);

kernel/trace/rv/rv_reactors_kunit.c new file +15 -0
+}
+
+static void test_double_register(struct kunit *test)
+{
+	register_test_reactor(test);
+	KUNIT_EXPECT_EQ(test, rv_register_reactor(&test_reactor), -EINVAL);
+}
+
+/*
+ * Use a fixed-size array so sizeof() gives the exact byte count at
+ * compile time.
+ */
+static const char long_reactor_name[] = "kunit_reactor_name_too_long_xxx_";
+_Static_assert(sizeof(long_reactor_name) - 1 >= MAX_RV_REACTOR_NAME_SIZE,
+	       "long_reactor_name must be at least MAX_RV_REACTOR_NAME_SIZE

chars");

kernel/trace/rv/rv_reactors_kunit.c new file +78 -0
+
+static void test_name_too_long(struct kunit *test)
+{
+	static struct rv_reactor long_reactor = {
+		.name = long_reactor_name,
+	};
+
+	KUNIT_EXPECT_EQ(test, rv_register_reactor(&long_reactor), -EINVAL);
+}
+
+static struct kunit_case rv_reactor_registration_cases[] = {
+	KUNIT_CASE(test_double_register),
+	KUNIT_CASE(test_name_too_long),
+	{}
+};
+
+static struct kunit_suite rv_reactor_registration_suite = {
+	.name		= "rv_reactor_registration",
+	.test_cases	= rv_reactor_registration_cases,
+};
+
+static atomic_t react_call_count;
+
+__printf(1, 0) static void mock_react(const char *msg, va_list args)
+{
+	atomic_inc(&react_call_count);
+	/*
+	 * Hold the CPU for 5 ms so a timer interrupt is likely to fire
+	 * inside rv_react()'s lockdep context, exercising the LD_WAIT_SPIN
+	 * constraint.  mdelay() is a calibrated busy-wait with no scheduler
+	 * interaction.
+	 */
+	mdelay(5);
+}
+
+static void test_react_no_callback(struct kunit *test)
+{
+	struct rv_monitor monitor = {
+		.name = "kunit_null_react",
+	};
+
+	atomic_set(&react_call_count, 0);
+	rv_react(&monitor, "no callback");
+
+	/*
+	 * The only possible failure in this test case is a kernel panic.
+	 * NULL react guard: callback must NOT have been invoked
+	 */
+	KUNIT_EXPECT_EQ(test, atomic_read(&react_call_count), 0);
+}
+
+static void test_react_callback_invoked(struct kunit *test)
+{
+	struct rv_monitor monitor = {
+		.name	= "kunit_dispatch_monitor",
+		.react	= mock_react,
+	};
+
+	atomic_set(&react_call_count, 0);
+	rv_react(&monitor, "callback invocation test");
+	KUNIT_EXPECT_EQ(test, atomic_read(&react_call_count), 1);
+}
+
+static struct kunit_case rv_react_dispatch_cases[] = {
+	KUNIT_CASE(test_react_no_callback),
+	KUNIT_CASE(test_react_callback_invoked),
+	{}
+};
+
+static struct kunit_suite rv_react_dispatch_suite = {
+	.name		= "rv_react_dispatch",
+	.test_cases	= rv_react_dispatch_cases,
+};
+
+kunit_test_suites(&rv_reactor_registration_suite, &rv_react_dispatch_suite);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for RV reactor registration and dispatch");
#13 lore raw

Re: [PATCH v3 4/4] rv/reactors: add KUnit tests for reactor registration and dispatch

In reply to [email protected]
Quoted 4 lines · [email protected] writes:

+/*
+ * Use a fixed-size array so sizeof() gives the exact byte count at
+ * compile time.
+ */

There is no need for a comment. This is obvious from the code itself.

Quoted 8 lines
kernel/trace/rv/rv_reactors_kunit.c new file +8 -0
+static const char long_reactor_name[] = "kunit_reactor_name_too_long_xxx_";
+_Static_assert(sizeof(long_reactor_name) - 1 >= MAX_RV_REACTOR_NAME_SIZE,
+	       "long_reactor_name must be at least MAX_RV_REACTOR_NAME_SIZE chars");
+
+static void test_name_too_long(struct kunit *test)
+{
+	static struct rv_reactor long_reactor = {
+		.name = long_reactor_name,

kernel/trace/rv/rv_reactors_kunit.c line 56

Quoted 10 lines
kernel/trace/rv/rv_reactors_kunit.c new file +10 -0
+	};
+
+	KUNIT_EXPECT_EQ(test, rv_register_reactor(&long_reactor), -EINVAL);
+}
+
+static struct kunit_case rv_reactor_registration_cases[] = {
+	KUNIT_CASE(test_double_register),
+	KUNIT_CASE(test_name_too_long),
+	{}
+};

I am not sure about the usefulness of these test cases. rv_register_reactor() is not an user API that we have to prepare for corner case usage. We can expect its users to be sane since the users are us.

But well, if Gabriele wants to keep them..

kernel/trace/rv/rv_reactors_kunit.c line 63

Quoted 7 lines
kernel/trace/rv/rv_reactors_kunit.c new file +7 -0
+
+static struct kunit_suite rv_reactor_registration_suite = {
+	.name		= "rv_reactor_registration",
+	.test_cases	= rv_reactor_registration_cases,
+};
+
+static atomic_t react_call_count;

Do we really need atomic_t? Does int work?

kernel/trace/rv/rv_reactors_kunit.c line 73

Quoted 10 lines
kernel/trace/rv/rv_reactors_kunit.c new file +10 -0
+
+__printf(1, 0) static void mock_react(const char *msg, va_list args)
+{
+	atomic_inc(&react_call_count);
+	/*
+	 * Hold the CPU for 5 ms so a timer interrupt is likely to fire
+	 * inside rv_react()'s lockdep context, exercising the LD_WAIT_SPIN
+	 * constraint.  mdelay() is a calibrated busy-wait with no scheduler
+	 * interaction.
+	 */

The comment above mdelay()'s definition already explains what it does.

Quoted 6 lines
kernel/trace/rv/rv_reactors_kunit.c new file +6 -0
+	mdelay(5);
+}
+
+static void test_react_no_callback(struct kunit *test)
+{
+	struct rv_monitor monitor = {

kernel/trace/rv/rv_reactors_kunit.c line 89

Quoted 10 lines
kernel/trace/rv/rv_reactors_kunit.c new file +10 -0
+		.name = "kunit_null_react",
+	};
+
+	atomic_set(&react_call_count, 0);
+	rv_react(&monitor, "no callback");
+
+	/*
+	 * The only possible failure in this test case is a kernel panic.
+	 * NULL react guard: callback must NOT have been invoked
+	 */

Obvious comment.

Quoted 4 lines
kernel/trace/rv/rv_reactors_kunit.c new file +4 -0
+	KUNIT_EXPECT_EQ(test, atomic_read(&react_call_count), 0);
+}
+
+static void test_react_callback_invoked(struct kunit *test)

kernel/trace/rv/rv_reactors_kunit.c line 103

Quoted 10 lines
kernel/trace/rv/rv_reactors_kunit.c new file +10 -0
+{
+	struct rv_monitor monitor = {
+		.name	= "kunit_dispatch_monitor",
+		.react	= mock_react,
+	};
+
+	atomic_set(&react_call_count, 0);
+	rv_react(&monitor, "callback invocation test");
+	KUNIT_EXPECT_EQ(test, atomic_read(&react_call_count), 1);
+}

So the test calls rv_react(), and validates that the reactor is called? Honestly I am not sure how useful that is. Especially since Gabriele already made the selftest which validates that the reactor is invoked.

Nam

maintainer
#14 lore raw

Re: [PATCH v3 4/4] rv/reactors: add KUnit tests for reactor registration and dispatch

Quoted 33 lines · Nam Cao
Quoted 4 lines · [email protected] writes:

+/*
+ * Use a fixed-size array so sizeof() gives the exact byte count at
+ * compile time.
+ */

There is no need for a comment. This is obvious from the code itself.

Quoted 9 lines
kernel/trace/rv/rv_reactors_kunit.c new file +3 -0
+static const char long_reactor_name[] = "kunit_reactor_name_too_long_xxx_";
+_Static_assert(sizeof(long_reactor_name) - 1 >= MAX_RV_REACTOR_NAME_SIZE,
+	       "long_reactor_name must be at least MAX_RV_REACTOR_NAME_SIZE

chars");

kernel/trace/rv/rv_reactors_kunit.c new file +5 -0
+
+static void test_name_too_long(struct kunit *test)
+{
+	static struct rv_reactor long_reactor = {
+		.name = long_reactor_name,

kernel/trace/rv/rv_reactors_kunit.c line 56

Quoted 10 lines
kernel/trace/rv/rv_reactors_kunit.c new file +10 -0
+	};
+
+	KUNIT_EXPECT_EQ(test, rv_register_reactor(&long_reactor), -EINVAL);
+}
+
+static struct kunit_case rv_reactor_registration_cases[] = {
+	KUNIT_CASE(test_double_register),
+	KUNIT_CASE(test_name_too_long),
+	{}
+};

I am not sure about the usefulness of these test cases. rv_register_reactor() is not an user API that we have to prepare for corner case usage. We can expect its users to be sane since the users are us.

Quoted 2 lines

But well, if Gabriele wants to keep them..

That's right, it can be a bit of an overkill, but I'm thinking one day reactors could come from (out-of-tree) kernel modules, it wouldn't hurt to have this path tested, especially if it doesn't require much effort.

Quoted 9 lines

kernel/trace/rv/rv_reactors_kunit.c line 63

Quoted 7 lines
kernel/trace/rv/rv_reactors_kunit.c new file +7 -0
+
+static struct kunit_suite rv_reactor_registration_suite = {
+	.name		= "rv_reactor_registration",
+	.test_cases	= rv_reactor_registration_cases,
+};
+
+static atomic_t react_call_count;

Do we really need atomic_t? Does int work?

Missed that, yes, it should work just fine since KUnit runs in a single thread.

Quoted 44 lines

Review

Quoted 12 lines
kernel/trace/rv/rv_reactors_kunit.c new file +8 -0
+
+__printf(1, 0) static void mock_react(const char *msg, va_list args)
+{
+	atomic_inc(&react_call_count);
+	/*
+	 * Hold the CPU for 5 ms so a timer interrupt is likely to fire
+	 * inside rv_react()'s lockdep context, exercising the LD_WAIT_SPIN
+	 * constraint.  mdelay() is a calibrated busy-wait with no

scheduler

(quoted diff) +2 -0
+	 * interaction.
+	 */

The comment above mdelay()'s definition already explains what it does.

Quoted 6 lines
kernel/trace/rv/rv_reactors_kunit.c new file +6 -0
+	mdelay(5);
+}
+
+static void test_react_no_callback(struct kunit *test)
+{
+	struct rv_monitor monitor = {

kernel/trace/rv/rv_reactors_kunit.c line 89

Quoted 10 lines
kernel/trace/rv/rv_reactors_kunit.c new file +10 -0
+		.name = "kunit_null_react",
+	};
+
+	atomic_set(&react_call_count, 0);
+	rv_react(&monitor, "no callback");
+
+	/*
+	 * The only possible failure in this test case is a kernel panic.
+	 * NULL react guard: callback must NOT have been invoked
+	 */

Obvious comment.

Quoted 10 lines
kernel/trace/rv/rv_reactors_kunit.c new file +10 -0
+	KUNIT_EXPECT_EQ(test, atomic_read(&react_call_count), 0);
+}
+
+static void test_react_callback_invoked(struct kunit *test)
+{
+	struct rv_monitor monitor = {
+		.name	= "kunit_dispatch_monitor",
+		.react	= mock_react,
+	};
+
Quoted 8 lines

kernel/trace/rv/rv_reactors_kunit.c line 103

Quoted 4 lines
kernel/trace/rv/rv_reactors_kunit.c new file +4 -0
+	atomic_set(&react_call_count, 0);
+	rv_react(&monitor, "callback invocation test");
+	KUNIT_EXPECT_EQ(test, atomic_read(&react_call_count), 1);
+}

So the test calls rv_react(), and validates that the reactor is called? Honestly I am not sure how useful that is. Especially since Gabriele already made the selftest which validates that the reactor is invoked.

From what I understood, the value in this test suite isn't really about the assertions (especially in test_react_no_callback). It triggers cases that should not produce a splat (panic/lockdep) and a pass should consider that too. This makes them not quite pure KUnit, but again, I think it doesn't hurt to have this kind of test since KUnit output is in dmesg anyway, so a splat would hardly slip.

What do you think?

Gabriele