* [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions
@ 2026-11-07 17:12 Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 1/9] tracing/irq: Add generic IRQ context boundary tracepoints Karl Mehltretter
` (8 more replies)
0 siblings, 9 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, 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 tracepoints at the generic hardirq and serving-softirq
boundaries. The irq:irqstate RV monitor checks their order and agreement
with live kernel state. irq:irqexit follows the owner of a
preempt_schedule_irq() invocation across task switches and CPU migration.
The hardirq tracepoints 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.
Gabriele answered two of the v1 questions [1]:
- The boundaries are now bare DECLARE_TRACE() tracepoints like the
existing RV hooks in include/trace/events/sched.h, so they add no
tracefs ABI.
- irqstate and irqexit stay separate automata because one is per-CPU
and the other per-task. A hardirq or non-RT softirq belongs to the
CPU it interrupts. A preempt_schedule_irq() invocation belongs to
the interrupted task, several tasks can be inside one at the same
time, and it can migrate before it returns. Each RV automaton has a
single monitor type. Both now sit under an irq container, so they
are enabled together by default, and the irqentry/irqexit naming
clash is gone.
Questions still open from v1:
1. Are dedicated scheduler tracepoints the right way to follow
preempt_schedule_irq() across task switches and migration?
2. 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 tracepoints, monitors and documentation. Patches 6-9
add KUnit coverage.
The series is based on v7.4-rc1, which contains the v1 prerequisite and
Wen Yang's reactor lockdep fix:
commit feb5c9a2447a ("softirq: Preserve interrupt context on return from interrupt")
commit 39ccedfd9e55 ("rv: Use LD_WAIT_SPIN as the reactor lockdep wait type")
The series is independent of the early-hardirq series [2], which
installs hardirq state before entry tracing. Both change
kernel/softirq.c, so applying one on top of the other needs a small
conflict resolution in __irq_exit_rcu().
Changes since v1:
- Use bare DECLARE_TRACE() tracepoints instead of trace events
(Gabriele)
- Rename the irqentry container to irq (Gabriele)
- Add text schematics of both models to the documentation (Gabriele)
- Document why irqstate and irqexit are separate automata
- Rebase onto v7.4-rc1
Testing on v7.4-rc1 with Clang 23.1.5, GCC 14.2 (x86) and GCC 16.2
(cross), and QEMU 10.0 TCG, with lockdep and the locking selftests:
- Each patch builds with W=1 on x86_64 without new warnings.
RV_MONITORS_KUNIT_TEST=m builds on x86_64 and arm64. kernel-doc
reports no warnings for the changed headers.
- The rv_mon KUnit suite and the reactor suites passed without
failures on:
x86_64 PREEMPT SMP4 and UP, PREEMPT with nohz_full=1-3 and
threadirqs, PREEMPT_RT SMP4, PREEMPT_RT SMP8 with
nohz_full=1-3
arm64 PREEMPT, PREEMPT with nohz_full=1-3 and threadirqs,
PREEMPT_RT, PREEMPT_RT SMP8 with nohz_full=1-3
riscv64 PREEMPT
ARMv7 UP
Each configuration ran 13 to 21 of the 29 rv_mon cases. The others
are skipped because they need a different configuration.
- RV=n kernels boot on x86_64 and arm64.
- On top of the early-hardirq series, the same suites passed on
x86_64 (PREEMPT, PREEMPT_RT), arm64 (PREEMPT, PREEMPT_RT) and
riscv64.
An LLM assisted with the patches and the testing.
v1: https://lore.kernel.org/all/20261022050712.75035-1-kmehltretter@gmail.com/
[1] https://lore.kernel.org/all/FACA54EC-F846-4051-9ADD-DB46E7665FAD@redhat.com/
[2] https://lore.kernel.org/all/20261028033127.82193-1-kmehltretter@gmail.com/
Karl Mehltretter (9):
tracing/irq: Add generic IRQ context boundary tracepoints
tracing/sched: Add IRQ preemption lifecycle tracepoints
rv: Add a monitor for generic IRQ context transitions
rv: Add a monitor for IRQ-exit scheduling ownership
docs/rv: Document the IRQ 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_irq.rst | 475 ++++
include/linux/interrupt.h | 8 +
include/rv/kunit.h | 34 +-
include/trace/events/irq.h | 56 +
include/trace/events/sched.h | 26 +
kernel/sched/core.c | 6 +
kernel/softirq.c | 324 ++-
kernel/trace/rv/Kconfig | 11 +-
kernel/trace/rv/Makefile | 3 +
kernel/trace/rv/monitors/irq/Kconfig | 13 +
kernel/trace/rv/monitors/irq/irq.c | 35 +
kernel/trace/rv/monitors/irq/irq.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 | 540 ++++
.../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 | 2339 +++++++++++++++++
.../rv/monitors/irqstate/irqstate_kunit.h | 25 +
.../rv/monitors/irqstate/irqstate_trace.h | 15 +
kernel/trace/rv/rv_monitors_test.c | 30 +
kernel/trace/rv/rv_trace.h | 2 +
tools/verification/models/irq/irqexit.dot | 21 +
tools/verification/models/irq/irqstate.dot | 32 +
29 files changed, 4737 insertions(+), 15 deletions(-)
create mode 100644 Documentation/trace/rv/monitor_irq.rst
create mode 100644 kernel/trace/rv/monitors/irq/Kconfig
create mode 100644 kernel/trace/rv/monitors/irq/irq.c
create mode 100644 kernel/trace/rv/monitors/irq/irq.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/irq/irqexit.dot
create mode 100644 tools/verification/models/irq/irqstate.dot
base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v2 1/9] tracing/irq: Add generic IRQ context boundary tracepoints
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 2/9] tracing/sched: Add IRQ preemption lifecycle tracepoints Karl Mehltretter
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, 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, while synthetic
dispatch can invoke an action outside hardirq context.
Add bare tracepoints at the generic hardirq and serving-softirq
boundaries using DECLARE_TRACE() so they can be hooked by in-kernel
verifiers and tests without exposing a new user-space tracefs ABI.
Record hardirq depth and whether serving-softirq state temporarily
replaces hardirq state during interrupt return.
A boundary callback can raise work after the normal pending-work
decision. Include work raised at softirq entry in the initial pending
snapshot, and hand work raised at a final boundary to the appropriate
worker.
The tracepoints cover irq_enter() or irq_enter_rcu() and their matching
exits. Raw and direct low-level entry users remain outside their scope.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
include/trace/events/irq.h | 56 +++++++++++++++++++++++++
kernel/softirq.c | 83 +++++++++++++++++++++++++++++++++++++-
2 files changed, 137 insertions(+), 2 deletions(-)
diff --git a/include/trace/events/irq.h b/include/trace/events/irq.h
index 837c1740d0d07..1b2e05aaf7763 100644
--- a/include/trace/events/irq.h
+++ b/include/trace/events/irq.h
@@ -207,6 +207,62 @@ DEFINE_EVENT(tasklet, tasklet_exit,
TP_ARGS(t, func)
);
+/*
+ * Following tracepoints are not exported in tracefs and provide hooking
+ * mechanisms only for testing and debugging purposes.
+ */
+
+/*
+ * hardirq_context_entry - generic hardirq state has been established
+ * @depth: hardirq nesting depth, including the context being entered
+ *
+ * This tracepoint 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().
+ */
+DECLARE_TRACE(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 tracepoint is emitted with interrupts disabled near the end of generic
+ * hardirq exit, after tick-exit work and before hardirq preempt-count state is
+ * removed. A late-work handoff may run after callbacks return. Hardirq
+ * accounting ends earlier.
+ */
+DECLARE_TRACE(hardirq_context_exit,
+ TP_PROTO(unsigned int depth),
+ TP_ARGS(depth));
+
+/*
+ * 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 tracepoint 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.
+ */
+DECLARE_TRACE(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 tracepoint runs with local interrupts disabled before the serving bit is
+ * cleared. On PREEMPT_RT this includes teardown after handle_softirqs().
+ */
+DECLARE_TRACE(softirq_context_exit,
+ TP_PROTO(bool serving, bool from_hardirq),
+ TP_ARGS(serving, from_hardirq));
+
#endif /* _TRACE_IRQ_H */
/* This part must be outside protection */
diff --git a/kernel/softirq.c b/kernel/softirq.c
index 7fc2200392cdf..0d5fe265e354b 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_tp_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_tp(!!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_tp_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_tp(!!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_tp(!!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_tp(!!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 @@ static void handle_softirqs(bool ksirqd)
wakeup_softirqd();
}
+ if (!IS_ENABLED(CONFIG_PREEMPT_RT) &&
+ trace_softirq_context_exit_tp_enabled()) {
+ trace_softirq_context_exit_tp(!!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_tp(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_tp_enabled()) {
+ bool ksoftirqd_wake_requested = false;
+ bool timersd_wake_requested = false;
+
+ trace_hardirq_context_exit_tp(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] 10+ messages in thread
* [RFC PATCH v2 2/9] tracing/sched: Add IRQ preemption lifecycle tracepoints
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 1/9] tracing/irq: Add generic IRQ context boundary tracepoints Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, linux-trace-kernel, linux-rt-devel,
linux-doc, linux-kernel
Tracepoints around __schedule() do not identify the start and end of one
preempt_schedule_irq() invocation. The calling task may switch out and
resume on another CPU before the invocation returns.
Add bare entry and exit tracepoints around the generic helper using
DECLARE_TRACE() so they are not exported in tracefs. Record the task
that called preempt_schedule_irq() as the owner in both tracepoints so
consumers can follow the invocation across switches and migration.
Sample the preempt count and interrupt state before dispatching
tracepoint callbacks.
The tracepoints 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.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
include/trace/events/sched.h | 26 ++++++++++++++++++++++++++
kernel/sched/core.c | 6 ++++++
2 files changed, 32 insertions(+)
diff --git a/include/trace/events/sched.h b/include/trace/events/sched.h
index 535860581f159..c7aeaa85d27ef 100644
--- a/include/trace/events/sched.h
+++ b/include/trace/events/sched.h
@@ -896,6 +896,32 @@ DECLARE_TRACE(sched_set_need_resched,
TP_PROTO(struct task_struct *tsk, int cpu, int tif),
TP_ARGS(tsk, cpu, tif));
+/*
+ * 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.
+ */
+DECLARE_TRACE(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.
+ */
+DECLARE_TRACE(sched_irq_preempt_exit,
+ TP_PROTO(struct task_struct *owner, unsigned long pc, bool irq_off),
+ TP_ARGS(owner, pc, irq_off));
+
#define DL_OTHER 0
#define DL_TASK 1
#define DL_SERVER_FAIR 2
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 04b62c16fa8ca..e5c476d27eca7 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -7649,12 +7649,16 @@ EXPORT_SYMBOL_GPL(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_tp_enabled())
+ trace_sched_irq_preempt_entry_tp(owner, preempt_count(), irqs_disabled());
do {
preempt_disable();
@@ -7664,6 +7668,8 @@ asmlinkage __visible void __sched preempt_schedule_irq(void)
sched_preempt_enable_no_resched();
} while (need_resched());
+ if (trace_sched_irq_preempt_exit_tp_enabled())
+ trace_sched_irq_preempt_exit_tp(owner, preempt_count(), irqs_disabled());
exception_exit(prev_state);
}
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v2 3/9] rv: Add a monitor for generic IRQ context transitions
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 1/9] tracing/irq: Add generic IRQ context boundary tracepoints Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 2/9] tracing/sched: Add IRQ preemption lifecycle tracepoints Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, 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 whether they agree with live kernel state.
Add an irq container and an irqstate hybrid-automaton monitor. It
checks generic hardirq and serving-softirq nesting against the kernel's
live 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 observe
NMI entry, architecture-specific intervals before generic IRQ entry or
after generic IRQ exit, or paths that bypass the generic IRQ entry and exit
helpers.
The generated model header is reproducible from the included DOT file
with the unmodified rvgen tool.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
kernel/trace/rv/Kconfig | 4 +
kernel/trace/rv/Makefile | 2 +
kernel/trace/rv/monitors/irq/Kconfig | 13 +
kernel/trace/rv/monitors/irq/irq.c | 35 +++
kernel/trace/rv/monitors/irq/irq.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 +
tools/verification/models/irq/irqstate.dot | 32 +++
11 files changed, 498 insertions(+)
create mode 100644 kernel/trace/rv/monitors/irq/Kconfig
create mode 100644 kernel/trace/rv/monitors/irq/irq.c
create mode 100644 kernel/trace/rv/monitors/irq/irq.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/irq/irqstate.dot
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index 9bfd429ffdea2..a9238e41806a6 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/irq/Kconfig"
+source "kernel/trace/rv/monitors/irqstate/Kconfig"
+# Add new irq 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 c895d81dfdad9..3c92202933207 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_IRQ) += monitors/irq/irq.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/irq/Kconfig b/kernel/trace/rv/monitors/irq/Kconfig
new file mode 100644
index 0000000000000..598459c2982d6
--- /dev/null
+++ b/kernel/trace/rv/monitors/irq/Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+config RV_MON_IRQ
+ depends on RV
+ bool "irq monitor"
+ help
+ Enable the container for monitors which check IRQ 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_irq.rst
diff --git a/kernel/trace/rv/monitors/irq/irq.c b/kernel/trace/rv/monitors/irq/irq.c
new file mode 100644
index 0000000000000..6542b99f9a7d7
--- /dev/null
+++ b/kernel/trace/rv/monitors/irq/irq.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 "irq"
+
+#include "irq.h"
+
+struct rv_monitor rv_irq = {
+ .name = "irq",
+ .description = "IRQ context monitors.",
+ .enable = NULL,
+ .disable = NULL,
+ .reset = NULL,
+ .enabled = 0,
+};
+
+static int __init register_irq(void)
+{
+ return rv_register_monitor(&rv_irq, NULL);
+}
+
+static void __exit unregister_irq(void)
+{
+ rv_unregister_monitor(&rv_irq);
+}
+
+module_init(register_irq);
+module_exit(unregister_irq);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Karl Mehltretter <kmehltretter@gmail.com>");
+MODULE_DESCRIPTION("irq: IRQ context monitors.");
diff --git a/kernel/trace/rv/monitors/irq/irq.h b/kernel/trace/rv/monitors/irq/irq.h
new file mode 100644
index 0000000000000..18dd4225b3be8
--- /dev/null
+++ b/kernel/trace/rv/monitors/irq/irq.h
@@ -0,0 +1,3 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+extern struct rv_monitor rv_irq;
diff --git a/kernel/trace/rv/monitors/irqstate/Kconfig b/kernel/trace/rv/monitors/irqstate/Kconfig
new file mode 100644
index 0000000000000..6accd907f14e4
--- /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_IRQ
+ 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_irq.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..bc7e9957bc750
--- /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/irq/irq.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_tp, handle_hardirq_entry);
+ rv_attach_trace_probe("irqstate", hardirq_context_exit_tp, handle_hardirq_exit);
+ rv_attach_trace_probe("irqstate", softirq_context_entry_tp, handle_softirq_entry);
+ rv_attach_trace_probe("irqstate", softirq_context_exit_tp, 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_tp, handle_hardirq_entry);
+ rv_detach_trace_probe("irqstate", hardirq_context_exit_tp, handle_hardirq_exit);
+ rv_detach_trace_probe("irqstate", softirq_context_entry_tp, handle_softirq_entry);
+ rv_detach_trace_probe("irqstate", softirq_context_exit_tp, 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_irq);
+}
+
+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/irq/irqstate.dot b/tools/verification/models/irq/irqstate.dot
new file mode 100644
index 0000000000000..90c41e8909e4e
--- /dev/null
+++ b/tools/verification/models/irq/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] 10+ messages in thread
* [RFC PATCH v2 4/9] rv: Add a monitor for IRQ-exit scheduling ownership
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
` (2 preceding siblings ...)
2026-11-07 17:12 ` [RFC PATCH v2 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 5/9] docs/rv: Document the IRQ monitors Karl Mehltretter
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, linux-trace-kernel, linux-rt-devel,
linux-doc, linux-kernel
Existing scheduler events do not retain the task that owns a
preempt_schedule_irq() invocation after that task switches out.
Add a per-task irqexit hybrid-automaton monitor. It follows an
invocation through entry, suspension, migration, resumption, and return.
Its guards verify 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.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
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 +
tools/verification/models/irq/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/irq/irqexit.dot
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index a9238e41806a6..9cdc881b8da29 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/irq/Kconfig"
source "kernel/trace/rv/monitors/irqstate/Kconfig"
+source "kernel/trace/rv/monitors/irqexit/Kconfig"
# Add new irq monitors here
source "kernel/trace/rv/monitors/rtapp/Kconfig"
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index 3c92202933207..90ad2361e0b05 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_IRQ) += monitors/irq/irq.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..292aae51be19f
--- /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_IRQ
+ 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_irq.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..f09a6ce5c463a
--- /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/irq/irq.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_tp, probe_resched_entry);
+ rv_attach_trace_probe("irqexit", sched_irq_preempt_exit_tp, 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_tp, probe_resched_entry);
+ rv_detach_trace_probe("irqexit", sched_irq_preempt_exit_tp, 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_irq);
+}
+
+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/irq/irqexit.dot b/tools/verification/models/irq/irqexit.dot
new file mode 100644
index 0000000000000..eb2e5499ad7a7
--- /dev/null
+++ b/tools/verification/models/irq/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] 10+ messages in thread
* [RFC PATCH v2 5/9] docs/rv: Document the IRQ monitors
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
` (3 preceding siblings ...)
2026-11-07 17:12 ` [RFC PATCH v2 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, linux-trace-kernel, linux-rt-devel,
linux-doc, linux-kernel
Document the irqstate and irqexit models, including their text
schematics, synchronization rules, PREEMPT_RT behavior, bare-tracepoint
boundaries, per-CPU vs per-task automaton separation, 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() as a per-task
automaton rather than treating it as the complete interrupt-return path.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
Documentation/trace/rv/index.rst | 1 +
Documentation/trace/rv/monitor_irq.rst | 350 +++++++++++++++++++++++++
2 files changed, 351 insertions(+)
create mode 100644 Documentation/trace/rv/monitor_irq.rst
diff --git a/Documentation/trace/rv/index.rst b/Documentation/trace/rv/index.rst
index 29769f06bb0f0..8dd4041e1a0eb 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_irq.rst
monitor_rtapp.rst
monitor_stall.rst
monitor_deadline.rst
diff --git a/Documentation/trace/rv/monitor_irq.rst b/Documentation/trace/rv/monitor_irq.rst
new file mode 100644
index 0000000000000..3e4aa819fcbbc
--- /dev/null
+++ b/Documentation/trace/rv/monitor_irq.rst
@@ -0,0 +1,350 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+IRQ monitors
+============
+
+- Name: irq
+- Type: container for IRQ context monitors
+- Author: Karl Mehltretter <kmehltretter@gmail.com>
+
+Description
+-----------
+
+The ``irq`` 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. The boundary hooks are bare tracepoints declared
+with ``DECLARE_TRACE()`` rather than user-space tracefs events.
+
+The two specifications use distinct automaton instances because they track
+different objects. ``irqstate`` is a per-CPU automaton: a hardirq interrupts
+whichever task happens to be running on that CPU, and non-RT softirq
+execution is CPU-local. ``irqexit`` is a per-task automaton: an invocation
+of ``preempt_schedule_irq()`` belongs to the interrupted task's stack frame,
+multiple tasks may be suspended inside the helper at the same time, and a
+suspended invocation can migrate to another CPU before returning. The
+container enables and disables both automata together by default while
+keeping per-CPU and per-task state independent.
+
+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``. The initial and accepting state is
+``idle``. The following schematics omit the guards listed above. To keep the
+state names short, ``A > B`` denotes the state reached when context ``B`` is
+entered from context ``A``; it does not imply that both context-count
+components remain set::
+
+ hardirq_exit
+ +----------------------------+
+ | v
+ +---------+ hardirq_entry #=================================#
+ | hardirq | <--------------- H idle H <+
+ +---------+ #=================================# |
+ | |
+ | softirq_entry | softirq_exit
+ | softirq_resume | softirq_suspend
+ v |
+ +---------------------------------+ |
+ +-------------> | softirq | -+
+ | +---------------------------------+
+ | |
+ | hardirq_exit | hardirq_entry
+ | v
+ | +---------------------------------+
+ +-------------- | softirq > hardirq |
+ +---------------------------------+
+
+The softirq interval during hardirq return and a hardirq nested within that
+interval form the remaining part of the model::
+
+ +-----------------------------+
+ | hardirq | <+
+ +-----------------------------+ |
+ | |
+ | softirq_entry_from_hardirq | softirq_exit_to_hardirq
+ v |
+ +-----------------------------+ |
+ +------------> | hardirq > softirq | -+
+ | +-----------------------------+
+ | |
+ | hardirq_exit | hardirq_entry
+ | v
+ | +-----------------------------+
+ +------------- | hardirq > softirq > hardirq |
+ +-----------------------------+
+
+The ``softirq_from_hardirq`` state 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 ``hardirq_from_softirq_from_hardirq`` state
+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 near the end of the generic
+exit path, after tick-exit work and before removal of ``HARDIRQ_OFFSET``.
+Hardirq accounting ends earlier, before inline softirq and tick-exit work.
+``irqstate`` returns to the outer model state at this boundary. When the event
+is enabled, the outermost-exit handoff passes pending timer work to ktimerd
+with forced IRQ threading. It also passes pending softirq work to ksoftirqd
+when no enclosing softirq or bottom-half-disabled state suppresses the wake.
+These handoffs catch work raised by tick-exit and event callbacks. 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 tracepoints pass preempt-count and interrupt-state snapshots
+taken at the scheduler call site, before tracepoint dispatch can change the
+observed context. Disabled hooks do not take these extra snapshots. The RV
+adapter still evaluates its original live-context guards; the snapshot
+arguments provide an independent check for direct tracepoint probes.
+
+The model has three states: ``idle``, ``rescheduling``, and ``suspended``.
+The initial and accepting state is ``idle``. The schematic omits the guards
+described above::
+
+ switch_in / switch_out #================#
+ +------------------------- H H
+ | H idle H
+ +------------------------> H H <+
+ #================# |
+ | |
+ | resched_entry | resched_exit
+ v |
+ +----------------+ |
+ +------------------------> | rescheduling | -+
+ | +----------------+
+ | |
+ | switch_in | switch_out
+ | v
+ | +----------------+
+ +------------------------- | suspended |
+ +----------------+
+
+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``
+ - ``irq/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
+- instrumentation reached while removing hardirq-count or non-RT
+ serving-softirq state, work after those removals, 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 irq:irqstate available_monitors
+ # echo irqstate > enabled_monitors
+ # echo irqexit >> enabled_monitors
+ # echo nop > monitors/irq/irqstate/reactors
+ # echo nop > monitors/irq/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] 10+ messages in thread
* [RFC PATCH v2 6/9] rv: Add KUnit coverage for IRQ context transitions
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
` (4 preceding siblings ...)
2026-11-07 17:12 ` [RFC PATCH v2 5/9] docs/rv: Document the IRQ monitors Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, 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 synchronously emitting a
synthetic hardirq boundary while only one of the two hardirq boundary
probes 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
decision. Verify that ksoftirqd is already runnable and drains the tasklet.
KUnit failure reporting may allocate with GFP_KERNEL. Snapshot reaction
counts inside synthetic IRQ-disabled, bottom-half-disabled, and hardirq
intervals, then evaluate expectations after normal task context is
restored. This avoids secondary atomic-sleep or lockdep diagnostics when
a monitor check fails.
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 require
non-exported helpers or tracepoints only when the suite is built in, and
report them as skipped for a module build.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
include/rv/kunit.h | 34 +-
kernel/trace/rv/monitors/irqstate/irqstate.c | 49 +
.../rv/monitors/irqstate/irqstate_kunit.c | 1420 +++++++++++++++++
.../rv/monitors/irqstate/irqstate_kunit.h | 25 +
kernel/trace/rv/rv_monitors_test.c | 12 +
5 files changed, 1530 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/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 bc7e9957bc750..89457c65d0441 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_tp, handle_hardirq_entry);
+ irqstate_test_lifecycle_point(true);
rv_attach_trace_probe("irqstate", hardirq_context_exit_tp, handle_hardirq_exit);
rv_attach_trace_probe("irqstate", softirq_context_entry_tp, handle_softirq_entry);
rv_attach_trace_probe("irqstate", softirq_context_exit_tp, 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_tp, handle_hardirq_entry);
+ irqstate_test_lifecycle_point(false);
rv_detach_trace_probe("irqstate", hardirq_context_exit_tp, handle_hardirq_exit);
rv_detach_trace_probe("irqstate", softirq_context_entry_tp, handle_softirq_entry);
rv_detach_trace_probe("irqstate", softirq_context_exit_tp, 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..e61c500a954a9
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
@@ -0,0 +1,1420 @@
+// 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_tp(hardirq_count() >> HARDIRQ_SHIFT);
+ trace_hardirq_context_exit_tp(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",
+ };
+ int ret;
+#endif
+ unsigned long flags;
+ int reactions[8];
+ unsigned int i;
+ bool completed;
+
+ prepare_test(test, &rv_irqstate_ops.mon);
+ /* Fatal KUnit exits do not unwind this migration guard. */
+ 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. */
+ completed = exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_VALID);
+ KUNIT_EXPECT_TRUE(test, completed);
+ if (!completed)
+ return;
+ RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+ completed = exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_INTERRUPTED);
+ KUNIT_EXPECT_TRUE(test, completed);
+ if (!completed)
+ return;
+ RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+ completed = exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_DOUBLE_ENTRY);
+ KUNIT_EXPECT_TRUE(test, completed);
+ if (!completed)
+ return;
+ RV_KUNIT_EXPECT_REACTION(test, ctx);
+ completed = exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_DOUBLE_EXIT);
+ KUNIT_EXPECT_TRUE(test, completed);
+ if (!completed)
+ return;
+ RV_KUNIT_EXPECT_REACTION(test, ctx);
+
+#ifndef MODULE
+ /* Exercise the actual generic tracepoint attachments as well. */
+ attach_irqstate_test_probes();
+ ret = kunit_add_action_or_reset(test, detach_irqstate_test_probes, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ if (ret)
+ return;
+
+ /* 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_tp(0);
+ reactions[1] = READ_ONCE(ctx->reactions);
+ reactions[2] = READ_ONCE(ctx->reactions);
+ trace_hardirq_context_exit_tp(0);
+ reactions[3] = READ_ONCE(ctx->reactions);
+ reactions[4] = READ_ONCE(ctx->reactions);
+ trace_softirq_context_entry_tp(false, false);
+ reactions[5] = READ_ONCE(ctx->reactions);
+ reactions[6] = READ_ONCE(ctx->reactions);
+ trace_softirq_context_exit_tp(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. */
+ completed = exercise_irqstate_softirq(IRQSTATE_SOFTIRQ_TRACEPOINT_ONLY);
+ KUNIT_EXPECT_TRUE(test, completed);
+ if (!completed)
+ return;
+ 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);
+ 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;
+
+ ret = register_trace_irq_handler_entry(irqstate_shared_action_entry,
+ shared_ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ shared_ctx->entry_attached = true;
+ ret = kunit_add_action_or_reset(test, irqstate_shared_actions_detach,
+ shared_ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ 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();
+ ret = kunit_add_action_or_reset(test, detach_irqstate_test_probes, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ scoped_guard(migrate) {
+ shared_ctx->owner = current;
+ shared_ctx->owner_cpu = raw_smp_processor_id();
+ 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);
+ /* Use only nonfatal checks and normal returns after pinning current. */
+ 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);
+ /* Use only nonfatal checks and normal returns after pinning current. */
+ 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_EXPECT_TRUE(test, completed);
+ if (!completed)
+ return;
+ 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_EXPECT_TRUE(test, completed);
+ if (!completed)
+ return;
+ 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_tp(irqstate_inline_bh_entry, inline_ctx);
+ if (inline_ctx->exit_attached)
+ unregister_trace_softirq_context_exit_tp(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_tp(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_tp(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);
+ /* Use only nonfatal checks and normal returns after pinning current. */
+ guard(migrate)();
+ inline_ctx->owner = current;
+ inline_ctx->owner_cpu = raw_smp_processor_id();
+ inline_ctx->softirqd = this_cpu_ksoftirqd();
+ KUNIT_EXPECT_NOT_NULL(test, inline_ctx->softirqd);
+ if (!inline_ctx->softirqd)
+ return;
+ if (local_softirq_pending() ||
+ !wait_task_inactive(inline_ctx->softirqd, TASK_INTERRUPTIBLE)) {
+ kunit_mark_skipped(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_mark_skipped(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_mark_skipped(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_tp(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_tp(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;
+ int ret;
+
+ if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+ kunit_skip(test, "Requires preemptible softirq processing");
+ return;
+ }
+
+ prepare_test(test, &rv_irqstate_ops.mon);
+ 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_NOT_ERR_OR_NULL(test, worker);
+ preempt_ctx->worker = worker;
+ ret = kunit_add_action_or_reset(test, stop_irqstate_preempt_worker,
+ worker);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ attach_irqstate_test_probes();
+ ret = kunit_add_action_or_reset(test, detach_irqstate_test_probes, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ scoped_guard(migrate) {
+ kthread_bind(worker, smp_processor_id());
+ sched_set_fifo(worker);
+ 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);
+ if (completed)
+ completed = wait_for_completion_timeout(&preempt_ctx->worker_done,
+ HZ);
+ }
+ KUNIT_ASSERT_NE(test, completed, 0UL);
+ 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[3];
+ int ret;
+ bool serving;
+
+ if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+ kunit_skip(test, "Requires RT serving-softirq accounting");
+ return;
+ }
+
+ prepare_test(test, &rv_irqstate_ops.mon);
+ attach_irqstate_test_probes();
+ ret = kunit_add_action_or_reset(test, detach_irqstate_test_probes, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ saved_count = preempt_count();
+ saved_softirq = softirq_count();
+
+ scoped_guard(migrate) {
+ /*
+ * 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();
+
+ reactions[0] = READ_ONCE(ctx->reactions);
+ local_irq_save(flags);
+ __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);
+ }
+
+ 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_OBSERVED(test, ctx, reactions[0], 0);
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 0);
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 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[3];
+ int ret;
+ 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);
+ 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_NOT_ERR_OR_NULL(test, worker);
+ ret = kunit_add_action_or_reset(test, stop_irqstate_preempt_worker,
+ worker);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ attach_irqstate_test_probes();
+ ret = kunit_add_action_or_reset(test, detach_irqstate_test_probes, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ saved_count = preempt_count();
+ saved_softirq = softirq_count();
+
+ scoped_guard(migrate) {
+ kthread_bind(worker, smp_processor_id());
+ sched_set_fifo(worker);
+ 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);
+ reactions[0] = READ_ONCE(ctx->reactions);
+ local_irq_save(flags);
+ __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);
+ }
+ 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_OBSERVED(test, ctx, reactions[0], 0);
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[1], 0);
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, reactions[2], 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] 10+ messages in thread
* [RFC PATCH v2 7/9] rv: Add KUnit coverage for IRQ-exit scheduling
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
` (5 preceding siblings ...)
2026-11-07 17:12 ` [RFC PATCH v2 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 8/9] rv: Test empty PREEMPT_RT softirq worker intervals Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, 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 a switch, across a task switch,
and across CPU migration 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. Snapshot reaction
counts inside synthetic IRQ-disabled and hardirq intervals, then evaluate
expectations after normal task context is restored. This avoids secondary
atomic-sleep or lockdep diagnostics when a monitor check fails.
The live tests invoke the generic helper directly. They do not exercise
an architecture-specific physical interrupt-return path.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
kernel/trace/rv/monitors/irqexit/irqexit.c | 13 +
.../trace/rv/monitors/irqexit/irqexit_kunit.c | 540 ++++++++++++++++++
.../trace/rv/monitors/irqexit/irqexit_kunit.h | 21 +
kernel/trace/rv/rv_monitors_test.c | 13 +
4 files changed, 587 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/kernel/trace/rv/monitors/irqexit/irqexit.c b/kernel/trace/rv/monitors/irqexit/irqexit.c
index f09a6ce5c463a..ff85504e9cead 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..d5d6b8eefbbf6
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
@@ -0,0 +1,540 @@
+// 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_tp(irqexit_observe_entry, live);
+ if (ret)
+ return ret;
+ live->raw_entry_attached = true;
+ ret = register_trace_sched_irq_preempt_exit_tp(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_tp(irqexit_observe_entry, live);
+ if (live->raw_exit_attached)
+ unregister_trace_sched_irq_preempt_exit_tp(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;
+ int ret;
+
+ 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));
+ worker = kthread_create(irqexit_live_worker, live, "rv-irqexit-test");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, worker);
+ live->worker = worker;
+ ret = register_trace_event_irqexit(irqexit_observe_event, live);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ live->observer_attached = true;
+ ret = irqexit_observe_boundaries(live);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ mon->enabled = 1;
+
+ KUNIT_ASSERT_EQ(test, preempt_count(), 0);
+ /* The worker must run on this CPU inside the explicit scheduling loop. */
+ scoped_guard(migrate) {
+ kthread_bind(worker, smp_processor_id());
+ sched_set_fifo(worker);
+ 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] 10+ messages in thread
* [RFC PATCH v2 8/9] rv: Test empty PREEMPT_RT softirq worker intervals
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
` (6 preceding siblings ...)
2026-11-07 17:12 ` [RFC PATCH v2 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, linux-trace-kernel, linux-rt-devel,
linux-doc, linux-kernel
On PREEMPT_RT, a worker enters serving-softirq state before it checks for
pending work and leaves that state afterward. A worker with no pending
action therefore creates a valid interval that action-based tests miss.
Add KUnit-only helpers that exercise the worker boundaries directly and
force the real per-CPU ksoftirqd task through one invocation with no
pending softirqs. Preserve pending work that races with the test seam.
Verify that each path emits one context pair without an intervening softirq
action.
Keep the seam inactive outside KUnit tests. Gate its predicate and worker
branches with the existing kunit_running static key, and isolate the test
bookkeeping in a helper. Outside a KUnit run, both added worker checks take
only the disabled static-key path.
When another KUnit suite enables the global static key, check the per-CPU
phase before trying the atomic compare-exchange. Keep the compare-exchange
as the arbiter between the worker claim and timeout disarm.
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.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
include/linux/interrupt.h | 7 +
kernel/softirq.c | 228 ++++++++++++++++-
kernel/trace/rv/Kconfig | 6 +-
.../rv/monitors/irqstate/irqstate_kunit.c | 239 ++++++++++++++++++
kernel/trace/rv/rv_monitors_test.c | 2 +
5 files changed, 479 insertions(+), 3 deletions(-)
diff --git a/include/linux/interrupt.h b/include/linux/interrupt.h
index ee2dd5349697d..e1a4090596609 100644
--- a/include/linux/interrupt.h
+++ b/include/linux/interrupt.h
@@ -599,6 +599,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 0d5fe265e354b..f6ed814f1f94a 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,190 @@ 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);
+ if (atomic_read(&worker->phase) != SOFTIRQ_KUNIT_EMPTY_ARMED ||
+ current != __this_cpu_read(ksoftirqd))
+ return false;
+
+ return 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 +1385,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/Kconfig b/kernel/trace/rv/Kconfig
index 9cdc881b8da29..2dc97edc38b1e 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -139,7 +139,9 @@ config RV_MONITORS_KUNIT_TEST
These tests verify that monitors correctly detect violations by
triggering fake events and validating the expected reactions.
- Enabling this may slightly increase overhead of some monitors if any
- unrelated KUnit test is running.
+ Building this option, including as a module, adds test-only hooks to
+ some monitored paths. KUnit's static key keeps the hooks dormant while
+ no test is running. Running this or another KUnit suite can temporarily
+ add slight overhead to those paths.
If unsure, say N.
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
index e61c500a954a9..40fe9fd4b1fb4 100644
--- a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
@@ -885,6 +885,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)
{
@@ -1154,6 +1185,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_tp(irqstate_empty_worker_exit,
+ empty_ctx);
+ if (empty_ctx->entry_attached)
+ unregister_trace_softirq_context_entry_tp(
+ 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_tp(
+ 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_tp(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;
@@ -1389,6 +1616,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");
@@ -1414,6 +1651,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] 10+ messages in thread
* [RFC PATCH v2 9/9] rv: Test IRQ-exit softirq provenance and handoff
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
` (7 preceding siblings ...)
2026-11-07 17:12 ` [RFC PATCH v2 8/9] rv: Test empty PREEMPT_RT softirq worker intervals Karl Mehltretter
@ 2026-11-07 17:12 ` Karl Mehltretter
8 siblings, 0 replies; 10+ messages in thread
From: Karl Mehltretter @ 2026-11-07 17:12 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, Wen Yang, linux-trace-kernel, linux-rt-devel,
linux-doc, linux-kernel
IRQ-exit softirq processing preserves hardirq state until it finishes. Test
the resulting hardirq-to-softirq-to-hardirq sequence, including a nested
hardirq and monitor attachment during the transition.
Boundary and tick-exit callbacks can themselves raise more softirq work.
Raise work at softirq entry, softirq exit, the final hardirq boundary, and
a full-NOHZ tick_stop event. Verify that ordinary work reaches ksoftirqd,
forced-thread timer work reaches ktimerd, and a timer-wakeup callback which
raises ordinary work gets the final ksoftirqd recheck.
Track the synthetic owner and serving-softirq interval so unrelated nested
interrupts do not corrupt the expected sequence. Reject the handoff setup
while bottom halves are blocked, attach observers before waiting for
workers, and recheck the setup with interrupts disabled.
On PREEMPT_RT, verify that worker events do not report hardirq provenance.
Do not run the synthetic inline IRQ-exit cases there. IRQ exit defers
softirqs to workers, and serving-softirq state is task-local rather than
encoded solely in preempt_count().
Snapshot reaction counts inside synthetic IRQ-disabled and hardirq
intervals, then evaluate the KUnit expectations after restoring normal task
context.
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
Documentation/trace/rv/monitor_irq.rst | 125 ++++
include/linux/interrupt.h | 1 +
kernel/softirq.c | 15 +-
.../rv/monitors/irqstate/irqstate_kunit.c | 684 +++++++++++++++++-
kernel/trace/rv/rv_monitors_test.c | 3 +
5 files changed, 825 insertions(+), 3 deletions(-)
diff --git a/Documentation/trace/rv/monitor_irq.rst b/Documentation/trace/rv/monitor_irq.rst
index 3e4aa819fcbbc..f1e7b3d8923a6 100644
--- a/Documentation/trace/rv/monitor_irq.rst
+++ b/Documentation/trace/rv/monitor_irq.rst
@@ -300,6 +300,131 @@ 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. 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 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_tick_exit_handoff`` runs on a full-NOHZ CPU and forces a
+``tick_stop`` event from the real IRQ-exit tick path. Its callback raises a
+tasklet after normal softirq processing. The test requires the final hardirq
+boundary to make ksoftirqd runnable before interrupts are restored. It skips
+when no full-NOHZ CPU is online.
+
+``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;
+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. 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
+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
+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/include/linux/interrupt.h b/include/linux/interrupt.h
index e1a4090596609..851d2e3769946 100644
--- a/include/linux/interrupt.h
+++ b/include/linux/interrupt.h
@@ -602,6 +602,7 @@ 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);
+bool softirq_kunit_local_bh_blocked(void);
int softirq_kunit_run_empty_ksoftirqd(struct task_struct **owner,
int *owner_cpu, bool *armed);
#endif
diff --git a/kernel/softirq.c b/kernel/softirq.c
index f6ed814f1f94a..196029a670a8a 100644
--- a/kernel/softirq.c
+++ b/kernel/softirq.c
@@ -630,6 +630,16 @@ struct softirq_kunit_empty_worker {
static DEFINE_PER_CPU(struct softirq_kunit_empty_worker,
softirq_kunit_empty_workers);
+bool softirq_kunit_local_bh_blocked(void)
+{
+#ifdef CONFIG_PREEMPT_RT
+ return local_bh_blocked();
+#else
+ return false;
+#endif
+}
+EXPORT_SYMBOL_IF_KUNIT(softirq_kunit_local_bh_blocked);
+
/*
* Exercise the worker boundaries without entering handle_softirqs().
*
@@ -1038,7 +1048,10 @@ static inline void __irq_exit_rcu(void)
trace_hardirq_context_exit_tp(hardirq_count() >> HARDIRQ_SHIFT);
- /* Hand work raised by the final boundary event to its worker. */
+ /*
+ * Hand off work raised after normal softirq processing, including
+ * by tick-exit and final-boundary callbacks.
+ */
if (hardirq_count() == HARDIRQ_OFFSET) {
if (irq_count() == HARDIRQ_OFFSET &&
unlikely(local_softirq_pending()) &&
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
index 40fe9fd4b1fb4..99bd51d72d25c 100644
--- a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
@@ -9,10 +9,13 @@
#include <linux/kthread.h>
#include <linux/rv.h>
#include <linux/sched.h>
+#include <linux/tick.h>
#include <linux/wait.h>
#include <rv/kunit.h>
#include <rv_trace.h>
#include <trace/events/irq.h>
+#include <trace/events/sched.h>
+#include <trace/events/timer.h>
#include "irqstate_kunit.h"
@@ -512,6 +515,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();
@@ -830,6 +871,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_EXPECT_TRUE(test, completed);
@@ -861,20 +928,33 @@ 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 nested_hardirq_runs;
+ unsigned int entry_tasklet_runs;
unsigned int exit_tasklet_runs;
bool armed;
+ bool raise_on_entry;
bool raise_on_exit;
+ bool inject_nested_hardirq;
+ 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;
@@ -883,6 +963,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 {
@@ -906,6 +988,44 @@ 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;
+};
+
+#ifdef CONFIG_NO_HZ_FULL
+struct irqstate_tick_exit_ctx {
+ struct kunit *test;
+ struct tasklet_struct tasklet;
+ struct task_struct *owner;
+ struct task_struct *softirqd;
+ cpumask_t saved_mask;
+ unsigned int tick_dep_before;
+ unsigned int tick_dep_after;
+ int owner_cpu;
+ int tick_success;
+ int tick_dependency;
+ bool armed;
+ bool callback_seen;
+ bool tasklet_pending;
+ bool softirqd_already_runnable;
+ bool bad_context;
+ bool tick_attached;
+ bool tick_dep_added;
+ bool tick_dep_sampled;
+ bool affinity_changed;
+ bool hotplug_disabled;
+};
+#endif
+
static bool irqstate_empty_worker_matches(struct irqstate_empty_worker_ctx *empty_ctx)
{
/* Pairs with owner publication in both empty-worker test paths. */
@@ -916,6 +1036,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 ||
+ in_serving_softirq() || 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 ||
+ in_serving_softirq() || 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)
{
@@ -924,10 +1077,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,
@@ -938,8 +1102,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)) {
@@ -951,6 +1122,7 @@ static void irqstate_inline_bh_exit(void *data, bool serving,
static void irqstate_inline_bh_tasklet(struct tasklet_struct *tasklet)
{
struct irqstate_inline_bh_ctx *inline_ctx;
+ unsigned long flags;
inline_ctx = from_tasklet(inline_ctx, tasklet, tasklet);
inline_ctx->tasklet_runs++;
@@ -962,6 +1134,13 @@ static void irqstate_inline_bh_tasklet(struct tasklet_struct *tasklet)
inline_ctx->tasklet_in_hardirq = !!in_hardirq();
inline_ctx->tasklet_bracketed = inline_ctx->entries == 1 &&
inline_ctx->exits == 0;
+ if (inline_ctx->inject_nested_hardirq) {
+ local_irq_save(flags);
+ irq_enter_rcu();
+ irq_exit_rcu();
+ local_irq_restore(flags);
+ inline_ctx->nested_hardirq_runs++;
+ }
}
static void irqstate_inline_bh_exit_tasklet(struct tasklet_struct *tasklet)
@@ -972,6 +1151,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;
@@ -984,6 +1171,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);
}
@@ -992,6 +1180,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_tp(
+ irqstate_inline_hardirq_exit, inline_ctx);
+ if (inline_ctx->hardirq_entry_attached)
+ unregister_trace_hardirq_context_entry_tp(
+ irqstate_inline_hardirq_entry, inline_ctx);
if (inline_ctx->entry_attached)
unregister_trace_softirq_context_entry_tp(irqstate_inline_bh_entry, inline_ctx);
if (inline_ctx->exit_attached)
@@ -999,6 +1193,444 @@ 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_tp(
+ irqstate_hardirq_exit_raise, exit_ctx);
+ tracepoint_synchronize_unregister();
+ tasklet_kill(&exit_ctx->tasklet);
+}
+
+#ifdef CONFIG_NO_HZ_FULL
+static void irqstate_tick_exit_tasklet(struct tasklet_struct *tasklet)
+{
+ (void)tasklet;
+}
+
+static void irqstate_tick_exit_raise(void *data, int success, int dependency)
+{
+ struct irqstate_tick_exit_ctx *tick_ctx = data;
+
+ if (!READ_ONCE(tick_ctx->armed) ||
+ current != READ_ONCE(tick_ctx->owner) ||
+ READ_ONCE(tick_ctx->callback_seen))
+ return;
+
+ WRITE_ONCE(tick_ctx->callback_seen, true);
+ tick_ctx->tick_success = success;
+ tick_ctx->tick_dependency = dependency;
+ if (success || !dependency ||
+ hardirq_count() != HARDIRQ_OFFSET ||
+ irq_count() != HARDIRQ_OFFSET || in_serving_softirq() ||
+ !irqs_disabled() ||
+ raw_smp_processor_id() != tick_ctx->owner_cpu)
+ tick_ctx->bad_context = true;
+ tasklet_schedule(&tick_ctx->tasklet);
+}
+
+static void irqstate_tick_exit_restore_dep(struct irqstate_tick_exit_ctx *tick_ctx)
+{
+ if (tick_ctx->tick_dep_added) {
+ tick_dep_clear_task(tick_ctx->owner, TICK_DEP_BIT_POSIX_TIMER);
+ tick_ctx->tick_dep_added = false;
+ }
+ if (!tick_ctx->tick_dep_sampled) {
+ tick_ctx->tick_dep_after =
+ atomic_read(&tick_ctx->owner->tick_dep_mask);
+ tick_ctx->tick_dep_sampled = true;
+ }
+}
+
+static void irqstate_tick_exit_cleanup(void *data)
+{
+ struct irqstate_tick_exit_ctx *tick_ctx = data;
+ int ret;
+
+ WRITE_ONCE(tick_ctx->armed, false);
+ if (tick_ctx->tick_attached)
+ unregister_trace_tick_stop(irqstate_tick_exit_raise, tick_ctx);
+ tracepoint_synchronize_unregister();
+ irqstate_tick_exit_restore_dep(tick_ctx);
+ tasklet_kill(&tick_ctx->tasklet);
+ if (tick_ctx->affinity_changed &&
+ !(READ_ONCE(tick_ctx->owner->flags) & PF_EXITING)) {
+ ret = set_cpus_allowed_ptr(tick_ctx->owner,
+ &tick_ctx->saved_mask);
+ KUNIT_EXPECT_EQ(tick_ctx->test, ret, 0);
+ if (!ret)
+ tick_ctx->affinity_changed = false;
+ }
+ if (tick_ctx->hotplug_disabled) {
+ cpu_hotplug_enable();
+ tick_ctx->hotplug_disabled = false;
+ }
+ put_task_struct(tick_ctx->owner);
+ tick_ctx->owner = NULL;
+}
+#endif
+
+/* 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;
+ const char *skip_reason = NULL;
+ unsigned long flags;
+ int ret;
+
+ prepare_test(test, &rv_irqstate_ops.mon);
+ 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);
+ ret = register_trace_hardirq_context_exit_tp(
+ 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));
+
+ migrate_disable();
+ exit_ctx->owner = current;
+ exit_ctx->owner_cpu = raw_smp_processor_id();
+ exit_ctx->softirqd = this_cpu_ksoftirqd();
+ if (!exit_ctx->softirqd || local_softirq_pending() ||
+ softirq_kunit_local_bh_blocked() ||
+ !wait_task_inactive(exit_ctx->softirqd, TASK_INTERRUPTIBLE)) {
+ skip_reason = "Requires an idle local ksoftirqd";
+ goto out_enable_migration;
+ }
+
+ local_irq_save(flags);
+ if (local_softirq_pending() || softirq_kunit_local_bh_blocked() ||
+ task_is_running(exit_ctx->softirqd)) {
+ local_irq_restore(flags);
+ skip_reason = "Local ksoftirqd stopped being idle";
+ goto out_enable_migration;
+ }
+ 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);
+
+out_enable_migration:
+ migrate_enable();
+ if (skip_reason) {
+ kunit_skip(test, "%s", skip_reason);
+ return;
+ }
+
+ 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_NO_HZ_FULL
+/* Tick-exit instrumentation must run before the final hardirq handoff. */
+static void rv_test_irqstate_tick_exit_handoff(struct kunit *test)
+{
+ struct irqstate_tick_exit_ctx *tick_ctx;
+ struct rv_kunit_ctx *ctx = test->priv;
+ const char *skip_reason = NULL;
+ unsigned long flags;
+ unsigned int cpu;
+ int ret;
+
+ prepare_test(test, &rv_irqstate_ops.mon);
+ tick_ctx = kunit_kzalloc(test, sizeof(*tick_ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, tick_ctx);
+ tick_ctx->test = test;
+ tick_ctx->owner = current;
+ get_task_struct(tick_ctx->owner);
+ tasklet_setup(&tick_ctx->tasklet, irqstate_tick_exit_tasklet);
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test,
+ irqstate_tick_exit_cleanup,
+ tick_ctx));
+ ret = register_trace_tick_stop(irqstate_tick_exit_raise, tick_ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ tick_ctx->tick_attached = true;
+ attach_irqstate_test_probes();
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+ NULL));
+
+ cpu_hotplug_disable();
+ tick_ctx->hotplug_disabled = true;
+ for_each_online_cpu(cpu) {
+ if (tick_nohz_full_cpu(cpu))
+ break;
+ }
+ if (cpu >= nr_cpu_ids) {
+ skip_reason = "Requires an online full-NOHZ CPU";
+ goto out;
+ }
+
+ cpumask_copy(&tick_ctx->saved_mask, current->cpus_ptr);
+ ret = set_cpus_allowed_ptr(current, cpumask_of(cpu));
+ if (ret) {
+ skip_reason = "Could not run on a full-NOHZ CPU";
+ goto out;
+ }
+ tick_ctx->affinity_changed = true;
+ if (raw_smp_processor_id() != cpu) {
+ skip_reason = "Did not migrate to the full-NOHZ CPU";
+ goto out;
+ }
+
+ tick_ctx->owner_cpu = cpu;
+ tick_ctx->softirqd = per_cpu(ksoftirqd, cpu);
+ if (!tick_ctx->softirqd ||
+ !wait_task_inactive(tick_ctx->softirqd, TASK_INTERRUPTIBLE)) {
+ skip_reason = "Requires an idle local ksoftirqd";
+ goto out;
+ }
+
+ local_irq_save(flags);
+ if (local_softirq_pending() || softirq_kunit_local_bh_blocked() ||
+ task_is_running(tick_ctx->softirqd)) {
+ skip_reason = "Local ksoftirqd stopped being idle";
+ goto out_restore_irqs;
+ }
+
+ tick_ctx->tick_dep_before = atomic_read(¤t->tick_dep_mask);
+ if (!(tick_ctx->tick_dep_before & TICK_DEP_MASK_POSIX_TIMER)) {
+ tick_dep_set_task(current, TICK_DEP_BIT_POSIX_TIMER);
+ tick_ctx->tick_dep_added = true;
+ }
+ WRITE_ONCE(tick_ctx->armed, true);
+ irq_enter_rcu();
+ irq_exit_rcu();
+ WRITE_ONCE(tick_ctx->armed, false);
+ irqstate_tick_exit_restore_dep(tick_ctx);
+ tick_ctx->tasklet_pending =
+ !!(local_softirq_pending() & BIT(TASKLET_SOFTIRQ));
+ /* A zero return proves that the final handoff already woke the worker. */
+ tick_ctx->softirqd_already_runnable =
+ !wake_up_process(tick_ctx->softirqd);
+
+out_restore_irqs:
+ local_irq_restore(flags);
+out:
+ if (skip_reason) {
+ kunit_mark_skipped(test, "%s", skip_reason);
+ return;
+ }
+
+ kunit_release_action(test, irqstate_tick_exit_cleanup, tick_ctx);
+ KUNIT_EXPECT_TRUE(test, tick_ctx->callback_seen);
+ KUNIT_EXPECT_EQ(test, tick_ctx->tick_success, 0);
+ KUNIT_EXPECT_NE(test, tick_ctx->tick_dependency, 0);
+ KUNIT_EXPECT_EQ(test, tick_ctx->tick_dep_after,
+ tick_ctx->tick_dep_before);
+ KUNIT_EXPECT_TRUE(test, tick_ctx->tasklet_pending);
+ KUNIT_EXPECT_TRUE(test, tick_ctx->softirqd_already_runnable);
+ KUNIT_EXPECT_FALSE(test, tick_ctx->bad_context);
+ RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+}
+#else
+static void rv_test_irqstate_tick_exit_handoff(struct kunit *test)
+{
+ kunit_skip(test, "Requires full NO_HZ support");
+}
+#endif
+
+#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_tp(
+ 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;
+ const char *skip_reason = NULL;
+ unsigned long flags;
+ int ret;
+
+ if (!force_irqthreads()) {
+ kunit_skip(test, "Requires forced IRQ threading");
+ return;
+ }
+
+ prepare_test(test, &rv_irqstate_ops.mon);
+ 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);
+ ret = register_trace_hardirq_context_exit_tp(
+ 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));
+
+ migrate_disable();
+ 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);
+ if (!timer_ctx->softirqd || !timer_ctx->timersd ||
+ local_softirq_pending() || local_timers_pending_force_th() ||
+ softirq_kunit_local_bh_blocked() ||
+ !wait_task_inactive(timer_ctx->softirqd, TASK_INTERRUPTIBLE) ||
+ !wait_task_inactive(timer_ctx->timersd, TASK_INTERRUPTIBLE)) {
+ skip_reason = "Requires idle local softirq workers";
+ goto out_enable_migration;
+ }
+
+ local_irq_save(flags);
+ if (local_softirq_pending() || local_timers_pending_force_th() ||
+ softirq_kunit_local_bh_blocked() ||
+ task_is_running(timer_ctx->softirqd) ||
+ task_is_running(timer_ctx->timersd)) {
+ local_irq_restore(flags);
+ skip_reason = "Local softirq worker stopped being idle";
+ goto out_enable_migration;
+ }
+ 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);
+
+out_enable_migration:
+ migrate_enable();
+ if (skip_reason) {
+ kunit_skip(test, "%s", skip_reason);
+ return;
+ }
+
+ 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)
{
@@ -1018,8 +1650,12 @@ 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;
+ inline_ctx->inject_nested_hardirq = true;
KUNIT_ASSERT_EQ(test, 0,
kunit_add_action_or_reset(test, irqstate_inline_bh_kill_all,
inline_ctx));
@@ -1031,6 +1667,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_tp(
+ irqstate_inline_hardirq_entry, inline_ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ inline_ctx->hardirq_entry_attached = true;
+ ret = register_trace_hardirq_context_exit_tp(
+ irqstate_inline_hardirq_exit, inline_ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ inline_ctx->hardirq_exit_attached = true;
ret = register_trace_softirq_context_exit_tp(irqstate_inline_bh_exit,
inline_ctx);
KUNIT_ASSERT_EQ(test, ret, 0);
@@ -1065,6 +1709,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);
@@ -1090,6 +1735,8 @@ 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->nested_hardirq_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);
@@ -1101,8 +1748,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);
@@ -1132,6 +1786,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,
@@ -1171,8 +1826,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);
@@ -1197,7 +1857,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;
@@ -1214,7 +1875,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;
@@ -1606,6 +2268,21 @@ 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_tick_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");
@@ -1649,6 +2326,9 @@ 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_tick_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..0b8a55d02645c 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -167,6 +167,9 @@ 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_tick_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] 10+ messages in thread
end of thread, other threads:[~2026-11-07 17:13 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 1/9] tracing/irq: Add generic IRQ context boundary tracepoints Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 2/9] tracing/sched: Add IRQ preemption lifecycle tracepoints Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 5/9] docs/rv: Document the IRQ monitors Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 8/9] rv: Test empty PREEMPT_RT softirq worker intervals Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
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®