mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH 0/9] rv: Verify generic IRQ context transitions
@ 2026-10-22  5:07 Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events Karl Mehltretter
                   ` (8 more replies)
  0 siblings, 9 replies; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

IRQ action tracepoints describe handlers, but not the lifetime of the
enclosing IRQ context. One context may run several actions, and an action
may be dispatched outside hardirq context. PREEMPT_RT may also enter
serving-softirq state without invoking an action. This leaves no generic
event stream from which an RV monitor can check IRQ-context transitions.

This RFC adds events at the generic hardirq and serving-softirq
boundaries. The irqstate RV monitor checks their order and agreement with
live kernel state. irqexit follows the owner of a preempt_schedule_irq()
invocation across task switches and CPU migration.

The hardirq events cover paths through irq_enter() or irq_enter_rcu() and
their matching exits. They do not cover direct __irq_enter() and
__irq_exit() users, raw paths, or architecture-specific entry and return
intervals. irqexit covers preempt_schedule_irq(), not the complete
interrupt-return path.

Boundary callbacks can raise softirq work after the pending-work check.
The series wakes ksoftirqd or ktimerd for remaining work. KUnit covers
nesting, monitor synchronization, PREEMPT_RT worker intervals, provenance
and late-work handoff.

Cross-architecture RV KUnit testing also exposed an Arm-specific opid bug:

  https://lore.kernel.org/r/20261020035158.90190-1-kmehltretter@gmail.com

I would especially appreciate feedback on:

  1. Should the context boundaries be trace events or private RV
     instrumentation?
  2. Are dedicated scheduler events the right way to follow
     preempt_schedule_irq() across task switches and migration?
  3. Should irqstate and irqexit remain separate monitors?
  4. Is the KUnit-only hook for an empty ksoftirqd invocation acceptable?
     Outside a KUnit run, its worker checks take a disabled static-key
     path.

Patches 1-5 add the events, monitors, and documentation. Patches 6-9 add
KUnit coverage.

This series is based on v7.3 (c03c1a14507d) and depends on the following
accepted change, which is not included:

  softirq: Preserve interrupt context on return from interrupt
  https://lore.kernel.org/r/20261011060742.30584-1-kmehltretter@gmail.com

A pre-existing issue can make rv_test_dummy intermittently report a
false-positive invalid-wait-context warning under lockdep. Wen Yang has
posted a fix:

  https://lore.kernel.org/lkml/5586dd00789a0f904cc299cd8889744b8c6f8dc9.1789412799.git.wen.yang@linux.dev/

This series does not include that fix.

Karl Mehltretter (9):
  tracing/irq: Add generic IRQ context boundary events
  tracing/sched: Add IRQ preemption lifecycle events
  rv: Add a monitor for generic IRQ context transitions
  rv: Add a monitor for IRQ-exit scheduling ownership
  docs/rv: Document the IRQ-entry monitors
  rv: Add KUnit coverage for IRQ context transitions
  rv: Add KUnit coverage for IRQ-exit scheduling
  rv: Test empty PREEMPT_RT softirq worker intervals
  rv: Test IRQ-exit softirq provenance and handoff

 Documentation/trace/rv/index.rst              |    1 +
 Documentation/trace/rv/monitor_irqentry.rst   |  398 ++++
 include/linux/interrupt.h                     |    7 +
 include/rv/kunit.h                            |   34 +-
 include/trace/events/irq.h                    |   70 +
 include/trace/events/sched.h                  |   51 +
 kernel/sched/core.c                           |    6 +
 kernel/softirq.c                              |  308 ++-
 kernel/trace/rv/Kconfig                       |    5 +
 kernel/trace/rv/Makefile                      |    3 +
 kernel/trace/rv/monitors/irqentry/Kconfig     |   13 +
 kernel/trace/rv/monitors/irqentry/irqentry.c  |   35 +
 kernel/trace/rv/monitors/irqentry/irqentry.h  |    3 +
 kernel/trace/rv/monitors/irqexit/Kconfig      |   14 +
 kernel/trace/rv/monitors/irqexit/irqexit.c    |  172 ++
 kernel/trace/rv/monitors/irqexit/irqexit.h    |   85 +
 .../trace/rv/monitors/irqexit/irqexit_kunit.c |  537 +++++
 .../trace/rv/monitors/irqexit/irqexit_kunit.h |   21 +
 .../trace/rv/monitors/irqexit/irqexit_trace.h |   19 +
 kernel/trace/rv/monitors/irqstate/Kconfig     |   14 +
 kernel/trace/rv/monitors/irqstate/irqstate.c  |  289 +++
 kernel/trace/rv/monitors/irqstate/irqstate.h  |  139 ++
 .../rv/monitors/irqstate/irqstate_kunit.c     | 2062 +++++++++++++++++
 .../rv/monitors/irqstate/irqstate_kunit.h     |   25 +
 .../rv/monitors/irqstate/irqstate_trace.h     |   15 +
 kernel/trace/rv/rv_monitors_test.c            |   29 +
 kernel/trace/rv/rv_trace.h                    |    2 +
 .../verification/models/irqentry/irqexit.dot  |   21 +
 .../verification/models/irqentry/irqstate.dot |   32 +
 29 files changed, 4397 insertions(+), 13 deletions(-)
 create mode 100644 Documentation/trace/rv/monitor_irqentry.rst
 create mode 100644 kernel/trace/rv/monitors/irqentry/Kconfig
 create mode 100644 kernel/trace/rv/monitors/irqentry/irqentry.c
 create mode 100644 kernel/trace/rv/monitors/irqentry/irqentry.h
 create mode 100644 kernel/trace/rv/monitors/irqexit/Kconfig
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit.c
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit.h
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_kunit.h
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_trace.h
 create mode 100644 kernel/trace/rv/monitors/irqstate/Kconfig
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate.c
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate.h
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate_kunit.h
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate_trace.h
 create mode 100644 tools/verification/models/irqentry/irqexit.dot
 create mode 100644 tools/verification/models/irqentry/irqstate.dot

base-commit: c03c1a14507dfaf8b856c2608f9ac216021fc222
prerequisite-patch-id: 6ac7167f0aa7c5319dac78bf5b06a989fd7dfac5
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:44   ` sashiko-bot
  2026-10-22  5:07 ` [RFC PATCH 2/9] tracing/sched: Add IRQ preemption lifecycle events Karl Mehltretter
                   ` (7 subsequent siblings)
  8 siblings, 1 reply; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

IRQ action tracepoints describe handlers, not the enclosing interrupt
context. One hardirq context may run several actions. Synthetic dispatch
may also invoke an action outside hardirq context.

Add events at the generic hardirq and serving-softirq boundaries. Record
the hardirq depth and whether serving-softirq state temporarily replaces
hardirq state during interrupt return.

A callback attached to a boundary event can raise softirq work after the
normal pending-work check. Include work raised by the softirq-entry
callback in the initial pending snapshot. Wake ksoftirqd or ktimerd for
work raised by a final boundary callback.

The events cover paths through irq_enter()/irq_exit() or
irq_enter_rcu()/irq_exit_rcu(). Direct __irq_enter()/__irq_exit() users
and raw paths remain outside their scope.

Assisted-by: LLM
---
 include/trace/events/irq.h | 70 ++++++++++++++++++++++++++++++++
 kernel/softirq.c           | 83 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 151 insertions(+), 2 deletions(-)

diff --git a/include/trace/events/irq.h b/include/trace/events/irq.h
index 837c1740d0d07..0549fbcdbb1b6 100644
--- a/include/trace/events/irq.h
+++ b/include/trace/events/irq.h
@@ -39,6 +39,76 @@ SOFTIRQ_NAME_LIST
 #define show_softirq_name(val)				\
 	__print_symbolic(val, SOFTIRQ_NAME_LIST)
 
+DECLARE_EVENT_CLASS(hardirq_context,
+		    TP_PROTO(unsigned int depth),
+		    TP_ARGS(depth),
+		    TP_STRUCT__entry(__field(unsigned int, depth)),
+		    TP_fast_assign(__entry->depth = depth;),
+		    TP_printk("depth=%u", __entry->depth));
+
+/**
+ * hardirq_context_entry - generic hardirq state has been established
+ * @depth: hardirq nesting depth, including the context being entered
+ *
+ * This event is emitted after generic hardirq preempt-count, lockdep, timer,
+ * and accounting state has been established. It does not describe the
+ * architecture-specific interval before irq_enter() or irq_enter_rcu().
+ */
+DEFINE_EVENT(hardirq_context, hardirq_context_entry,
+	     TP_PROTO(unsigned int depth),
+	     TP_ARGS(depth));
+
+/**
+ * hardirq_context_exit - generic hardirq state is about to be removed
+ * @depth: hardirq nesting depth, including the context being exited
+ *
+ * This event is emitted with interrupts disabled immediately before generic
+ * hardirq preempt-count state is removed. Hardirq accounting ends earlier.
+ */
+DEFINE_EVENT(hardirq_context, hardirq_context_exit,
+	     TP_PROTO(unsigned int depth),
+	     TP_ARGS(depth));
+
+DECLARE_EVENT_CLASS(softirq_context,
+		    TP_PROTO(bool serving, bool from_hardirq),
+		    TP_ARGS(serving, from_hardirq),
+		    TP_STRUCT__entry(
+			    __field(bool, serving)
+			    __field(bool, from_hardirq)
+		    ),
+		    TP_fast_assign(
+			    __entry->serving = serving;
+			    __entry->from_hardirq = from_hardirq;
+		    ),
+		    TP_printk("serving=%d from_hardirq=%d",
+			      __entry->serving, __entry->from_hardirq));
+
+/**
+ * softirq_context_entry - serving-softirq state has been established
+ * @serving: whether the current task is serving a softirq
+ * @from_hardirq: whether hardirq state was replaced by serving-softirq state
+ *
+ * This event runs with local interrupts disabled after the serving bit is
+ * set. On PREEMPT_RT it includes preparation before handle_softirqs(), even
+ * if the pending-work check finds nothing. It does not mark an individual
+ * action or the start of CPU-time accounting.
+ */
+DEFINE_EVENT(softirq_context, softirq_context_entry,
+	     TP_PROTO(bool serving, bool from_hardirq),
+	     TP_ARGS(serving, from_hardirq));
+
+/**
+ * softirq_context_exit - serving-softirq state is about to be removed
+ * @serving: whether the current task is serving a softirq
+ * @from_hardirq: whether hardirq state will replace serving-softirq state
+ *
+ * This event runs with local interrupts disabled before the serving bit is
+ * cleared. On PREEMPT_RT this includes teardown after handle_softirqs().
+ */
+DEFINE_EVENT(softirq_context, softirq_context_exit,
+	     TP_PROTO(bool serving, bool from_hardirq),
+	     TP_ARGS(serving, from_hardirq));
+
 /**
  * irq_handler_entry - called immediately before the irq action handler
  * @irq: irq number
diff --git a/kernel/softirq.c b/kernel/softirq.c
index be49e6469f3ad..2e3b6fd3f6d5d 100644
--- a/kernel/softirq.c
+++ b/kernel/softirq.c
@@ -178,7 +178,9 @@ bool local_bh_blocked(void)
 
 void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
 {
+	unsigned long trace_flags = 0;
 	unsigned long flags;
+	bool trace_serving;
 	int newcnt;
 
 	WARN_ON_ONCE(in_hardirq());
@@ -200,6 +202,16 @@ void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
 		}
 	}
 
+	/*
+	 * A serving interval starts here, before handle_softirqs(). When its
+	 * entry is traced, keep IRQs and task switches from observing the bit
+	 * before the event. Any blocking BH lock was acquired above.
+	 */
+	trace_serving = (cnt & SOFTIRQ_OFFSET) &&
+			trace_softirq_context_entry_enabled();
+	if (trace_serving)
+		local_irq_save(trace_flags);
+
 	/*
 	 * Track the per CPU softirq disabled state. On RT this is per CPU
 	 * state to allow preemption of bottom half disabled sections.
@@ -233,15 +245,27 @@ void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
 			raw_local_irq_restore(flags);
 		}
 	}
+
+	if (trace_serving) {
+		trace_softirq_context_entry(!!in_serving_softirq(), false);
+		local_irq_restore(trace_flags);
+	}
 }
 EXPORT_SYMBOL(__local_bh_disable_ip);
 
 static void __local_bh_enable(unsigned int cnt, bool unlock)
 {
+	bool entering = (cnt & SOFTIRQ_OFFSET) && !in_serving_softirq();
+	bool trace_exiting = (cnt & SOFTIRQ_OFFSET) && !entering &&
+			     trace_softirq_context_exit_enabled();
 	unsigned long flags;
 	bool sirq_en = false;
 	int newcnt;
 
+	/* Callers already disabled IRQs around the counter and its events. */
+	if (trace_exiting)
+		trace_softirq_context_exit(!!in_serving_softirq(), false);
+
 	if (IS_ENABLED(CONFIG_PREEMPT_RT_NEEDS_BH_LOCK)) {
 		DEBUG_LOCKS_WARN_ON(current->softirq_disable_cnt !=
 				    this_cpu_read(softirq_ctrl.cnt));
@@ -276,6 +300,18 @@ static void __local_bh_enable(unsigned int cnt, bool unlock)
 			WARN_ON_ONCE(current->softirq_disable_cnt < 0);
 		}
 	}
+
+	/*
+	 * An exit probe can raise a softirq while serving state still suppresses
+	 * the normal ksoftirqd wakeup. Hand it off once no outer BH-disable
+	 * reference remains.
+	 */
+	if (trace_exiting && sirq_en && unlikely(local_softirq_pending()))
+		wakeup_softirqd();
+
+	/* local_bh_enable() can turn BH-disabled state into serving state. */
+	if (entering)
+		trace_softirq_context_entry(!!in_serving_softirq(), false);
 }
 
 void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
@@ -640,11 +676,15 @@ static void handle_softirqs(bool ksirqd)
 	 */
 	current->flags &= ~PF_MEMALLOC;
 
-	pending = local_softirq_pending();
-
 	from_irq_exit = softirq_handle_begin();
 	in_hardirq = lockdep_softirq_start();
 	account_softirq_enter(current);
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT))
+		trace_softirq_context_entry(!!in_serving_softirq(),
+					    from_irq_exit);
+
+	/* Include work raised by the context-entry event. */
+	pending = local_softirq_pending();
 
 restart:
 	/* Reset the pending bitmask before enabling irqs */
@@ -692,6 +732,14 @@ restart:
 		wakeup_softirqd();
 	}
 
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT) &&
+	    trace_softirq_context_exit_enabled()) {
+		trace_softirq_context_exit(!!in_serving_softirq(),
+					   from_irq_exit);
+		/* The final pending-work decision above has already completed. */
+		if (unlikely(local_softirq_pending()))
+			wakeup_softirqd();
+	}
 	account_softirq_exit(current);
 	lockdep_softirq_end(in_hardirq);
 	softirq_handle_end(from_irq_exit);
@@ -722,6 +770,7 @@ void irq_enter_rcu(void)
 		tick_irq_enter();
 
 	account_hardirq_enter(current);
+	trace_hardirq_context_entry(hardirq_count() >> HARDIRQ_SHIFT);
 }
 
 /**
@@ -796,6 +845,36 @@ static inline void __irq_exit_rcu(void)
 		wake_timersd();
 
 	tick_irq_exit();
+	if (trace_hardirq_context_exit_enabled()) {
+		bool ksoftirqd_wake_requested = false;
+		bool timersd_wake_requested = false;
+
+		trace_hardirq_context_exit(hardirq_count() >> HARDIRQ_SHIFT);
+
+		/* Hand work raised by the final boundary event to its worker. */
+		if (hardirq_count() == HARDIRQ_OFFSET) {
+			if (irq_count() == HARDIRQ_OFFSET &&
+			    unlikely(local_softirq_pending()) &&
+			    should_wake_ksoftirqd()) {
+				wakeup_softirqd();
+				ksoftirqd_wake_requested = true;
+			}
+
+			if (IS_ENABLED(CONFIG_IRQ_FORCED_THREADING) &&
+			    force_irqthreads() && local_timers_pending_force_th()) {
+				wake_timersd();
+				timersd_wake_requested = true;
+			}
+
+			/* The timersd wakeup can itself raise a softirq. */
+			if (timersd_wake_requested &&
+			    !ksoftirqd_wake_requested &&
+			    irq_count() == HARDIRQ_OFFSET &&
+			    unlikely(local_softirq_pending()) &&
+			    should_wake_ksoftirqd())
+				wakeup_softirqd();
+		}
+	}
 	preempt_count_sub(HARDIRQ_OFFSET);
 }
 
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 2/9] tracing/sched: Add IRQ preemption lifecycle events
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

Tracepoints around __schedule() do not identify the boundaries of one
preempt_schedule_irq() invocation. The calling task may switch out and
resume on another CPU before the invocation returns.

Add events at entry to and return from preempt_schedule_irq(). Record the
calling task as the owner in both events so consumers can follow the
invocation across switches and migration. Sample the preempt count and
interrupt state before dispatching the callbacks.

The events bracket preempt_schedule_irq() only. They do not cover
architecture-specific interrupt-return work or establish that the helper
was reached from a physical interrupt.

Assisted-by: LLM
---
 include/trace/events/sched.h | 51 ++++++++++++++++++++++++++++++++++++
 kernel/sched/core.c          |  6 +++++
 2 files changed, 57 insertions(+)

diff --git a/include/trace/events/sched.h b/include/trace/events/sched.h
index 535860581f159..d0f510c35b02b 100644
--- a/include/trace/events/sched.h
+++ b/include/trace/events/sched.h
@@ -52,6 +52,57 @@ TRACE_EVENT(sched_kthread_stop_ret,
 	TP_printk("ret=%d", __entry->ret)
 );
 
+DECLARE_EVENT_CLASS(sched_irq_preempt,
+
+	TP_PROTO(struct task_struct *owner, unsigned long pc, bool irq_off),
+
+	TP_ARGS(owner, pc, irq_off),
+
+	TP_STRUCT__entry(
+		__array(char, owner_comm, TASK_COMM_LEN)
+		__field(pid_t, owner_pid)
+		__field(unsigned long, pc)
+		__field(bool, irq_off)
+	),
+
+	TP_fast_assign(
+		memcpy(__entry->owner_comm, owner->comm, TASK_COMM_LEN);
+		__entry->owner_pid = owner->pid;
+		__entry->pc = pc;
+		__entry->irq_off = irq_off;
+	),
+
+	TP_printk("owner=%s/%d pc=0x%lx irq_off=%d",
+		  __entry->owner_comm, __entry->owner_pid,
+		  __entry->pc, __entry->irq_off)
+);
+
+/**
+ * sched_irq_preempt_entry - begin preempt_schedule_irq() processing
+ * @owner: task whose interrupted execution will resume after scheduling
+ * @pc: preempt count sampled at the call site, before tracing bookkeeping
+ * @irq_off: interrupt state sampled at the same call site
+ *
+ * Emitted after entry assertions and context-tracking setup, with zero
+ * preempt count and local interrupts disabled. Exception-return paths can
+ * also use this scheduler entrypoint.
+ */
+DEFINE_EVENT(sched_irq_preempt, sched_irq_preempt_entry,
+	     TP_PROTO(struct task_struct *owner, unsigned long pc, bool irq_off),
+	     TP_ARGS(owner, pc, irq_off));
+
+/**
+ * sched_irq_preempt_exit - original preempt_schedule_irq() invocation resumes
+ * @owner: task saved by the matching entry, possibly on a different CPU
+ * @pc: preempt count sampled at the call site, before tracing bookkeeping
+ * @irq_off: interrupt state sampled at the same call site
+ *
+ * Emitted with local interrupts disabled before restoring context tracking.
+ */
+DEFINE_EVENT(sched_irq_preempt, sched_irq_preempt_exit,
+	     TP_PROTO(struct task_struct *owner, unsigned long pc, bool irq_off),
+	     TP_ARGS(owner, pc, irq_off));
+
 /**
  * sched_kthread_work_queue_work - called when a work gets queued
  * @worker:	pointer to the kthread_worker
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 1754d9f95f2cf..b971a373ea3a1 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -7593,12 +7593,16 @@ EXPORT_SYMBOL(dynamic_preempt_schedule_notrace);
  */
 asmlinkage __visible void __sched preempt_schedule_irq(void)
 {
+	struct task_struct *owner = current;
 	enum ctx_state prev_state;
 
 	/* Catch callers which need to be fixed */
 	BUG_ON(preempt_count() || !irqs_disabled());
 
 	prev_state = exception_enter();
+	/* Sample before tracing bookkeeping can change the observed context. */
+	if (trace_sched_irq_preempt_entry_enabled())
+		trace_sched_irq_preempt_entry(owner, preempt_count(), irqs_disabled());
 
 	do {
 		preempt_disable();
@@ -7608,6 +7612,8 @@ asmlinkage __visible void __sched preempt_schedule_irq(void)
 		sched_preempt_enable_no_resched();
 	} while (need_resched());
 
+	if (trace_sched_irq_preempt_exit_enabled())
+		trace_sched_irq_preempt_exit(owner, preempt_count(), irqs_disabled());
 	exception_exit(prev_state);
 }
 
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 3/9] rv: Add a monitor for generic IRQ context transitions
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 2/9] tracing/sched: Add IRQ preemption lifecycle events Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

The generic IRQ boundary events expose state transitions but do not check
their order or agreement with live kernel state.

Add the irqentry monitor group and its irqstate hybrid automaton. The
monitor checks generic hardirq and serving-softirq nesting against live
kernel state. On PREEMPT_RT, it suspends and resumes task-local softirq
state across sched_switch().

The monitor observes only the generic boundary events. It does not cover
NMI entry, architecture-specific intervals before generic IRQ entry or
after generic IRQ exit, or paths that bypass the generic helpers.

The generated model header is reproducible from the included DOT file
with the unmodified rvgen tool.

Assisted-by: LLM
---
 kernel/trace/rv/Kconfig                       |   4 +
 kernel/trace/rv/Makefile                      |   2 +
 kernel/trace/rv/monitors/irqentry/Kconfig     |  13 +
 kernel/trace/rv/monitors/irqentry/irqentry.c  |  35 +++
 kernel/trace/rv/monitors/irqentry/irqentry.h  |   3 +
 kernel/trace/rv/monitors/irqstate/Kconfig     |  14 +
 kernel/trace/rv/monitors/irqstate/irqstate.c  | 240 ++++++++++++++++++
 kernel/trace/rv/monitors/irqstate/irqstate.h  | 139 ++++++++++
 .../rv/monitors/irqstate/irqstate_trace.h     |  15 ++
 kernel/trace/rv/rv_trace.h                    |   1 +
 .../verification/models/irqentry/irqstate.dot |  32 +++
 11 files changed, 498 insertions(+)
 create mode 100644 kernel/trace/rv/monitors/irqentry/Kconfig
 create mode 100644 kernel/trace/rv/monitors/irqentry/irqentry.c
 create mode 100644 kernel/trace/rv/monitors/irqentry/irqentry.h
 create mode 100644 kernel/trace/rv/monitors/irqstate/Kconfig
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate.c
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate.h
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate_trace.h
 create mode 100644 tools/verification/models/irqentry/irqstate.dot

diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index efa930f94ea4a..e7b30668a559b 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -73,6 +73,10 @@ source "kernel/trace/rv/monitors/sssw/Kconfig"
 source "kernel/trace/rv/monitors/opid/Kconfig"
 # Add new sched monitors here
 
+source "kernel/trace/rv/monitors/irqentry/Kconfig"
+source "kernel/trace/rv/monitors/irqstate/Kconfig"
+# Add new irqentry monitors here
+
 source "kernel/trace/rv/monitors/rtapp/Kconfig"
 source "kernel/trace/rv/monitors/pagefault/Kconfig"
 source "kernel/trace/rv/monitors/sleep/Kconfig"
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index cdbf68c84f5a4..f39a9f8815764 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -17,6 +17,8 @@ obj-$(CONFIG_RV_MON_STS) += monitors/sts/sts.o
 obj-$(CONFIG_RV_MON_NRP) += monitors/nrp/nrp.o
 obj-$(CONFIG_RV_MON_SSSW) += monitors/sssw/sssw.o
 obj-$(CONFIG_RV_MON_OPID) += monitors/opid/opid.o
+obj-$(CONFIG_RV_MON_IRQENTRY) += monitors/irqentry/irqentry.o
+obj-$(CONFIG_RV_MON_IRQSTATE) += monitors/irqstate/irqstate.o
 obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o
 obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o
 obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o
