mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Karl Mehltretter <kmehltretter@gmail.com>
To: Steven Rostedt <rostedt@goodmis.org>,
	Gabriele Monaco <gmonaco@redhat.com>
Cc: Karl Mehltretter <kmehltretter@gmail.com>,
	Masami Hiramatsu <mhiramat@kernel.org>,
	Mathieu Desnoyers <mathieu.desnoyers@efficios.com>,
	Thomas Gleixner <tglx@kernel.org>,
	Sebastian Andrzej Siewior <bigeasy@linutronix.de>,
	Clark Williams <clrkwllms@kernel.org>,
	Ingo Molnar <mingo@redhat.com>,
	Peter Zijlstra <peterz@infradead.org>,
	Andy Lutomirski <luto@kernel.org>,
	Juri Lelli <juri.lelli@redhat.com>,
	Vincent Guittot <vincent.guittot@linaro.org>,
	Dietmar Eggemann <dietmar.eggemann@arm.com>,
	Ben Segall <bsegall@google.com>, Mel Gorman <mgorman@suse.de>,
	Valentin Schneider <vschneid@redhat.com>,
	K Prateek Nayak <kprateek.nayak@amd.com>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>,
	Randy Dunlap <rdunlap@infradead.org>,
	linux-trace-kernel@vger.kernel.org,
	linux-rt-devel@lists.linux.dev, linux-doc@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions
Date: Thu, 22 Oct 2026 07:07:09 +0200	[thread overview]
Message-ID: <20261022050712.75035-7-kmehltretter@gmail.com> (raw)
In-Reply-To: <20261022050712.75035-1-kmehltretter@gmail.com>

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)


  parent reply	other threads:[~2026-10-22  5:07 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-22  5:07 [RFC PATCH 0/9] rv: Verify generic " 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 ` Karl Mehltretter [this message]
2026-10-22  5:33   ` [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions 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
2026-10-23 16:05 ` [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Gabriele Monaco

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261022050712.75035-7-kmehltretter@gmail.com \
    --to=kmehltretter@gmail.com \
    --cc=bigeasy@linutronix.de \
    --cc=bsegall@google.com \
    --cc=clrkwllms@kernel.org \
    --cc=corbet@lwn.net \
    --cc=dietmar.eggemann@arm.com \
    --cc=gmonaco@redhat.com \
    --cc=juri.lelli@redhat.com \
    --cc=kprateek.nayak@amd.com \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-rt-devel@lists.linux.dev \
    --cc=linux-trace-kernel@vger.kernel.org \
    --cc=luto@kernel.org \
    --cc=mathieu.desnoyers@efficios.com \
    --cc=mgorman@suse.de \
    --cc=mhiramat@kernel.org \
    --cc=mingo@redhat.com \
    --cc=peterz@infradead.org \
    --cc=rdunlap@infradead.org \
    --cc=rostedt@goodmis.org \
    --cc=skhan@linuxfoundation.org \
    --cc=tglx@kernel.org \
    --cc=vincent.guittot@linaro.org \
    --cc=vschneid@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®