diff --git a/kernel/trace/rv/monitors/irqentry/Kconfig b/kernel/trace/rv/monitors/irqentry/Kconfig
new file mode 100644
index 0000000000000..c4476650cd930
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqentry/Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+config RV_MON_IRQENTRY
+	depends on RV
+	bool "irqentry monitor"
+	help
+	  Enable the container for monitors which check IRQ-entry execution
+	  context and ordering. Individual monitors can compare hardirq,
+	  softirq, and scheduler events with the kernel's recorded execution
+	  context.
+
+	  For further information, see:
+	    Documentation/trace/rv/monitor_irqentry.rst
diff --git a/kernel/trace/rv/monitors/irqentry/irqentry.c b/kernel/trace/rv/monitors/irqentry/irqentry.c
new file mode 100644
index 0000000000000..4029ef14389b9
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqentry/irqentry.c
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/rv.h>
+
+#define MODULE_NAME "irqentry"
+
+#include "irqentry.h"
+
+struct rv_monitor rv_irqentry = {
+	.name = "irqentry",
+	.description = "IRQ-entry context monitors.",
+	.enable = NULL,
+	.disable = NULL,
+	.reset = NULL,
+	.enabled = 0,
+};
+
+static int __init register_irqentry(void)
+{
+	return rv_register_monitor(&rv_irqentry, NULL);
+}
+
+static void __exit unregister_irqentry(void)
+{
+	rv_unregister_monitor(&rv_irqentry);
+}
+
+module_init(register_irqentry);
+module_exit(unregister_irqentry);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Karl Mehltretter <kmehltretter@gmail.com>");
+MODULE_DESCRIPTION("irqentry: IRQ-entry context monitors.");
diff --git a/kernel/trace/rv/monitors/irqentry/irqentry.h b/kernel/trace/rv/monitors/irqentry/irqentry.h
new file mode 100644
index 0000000000000..a7d72483522d2
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqentry/irqentry.h
@@ -0,0 +1,3 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+extern struct rv_monitor rv_irqentry;
diff --git a/kernel/trace/rv/monitors/irqstate/Kconfig b/kernel/trace/rv/monitors/irqstate/Kconfig
new file mode 100644
index 0000000000000..bf43b9a4d64fd
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqstate/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+config RV_MON_IRQSTATE
+	depends on RV
+	depends on RV_MON_IRQENTRY
+	default y
+	select HA_MON_EVENTS_IMPLICIT
+	bool "irqstate monitor"
+	help
+	  Verify that generic hardirq and softirq context boundaries agree
+	  with the execution context recorded in preempt count.
+
+	  For further information, see:
+	    Documentation/trace/rv/monitor_irqentry.rst
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate.c b/kernel/trace/rv/monitors/irqstate/irqstate.c
new file mode 100644
index 0000000000000..2d8c824a4870a
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqstate/irqstate.c
@@ -0,0 +1,240 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/ftrace.h>
+#include <linux/hardirq.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/rcupdate.h>
+#include <linux/rv.h>
+#include <linux/tracepoint.h>
+#include <rv/instrumentation.h>
+
+#define MODULE_NAME "irqstate"
+
+#include <trace/events/irq.h>
+#include <trace/events/sched.h>
+#include <rv_trace.h>
+#include <monitors/irqentry/irqentry.h>
+
+#define RV_MON_TYPE RV_MON_PER_CPU
+#include "irqstate.h"
+#include <rv/ha_monitor.h>
+
+static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_irqstate env,
+		      u64 time_ns)
+{
+	switch (env) {
+	case hardirq_irqstate:
+		return hardirq_count();
+	case serving_softirq_irqstate:
+		return !!in_serving_softirq();
+	default:
+		return ENV_INVALID_VALUE;
+	}
+}
+
+static inline bool ha_verify_guards(struct ha_monitor *ha_mon,
+				    enum states curr_state, enum events event,
+				    enum states next_state, u64 time_ns)
+{
+	bool res = true;
+
+	if (curr_state == in_hardirq_irqstate && event == hardirq_exit_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) > 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 0;
+	else if (curr_state == in_hardirq_irqstate &&
+		 event == softirq_entry_from_hardirq_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) == 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+	else if (curr_state == idle_irqstate && event == hardirq_entry_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) > 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 0;
+	else if (curr_state == idle_irqstate && event == softirq_entry_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) == 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+	else if (curr_state == idle_irqstate && event == softirq_resume_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) == 0;
+	else if (curr_state == in_softirq_irqstate && event == hardirq_entry_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) > 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+	else if (curr_state == in_softirq_irqstate && event == softirq_exit_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) == 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+	else if (curr_state == in_softirq_irqstate && event == softirq_suspend_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) == 0;
+	else if (curr_state == hardirq_from_softirq_irqstate &&
+		 event == hardirq_exit_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) > 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+	else if (curr_state == softirq_from_hardirq_irqstate &&
+		 event == hardirq_entry_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) > 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+	else if (curr_state == softirq_from_hardirq_irqstate &&
+		 event == softirq_exit_to_hardirq_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) == 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+	else if (curr_state == hardirq_from_softirq_from_hardirq_irqstate &&
+		 event == hardirq_exit_irqstate)
+		res = ha_get_env(ha_mon, hardirq_irqstate, time_ns) > 0 &&
+		      ha_get_env(ha_mon, serving_softirq_irqstate, time_ns) == 1;
+
+	return res;
+}
+
+static bool ha_verify_constraint(struct ha_monitor *ha_mon,
+				 enum states curr_state, enum events event,
+				 enum states next_state, u64 time_ns)
+{
+	return ha_verify_guards(ha_mon, curr_state, event, next_state, time_ns);
+}
+
+static void handle_hardirq_entry(void *data, unsigned int depth)
+{
+	if (!hardirq_count()) {
+		da_handle_start_run_event(hardirq_entry_irqstate);
+		return;
+	}
+
+	/* Nested hardirqs leave the outer context state unchanged. */
+	if (depth > 1)
+		return;
+
+	if (in_serving_softirq())
+		da_handle_event(hardirq_entry_irqstate);
+	else
+		da_handle_start_run_event(hardirq_entry_irqstate);
+}
+
+static void handle_hardirq_exit(void *data, unsigned int depth)
+{
+	if (!hardirq_count()) {
+		da_handle_start_run_event(hardirq_exit_irqstate);
+		return;
+	}
+
+	/* Nested hardirqs leave the outer context state unchanged. */
+	if (depth > 1)
+		return;
+
+	if (in_serving_softirq())
+		da_handle_event(hardirq_exit_irqstate);
+	else
+		da_handle_start_event(hardirq_exit_irqstate);
+}
+
+static void handle_softirq_entry(void *data, bool serving, bool from_hardirq)
+{
+	if (from_hardirq)
+		da_handle_event(softirq_entry_from_hardirq_irqstate);
+	else
+		da_handle_start_run_event(softirq_entry_irqstate);
+}
+
+static void handle_softirq_exit(void *data, bool serving, bool from_hardirq)
+{
+	if (from_hardirq) {
+		da_handle_event(softirq_exit_to_hardirq_irqstate);
+		return;
+	}
+
+	if (in_serving_softirq())
+		da_handle_start_event(softirq_exit_irqstate);
+	else
+		da_handle_start_run_event(softirq_exit_irqstate);
+}
+
+#ifdef CONFIG_PREEMPT_RT
+static void handle_sched_switch(void *data, bool preempt,
+				struct task_struct *prev,
+				struct task_struct *next,
+				unsigned int prev_state)
+{
+	/*
+	 * Serving-softirq state belongs to a task on RT. At this tracepoint
+	 * interrupts are disabled and current still names prev. Suspend its
+	 * context before representing the context of the incoming task.
+	 */
+	if (READ_ONCE(prev->softirq_disable_cnt) & SOFTIRQ_OFFSET)
+		da_handle_start_event(softirq_suspend_irqstate);
+	if (READ_ONCE(next->softirq_disable_cnt) & SOFTIRQ_OFFSET)
+		da_handle_start_run_event(softirq_resume_irqstate);
+}
+#endif
+
+static void attach_irqstate(void)
+{
+	rv_attach_trace_probe("irqstate", hardirq_context_entry, handle_hardirq_entry);
+	rv_attach_trace_probe("irqstate", hardirq_context_exit, handle_hardirq_exit);
+	rv_attach_trace_probe("irqstate", softirq_context_entry, handle_softirq_entry);
+	rv_attach_trace_probe("irqstate", softirq_context_exit, handle_softirq_exit);
+#ifdef CONFIG_PREEMPT_RT
+	rv_attach_trace_probe("irqstate", sched_switch, handle_sched_switch);
+	/*
+	 * Drain BH preparations which sampled the entry tracepoint disabled.
+	 * Their RCU read-side protection covers that sample and the serving
+	 * interval. Do not enable the monitor while one can still publish a
+	 * serving bit without its entry event after an IRQ synchronizes idle.
+	 */
+	synchronize_rcu();
+#endif
+}
+
+static void detach_irqstate(void)
+{
+	rv_detach_trace_probe("irqstate", hardirq_context_entry, handle_hardirq_entry);
+	rv_detach_trace_probe("irqstate", hardirq_context_exit, handle_hardirq_exit);
+	rv_detach_trace_probe("irqstate", softirq_context_entry, handle_softirq_entry);
+	rv_detach_trace_probe("irqstate", softirq_context_exit, handle_softirq_exit);
+#ifdef CONFIG_PREEMPT_RT
+	rv_detach_trace_probe("irqstate", sched_switch, handle_sched_switch);
+#endif
+}
+
+static int enable_irqstate(void)
+{
+	int retval;
+
+	retval = ha_monitor_init();
+	if (retval)
+		return retval;
+
+	attach_irqstate();
+
+	return 0;
+}
+
+static void disable_irqstate(void)
+{
+	rv_this.enabled = 0;
+
+	detach_irqstate();
+
+	ha_monitor_destroy();
+}
+
+static struct rv_monitor rv_this = {
+	.name = "irqstate",
+	.description = "IRQ context boundaries agree with execution state.",
+	.enable = enable_irqstate,
+	.disable = disable_irqstate,
+	.reset = da_monitor_reset_all,
+	.enabled = 0,
+};
+
+static int __init register_irqstate(void)
+{
+	return rv_register_monitor(&rv_this, &rv_irqentry);
+}
+
+static void __exit unregister_irqstate(void)
+{
+	rv_unregister_monitor(&rv_this);
+}
+
+module_init(register_irqstate);
+module_exit(unregister_irqstate);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Karl Mehltretter <kmehltretter@gmail.com>");
+MODULE_DESCRIPTION("irqstate: verify IRQ event and execution context agreement");
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate.h b/kernel/trace/rv/monitors/irqstate/irqstate.h
new file mode 100644
index 0000000000000..2459f579c4696
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqstate/irqstate.h
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Automatically generated C representation of irqstate automaton
+ * For further information about this format, see kernel documentation:
+ *   Documentation/trace/rv/deterministic_automata.rst
+ */
+
+#define MONITOR_NAME irqstate
+
+enum states_irqstate {
+	idle_irqstate,
+	hardirq_from_softirq_irqstate,
+	hardirq_from_softirq_from_hardirq_irqstate,
+	in_hardirq_irqstate,
+	in_softirq_irqstate,
+	softirq_from_hardirq_irqstate,
+	state_max_irqstate,
+};
+
+#define INVALID_STATE state_max_irqstate
+
+enum events_irqstate {
+	hardirq_entry_irqstate,
+	hardirq_exit_irqstate,
+	softirq_entry_irqstate,
+	softirq_entry_from_hardirq_irqstate,
+	softirq_exit_irqstate,
+	softirq_exit_to_hardirq_irqstate,
+	softirq_resume_irqstate,
+	softirq_suspend_irqstate,
+	event_max_irqstate,
+};
+
+enum envs_irqstate {
+	hardirq_irqstate,
+	serving_softirq_irqstate,
+	env_max_irqstate,
+	env_max_stored_irqstate = hardirq_irqstate,
+};
+
+_Static_assert(env_max_stored_irqstate <= MAX_HA_ENV_LEN, "Not enough slots");
+
+struct automaton_irqstate {
+	char *state_names[state_max_irqstate];
+	char *event_names[event_max_irqstate];
+	char *env_names[env_max_irqstate];
+	unsigned char function[state_max_irqstate][event_max_irqstate];
+	unsigned char initial_state;
+	bool final_states[state_max_irqstate];
+};
+
+static const struct automaton_irqstate automaton_irqstate = {
+	.state_names = {
+		"idle",
+		"hardirq_from_softirq",
+		"hardirq_from_softirq_from_hardirq",
+		"in_hardirq",
+		"in_softirq",
+		"softirq_from_hardirq",
+	},
+	.event_names = {
+		"hardirq_entry",
+		"hardirq_exit",
+		"softirq_entry",
+		"softirq_entry_from_hardirq",
+		"softirq_exit",
+		"softirq_exit_to_hardirq",
+		"softirq_resume",
+		"softirq_suspend",
+	},
+	.env_names = {
+		"hardirq",
+		"serving_softirq",
+	},
+	.function = {
+		{
+			in_hardirq_irqstate,
+			INVALID_STATE,
+			in_softirq_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			in_softirq_irqstate,
+			INVALID_STATE,
+		},
+		{
+			INVALID_STATE,
+			in_softirq_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+		},
+		{
+			INVALID_STATE,
+			softirq_from_hardirq_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+		},
+		{
+			INVALID_STATE,
+			idle_irqstate,
+			INVALID_STATE,
+			softirq_from_hardirq_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+		},
+		{
+			hardirq_from_softirq_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			idle_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+			idle_irqstate,
+		},
+		{
+			hardirq_from_softirq_from_hardirq_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			INVALID_STATE,
+			in_hardirq_irqstate,
+			INVALID_STATE,
+			INVALID_STATE,
+		},
+	},
+	.initial_state = idle_irqstate,
+	.final_states = { 1, 0, 0, 0, 0, 0 },
+};
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_trace.h b/kernel/trace/rv/monitors/irqstate/irqstate_trace.h
new file mode 100644
index 0000000000000..43de706dda7b6
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_trace.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifdef CONFIG_RV_MON_IRQSTATE
+DEFINE_EVENT(event_da_monitor, event_irqstate,
+	     TP_PROTO(char *state, char *event, char *next_state, bool final_state),
+	     TP_ARGS(state, event, next_state, final_state));
+
+DEFINE_EVENT(error_da_monitor, error_irqstate,
+	     TP_PROTO(char *state, char *event),
+	     TP_ARGS(state, event));
+
+DEFINE_EVENT(error_env_da_monitor, error_env_irqstate,
+	     TP_PROTO(char *state, char *event, char *env),
+	     TP_ARGS(state, event, env));
+#endif /* CONFIG_RV_MON_IRQSTATE */
diff --git a/kernel/trace/rv/rv_trace.h b/kernel/trace/rv/rv_trace.h
index 2f8a932432c95..040b3635ef289 100644
--- a/kernel/trace/rv/rv_trace.h
+++ b/kernel/trace/rv/rv_trace.h
@@ -91,6 +91,7 @@ DECLARE_EVENT_CLASS(error_env_da_monitor,
 );
 
 #include <monitors/opid/opid_trace.h>
+#include <monitors/irqstate/irqstate_trace.h>
 // Add new monitors based on CONFIG_HA_MON_EVENTS_IMPLICIT here
 
 #endif
diff --git a/tools/verification/models/irqentry/irqstate.dot b/tools/verification/models/irqentry/irqstate.dot
new file mode 100644
index 0000000000000..90c41e8909e4e
--- /dev/null
+++ b/tools/verification/models/irqentry/irqstate.dot
@@ -0,0 +1,32 @@
+digraph state_automaton {
+	center = true;
+	size = "7,11";
+	{node [shape = plaintext, style=invis, label=""] "__init_idle"};
+	{node [shape = doublecircle] "idle"};
+	{node [shape = circle] "in_hardirq"};
+	{node [shape = circle] "in_softirq"};
+	{node [shape = circle] "hardirq_from_softirq"};
+	{node [shape = circle] "softirq_from_hardirq"};
+	{node [shape = circle] "hardirq_from_softirq_from_hardirq"};
+	"__init_idle" -> "idle";
+	"idle" [label = "idle", color = green3];
+	"idle" -> "in_hardirq" [ label = "hardirq_entry;hardirq > 0 && serving_softirq == 0" ];
+	"idle" -> "in_softirq" [ label = "softirq_entry;hardirq == 0 && serving_softirq == 1\nsoftirq_resume;hardirq == 0" ];
+	"in_hardirq" [label = "in_hardirq"];
+	"in_hardirq" -> "idle" [ label = "hardirq_exit;hardirq > 0 && serving_softirq == 0" ];
+	"in_hardirq" -> "softirq_from_hardirq" [ label = "softirq_entry_from_hardirq;hardirq == 0 && serving_softirq == 1" ];
+	"in_softirq" [label = "in_softirq"];
+	"in_softirq" -> "idle" [ label = "softirq_exit;hardirq == 0 && serving_softirq == 1\nsoftirq_suspend;hardirq == 0" ];
+	"in_softirq" -> "hardirq_from_softirq" [ label = "hardirq_entry;hardirq > 0 && serving_softirq == 1" ];
+	"hardirq_from_softirq" [label = "hardirq_from_softirq"];
+	"hardirq_from_softirq" -> "in_softirq" [ label = "hardirq_exit;hardirq > 0 && serving_softirq == 1" ];
+	"softirq_from_hardirq" [label = "softirq_from_hardirq"];
+	"softirq_from_hardirq" -> "in_hardirq" [ label = "softirq_exit_to_hardirq;hardirq == 0 && serving_softirq == 1" ];
+	"softirq_from_hardirq" -> "hardirq_from_softirq_from_hardirq" [ label = "hardirq_entry;hardirq > 0 && serving_softirq == 1" ];
+	"hardirq_from_softirq_from_hardirq" [label = "hardirq_from_softirq_from_hardirq"];
+	"hardirq_from_softirq_from_hardirq" -> "softirq_from_hardirq" [ label = "hardirq_exit;hardirq > 0 && serving_softirq == 1" ];
+	{ rank = min ;
+		"__init_idle";
+		"idle";
+	}
+}
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 4/9] rv: Add a monitor for IRQ-exit scheduling ownership
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
                   ` (2 preceding siblings ...)
  2026-10-22  5:07 ` [RFC PATCH 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 5/9] docs/rv: Document the IRQ-entry monitors Karl Mehltretter
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

Existing scheduler events do not identify the task that owns a
preempt_schedule_irq() invocation after it switches out.

Add irqexit, a per-task hybrid-automaton monitor that follows an
invocation through entry, suspension, migration, resumption, and return.
Its guards check hardirq context, local interrupt state, and task
ownership.

The monitor covers preempt_schedule_irq() only. It does not cover
architecture-specific interrupt-return work or establish that the helper
was reached from a physical interrupt.

The generated model header is reproducible from the included DOT file
with the unmodified rvgen tool.

Assisted-by: LLM
---
 kernel/trace/rv/Kconfig                       |   1 +
 kernel/trace/rv/Makefile                      |   1 +
 kernel/trace/rv/monitors/irqexit/Kconfig      |  14 ++
 kernel/trace/rv/monitors/irqexit/irqexit.c    | 159 ++++++++++++++++++
 kernel/trace/rv/monitors/irqexit/irqexit.h    |  85 ++++++++++
 .../trace/rv/monitors/irqexit/irqexit_trace.h |  19 +++
 kernel/trace/rv/rv_trace.h                    |   1 +
 .../verification/models/irqentry/irqexit.dot  |  21 +++
 8 files changed, 301 insertions(+)
 create mode 100644 kernel/trace/rv/monitors/irqexit/Kconfig
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit.c
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit.h
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_trace.h
 create mode 100644 tools/verification/models/irqentry/irqexit.dot

diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index e7b30668a559b..12854fc9643f9 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -75,6 +75,7 @@ source "kernel/trace/rv/monitors/opid/Kconfig"
 
 source "kernel/trace/rv/monitors/irqentry/Kconfig"
 source "kernel/trace/rv/monitors/irqstate/Kconfig"
+source "kernel/trace/rv/monitors/irqexit/Kconfig"
 # Add new irqentry monitors here
 
 source "kernel/trace/rv/monitors/rtapp/Kconfig"
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index f39a9f8815764..c9a0bf8d9f9b8 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -19,6 +19,7 @@ obj-$(CONFIG_RV_MON_SSSW) += monitors/sssw/sssw.o
 obj-$(CONFIG_RV_MON_OPID) += monitors/opid/opid.o
 obj-$(CONFIG_RV_MON_IRQENTRY) += monitors/irqentry/irqentry.o
 obj-$(CONFIG_RV_MON_IRQSTATE) += monitors/irqstate/irqstate.o
+obj-$(CONFIG_RV_MON_IRQEXIT) += monitors/irqexit/irqexit.o
 obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o
 obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o
 obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o
diff --git a/kernel/trace/rv/monitors/irqexit/Kconfig b/kernel/trace/rv/monitors/irqexit/Kconfig
new file mode 100644
index 0000000000000..55d85e3aa3fca
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+config RV_MON_IRQEXIT
+	depends on RV
+	depends on RV_MON_IRQENTRY
+	default y
+	select HA_MON_EVENTS_ID
+	bool "irqexit monitor"
+	help
+	  Verify that IRQ-exit scheduling starts outside hardirq context
+	  with interrupts disabled and returns to its original task.
+
+	  For further information, see:
+	    Documentation/trace/rv/monitor_irqentry.rst
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit.c b/kernel/trace/rv/monitors/irqexit/irqexit.c
new file mode 100644
index 0000000000000..a088b28b62b17
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit.c
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/ftrace.h>
+#include <linux/hardirq.h>
+#include <linux/init.h>
+#include <linux/irqflags.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/rv.h>
+#include <linux/tracepoint.h>
+#include <rv/instrumentation.h>
+
+#define MODULE_NAME "irqexit"
+
+#include <trace/events/sched.h>
+#include <rv_trace.h>
+#include <monitors/irqentry/irqentry.h>
+
+#define RV_MON_TYPE RV_MON_PER_TASK
+#include "irqexit.h"
+#include <rv/ha_monitor.h>
+
+static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_irqexit env, u64 time_ns)
+{
+	switch (env) {
+	case hardirq_irqexit:
+		return hardirq_count();
+	case irq_off_irqexit:
+		return !!irqs_disabled();
+	case owner_current_irqexit:
+		return ha_get_target(ha_mon) == rv_get_current();
+	default:
+		return ENV_INVALID_VALUE;
+	}
+}
+
+static inline bool ha_verify_guards(struct ha_monitor *ha_mon,
+				    enum states curr_state, enum events event,
+				    enum states next_state, u64 time_ns)
+{
+	bool res = true;
+
+	if (curr_state == idle_irqexit && event == resched_entry_irqexit)
+		res = ha_get_env(ha_mon, hardirq_irqexit, time_ns) == 0ull &&
+		      ha_get_env(ha_mon, irq_off_irqexit, time_ns) == 1ull &&
+		      ha_get_env(ha_mon, owner_current_irqexit, time_ns) == 1ull;
+	else if (curr_state == idle_irqexit && event == switch_in_irqexit)
+		res = ha_get_env(ha_mon, hardirq_irqexit, time_ns) == 0ull &&
+		      ha_get_env(ha_mon, irq_off_irqexit, time_ns) == 1ull;
+	else if (curr_state == idle_irqexit && event == switch_out_irqexit)
+		res = ha_get_env(ha_mon, hardirq_irqexit, time_ns) == 0ull &&
+		      ha_get_env(ha_mon, irq_off_irqexit, time_ns) == 1ull;
+	else if (curr_state == rescheduling_irqexit && event == resched_exit_irqexit)
+		res = ha_get_env(ha_mon, hardirq_irqexit, time_ns) == 0ull &&
+		      ha_get_env(ha_mon, irq_off_irqexit, time_ns) == 1ull &&
+		      ha_get_env(ha_mon, owner_current_irqexit, time_ns) == 1ull;
+	else if (curr_state == rescheduling_irqexit && event == switch_out_irqexit)
+		res = ha_get_env(ha_mon, hardirq_irqexit, time_ns) == 0ull &&
+		      ha_get_env(ha_mon, irq_off_irqexit, time_ns) == 1ull &&
+		      ha_get_env(ha_mon, owner_current_irqexit, time_ns) == 1ull;
+	else if (curr_state == suspended_irqexit && event == switch_in_irqexit)
+		res = ha_get_env(ha_mon, hardirq_irqexit, time_ns) == 0ull &&
+		      ha_get_env(ha_mon, irq_off_irqexit, time_ns) == 1ull;
+	return res;
+}
+
+static bool ha_verify_constraint(struct ha_monitor *ha_mon,
+				 enum states curr_state, enum events event,
+				 enum states next_state, u64 time_ns)
+{
+	return ha_verify_guards(ha_mon, curr_state, event, next_state, time_ns);
+}
+
+static void handle_resched_entry(void *data, struct task_struct *owner)
+{
+	da_handle_start_run_event(owner, resched_entry_irqexit);
+}
+
+static void handle_resched_exit(void *data, struct task_struct *owner)
+{
+	/* Do not hide an invalid return behind enable-mid-invocation sync. */
+	if (owner != rv_get_current() || hardirq_count() || !irqs_disabled())
+		da_handle_start_run_event(owner, resched_exit_irqexit);
+	else
+		da_handle_start_event(owner, resched_exit_irqexit);
+}
+
+static void probe_resched_entry(void *data, struct task_struct *owner,
+				unsigned long pc, bool irq_off)
+{
+	handle_resched_entry(data, owner);
+}
+
+static void probe_resched_exit(void *data, struct task_struct *owner,
+			       unsigned long pc, bool irq_off)
+{
+	handle_resched_exit(data, owner);
+}
+
+static void handle_sched_switch(void *data, bool preempt,
+				struct task_struct *prev,
+				struct task_struct *next,
+				unsigned int prev_state)
+{
+	/* current is still prev; the incoming task must not use that guard. */
+	da_handle_event(prev, switch_out_irqexit);
+	da_handle_event(next, switch_in_irqexit);
+}
+
+static int enable_irqexit(void)
+{
+	int retval;
+
+	retval = ha_monitor_init();
+	if (retval)
+		return retval;
+
+	rv_attach_trace_probe("irqexit", sched_irq_preempt_entry, probe_resched_entry);
+	rv_attach_trace_probe("irqexit", sched_irq_preempt_exit, probe_resched_exit);
+	rv_attach_trace_probe("irqexit", sched_switch, handle_sched_switch);
+
+	return 0;
+}
+
+static void disable_irqexit(void)
+{
+	rv_this.enabled = 0;
+
+	rv_detach_trace_probe("irqexit", sched_irq_preempt_entry, probe_resched_entry);
+	rv_detach_trace_probe("irqexit", sched_irq_preempt_exit, probe_resched_exit);
+	rv_detach_trace_probe("irqexit", sched_switch, handle_sched_switch);
+
+	ha_monitor_destroy();
+}
+
+static struct rv_monitor rv_this = {
+	.name = "irqexit",
+	.description = "Verify IRQ-exit scheduling ownership.",
+	.enable = enable_irqexit,
+	.disable = disable_irqexit,
+	.reset = da_monitor_reset_all,
+	.enabled = 0,
+};
+
+static int __init register_irqexit(void)
+{
+	return rv_register_monitor(&rv_this, &rv_irqentry);
+}
+
+static void __exit unregister_irqexit(void)
+{
+	rv_unregister_monitor(&rv_this);
+}
+
+module_init(register_irqexit);
+module_exit(unregister_irqexit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Karl Mehltretter <kmehltretter@gmail.com>");
+MODULE_DESCRIPTION("irqexit: verify IRQ-exit scheduling ownership");
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit.h b/kernel/trace/rv/monitors/irqexit/irqexit.h
new file mode 100644
index 0000000000000..f48d44d1dff73
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit.h
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Automatically generated C representation of irqexit automaton
+ * For further information about this format, see kernel documentation:
+ *   Documentation/trace/rv/deterministic_automata.rst
+ */
+
+#define MONITOR_NAME irqexit
+
+enum states_irqexit {
+	idle_irqexit,
+	rescheduling_irqexit,
+	suspended_irqexit,
+	state_max_irqexit,
+};
+
+#define INVALID_STATE state_max_irqexit
+
+enum events_irqexit {
+	resched_entry_irqexit,
+	resched_exit_irqexit,
+	switch_in_irqexit,
+	switch_out_irqexit,
+	event_max_irqexit,
+};
+
+enum envs_irqexit {
+	hardirq_irqexit,
+	irq_off_irqexit,
+	owner_current_irqexit,
+	env_max_irqexit,
+	env_max_stored_irqexit = hardirq_irqexit,
+};
+
+_Static_assert(env_max_stored_irqexit <= MAX_HA_ENV_LEN, "Not enough slots");
+
+struct automaton_irqexit {
+	char *state_names[state_max_irqexit];
+	char *event_names[event_max_irqexit];
+	char *env_names[env_max_irqexit];
+	unsigned char function[state_max_irqexit][event_max_irqexit];
+	unsigned char initial_state;
+	bool final_states[state_max_irqexit];
+};
+
+static const struct automaton_irqexit automaton_irqexit = {
+	.state_names = {
+		"idle",
+		"rescheduling",
+		"suspended",
+	},
+	.event_names = {
+		"resched_entry",
+		"resched_exit",
+		"switch_in",
+		"switch_out",
+	},
+	.env_names = {
+		"hardirq",
+		"irq_off",
+		"owner_current",
+	},
+	.function = {
+		{
+			rescheduling_irqexit,
+			INVALID_STATE,
+			idle_irqexit,
+			idle_irqexit,
+		},
+		{
+			INVALID_STATE,
+			idle_irqexit,
+			INVALID_STATE,
+			suspended_irqexit,
+		},
+		{
+			INVALID_STATE,
+			INVALID_STATE,
+			rescheduling_irqexit,
+			INVALID_STATE,
+		},
+	},
+	.initial_state = idle_irqexit,
+	.final_states = { 1, 0, 0 },
+};
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit_trace.h b/kernel/trace/rv/monitors/irqexit/irqexit_trace.h
new file mode 100644
index 0000000000000..f35366875feff
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit_trace.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+/*
+ * Snippet to be included in rv_trace.h
+ */
+
+#ifdef CONFIG_RV_MON_IRQEXIT
+DEFINE_EVENT(event_da_monitor_id, event_irqexit,
+	     TP_PROTO(int id, char *state, char *event, char *next_state, bool final_state),
+	     TP_ARGS(id, state, event, next_state, final_state));
+
+DEFINE_EVENT(error_da_monitor_id, error_irqexit,
+	     TP_PROTO(int id, char *state, char *event),
+	     TP_ARGS(id, state, event));
+
+DEFINE_EVENT(error_env_da_monitor_id, error_env_irqexit,
+	     TP_PROTO(int id, char *state, char *event, char *env),
+	     TP_ARGS(id, state, event, env));
+#endif /* CONFIG_RV_MON_IRQEXIT */
diff --git a/kernel/trace/rv/rv_trace.h b/kernel/trace/rv/rv_trace.h
index 040b3635ef289..c90dae4338bcc 100644
--- a/kernel/trace/rv/rv_trace.h
+++ b/kernel/trace/rv/rv_trace.h
@@ -190,6 +190,7 @@ DECLARE_EVENT_CLASS(error_env_da_monitor_id,
 
 #include <monitors/stall/stall_trace.h>
 #include <monitors/nomiss/nomiss_trace.h>
+#include <monitors/irqexit/irqexit_trace.h>
 // Add new monitors based on CONFIG_HA_MON_EVENTS_ID here
 
 #endif
diff --git a/tools/verification/models/irqentry/irqexit.dot b/tools/verification/models/irqentry/irqexit.dot
new file mode 100644
index 0000000000000..eb2e5499ad7a7
--- /dev/null
+++ b/tools/verification/models/irqentry/irqexit.dot
@@ -0,0 +1,21 @@
+digraph state_automaton {
+	center = true;
+	size = "7,11";
+	{node [shape = plaintext, style=invis, label=""] "__init_idle"};
+	{node [shape = doublecircle] "idle"};
+	{node [shape = circle] "rescheduling"};
+	{node [shape = circle] "suspended"};
+	"__init_idle" -> "idle";
+	"idle" [label = "idle", color = green3];
+	"idle" -> "idle" [ label = "switch_in;hardirq == 0 && irq_off == 1\nswitch_out;hardirq == 0 && irq_off == 1" ];
+	"idle" -> "rescheduling" [ label = "resched_entry;hardirq == 0 && irq_off == 1 && owner_current == 1" ];
+	"rescheduling" [label = "rescheduling"];
+	"rescheduling" -> "idle" [ label = "resched_exit;hardirq == 0 && irq_off == 1 && owner_current == 1" ];
+	"rescheduling" -> "suspended" [ label = "switch_out;hardirq == 0 && irq_off == 1 && owner_current == 1" ];
+	"suspended" [label = "suspended"];
+	"suspended" -> "rescheduling" [ label = "switch_in;hardirq == 0 && irq_off == 1" ];
+	{ rank = min ;
+		"__init_idle";
+		"idle";
+	}
+}
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 5/9] docs/rv: Document the IRQ-entry monitors
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
                   ` (3 preceding siblings ...)
  2026-10-22  5:07 ` [RFC PATCH 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

Document the irqstate and irqexit models, their synchronization rules,
PREEMPT_RT behavior, tracefs interface, and coverage limits.

irqstate observes hardirq contexts whose boundaries pass through
irq_enter() or irq_enter_rcu() and irq_exit() or irq_exit_rcu(). Direct
__irq_enter() and __irq_exit() users, raw-only paths, and
architecture-specific intervals before generic entry and after generic
exit remain outside its coverage.

Clarify that irqexit monitors preempt_schedule_irq(), not the complete
interrupt-return path.

Assisted-by: LLM
---
 Documentation/trace/rv/index.rst            |   1 +
 Documentation/trace/rv/monitor_irqentry.rst | 279 ++++++++++++++++++++
 2 files changed, 280 insertions(+)
 create mode 100644 Documentation/trace/rv/monitor_irqentry.rst

diff --git a/Documentation/trace/rv/index.rst b/Documentation/trace/rv/index.rst
index 29769f06bb0f0..6b0c7db790786 100644
--- a/Documentation/trace/rv/index.rst
+++ b/Documentation/trace/rv/index.rst
@@ -15,6 +15,7 @@ Runtime Verification
    monitor_wip.rst
    monitor_wwnr.rst
    monitor_sched.rst
+   monitor_irqentry.rst
    monitor_rtapp.rst
    monitor_stall.rst
    monitor_deadline.rst
diff --git a/Documentation/trace/rv/monitor_irqentry.rst b/Documentation/trace/rv/monitor_irqentry.rst
new file mode 100644
index 0000000000000..f2bb9ed2c0029
--- /dev/null
+++ b/Documentation/trace/rv/monitor_irqentry.rst
@@ -0,0 +1,279 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+IRQ-entry monitors
+==================
+
+- Name: irqentry
+- Type: container for IRQ-entry context monitors
+- Author: Karl Mehltretter <kmehltretter@gmail.com>
+
+Description
+-----------
+
+The ``irqentry`` container groups monitors which compare interrupt-related
+events with the execution context recorded by the kernel.  The monitors are
+architecture independent: they consume generic tracepoints and do not require
+an architecture assembly hook.
+
+The first monitor deliberately observes only safe, instrumentable events.  It
+does not cover the interval between physical interrupt entry and establishment
+of generic hardirq state.
+
+Monitor irqstate
+----------------
+
+The ``irqstate`` monitor checks the ordering of hardirq contexts whose entry
+and exit pass through ``irq_enter()``/``irq_exit()`` or
+``irq_enter_rcu()``/``irq_exit_rcu()``, and of complete serving-softirq
+contexts. Each boundary must agree with the corresponding preempt-count state:
+
+.. list-table:: Event and required context
+   :header-rows: 1
+
+   * - Kernel event
+     - Model event
+     - Required environment
+   * - ``hardirq_context_entry``
+     - ``hardirq_entry``
+     - ``hardirq > 0``; serving-softirq state selects the source state
+   * - ``hardirq_context_exit``
+     - ``hardirq_exit``
+     - ``hardirq > 0``; serving-softirq state selects the destination state
+   * - ``softirq_context_entry``
+     - ``softirq_entry`` or ``softirq_entry_from_hardirq``
+     - ``hardirq == 0 && serving_softirq == 1``
+   * - ``softirq_context_exit``
+     - ``softirq_exit`` or ``softirq_exit_to_hardirq``
+     - ``hardirq == 0 && serving_softirq == 1``
+   * - ``sched_switch`` from a serving-softirq task (PREEMPT_RT)
+     - ``softirq_suspend``
+     - ``hardirq == 0``
+   * - ``sched_switch`` to a serving-softirq task (PREEMPT_RT)
+     - ``softirq_resume``
+     - ``hardirq == 0``
+
+``serving_softirq`` uses ``in_serving_softirq()``.  It therefore distinguishes
+a serving-softirq interval from a task which merely disabled bottom halves.
+The hardirq condition uses the hardirq component of ``preempt_count``.
+Softirq events also report whether hardirq state was replaced for inline
+processing during interrupt return. The exit event reports whether hardirq
+state will be restored.
+
+On PREEMPT_RT the serving interval includes preparation before
+``handle_softirqs()`` and teardown after it, not just handler execution or
+CPU-time accounting. A worker can enter and leave this context even if its
+pending-work check finds nothing. The RT entry event follows the counter
+update that sets the serving bit, and the exit event precedes the update
+that clears it. Processing inline from ``local_bh_enable()`` has the same
+boundaries; ordinary BH-disable nesting does not emit context events.
+
+RT worker entry acquires any blocking BH lock before the counter update.
+When the entry tracepoint is enabled, the update and event execute together
+with local IRQs disabled, then restore the original IRQ state. No new IRQ
+disable/enable pair is introduced when that tracepoint is disabled. RT
+decrements already execute with IRQs disabled. Non-RT retains its existing
+IRQ-disabled processing-loop boundaries.
+
+The model has six states: ``idle``, ``in_hardirq``, ``in_softirq``,
+``hardirq_from_softirq``, ``softirq_from_hardirq``, and
+``hardirq_from_softirq_from_hardirq``. It accepts these outer-context
+sequences::
+
+  idle -> in_hardirq -> idle
+  idle -> in_softirq -> idle
+  in_softirq -> hardirq_from_softirq -> in_softirq
+  in_hardirq -> softirq_from_hardirq -> in_hardirq
+  softirq_from_hardirq -> hardirq_from_softirq_from_hardirq
+                        -> softirq_from_hardirq
+
+The fourth sequence represents non-RT softirq processing during interrupt
+return. The kernel replaces ``HARDIRQ_OFFSET`` with ``SOFTIRQ_OFFSET`` while
+the handlers run, then restores hardirq state before completing interrupt
+exit. The fifth sequence represents a nested hardirq during those handlers.
+
+On PREEMPT_RT, serving-softirq state is task-local and the serving interval is
+preemptible.  A switch away from that task suspends its CPU context
+(``in_softirq -> idle``); a switch back resumes it (``idle -> in_softirq``).
+The adapter reads the outgoing and incoming task's softirq count at
+``sched_switch``.  Interrupts are disabled there, so no hardirq boundary can
+occur between representing the incoming task and the actual context switch.
+A hardirq in a task which preempted a softirq therefore starts in ``idle``.
+
+Hardirq depth greater than one is collapsed into the surrounding hardirq
+state.  The depth comes from the existing hardirq component, so the monitor
+does not maintain a second counter which could wrap independently.
+
+The boundary events run after entry state has been established and before it
+is removed.  A false transition or guard invokes the selected RV reactor and
+emits the monitor's error trace event.
+
+On hardirq exit, ``hardirq_context_exit`` runs immediately before removal of
+``HARDIRQ_OFFSET``. Hardirq accounting ends earlier, before inline softirq and
+tick-exit work. ``irqstate`` returns to the outer model state at the final
+boundary event. The event does not represent return from ``irq_exit_rcu()``,
+the later reschedule check, or the architecture return tail.
+
+Hardirq accounting and lockdep hardirq state have separate lifetimes. Lockdep
+can also create a synthetic hardirq interval without changing
+``HARDIRQ_OFFSET``. ``irqstate`` models the preempt-count context and does not
+report those lockdep-only intervals.
+
+Synchronization
+---------------
+
+Enabling a per-CPU monitor does not imply that the CPU is in task context.
+An outer hardirq or softirq entry from a known idle context starts the model
+and consumes the event. An outer exit can instead establish the idle state
+without consuming an unmatched event when monitoring starts in the middle of
+that context. Provenance-marked softirq boundaries do not start monitoring:
+they belong to an enclosing hardirq. If attachment occurs during that
+sequence, its final hardirq exit establishes ``idle``. If monitoring starts
+while a hardirq interrupts an ordinary softirq, the hardirq pair is ignored
+until the enclosing softirq exit establishes the idle state. On PREEMPT_RT, a
+softirq suspension also establishes ``idle``, and a resumption can start
+monitoring directly in ``in_softirq``.
+
+On PREEMPT_RT, probe attachment waits for an RCU grace period before the
+monitor can become enabled. This drains BH preparations which sampled the
+entry tracepoint disabled before attachment. Otherwise an intervening IRQ
+could synchronize the model to idle before a preparation publishes its
+serving bit without an entry event. New preparations see the attached
+entry probe and serialize the bit update with their event.
+
+Monitor irqexit
+---------------
+
+The ``irqexit`` monitor follows each task's invocation of
+``preempt_schedule_irq()``. It consumes ``sched_irq_preempt_entry``,
+``sched_irq_preempt_exit``, and ``sched_switch``. Both boundary events must
+observe zero hardirq count and disabled local interrupts. The owner saved
+in the invocation's stack frame must match the current task at both
+boundaries.
+
+The boundary records carry preempt-count and interrupt-state snapshots
+taken at the scheduler call site, before tracepoint dispatch or ring-buffer
+reservation. Sampling in ``TP_fast_assign`` would instead include tracing's
+own context changes. Disabled hooks do not take these extra snapshots.
+The RV adapter still evaluates its original live-context guards; the raw
+record is an independent diagnostic channel, not a replacement verdict.
+
+The model has three states: ``idle``, ``rescheduling``, and ``suspended``.
+Entry changes ``idle`` to ``rescheduling``; return changes it back to
+``idle``. Switching out an active invocation changes ``rescheduling`` to
+``suspended``; switching that task in resumes ``rescheduling``. The
+incoming event cannot require its target to be current: ``sched_switch``
+still executes in the outgoing task. Local interrupts remain disabled
+until the incoming task runs.
+
+Instances belong to tasks, not CPUs. Two tasks may have independently
+suspended invocations, and resuming on another CPU must use the same task's
+instance. An interrupt or IRQ-exit schedule in another task cannot return
+on behalf of the suspended owner. Ordinary switches while ``idle`` do not
+change its state.
+
+An entry starts monitoring and consumes the event. If the monitor was
+enabled during an existing invocation, its first valid exit instead
+establishes ``idle`` without consuming an unmatched event. Switches alone
+do not synchronize a task. An exit with an invalid context or owner must
+not be skipped as a synchronization event.
+
+The helper can also run on a kernel exception return. Its events therefore
+do not prove that an invocation followed a physical interrupt, and they do
+not describe architecture register restoration or context tracking after
+the helper returns. The existing helper entry assertions can reject an
+invalid caller before the entry event; such a failure is not a violation
+detected by this monitor.
+
+``irqexit`` does not associate an invocation with a preceding hardirq. It
+cannot observe work between ``hardirq_context_exit`` and the helper entry or
+between the helper exit and return to the interrupted context.
+
+Relationship to sched/sts
+-------------------------
+
+The scheduler ``sts`` monitor models the ordering of IRQ disable/enable,
+scheduler entry/exit, and task switches.  It uses ``irq_handler_entry`` and
+selected x86 vector events to refine that scheduler model.
+
+``irqstate`` does not consume IRQ action or scheduler entry/exit events.  It
+observes generic context boundaries, so multiple actions in one interrupt and
+an action dispatched through ``generic_handle_irq_safe()`` do not change its
+state.  On PREEMPT_RT it uses task switches only to follow the active softirq
+context. ``irqexit`` observes the surrounding IRQ-preemption loop and its
+task owner, rather than inferring that protocol from individual scheduler
+entry/exit events used by ``sts``.
+
+.. list-table:: Monitor coverage
+   :header-rows: 1
+
+   * - Property or event
+     - ``sched/sts``
+     - ``irqentry/irqstate``
+   * - IRQ disable and enable ordering
+     - yes
+     - no
+   * - IRQ action entry
+     - yes
+     - no
+   * - scheduler entry and exit
+     - yes
+     - no
+   * - task switch
+     - yes
+     - PREEMPT_RT softirq context only
+   * - ``irq_enter[_rcu]()``/``irq_exit[_rcu]()`` hardirq context
+     - no
+     - yes
+   * - complete serving-softirq context
+     - no
+     - yes
+   * - hardirq interruption of a softirq
+     - no
+     - yes
+
+Coverage limits
+---------------
+
+The context tracepoints are emitted by ``irq_enter_rcu()`` and
+``irq_exit_rcu()``. Softirq boundaries come from the RT serving-bit updates
+or the non-RT generic processing loop. Consequently this monitor cannot
+observe:
+
+- the early architecture entry interval before ``irq_enter_rcu()``;
+- paths which directly use ``__irq_enter()`` and ``__irq_exit()``, such as
+  the arm64 APEI SEA irq-work interval;
+- architecture or special-vector paths which use only
+  ``__irq_enter_raw()`` and ``__irq_exit_raw()`` (including x86 simple system
+  vectors);
+- NMI entry and exit; or
+- work after hardirq-count removal, return from ``irq_exit_rcu()``, the
+  conditional reschedule check, and architecture-specific state restoration.
+
+``irqexit`` checks ``preempt_schedule_irq()`` separately. It does not join
+that helper to the preceding ``irqstate`` instance or model an architecture's
+entire exit.
+
+The standard hardirq tracepoints describe individual IRQ actions rather than
+physical interrupt boundaries.  They are deliberately not used: synthetic
+dispatch and software resend can execute an action outside hardirq context,
+and one interrupt can execute several actions.
+
+These limitations are intentional.  A later architecture hook may provide a
+deferred raw-entry marker for the very-early interval.  Such an addition needs
+a separate event contract and does not change the meaning of ``irqstate``.
+
+Enabling the monitor
+--------------------
+
+With tracefs mounted at ``/sys/kernel/tracing``::
+
+  # cd /sys/kernel/tracing/rv
+  # grep -x irqentry:irqstate available_monitors
+  # echo irqstate > enabled_monitors
+  # echo irqexit >> enabled_monitors
+  # echo nop > monitors/irqentry/irqstate/reactors
+  # echo nop > monitors/irqentry/irqexit/reactors
+
+Use the ``nop`` reactor during initial qualification.  Select the ``panic``
+reactor only after unexplained violations have been excluded on the target
+configuration.
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
                   ` (4 preceding siblings ...)
  2026-10-22  5:07 ` [RFC PATCH 5/9] docs/rv: Document the IRQ-entry monitors Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:33   ` sashiko-bot
  2026-10-22  5:07 ` [RFC PATCH 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
                   ` (2 subsequent siblings)
  8 siblings, 1 reply; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

Add KUnit coverage for the irqstate generated state table, invalid
transitions, enable-time synchronization, nested hardirqs, shared IRQ
actions, PREEMPT_RT serving intervals, and live tracepoint wiring.

Test probe attachment and detachment by emitting a synthetic hardirq
boundary while only one boundary probe is attached. Combine raw hardirq
accounting with explicit boundary events so the test does not depend on
cross-CPU scheduling, affinity changes, CPU hotplug, or wait queues.

In the non-RT hardirq-exit case, raise a second tasklet from the softirq
exit probe after inline processing has made its final pending-work check.
Verify that ksoftirqd is already runnable and drains the tasklet.

KUnit failure reporting may allocate with GFP_KERNEL. Record reaction
counts inside synthetic IRQ-disabled, bottom-half-disabled, and hardirq
intervals. Evaluate the expectations after restoring normal task context
to avoid secondary atomic-sleep or lockdep diagnostics.

The live tests invoke generic helpers directly. They do not exercise
physical interrupt-entry assembly.

RV_MONITORS_KUNIT_TEST is a tristate. Keep the state-table and adapter
cases available when the suite is a module. Build cases that need
non-exported helpers or tracepoints only when the suite is built in, and
report them as skipped for a module build.

Assisted-by: LLM
---
 Documentation/trace/rv/monitor_irqentry.rst   |   61 +
 include/rv/kunit.h                            |   34 +-
 kernel/trace/rv/monitors/irqstate/irqstate.c  |   49 +
 .../rv/monitors/irqstate/irqstate_kunit.c     | 1384 +++++++++++++++++
 .../rv/monitors/irqstate/irqstate_kunit.h     |   25 +
 kernel/trace/rv/rv_monitors_test.c            |   12 +
 6 files changed, 1555 insertions(+), 10 deletions(-)
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
 create mode 100644 kernel/trace/rv/monitors/irqstate/irqstate_kunit.h

diff --git a/Documentation/trace/rv/monitor_irqentry.rst b/Documentation/trace/rv/monitor_irqentry.rst
index f2bb9ed2c0029..ba9e78e7c4a08 100644
--- a/Documentation/trace/rv/monitor_irqentry.rst
+++ b/Documentation/trace/rv/monitor_irqentry.rst
@@ -231,6 +231,67 @@ entry/exit events used by ``sts``.
      - no
      - yes
 
+KUnit scheduling coverage
+-------------------------
+
+``RV_MONITORS_KUNIT_TEST`` can be built in or as a module. The generated
+state-table and adapter cases run in either form. Tests which invoke
+non-exported generic IRQ entry/exit helpers or attach to non-exported boundary
+tracepoints require the built-in suite. A module build reports the standalone
+cases as skipped.
+
+``rv_test_irqstate_shared_actions`` emits two distinct IRQ action pairs for
+one IRQ number inside a single real ``irq_enter_rcu()``/``irq_exit_rcu()``
+boundary. Independent probes, filtered by the exact action pointers, require
+the first action to return ``IRQ_NONE`` and the second ``IRQ_HANDLED`` in the
+same order used by generic shared-IRQ dispatch. They also check hardirq and
+interrupt state, task and CPU identity, event counts, and count restoration.
+The monitor must accept the surrounding context without treating either
+action as another hardirq boundary. The test emits the action tracepoints
+directly; it does not construct an IRQ descriptor or invoke action handlers.
+
+``rv_test_irqstate_depth`` constructs every representable hardirq nesting
+level with the raw accounting helpers, then unwinds the complete stack. It
+checks the actual deepest component and restoration of the original count.
+An unmatched exit after the valid cycle must fail, so ignoring every event
+cannot satisfy the test. This is maximum valid nesting, not live counter
+overflow or physical interrupt-stack recursion.
+
+``rv_test_irqstate_depth_overflow`` fills the hardirq component to its maximum
+and then supplies the adapter with the depth value that another increment
+would wrap to. It requires exactly one reaction, no additional reaction while
+the valid stack is unwound, an unchanged NMI component, and exact restoration
+of the original preempt count. The test deliberately does not execute the
+overflowing raw increment, because that would carry into the adjacent NMI
+component. It tests rejection of wrapped boundary metadata, not live counter
+overflow.
+
+``rv_test_irqstate_sync`` resets and enables the bare adapter inside single
+and nested hardirq contexts, inside a real serving-softirq callback, and
+inside a hardirq interrupting that callback. Valid unwinding must establish
+idle without a reaction. A separate unmatched exit must then react exactly
+once. The softirq cases snapshot reactions before the invalid control, so
+an error on the valid sequence cannot replace the expected later error.
+These are adapter synchronization tests, not concurrent tracefs writes.
+
+``rv_test_irqstate_irq_exit_inline`` schedules a tasklet inside a real generic
+hardirq boundary and requires non-RT ``irq_exit_rcu()`` to process it before
+returning. Independent boundary probes require exactly one softirq entry
+before the tasklet and one exit afterward. The callback must run in the same
+task and on the same CPU with interrupts enabled and serving-softirq state set,
+but outside hardirq context; the monitor must accept both context pairs and
+the original counts must be restored. If the kernel diverts the tasklet to
+ksoftirqd, for example under forced IRQ threading, the test drains it and
+skips because that configuration does not provide the inline path.
+
+``rv_test_irqstate_rt_inline_bh`` forces a pending tasklet through the
+PREEMPT_RT ``local_bh_enable()`` inline-processing path. It requires the
+tasklet to finish synchronously in the enabling task, on the same CPU, while
+serving-softirq state is set and local interrupts are enabled. Independent
+boundary probes require exactly one entry before the callback and one exit
+afterward, both with local interrupts disabled and live serving state set;
+the monitor must accept the pair and the original counts must be restored.
+
 Coverage limits
 ---------------
 
diff --git a/include/rv/kunit.h b/include/rv/kunit.h
index 31e0b93c40ea8..c77f0c92d9105 100644
--- a/include/rv/kunit.h
+++ b/include/rv/kunit.h
@@ -15,6 +15,7 @@
 
 #include <kunit/test.h>
 #include <kunit/test-bug.h>
+#include <linux/compiler.h>
 #include <linux/delay.h>
 
 int rv_set_testing(struct kunit_suite *suite);
@@ -28,18 +29,31 @@ struct rv_kunit_ctx {
 	struct task_struct *mock_tasks[RV_KUNIT_MAX_MOCK_TASKS];
 };
 
-#define RV_KUNIT_EXPECT_REACTION(test, ctx)                             \
-	do {                                                            \
-		KUNIT_EXPECT_EQ(test, ctx->reactions, ++ctx->expected); \
-		if (ctx->reactions != ctx->expected)                    \
-			ctx->expected = ctx->reactions;                 \
+#define RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed, delta) \
+	do {                                                    \
+		struct rv_kunit_ctx *__rv_observed_ctx = (ctx); \
+		int __rv_observed = (observed);                 \
+		int __rv_expected =                            \
+			__rv_observed_ctx->expected + (delta);    \
+		KUNIT_EXPECT_EQ(test, __rv_observed,           \
+				__rv_expected);                     \
+		__rv_observed_ctx->expected = __rv_observed;   \
 	} while (0)
 
-#define RV_KUNIT_EXPECT_NO_REACTION(test, ctx)                        \
-	do {                                                          \
-		KUNIT_EXPECT_EQ(test, ctx->reactions, ctx->expected); \
-		if (ctx->reactions != ctx->expected)                  \
-			ctx->expected = ctx->reactions;               \
+#define RV_KUNIT_EXPECT_REACTION(test, ctx)                       \
+	do {                                                       \
+		struct rv_kunit_ctx *__rv_reaction_ctx = (ctx);     \
+		RV_KUNIT_EXPECT_OBSERVED(                            \
+			test, __rv_reaction_ctx,                      \
+			READ_ONCE(__rv_reaction_ctx->reactions), 1);  \
+	} while (0)
+
+#define RV_KUNIT_EXPECT_NO_REACTION(test, ctx)                    \
+	do {                                                       \
+		struct rv_kunit_ctx *__rv_reaction_ctx = (ctx);     \
+		RV_KUNIT_EXPECT_OBSERVED(                            \
+			test, __rv_reaction_ctx,                      \
+			READ_ONCE(__rv_reaction_ctx->reactions), 0);  \
 	} while (0)
 
 #define RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)                             \
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate.c b/kernel/trace/rv/monitors/irqstate/irqstate.c
index 2d8c824a4870a..4bbcbfcfedadc 100644
--- a/kernel/trace/rv/monitors/irqstate/irqstate.c
+++ b/kernel/trace/rv/monitors/irqstate/irqstate.c
@@ -20,6 +20,36 @@
 #include "irqstate.h"
 #include <rv/ha_monitor.h>
 
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+static void (*irqstate_lifecycle_hook)(void *data, bool enabling);
+static void *irqstate_lifecycle_hook_data;
+
+/*
+ * Let KUnit force an IRQ pair while the boundary probes are deliberately
+ * only half attached or detached. The disabled monitor must reject that
+ * otherwise inconsistent instrumentation window.
+ */
+static void irqstate_test_lifecycle_point(bool enabling)
+{
+	void (*hook)(void *data, bool enabling);
+
+	hook = READ_ONCE(irqstate_lifecycle_hook);
+	if (hook)
+		hook(READ_ONCE(irqstate_lifecycle_hook_data), enabling);
+}
+
+static void irqstate_set_lifecycle_hook(void (*hook)(void *data, bool enabling),
+					void *data)
+{
+	WRITE_ONCE(irqstate_lifecycle_hook_data, data);
+	WRITE_ONCE(irqstate_lifecycle_hook, hook);
+}
+#else
+static inline void irqstate_test_lifecycle_point(bool enabling)
+{
+}
+#endif
+
 static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_irqstate env,
 		      u64 time_ns)
 {
@@ -165,6 +195,7 @@ static void handle_sched_switch(void *data, bool preempt,
 static void attach_irqstate(void)
 {
 	rv_attach_trace_probe("irqstate", hardirq_context_entry, handle_hardirq_entry);
+	irqstate_test_lifecycle_point(true);
 	rv_attach_trace_probe("irqstate", hardirq_context_exit, handle_hardirq_exit);
 	rv_attach_trace_probe("irqstate", softirq_context_entry, handle_softirq_entry);
 	rv_attach_trace_probe("irqstate", softirq_context_exit, handle_softirq_exit);
@@ -183,6 +214,7 @@ static void attach_irqstate(void)
 static void detach_irqstate(void)
 {
 	rv_detach_trace_probe("irqstate", hardirq_context_entry, handle_hardirq_entry);
+	irqstate_test_lifecycle_point(false);
 	rv_detach_trace_probe("irqstate", hardirq_context_exit, handle_hardirq_exit);
 	rv_detach_trace_probe("irqstate", softirq_context_entry, handle_softirq_entry);
 	rv_detach_trace_probe("irqstate", softirq_context_exit, handle_softirq_exit);
@@ -238,3 +270,20 @@ module_exit(unregister_irqstate);
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Karl Mehltretter <kmehltretter@gmail.com>");
 MODULE_DESCRIPTION("irqstate: verify IRQ event and execution context agreement");
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+#include <kunit/visibility.h>
+#include "irqstate_kunit.h"
+
+const struct rv_irqstate_ops rv_irqstate_ops = {
+	.mon = RV_MON_OPS_INIT(),
+	.attach = attach_irqstate,
+	.detach = detach_irqstate,
+	.handle_hardirq_entry = handle_hardirq_entry,
+	.handle_hardirq_exit = handle_hardirq_exit,
+	.handle_softirq_entry = handle_softirq_entry,
+	.handle_softirq_exit = handle_softirq_exit,
+	.set_lifecycle_hook = irqstate_set_lifecycle_hook,
+};
+EXPORT_SYMBOL_IF_KUNIT(rv_irqstate_ops);
+#endif
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
new file mode 100644
index 0000000000000..c5b8834fea3d9
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
@@ -0,0 +1,1384 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/completion.h>
+#include <linux/cpu.h>
+#include <linux/hardirq.h>
+#include <linux/interrupt.h>
+#include <linux/irqflags.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/rv.h>
+#include <linux/sched.h>
+#include <linux/wait.h>
+#include <rv/kunit.h>
+#include <rv_trace.h>
+#include <trace/events/irq.h>
+
+#include "irqstate_kunit.h"
+
+#if IS_REACHABLE(CONFIG_RV_MON_IRQSTATE)
+
+enum irqstate_softirq_test {
+	IRQSTATE_SOFTIRQ_VALID,
+	IRQSTATE_SOFTIRQ_INTERRUPTED,
+	IRQSTATE_SOFTIRQ_DOUBLE_ENTRY,
+	IRQSTATE_SOFTIRQ_DOUBLE_EXIT,
+	IRQSTATE_SOFTIRQ_TRACEPOINT_ONLY,
+	IRQSTATE_SOFTIRQ_SYNC,
+	IRQSTATE_SOFTIRQ_SYNC_INTERRUPTED,
+};
+
+struct irqstate_softirq_ctx {
+	struct tasklet_struct tasklet;
+	struct completion done;
+	enum irqstate_softirq_test test;
+	int reactions_after_sync;
+};
+
+static void run_irqstate_softirq(struct tasklet_struct *tasklet)
+{
+	struct irqstate_softirq_ctx *softirq_ctx;
+	unsigned long flags;
+
+	softirq_ctx = from_tasklet(softirq_ctx, tasklet, tasklet);
+
+	if (softirq_ctx->test == IRQSTATE_SOFTIRQ_SYNC ||
+	    softirq_ctx->test == IRQSTATE_SOFTIRQ_SYNC_INTERRUPTED) {
+		local_irq_save(flags);
+		rv_irqstate_ops.mon.rv_this->enabled = 0;
+		rv_irqstate_ops.handle_softirq_entry(NULL, true, false);
+		rv_irqstate_ops.mon.rv_this->reset();
+		rv_irqstate_ops.mon.rv_this->enabled = 1;
+		if (softirq_ctx->test == IRQSTATE_SOFTIRQ_SYNC_INTERRUPTED) {
+			__irq_enter_raw();
+			rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+			rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+			__irq_exit_raw();
+		}
+		/* The enclosing softirq exit establishes idle without an entry. */
+		rv_irqstate_ops.handle_softirq_exit(NULL, true, false);
+		softirq_ctx->reactions_after_sync = active_ctx->reactions;
+		/* Once synchronized, an unmatched hardirq exit must be rejected. */
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		__irq_exit_raw();
+		local_irq_restore(flags);
+		goto out;
+	}
+
+	if (softirq_ctx->test == IRQSTATE_SOFTIRQ_TRACEPOINT_ONLY)
+		goto out;
+
+	rv_irqstate_ops.handle_softirq_entry(NULL, true, false);
+
+	switch (softirq_ctx->test) {
+	case IRQSTATE_SOFTIRQ_INTERRUPTED:
+		local_irq_save(flags);
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		__irq_exit_raw();
+		local_irq_restore(flags);
+		break;
+	case IRQSTATE_SOFTIRQ_DOUBLE_ENTRY:
+		rv_irqstate_ops.handle_softirq_entry(NULL, true, false);
+		goto out;
+	case IRQSTATE_SOFTIRQ_DOUBLE_EXIT:
+		rv_irqstate_ops.handle_softirq_exit(NULL, true, false);
+		break;
+	case IRQSTATE_SOFTIRQ_VALID:
+		break;
+	case IRQSTATE_SOFTIRQ_TRACEPOINT_ONLY:
+	case IRQSTATE_SOFTIRQ_SYNC:
+	case IRQSTATE_SOFTIRQ_SYNC_INTERRUPTED:
+		WARN_ON_ONCE(1);
+		break;
+	}
+
+	rv_irqstate_ops.handle_softirq_exit(NULL, true, false);
+out:
+	complete(&softirq_ctx->done);
+}
+
+static bool run_irqstate_softirq_case(enum irqstate_softirq_test test, int *sync_rx)
+{
+	struct irqstate_softirq_ctx softirq_ctx = {
+		.test = test,
+		.reactions_after_sync = -1,
+	};
+	unsigned long completed;
+
+	init_completion(&softirq_ctx.done);
+	tasklet_setup(&softirq_ctx.tasklet, run_irqstate_softirq);
+	tasklet_schedule(&softirq_ctx.tasklet);
+	completed = wait_for_completion_timeout(&softirq_ctx.done, HZ);
+	tasklet_kill(&softirq_ctx.tasklet);
+	if (sync_rx)
+		*sync_rx = softirq_ctx.reactions_after_sync;
+
+	return completed != 0;
+}
+
+static bool exercise_irqstate_softirq(enum irqstate_softirq_test test)
+{
+	return run_irqstate_softirq_case(test, NULL);
+}
+
+#ifndef MODULE
+static void attach_irqstate_test_probes(void)
+{
+	/* Match RV core: do not process an incomplete set of probes. */
+	rv_irqstate_ops.mon.rv_this->enabled = 0;
+	rv_irqstate_ops.attach();
+	rv_irqstate_ops.mon.rv_this->reset();
+	rv_irqstate_ops.mon.rv_this->enabled = 1;
+}
+
+static void detach_irqstate_test_probes(void *unused)
+{
+	rv_irqstate_ops.mon.rv_this->enabled = 0;
+	rv_irqstate_ops.detach();
+	tracepoint_synchronize_unregister();
+}
+
+#define IRQSTATE_LIVE_CYCLES	2
+#define IRQSTATE_LIVE_BATCH	2
+
+struct irqstate_live_toggle_ctx {
+	struct task_struct *owner;
+	int pairs;
+	int model_entries;
+	int model_exits;
+	int enable_overlaps;
+	int disable_overlaps;
+	int pair_state_errors;
+	bool monitor_initialized;
+	bool observer_attached;
+	bool hook_installed;
+	bool pair_active;
+};
+
+static void irqstate_live_emit_pair(struct irqstate_live_toggle_ctx *live)
+{
+	unsigned int preempt_count_before;
+	int lockdep_context_before;
+	bool irqs_disabled_before;
+	unsigned long flags;
+
+	irqs_disabled_before = irqs_disabled();
+	preempt_count_before = preempt_count();
+	if (irqs_disabled_before || in_interrupt())
+		live->pair_state_errors++;
+
+	/* Emit only the state and events whose probe lifetime is under test. */
+	local_irq_save(flags);
+	lockdep_context_before = lockdep_hardirq_context();
+	WRITE_ONCE(live->pair_active, true);
+	__irq_enter_raw();
+	if (hardirq_count() != HARDIRQ_OFFSET || !irqs_disabled() ||
+	    (IS_ENABLED(CONFIG_TRACE_IRQFLAGS) && !lockdep_hardirq_context()))
+		live->pair_state_errors++;
+	trace_hardirq_context_entry(hardirq_count() >> HARDIRQ_SHIFT);
+	trace_hardirq_context_exit(hardirq_count() >> HARDIRQ_SHIFT);
+	__irq_exit_raw();
+	WRITE_ONCE(live->pair_active, false);
+
+	if (preempt_count() != preempt_count_before || !irqs_disabled() ||
+	    lockdep_hardirq_context() != lockdep_context_before)
+		live->pair_state_errors++;
+	local_irq_restore(flags);
+	if (irqs_disabled() != irqs_disabled_before)
+		live->pair_state_errors++;
+	live->pairs++;
+}
+
+static void irqstate_live_emit_pairs(struct irqstate_live_toggle_ctx *live,
+				     unsigned int nr)
+{
+	while (nr--)
+		irqstate_live_emit_pair(live);
+}
+
+static void irqstate_live_lifecycle_hook(void *data, bool enabling)
+{
+	struct irqstate_live_toggle_ctx *live = data;
+
+	irqstate_live_emit_pair(live);
+	if (enabling)
+		live->enable_overlaps++;
+	else
+		live->disable_overlaps++;
+}
+
+static void irqstate_live_toggle_observe(void *data, char *state,
+					 char *event, char *next_state,
+					 bool final_state)
+{
+	struct irqstate_live_toggle_ctx *live = data;
+
+	if (current != READ_ONCE(live->owner) ||
+	    !READ_ONCE(live->pair_active))
+		return;
+	/*
+	 * Count only the synthetic boundary pair emitted while the lifecycle
+	 * hook has stopped probe attachment or detachment at its midpoint.
+	 */
+	if (!strcmp(state, "idle") &&
+	    !strcmp(event, "hardirq_entry") &&
+	    !strcmp(next_state, "in_hardirq"))
+		live->model_entries++;
+	else if (!strcmp(state, "in_hardirq") &&
+		 !strcmp(event, "hardirq_exit") &&
+		 !strcmp(next_state, "idle"))
+		live->model_exits++;
+}
+
+static void irqstate_live_toggle_cleanup(void *data)
+{
+	struct irqstate_live_toggle_ctx *live = data;
+	struct rv_monitor *mon = rv_irqstate_ops.mon.rv_this;
+
+	if (live->hook_installed) {
+		rv_irqstate_ops.set_lifecycle_hook(NULL, NULL);
+		live->hook_installed = false;
+	}
+	mon->enabled = 0;
+	if (live->monitor_initialized) {
+		mon->disable();
+		live->monitor_initialized = false;
+	}
+	if (live->observer_attached) {
+		unregister_trace_event_irqstate(irqstate_live_toggle_observe, live);
+		live->observer_attached = false;
+	}
+	tracepoint_synchronize_unregister();
+
+	RV_KUNIT_EXPECT_NO_REACTION(kunit_get_current_test(), active_ctx);
+	mon->react = mon->reactor ? mon->reactor->react : NULL;
+	active_ctx = NULL;
+	rv_mock_current(NULL);
+}
+
+static void rv_test_irqstate_live_toggle(struct kunit *test)
+{
+	struct rv_monitor *mon = rv_irqstate_ops.mon.rv_this;
+	struct rv_kunit_ctx *ctx = test->priv;
+	struct irqstate_live_toggle_ctx *live;
+	int disabled_entries, disabled_exits, cycle;
+	int ret;
+
+	KUNIT_ASSERT_FALSE(test, mon->enabled);
+	live = kunit_kzalloc(test, sizeof(*live), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, live);
+	live->owner = current;
+	active_ctx = ctx;
+	mon->react = rv_kunit_mock_react;
+	rv_mock_current(NULL);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test,
+						  irqstate_live_toggle_cleanup,
+						  live));
+
+	ret = register_trace_event_irqstate(irqstate_live_toggle_observe, live);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	live->observer_attached = true;
+
+	rv_irqstate_ops.set_lifecycle_hook(irqstate_live_lifecycle_hook, live);
+	live->hook_installed = true;
+	irqstate_live_emit_pairs(live, IRQSTATE_LIVE_BATCH);
+	KUNIT_EXPECT_EQ(test, live->model_entries, 0);
+	KUNIT_EXPECT_EQ(test, live->model_exits, 0);
+
+	for (cycle = 0; cycle < IRQSTATE_LIVE_CYCLES; cycle++) {
+		disabled_entries = live->model_entries;
+		disabled_exits = live->model_exits;
+		ret = mon->enable();
+		if (!ret) {
+			live->monitor_initialized = true;
+			mon->enabled = 1;
+		}
+		KUNIT_ASSERT_EQ(test, ret, 0);
+		KUNIT_EXPECT_EQ(test, live->model_entries,
+				disabled_entries);
+		KUNIT_EXPECT_EQ(test, live->model_exits,
+				disabled_exits);
+
+		irqstate_live_emit_pairs(live, IRQSTATE_LIVE_BATCH);
+		KUNIT_EXPECT_EQ(test, live->model_entries,
+				disabled_entries + IRQSTATE_LIVE_BATCH);
+		KUNIT_EXPECT_EQ(test, live->model_exits,
+				disabled_exits + IRQSTATE_LIVE_BATCH);
+
+		disabled_entries = live->model_entries;
+		disabled_exits = live->model_exits;
+		mon->enabled = 0;
+		mon->disable();
+		live->monitor_initialized = false;
+		tracepoint_synchronize_unregister();
+		KUNIT_EXPECT_EQ(test, live->model_entries,
+				disabled_entries);
+		KUNIT_EXPECT_EQ(test, live->model_exits,
+				disabled_exits);
+		irqstate_live_emit_pairs(live, IRQSTATE_LIVE_BATCH);
+		KUNIT_EXPECT_EQ(test, live->model_entries,
+				disabled_entries);
+		KUNIT_EXPECT_EQ(test, live->model_exits,
+				disabled_exits);
+	}
+
+	KUNIT_EXPECT_EQ(test, live->pairs,
+			IRQSTATE_LIVE_BATCH + IRQSTATE_LIVE_CYCLES *
+			(2 + 2 * IRQSTATE_LIVE_BATCH));
+	KUNIT_EXPECT_GT(test, live->model_entries, 0);
+	KUNIT_EXPECT_GT(test, live->model_exits, 0);
+	KUNIT_EXPECT_EQ(test, live->enable_overlaps,
+			IRQSTATE_LIVE_CYCLES);
+	KUNIT_EXPECT_EQ(test, live->disable_overlaps,
+			IRQSTATE_LIVE_CYCLES);
+	KUNIT_EXPECT_EQ(test, live->pair_state_errors, 0);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	kunit_info(test, "live toggle cycles=%d pairs=%d model=%d/%d overlap=%d/%d state_errors=%d\n",
+		   cycle,
+		   live->pairs,
+		   live->model_entries,
+		   live->model_exits,
+		   live->enable_overlaps,
+		   live->disable_overlaps,
+		   live->pair_state_errors);
+}
+#else
+static void rv_test_irqstate_live_toggle(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+#endif
+
+#ifndef MODULE
+struct irqstate_shared_actions_ctx {
+	struct irqaction actions[2];
+	struct task_struct *owner;
+	int owner_cpu;
+	int irq;
+	int results[2];
+	unsigned int entries;
+	unsigned int exits;
+	unsigned int entry_mask;
+	unsigned int exit_mask;
+	unsigned int event_index;
+	bool armed;
+	bool bad_order;
+	bool bad_context;
+	bool bad_owner;
+	bool bad_cpu;
+	bool bad_irq;
+	bool bad_result;
+	bool entry_attached;
+	bool exit_attached;
+};
+
+static int irqstate_shared_action_index(struct irqstate_shared_actions_ctx *shared_ctx,
+					struct irqaction *action)
+{
+	if (action == &shared_ctx->actions[0])
+		return 0;
+	if (action == &shared_ctx->actions[1])
+		return 1;
+
+	return -1;
+}
+
+static void irqstate_shared_action_entry(void *data, int irq,
+					 struct irqaction *action)
+{
+	struct irqstate_shared_actions_ctx *shared_ctx = data;
+	int index;
+
+	/* Filter globally registered callbacks by exact action identity first. */
+	index = irqstate_shared_action_index(shared_ctx, action);
+	if (index < 0 || !READ_ONCE(shared_ctx->armed))
+		return;
+
+	shared_ctx->entries++;
+	shared_ctx->entry_mask |= BIT(index);
+	if (shared_ctx->event_index != index * 2)
+		shared_ctx->bad_order = true;
+	shared_ctx->event_index++;
+	if (!in_hardirq() || hardirq_count() != HARDIRQ_OFFSET ||
+	    !irqs_disabled())
+		shared_ctx->bad_context = true;
+	if (current != shared_ctx->owner)
+		shared_ctx->bad_owner = true;
+	if (raw_smp_processor_id() != shared_ctx->owner_cpu)
+		shared_ctx->bad_cpu = true;
+	if (irq != shared_ctx->irq)
+		shared_ctx->bad_irq = true;
+}
+
+static void irqstate_shared_action_exit(void *data, int irq,
+					struct irqaction *action, int ret)
+{
+	struct irqstate_shared_actions_ctx *shared_ctx = data;
+	int index;
+
+	/* Filter globally registered callbacks by exact action identity first. */
+	index = irqstate_shared_action_index(shared_ctx, action);
+	if (index < 0 || !READ_ONCE(shared_ctx->armed))
+		return;
+
+	shared_ctx->exits++;
+	shared_ctx->exit_mask |= BIT(index);
+	if (shared_ctx->event_index != index * 2 + 1)
+		shared_ctx->bad_order = true;
+	shared_ctx->event_index++;
+	if (!in_hardirq() || hardirq_count() != HARDIRQ_OFFSET ||
+	    !irqs_disabled())
+		shared_ctx->bad_context = true;
+	if (current != shared_ctx->owner)
+		shared_ctx->bad_owner = true;
+	if (raw_smp_processor_id() != shared_ctx->owner_cpu)
+		shared_ctx->bad_cpu = true;
+	if (irq != shared_ctx->irq)
+		shared_ctx->bad_irq = true;
+	if (ret != shared_ctx->results[index])
+		shared_ctx->bad_result = true;
+}
+
+static void irqstate_shared_actions_detach(void *data)
+{
+	struct irqstate_shared_actions_ctx *shared_ctx = data;
+
+	WRITE_ONCE(shared_ctx->armed, false);
+	if (shared_ctx->exit_attached)
+		unregister_trace_irq_handler_exit(irqstate_shared_action_exit,
+						  shared_ctx);
+	if (shared_ctx->entry_attached)
+		unregister_trace_irq_handler_entry(irqstate_shared_action_entry,
+						   shared_ctx);
+	tracepoint_synchronize_unregister();
+}
+#endif
+
+static void rv_test_irqstate(struct kunit *test)
+{
+	struct rv_kunit_ctx *ctx = test->priv;
+#ifndef MODULE
+	struct irqaction action = {
+		.name = "rv-kunit",
+	};
+#endif
+	unsigned long flags;
+	int reactions[8];
+	unsigned int i;
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	/* Per-CPU monitor, make sure we do not change CPU mid-test. */
+	guard(migrate)();
+
+	/* Context-boundary events must agree with the live context state. */
+	RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+		rv_irqstate_ops.handle_hardirq_entry(NULL, 0);
+	RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 0);
+	RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+		rv_irqstate_ops.handle_softirq_entry(NULL, false, false);
+	RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+		rv_irqstate_ops.handle_softirq_exit(NULL, false, false);
+
+	/* Disabling BH does not mean that a softirq is being served. */
+	local_bh_disable();
+	reactions[0] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_softirq_entry(NULL, false, false);
+	reactions[1] = READ_ONCE(ctx->reactions);
+	reactions[2] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_softirq_exit(NULL, false, false);
+	reactions[3] = READ_ONCE(ctx->reactions);
+	local_bh_enable();
+	for (i = 0; i < 4; i++)
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[i], i & 1);
+
+	/* Nested hardirqs leave the outer hardirq automaton state unchanged. */
+	local_irq_save(flags);
+	__irq_enter_raw();
+	rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+	__irq_enter_raw();
+	rv_irqstate_ops.handle_hardirq_entry(NULL, 2);
+	rv_irqstate_ops.handle_hardirq_exit(NULL, 2);
+	__irq_exit_raw();
+	rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+	reactions[0] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+
+	/* A second outer entry is not a nested interrupt. */
+	local_irq_save(flags);
+	__irq_enter_raw();
+	rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+	reactions[0] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+	reactions[1] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+	reactions[2] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 1);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 0);
+
+	/* An exit without its corresponding entry is invalid after sync. */
+	local_irq_save(flags);
+	__irq_enter_raw();
+	reactions[0] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+	reactions[1] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 1);
+
+	/* Exercise valid and deliberately invalid serving-softirq sequences. */
+	KUNIT_ASSERT_TRUE(test, exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_VALID));
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	KUNIT_ASSERT_TRUE(test,
+			  exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_INTERRUPTED));
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	KUNIT_ASSERT_TRUE(test,
+			  exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_DOUBLE_ENTRY));
+	RV_KUNIT_EXPECT_REACTION(test, ctx);
+	KUNIT_ASSERT_TRUE(test,
+			  exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_DOUBLE_EXIT));
+	RV_KUNIT_EXPECT_REACTION(test, ctx);
+
+#ifndef MODULE
+	/* Exercise the actual generic tracepoint attachments as well. */
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	/* IRQ action events no longer drive the context-boundary monitor. */
+	trace_irq_handler_entry(0, &action);
+	trace_irq_handler_exit(0, &action, IRQ_HANDLED);
+	trace_softirq_entry(0);
+	trace_softirq_exit(0);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+
+	/* Manually invalid boundary events prove that all probes are attached. */
+	local_irq_save(flags);
+	reactions[0] = READ_ONCE(ctx->reactions);
+	trace_hardirq_context_entry(0);
+	reactions[1] = READ_ONCE(ctx->reactions);
+	reactions[2] = READ_ONCE(ctx->reactions);
+	trace_hardirq_context_exit(0);
+	reactions[3] = READ_ONCE(ctx->reactions);
+	reactions[4] = READ_ONCE(ctx->reactions);
+	trace_softirq_context_entry(false, false);
+	reactions[5] = READ_ONCE(ctx->reactions);
+	reactions[6] = READ_ONCE(ctx->reactions);
+	trace_softirq_context_exit(false, false);
+	reactions[7] = READ_ONCE(ctx->reactions);
+	local_irq_restore(flags);
+	for (i = 0; i < ARRAY_SIZE(reactions); i++)
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[i], i & 1);
+
+	/* These helpers emit valid hardirq context-boundary tracepoints. */
+	local_irq_save(flags);
+	irq_enter_rcu();
+	irq_exit_rcu();
+	reactions[0] = READ_ONCE(ctx->reactions);
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+
+	/* This tasklet relies only on the live softirq context tracepoints. */
+	KUNIT_ASSERT_TRUE(test,
+			  exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_TRACEPOINT_ONLY));
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+#endif
+}
+
+#ifndef MODULE
+static void rv_test_irqstate_shared_actions(struct kunit *test)
+{
+	struct irqstate_shared_actions_ctx *shared_ctx;
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int saved_count, saved_softirq;
+	unsigned long flags;
+	int ret;
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	shared_ctx = kunit_kzalloc(test, sizeof(*shared_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, shared_ctx);
+	shared_ctx->actions[0].name = "rv-kunit-none";
+	shared_ctx->actions[1].name = "rv-kunit-handled";
+	shared_ctx->results[0] = IRQ_NONE;
+	shared_ctx->results[1] = IRQ_HANDLED;
+	shared_ctx->irq = 42;
+	shared_ctx->owner = current;
+	shared_ctx->owner_cpu = raw_smp_processor_id();
+
+	ret = register_trace_irq_handler_entry(irqstate_shared_action_entry,
+					       shared_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	shared_ctx->entry_attached = true;
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test,
+						  irqstate_shared_actions_detach,
+						  shared_ctx));
+	ret = register_trace_irq_handler_exit(irqstate_shared_action_exit,
+					      shared_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	shared_ctx->exit_attached = true;
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	saved_count = preempt_count();
+	saved_softirq = softirq_count();
+	WRITE_ONCE(shared_ctx->armed, true);
+	local_irq_save(flags);
+	irq_enter_rcu();
+	trace_irq_handler_entry(shared_ctx->irq, &shared_ctx->actions[0]);
+	trace_irq_handler_exit(shared_ctx->irq, &shared_ctx->actions[0],
+			       shared_ctx->results[0]);
+	trace_irq_handler_entry(shared_ctx->irq, &shared_ctx->actions[1]);
+	trace_irq_handler_exit(shared_ctx->irq, &shared_ctx->actions[1],
+			       shared_ctx->results[1]);
+	irq_exit_rcu();
+	WRITE_ONCE(shared_ctx->armed, false);
+	local_irq_restore(flags);
+
+	KUNIT_EXPECT_EQ(test, shared_ctx->entries, 2U);
+	KUNIT_EXPECT_EQ(test, shared_ctx->exits, 2U);
+	KUNIT_EXPECT_EQ(test, shared_ctx->entry_mask, 3U);
+	KUNIT_EXPECT_EQ(test, shared_ctx->exit_mask, 3U);
+	KUNIT_EXPECT_EQ(test, shared_ctx->event_index, 4U);
+	KUNIT_EXPECT_FALSE(test, shared_ctx->bad_order);
+	KUNIT_EXPECT_FALSE(test, shared_ctx->bad_context);
+	KUNIT_EXPECT_FALSE(test, shared_ctx->bad_owner);
+	KUNIT_EXPECT_FALSE(test, shared_ctx->bad_cpu);
+	KUNIT_EXPECT_FALSE(test, shared_ctx->bad_irq);
+	KUNIT_EXPECT_FALSE(test, shared_ctx->bad_result);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	kunit_info(test,
+		   "shared actions events=%u/%u order=%u context=%u same_task=%u same_cpu=%u restored=%u\n",
+		   shared_ctx->entries, shared_ctx->exits,
+		   !shared_ctx->bad_order, !shared_ctx->bad_context,
+		   !shared_ctx->bad_owner, !shared_ctx->bad_cpu,
+		   preempt_count() == saved_count &&
+		   softirq_count() == saved_softirq);
+}
+#else
+static void rv_test_irqstate_shared_actions(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+#endif
+
+static void rv_test_irqstate_depth(struct kunit *test)
+{
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int max_depth = HARDIRQ_MASK >> HARDIRQ_SHIFT;
+	unsigned int observed_depth, saved_count;
+	unsigned long flags;
+	int reactions[3];
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	saved_count = preempt_count();
+	local_irq_save(flags);
+	for (unsigned int depth = 1; depth <= max_depth; depth++) {
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_entry(NULL, depth);
+	}
+	observed_depth = hardirq_count() >> HARDIRQ_SHIFT;
+	for (unsigned int depth = max_depth; depth; depth--) {
+		rv_irqstate_ops.handle_hardirq_exit(NULL, depth);
+		__irq_exit_raw();
+	}
+	reactions[0] = READ_ONCE(ctx->reactions);
+	/* A silent adapter cannot pass: the completed outer pair armed idle. */
+	__irq_enter_raw();
+	reactions[1] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+	reactions[2] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 0);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 1);
+	KUNIT_EXPECT_EQ(test, observed_depth, max_depth);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	kunit_info(test, "hardirq max_depth=%u observed=%u restored=%u\n",
+		   max_depth, observed_depth, preempt_count() == saved_count);
+}
+
+static void rv_test_irqstate_depth_overflow(struct kunit *test)
+{
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int max_depth = HARDIRQ_MASK >> HARDIRQ_SHIFT;
+	unsigned int observed_depth, saved_count, saved_nmi;
+	unsigned int nmi_at_max, nmi_at_overflow;
+	int reactions_before, reactions_before_overflow;
+	int reactions_at_overflow, reactions_after_unwind;
+	unsigned long flags;
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	saved_count = preempt_count();
+	saved_nmi = nmi_count();
+	reactions_before = ctx->reactions;
+
+	local_irq_save(flags);
+	for (unsigned int depth = 1; depth <= max_depth; depth++) {
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_entry(NULL, depth);
+	}
+	observed_depth = hardirq_count() >> HARDIRQ_SHIFT;
+	nmi_at_max = nmi_count();
+	reactions_before_overflow = ctx->reactions;
+
+	/*
+	 * A real increment here would carry into NMI_MASK. Feed the resulting
+	 * wrapped depth directly to the adapter without corrupting preempt_count.
+	 */
+	rv_irqstate_ops.handle_hardirq_entry(NULL, 0);
+	nmi_at_overflow = nmi_count();
+	reactions_at_overflow = ctx->reactions;
+	for (unsigned int depth = max_depth; depth; depth--) {
+		rv_irqstate_ops.handle_hardirq_exit(NULL, depth);
+		__irq_exit_raw();
+	}
+	reactions_after_unwind = ctx->reactions;
+	local_irq_restore(flags);
+
+	KUNIT_EXPECT_EQ(test, observed_depth, max_depth);
+	KUNIT_EXPECT_EQ(test, reactions_before_overflow, reactions_before);
+	KUNIT_EXPECT_EQ(test, reactions_at_overflow,
+			reactions_before_overflow + 1);
+	KUNIT_EXPECT_EQ(test, reactions_after_unwind, reactions_at_overflow);
+	RV_KUNIT_EXPECT_REACTION(test, ctx);
+	KUNIT_EXPECT_EQ(test, nmi_at_max, saved_nmi);
+	KUNIT_EXPECT_EQ(test, nmi_at_overflow, saved_nmi);
+	KUNIT_EXPECT_EQ(test, nmi_count(), saved_nmi);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	kunit_info(test,
+		   "hardirq overflow max_depth=%u observed=%u reactions=%d restored=%u\n",
+		   max_depth, observed_depth,
+		   reactions_after_unwind - reactions_before,
+		   preempt_count() == saved_count && nmi_count() == saved_nmi);
+}
+
+static void rv_test_irqstate_sync(struct kunit *test)
+{
+	struct rv_kunit_ctx *ctx = test->priv;
+	struct rv_monitor *mon = rv_irqstate_ops.mon.rv_this;
+	unsigned int saved_count;
+	unsigned long flags;
+	int expected, rx_at_sync;
+	int reactions[3];
+	bool completed;
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	saved_count = preempt_count();
+	for (unsigned int depth = 1; depth <= 2; depth++) {
+		local_irq_save(flags);
+		mon->enabled = 0;
+		for (unsigned int level = 1; level <= depth; level++) {
+			__irq_enter_raw();
+			rv_irqstate_ops.handle_hardirq_entry(NULL, level);
+		}
+		mon->reset();
+		mon->enabled = 1;
+		for (unsigned int level = depth; level; level--) {
+			rv_irqstate_ops.handle_hardirq_exit(NULL, level);
+			__irq_exit_raw();
+		}
+		reactions[0] = READ_ONCE(ctx->reactions);
+		/* The first unmatched exit after synchronization must now fail. */
+		__irq_enter_raw();
+		reactions[1] = READ_ONCE(ctx->reactions);
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		reactions[2] = READ_ONCE(ctx->reactions);
+		__irq_exit_raw();
+		local_irq_restore(flags);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 0);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 1);
+	}
+
+	expected = ctx->expected;
+	completed = run_irqstate_softirq_case(IRQSTATE_SOFTIRQ_SYNC, &rx_at_sync);
+	KUNIT_ASSERT_TRUE(test, completed);
+	KUNIT_EXPECT_EQ(test, rx_at_sync, expected);
+	RV_KUNIT_EXPECT_REACTION(test, ctx);
+	expected = ctx->expected;
+	completed = run_irqstate_softirq_case(IRQSTATE_SOFTIRQ_SYNC_INTERRUPTED, &rx_at_sync);
+	KUNIT_ASSERT_TRUE(test, completed);
+	KUNIT_EXPECT_EQ(test, rx_at_sync, expected);
+	RV_KUNIT_EXPECT_REACTION(test, ctx);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	kunit_info(test, "sync hardirq=1 nested=2 softirq=1 interrupted_softirq=1 restored=%u\n",
+		   preempt_count() == saved_count);
+}
+
+#ifndef MODULE
+struct irqstate_preempt_ctx {
+	struct tasklet_struct tasklet;
+	struct task_struct *worker;
+	struct completion tasklet_done;
+	struct completion worker_done;
+	bool preempted;
+	bool worker_in_softirq;
+};
+
+struct irqstate_inline_bh_ctx {
+	struct tasklet_struct tasklet;
+	struct tasklet_struct exit_tasklet;
+	struct task_struct *owner;
+	struct task_struct *softirqd;
+	int owner_cpu;
+	unsigned int entries;
+	unsigned int exits;
+	unsigned int tasklet_runs;
+	unsigned int exit_tasklet_runs;
+	bool armed;
+	bool raise_on_exit;
+	bool exit_tasklet_pending;
+	bool softirqd_already_runnable;
+	bool bad_boundary_context;
+	bool bad_boundary_cpu;
+	bool tasklet_serving;
+	bool tasklet_same_task;
+	bool tasklet_same_cpu;
+	bool tasklet_irqs_enabled;
+	bool tasklet_in_hardirq;
+	bool tasklet_bracketed;
+	bool entry_attached;
+	bool exit_attached;
+};
+
+static void irqstate_inline_bh_entry(void *data, bool serving,
+				     bool from_hardirq)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx = data;
+
+	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner)
+		return;
+	inline_ctx->entries++;
+	if (!serving || !in_serving_softirq() || !irqs_disabled())
+		inline_ctx->bad_boundary_context = true;
+	if (raw_smp_processor_id() != inline_ctx->owner_cpu)
+		inline_ctx->bad_boundary_cpu = true;
+}
+
+static void irqstate_inline_bh_exit(void *data, bool serving,
+				    bool from_hardirq)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx = data;
+
+	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner)
+		return;
+	inline_ctx->exits++;
+	if (!serving || !in_serving_softirq() || !irqs_disabled())
+		inline_ctx->bad_boundary_context = true;
+	if (raw_smp_processor_id() != inline_ctx->owner_cpu)
+		inline_ctx->bad_boundary_cpu = true;
+	if (READ_ONCE(inline_ctx->raise_on_exit)) {
+		WRITE_ONCE(inline_ctx->raise_on_exit, false);
+		tasklet_schedule(&inline_ctx->exit_tasklet);
+	}
+}
+
+static void irqstate_inline_bh_tasklet(struct tasklet_struct *tasklet)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx;
+
+	inline_ctx = from_tasklet(inline_ctx, tasklet, tasklet);
+	inline_ctx->tasklet_runs++;
+	inline_ctx->tasklet_serving = !!in_serving_softirq();
+	inline_ctx->tasklet_same_task = current == inline_ctx->owner;
+	inline_ctx->tasklet_same_cpu = raw_smp_processor_id() ==
+					inline_ctx->owner_cpu;
+	inline_ctx->tasklet_irqs_enabled = !irqs_disabled();
+	inline_ctx->tasklet_in_hardirq = !!in_hardirq();
+	inline_ctx->tasklet_bracketed = inline_ctx->entries == 1 &&
+					 inline_ctx->exits == 0;
+}
+
+static void irqstate_inline_bh_exit_tasklet(struct tasklet_struct *tasklet)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx;
+
+	inline_ctx = from_tasklet(inline_ctx, tasklet, exit_tasklet);
+	inline_ctx->exit_tasklet_runs++;
+}
+
+static void irqstate_inline_bh_kill(void *data)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx = data;
+
+	tasklet_kill(&inline_ctx->tasklet);
+}
+
+static void irqstate_inline_bh_kill_all(void *data)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx = data;
+
+	tasklet_kill(&inline_ctx->tasklet);
+	tasklet_kill(&inline_ctx->exit_tasklet);
+}
+
+static void irqstate_inline_bh_detach(void *data)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx = data;
+
+	WRITE_ONCE(inline_ctx->armed, false);
+	if (inline_ctx->entry_attached)
+		unregister_trace_softirq_context_entry(irqstate_inline_bh_entry, inline_ctx);
+	if (inline_ctx->exit_attached)
+		unregister_trace_softirq_context_exit(irqstate_inline_bh_exit, inline_ctx);
+	tracepoint_synchronize_unregister();
+}
+
+/* Exercise non-RT inline softirq processing from a real hardirq exit. */
+static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx;
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int saved_count, saved_softirq;
+	unsigned long flags;
+	bool ran_inline;
+	int ret;
+
+	if (IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		kunit_skip(test, "Requires non-RT IRQ-exit softirq processing");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	inline_ctx = kunit_kzalloc(test, sizeof(*inline_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, inline_ctx);
+	tasklet_setup(&inline_ctx->tasklet, irqstate_inline_bh_tasklet);
+	tasklet_setup(&inline_ctx->exit_tasklet,
+		      irqstate_inline_bh_exit_tasklet);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, irqstate_inline_bh_kill_all,
+						  inline_ctx));
+
+	ret = register_trace_softirq_context_entry(irqstate_inline_bh_entry,
+						   inline_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	inline_ctx->entry_attached = true;
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, irqstate_inline_bh_detach,
+						  inline_ctx));
+	ret = register_trace_softirq_context_exit(irqstate_inline_bh_exit,
+						  inline_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	inline_ctx->exit_attached = true;
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	KUNIT_ASSERT_FALSE(test, in_interrupt());
+	KUNIT_ASSERT_FALSE(test, irqs_disabled());
+	KUNIT_ASSERT_EQ(test, softirq_count(), 0U);
+	guard(migrate)();
+	inline_ctx->owner = current;
+	inline_ctx->owner_cpu = raw_smp_processor_id();
+	inline_ctx->softirqd = this_cpu_ksoftirqd();
+	KUNIT_ASSERT_NOT_NULL(test, inline_ctx->softirqd);
+	if (local_softirq_pending() ||
+	    !wait_task_inactive(inline_ctx->softirqd, TASK_INTERRUPTIBLE)) {
+		kunit_skip(test, "Requires an idle local ksoftirqd");
+		return;
+	}
+	saved_count = preempt_count();
+	saved_softirq = softirq_count();
+	local_irq_save(flags);
+	if (local_softirq_pending() || task_is_running(inline_ctx->softirqd)) {
+		local_irq_restore(flags);
+		kunit_skip(test, "Local ksoftirqd stopped being idle");
+		return;
+	}
+	WRITE_ONCE(inline_ctx->armed, true);
+	WRITE_ONCE(inline_ctx->raise_on_exit, true);
+	irq_enter_rcu();
+	tasklet_schedule(&inline_ctx->tasklet);
+	irq_exit_rcu();
+	ran_inline = inline_ctx->tasklet_runs != 0;
+	inline_ctx->exit_tasklet_pending =
+		!!(local_softirq_pending() & BIT(TASKLET_SOFTIRQ));
+	/*
+	 * A zero return means the exit handoff already made ksoftirqd runnable.
+	 * A one return also makes the failure path safe to drain.
+	 */
+	inline_ctx->softirqd_already_runnable =
+		!wake_up_process(inline_ctx->softirqd);
+	WRITE_ONCE(inline_ctx->armed, false);
+	local_irq_restore(flags);
+
+	/* Drain a tasklet diverted to ksoftirqd before inspecting or skipping. */
+	kunit_release_action(test, irqstate_inline_bh_kill_all, inline_ctx);
+	if (!ran_inline) {
+		kunit_skip(test, "IRQ exit diverted pending softirq to ksoftirqd");
+		return;
+	}
+
+	KUNIT_EXPECT_EQ(test, inline_ctx->tasklet_runs, 1U);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_serving);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_same_task);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_same_cpu);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_irqs_enabled);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->tasklet_in_hardirq);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_bracketed);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->exit_tasklet_pending);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->softirqd_already_runnable);
+	KUNIT_EXPECT_EQ(test, inline_ctx->exit_tasklet_runs, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->entries, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->exits, 1U);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_context);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_cpu);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	kunit_info(test,
+		   "IRQ-exit inline tasklets=%u/%u handoff=%u events=%u/%u restored=%u\n",
+		   inline_ctx->tasklet_runs, inline_ctx->exit_tasklet_runs,
+		   inline_ctx->softirqd_already_runnable,
+		   inline_ctx->entries, inline_ctx->exits,
+		   preempt_count() == saved_count &&
+		   softirq_count() == saved_softirq);
+}
+
+/* Force the RT BH-disabled to inline-serving transition. */
+static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx;
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int saved_count, saved_softirq;
+	int ret;
+
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		kunit_skip(test, "Requires RT inline softirq processing");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	inline_ctx = kunit_kzalloc(test, sizeof(*inline_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, inline_ctx);
+	inline_ctx->owner = current;
+	tasklet_setup(&inline_ctx->tasklet, irqstate_inline_bh_tasklet);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, irqstate_inline_bh_kill,
+						  inline_ctx));
+
+	ret = register_trace_softirq_context_entry(irqstate_inline_bh_entry, inline_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	inline_ctx->entry_attached = true;
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, irqstate_inline_bh_detach,
+						  inline_ctx));
+	ret = register_trace_softirq_context_exit(irqstate_inline_bh_exit, inline_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	inline_ctx->exit_attached = true;
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	KUNIT_ASSERT_TRUE(test, preemptible());
+	KUNIT_ASSERT_EQ(test, softirq_count(), 0U);
+	saved_count = preempt_count();
+	saved_softirq = softirq_count();
+	local_bh_disable();
+	inline_ctx->owner_cpu = raw_smp_processor_id();
+	WRITE_ONCE(inline_ctx->armed, true);
+	tasklet_schedule(&inline_ctx->tasklet);
+	local_bh_enable();
+	WRITE_ONCE(inline_ctx->armed, false);
+	kunit_release_action(test, irqstate_inline_bh_kill, inline_ctx);
+
+	KUNIT_EXPECT_EQ(test, inline_ctx->tasklet_runs, 1U);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_serving);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_same_task);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_same_cpu);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_irqs_enabled);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_bracketed);
+	KUNIT_EXPECT_EQ(test, inline_ctx->entries, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->exits, 1U);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_context);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_cpu);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	kunit_info(test,
+		   "RT inline BH tasklet=%u same_task=%u same_cpu=%u serving=%u events=%u/%u restored=%u\n",
+		   inline_ctx->tasklet_runs, inline_ctx->tasklet_same_task,
+		   inline_ctx->tasklet_same_cpu, inline_ctx->tasklet_serving,
+		   inline_ctx->entries, inline_ctx->exits,
+		   preempt_count() == saved_count &&
+		   softirq_count() == saved_softirq);
+}
+
+static int irqstate_preempt_worker(void *arg)
+{
+	struct irqstate_preempt_ctx *preempt_ctx = arg;
+	unsigned long flags;
+
+	preempt_ctx->worker_in_softirq = !!in_serving_softirq();
+	local_irq_save(flags);
+	irq_enter_rcu();
+	irq_exit_rcu();
+	local_irq_restore(flags);
+	complete(&preempt_ctx->worker_done);
+
+	for (;;) {
+		set_current_state(TASK_INTERRUPTIBLE);
+		if (kthread_should_stop())
+			break;
+		schedule();
+	}
+	__set_current_state(TASK_RUNNING);
+	return 0;
+}
+
+static void irqstate_preempt_tasklet(struct tasklet_struct *tasklet)
+{
+	struct irqstate_preempt_ctx *preempt_ctx;
+
+	preempt_ctx = from_tasklet(preempt_ctx, tasklet, tasklet);
+	wake_up_process(preempt_ctx->worker);
+	preempt_ctx->preempted = completion_done(&preempt_ctx->worker_done);
+	complete(&preempt_ctx->tasklet_done);
+}
+
+static void stop_irqstate_preempt_worker(void *worker)
+{
+	kthread_stop(worker);
+}
+
+/* A hardirq in another task must not inherit a suspended RT softirq. */
+static void rv_test_irqstate_preempt(struct kunit *test)
+{
+	struct rv_kunit_ctx *ctx = test->priv;
+	struct irqstate_preempt_ctx *preempt_ctx;
+	struct task_struct *worker;
+	unsigned long completed;
+
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		kunit_skip(test, "Requires preemptible softirq processing");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+
+	preempt_ctx = kunit_kzalloc(test, sizeof(*preempt_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, preempt_ctx);
+	init_completion(&preempt_ctx->tasklet_done);
+	init_completion(&preempt_ctx->worker_done);
+	worker = kthread_create(irqstate_preempt_worker, preempt_ctx,
+				"irqstate-preempt");
+	KUNIT_ASSERT_FALSE(test, IS_ERR(worker));
+	preempt_ctx->worker = worker;
+	kthread_bind(worker, smp_processor_id());
+	sched_set_fifo(worker);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, stop_irqstate_preempt_worker,
+						  worker));
+
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+	tasklet_setup(&preempt_ctx->tasklet, irqstate_preempt_tasklet);
+	tasklet_schedule(&preempt_ctx->tasklet);
+	completed = wait_for_completion_timeout(&preempt_ctx->tasklet_done, HZ);
+	tasklet_kill(&preempt_ctx->tasklet);
+	KUNIT_ASSERT_NE(test, completed, 0);
+	KUNIT_ASSERT_NE(test,
+			wait_for_completion_timeout(&preempt_ctx->worker_done, HZ), 0);
+	KUNIT_EXPECT_TRUE(test, preempt_ctx->preempted);
+	KUNIT_EXPECT_FALSE(test, preempt_ctx->worker_in_softirq);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+}
+
+/* An RT serving interval starts before handle_softirqs() is entered. */
+static void rv_test_irqstate_rt_prepare_irq(struct kunit *test)
+{
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int saved_count, saved_softirq;
+	unsigned long flags;
+	int reactions[2];
+	bool serving;
+
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		kunit_skip(test, "Requires RT serving-softirq accounting");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+	saved_count = preempt_count();
+	saved_softirq = softirq_count();
+
+	/*
+	 * Keep an outer BH-disable reference so the matching enable of the
+	 * serving reference cannot dispatch pending softirqs. Both references
+	 * use the real RT helpers; no task or per-CPU counter is fabricated.
+	 */
+	local_bh_disable();
+	local_irq_save(flags);
+	irq_enter_rcu();
+	irq_exit_rcu();
+	local_irq_restore(flags);
+
+	/* The same serving-bit transition used by ksoftirqd_run_begin(). */
+	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
+	serving = !!in_serving_softirq();
+	local_irq_save(flags);
+	irq_enter_rcu();
+	irq_exit_rcu();
+	local_irq_restore(flags);
+	__local_bh_enable_ip(_RET_IP_, SOFTIRQ_OFFSET);
+	local_bh_enable();
+
+	KUNIT_EXPECT_TRUE(test, serving);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
+	kunit_info(test, "RT preparation IRQ serving=%u restored=%u\n",
+		   serving, preempt_count() == saved_count &&
+		   softirq_count() == saved_softirq);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	local_irq_save(flags);
+	__irq_enter_raw();
+	reactions[0] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+	reactions[1] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 1);
+}
+
+/* A real task switch is legal before handle_softirqs(), too. */
+static void rv_test_irqstate_rt_prepare_switch(struct kunit *test)
+{
+	struct rv_kunit_ctx *ctx = test->priv;
+	struct irqstate_preempt_ctx *preempt_ctx;
+	struct task_struct *worker;
+	unsigned int saved_count, saved_softirq;
+	unsigned long completed, flags;
+	int reactions[2];
+	bool preempted, serving;
+
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		kunit_skip(test, "Requires RT serving-softirq accounting");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	preempt_ctx = kunit_kzalloc(test, sizeof(*preempt_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, preempt_ctx);
+	init_completion(&preempt_ctx->worker_done);
+	worker = kthread_create(irqstate_preempt_worker, preempt_ctx,
+				"irqstate-prepare");
+	KUNIT_ASSERT_FALSE(test, IS_ERR(worker));
+	kthread_bind(worker, smp_processor_id());
+	sched_set_fifo(worker);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, stop_irqstate_preempt_worker,
+						  worker));
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+	saved_count = preempt_count();
+	saved_softirq = softirq_count();
+
+	local_bh_disable();
+	local_irq_save(flags);
+	irq_enter_rcu();
+	irq_exit_rcu();
+	local_irq_restore(flags);
+	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
+	serving = !!in_serving_softirq();
+	wake_up_process(worker);
+	preempted = completion_done(&preempt_ctx->worker_done);
+	__local_bh_enable_ip(_RET_IP_, SOFTIRQ_OFFSET);
+	local_bh_enable();
+
+	completed = wait_for_completion_timeout(&preempt_ctx->worker_done, HZ);
+	KUNIT_EXPECT_NE(test, completed, 0);
+	KUNIT_EXPECT_TRUE(test, serving);
+	KUNIT_EXPECT_TRUE(test, preempted);
+	KUNIT_EXPECT_FALSE(test, preempt_ctx->worker_in_softirq);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
+	kunit_info(test, "RT preparation switch serving=%u preempted=%u worker_serving=%u restored=%u\n",
+		   serving, preempted, preempt_ctx->worker_in_softirq,
+		   preempt_count() == saved_count && softirq_count() == saved_softirq);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	local_irq_save(flags);
+	__irq_enter_raw();
+	reactions[0] = READ_ONCE(ctx->reactions);
+	rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+	reactions[1] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 1);
+}
+
+#else
+static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqstate_preempt(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqstate_rt_prepare_irq(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqstate_rt_prepare_switch(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+#endif
+
+#else
+#define rv_test_irqstate rv_test_stub
+#define rv_test_irqstate_shared_actions rv_test_stub
+#define rv_test_irqstate_depth rv_test_stub
+#define rv_test_irqstate_depth_overflow rv_test_stub
+#define rv_test_irqstate_sync rv_test_stub
+#define rv_test_irqstate_live_toggle rv_test_stub
+#define rv_test_irqstate_irq_exit_inline rv_test_stub
+#define rv_test_irqstate_rt_inline_bh rv_test_stub
+#define rv_test_irqstate_preempt rv_test_stub
+#define rv_test_irqstate_rt_prepare_irq rv_test_stub
+#define rv_test_irqstate_rt_prepare_switch rv_test_stub
+#endif
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.h b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.h
new file mode 100644
index 0000000000000..4a0cba58eb662
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __IRQSTATE_KUNIT_H
+#define __IRQSTATE_KUNIT_H
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+
+#include <linux/rv.h>
+#include <rv/kunit.h>
+
+struct irqaction;
+
+extern const struct rv_irqstate_ops {
+	struct rv_kunit_mon mon;
+	void (*attach)(void);
+	void (*detach)(void);
+	void (*handle_hardirq_entry)(void *data, unsigned int depth);
+	void (*handle_hardirq_exit)(void *data, unsigned int depth);
+	void (*handle_softirq_entry)(void *data, bool serving, bool from_hardirq);
+	void (*handle_softirq_exit)(void *data, bool serving, bool from_hardirq);
+	void (*set_lifecycle_hook)(void (*hook)(void *data, bool enabling),
+				   void *data);
+} rv_irqstate_ops;
+#endif
+
+#endif /* __IRQSTATE_KUNIT_H */
diff --git a/kernel/trace/rv/rv_monitors_test.c b/kernel/trace/rv/rv_monitors_test.c
index 3ad11195e664a..4fb611a9f8b85 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -149,6 +149,7 @@ static void rv_test_dummy(struct kunit *test)
 #include "monitors/sco/sco_kunit.c"
 #include "monitors/sssw/sssw_kunit.c"
 #include "monitors/sts/sts_kunit.c"
+#include "monitors/irqstate/irqstate_kunit.c"
 #include "monitors/opid/opid_kunit.c"
 #include "monitors/nomiss/nomiss_kunit.c"
 #include "monitors/pagefault/pagefault_kunit.c"
@@ -159,6 +160,17 @@ static struct kunit_case rv_mon_test_cases[] = {
 	KUNIT_CASE(rv_test_sco),
 	KUNIT_CASE(rv_test_sssw),
 	KUNIT_CASE(rv_test_sts),
+	KUNIT_CASE(rv_test_irqstate),
+	KUNIT_CASE(rv_test_irqstate_shared_actions),
+	KUNIT_CASE(rv_test_irqstate_depth),
+	KUNIT_CASE(rv_test_irqstate_depth_overflow),
+	KUNIT_CASE(rv_test_irqstate_sync),
+	KUNIT_CASE(rv_test_irqstate_live_toggle),
+	KUNIT_CASE(rv_test_irqstate_irq_exit_inline),
+	KUNIT_CASE(rv_test_irqstate_rt_inline_bh),
+	KUNIT_CASE(rv_test_irqstate_preempt),
+	KUNIT_CASE(rv_test_irqstate_rt_prepare_irq),
+	KUNIT_CASE(rv_test_irqstate_rt_prepare_switch),
 	KUNIT_CASE(rv_test_opid),
 	KUNIT_CASE(rv_test_nomiss),
 	KUNIT_CASE(rv_test_pagefault),
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 7/9] rv: Add KUnit coverage for IRQ-exit scheduling
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
                   ` (5 preceding siblings ...)
  2026-10-22  5:07 ` [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:30   ` sashiko-bot
  2026-10-22  5:07 ` [RFC PATCH 8/9] rv: Test empty PREEMPT_RT softirq worker intervals Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
  8 siblings, 1 reply; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

Add KUnit coverage for the irqexit generated state table, invalid
transitions, and enable-time synchronization. Test live
preempt_schedule_irq() invocations without switching, across a task switch,
and across CPU migration. The migration case runs when another CPU is
available. The live cases require the built-in suite because they invoke a
non-exported helper.

KUnit failure reporting may allocate with GFP_KERNEL. Record reaction
counts inside synthetic IRQ-disabled and hardirq intervals. Evaluate the
expectations after restoring normal task context to avoid secondary
atomic-sleep or lockdep diagnostics.

The live tests invoke the generic helper directly. They do not exercise
an architecture-specific physical interrupt-return path.

Assisted-by: LLM
---
 Documentation/trace/rv/monitor_irqentry.rst   |  21 +
 kernel/trace/rv/monitors/irqexit/irqexit.c    |  13 +
 .../trace/rv/monitors/irqexit/irqexit_kunit.c | 537 ++++++++++++++++++
 .../trace/rv/monitors/irqexit/irqexit_kunit.h |  21 +
 kernel/trace/rv/rv_monitors_test.c            |  13 +
 5 files changed, 605 insertions(+)
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
 create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_kunit.h

diff --git a/Documentation/trace/rv/monitor_irqentry.rst b/Documentation/trace/rv/monitor_irqentry.rst
index ba9e78e7c4a08..f4cd34c1b6bf1 100644
--- a/Documentation/trace/rv/monitor_irqentry.rst
+++ b/Documentation/trace/rv/monitor_irqentry.rst
@@ -292,6 +292,27 @@ boundary probes require exactly one entry before the callback and one exit
 afterward, both with local interrupts disabled and live serving state set;
 the monitor must accept the pair and the original counts must be restored.
 
+With built-in RV KUnit tests, ``rv_test_irqexit_live`` enters the real
+``preempt_schedule_irq()`` helper with and without a task switch on one CPU.
+``rv_test_irqexit_migrate`` instead wakes a same-CPU FIFO worker which moves
+the switched-out owner to a different allowed online CPU. The test requires
+accepted entry and suspension on the source CPU and resumption and return
+on the destination. It also samples the CPU on which the helper actually
+returned, independently of the RV observer, and checks restoration of the
+owner's original affinity before the tested task exits.
+
+The two live helper cases use single-element parameter fixtures. Monitor
+slot allocation and destruction, and CPU-hotplug read locking, run in the
+suite runner, which owns the RV interface mutex. The case body and its
+cleanup can run in different KUnit threads. The tests retain a task
+reference until the observer and worker are stopped; the migration case
+checks restoration of the tested task's affinity before that task exits.
+
+The migration case skips when fewer than two allowed CPUs are online, or
+when the tests are modules: the helper is not exported. These tests exercise
+the real helper and scheduler, but invoke the helper directly. They are not
+evidence of an architecture's physical interrupt-return assembly path.
+
 Coverage limits
 ---------------
 
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit.c b/kernel/trace/rv/monitors/irqexit/irqexit.c
index a088b28b62b17..baee718dbe83c 100644
--- a/kernel/trace/rv/monitors/irqexit/irqexit.c
+++ b/kernel/trace/rv/monitors/irqexit/irqexit.c
@@ -157,3 +157,16 @@ module_exit(unregister_irqexit);
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Karl Mehltretter <kmehltretter@gmail.com>");
 MODULE_DESCRIPTION("irqexit: verify IRQ-exit scheduling ownership");
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+#include <kunit/visibility.h>
+#include "irqexit_kunit.h"
+
+const struct rv_irqexit_ops rv_irqexit_ops = {
+	.mon = RV_MON_OPS_INIT(),
+	.handle_resched_entry = handle_resched_entry,
+	.handle_resched_exit = handle_resched_exit,
+	.handle_sched_switch = handle_sched_switch,
+};
+EXPORT_SYMBOL_IF_KUNIT(rv_irqexit_ops);
+#endif
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
new file mode 100644
index 0000000000000..56a9442e2f704
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
@@ -0,0 +1,537 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/completion.h>
+#include <linux/cpu.h>
+#include <linux/hardirq.h>
+#include <linux/irqflags.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/rv.h>
+#include <linux/sched.h>
+#include <linux/sched/task.h>
+#include <rv/kunit.h>
+#include <rv_trace.h>
+#include <trace/events/sched.h>
+
+#include "irqexit_kunit.h"
+
+#if IS_REACHABLE(CONFIG_RV_MON_IRQEXIT)
+
+static void irqexit_test_switch(struct task_struct *prev, struct task_struct *next)
+{
+	rv_mock_current(prev);
+	rv_irqexit_ops.handle_sched_switch(NULL, true, prev, next, TASK_RUNNING);
+	rv_mock_current(next);
+}
+
+static void rv_test_irqexit(struct kunit *test)
+{
+	struct task_struct *owner = rv_kunit_alloc_mock_task(test);
+	struct task_struct *other = rv_kunit_alloc_mock_task(test);
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned long flags;
+	int observed;
+
+	owner->pid = 101;
+	other->pid = 102;
+	prepare_test(test, &rv_irqexit_ops.mon);
+	rv_mock_current(owner);
+
+	local_irq_save(flags);
+	/* The scheduling loop may return without an actual task switch. */
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+
+	/* A different task's IRQ-exit loop cannot complete owner's loop. */
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	irqexit_test_switch(owner, other);
+	rv_irqexit_ops.handle_resched_entry(NULL, other);
+	__irq_enter_raw();
+	__irq_exit_raw();
+	rv_irqexit_ops.handle_resched_exit(NULL, other);
+	irqexit_test_switch(other, owner);
+
+	/* Both tasks can have suspended invocations at the same time. */
+	irqexit_test_switch(owner, other);
+	rv_irqexit_ops.handle_resched_entry(NULL, other);
+	irqexit_test_switch(other, owner);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	irqexit_test_switch(owner, other);
+	rv_irqexit_ops.handle_resched_exit(NULL, other);
+	observed = READ_ONCE(ctx->reactions);
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed, 0);
+}
+
+static void rv_test_irqexit_invalid(struct kunit *test)
+{
+	struct task_struct *owner = rv_kunit_alloc_mock_task(test);
+	struct task_struct *other = rv_kunit_alloc_mock_task(test);
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned long flags;
+	int observed[14];
+	unsigned int i = 0;
+
+	owner->pid = 101;
+	other->pid = 102;
+	prepare_test(test, &rv_irqexit_ops.mon);
+	rv_mock_current(owner);
+
+	KUNIT_ASSERT_FALSE(test, irqs_disabled());
+	RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+		rv_irqexit_ops.handle_resched_entry(NULL, owner);
+
+	local_irq_save(flags);
+	/* Scheduling must not start before the hardirq count is removed. */
+	__irq_enter_raw();
+	observed[i++] = READ_ONCE(ctx->reactions);
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+
+	/* A return with hardirq state still active is invalid as well. */
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	__irq_enter_raw();
+	observed[i++] = READ_ONCE(ctx->reactions);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	__irq_exit_raw();
+
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+
+	/* The first exit synchronizes; the second unmatched exit must fail. */
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+
+	/* The state permits exit, but its task identity must still agree. */
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	rv_mock_current(other);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+
+	/* Matching current is insufficient if the invocation is suspended. */
+	rv_mock_current(owner);
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	irqexit_test_switch(owner, other);
+	rv_mock_current(owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	observed[i++] = READ_ONCE(ctx->reactions);
+
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	local_irq_restore(flags);
+	for (i = 0; i < ARRAY_SIZE(observed); i += 2) {
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed[i], 0);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed[i + 1], 1);
+	}
+	RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+		rv_irqexit_ops.handle_resched_exit(NULL, owner);
+
+	/* Even an unsynchronized bad return must not be silently skipped. */
+	rv_mock_current(other);
+	RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+		rv_irqexit_ops.handle_resched_exit(NULL, owner);
+}
+
+static void rv_test_irqexit_sync(struct kunit *test)
+{
+	struct task_struct *owner = rv_kunit_alloc_mock_task(test);
+	struct task_struct *other = rv_kunit_alloc_mock_task(test);
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned long flags;
+	int observed;
+
+	owner->pid = 101;
+	other->pid = 102;
+	prepare_test(test, &rv_irqexit_ops.mon);
+	rv_mock_current(owner);
+
+	local_irq_save(flags);
+	/* Switch events alone do not synchronize an already active loop. */
+	irqexit_test_switch(owner, other);
+	irqexit_test_switch(other, owner);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+
+	/* Model disable/reset/re-enable while that invocation is suspended. */
+	rv_irqexit_ops.mon.rv_this->enabled = 0;
+	irqexit_test_switch(owner, other);
+	rv_irqexit_ops.handle_resched_entry(NULL, other);
+	rv_irqexit_ops.handle_resched_exit(NULL, other);
+	rv_irqexit_ops.mon.task_reset(owner);
+	rv_irqexit_ops.mon.task_reset(other);
+	rv_irqexit_ops.mon.rv_this->enabled = 1;
+	irqexit_test_switch(other, owner);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	rv_irqexit_ops.handle_resched_entry(NULL, owner);
+	rv_irqexit_ops.handle_resched_exit(NULL, owner);
+	observed = READ_ONCE(ctx->reactions);
+	local_irq_restore(flags);
+	RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed, 0);
+}
+
+#ifndef MODULE
+struct irqexit_live_ctx {
+	struct task_struct *owner;
+	struct task_struct *worker;
+	const char *skip_reason;
+	struct completion worker_done;
+	unsigned int entries;
+	unsigned int exits;
+	unsigned int switches_out;
+	unsigned int switches_in;
+	unsigned int entry_cpu;
+	unsigned int exit_cpu;
+	unsigned int switch_out_cpu;
+	unsigned int switch_in_cpu;
+	unsigned int target_cpu;
+	unsigned int raw_entries;
+	unsigned int raw_exits;
+	unsigned int bad_raw_context;
+	int affinity_result;
+	cpumask_t saved_mask;
+	cpumask_t test_mask;
+	bool migrate;
+	bool affinity_changed;
+	bool observer_attached;
+	bool raw_entry_attached;
+	bool raw_exit_attached;
+	bool hotplug_disabled;
+};
+
+/* Parent fixtures run in the suite runner, which owns rv_interface_lock. */
+static int irqexit_live_fixture_init(struct kunit *test)
+{
+	struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+	struct irqexit_live_ctx *live;
+	int ret;
+
+	if (mon->enabled)
+		return -EBUSY;
+	live = kunit_kzalloc(test, sizeof(*live), GFP_KERNEL);
+	if (!live)
+		return -ENOMEM;
+
+	/* Keep the online mask stable without nesting tracepoint registration. */
+	cpu_hotplug_disable();
+	live->hotplug_disabled = true;
+	ret = mon->enable();
+	if (ret) {
+		cpu_hotplug_enable();
+		live->hotplug_disabled = false;
+		kunit_kfree(test, live);
+		return ret;
+	}
+	kunit_register_params_array(test, live, 1, NULL);
+	return 0;
+}
+
+static void irqexit_live_fixture_exit(struct kunit *test)
+{
+	struct irqexit_live_ctx *live = (void *)test->params_array.params;
+	struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+
+	mon->enabled = 0;
+	mon->disable();
+	if (live->hotplug_disabled) {
+		cpu_hotplug_enable();
+		live->hotplug_disabled = false;
+	}
+	/* Retain the child's explicit skip reason on the parent case. */
+	if (live->skip_reason)
+		kunit_mark_skipped(test, "%s", live->skip_reason);
+}
+
+static int irqexit_live_worker(void *data)
+{
+	struct irqexit_live_ctx *live = data;
+
+	if (live->migrate)
+		live->affinity_result =
+			set_cpus_allowed_ptr(live->owner, cpumask_of(live->target_cpu));
+	complete(&live->worker_done);
+	while (!kthread_should_stop()) {
+		set_current_state(TASK_INTERRUPTIBLE);
+		if (!kthread_should_stop())
+			schedule();
+	}
+	__set_current_state(TASK_RUNNING);
+	return 0;
+}
+
+static void irqexit_observe_event(void *data, int id, char *state,
+				  char *event, char *next_state, bool final_state)
+{
+	struct irqexit_live_ctx *live = data;
+
+	if (id != live->owner->pid)
+		return;
+	if (!strcmp(event, "resched_entry")) {
+		live->entry_cpu = raw_smp_processor_id();
+		live->entries++;
+	} else if (!strcmp(event, "resched_exit")) {
+		live->exit_cpu = raw_smp_processor_id();
+		live->exits++;
+	} else if (!strcmp(event, "switch_out") && !strcmp(state, "rescheduling")) {
+		if (!live->switches_out)
+			live->switch_out_cpu = raw_smp_processor_id();
+		live->switches_out++;
+	} else if (!strcmp(event, "switch_in") && !strcmp(state, "suspended")) {
+		if (!live->switches_in)
+			live->switch_in_cpu = raw_smp_processor_id();
+		live->switches_in++;
+	}
+}
+
+static void irqexit_observe_entry(void *data, struct task_struct *owner,
+				  unsigned long pc, bool irq_off)
+{
+	struct irqexit_live_ctx *live = data;
+
+	if (owner != live->owner)
+		return;
+	live->raw_entries++;
+	if (pc || !irq_off)
+		live->bad_raw_context++;
+}
+
+static void irqexit_observe_exit(void *data, struct task_struct *owner,
+				 unsigned long pc, bool irq_off)
+{
+	struct irqexit_live_ctx *live = data;
+
+	if (owner != live->owner)
+		return;
+	live->raw_exits++;
+	if (pc || !irq_off)
+		live->bad_raw_context++;
+}
+
+static int irqexit_observe_boundaries(struct irqexit_live_ctx *live)
+{
+	int ret;
+
+	ret = register_trace_sched_irq_preempt_entry(irqexit_observe_entry, live);
+	if (ret)
+		return ret;
+	live->raw_entry_attached = true;
+	ret = register_trace_sched_irq_preempt_exit(irqexit_observe_exit, live);
+	if (!ret)
+		live->raw_exit_attached = true;
+	return ret;
+}
+
+static void irqexit_live_cleanup(void *data)
+{
+	struct irqexit_live_ctx *live = data;
+	struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+
+	mon->enabled = 0;
+	if (live->observer_attached)
+		unregister_trace_event_irqexit(irqexit_observe_event, live);
+	if (live->raw_entry_attached)
+		unregister_trace_sched_irq_preempt_entry(irqexit_observe_entry, live);
+	if (live->raw_exit_attached)
+		unregister_trace_sched_irq_preempt_exit(irqexit_observe_exit, live);
+	tracepoint_synchronize_unregister();
+	if (live->worker)
+		kthread_stop(live->worker);
+	if (live->affinity_changed && !(READ_ONCE(live->owner->flags) & PF_EXITING))
+		KUNIT_EXPECT_EQ(kunit_get_current_test(),
+				set_cpus_allowed_ptr(live->owner, &live->saved_mask), 0);
+	put_task_struct(live->owner);
+	live->owner = NULL;
+
+	RV_KUNIT_EXPECT_NO_REACTION(kunit_get_current_test(), active_ctx);
+	mon->react = mon->reactor ? mon->reactor->react : NULL;
+	active_ctx = NULL;
+	rv_mock_current(NULL);
+}
+
+static void rv_test_irqexit_live(struct kunit *test)
+{
+	struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+	struct rv_kunit_ctx *ctx = test->priv;
+	struct irqexit_live_ctx *live = (void *)test->param_value;
+	struct task_struct *worker;
+	unsigned int entries, exits, switches_out, switches_in;
+	unsigned int first_entries, first_switches;
+	unsigned long flags;
+	bool worker_done;
+
+	KUNIT_ASSERT_FALSE(test, mon->enabled);
+	live->owner = current;
+	get_task_struct(live->owner);
+	init_completion(&live->worker_done);
+
+	active_ctx = ctx;
+	mon->react = rv_kunit_mock_react;
+	rv_mock_current(NULL);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, irqexit_live_cleanup, live));
+	/* The worker must run on this CPU inside the explicit scheduling loop. */
+	guard(migrate)();
+	worker = kthread_create(irqexit_live_worker, live, "rv-irqexit-test");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, worker);
+	live->worker = worker;
+	kthread_bind(worker, smp_processor_id());
+	sched_set_fifo(worker);
+	KUNIT_ASSERT_EQ(test, 0,
+			register_trace_event_irqexit(irqexit_observe_event, live));
+	live->observer_attached = true;
+	KUNIT_ASSERT_EQ(test, irqexit_observe_boundaries(live), 0);
+
+	mon->enabled = 1;
+
+	KUNIT_ASSERT_EQ(test, preempt_count(), 0);
+	local_irq_save(flags);
+	preempt_schedule_irq();
+	first_entries = live->entries;
+	first_switches = live->switches_out;
+	wake_up_process(worker);
+	preempt_schedule_irq();
+	entries = live->entries;
+	exits = live->exits;
+	switches_out = live->switches_out;
+	switches_in = live->switches_in;
+	worker_done = completion_done(&live->worker_done);
+	local_irq_restore(flags);
+
+	/* Accepted model events prove that the real boundary probes ran. */
+	KUNIT_EXPECT_GT(test, entries, 0);
+	KUNIT_EXPECT_GT(test, first_entries, 0);
+	KUNIT_EXPECT_GT(test, entries, first_entries);
+	KUNIT_EXPECT_EQ(test, entries, exits);
+	KUNIT_EXPECT_EQ(test, switches_out, switches_in);
+	KUNIT_EXPECT_GT(test, switches_out, first_switches);
+	KUNIT_EXPECT_TRUE(test, worker_done);
+	KUNIT_EXPECT_GE(test, live->raw_entries, 2U);
+	KUNIT_EXPECT_GE(test, live->raw_exits, 2U);
+	KUNIT_EXPECT_EQ(test, live->bad_raw_context, 0U);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	kunit_info(test, "live IRQ-exit entries=%u exits=%u switches=%u\n",
+		   entries, exits, switches_out);
+	mon->enabled = 0;
+}
+
+static void rv_test_irqexit_migrate(struct kunit *test)
+{
+	struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+	struct rv_kunit_ctx *ctx = test->priv;
+	struct irqexit_live_ctx *live = (void *)test->param_value;
+	struct task_struct *worker;
+	unsigned int source_cpu, resumed_cpu;
+	unsigned long flags;
+	bool affinity_restored;
+	int restore_result;
+
+	KUNIT_ASSERT_FALSE(test, mon->enabled);
+	live->owner = current;
+	get_task_struct(live->owner);
+	init_completion(&live->worker_done);
+	active_ctx = ctx;
+	mon->react = rv_kunit_mock_react;
+	rv_mock_current(NULL);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, irqexit_live_cleanup, live));
+	if (num_online_cpus() < 2) {
+		live->skip_reason = "Requires two online CPUs";
+		kunit_skip(test, "%s", live->skip_reason);
+		return;
+	}
+	cpumask_copy(&live->saved_mask, current->cpus_ptr);
+	cpumask_and(&live->test_mask, &live->saved_mask, cpu_online_mask);
+	if (cpumask_weight(&live->test_mask) < 2) {
+		live->skip_reason = "Requires two allowed online CPUs";
+		kunit_skip(test, "%s", live->skip_reason);
+		return;
+	}
+	source_cpu = cpumask_first(&live->test_mask);
+	live->target_cpu = cpumask_any_but(&live->test_mask, source_cpu);
+	KUNIT_ASSERT_EQ(test, set_cpus_allowed_ptr(current, cpumask_of(source_cpu)), 0);
+	live->affinity_changed = true;
+	live->migrate = true;
+	worker = kthread_create(irqexit_live_worker, live, "rv-irqexit-mig");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, worker);
+	live->worker = worker;
+	kthread_bind(worker, source_cpu);
+	sched_set_fifo(worker);
+	KUNIT_ASSERT_EQ(test, 0,
+			register_trace_event_irqexit(irqexit_observe_event, live));
+	live->observer_attached = true;
+	KUNIT_ASSERT_EQ(test, irqexit_observe_boundaries(live), 0);
+	mon->enabled = 1;
+
+	KUNIT_ASSERT_EQ(test, preempt_count(), 0);
+	local_irq_save(flags);
+	live->entries = 0;
+	live->exits = 0;
+	live->switches_out = 0;
+	live->switches_in = 0;
+	/*
+	 * The FIFO worker runs only after this task switches out inside the
+	 * real helper. It moves the queued, non-running owner to another CPU.
+	 * Do not migration-disable the owner: migration is the property tested.
+	 */
+	wake_up_process(worker);
+	preempt_schedule_irq();
+	resumed_cpu = raw_smp_processor_id();
+	mon->enabled = 0;
+	local_irq_restore(flags);
+	KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&live->worker_done, 5 * HZ), 0UL);
+	KUNIT_EXPECT_EQ(test, live->affinity_result, 0);
+	KUNIT_EXPECT_EQ(test, live->entries, 1U);
+	KUNIT_EXPECT_EQ(test, live->exits, 1U);
+	KUNIT_EXPECT_GT(test, live->switches_out, 0U);
+	KUNIT_EXPECT_EQ(test, live->switches_out, live->switches_in);
+	KUNIT_EXPECT_EQ(test, live->entry_cpu, source_cpu);
+	KUNIT_EXPECT_EQ(test, live->switch_out_cpu, source_cpu);
+	KUNIT_EXPECT_EQ(test, live->switch_in_cpu, live->target_cpu);
+	KUNIT_EXPECT_EQ(test, live->exit_cpu, live->target_cpu);
+	/* Check actual resumed execution independently of the RV observer. */
+	KUNIT_EXPECT_EQ(test, resumed_cpu, live->target_cpu);
+	KUNIT_EXPECT_GT(test, live->raw_entries, 0U);
+	KUNIT_EXPECT_GT(test, live->raw_exits, 0U);
+	KUNIT_EXPECT_EQ(test, live->bad_raw_context, 0U);
+	restore_result = set_cpus_allowed_ptr(current, &live->saved_mask);
+	KUNIT_EXPECT_EQ(test, restore_result, 0);
+	affinity_restored = !restore_result &&
+		cpumask_equal(current->cpus_ptr, &live->saved_mask);
+	KUNIT_EXPECT_TRUE(test, affinity_restored);
+	if (affinity_restored)
+		live->affinity_changed = false;
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	kunit_info(test, "live IRQ-exit migration cpu=%u->%u actual=%u entries=%u exits=%u switches=%u\n",
+		   live->entry_cpu, live->exit_cpu, resumed_cpu, live->entries, live->exits,
+		   live->switches_out);
+	kunit_info(test, "live IRQ-exit affinity restored=%u\n", affinity_restored);
+}
+#else
+static void rv_test_irqexit_live(struct kunit *test)
+{
+	/* preempt_schedule_irq() is deliberately not exported to modules. */
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqexit_migrate(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+#endif
+
+#else
+#define rv_test_irqexit rv_test_stub
+#define rv_test_irqexit_invalid rv_test_stub
+#define rv_test_irqexit_sync rv_test_stub
+#define rv_test_irqexit_live rv_test_stub
+#define rv_test_irqexit_migrate rv_test_stub
+#endif
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit_kunit.h b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.h
new file mode 100644
index 0000000000000..b475388455c2a
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __IRQEXIT_KUNIT_H
+#define __IRQEXIT_KUNIT_H
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+
+#include <linux/rv.h>
+#include <rv/kunit.h>
+
+extern const struct rv_irqexit_ops {
+	struct rv_kunit_mon mon;
+	void (*handle_resched_entry)(void *data, struct task_struct *owner);
+	void (*handle_resched_exit)(void *data, struct task_struct *owner);
+	void (*handle_sched_switch)(void *data, bool preempt,
+				    struct task_struct *prev,
+				    struct task_struct *next,
+				    unsigned int prev_state);
+} rv_irqexit_ops;
+#endif
+
+#endif /* __IRQEXIT_KUNIT_H */
diff --git a/kernel/trace/rv/rv_monitors_test.c b/kernel/trace/rv/rv_monitors_test.c
index 4fb611a9f8b85..9eb37cbd2de22 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -150,6 +150,7 @@ static void rv_test_dummy(struct kunit *test)
 #include "monitors/sssw/sssw_kunit.c"
 #include "monitors/sts/sts_kunit.c"
 #include "monitors/irqstate/irqstate_kunit.c"
+#include "monitors/irqexit/irqexit_kunit.c"
 #include "monitors/opid/opid_kunit.c"
 #include "monitors/nomiss/nomiss_kunit.c"
 #include "monitors/pagefault/pagefault_kunit.c"
@@ -171,6 +172,18 @@ static struct kunit_case rv_mon_test_cases[] = {
 	KUNIT_CASE(rv_test_irqstate_preempt),
 	KUNIT_CASE(rv_test_irqstate_rt_prepare_irq),
 	KUNIT_CASE(rv_test_irqstate_rt_prepare_switch),
+	KUNIT_CASE(rv_test_irqexit),
+	KUNIT_CASE(rv_test_irqexit_invalid),
+	KUNIT_CASE(rv_test_irqexit_sync),
+#if IS_REACHABLE(CONFIG_RV_MON_IRQEXIT) && !defined(MODULE)
+	KUNIT_CASE_PARAM_WITH_INIT(rv_test_irqexit_live, kunit_array_gen_params,
+				   irqexit_live_fixture_init, irqexit_live_fixture_exit),
+	KUNIT_CASE_PARAM_WITH_INIT(rv_test_irqexit_migrate, kunit_array_gen_params,
+				   irqexit_live_fixture_init, irqexit_live_fixture_exit),
+#else
+	KUNIT_CASE(rv_test_irqexit_live),
+	KUNIT_CASE(rv_test_irqexit_migrate),
+#endif
 	KUNIT_CASE(rv_test_opid),
 	KUNIT_CASE(rv_test_nomiss),
 	KUNIT_CASE(rv_test_pagefault),
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 8/9] rv: Test empty PREEMPT_RT softirq worker intervals
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
                   ` (6 preceding siblings ...)
  2026-10-22  5:07 ` [RFC PATCH 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:07 ` [RFC PATCH 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
  8 siblings, 0 replies; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

On PREEMPT_RT, a worker enters serving-softirq state before checking for
pending work and leaves that state afterward. A worker with no pending
work therefore creates a valid interval that action-based tests miss.

Add KUnit-only helpers that exercise the worker boundaries directly and
force the per-CPU ksoftirqd task through one invocation with no pending
softirqs. Preserve work that races with the test seam. Verify that each
path emits one context pair without an intervening softirq action.

Gate the seam's predicate and worker branches with the existing
kunit_running static key. Outside a KUnit run, both added checks take only
the disabled static-key path.

Both tests attach to non-exported IRQ boundary tracepoints. Run them only
when the RV KUnit suite is built in and report them as skipped when it is a
module.

Assisted-by: LLM
---
 Documentation/trace/rv/monitor_irqentry.rst   |  23 ++
 include/linux/interrupt.h                     |   7 +
 kernel/softirq.c                              | 225 ++++++++++++++++-
 .../rv/monitors/irqstate/irqstate_kunit.c     | 239 ++++++++++++++++++
 kernel/trace/rv/rv_monitors_test.c            |   2 +
 5 files changed, 495 insertions(+), 1 deletion(-)

diff --git a/Documentation/trace/rv/monitor_irqentry.rst b/Documentation/trace/rv/monitor_irqentry.rst
index f4cd34c1b6bf1..c2b7739a061f0 100644
--- a/Documentation/trace/rv/monitor_irqentry.rst
+++ b/Documentation/trace/rv/monitor_irqentry.rst
@@ -292,6 +292,29 @@ boundary probes require exactly one entry before the callback and one exit
 afterward, both with local interrupts disabled and live serving state set;
 the monitor must accept the pair and the original counts must be restored.
 
+``rv_test_irqstate_rt_empty_worker`` uses a KUnit-only seam around the same
+PREEMPT_RT worker entry and exit helpers, without calling
+``handle_softirqs()``. It requires exactly one accepted context pair and no
+``softirq_entry`` or ``softirq_exit`` action from the synthetic owner between
+those boundaries. The seam wakes ksoftirqd while interrupts are disabled so
+pending work which races with the test remains scheduled. This
+deterministically covers the owner's no-handler interval; it does not invoke
+the smpboot predicate or prove that the current task is the real ksoftirqd
+thread.
+
+``rv_test_irqstate_rt_smpboot_empty_worker`` complements that unit seam. A
+KUnit-only flag makes the real ``ksoftirqd_should_run()`` predicate return true
+once when no work is pending. The smpboot loop then invokes
+``run_ksoftirqd()`` in the real per-CPU ksoftirqd task. The test requires the
+same empty boundary pair and accepts samples only while that forced invocation
+is active. This exercises the real predicate-to-worker path and pending-zero
+decision; it does not claim to reproduce the naturally raced pending-bit
+interleaving.
+
+Both empty-worker tests require a built-in RV KUnit suite. A modular suite
+reports them as skipped because the IRQ boundary tracepoints they attach to
+are not exported.
+
 With built-in RV KUnit tests, ``rv_test_irqexit_live`` enters the real
 ``preempt_schedule_irq()`` helper with and without a task switch on one CPU.
 ``rv_test_irqexit_migrate`` instead wakes a same-CPU FIFO worker which moves
diff --git a/include/linux/interrupt.h b/include/linux/interrupt.h
index 3bf969ad8fe07..f268e025921c1 100644
--- a/include/linux/interrupt.h
+++ b/include/linux/interrupt.h
@@ -595,6 +595,13 @@ struct softirq_action
 asmlinkage void do_softirq(void);
 asmlinkage void __do_softirq(void);
 
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+void softirq_kunit_run_empty_worker(void);
+bool softirq_kunit_empty_worker_active(void);
+int softirq_kunit_run_empty_ksoftirqd(struct task_struct **owner,
+				      int *owner_cpu, bool *armed);
+#endif
+
 #ifdef CONFIG_PREEMPT_RT
 extern void do_softirq_post_smp_call_flush(unsigned int was_pending);
 #else
diff --git a/kernel/softirq.c b/kernel/softirq.c
index 2e3b6fd3f6d5d..e85bd3b81e779 100644
--- a/kernel/softirq.c
+++ b/kernel/softirq.c
@@ -10,6 +10,9 @@
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
 #define INSTANTIATE_EXPORTED_INTERRUPT_DISABLE
+#include <kunit/test-bug.h>
+#include <kunit/visibility.h>
+#include <linux/completion.h>
 #include <linux/export.h>
 #include <linux/kernel_stat.h>
 #include <linux/interrupt.h>
@@ -609,6 +612,187 @@ asmlinkage __visible void do_softirq(void)
 
 #endif /* !CONFIG_PREEMPT_RT */
 
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+enum softirq_kunit_empty_worker_phase {
+	SOFTIRQ_KUNIT_EMPTY_IDLE,
+	SOFTIRQ_KUNIT_EMPTY_PREPARING,
+	SOFTIRQ_KUNIT_EMPTY_ARMED,
+	SOFTIRQ_KUNIT_EMPTY_RUNNING,
+	SOFTIRQ_KUNIT_EMPTY_DONE,
+};
+
+struct softirq_kunit_empty_worker {
+	atomic_t phase;
+	struct completion done;
+	u32 held_pending;
+};
+
+static DEFINE_PER_CPU(struct softirq_kunit_empty_worker,
+			      softirq_kunit_empty_workers);
+
+/*
+ * Exercise the worker boundaries without entering handle_softirqs().
+ *
+ * The caller must be in preemptible task context with interrupts enabled and
+ * no active softirq context. Like run_ksoftirqd(), this returns with local
+ * interrupts enabled.
+ */
+void softirq_kunit_run_empty_worker(void)
+{
+	ksoftirqd_run_begin();
+	/* Preserve any pending work which raced with the test seam. */
+	wakeup_softirqd();
+	ksoftirqd_run_end();
+}
+EXPORT_SYMBOL_IF_KUNIT(softirq_kunit_run_empty_worker);
+
+bool softirq_kunit_empty_worker_active(void)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	worker = raw_cpu_ptr(&softirq_kunit_empty_workers);
+	return atomic_read(&worker->phase) == SOFTIRQ_KUNIT_EMPTY_RUNNING &&
+	       current == raw_cpu_read(ksoftirqd);
+}
+EXPORT_SYMBOL_IF_KUNIT(softirq_kunit_empty_worker_active);
+
+/*
+ * Force one real ksoftirqd invocation after its smpboot predicate observes
+ * no pending work. The run itself still uses run_ksoftirqd(). The caller must
+ * hold the CPU hotplug read lock across this call so the selected CPU and its
+ * ksoftirqd remain online. The returned task pointer carries no reference.
+ */
+int softirq_kunit_run_empty_ksoftirqd(struct task_struct **owner,
+				      int *owner_cpu, bool *armed)
+{
+	struct softirq_kunit_empty_worker *worker;
+	struct task_struct *tsk;
+	unsigned long completed;
+	int this_cpu, phase, ret;
+
+	if (!owner || !owner_cpu || !armed)
+		return -EINVAL;
+	WRITE_ONCE(*owner, NULL);
+	WRITE_ONCE(*owner_cpu, -1);
+	WRITE_ONCE(*armed, false);
+
+	lockdep_assert_cpus_held();
+	this_cpu = get_cpu();
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	if (atomic_cmpxchg(&worker->phase, SOFTIRQ_KUNIT_EMPTY_IDLE,
+			   SOFTIRQ_KUNIT_EMPTY_PREPARING) !=
+	    SOFTIRQ_KUNIT_EMPTY_IDLE) {
+		put_cpu();
+		return -EBUSY;
+	}
+
+	tsk = __this_cpu_read(ksoftirqd);
+	if (!tsk) {
+		atomic_set(&worker->phase, SOFTIRQ_KUNIT_EMPTY_IDLE);
+		put_cpu();
+		return -ENODEV;
+	}
+
+	init_completion(&worker->done);
+	worker->held_pending = 0;
+	WRITE_ONCE(*owner_cpu, this_cpu);
+	WRITE_ONCE(*owner, tsk);
+	/* Publish the owner before globally registered callbacks can match it. */
+	smp_store_release(armed, true);
+	atomic_set(&worker->phase, SOFTIRQ_KUNIT_EMPTY_ARMED);
+	wake_up_process(tsk);
+	put_cpu();
+
+	completed = wait_for_completion_timeout(&worker->done, 5 * HZ);
+	if (!completed) {
+		phase = atomic_cmpxchg(&worker->phase,
+				       SOFTIRQ_KUNIT_EMPTY_ARMED,
+				       SOFTIRQ_KUNIT_EMPTY_IDLE);
+		if (phase == SOFTIRQ_KUNIT_EMPTY_ARMED) {
+			ret = -ETIMEDOUT;
+			goto out_disarm;
+		}
+
+		/*
+		 * Once the predicate has claimed the request, synchronize with
+		 * complete() before making the completion reusable. CPU hotplug
+		 * remains excluded by the caller while this helper runs. RT lock
+		 * contention can delay the worker after it claims the request, so
+		 * wait for it to finish.
+		 */
+		if (phase != SOFTIRQ_KUNIT_EMPTY_RUNNING &&
+		    phase != SOFTIRQ_KUNIT_EMPTY_DONE) {
+			ret = -EIO;
+			goto out_disarm;
+		}
+		wait_for_completion(&worker->done);
+		phase = atomic_cmpxchg(&worker->phase,
+				       SOFTIRQ_KUNIT_EMPTY_DONE,
+				       SOFTIRQ_KUNIT_EMPTY_IDLE);
+		ret = phase == SOFTIRQ_KUNIT_EMPTY_DONE ? -ETIMEDOUT : -EIO;
+		goto out_disarm;
+	}
+
+	phase = atomic_cmpxchg(&worker->phase, SOFTIRQ_KUNIT_EMPTY_DONE,
+			       SOFTIRQ_KUNIT_EMPTY_IDLE);
+	ret = phase == SOFTIRQ_KUNIT_EMPTY_DONE ? 0 : -EIO;
+
+out_disarm:
+	WRITE_ONCE(*armed, false);
+	return ret;
+}
+EXPORT_SYMBOL_IF_KUNIT(softirq_kunit_run_empty_ksoftirqd);
+
+static bool softirq_kunit_force_empty_ksoftirqd(void)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	return current == __this_cpu_read(ksoftirqd) &&
+	       atomic_cmpxchg(&worker->phase, SOFTIRQ_KUNIT_EMPTY_ARMED,
+			      SOFTIRQ_KUNIT_EMPTY_RUNNING) ==
+			      SOFTIRQ_KUNIT_EMPTY_ARMED;
+}
+
+static bool softirq_kunit_begin_empty_ksoftirqd(void)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	if (atomic_read(&worker->phase) != SOFTIRQ_KUNIT_EMPTY_RUNNING)
+		return false;
+
+	worker->held_pending = local_softirq_pending();
+	set_softirq_pending(0);
+	return true;
+}
+
+static void softirq_kunit_restore_empty_ksoftirqd(bool active)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	if (!active)
+		return;
+
+	lockdep_assert_irqs_disabled();
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	or_softirq_pending(worker->held_pending);
+	worker->held_pending = 0;
+}
+
+static void softirq_kunit_finish_empty_ksoftirqd(bool active)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	if (!active)
+		return;
+
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	atomic_set(&worker->phase, SOFTIRQ_KUNIT_EMPTY_DONE);
+	complete(&worker->done);
+}
+#endif
+
 /*
  * We restart softirq processing for at most MAX_SOFTIRQ_RESTART times,
  * but break the loop if need_resched() is set or after 2 ms.
@@ -1198,11 +1382,50 @@ void __init softirq_init(void)
 
 static int ksoftirqd_should_run(unsigned int cpu)
 {
-	return local_softirq_pending();
+	unsigned int pending = local_softirq_pending();
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+	if (!pending && static_branch_unlikely(&kunit_running) &&
+	    softirq_kunit_force_empty_ksoftirqd())
+		return 1;
+#endif
+	return pending;
 }
 
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+static void softirq_kunit_run_ksoftirqd(unsigned int cpu)
+{
+	bool kunit_empty;
+
+	ksoftirqd_run_begin();
+	kunit_empty = softirq_kunit_begin_empty_ksoftirqd();
+	if (local_softirq_pending()) {
+		/*
+		 * We can safely run softirq on inline stack, as we are not deep
+		 * in the task stack here.
+		 */
+		handle_softirqs(true);
+		softirq_kunit_restore_empty_ksoftirqd(kunit_empty);
+		ksoftirqd_run_end();
+		softirq_kunit_finish_empty_ksoftirqd(kunit_empty);
+		cond_resched();
+	} else {
+		softirq_kunit_restore_empty_ksoftirqd(kunit_empty);
+		ksoftirqd_run_end();
+		softirq_kunit_finish_empty_ksoftirqd(kunit_empty);
+	}
+}
+#endif
+
 static void run_ksoftirqd(unsigned int cpu)
 {
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+	if (static_branch_unlikely(&kunit_running)) {
+		softirq_kunit_run_ksoftirqd(cpu);
+		return;
+	}
+#endif
+
 	ksoftirqd_run_begin();
 	if (local_softirq_pending()) {
 		/*
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
index c5b8834fea3d9..e3928d10b2825 100644
--- a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
@@ -862,6 +862,37 @@ struct irqstate_inline_bh_ctx {
 	bool exit_attached;
 };
 
+struct irqstate_empty_worker_ctx {
+	struct task_struct *owner;
+	int owner_cpu;
+	unsigned int entries;
+	unsigned int exits;
+	unsigned int action_entries;
+	unsigned int action_exits;
+	unsigned int event_index;
+	bool armed;
+	bool real_worker;
+	bool inside_pair;
+	bool bad_order;
+	bool bad_boundary_context;
+	bool bad_boundary_cpu;
+	bool actions_at_exit;
+	bool entry_attached;
+	bool exit_attached;
+	bool action_entry_attached;
+	bool action_exit_attached;
+};
+
+static bool irqstate_empty_worker_matches(struct irqstate_empty_worker_ctx *empty_ctx)
+{
+	/* Pairs with owner publication in both empty-worker test paths. */
+	if (!smp_load_acquire(&empty_ctx->armed) ||
+	    current != READ_ONCE(empty_ctx->owner))
+		return false;
+
+	return !empty_ctx->real_worker || softirq_kunit_empty_worker_active();
+}
+
 static void irqstate_inline_bh_entry(void *data, bool serving,
 				     bool from_hardirq)
 {
@@ -1127,6 +1158,202 @@ static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
 		   softirq_count() == saved_softirq);
 }
 
+static void irqstate_empty_worker_entry(void *data, bool serving,
+					bool from_hardirq)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx))
+		return;
+	empty_ctx->entries++;
+	if (empty_ctx->event_index != 0 || empty_ctx->inside_pair)
+		empty_ctx->bad_order = true;
+	empty_ctx->event_index++;
+	empty_ctx->inside_pair = true;
+	if (!serving || !in_serving_softirq() || !irqs_disabled())
+		empty_ctx->bad_boundary_context = true;
+	if (raw_smp_processor_id() != READ_ONCE(empty_ctx->owner_cpu))
+		empty_ctx->bad_boundary_cpu = true;
+}
+
+static void irqstate_empty_worker_exit(void *data, bool serving,
+				       bool from_hardirq)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx))
+		return;
+	empty_ctx->exits++;
+	if (empty_ctx->event_index != 1 || !empty_ctx->inside_pair)
+		empty_ctx->bad_order = true;
+	empty_ctx->event_index++;
+	if (!serving || !in_serving_softirq() || !irqs_disabled())
+		empty_ctx->bad_boundary_context = true;
+	if (raw_smp_processor_id() != READ_ONCE(empty_ctx->owner_cpu))
+		empty_ctx->bad_boundary_cpu = true;
+	empty_ctx->actions_at_exit = empty_ctx->action_entries ||
+				     empty_ctx->action_exits;
+	empty_ctx->inside_pair = false;
+}
+
+static void irqstate_empty_worker_action_entry(void *data, unsigned int vec_nr)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx) ||
+	    !empty_ctx->inside_pair)
+		return;
+	(void)vec_nr;
+	empty_ctx->action_entries++;
+	if (raw_smp_processor_id() != empty_ctx->owner_cpu)
+		empty_ctx->bad_boundary_cpu = true;
+}
+
+static void irqstate_empty_worker_action_exit(void *data, unsigned int vec_nr)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx) ||
+	    !empty_ctx->inside_pair)
+		return;
+	(void)vec_nr;
+	empty_ctx->action_exits++;
+	if (raw_smp_processor_id() != empty_ctx->owner_cpu)
+		empty_ctx->bad_boundary_cpu = true;
+}
+
+static void irqstate_empty_worker_detach(void *data)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	WRITE_ONCE(empty_ctx->armed, false);
+	if (empty_ctx->action_exit_attached)
+		unregister_trace_softirq_exit(irqstate_empty_worker_action_exit,
+					      empty_ctx);
+	if (empty_ctx->action_entry_attached)
+		unregister_trace_softirq_entry(irqstate_empty_worker_action_entry,
+					       empty_ctx);
+	if (empty_ctx->exit_attached)
+		unregister_trace_softirq_context_exit(irqstate_empty_worker_exit,
+						      empty_ctx);
+	if (empty_ctx->entry_attached)
+		unregister_trace_softirq_context_entry(irqstate_empty_worker_entry,
+						       empty_ctx);
+	tracepoint_synchronize_unregister();
+}
+
+static void run_irqstate_rt_empty_worker(struct kunit *test, bool real_worker)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx;
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int saved_count, saved_softirq;
+	bool order_ok, restored;
+	int ret;
+
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		kunit_skip(test, "Requires RT worker serving boundaries");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	empty_ctx = kunit_kzalloc(test, sizeof(*empty_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, empty_ctx);
+	empty_ctx->real_worker = real_worker;
+
+	ret = register_trace_softirq_context_entry(irqstate_empty_worker_entry,
+						   empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->entry_attached = true;
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test,
+						  irqstate_empty_worker_detach,
+						  empty_ctx));
+	ret = register_trace_softirq_context_exit(irqstate_empty_worker_exit,
+						  empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->exit_attached = true;
+	ret = register_trace_softirq_entry(irqstate_empty_worker_action_entry,
+					   empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->action_entry_attached = true;
+	ret = register_trace_softirq_exit(irqstate_empty_worker_action_exit,
+					  empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->action_exit_attached = true;
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	KUNIT_ASSERT_TRUE(test, preemptible());
+	KUNIT_ASSERT_FALSE(test, irqs_disabled());
+	KUNIT_ASSERT_EQ(test, softirq_count(), 0U);
+	saved_count = preempt_count();
+	saved_softirq = softirq_count();
+	if (real_worker) {
+		cpus_read_lock();
+		ret = softirq_kunit_run_empty_ksoftirqd(&empty_ctx->owner,
+							&empty_ctx->owner_cpu,
+							&empty_ctx->armed);
+		cpus_read_unlock();
+	} else {
+		migrate_disable();
+		WRITE_ONCE(empty_ctx->owner_cpu, smp_processor_id());
+		WRITE_ONCE(empty_ctx->owner, current);
+		/* Publish the owner before globally registered callbacks match it. */
+		smp_store_release(&empty_ctx->armed, true);
+		softirq_kunit_run_empty_worker();
+		WRITE_ONCE(empty_ctx->armed, false);
+		migrate_enable();
+		ret = 0;
+	}
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_ASSERT_NOT_NULL(test, empty_ctx->owner);
+
+	order_ok = empty_ctx->event_index == 2 && !empty_ctx->inside_pair &&
+		   !empty_ctx->bad_order;
+	restored = preempt_count() == saved_count &&
+		   softirq_count() == saved_softirq && !irqs_disabled();
+	KUNIT_EXPECT_EQ(test, empty_ctx->entries, 1U);
+	KUNIT_EXPECT_EQ(test, empty_ctx->exits, 1U);
+	KUNIT_EXPECT_EQ(test, empty_ctx->action_entries, 0U);
+	KUNIT_EXPECT_EQ(test, empty_ctx->action_exits, 0U);
+	KUNIT_EXPECT_TRUE(test, order_ok);
+	KUNIT_EXPECT_FALSE(test, empty_ctx->bad_boundary_context);
+	KUNIT_EXPECT_FALSE(test, empty_ctx->bad_boundary_cpu);
+	KUNIT_EXPECT_FALSE(test, empty_ctx->actions_at_exit);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
+	KUNIT_EXPECT_FALSE(test, irqs_disabled());
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	if (real_worker)
+		kunit_info(test,
+			   "RT smpboot empty worker events=%u/%u actions=%u/%u order=%u serving=%u same_cpu=%u restored=%u\n",
+			   empty_ctx->entries, empty_ctx->exits,
+			   empty_ctx->action_entries, empty_ctx->action_exits,
+			   order_ok, !empty_ctx->bad_boundary_context,
+			   !empty_ctx->bad_boundary_cpu, restored);
+	else
+		kunit_info(test,
+			   "RT empty worker events=%u/%u actions=%u/%u order=%u serving=%u same_cpu=%u restored=%u\n",
+			   empty_ctx->entries, empty_ctx->exits,
+			   empty_ctx->action_entries, empty_ctx->action_exits,
+			   order_ok, !empty_ctx->bad_boundary_context,
+			   !empty_ctx->bad_boundary_cpu, restored);
+}
+
+/* Exercise an RT worker interval which executes no softirq action. */
+static void rv_test_irqstate_rt_empty_worker(struct kunit *test)
+{
+	run_irqstate_rt_empty_worker(test, false);
+}
+
+/* Exercise the real ksoftirqd task and its smpboot predicate. */
+static void rv_test_irqstate_rt_smpboot_empty_worker(struct kunit *test)
+{
+	run_irqstate_rt_empty_worker(test, true);
+}
+
 static int irqstate_preempt_worker(void *arg)
 {
 	struct irqstate_preempt_ctx *preempt_ctx = arg;
@@ -1353,6 +1580,16 @@ static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
 	kunit_skip(test, "Requires built-in RV tests");
 }
 
+static void rv_test_irqstate_rt_empty_worker(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqstate_rt_smpboot_empty_worker(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
 static void rv_test_irqstate_preempt(struct kunit *test)
 {
 	kunit_skip(test, "Requires built-in RV tests");
@@ -1378,6 +1615,8 @@ static void rv_test_irqstate_rt_prepare_switch(struct kunit *test)
 #define rv_test_irqstate_live_toggle rv_test_stub
 #define rv_test_irqstate_irq_exit_inline rv_test_stub
 #define rv_test_irqstate_rt_inline_bh rv_test_stub
+#define rv_test_irqstate_rt_empty_worker rv_test_stub
+#define rv_test_irqstate_rt_smpboot_empty_worker rv_test_stub
 #define rv_test_irqstate_preempt rv_test_stub
 #define rv_test_irqstate_rt_prepare_irq rv_test_stub
 #define rv_test_irqstate_rt_prepare_switch rv_test_stub
diff --git a/kernel/trace/rv/rv_monitors_test.c b/kernel/trace/rv/rv_monitors_test.c
index 9eb37cbd2de22..be3aaae2613a8 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -169,6 +169,8 @@ static struct kunit_case rv_mon_test_cases[] = {
 	KUNIT_CASE(rv_test_irqstate_live_toggle),
 	KUNIT_CASE(rv_test_irqstate_irq_exit_inline),
 	KUNIT_CASE(rv_test_irqstate_rt_inline_bh),
+	KUNIT_CASE(rv_test_irqstate_rt_empty_worker),
+	KUNIT_CASE(rv_test_irqstate_rt_smpboot_empty_worker),
 	KUNIT_CASE(rv_test_irqstate_preempt),
 	KUNIT_CASE(rv_test_irqstate_rt_prepare_irq),
 	KUNIT_CASE(rv_test_irqstate_rt_prepare_switch),
-- 
2.39.5 (Apple Git-154)


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

* [RFC PATCH 9/9] rv: Test IRQ-exit softirq provenance and handoff
  2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
                   ` (7 preceding siblings ...)
  2026-10-22  5:07 ` [RFC PATCH 8/9] rv: Test empty PREEMPT_RT softirq worker intervals Karl Mehltretter
@ 2026-10-22  5:07 ` Karl Mehltretter
  2026-10-22  5:33   ` sashiko-bot
  8 siblings, 1 reply; 14+ messages in thread
From: Karl Mehltretter @ 2026-10-22  5:07 UTC (permalink / raw)
  To: Steven Rostedt, Gabriele Monaco
  Cc: Karl Mehltretter, Masami Hiramatsu, Mathieu Desnoyers,
	Thomas Gleixner, Sebastian Andrzej Siewior, Clark Williams,
	Ingo Molnar, Peter Zijlstra, Andy Lutomirski, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-trace-kernel, linux-rt-devel, linux-doc,
	linux-kernel

When IRQ exit processes softirqs inline, it replaces hardirq context with
serving-softirq context and restores hardirq context afterwards. Extend
the irqstate KUnit tests to cover this transition, nested hardirqs,
monitor synchronization and the from_hardirq value.

Exercise work raised by the softirq-entry, softirq-exit and final
hardirq-exit callbacks. Verify the ksoftirqd handoff and, with forced IRQ
threading, the ktimerd-to-ksoftirqd chain.

PREEMPT_RT does not process softirqs from IRQ exit. Skip the IRQ-exit
inline cases and require no hardirq provenance for RT softirq events.

Assisted-by: LLM
---
 Documentation/trace/rv/monitor_irqentry.rst   |  40 +-
 .../rv/monitors/irqstate/irqstate_kunit.c     | 443 +++++++++++++++++-
 kernel/trace/rv/rv_monitors_test.c            |   2 +
 3 files changed, 470 insertions(+), 15 deletions(-)

diff --git a/Documentation/trace/rv/monitor_irqentry.rst b/Documentation/trace/rv/monitor_irqentry.rst
index c2b7739a061f0..5d293207b6491 100644
--- a/Documentation/trace/rv/monitor_irqentry.rst
+++ b/Documentation/trace/rv/monitor_irqentry.rst
@@ -269,20 +269,32 @@ overflow.
 ``rv_test_irqstate_sync`` resets and enables the bare adapter inside single
 and nested hardirq contexts, inside a real serving-softirq callback, and
 inside a hardirq interrupting that callback. Valid unwinding must establish
-idle without a reaction. A separate unmatched exit must then react exactly
-once. The softirq cases snapshot reactions before the invalid control, so
-an error on the valid sequence cannot replace the expected later error.
-These are adapter synchronization tests, not concurrent tracefs writes.
+idle without a reaction. On non-RT, it also enables between hardirq entry
+and inline softirq processing. The provenance-marked softirq pair must
+remain unsynchronized until the final hardirq exit establishes idle. A
+separate unmatched exit must then react exactly once. The softirq cases
+snapshot reactions before the invalid control, so an error on the valid
+sequence cannot replace the expected later error. These are adapter
+synchronization tests, not concurrent tracefs writes.
 
 ``rv_test_irqstate_irq_exit_inline`` schedules a tasklet inside a real generic
 hardirq boundary and requires non-RT ``irq_exit_rcu()`` to process it before
-returning. Independent boundary probes require exactly one softirq entry
-before the tasklet and one exit afterward. The callback must run in the same
-task and on the same CPU with interrupts enabled and serving-softirq state set,
-but outside hardirq context; the monitor must accept both context pairs and
-the original counts must be restored. If the kernel diverts the tasklet to
-ksoftirqd, for example under forced IRQ threading, the test drains it and
-skips because that configuration does not provide the inline path.
+returning. Independent probes require hardirq entry, softirq entry, softirq
+exit, and final hardirq exit in that order. Both softirq events must report
+hardirq provenance. The entry callback raises a high-priority tasklet and
+requires it to run in the same interval, so entry-created work cannot be lost
+by the initial pending-bit clear. The exit callback raises another tasklet and
+requires the late ksoftirqd handoff. If the kernel diverts the original
+tasklet to ksoftirqd, for example under forced IRQ threading, the test drains
+it and skips because that configuration does not provide the inline path.
+
+``rv_test_irqstate_hardirq_exit_handoff`` raises a tasklet from the final
+hardirq-exit event. It requires the pending bit to remain set and ksoftirqd to
+be runnable before interrupts are restored. The corresponding forced-thread
+timer test raises timer work at the same event. The ktimerd wakeup callback
+then raises a tasklet. Both workers must be runnable before interrupts are
+restored, which checks the finite ksoftirqd, ktimerd, ksoftirqd closure. The
+test skips when forced IRQ threading is unavailable.
 
 ``rv_test_irqstate_rt_inline_bh`` forces a pending tasklet through the
 PREEMPT_RT ``local_bh_enable()`` inline-processing path. It requires the
@@ -290,13 +302,15 @@ tasklet to finish synchronously in the enabling task, on the same CPU, while
 serving-softirq state is set and local interrupts are enabled. Independent
 boundary probes require exactly one entry before the callback and one exit
 afterward, both with local interrupts disabled and live serving state set;
-the monitor must accept the pair and the original counts must be restored.
+both must report no hardirq provenance. The monitor must accept the pair and
+the original counts must be restored.
 
 ``rv_test_irqstate_rt_empty_worker`` uses a KUnit-only seam around the same
 PREEMPT_RT worker entry and exit helpers, without calling
 ``handle_softirqs()``. It requires exactly one accepted context pair and no
 ``softirq_entry`` or ``softirq_exit`` action from the synthetic owner between
-those boundaries. The seam wakes ksoftirqd while interrupts are disabled so
+those boundaries. Both events must report no hardirq provenance. The seam
+wakes ksoftirqd while interrupts are disabled so
 pending work which races with the test remains scheduled. This
 deterministically covers the owner's no-handler interval; it does not invoke
 the smpboot predicate or prove that the current task is the real ksoftirqd
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
index e3928d10b2825..31e827cdba1c6 100644
--- a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
@@ -13,6 +13,7 @@
 #include <rv/kunit.h>
 #include <rv_trace.h>
 #include <trace/events/irq.h>
+#include <trace/events/sched.h>
 
 #include "irqstate_kunit.h"
 
@@ -510,6 +511,44 @@ static void rv_test_irqstate(struct kunit *test)
 	local_irq_restore(flags);
 	RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
 
+	/* PREEMPT_RT never serves softirqs from irq_exit(). */
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		/* Softirq processing replaces and then restores hardirq state. */
+		local_irq_save(flags);
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+		__preempt_count_sub(HARDIRQ_OFFSET - SOFTIRQ_OFFSET);
+		rv_irqstate_ops.handle_softirq_entry(NULL, true, true);
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		__irq_exit_raw();
+		rv_irqstate_ops.handle_softirq_exit(NULL, true, true);
+		__preempt_count_add(HARDIRQ_OFFSET - SOFTIRQ_OFFSET);
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		reactions[0] = READ_ONCE(ctx->reactions);
+		__irq_exit_raw();
+		local_irq_restore(flags);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+
+		/* Provenance selects the transition which returns to hardirq. */
+		local_irq_save(flags);
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+		__preempt_count_sub(HARDIRQ_OFFSET - SOFTIRQ_OFFSET);
+		reactions[0] = READ_ONCE(ctx->reactions);
+		rv_irqstate_ops.handle_softirq_entry(NULL, true, false);
+		reactions[1] = READ_ONCE(ctx->reactions);
+		__preempt_count_add(HARDIRQ_OFFSET - SOFTIRQ_OFFSET);
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		reactions[2] = READ_ONCE(ctx->reactions);
+		__irq_exit_raw();
+		local_irq_restore(flags);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 1);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 0);
+	}
+
 	/* A second outer entry is not a nested interrupt. */
 	local_irq_save(flags);
 	__irq_enter_raw();
@@ -811,6 +850,32 @@ static void rv_test_irqstate_sync(struct kunit *test)
 		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 1);
 	}
 
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		/* A mid-hardirq attach ignores softirq and syncs at H-exit. */
+		local_irq_save(flags);
+		mon->enabled = 0;
+		__irq_enter_raw();
+		rv_irqstate_ops.handle_hardirq_entry(NULL, 1);
+		mon->reset();
+		mon->enabled = 1;
+		__preempt_count_sub(HARDIRQ_OFFSET - SOFTIRQ_OFFSET);
+		rv_irqstate_ops.handle_softirq_entry(NULL, true, true);
+		rv_irqstate_ops.handle_softirq_exit(NULL, true, true);
+		__preempt_count_add(HARDIRQ_OFFSET - SOFTIRQ_OFFSET);
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		__irq_exit_raw();
+		reactions[0] = READ_ONCE(ctx->reactions);
+		__irq_enter_raw();
+		reactions[1] = READ_ONCE(ctx->reactions);
+		rv_irqstate_ops.handle_hardirq_exit(NULL, 1);
+		reactions[2] = READ_ONCE(ctx->reactions);
+		__irq_exit_raw();
+		local_irq_restore(flags);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[0], 0);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 0);
+		RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 1);
+	}
+
 	expected = ctx->expected;
 	completed = run_irqstate_softirq_case(IRQSTATE_SOFTIRQ_SYNC, &rx_at_sync);
 	KUNIT_ASSERT_TRUE(test, completed);
@@ -838,20 +903,31 @@ struct irqstate_preempt_ctx {
 
 struct irqstate_inline_bh_ctx {
 	struct tasklet_struct tasklet;
+	struct tasklet_struct entry_tasklet;
 	struct tasklet_struct exit_tasklet;
 	struct task_struct *owner;
 	struct task_struct *softirqd;
 	int owner_cpu;
 	unsigned int entries;
 	unsigned int exits;
+	unsigned int hardirq_entries;
+	unsigned int hardirq_exits;
+	unsigned int event_index;
 	unsigned int tasklet_runs;
+	unsigned int entry_tasklet_runs;
 	unsigned int exit_tasklet_runs;
 	bool armed;
+	bool raise_on_entry;
 	bool raise_on_exit;
+	bool expected_from_hardirq;
+	bool entry_from_hardirq;
+	bool exit_from_hardirq;
 	bool exit_tasklet_pending;
 	bool softirqd_already_runnable;
 	bool bad_boundary_context;
 	bool bad_boundary_cpu;
+	bool bad_provenance;
+	bool bad_order;
 	bool tasklet_serving;
 	bool tasklet_same_task;
 	bool tasklet_same_cpu;
@@ -860,6 +936,8 @@ struct irqstate_inline_bh_ctx {
 	bool tasklet_bracketed;
 	bool entry_attached;
 	bool exit_attached;
+	bool hardirq_entry_attached;
+	bool hardirq_exit_attached;
 };
 
 struct irqstate_empty_worker_ctx {
@@ -883,6 +961,19 @@ struct irqstate_empty_worker_ctx {
 	bool action_exit_attached;
 };
 
+struct irqstate_hardirq_exit_ctx {
+	struct tasklet_struct tasklet;
+	struct task_struct *owner;
+	struct task_struct *softirqd;
+	int owner_cpu;
+	bool armed;
+	bool callback_seen;
+	bool tasklet_pending;
+	bool softirqd_already_runnable;
+	bool bad_context;
+	bool attached;
+};
+
 static bool irqstate_empty_worker_matches(struct irqstate_empty_worker_ctx *empty_ctx)
 {
 	/* Pairs with owner publication in both empty-worker test paths. */
@@ -893,6 +984,39 @@ static bool irqstate_empty_worker_matches(struct irqstate_empty_worker_ctx *empt
 	return !empty_ctx->real_worker || softirq_kunit_empty_worker_active();
 }
 
+static void irqstate_inline_hardirq_entry(void *data, unsigned int depth)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx = data;
+
+	/* Inline softirq processing enables IRQs; ignore a nested real IRQ. */
+	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner ||
+	    depth != 1)
+		return;
+	inline_ctx->hardirq_entries++;
+	if (inline_ctx->event_index != 0)
+		inline_ctx->bad_order = true;
+	inline_ctx->event_index++;
+	if (hardirq_count() != HARDIRQ_OFFSET || in_serving_softirq() ||
+	    !irqs_disabled())
+		inline_ctx->bad_boundary_context = true;
+}
+
+static void irqstate_inline_hardirq_exit(void *data, unsigned int depth)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx = data;
+
+	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner ||
+	    depth != 1)
+		return;
+	inline_ctx->hardirq_exits++;
+	if (inline_ctx->event_index != 3)
+		inline_ctx->bad_order = true;
+	inline_ctx->event_index++;
+	if (hardirq_count() != HARDIRQ_OFFSET || in_serving_softirq() ||
+	    !irqs_disabled())
+		inline_ctx->bad_boundary_context = true;
+}
+
 static void irqstate_inline_bh_entry(void *data, bool serving,
 				     bool from_hardirq)
 {
@@ -901,10 +1025,21 @@ static void irqstate_inline_bh_entry(void *data, bool serving,
 	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner)
 		return;
 	inline_ctx->entries++;
+	if (inline_ctx->event_index !=
+	    (inline_ctx->expected_from_hardirq ? 1 : 0))
+		inline_ctx->bad_order = true;
+	inline_ctx->event_index++;
+	inline_ctx->entry_from_hardirq = from_hardirq;
 	if (!serving || !in_serving_softirq() || !irqs_disabled())
 		inline_ctx->bad_boundary_context = true;
+	if (from_hardirq != inline_ctx->expected_from_hardirq)
+		inline_ctx->bad_provenance = true;
 	if (raw_smp_processor_id() != inline_ctx->owner_cpu)
 		inline_ctx->bad_boundary_cpu = true;
+	if (READ_ONCE(inline_ctx->raise_on_entry)) {
+		WRITE_ONCE(inline_ctx->raise_on_entry, false);
+		tasklet_hi_schedule(&inline_ctx->entry_tasklet);
+	}
 }
 
 static void irqstate_inline_bh_exit(void *data, bool serving,
@@ -915,8 +1050,15 @@ static void irqstate_inline_bh_exit(void *data, bool serving,
 	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner)
 		return;
 	inline_ctx->exits++;
+	if (inline_ctx->event_index !=
+	    (inline_ctx->expected_from_hardirq ? 2 : 1))
+		inline_ctx->bad_order = true;
+	inline_ctx->event_index++;
+	inline_ctx->exit_from_hardirq = from_hardirq;
 	if (!serving || !in_serving_softirq() || !irqs_disabled())
 		inline_ctx->bad_boundary_context = true;
+	if (from_hardirq != inline_ctx->expected_from_hardirq)
+		inline_ctx->bad_provenance = true;
 	if (raw_smp_processor_id() != inline_ctx->owner_cpu)
 		inline_ctx->bad_boundary_cpu = true;
 	if (READ_ONCE(inline_ctx->raise_on_exit)) {
@@ -949,6 +1091,14 @@ static void irqstate_inline_bh_exit_tasklet(struct tasklet_struct *tasklet)
 	inline_ctx->exit_tasklet_runs++;
 }
 
+static void irqstate_inline_bh_entry_tasklet(struct tasklet_struct *tasklet)
+{
+	struct irqstate_inline_bh_ctx *inline_ctx;
+
+	inline_ctx = from_tasklet(inline_ctx, tasklet, entry_tasklet);
+	inline_ctx->entry_tasklet_runs++;
+}
+
 static void irqstate_inline_bh_kill(void *data)
 {
 	struct irqstate_inline_bh_ctx *inline_ctx = data;
@@ -961,6 +1111,7 @@ static void irqstate_inline_bh_kill_all(void *data)
 	struct irqstate_inline_bh_ctx *inline_ctx = data;
 
 	tasklet_kill(&inline_ctx->tasklet);
+	tasklet_kill(&inline_ctx->entry_tasklet);
 	tasklet_kill(&inline_ctx->exit_tasklet);
 }
 
@@ -969,6 +1120,12 @@ static void irqstate_inline_bh_detach(void *data)
 	struct irqstate_inline_bh_ctx *inline_ctx = data;
 
 	WRITE_ONCE(inline_ctx->armed, false);
+	if (inline_ctx->hardirq_exit_attached)
+		unregister_trace_hardirq_context_exit(
+			irqstate_inline_hardirq_exit, inline_ctx);
+	if (inline_ctx->hardirq_entry_attached)
+		unregister_trace_hardirq_context_entry(
+			irqstate_inline_hardirq_entry, inline_ctx);
 	if (inline_ctx->entry_attached)
 		unregister_trace_softirq_context_entry(irqstate_inline_bh_entry, inline_ctx);
 	if (inline_ctx->exit_attached)
@@ -976,6 +1133,248 @@ static void irqstate_inline_bh_detach(void *data)
 	tracepoint_synchronize_unregister();
 }
 
+static void irqstate_hardirq_exit_tasklet(struct tasklet_struct *tasklet)
+{
+	(void)tasklet;
+}
+
+static void irqstate_hardirq_exit_raise(void *data, unsigned int depth)
+{
+	struct irqstate_hardirq_exit_ctx *exit_ctx = data;
+
+	if (!READ_ONCE(exit_ctx->armed) || current != exit_ctx->owner)
+		return;
+	exit_ctx->callback_seen = true;
+	if (depth != 1 || hardirq_count() != HARDIRQ_OFFSET ||
+	    in_serving_softirq() || !irqs_disabled() ||
+	    raw_smp_processor_id() != exit_ctx->owner_cpu)
+		exit_ctx->bad_context = true;
+	tasklet_schedule(&exit_ctx->tasklet);
+}
+
+static void irqstate_hardirq_exit_cleanup(void *data)
+{
+	struct irqstate_hardirq_exit_ctx *exit_ctx = data;
+
+	WRITE_ONCE(exit_ctx->armed, false);
+	if (exit_ctx->attached)
+		unregister_trace_hardirq_context_exit(
+			irqstate_hardirq_exit_raise, exit_ctx);
+	tracepoint_synchronize_unregister();
+	tasklet_kill(&exit_ctx->tasklet);
+}
+
+/* Work first raised at the final hardirq boundary must reach ksoftirqd. */
+static void rv_test_irqstate_hardirq_exit_handoff(struct kunit *test)
+{
+	struct irqstate_hardirq_exit_ctx *exit_ctx;
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned long flags;
+	int ret;
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	exit_ctx = kunit_kzalloc(test, sizeof(*exit_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, exit_ctx);
+	tasklet_setup(&exit_ctx->tasklet, irqstate_hardirq_exit_tasklet);
+	exit_ctx->owner = current;
+	exit_ctx->owner_cpu = raw_smp_processor_id();
+	exit_ctx->softirqd = this_cpu_ksoftirqd();
+	KUNIT_ASSERT_NOT_NULL(test, exit_ctx->softirqd);
+	if (local_softirq_pending() ||
+	    !wait_task_inactive(exit_ctx->softirqd, TASK_INTERRUPTIBLE)) {
+		kunit_skip(test, "Requires an idle local ksoftirqd");
+		return;
+	}
+
+	ret = register_trace_hardirq_context_exit(
+		irqstate_hardirq_exit_raise, exit_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	exit_ctx->attached = true;
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test,
+						  irqstate_hardirq_exit_cleanup,
+						  exit_ctx));
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	local_irq_save(flags);
+	if (local_softirq_pending() || task_is_running(exit_ctx->softirqd)) {
+		local_irq_restore(flags);
+		kunit_skip(test, "Local ksoftirqd stopped being idle");
+		return;
+	}
+	WRITE_ONCE(exit_ctx->armed, true);
+	irq_enter_rcu();
+	irq_exit_rcu();
+	exit_ctx->tasklet_pending =
+		!!(local_softirq_pending() & BIT(TASKLET_SOFTIRQ));
+	exit_ctx->softirqd_already_runnable =
+		!wake_up_process(exit_ctx->softirqd);
+	WRITE_ONCE(exit_ctx->armed, false);
+	local_irq_restore(flags);
+
+	kunit_release_action(test, irqstate_hardirq_exit_cleanup, exit_ctx);
+	KUNIT_EXPECT_TRUE(test, exit_ctx->callback_seen);
+	KUNIT_EXPECT_TRUE(test, exit_ctx->tasklet_pending);
+	KUNIT_EXPECT_TRUE(test, exit_ctx->softirqd_already_runnable);
+	KUNIT_EXPECT_FALSE(test, exit_ctx->bad_context);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+}
+
+#ifdef CONFIG_IRQ_FORCED_THREADING
+struct irqstate_timer_exit_ctx {
+	struct tasklet_struct tasklet;
+	struct task_struct *owner;
+	struct task_struct *softirqd;
+	struct task_struct *timersd;
+	int owner_cpu;
+	bool armed;
+	bool callback_seen;
+	bool timer_wakeup_seen;
+	bool timer_pending;
+	bool tasklet_pending;
+	bool softirqd_already_runnable;
+	bool timersd_already_runnable;
+	bool bad_context;
+	bool hardirq_exit_attached;
+	bool sched_waking_attached;
+};
+
+static void irqstate_timer_exit_tasklet(struct tasklet_struct *tasklet)
+{
+	(void)tasklet;
+}
+
+static void irqstate_timer_exit_raise(void *data, unsigned int depth)
+{
+	struct irqstate_timer_exit_ctx *timer_ctx = data;
+
+	if (!READ_ONCE(timer_ctx->armed) || current != timer_ctx->owner)
+		return;
+	timer_ctx->callback_seen = true;
+	if (depth != 1 || hardirq_count() != HARDIRQ_OFFSET ||
+	    in_serving_softirq() || !irqs_disabled() ||
+	    raw_smp_processor_id() != timer_ctx->owner_cpu)
+		timer_ctx->bad_context = true;
+	raise_ktimers_thread(TIMER_SOFTIRQ);
+}
+
+static void irqstate_timer_sched_waking(void *data, struct task_struct *task)
+{
+	struct irqstate_timer_exit_ctx *timer_ctx = data;
+
+	if (!READ_ONCE(timer_ctx->armed) || task != timer_ctx->timersd)
+		return;
+	timer_ctx->timer_wakeup_seen = true;
+	if (current != timer_ctx->owner || !irqs_disabled() ||
+	    raw_smp_processor_id() != timer_ctx->owner_cpu)
+		timer_ctx->bad_context = true;
+	tasklet_schedule(&timer_ctx->tasklet);
+}
+
+static void irqstate_timer_exit_detach(void *data)
+{
+	struct irqstate_timer_exit_ctx *timer_ctx = data;
+
+	WRITE_ONCE(timer_ctx->armed, false);
+	if (timer_ctx->sched_waking_attached)
+		unregister_trace_sched_waking(irqstate_timer_sched_waking,
+					      timer_ctx);
+	if (timer_ctx->hardirq_exit_attached)
+		unregister_trace_hardirq_context_exit(
+			irqstate_timer_exit_raise, timer_ctx);
+	tracepoint_synchronize_unregister();
+	tasklet_kill(&timer_ctx->tasklet);
+}
+
+/* Late timer-wakeup work must reach ktimerd and then ksoftirqd. */
+static void rv_test_irqstate_hardirq_exit_timer_handoff(struct kunit *test)
+{
+	struct irqstate_timer_exit_ctx *timer_ctx;
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned long flags;
+	int ret;
+
+	if (!force_irqthreads()) {
+		kunit_skip(test, "Requires forced IRQ threading");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	guard(migrate)();
+	timer_ctx = kunit_kzalloc(test, sizeof(*timer_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, timer_ctx);
+	tasklet_setup(&timer_ctx->tasklet, irqstate_timer_exit_tasklet);
+	timer_ctx->owner = current;
+	timer_ctx->owner_cpu = raw_smp_processor_id();
+	timer_ctx->softirqd = this_cpu_ksoftirqd();
+	timer_ctx->timersd = this_cpu_read(ktimerd);
+	KUNIT_ASSERT_NOT_NULL(test, timer_ctx->softirqd);
+	KUNIT_ASSERT_NOT_NULL(test, timer_ctx->timersd);
+	if (local_softirq_pending() || local_timers_pending_force_th() ||
+	    !wait_task_inactive(timer_ctx->softirqd, TASK_INTERRUPTIBLE) ||
+	    !wait_task_inactive(timer_ctx->timersd, TASK_INTERRUPTIBLE)) {
+		kunit_skip(test, "Requires idle local softirq workers");
+		return;
+	}
+
+	ret = register_trace_hardirq_context_exit(
+		irqstate_timer_exit_raise, timer_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	timer_ctx->hardirq_exit_attached = true;
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, irqstate_timer_exit_detach,
+						  timer_ctx));
+	ret = register_trace_sched_waking(irqstate_timer_sched_waking, timer_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	timer_ctx->sched_waking_attached = true;
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	local_irq_save(flags);
+	if (local_softirq_pending() || local_timers_pending_force_th() ||
+	    task_is_running(timer_ctx->softirqd) ||
+	    task_is_running(timer_ctx->timersd)) {
+		local_irq_restore(flags);
+		kunit_skip(test, "Local softirq worker stopped being idle");
+		return;
+	}
+	WRITE_ONCE(timer_ctx->armed, true);
+	irq_enter_rcu();
+	irq_exit_rcu();
+	timer_ctx->timer_pending =
+		!!(local_timers_pending_force_th() & BIT(TIMER_SOFTIRQ));
+	timer_ctx->tasklet_pending =
+		!!(local_softirq_pending() & BIT(TASKLET_SOFTIRQ));
+	timer_ctx->timersd_already_runnable =
+		!wake_up_process(timer_ctx->timersd);
+	timer_ctx->softirqd_already_runnable =
+		!wake_up_process(timer_ctx->softirqd);
+	WRITE_ONCE(timer_ctx->armed, false);
+	local_irq_restore(flags);
+
+	kunit_release_action(test, irqstate_timer_exit_detach, timer_ctx);
+	KUNIT_EXPECT_TRUE(test, timer_ctx->callback_seen);
+	KUNIT_EXPECT_TRUE(test, timer_ctx->timer_wakeup_seen);
+	KUNIT_EXPECT_TRUE(test, timer_ctx->timer_pending);
+	KUNIT_EXPECT_TRUE(test, timer_ctx->tasklet_pending);
+	KUNIT_EXPECT_TRUE(test, timer_ctx->timersd_already_runnable);
+	KUNIT_EXPECT_TRUE(test, timer_ctx->softirqd_already_runnable);
+	KUNIT_EXPECT_FALSE(test, timer_ctx->bad_context);
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+}
+#else
+static void rv_test_irqstate_hardirq_exit_timer_handoff(struct kunit *test)
+{
+	kunit_skip(test, "Requires forced IRQ threading");
+}
+#endif
+
 /* Exercise non-RT inline softirq processing from a real hardirq exit. */
 static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
 {
@@ -995,8 +1394,11 @@ static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
 	inline_ctx = kunit_kzalloc(test, sizeof(*inline_ctx), GFP_KERNEL);
 	KUNIT_ASSERT_NOT_NULL(test, inline_ctx);
 	tasklet_setup(&inline_ctx->tasklet, irqstate_inline_bh_tasklet);
+	tasklet_setup(&inline_ctx->entry_tasklet,
+		      irqstate_inline_bh_entry_tasklet);
 	tasklet_setup(&inline_ctx->exit_tasklet,
 		      irqstate_inline_bh_exit_tasklet);
+	inline_ctx->expected_from_hardirq = true;
 	KUNIT_ASSERT_EQ(test, 0,
 			kunit_add_action_or_reset(test, irqstate_inline_bh_kill_all,
 						  inline_ctx));
@@ -1008,6 +1410,14 @@ static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
 	KUNIT_ASSERT_EQ(test, 0,
 			kunit_add_action_or_reset(test, irqstate_inline_bh_detach,
 						  inline_ctx));
+	ret = register_trace_hardirq_context_entry(
+		irqstate_inline_hardirq_entry, inline_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	inline_ctx->hardirq_entry_attached = true;
+	ret = register_trace_hardirq_context_exit(
+		irqstate_inline_hardirq_exit, inline_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	inline_ctx->hardirq_exit_attached = true;
 	ret = register_trace_softirq_context_exit(irqstate_inline_bh_exit,
 						  inline_ctx);
 	KUNIT_ASSERT_EQ(test, ret, 0);
@@ -1039,6 +1449,7 @@ static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
 		return;
 	}
 	WRITE_ONCE(inline_ctx->armed, true);
+	WRITE_ONCE(inline_ctx->raise_on_entry, true);
 	WRITE_ONCE(inline_ctx->raise_on_exit, true);
 	irq_enter_rcu();
 	tasklet_schedule(&inline_ctx->tasklet);
@@ -1063,6 +1474,7 @@ static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
 	}
 
 	KUNIT_EXPECT_EQ(test, inline_ctx->tasklet_runs, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->entry_tasklet_runs, 1U);
 	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_serving);
 	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_same_task);
 	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_same_cpu);
@@ -1074,8 +1486,15 @@ static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, inline_ctx->exit_tasklet_runs, 1U);
 	KUNIT_EXPECT_EQ(test, inline_ctx->entries, 1U);
 	KUNIT_EXPECT_EQ(test, inline_ctx->exits, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->hardirq_entries, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->hardirq_exits, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->event_index, 4U);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->entry_from_hardirq);
+	KUNIT_EXPECT_TRUE(test, inline_ctx->exit_from_hardirq);
 	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_context);
 	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_cpu);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_provenance);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_order);
 	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
 	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
 	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
@@ -1105,6 +1524,7 @@ static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
 	inline_ctx = kunit_kzalloc(test, sizeof(*inline_ctx), GFP_KERNEL);
 	KUNIT_ASSERT_NOT_NULL(test, inline_ctx);
 	inline_ctx->owner = current;
+	inline_ctx->expected_from_hardirq = false;
 	tasklet_setup(&inline_ctx->tasklet, irqstate_inline_bh_tasklet);
 	KUNIT_ASSERT_EQ(test, 0,
 			kunit_add_action_or_reset(test, irqstate_inline_bh_kill,
@@ -1144,8 +1564,13 @@ static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
 	KUNIT_EXPECT_TRUE(test, inline_ctx->tasklet_bracketed);
 	KUNIT_EXPECT_EQ(test, inline_ctx->entries, 1U);
 	KUNIT_EXPECT_EQ(test, inline_ctx->exits, 1U);
+	KUNIT_EXPECT_EQ(test, inline_ctx->event_index, 2U);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->entry_from_hardirq);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->exit_from_hardirq);
 	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_context);
 	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_boundary_cpu);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_provenance);
+	KUNIT_EXPECT_FALSE(test, inline_ctx->bad_order);
 	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
 	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
 	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
@@ -1170,7 +1595,8 @@ static void irqstate_empty_worker_entry(void *data, bool serving,
 		empty_ctx->bad_order = true;
 	empty_ctx->event_index++;
 	empty_ctx->inside_pair = true;
-	if (!serving || !in_serving_softirq() || !irqs_disabled())
+	if (!serving || from_hardirq || !in_serving_softirq() ||
+	    !irqs_disabled())
 		empty_ctx->bad_boundary_context = true;
 	if (raw_smp_processor_id() != READ_ONCE(empty_ctx->owner_cpu))
 		empty_ctx->bad_boundary_cpu = true;
@@ -1187,7 +1613,8 @@ static void irqstate_empty_worker_exit(void *data, bool serving,
 	if (empty_ctx->event_index != 1 || !empty_ctx->inside_pair)
 		empty_ctx->bad_order = true;
 	empty_ctx->event_index++;
-	if (!serving || !in_serving_softirq() || !irqs_disabled())
+	if (!serving || from_hardirq || !in_serving_softirq() ||
+	    !irqs_disabled())
 		empty_ctx->bad_boundary_context = true;
 	if (raw_smp_processor_id() != READ_ONCE(empty_ctx->owner_cpu))
 		empty_ctx->bad_boundary_cpu = true;
@@ -1570,6 +1997,16 @@ static void rv_test_irqstate_rt_prepare_switch(struct kunit *test)
 }
 
 #else
+static void rv_test_irqstate_hardirq_exit_handoff(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqstate_hardirq_exit_timer_handoff(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
 static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
 {
 	kunit_skip(test, "Requires built-in RV tests");
@@ -1613,6 +2050,8 @@ static void rv_test_irqstate_rt_prepare_switch(struct kunit *test)
 #define rv_test_irqstate_depth_overflow rv_test_stub
 #define rv_test_irqstate_sync rv_test_stub
 #define rv_test_irqstate_live_toggle rv_test_stub
+#define rv_test_irqstate_hardirq_exit_handoff rv_test_stub
+#define rv_test_irqstate_hardirq_exit_timer_handoff rv_test_stub
 #define rv_test_irqstate_irq_exit_inline rv_test_stub
 #define rv_test_irqstate_rt_inline_bh rv_test_stub
 #define rv_test_irqstate_rt_empty_worker rv_test_stub
diff --git a/kernel/trace/rv/rv_monitors_test.c b/kernel/trace/rv/rv_monitors_test.c
index be3aaae2613a8..0e2d693673cc3 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -167,6 +167,8 @@ static struct kunit_case rv_mon_test_cases[] = {
 	KUNIT_CASE(rv_test_irqstate_depth_overflow),
 	KUNIT_CASE(rv_test_irqstate_sync),
 	KUNIT_CASE(rv_test_irqstate_live_toggle),
+	KUNIT_CASE(rv_test_irqstate_hardirq_exit_handoff),
+	KUNIT_CASE(rv_test_irqstate_hardirq_exit_timer_handoff),
 	KUNIT_CASE(rv_test_irqstate_irq_exit_inline),
 	KUNIT_CASE(rv_test_irqstate_rt_inline_bh),
 	KUNIT_CASE(rv_test_irqstate_rt_empty_worker),
-- 
2.39.5 (Apple Git-154)


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

* Re: [RFC PATCH 7/9] rv: Add KUnit coverage for IRQ-exit scheduling
  2026-10-22  5:07 ` [RFC PATCH 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
@ 2026-10-22  5:30   ` sashiko-bot
  0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-22  5:30 UTC (permalink / raw)
  To: Karl Mehltretter; +Cc: linux-trace-kernel, linux-rt-devel, linux-kernel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] rv: leaked rq->nr_pinned on assertion abort in rv_test_irqexit_live()
--

commit 69d8a9063a6f40de41bbbc2fc85512c36f8a0acb
Author: Karl Mehltretter <kmehltretter@gmail.com>

rv: Add KUnit coverage for IRQ-exit scheduling

This commit adds KUnit test cases for the irqexit runtime verification
monitor, covering its state transitions, synchronization, and live
invocations of preempt_schedule_irq() including task switching and CPU
migration.

> diff --git a/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
> new file mode 100644
> index 0000000000000..56a9442e2f704
> --- /dev/null
> +++ b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
[ ... ]
> +static void rv_test_irqexit_live(struct kunit *test)
> +{
> +	struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
> +	struct rv_kunit_ctx *ctx = test->priv;
> +	struct irqexit_live_ctx *live = (void *)test->param_value;
> +	struct task_struct *worker;
> +	unsigned int entries, exits, switches_out, switches_in;
> +	unsigned int first_entries, first_switches;
> +	unsigned long flags;
> +	bool worker_done;
> +
> +	KUNIT_ASSERT_FALSE(test, mon->enabled);
> +	live->owner = current;
> +	get_task_struct(live->owner);
> +	init_completion(&live->worker_done);
> +
> +	active_ctx = ctx;
> +	mon->react = rv_kunit_mock_react;
> +	rv_mock_current(NULL);
> +	KUNIT_ASSERT_EQ(test, 0,
> +			kunit_add_action_or_reset(test, irqexit_live_cleanup, live));
> +	/* The worker must run on this CPU inside the explicit scheduling loop. */
> +	guard(migrate)();
> +	worker = kthread_create(irqexit_live_worker, live, "rv-irqexit-test");
> +	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, worker);
> +	live->worker = worker;
> +	kthread_bind(worker, smp_processor_id());
> +	sched_set_fifo(worker);
> +	KUNIT_ASSERT_EQ(test, 0,
> +			register_trace_event_irqexit(irqexit_observe_event, live));
> +	live->observer_attached = true;
> +	KUNIT_ASSERT_EQ(test, irqexit_observe_boundaries(live), 0);
> +
> +	mon->enabled = 1;
> +
> +	KUNIT_ASSERT_EQ(test, preempt_count(), 0);

[Severity: Medium]
Does this leak the per-CPU rq->nr_pinned counter if any of the KUNIT_ASSERT_*
checks inside the guard(migrate)() scope fail?

In kernel/trace/rv/monitors/irqexit/irqexit_kunit.c, rv_test_irqexit_live()
enters guard(migrate)(), which calls migrate_disable() -> __migrate_disable()
in include/linux/sched.h:

	this_rq_pinned()++;
	p->migration_disabled = 1;

If a subsequent assertion fails (such as KUNIT_ASSERT_NOT_ERR_OR_NULL() after
kthread_create(), or KUNIT_ASSERT_EQ() after register_trace_event_irqexit(),
irqexit_observe_boundaries(), or preempt_count()), KUnit aborts the test case
via __kunit_abort() -> kunit_try_catch_throw() in lib/kunit/try-catch.c:

void __noreturn kunit_try_catch_throw(struct kunit_try_catch *try_catch)
{
	try_catch->try_result = -EFAULT;
	kthread_exit(0);
}

Because kthread_exit(0) terminates the KUnit runner kthread directly via
do_exit(0) without unwinding the C stack, the scope cleanup destructor for
guard(migrate)() (__migrate_enable()) never runs. Neither kthread_exit() nor
do_exit() decrements this_rq_pinned(), leaving rq->nr_pinned permanently
elevated on that CPU and causing any later CPU hotplug offline operation to
hang in balance_hotplug_wait() waiting for !rq_has_pinned_tasks(rq).

Could migration disabling be narrowed with scoped_guard(migrate) or explicit
migrate_disable() / migrate_enable() around the non-asserting section, or
cleaned up via a KUnit action when current->migration_disabled is set?

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261022050712.75035-1-kmehltretter@gmail.com?part=7

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

* Re: [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions
  2026-10-22  5:07 ` [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
@ 2026-10-22  5:33   ` sashiko-bot
  0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-22  5:33 UTC (permalink / raw)
  To: Karl Mehltretter; +Cc: linux-trace-kernel, linux-rt-devel, linux-kernel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] rv: kunit: rq->nr_pinned leak on kunit_skip() under guard(migrate)()
--

commit 3183a8d5a8d274f51929b98832f2ad4013dd57a7
Author: Karl Mehltretter <kmehltretter@gmail.com>

rv: Add KUnit coverage for IRQ context transitions

This commit adds KUnit test cases for the irqstate runtime verification
monitor covering generated state tables, invalid transitions, enable-time
synchronization, nested hardirqs, shared IRQ actions, PREEMPT_RT serving
intervals, and live tracepoint wiring.

> diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
> new file mode 100644
> index 0000000000000..c5b8834fea3d9
> --- /dev/null
> +++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
> @@ -0,0 +1,1384 @@

[ ... ]

> +/* Exercise non-RT inline softirq processing from a real hardirq exit. */
> +static void rv_test_irqstate_irq_exit_inline(struct kunit *test)
> +{

[ ... ]

> +	KUNIT_ASSERT_FALSE(test, in_interrupt());
> +	KUNIT_ASSERT_FALSE(test, irqs_disabled());
> +	KUNIT_ASSERT_EQ(test, softirq_count(), 0U);
> +	guard(migrate)();
> +	inline_ctx->owner = current;
> +	inline_ctx->owner_cpu = raw_smp_processor_id();
> +	inline_ctx->softirqd = this_cpu_ksoftirqd();
> +	KUNIT_ASSERT_NOT_NULL(test, inline_ctx->softirqd);
> +	if (local_softirq_pending() ||
> +	    !wait_task_inactive(inline_ctx->softirqd, TASK_INTERRUPTIBLE)) {
> +		kunit_skip(test, "Requires an idle local ksoftirqd");
> +		return;
> +	}
> +	saved_count = preempt_count();
> +	saved_softirq = softirq_count();
> +	local_irq_save(flags);
> +	if (local_softirq_pending() || task_is_running(inline_ctx->softirqd)) {
> +		local_irq_restore(flags);
> +		kunit_skip(test, "Local ksoftirqd stopped being idle");
> +		return;
> +	}
> +	WRITE_ONCE(inline_ctx->armed, true);
> +	WRITE_ONCE(inline_ctx->raise_on_exit, true);
> +	irq_enter_rcu();
> +	tasklet_schedule(&inline_ctx->tasklet);
> +	irq_exit_rcu();
> +	ran_inline = inline_ctx->tasklet_runs != 0;
> +	inline_ctx->exit_tasklet_pending =
> +		!!(local_softirq_pending() & BIT(TASKLET_SOFTIRQ));
> +	/*
> +	 * A zero return means the exit handoff already made ksoftirqd runnable.
> +	 * A one return also makes the failure path safe to drain.
> +	 */
> +	inline_ctx->softirqd_already_runnable =
> +		!wake_up_process(inline_ctx->softirqd);
> +	WRITE_ONCE(inline_ctx->armed, false);
> +	local_irq_restore(flags);
> +
> +	/* Drain a tasklet diverted to ksoftirqd before inspecting or skipping. */
> +	kunit_release_action(test, irqstate_inline_bh_kill_all, inline_ctx);
> +	if (!ran_inline) {
> +		kunit_skip(test, "IRQ exit diverted pending softirq to ksoftirqd");
> +		return;
> +	}

[Severity: High]
Does calling kunit_skip() or KUNIT_ASSERT_*() while guard(migrate)() is
active in kernel/trace/rv/monitors/irqstate/irqstate_kunit.c leak the
runqueue nr_pinned counter?

In rv_test_irqstate_irq_exit_inline(), guard(migrate)() invokes
migrate_disable() -> __migrate_disable() in include/linux/sched.h:

	guard(preempt)();
	this_rq_pinned()++;
	p->migration_disabled = 1;

Inside that scope, rv_test_irqstate_irq_exit_inline() calls kunit_skip()
when local_softirq_pending() is non-zero, when ksoftirqd is not idle, or
when !ran_inline (for example, when forced IRQ threading diverts the
tasklet to ksoftirqd). However, kunit_skip() in include/kunit/test.h is:

	#define kunit_skip(test, fmt, ...)				\
		do {							\
			kunit_mark_skipped((test), fmt, ##__VA_ARGS__);	\
			kunit_try_catch_throw(&((test)->try_catch));	\
		} while (0)

Both kunit_skip() and failed KUNIT_ASSERT_*() macros call
kunit_try_catch_throw(), which terminates the KUnit runner kthread directly
via kthread_exit(0) -> do_exit(0) without returning to the caller or
unwinding the C stack.

Because scope-based cleanup destructors do not run when kthread_exit(0)
exits the thread, migrate_enable() -> __migrate_enable() never executes to
decrement this_rq_pinned() (rq->nr_pinned). Wouldn't that leave
rq_has_pinned_tasks(rq) permanently true on that CPU and cause subsequent
CPU hotplug offline operations to hang in balance_hotplug_wait()?

The same issue also applies if any KUNIT_ASSERT_*() fails while
guard(migrate)() is held in rv_test_irqstate(),
rv_test_irqstate_shared_actions(), rv_test_irqstate_sync(),
rv_test_irqstate_irq_exit_inline(), rv_test_irqstate_preempt(),
rv_test_irqstate_rt_prepare_irq(), and rv_test_irqstate_rt_prepare_switch().
Could migration disabling be managed with a kunit_add_action_or_reset()
cleanup action, or released before calling kunit_skip() and KUNIT_ASSERT_*()
macros?

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261022050712.75035-1-kmehltretter@gmail.com?part=6

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

* Re: [RFC PATCH 9/9] rv: Test IRQ-exit softirq provenance and handoff
  2026-10-22  5:07 ` [RFC PATCH 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
@ 2026-10-22  5:33   ` sashiko-bot
  0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-22  5:33 UTC (permalink / raw)
  To: Karl Mehltretter; +Cc: linux-trace-kernel, linux-rt-devel, linux-kernel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] rv: rq->nr_pinned leak on kunit_skip() under guard(migrate)() in irqstate tests
- [Medium] rv: broken nested IRQ filter in irqstate_inline_hardirq_entry() and exit
- [Medium] rv: missing local_bh_blocked() check and early wait in hardirq exit tests
--

commit 7fb79c597a519150ad675f18bceb7d2417080c8b
Author: Karl Mehltretter <kmehltretter@gmail.com>

rv: Test IRQ-exit softirq provenance and handoff

This commit extends the irqstate KUnit test suite to cover inline softirq
processing during hardirq exit, hardirq provenance tracking, and monitor
synchronization. It also adds tests verifying late work handoff to ksoftirqd
and ktimerd at the softirq and hardirq exit boundaries.

> diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
> index e3928d10b2825..31e827cdba1c6 100644
> --- a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
> +++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
[ ... ]
> @@ -893,6 +984,39 @@ static bool irqstate_empty_worker_matches(struct irqstate_empty_worker_ctx *empt
>  	return !empty_ctx->real_worker || softirq_kunit_empty_worker_active();
>  }
>  
> +static void irqstate_inline_hardirq_entry(void *data, unsigned int depth)
> +{
> +	struct irqstate_inline_bh_ctx *inline_ctx = data;
> +
> +	/* Inline softirq processing enables IRQs; ignore a nested real IRQ. */
> +	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner ||
> +	    depth != 1)
> +		return;
> +	inline_ctx->hardirq_entries++;
> +	if (inline_ctx->event_index != 0)
> +		inline_ctx->bad_order = true;
> +	inline_ctx->event_index++;
> +	if (hardirq_count() != HARDIRQ_OFFSET || in_serving_softirq() ||
> +	    !irqs_disabled())
> +		inline_ctx->bad_boundary_context = true;
> +}
> +
> +static void irqstate_inline_hardirq_exit(void *data, unsigned int depth)
> +{
> +	struct irqstate_inline_bh_ctx *inline_ctx = data;
> +
> +	if (!READ_ONCE(inline_ctx->armed) || current != inline_ctx->owner ||
> +	    depth != 1)
> +		return;
> +	inline_ctx->hardirq_exits++;
> +	if (inline_ctx->event_index != 3)
> +		inline_ctx->bad_order = true;
> +	inline_ctx->event_index++;
> +	if (hardirq_count() != HARDIRQ_OFFSET || in_serving_softirq() ||
> +	    !irqs_disabled())
> +		inline_ctx->bad_boundary_context = true;
> +}

[Severity: Medium]
Will checking depth != 1 in irqstate_inline_hardirq_entry() and
irqstate_inline_hardirq_exit() actually filter out a real hardware interrupt
that arrives while inline softirq processing runs with interrupts enabled?

In the rv_test_irqstate_irq_exit_inline() -> irq_exit_rcu() ->
__do_softirq() -> handle_softirqs() path, softirq_handle_begin() replaces
HARDIRQ_OFFSET with SOFTIRQ_OFFSET before local_irq_enable() enables
interrupts:

kernel/softirq.c:softirq_handle_begin() {
	__preempt_count_sub(HARDIRQ_OFFSET - SOFTIRQ_OFFSET);
	lockdep_softirqs_off(_RET_IP_);
	WARN_ON_ONCE(irq_count() != SOFTIRQ_OFFSET);
}

Because HARDIRQ_OFFSET has been cleared, a real hardware interrupt arriving
during the IRQ-enabled softirq window enters irq_enter_rcu() with
hardirq_count() equal to HARDIRQ_OFFSET (so depth is 1, while
in_serving_softirq() is non-zero), and exits irq_exit_rcu() with depth equal
to 1 as well.

As a result, depth != 1 is false for that interrupt in both
irqstate_inline_hardirq_entry() and irqstate_inline_hardirq_exit(), causing
both probes to increment hardirq_entries, hardirq_exits, and event_index and
to set bad_order and bad_boundary_context. Should both callbacks also ignore
events when in_serving_softirq() is true?

[ ... ]
> +/* Work first raised at the final hardirq boundary must reach ksoftirqd. */
> +static void rv_test_irqstate_hardirq_exit_handoff(struct kunit *test)
> +{
> +	struct irqstate_hardirq_exit_ctx *exit_ctx;
> +	struct rv_kunit_ctx *ctx = test->priv;
> +	unsigned long flags;
> +	int ret;
> +
> +	prepare_test(test, &rv_irqstate_ops.mon);
> +	guard(migrate)();
> +	exit_ctx = kunit_kzalloc(test, sizeof(*exit_ctx), GFP_KERNEL);
> +	KUNIT_ASSERT_NOT_NULL(test, exit_ctx);
> +	tasklet_setup(&exit_ctx->tasklet, irqstate_hardirq_exit_tasklet);
> +	exit_ctx->owner = current;
> +	exit_ctx->owner_cpu = raw_smp_processor_id();
> +	exit_ctx->softirqd = this_cpu_ksoftirqd();
> +	KUNIT_ASSERT_NOT_NULL(test, exit_ctx->softirqd);
> +	if (local_softirq_pending() ||
> +	    !wait_task_inactive(exit_ctx->softirqd, TASK_INTERRUPTIBLE)) {
> +		kunit_skip(test, "Requires an idle local ksoftirqd");
> +		return;
> +	}

[Severity: High]
Does calling kunit_skip() or failing a KUNIT_ASSERT_*() while guard(migrate)()
is active leak the per-CPU rq->nr_pinned counter?

guard(migrate)() calls migrate_disable(), which increments rq->nr_pinned
(this_rq_pinned()) and relies on the C cleanup attribute to invoke
migrate_enable() at scope exit. However, kunit_skip() and failing
KUNIT_ASSERT_*() macros call kunit_try_catch_throw():

lib/kunit/try-catch.c:kunit_try_catch_throw() {
	try_catch->try_result = -EFAULT;
	kthread_exit(0);
}

Because kunit_try_catch_throw() is noreturn and exits the KUnit runner
kthread directly via kthread_exit(0) -> do_exit(0) without unwinding C stack
frames, the guard(migrate)() cleanup never runs and do_exit() does not call
migrate_enable(). That leaves rq->nr_pinned permanently elevated on the CPU,
which makes rq_has_pinned_tasks(rq) stay true and causes subsequent CPU
hotplug offline operations to hang in balance_hotplug_wait().

The same pattern occurs in rv_test_irqstate_hardirq_exit_timer_handoff()
below, and when force_irqthreads() is enabled on a !CONFIG_PREEMPT_RT kernel
to run rv_test_irqstate_hardirq_exit_timer_handoff(), the following test
rv_test_irqstate_irq_exit_inline() diverts its softirq to ksoftirqd
(ran_inline is false) and deterministically hits kunit_skip() under
guard(migrate)().

> +
> +	ret = register_trace_hardirq_context_exit(
> +		irqstate_hardirq_exit_raise, exit_ctx);
> +	KUNIT_ASSERT_EQ(test, ret, 0);
> +	exit_ctx->attached = true;
> +	KUNIT_ASSERT_EQ(test, 0,
> +			kunit_add_action_or_reset(test,
> +						  irqstate_hardirq_exit_cleanup,
> +						  exit_ctx));
> +	attach_irqstate_test_probes();
> +	KUNIT_ASSERT_EQ(test, 0,
> +			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
> +						  NULL));
> +
> +	local_irq_save(flags);
> +	if (local_softirq_pending() || task_is_running(exit_ctx->softirqd)) {
> +		local_irq_restore(flags);
> +		kunit_skip(test, "Local ksoftirqd stopped being idle");
> +		return;
> +	}

[Severity: Medium]
Could rv_test_irqstate_hardirq_exit_handoff() and
rv_test_irqstate_hardirq_exit_timer_handoff() fail or spuriously skip due to
missing a local_bh_blocked() check on CONFIG_PREEMPT_RT and waiting for
worker inactivity before registering tracepoints?

In __irq_exit_rcu(), waking ksoftirqd for softirq work raised during
trace_hardirq_context_exit() is gated on should_wake_ksoftirqd():

kernel/softirq.c:__irq_exit_rcu() {
	if (irq_count() == HARDIRQ_OFFSET &&
	    unlikely(local_softirq_pending()) &&
	    should_wake_ksoftirqd()) {
		wakeup_softirqd();
		ksoftirqd_wake_requested = true;
	}
}

On CONFIG_PREEMPT_RT, should_wake_ksoftirqd() checks
!this_cpu_read(softirq_ctrl.cnt) (!local_bh_blocked()). Because BH-disabled
sections on PREEMPT_RT are preemptible and can block on RT locks
(TASK_RTLOCK_WAIT), another task on the local CPU (or ksoftirqd itself after
clearing local_softirq_pending() and blocking in TASK_RTLOCK_WAIT, where
task_is_running() is false) can hold softirq_ctrl.cnt non-zero while
local_softirq_pending() is 0 and task_is_running(softirqd) is false. In that
case __irq_exit_rcu() skips wakeup_softirqd() and the assertion on
softirqd_already_runnable fails.

Additionally, unlike rv_test_irqstate_irq_exit_inline(), both handoff tests
call wait_task_inactive() before register_trace_hardirq_context_exit(),
register_trace_sched_waking(), and attach_irqstate_test_probes(), which can
sleep on mutexes and queue RCU work that wakes ksoftirqd or ktimerd after the
wait has already finished. Would it be better to attach the probes before
waiting for the workers to go inactive, and also check local_bh_blocked()?

> +	WRITE_ONCE(exit_ctx->armed, true);
> +	irq_enter_rcu();
> +	irq_exit_rcu();
> +	exit_ctx->tasklet_pending =
> +		!!(local_softirq_pending() & BIT(TASKLET_SOFTIRQ));
> +	exit_ctx->softirqd_already_runnable =
> +		!wake_up_process(exit_ctx->softirqd);
> +	WRITE_ONCE(exit_ctx->armed, false);
> +	local_irq_restore(flags);
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261022050712.75035-1-kmehltretter@gmail.com?part=9

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

* Re: [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events
  2026-10-22  5:07 ` [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events Karl Mehltretter
@ 2026-10-22  5:44   ` sashiko-bot
  0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-22  5:44 UTC (permalink / raw)
  To: Karl Mehltretter; +Cc: linux-trace-kernel, linux-rt-devel, linux-kernel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] softirq: tick_irq_exit() called before trace_hardirq_context_exit() handoff
--

commit 2beb0d9dac0a7b76bfe5b73b550fff3be238bbab
Author: Karl Mehltretter <kmehltretter@gmail.com>

tracing/irq: Add generic IRQ context boundary events

This commit adds tracepoints at the generic hardirq and serving-softirq
context boundaries. It also updates softirq pending snapshots and wakes
worker threads when callbacks attached to boundary events raise new work.

> diff --git a/kernel/softirq.c b/kernel/softirq.c
> index be49e6469f3ad..2e3b6fd3f6d5d 100644
> --- a/kernel/softirq.c
> +++ b/kernel/softirq.c

[ ... ]

> @@ -722,6 +770,7 @@ void irq_enter_rcu(void)
>  		tick_irq_enter();
>  
>  	account_hardirq_enter(current);
> +	trace_hardirq_context_entry(hardirq_count() >> HARDIRQ_SHIFT);
>  }
>  
>  /**
> @@ -796,6 +845,36 @@ static inline void __irq_exit_rcu(void)
>  		wake_timersd();
>  
>  	tick_irq_exit();
> +	if (trace_hardirq_context_exit_enabled()) {
> +		bool ksoftirqd_wake_requested = false;
> +		bool timersd_wake_requested = false;
> +
> +		trace_hardirq_context_exit(hardirq_count() >> HARDIRQ_SHIFT);
> +
> +		/* Hand work raised by the final boundary event to its worker. */
> +		if (hardirq_count() == HARDIRQ_OFFSET) {
> +			if (irq_count() == HARDIRQ_OFFSET &&
> +			    unlikely(local_softirq_pending()) &&
> +			    should_wake_ksoftirqd()) {
> +				wakeup_softirqd();
> +				ksoftirqd_wake_requested = true;
> +			}
> +
> +			if (IS_ENABLED(CONFIG_IRQ_FORCED_THREADING) &&
> +			    force_irqthreads() && local_timers_pending_force_th()) {
> +				wake_timersd();
> +				timersd_wake_requested = true;
> +			}
> +
> +			/* The timersd wakeup can itself raise a softirq. */
> +			if (timersd_wake_requested &&
> +			    !ksoftirqd_wake_requested &&
> +			    irq_count() == HARDIRQ_OFFSET &&
> +			    unlikely(local_softirq_pending()) &&
> +			    should_wake_ksoftirqd())
> +				wakeup_softirqd();
> +		}
> +	}
>  	preempt_count_sub(HARDIRQ_OFFSET);
>  }

[Severity: Medium]
Should tick_irq_exit() be called after the trace_hardirq_context_exit()
block rather than before it?

In irq_enter_rcu(), tick_irq_enter() runs before
trace_hardirq_context_entry() so that the nohz clock wakeup and idle
cputime accounting state are established before the entry event fires.

In __irq_exit_rcu(), however, tick_irq_exit() runs before
trace_hardirq_context_exit() and the subsequent wakeup_softirqd() and
wake_timersd() calls:

kernel/softirq.c:tick_irq_exit() {
	if ((sched_core_idle_cpu(cpu) && !need_resched()) || tick_nohz_full_cpu(cpu)) {
		if (hardirq_count() == HARDIRQ_OFFSET)
			tick_nohz_irq_exit();
	}
}

kernel/time/tick-sched.c:tick_nohz_irq_exit() {
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);

	if (tick_sched_flag_test(ts, TS_FLAG_INIDLE)) {
		tick_nohz_clock_sleep(ts);
		ts->idle_entrytime = ktime_get();
		if (tick_sched_flag_test(ts, TS_FLAG_STOPPED))
			kcpustat_irq_exit(ts->idle_entrytime);
	} else {
		tick_nohz_full_update_tick(ts);
	}
}

On an idle cpu where need_resched() is false before the tracepoint,
tick_irq_exit() calls tick_nohz_irq_exit(), which places sched_clock and
kcpustat back into idle sleep via tick_nohz_clock_sleep() and
kcpustat_irq_exit(). If the hardirq_context_exit callback then raises work
and wakes ksoftirqd or ktimerd, try_to_wake_up() reads sched_clock_cpu()
and sets need_resched after the idle clock and cputime state have already
gone to sleep.

Similarly, on a nohz_full cpu, tick_nohz_irq_exit() calls
tick_nohz_full_update_tick() to evaluate and potentially stop the tick
before trace_hardirq_context_exit() runs. Any timer, softirq, rcu, or
irq_work state raised by the exit callback is missed by that evaluation,
or forces an extra self-ipi when waking ksoftirqd after the tick was just
stopped.

Would moving tick_irq_exit() to just after the
trace_hardirq_context_exit_enabled() block (while hardirq_count() is still
equal to the outermost offset) avoid this regression and keep entry and
exit symmetric?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261022050712.75035-1-kmehltretter@gmail.com?part=1

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

end of thread, other threads:[~2026-10-22  5:44 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
2026-10-22  5:07 ` [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events Karl Mehltretter
2026-10-22  5:44   ` sashiko-bot
2026-10-22  5:07 ` [RFC PATCH 2/9] tracing/sched: Add IRQ preemption lifecycle events Karl Mehltretter
2026-10-22  5:07 ` [RFC PATCH 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
2026-10-22  5:07 ` [RFC PATCH 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
2026-10-22  5:07 ` [RFC PATCH 5/9] docs/rv: Document the IRQ-entry monitors Karl Mehltretter
2026-10-22  5:07 ` [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
2026-10-22  5:33   ` sashiko-bot
2026-10-22  5:07 ` [RFC PATCH 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
2026-10-22  5:30   ` sashiko-bot
2026-10-22  5:07 ` [RFC PATCH 8/9] rv: Test empty PREEMPT_RT softirq worker intervals Karl Mehltretter
2026-10-22  5:07 ` [RFC PATCH 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
2026-10-22  5:33   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®