* [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails
@ 2026-10-22 5:16 Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 01/12] sched: Split preempt-count bookkeeping from raw updates Karl Mehltretter
` (11 more replies)
0 siblings, 12 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
Tracing's transition recursion slot exists because a callback can run after
architectural hardirq entry but before generic entry increments
preempt_count(). Steve asked whether the count could be updated soon enough
to remove that slot. This RFC establishes that entry contract on arm64 and
closes a separate recursion window exposed by the same tests.
This is a design RFC, not a merge-ready proposal. I am looking for
feedback on the entry contract before extending it to other architectures
or removing TRACE_CTX_TRANSITION.
Clang can outline raw preempt-count updates and
__kern_my_cpu_offset() from noinstr entry code. This series applies to a
tree with the following two patches added:
- Hongyan Xia, "arm64: Always inline raw preempt count updates"
https://lore.kernel.org/all/c3a90368dd6c096af9d918e036a27728696fb8a4.1786603168.git.hongyan.xia@transsion.com/
- Will Deacon, "arm64: percpu: Annotate __kern_my_cpu_offset() as
'__always_inline'"
https://lore.kernel.org/r/20260918161407.2300-12-will@kernel.org
Those patches are external prerequisites and are not included in this
RFC.
Patches 1 through 11 split raw preempt-count updates from generic IRQ
bookkeeping. On arm64, ordinary interrupts add HARDIRQ_OFFSET after the
pseudo-NMI decision but before instrumentable generic entry work. The
remaining accounting still happens in irq_enter_rcu().
The exit side has a similar gap. Function-graph and MTE callbacks can run
after the real hardirq state has been removed, and kernel return may
schedule before them. The series records ordinary-hardirq provenance and
restores a synthetic hardirq classification after the preemption decision.
That tail changes only preempt-count and lockdep state. It does not repeat
interrupt accounting, RCU, or tick entry.
Patch 12 keeps tracing's recursion state inside one preemption-disabled
interval. It closes the transition collision reproduced at the IRQ-exit
reschedule boundary. TRACE_CTX_TRANSITION remains unchanged because
unconverted architecture entry intervals can still run before
preempt_count() reflects hardirq context. An x86-64 no-transition control
still recorded recursion at system-vector entry and exit and in the
deferred hrtimer return tail.
The base includes "entry: Account the deferred hrtimer rearm as hardirq
context for lockdep" and "softirq: Preserve interrupt context on return
from interrupt".
The main questions are:
- Should generic entry expose separate raw-update and bookkeeping phases?
- Are preempt count and lockdep state sufficient for the return tail?
- Is arm64 a suitable first user of this contract?
- Should the recursion-lifetime fix travel with the entry conversion?
Karl Mehltretter (12):
sched: Split preempt-count bookkeeping from raw updates
lockdep: Use raw access for pinned hardirq state
entry: Add the pre-counted ordinary-hardirq entry/exit contract
entry: Skip inactive IRQ-return reschedule calls
context_tracking: Split IRQ-exit preparation from the EQS transition
lockdep: Prepare IRQ-on state from a hardirq return tail
entry: Carry ordinary-hardirq provenance through kernel return
entry: Classify ordinary hardirq return-tail work as hardirq context
arm64: Use the pre-counted hardirq lifecycle
docs: entry: Describe the pre-counted hardirq lifecycle
locking/selftest: Exercise hardirq return-tail preparation
tracing: Pin recursion state for its complete lifetime
Documentation/core-api/entry.rst | 75 ++++++++++--
arch/arm64/include/asm/preempt.h | 2 +-
arch/arm64/kernel/entry-common.c | 25 ++--
include/linux/context_tracking_irq.h | 32 +++++-
include/linux/hardirq.h | 29 +++++
include/linux/interrupt.h | 2 +-
include/linux/irq-entry-common.h | 96 ++++++++++++++--
include/linux/irqflags.h | 14 ++-
include/linux/preempt.h | 9 ++
include/linux/trace_recursion.h | 14 ++-
kernel/context_tracking.c | 78 ++++++++++---
kernel/entry/common.c | 38 +++++-
kernel/locking/lockdep.c | 48 +++++---
kernel/sched/core.c | 68 +++++++++--
kernel/softirq.c | 166 +++++++++++++++++++++++----
lib/locking-selftest.c | 53 +++++++++
16 files changed, 648 insertions(+), 101 deletions(-)
base-commit: 779ab5d73a22c798d1273b4aaf50cf5b8d750be9
prerequisite-patch-id: 9c009809614854fbff979d65aad6732757d006d5
prerequisite-patch-id: 764fd197db36a3889548bbc41890dd5b3d902f99
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 13+ messages in thread
* [RFC PATCH 01/12] sched: Split preempt-count bookkeeping from raw updates
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 02/12] lockdep: Use raw access for pinned hardirq state Karl Mehltretter
` (10 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
Very early entry code may need to update the preempt count before
instrumentable debug and tracing code can run. Conversely, return code may
need to finish instrumentable validation and latency tracing before raw
teardown.
Add preempt_count_add_finish() for bookkeeping after a raw addition. Add
preempt_count_sub_prepare() for validation and tracing before a raw
subtraction. Keep preempt_count_add() and preempt_count_sub() behavior and
ordering unchanged.
Keep latency tracing in each calling path so CALLER_ADDR0 identifies the
caller. Force the start and stop helpers inline because trace provenance
depends on their stack frames.
Assisted-by: LLM
---
include/linux/preempt.h | 9 ++++++
kernel/sched/core.c | 68 +++++++++++++++++++++++++++++++++--------
2 files changed, 65 insertions(+), 12 deletions(-)
diff --git a/include/linux/preempt.h b/include/linux/preempt.h
index 2e689de7b29ab..ad2c35b6813fb 100644
--- a/include/linux/preempt.h
+++ b/include/linux/preempt.h
@@ -228,6 +228,15 @@ extern void preempt_count_sub(int val);
#define preempt_count_dec_and_test() __preempt_count_dec_and_test()
#endif
+#if defined(CONFIG_PREEMPTION) && (defined(CONFIG_DEBUG_PREEMPT) || \
+ defined(CONFIG_TRACE_PREEMPT_TOGGLE))
+void preempt_count_add_finish(int val);
+bool preempt_count_sub_prepare(int val);
+#else
+static __always_inline void preempt_count_add_finish(int val) { }
+static __always_inline bool preempt_count_sub_prepare(int val) { return true; }
+#endif
+
#define __preempt_count_inc() __preempt_count_add(1)
#define __preempt_count_dec() __preempt_count_sub(1)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 1754d9f95f2cf..562d54b978960 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -5978,7 +5978,7 @@ static inline void sched_tick_stop(int cpu) { }
* If the value passed in is equal to the current preempt count
* then we just disabled preemption. Start timing the latency.
*/
-static inline void preempt_latency_start(int val)
+static __always_inline void preempt_latency_start(int val)
{
if (preempt_count() == val) {
unsigned long ip = get_lock_parent_ip();
@@ -5989,6 +5989,28 @@ static inline void preempt_latency_start(int val)
}
}
+static __always_inline void preempt_count_add_debug_checks(void)
+{
+#ifdef CONFIG_DEBUG_PREEMPT
+ DEBUG_LOCKS_WARN_ON((preempt_count() & PREEMPT_MASK) >=
+ PREEMPT_MASK - 10);
+#endif
+}
+
+/**
+ * preempt_count_add_finish - Finish a raw preempt count addition
+ * @val: The value which was added to the preempt count
+ *
+ * Complete the debug checks and latency tracing after an entry path had to
+ * update the raw preempt count before instrumentable code could run.
+ */
+void preempt_count_add_finish(int val)
+{
+ preempt_count_add_debug_checks();
+ preempt_latency_start(val);
+}
+NOKPROBE_SYMBOL(preempt_count_add_finish);
+
void preempt_count_add(int val)
{
#ifdef CONFIG_DEBUG_PREEMPT
@@ -6004,13 +6026,8 @@ void preempt_count_add(int val)
return;
#endif
__preempt_count_add(val);
-#ifdef CONFIG_DEBUG_PREEMPT
- /*
- * Spinlock count overflowing soon?
- */
- DEBUG_LOCKS_WARN_ON((preempt_count() & PREEMPT_MASK) >=
- PREEMPT_MASK - 10);
-#endif
+ preempt_count_add_debug_checks();
+ /* Keep latency attribution in the preempt_count_add() stack frame. */
preempt_latency_start(val);
}
EXPORT_SYMBOL(preempt_count_add);
@@ -6020,13 +6037,13 @@ NOKPROBE_SYMBOL(preempt_count_add);
* If the value passed in equals to the current preempt count
* then we just enabled preemption. Stop timing the latency.
*/
-static inline void preempt_latency_stop(int val)
+static __always_inline void preempt_latency_stop(int val)
{
if (preempt_count() == val)
trace_preempt_on(CALLER_ADDR0, get_lock_parent_ip());
}
-void preempt_count_sub(int val)
+static __always_inline bool preempt_count_sub_debug_checks(int val)
{
#ifdef CONFIG_DEBUG_PREEMPT
/*
@@ -6036,15 +6053,42 @@ void preempt_count_sub(int val)
unsigned int pc = preempt_count();
if (DEBUG_LOCKS_WARN_ON(pc - uval > pc))
- return;
+ return false;
/*
* Is the spinlock portion underflowing?
*/
if (DEBUG_LOCKS_WARN_ON((val < PREEMPT_MASK) &&
!(preempt_count() & PREEMPT_MASK)))
- return;
+ return false;
#endif
+ return true;
+}
+
+/**
+ * preempt_count_sub_prepare - Prepare a raw preempt count subtraction
+ * @val: The value which will be subtracted from the preempt count
+ *
+ * Run debug checks and latency tracing before an entry path performs a raw
+ * preempt count subtraction after all instrumentable code has returned.
+ *
+ * Return: true when the caller should perform the subtraction.
+ */
+bool preempt_count_sub_prepare(int val)
+{
+ if (!preempt_count_sub_debug_checks(val))
+ return false;
+
+ preempt_latency_stop(val);
+ return true;
+}
+NOKPROBE_SYMBOL(preempt_count_sub_prepare);
+
+void preempt_count_sub(int val)
+{
+ if (!preempt_count_sub_debug_checks(val))
+ return;
+ /* Keep latency attribution in the preempt_count_sub() stack frame. */
preempt_latency_stop(val);
__preempt_count_sub(val);
}
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 02/12] lockdep: Use raw access for pinned hardirq state
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 01/12] sched: Split preempt-count bookkeeping from raw updates Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 03/12] entry: Add the pre-counted ordinary-hardirq entry/exit contract Karl Mehltretter
` (9 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
irqentry_nmi_enter() reads lockdep's hardirq state before __nmi_enter()
establishes NMI context. Arm64 does the same for its NMI-like EL1 debug
entry. Both paths run with interrupts disabled and cannot migrate.
lockdep_hardirqs_enabled() uses this_cpu_read(), whose preemption checks
are unnecessary for these callers. Add a raw accessor and use it in the
two entry paths and in lockdep_hardirqs_off(). Keep the existing accessor
for callers which are not already pinned.
Check the interrupt state before accessing per-CPU state in
lockdep_hardirqs_off(), independently of warning suppression.
On arm64, this change does not by itself make the call chain noinstr-safe:
__kern_my_cpu_offset() must also be forced inline. That prerequisite is
listed in the cover letter.
Assisted-by: LLM
---
arch/arm64/kernel/entry-common.c | 2 +-
include/linux/irqflags.h | 3 +++
kernel/entry/common.c | 2 +-
kernel/locking/lockdep.c | 6 ++++--
4 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/kernel/entry-common.c b/arch/arm64/kernel/entry-common.c
index 72c03ccea59fe..93056a766fb20 100644
--- a/arch/arm64/kernel/entry-common.c
+++ b/arch/arm64/kernel/entry-common.c
@@ -141,7 +141,7 @@ static noinstr irqentry_state_t arm64_enter_el1_dbg(struct pt_regs *regs)
{
irqentry_state_t state;
- state.lockdep = lockdep_hardirqs_enabled();
+ state.lockdep = lockdep_hardirqs_enabled_raw();
lockdep_hardirqs_off(CALLER_ADDR0);
ct_nmi_enter();
diff --git a/include/linux/irqflags.h b/include/linux/irqflags.h
index 57b074e0cfbbb..5bc179a88e513 100644
--- a/include/linux/irqflags.h
+++ b/include/linux/irqflags.h
@@ -52,6 +52,8 @@ extern void trace_hardirqs_off(void);
# define lockdep_hardirq_context() (raw_cpu_read(hardirq_context))
# define lockdep_softirq_context(p) ((p)->softirq_context)
# define lockdep_hardirqs_enabled() (this_cpu_read(hardirqs_enabled))
+/* The caller must already be pinned to the current CPU. */
+# define lockdep_hardirqs_enabled_raw() (raw_cpu_read(hardirqs_enabled))
# define lockdep_softirqs_enabled(p) ((p)->softirqs_enabled)
# define lockdep_hardirq_enter() \
do { \
@@ -113,6 +115,7 @@ do { \
# define lockdep_hardirq_context() 0
# define lockdep_softirq_context(p) 0
# define lockdep_hardirqs_enabled() 0
+# define lockdep_hardirqs_enabled_raw() 0
# define lockdep_softirqs_enabled(p) 0
# define lockdep_hardirq_enter() do { } while (0)
# define lockdep_hardirq_threaded() do { } while (0)
diff --git a/kernel/entry/common.c b/kernel/entry/common.c
index e3d381fd3d251..28399c41ebfb1 100644
--- a/kernel/entry/common.c
+++ b/kernel/entry/common.c
@@ -171,7 +171,7 @@ irqentry_state_t noinstr irqentry_nmi_enter(struct pt_regs *regs)
{
irqentry_state_t irq_state;
- irq_state.lockdep = lockdep_hardirqs_enabled();
+ irq_state.lockdep = lockdep_hardirqs_enabled_raw();
__nmi_enter();
lockdep_hardirqs_off(CALLER_ADDR0);
diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index c3dc84a7cef26..01dba8fb32a90 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -4543,10 +4543,12 @@ void noinstr lockdep_hardirqs_off(unsigned long ip)
* So we're supposed to get called after you mask local IRQs, but for
* some reason the hardware doesn't quite think you did a proper job.
*/
- if (DEBUG_LOCKS_WARN_ON(!irqs_disabled()))
+ if (unlikely(!irqs_disabled())) {
+ DEBUG_LOCKS_WARN_ON(1);
return;
+ }
- if (lockdep_hardirqs_enabled()) {
+ if (lockdep_hardirqs_enabled_raw()) {
struct irqtrace_events *trace = ¤t->irqtrace;
/*
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 03/12] entry: Add the pre-counted ordinary-hardirq entry/exit contract
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 01/12] sched: Split preempt-count bookkeeping from raw updates Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 02/12] lockdep: Use raw access for pinned hardirq state Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 04/12] entry: Skip inactive IRQ-return reschedule calls Karl Mehltretter
` (8 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
Architectures which identify ordinary hardirqs before generic entry need
to install HARDIRQ_OFFSET before context tracking and tracing callbacks
run. Lockdep hardirq state must agree with that count. On exit, both states
must remain installed until every traceable callback has returned.
Add irqentry_hardirq_enter() to install the raw count and lockdep state
before instrumentable entry work. irq_enter_rcu_preempt_counted() completes
the delayed preempt-count bookkeeping while preserving the existing timer
and hardirq accounting boundary.
Add a retained exit path which runs IRQ-exit processing and instrumentable
teardown preparation before returning to an immediate raw count and lockdep
teardown. This keeps function-graph return callbacks in hardirq context.
A return callback after the final softirq drain can raise regular or
forced-thread timer work while hardirq state still suppresses the usual
wakeup. Recheck ksoftirqd, ktimerd, then ksoftirqd after the last traceable
teardown step. Record wake requests made by the main exit path so the
closure does not repeat them.
Keep the existing entry and exit interfaces unchanged. Architectures opt
in only after distinguishing ordinary hardirqs from NMI-like entry. They
must not place instrumentable code between the retained-exit helper and
raw teardown.
Assisted-by: LLM
---
include/linux/hardirq.h | 24 +++++
include/linux/interrupt.h | 2 +-
include/linux/irq-entry-common.h | 31 ++++++-
include/linux/irqflags.h | 9 +-
kernel/entry/common.c | 30 +++++++
kernel/softirq.c | 148 ++++++++++++++++++++++++++-----
6 files changed, 215 insertions(+), 29 deletions(-)
diff --git a/include/linux/hardirq.h b/include/linux/hardirq.h
index 73b48dd9f1354..ad27a06c859a0 100644
--- a/include/linux/hardirq.h
+++ b/include/linux/hardirq.h
@@ -25,6 +25,30 @@ static __always_inline void rcu_irq_enter_check_tick(void)
__rcu_irq_enter_check_tick();
}
+void irq_enter_rcu_preempt_counted(void);
+bool irq_exit_rcu_preempt_counted_prepare(void);
+
+/*
+ * Prepare every potentially instrumentable part of hardirq teardown before
+ * the raw count and lockdep updates. The caller must invoke the matching
+ * finish helper without passing through instrumentable code.
+ */
+static __always_inline bool irqentry_hardirq_exit_prepare(void)
+{
+ bool do_sub;
+
+ do_sub = preempt_count_sub_prepare(HARDIRQ_OFFSET);
+ lockdep_hardirq_exit_prepare();
+ return do_sub;
+}
+
+static __always_inline void irqentry_hardirq_exit_finish(bool do_sub)
+{
+ if (do_sub)
+ __preempt_count_sub(HARDIRQ_OFFSET);
+ lockdep_hardirq_exit_finish();
+}
+
/*
* It is safe to do non-atomic ops on ->hardirq_context,
* because NMI handlers may not preempt and the ops are
diff --git a/include/linux/interrupt.h b/include/linux/interrupt.h
index 3bf969ad8fe07..fda5a29b176b4 100644
--- a/include/linux/interrupt.h
+++ b/include/linux/interrupt.h
@@ -636,7 +636,7 @@ DECLARE_PER_CPU(struct task_struct *, ktimerd);
DECLARE_PER_CPU(unsigned long, pending_timer_softirq);
void raise_ktimers_thread(unsigned int nr);
-static inline unsigned int local_timers_pending_force_th(void)
+static __always_inline unsigned int local_timers_pending_force_th(void)
{
return __this_cpu_read(pending_timer_softirq);
}
diff --git a/include/linux/irq-entry-common.h b/include/linux/irq-entry-common.h
index c74b5c87e10f6..3ee80e926a17a 100644
--- a/include/linux/irq-entry-common.h
+++ b/include/linux/irq-entry-common.h
@@ -76,8 +76,9 @@ static __always_inline bool arch_in_rcu_eqs(void) { return false; }
#endif
/**
- * enter_from_user_mode - Establish state when coming from user mode
+ * __enter_from_user_mode - Establish state when coming from user mode
* @regs: Pointer to currents pt_regs
+ * @hardirq: Whether to establish hardirq lockdep context before tracing
*
* Syscall/interrupt entry disables interrupts, but user mode is traced as
* interrupts enabled. Also with NO_HZ_FULL RCU might be idle.
@@ -95,10 +96,13 @@ static __always_inline bool arch_in_rcu_eqs(void) { return false; }
* done between establishing state and enabling interrupts. The caller must
* enable interrupts before invoking syscall_enter_from_user_mode_work().
*/
-static __always_inline void enter_from_user_mode(struct pt_regs *regs)
+static __always_inline void __enter_from_user_mode(struct pt_regs *regs,
+ bool hardirq)
{
arch_enter_from_user_mode(regs);
lockdep_hardirqs_off(CALLER_ADDR0);
+ if (hardirq)
+ lockdep_hardirq_enter();
CT_WARN_ON(__ct_state() != CT_STATE_USER);
user_exit_irqoff();
@@ -109,6 +113,11 @@ static __always_inline void enter_from_user_mode(struct pt_regs *regs)
instrumentation_end();
}
+static __always_inline void enter_from_user_mode(struct pt_regs *regs)
+{
+ __enter_from_user_mode(regs, false);
+}
+
/**
* arch_exit_to_user_mode_work - Architecture specific TIF work for exit
* to user mode.
@@ -341,6 +350,8 @@ typedef struct irqentry_state {
} irqentry_state_t;
#endif
+irqentry_state_t noinstr irqentry_hardirq_enter(struct pt_regs *regs);
+
/**
* irqentry_exit_cond_resched - Conditionally reschedule on return from interrupt
*
@@ -364,8 +375,9 @@ void dynamic_irqentry_exit_cond_resched(void);
#endif /* CONFIG_PREEMPT_DYNAMIC */
/**
- * irqentry_enter_from_kernel_mode - Establish state before invoking the irq handler
+ * __irqentry_enter_from_kernel_mode - Establish state before invoking an exception
* @regs: Pointer to currents pt_regs
+ * @hardirq: Whether to establish hardirq lockdep context before tracing
*
* Invoked from architecture specific entry code with interrupts disabled.
* Can only be called when the interrupt entry came from kernel mode. The
@@ -379,7 +391,8 @@ void dynamic_irqentry_exit_cond_resched(void);
*
* Returns: An opaque object that must be passed to irqentry_exit_to_kernel_mode().
*/
-static __always_inline irqentry_state_t irqentry_enter_from_kernel_mode(struct pt_regs *regs)
+static __always_inline irqentry_state_t
+__irqentry_enter_from_kernel_mode(struct pt_regs *regs, bool hardirq)
{
irqentry_state_t ret = {
.exit_rcu = false,
@@ -416,6 +429,8 @@ static __always_inline irqentry_state_t irqentry_enter_from_kernel_mode(struct p
* as in irqentry_enter_from_user_mode().
*/
lockdep_hardirqs_off(CALLER_ADDR0);
+ if (hardirq)
+ lockdep_hardirq_enter();
ct_irq_enter();
instrumentation_begin();
kmsan_unpoison_entry_regs(regs);
@@ -433,6 +448,8 @@ static __always_inline irqentry_state_t irqentry_enter_from_kernel_mode(struct p
* in having another one here.
*/
lockdep_hardirqs_off(CALLER_ADDR0);
+ if (hardirq)
+ lockdep_hardirq_enter();
instrumentation_begin();
kmsan_unpoison_entry_regs(regs);
rcu_irq_enter_check_tick();
@@ -442,6 +459,12 @@ static __always_inline irqentry_state_t irqentry_enter_from_kernel_mode(struct p
return ret;
}
+static __always_inline irqentry_state_t
+irqentry_enter_from_kernel_mode(struct pt_regs *regs)
+{
+ return __irqentry_enter_from_kernel_mode(regs, false);
+}
+
/**
* irqentry_exit_to_kernel_mode_preempt - Run preempt checks on return to kernel mode
* @regs: Pointer to current's pt_regs
diff --git a/include/linux/irqflags.h b/include/linux/irqflags.h
index 5bc179a88e513..e98313a8ad307 100644
--- a/include/linux/irqflags.h
+++ b/include/linux/irqflags.h
@@ -64,9 +64,14 @@ do { \
do { \
current->hardirq_threaded = 1; \
} while (0)
+# define lockdep_hardirq_exit_prepare() \
+ __this_cpu_preempt_check("add")
+# define lockdep_hardirq_exit_finish() \
+ raw_cpu_dec(hardirq_context)
# define lockdep_hardirq_exit() \
do { \
- __this_cpu_dec(hardirq_context); \
+ lockdep_hardirq_exit_prepare(); \
+ lockdep_hardirq_exit_finish(); \
} while (0)
# define lockdep_hrtimer_enter(__hrtimer) \
@@ -119,6 +124,8 @@ do { \
# define lockdep_softirqs_enabled(p) 0
# define lockdep_hardirq_enter() do { } while (0)
# define lockdep_hardirq_threaded() do { } while (0)
+# define lockdep_hardirq_exit_prepare() do { } while (0)
+# define lockdep_hardirq_exit_finish() do { } while (0)
# define lockdep_hardirq_exit() do { } while (0)
# define lockdep_softirq_enter() do { } while (0)
# define lockdep_softirq_exit() do { } while (0)
diff --git a/kernel/entry/common.c b/kernel/entry/common.c
index 28399c41ebfb1..36c43d3c76eae 100644
--- a/kernel/entry/common.c
+++ b/kernel/entry/common.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/futex.h>
+#include <linux/hardirq.h>
#include <linux/highmem.h>
#include <linux/irq-entry-common.h>
#include <linux/jump_label.h>
@@ -120,6 +121,35 @@ noinstr irqentry_state_t irqentry_enter(struct pt_regs *regs)
return irqentry_enter_from_kernel_mode(regs);
}
+/**
+ * irqentry_hardirq_enter - Establish state for ordinary hardirq entry
+ * @regs: Pointer to the interrupted register state
+ *
+ * Install the raw hardirq count before anything instrumentable can run. Then
+ * establish IRQ-disabled and hardirq lockdep state before context tracking or
+ * irqflag tracing can invoke callbacks. The caller must have distinguished an
+ * ordinary hardirq from NMI-like entry and must complete the delayed
+ * preempt-count bookkeeping with irq_enter_rcu_preempt_counted().
+ *
+ * Returns: An opaque object for the matching irqentry exit operation.
+ */
+noinstr irqentry_state_t irqentry_hardirq_enter(struct pt_regs *regs)
+{
+ __preempt_count_add(HARDIRQ_OFFSET);
+
+ if (user_mode(regs)) {
+ irqentry_state_t ret = {
+ .exit_rcu = false,
+ };
+
+ __enter_from_user_mode(regs, true);
+ rseq_note_user_irq_entry();
+ return ret;
+ }
+
+ return __irqentry_enter_from_kernel_mode(regs, true);
+}
+
/**
* arch_irqentry_exit_need_resched - Architecture specific need resched function
*
diff --git a/kernel/softirq.c b/kernel/softirq.c
index be49e6469f3ad..63427d29cb6d1 100644
--- a/kernel/softirq.c
+++ b/kernel/softirq.c
@@ -14,6 +14,7 @@
#include <linux/kernel_stat.h>
#include <linux/interrupt.h>
#include <linux/init.h>
+#include <linux/instrumentation.h>
#include <linux/local_lock.h>
#include <linux/mm.h>
#include <linux/notifier.h>
@@ -353,15 +354,18 @@ static inline void ksoftirqd_run_end(void)
static inline bool softirq_handle_begin(void) { return false; }
static inline void softirq_handle_end(bool from_irq_exit) { }
-static inline bool should_wake_ksoftirqd(void)
+static __always_inline bool should_wake_ksoftirqd(void)
{
return !this_cpu_read(softirq_ctrl.cnt);
}
-static inline void invoke_softirq(void)
+static inline bool invoke_softirq(void)
{
- if (should_wake_ksoftirqd())
- wakeup_softirqd();
+ if (!should_wake_ksoftirqd())
+ return false;
+
+ wakeup_softirqd();
+ return true;
}
#define SCHED_SOFTIRQ_MASK BIT(SCHED_SOFTIRQ)
@@ -525,12 +529,12 @@ static inline void ksoftirqd_run_end(void)
local_irq_enable();
}
-static inline bool should_wake_ksoftirqd(void)
+static __always_inline bool should_wake_ksoftirqd(void)
{
return true;
}
-static inline void invoke_softirq(void)
+static inline bool invoke_softirq(void)
{
if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
@@ -548,9 +552,11 @@ static inline void invoke_softirq(void)
*/
do_softirq_own_stack();
#endif
- } else {
- wakeup_softirqd();
+ return false;
}
+
+ wakeup_softirqd();
+ return true;
}
asmlinkage __visible void do_softirq(void)
@@ -703,13 +709,8 @@ asmlinkage __visible void __softirq_entry __do_softirq(void)
handle_softirqs(false);
}
-/**
- * irq_enter_rcu - Enter an interrupt context with RCU watching
- */
-void irq_enter_rcu(void)
+static __always_inline void irq_enter_rcu_common(void)
{
- __irq_enter_raw();
-
/*
* If this is a nested interrupt that hits the exit_to_user_mode_loop
* where it has enabled interrupts but before it has hit schedule() we
@@ -724,6 +725,28 @@ void irq_enter_rcu(void)
account_hardirq_enter(current);
}
+/**
+ * irq_enter_rcu_preempt_counted - Enter hardirq with its count already set
+ *
+ * Complete an early raw hardirq preempt-count update, then establish the
+ * remaining timer and accounting state. The matching lockdep hardirq state
+ * was established by irqentry_hardirq_enter() before entry tracing.
+ */
+void irq_enter_rcu_preempt_counted(void)
+{
+ preempt_count_add_finish(HARDIRQ_OFFSET);
+ irq_enter_rcu_common();
+}
+
+/**
+ * irq_enter_rcu - Enter an interrupt context with RCU watching
+ */
+void irq_enter_rcu(void)
+{
+ __irq_enter_raw();
+ irq_enter_rcu_common();
+}
+
/**
* irq_enter - Enter an interrupt context including RCU update
*/
@@ -764,8 +787,56 @@ static inline void wake_timersd(void) { }
#endif
-static inline void __irq_exit_rcu(void)
+static __always_inline bool irq_exit_wake_ksoftirqd(void)
{
+ if (irq_count() != HARDIRQ_OFFSET || !local_softirq_pending() ||
+ !should_wake_ksoftirqd())
+ return false;
+
+ wakeup_softirqd();
+ return true;
+}
+
+static __always_inline bool irq_exit_wake_timersd(void)
+{
+ if (!IS_ENABLED(CONFIG_IRQ_FORCED_THREADING) || !force_irqthreads() ||
+ !local_timers_pending_force_th() ||
+ hardirq_count() != HARDIRQ_OFFSET)
+ return false;
+
+ wake_timersd();
+ return true;
+}
+
+enum irq_exit_wake_flags {
+ IRQ_EXIT_WAKE_KSOFTIRQD = BIT(0),
+ IRQ_EXIT_WAKE_KTIMERD = BIT(1),
+};
+
+static __always_inline void
+irq_exit_wake_late_softirqs(unsigned int wake_flags)
+{
+ bool ksoftirqd_wake_requested = wake_flags & IRQ_EXIT_WAKE_KSOFTIRQD;
+ bool ktimerd_wake_requested = wake_flags & IRQ_EXIT_WAKE_KTIMERD;
+
+ /*
+ * A wakeup can be traced, and its callback can raise work for the other
+ * worker. Check ksoftirqd, then ktimerd, then ksoftirqd again. Skip a
+ * worker whose direct wake request was recorded above: it cannot run on
+ * this CPU before hardirq teardown, so later work remains visible to it.
+ */
+ if (!ksoftirqd_wake_requested)
+ ksoftirqd_wake_requested = irq_exit_wake_ksoftirqd();
+ if (!ktimerd_wake_requested)
+ ktimerd_wake_requested = irq_exit_wake_timersd();
+ if (!ksoftirqd_wake_requested && ktimerd_wake_requested)
+ irq_exit_wake_ksoftirqd();
+}
+
+static __always_inline unsigned int __irq_exit_rcu(bool keep_hardirq_count)
+{
+ unsigned int wake_flags = 0;
+
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
@@ -783,20 +854,51 @@ static inline void __irq_exit_rcu(void)
* before enabling interrupts to run softirq.
*/
hrtimer_rearm_deferred();
- invoke_softirq();
+ if (invoke_softirq() && keep_hardirq_count)
+ wake_flags |= IRQ_EXIT_WAKE_KSOFTIRQD;
}
/*
* Wake the timer thread even if the interrupt hit a softirq or a
* section with BHs disabled. Only the outermost interrupt can wake it.
*/
- if (IS_ENABLED(CONFIG_IRQ_FORCED_THREADING) && force_irqthreads() &&
- local_timers_pending_force_th() &&
- hardirq_count() == HARDIRQ_OFFSET)
- wake_timersd();
+ if (irq_exit_wake_timersd() && keep_hardirq_count)
+ wake_flags |= IRQ_EXIT_WAKE_KTIMERD;
tick_irq_exit();
- preempt_count_sub(HARDIRQ_OFFSET);
+ if (!keep_hardirq_count)
+ preempt_count_sub(HARDIRQ_OFFSET);
+
+ return wake_flags;
+}
+
+/**
+ * irq_exit_rcu_preempt_counted_prepare - Process IRQ exit and prepare teardown
+ *
+ * The caller established hardirq state before instrumentable IRQ entry. Keep
+ * that state through every traceable part of IRQ exit, then prepare its raw
+ * teardown. The caller must apply the raw teardown immediately after return.
+ *
+ * Return callbacks can raise regular or forced-thread timer softirqs after
+ * __irq_exit_rcu() performed its final drain and worker wakeups. Recheck both
+ * queues after all traceable preparation. Worker wakeups are traceable too,
+ * so order the rechecks such that a callback which raises work for the other
+ * worker cannot leave it asleep. Do not repeat a direct worker wake
+ * recorded by __irq_exit_rcu(); the worker cannot run on this CPU before
+ * hardirq teardown.
+ */
+bool noinstr irq_exit_rcu_preempt_counted_prepare(void)
+{
+ unsigned int wake_flags;
+ bool do_sub;
+
+ instrumentation_begin();
+ wake_flags = __irq_exit_rcu(true);
+ do_sub = irqentry_hardirq_exit_prepare();
+ irq_exit_wake_late_softirqs(wake_flags);
+ instrumentation_end();
+
+ return do_sub;
}
/**
@@ -806,7 +908,7 @@ static inline void __irq_exit_rcu(void)
*/
void irq_exit_rcu(void)
{
- __irq_exit_rcu();
+ __irq_exit_rcu(false);
/* must be last! */
lockdep_hardirq_exit();
}
@@ -818,7 +920,7 @@ void irq_exit_rcu(void)
*/
void irq_exit(void)
{
- __irq_exit_rcu();
+ __irq_exit_rcu(false);
ct_irq_exit();
/* must be last! */
lockdep_hardirq_exit();
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 04/12] entry: Skip inactive IRQ-return reschedule calls
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (2 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 03/12] entry: Add the pre-counted ordinary-hardirq entry/exit contract Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 05/12] context_tracking: Split IRQ-exit preparation from the EQS transition Karl Mehltretter
` (7 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
irqentry_exit_to_kernel_mode_preempt() calls the dynamic reschedule helper
when preemption is enabled even if the preempt count is nonzero. The helper
returns immediately, but its call and return remain traceable after hardirq
context has been removed.
Check the preempt count in the inline caller and avoid the inactive call.
Keep the existing check in the helper for its other callers. This does not
change whether IRQ return schedules.
Assisted-by: LLM
---
include/linux/irq-entry-common.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/include/linux/irq-entry-common.h b/include/linux/irq-entry-common.h
index 3ee80e926a17a..24c2b9fdff4f4 100644
--- a/include/linux/irq-entry-common.h
+++ b/include/linux/irq-entry-common.h
@@ -487,7 +487,7 @@ static inline void irqentry_exit_to_kernel_mode_preempt(struct pt_regs *regs,
if (regs_irqs_disabled(regs) || state.exit_rcu)
return;
- if (IS_ENABLED(CONFIG_PREEMPTION))
+ if (IS_ENABLED(CONFIG_PREEMPTION) && !preempt_count())
irqentry_exit_cond_resched();
}
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 05/12] context_tracking: Split IRQ-exit preparation from the EQS transition
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (3 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 04/12] entry: Skip inactive IRQ-return reschedule calls Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 06/12] lockdep: Prepare IRQ-on state from a hardirq return tail Karl Mehltretter
` (6 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
ct_irq_exit() combines traceable nesting and tracepoint work with the final
transition to an RCU extended quiescent state. A pre-counted ordinary
hardirq return needs to run that preparation before its synthetic hardirq
classification is removed, but it must enter the quiescent state only after
the raw count and lockdep teardown.
Split IRQ exit into prepare and finish phases. The prepare phase validates
and updates interrupt nesting, then emits the tracepoint while RCU is
watching. The finish phase performs the raw quiescent-state transition and
RCU Tasks bookkeeping.
Keep ct_irq_exit() and ct_nmi_exit() as adjacent prepare and finish
wrappers. Existing IRQ and NMI callers retain their ordering and behavior.
Assisted-by: LLM
---
include/linux/context_tracking_irq.h | 32 +++++++++++-
kernel/context_tracking.c | 78 ++++++++++++++++++++++------
2 files changed, 93 insertions(+), 17 deletions(-)
diff --git a/include/linux/context_tracking_irq.h b/include/linux/context_tracking_irq.h
index 197916ee91a4b..b038cd1249148 100644
--- a/include/linux/context_tracking_irq.h
+++ b/include/linux/context_tracking_irq.h
@@ -2,8 +2,23 @@
#ifndef _LINUX_CONTEXT_TRACKING_IRQ_H
#define _LINUX_CONTEXT_TRACKING_IRQ_H
+#include <linux/types.h>
+
+/**
+ * struct ct_irq_exit_state - Opaque context-tracking IRQ-exit state
+ * @exit_rcu: The interrupted context was in an RCU extended quiescent state
+ *
+ * This object is filled in by ct_irq_exit_prepare() and must be passed to the
+ * matching ct_irq_exit_finish(). Callers must consider its members private.
+ */
+struct ct_irq_exit_state {
+ bool exit_rcu;
+};
+
#ifdef CONFIG_CONTEXT_TRACKING_IDLE
void ct_irq_enter(void);
+struct ct_irq_exit_state ct_irq_exit_prepare(void);
+void ct_irq_exit_finish(struct ct_irq_exit_state state);
void ct_irq_exit(void);
void ct_irq_enter_irqson(void);
void ct_irq_exit_irqson(void);
@@ -11,7 +26,22 @@ void ct_nmi_enter(void);
void ct_nmi_exit(void);
#else
static __always_inline void ct_irq_enter(void) { }
-static __always_inline void ct_irq_exit(void) { }
+static __always_inline struct ct_irq_exit_state ct_irq_exit_prepare(void)
+{
+ return (struct ct_irq_exit_state) { };
+}
+
+static __always_inline void ct_irq_exit_finish(struct ct_irq_exit_state state)
+{
+ (void)state;
+}
+
+static __always_inline void ct_irq_exit(void)
+{
+ struct ct_irq_exit_state state = ct_irq_exit_prepare();
+
+ ct_irq_exit_finish(state);
+}
static inline void ct_irq_enter_irqson(void) { }
static inline void ct_irq_exit_irqson(void) { }
static __always_inline void ct_nmi_enter(void) { }
diff --git a/kernel/context_tracking.c b/kernel/context_tracking.c
index a743e7ffa6c00..80ed8ec606c6a 100644
--- a/kernel/context_tracking.c
+++ b/kernel/context_tracking.c
@@ -169,20 +169,10 @@ static void noinstr ct_kernel_enter(bool user, int offset)
instrumentation_end();
}
-/**
- * ct_nmi_exit - inform RCU of exit from NMI context
- *
- * If we are returning from the outermost NMI handler that interrupted an
- * RCU-idle period, update ct->state and ct->nmi_nesting
- * to let the RCU grace-period handling know that the CPU is back to
- * being RCU-idle.
- *
- * If you add or remove a call to ct_nmi_exit(), be sure to test
- * with CONFIG_RCU_EQS_DEBUG=y.
- */
-void noinstr ct_nmi_exit(void)
+static struct ct_irq_exit_state noinstr __ct_nmi_exit_prepare(void)
{
struct context_tracking *ct = this_cpu_ptr(&context_tracking);
+ struct ct_irq_exit_state state = { };
instrumentation_begin();
/*
@@ -203,7 +193,7 @@ void noinstr ct_nmi_exit(void)
WRITE_ONCE(ct->nmi_nesting, /* No store tearing. */
ct_nmi_nesting() - 2);
instrumentation_end();
- return;
+ return state;
}
/* This NMI interrupted an RCU-idle CPU, restore RCU-idleness. */
@@ -213,6 +203,15 @@ void noinstr ct_nmi_exit(void)
// instrumentation for the noinstr ct_kernel_exit_state()
instrument_atomic_write(&ct->state, sizeof(ct->state));
instrumentation_end();
+ state.exit_rcu = true;
+
+ return state;
+}
+
+static void noinstr __ct_nmi_exit_finish(struct ct_irq_exit_state state)
+{
+ if (!state.exit_rcu)
+ return;
// RCU is watching here ...
ct_kernel_exit_state(CT_RCU_WATCHING);
@@ -222,6 +221,24 @@ void noinstr ct_nmi_exit(void)
rcu_task_exit();
}
+/**
+ * ct_nmi_exit - inform RCU of exit from NMI context
+ *
+ * If we are returning from the outermost NMI handler that interrupted an
+ * RCU-idle period, update ct->state and ct->nmi_nesting
+ * to let the RCU grace-period handling know that the CPU is back to
+ * being RCU-idle.
+ *
+ * If you add or remove a call to ct_nmi_exit(), be sure to test
+ * with CONFIG_RCU_EQS_DEBUG=y.
+ */
+void noinstr ct_nmi_exit(void)
+{
+ struct ct_irq_exit_state state = __ct_nmi_exit_prepare();
+
+ __ct_nmi_exit_finish(state);
+}
+
/**
* ct_nmi_enter - inform RCU of entry to NMI context
*
@@ -348,7 +365,35 @@ noinstr void ct_irq_enter(void)
}
/**
- * ct_irq_exit - inform RCU that current CPU is exiting irq towards idle
+ * ct_irq_exit_prepare - prepare to exit an IRQ towards idle
+ *
+ * Perform the traceable part of an IRQ exit and return an opaque object for
+ * ct_irq_exit_finish(). RCU remains watching until the matching finish call.
+ * The caller must keep interrupts disabled, must not schedule or migrate, and
+ * must invoke ct_irq_exit_finish() exactly once.
+ *
+ * Return: An opaque object to pass to ct_irq_exit_finish().
+ */
+struct ct_irq_exit_state noinstr ct_irq_exit_prepare(void)
+{
+ lockdep_assert_irqs_disabled();
+ return __ct_nmi_exit_prepare();
+}
+
+/**
+ * ct_irq_exit_finish - finish exiting an IRQ towards idle
+ * @state: Return value from the matching ct_irq_exit_prepare()
+ *
+ * Complete a pending transition to an RCU extended quiescent state. Nothing
+ * traceable may run between this function and the low-level return path.
+ */
+void noinstr ct_irq_exit_finish(struct ct_irq_exit_state state)
+{
+ __ct_nmi_exit_finish(state);
+}
+
+/**
+ * ct_irq_exit - inform RCU that current CPU is exiting IRQ towards idle
*
* Exit from an interrupt handler, which might possibly result in entering
* idle mode, in other words, leaving the mode in which read-side critical
@@ -368,8 +413,9 @@ noinstr void ct_irq_enter(void)
*/
noinstr void ct_irq_exit(void)
{
- lockdep_assert_irqs_disabled();
- ct_nmi_exit();
+ struct ct_irq_exit_state state = ct_irq_exit_prepare();
+
+ ct_irq_exit_finish(state);
}
/*
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 06/12] lockdep: Prepare IRQ-on state from a hardirq return tail
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (4 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 05/12] context_tracking: Split IRQ-exit preparation from the EQS transition Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 07/12] entry: Carry ordinary-hardirq provenance through kernel return Karl Mehltretter
` (5 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
lockdep_hardirqs_on_prepare() rejects any live hardirq context. An ordinary
hardirq return tail needs to retain one synthetic hardirq nesting level
through traceable IRQ-on preparation so callbacks remain classified as
hardirq work.
Factor the common preparation and add a return-tail variant which accepts
exactly one nesting level. Keep the ordinary helper restricted to zero.
Neither helper changes IRQ or hardirq state. The caller must remove the
tail context before lockdep_hardirqs_on().
Assisted-by: LLM
---
include/linux/irqflags.h | 2 ++
kernel/locking/lockdep.c | 42 ++++++++++++++++++++++++++++------------
2 files changed, 32 insertions(+), 12 deletions(-)
diff --git a/include/linux/irqflags.h b/include/linux/irqflags.h
index e98313a8ad307..c7601a173e55b 100644
--- a/include/linux/irqflags.h
+++ b/include/linux/irqflags.h
@@ -25,6 +25,7 @@ struct task_struct;
extern void lockdep_softirqs_on(unsigned long ip);
extern void lockdep_softirqs_off(unsigned long ip);
extern void lockdep_hardirqs_on_prepare(void);
+void lockdep_hardirqs_on_prepare_irqtail(void);
extern void lockdep_hardirqs_on(unsigned long ip);
extern void lockdep_hardirqs_off(unsigned long ip);
extern void lockdep_cleanup_dead_cpu(unsigned int cpu,
@@ -33,6 +34,7 @@ struct task_struct;
static inline void lockdep_softirqs_on(unsigned long ip) { }
static inline void lockdep_softirqs_off(unsigned long ip) { }
static inline void lockdep_hardirqs_on_prepare(void) { }
+static inline void lockdep_hardirqs_on_prepare_irqtail(void) { }
static inline void lockdep_hardirqs_on(unsigned long ip) { }
static inline void lockdep_hardirqs_off(unsigned long ip) { }
static inline void lockdep_cleanup_dead_cpu(unsigned int cpu,
diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 01dba8fb32a90..457bef3ea1689 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -4395,15 +4395,7 @@ static void __trace_hardirqs_on_caller(void)
mark_held_locks(curr, LOCK_ENABLED_SOFTIRQ);
}
-/**
- * lockdep_hardirqs_on_prepare - Prepare for enabling interrupts
- *
- * Invoked before a possible transition to RCU idle from exit to user or
- * guest mode. This ensures that all RCU operations are done before RCU
- * stops watching. After the RCU transition lockdep_hardirqs_on() has to be
- * invoked to set the final state.
- */
-void lockdep_hardirqs_on_prepare(void)
+static void __lockdep_hardirqs_on_prepare(unsigned int expected_context)
{
if (unlikely(!debug_locks))
return;
@@ -4442,10 +4434,11 @@ void lockdep_hardirqs_on_prepare(void)
return;
/*
- * Can't allow enabling interrupts while in an interrupt handler,
- * that's general bad form and such. Recursion, limited stack etc..
+ * Ordinary IRQ enable requires no hardirq context. The interrupt return
+ * tail variant permits exactly the synthetic context it is about to
+ * remove.
*/
- if (DEBUG_LOCKS_WARN_ON(lockdep_hardirq_context()))
+ if (DEBUG_LOCKS_WARN_ON(lockdep_hardirq_context() != expected_context))
return;
current->hardirq_chain_key = current->curr_chain_key;
@@ -4454,8 +4447,33 @@ void lockdep_hardirqs_on_prepare(void)
__trace_hardirqs_on_caller();
lockdep_recursion_finish();
}
+
+/**
+ * lockdep_hardirqs_on_prepare - Prepare for enabling interrupts
+ *
+ * Invoked before a possible transition to RCU idle from exit to user or
+ * guest mode. This ensures that all RCU operations are done before RCU
+ * stops watching. After the RCU transition lockdep_hardirqs_on() has to be
+ * invoked to set the final state.
+ */
+void lockdep_hardirqs_on_prepare(void)
+{
+ __lockdep_hardirqs_on_prepare(0);
+}
EXPORT_SYMBOL_GPL(lockdep_hardirqs_on_prepare);
+/**
+ * lockdep_hardirqs_on_prepare_irqtail - Prepare an interrupt return tail
+ *
+ * Like lockdep_hardirqs_on_prepare(), but invoked while the single synthetic
+ * lockdep hardirq context covering an interrupt return tail is still active.
+ * The caller must remove that context before lockdep_hardirqs_on().
+ */
+void lockdep_hardirqs_on_prepare_irqtail(void)
+{
+ __lockdep_hardirqs_on_prepare(1);
+}
+
void noinstr lockdep_hardirqs_on(unsigned long ip)
{
struct irqtrace_events *trace = ¤t->irqtrace;
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 07/12] entry: Carry ordinary-hardirq provenance through kernel return
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (5 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 06/12] lockdep: Prepare IRQ-on state from a hardirq return tail Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 08/12] entry: Classify ordinary hardirq return-tail work as hardirq context Karl Mehltretter
` (4 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
The entry state can cross hardirq teardown and a possible reschedule before
the architecture completes its return tail. At that point preempt_count()
no longer identifies whether the original entry was an ordinary hardirq, a
synchronous exception, or an NMI-like event.
Record ordinary-hardirq provenance in irqentry_state_t and initialize it
in every entry path. A later return tail can restore hardirq classification
only for entries which established the pre-counted hardirq lifecycle.
Assisted-by: LLM
---
include/linux/irq-entry-common.h | 4 ++++
kernel/entry/common.c | 6 +++++-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/include/linux/irq-entry-common.h b/include/linux/irq-entry-common.h
index 24c2b9fdff4f4..2ed637cf66b0c 100644
--- a/include/linux/irq-entry-common.h
+++ b/include/linux/irq-entry-common.h
@@ -333,6 +333,8 @@ static __always_inline void irqentry_exit_to_user_mode(struct pt_regs *regs)
* exit path has to invoke ct_irq_exit().
* @lockdep: Used exclusively in the irqentry_nmi_*() calls; ensures that
* lockdep state is restored correctly on exit from nmi.
+ * @hardirq: Records that irqentry_hardirq_enter() established this state for
+ * an ordinary hard interrupt.
*
* This opaque object is filled in by the irqentry_*_enter() functions and
* must be passed back into the corresponding irqentry_*_exit() functions
@@ -347,6 +349,7 @@ typedef struct irqentry_state {
bool exit_rcu;
bool lockdep;
};
+ bool hardirq;
} irqentry_state_t;
#endif
@@ -396,6 +399,7 @@ __irqentry_enter_from_kernel_mode(struct pt_regs *regs, bool hardirq)
{
irqentry_state_t ret = {
.exit_rcu = false,
+ .hardirq = hardirq,
};
/*
diff --git a/kernel/entry/common.c b/kernel/entry/common.c
index 36c43d3c76eae..ba5959a84bc4a 100644
--- a/kernel/entry/common.c
+++ b/kernel/entry/common.c
@@ -112,6 +112,7 @@ noinstr irqentry_state_t irqentry_enter(struct pt_regs *regs)
if (user_mode(regs)) {
irqentry_state_t ret = {
.exit_rcu = false,
+ .hardirq = false,
};
irqentry_enter_from_user_mode(regs);
@@ -140,6 +141,7 @@ noinstr irqentry_state_t irqentry_hardirq_enter(struct pt_regs *regs)
if (user_mode(regs)) {
irqentry_state_t ret = {
.exit_rcu = false,
+ .hardirq = true,
};
__enter_from_user_mode(regs, true);
@@ -199,7 +201,9 @@ noinstr void irqentry_exit(struct pt_regs *regs, irqentry_state_t state)
irqentry_state_t noinstr irqentry_nmi_enter(struct pt_regs *regs)
{
- irqentry_state_t irq_state;
+ irqentry_state_t irq_state = {
+ .hardirq = false,
+ };
irq_state.lockdep = lockdep_hardirqs_enabled_raw();
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 08/12] entry: Classify ordinary hardirq return-tail work as hardirq context
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (6 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 07/12] entry: Carry ordinary-hardirq provenance through kernel return Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 09/12] arm64: Use the pre-counted hardirq lifecycle Karl Mehltretter
` (3 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
An ordinary hardirq removes its original count and lockdep state before the
return path may preempt. Traceable return-tail callbacks then appear to run
in the interrupted task or softirq context and can collide with recursion
state owned by that context.
Use the saved ordinary-hardirq provenance to restore only preempt-count and
lockdep classification after the preemption decision. Keep it across
traceable return-tail work, deferred hrtimer rearming, IRQ-on preparation,
and context-tracking preparation.
Before removing the raw tail state, prepare the count and lockdep teardown
and close late regular or forced-thread softirq work. Complete any pending
RCU quiescent-state transition only after that raw teardown.
Do not repeat interrupt accounting, RCU or tick entry. Existing non-hardirq
return paths remain unchanged.
Assisted-by: LLM
---
include/linux/hardirq.h | 5 +++
include/linux/irq-entry-common.h | 59 ++++++++++++++++++++++++++++++--
kernel/softirq.c | 18 ++++++++++
3 files changed, 79 insertions(+), 3 deletions(-)
diff --git a/include/linux/hardirq.h b/include/linux/hardirq.h
index ad27a06c859a0..a120e3756d076 100644
--- a/include/linux/hardirq.h
+++ b/include/linux/hardirq.h
@@ -27,6 +27,7 @@ static __always_inline void rcu_irq_enter_check_tick(void)
void irq_enter_rcu_preempt_counted(void);
bool irq_exit_rcu_preempt_counted_prepare(void);
+void noinstr irqentry_hardirq_tail_exit(void);
/*
* Prepare every potentially instrumentable part of hardirq teardown before
@@ -37,6 +38,10 @@ static __always_inline bool irqentry_hardirq_exit_prepare(void)
{
bool do_sub;
+ /*
+ * Preempt-on tracing can raise deferred work. The caller must perform
+ * the final worker closure before removing the raw hardirq count.
+ */
do_sub = preempt_count_sub_prepare(HARDIRQ_OFFSET);
lockdep_hardirq_exit_prepare();
return do_sub;
diff --git a/include/linux/irq-entry-common.h b/include/linux/irq-entry-common.h
index 2ed637cf66b0c..c6bdd5a2522e8 100644
--- a/include/linux/irq-entry-common.h
+++ b/include/linux/irq-entry-common.h
@@ -355,6 +355,28 @@ typedef struct irqentry_state {
irqentry_state_t noinstr irqentry_hardirq_enter(struct pt_regs *regs);
+/**
+ * irqentry_hardirq_tail_enter - Restore ordinary-hardirq return-tail state
+ * @state: Return value from the matching irqentry_hardirq_enter()
+ *
+ * Restore only the hardirq classification which traceable return-tail work
+ * needs. Accounting, RCU and tick handling belong to the completed interrupt
+ * and must not be repeated here.
+ */
+static __always_inline void
+irqentry_hardirq_tail_enter(irqentry_state_t state)
+{
+ if (!state.hardirq)
+ return;
+
+ __preempt_count_add(HARDIRQ_OFFSET);
+ lockdep_hardirq_enter();
+
+ instrumentation_begin();
+ preempt_count_add_finish(HARDIRQ_OFFSET);
+ instrumentation_end();
+}
+
/**
* irqentry_exit_cond_resched - Conditionally reschedule on return from interrupt
*
@@ -505,7 +527,9 @@ static inline void irqentry_exit_to_kernel_mode_preempt(struct pt_regs *regs,
*
* The rearm on return to user mode needs no such annotation: no locks are
* held there, so it uses the same lock chains as the rearm in hardirq
- * context.
+ * context. Ordinary-hardirq return paths do not use this wrapper: their saved
+ * provenance restores both the hardirq count and lockdep nesting across the
+ * whole traceable tail and they call hrtimer_rearm_deferred() directly.
*/
static __always_inline void irqentry_hrtimer_rearm_deferred(void)
{
@@ -520,7 +544,9 @@ static __always_inline void irqentry_hrtimer_rearm_deferred(void)
* @state: Return value from matching call to irqentry_enter_from_kernel_mode()
*
* This is to be invoked after irqentry_exit_to_kernel_mode_preempt() and before
- * actually returning to the interrupted context.
+ * actually returning to the interrupted context. If @state records ordinary
+ * hardirq provenance, the caller must first invoke
+ * irqentry_hardirq_tail_enter() exactly once.
*
* There are no requirements for the CPU state other than being able to complete
* the tracing, lockdep and RCU state transitions. After this function returns
@@ -529,6 +555,31 @@ static __always_inline void irqentry_hrtimer_rearm_deferred(void)
static __always_inline void
irqentry_exit_to_kernel_mode_after_preempt(struct pt_regs *regs, irqentry_state_t state)
{
+ struct ct_irq_exit_state ct_state = { };
+
+ if (state.hardirq) {
+ if (!regs_irqs_disabled(regs)) {
+ instrumentation_begin();
+ hrtimer_rearm_deferred();
+ /* Tell the tracer that the return will enable IRQs. */
+ trace_hardirqs_on_prepare();
+ lockdep_hardirqs_on_prepare_irqtail();
+ instrumentation_end();
+ }
+
+ if (state.exit_rcu)
+ ct_state = ct_irq_exit_prepare();
+
+ irqentry_hardirq_tail_exit();
+
+ if (state.exit_rcu)
+ ct_irq_exit_finish(ct_state);
+
+ if (!regs_irqs_disabled(regs))
+ lockdep_hardirqs_on(CALLER_ADDR0);
+ return;
+ }
+
if (!regs_irqs_disabled(regs)) {
/*
* If RCU was not watching on entry this needs to be done
@@ -570,7 +621,8 @@ irqentry_exit_to_kernel_mode_after_preempt(struct pt_regs *regs, irqentry_state_
*
* This is the counterpart of irqentry_enter_from_kernel_mode() and combines
* the calls to irqentry_exit_to_kernel_mode_preempt() and
- * irqentry_exit_to_kernel_mode_after_preempt().
+ * irqentry_exit_to_kernel_mode_after_preempt(). It also establishes the
+ * conditional hardirq return tail between those calls.
*
* The requirement for the CPU state is that it can schedule. After the function
* returns the tracing, lockdep and RCU state transitions are completed and the
@@ -585,6 +637,7 @@ static __always_inline void irqentry_exit_to_kernel_mode(struct pt_regs *regs,
irqentry_exit_to_kernel_mode_preempt(regs, state);
instrumentation_end();
+ irqentry_hardirq_tail_enter(state);
irqentry_exit_to_kernel_mode_after_preempt(regs, state);
}
diff --git a/kernel/softirq.c b/kernel/softirq.c
index 63427d29cb6d1..80d12deeb694a 100644
--- a/kernel/softirq.c
+++ b/kernel/softirq.c
@@ -833,6 +833,24 @@ irq_exit_wake_late_softirqs(unsigned int wake_flags)
irq_exit_wake_ksoftirqd();
}
+/**
+ * irqentry_hardirq_tail_exit - Close work and remove return-tail state
+ *
+ * Run every instrumentable teardown operation, including the final worker
+ * closure, before removing the raw hardirq count and lockdep nesting.
+ */
+void noinstr irqentry_hardirq_tail_exit(void)
+{
+ bool do_sub;
+
+ instrumentation_begin();
+ do_sub = irqentry_hardirq_exit_prepare();
+ irq_exit_wake_late_softirqs(0);
+ instrumentation_end();
+
+ irqentry_hardirq_exit_finish(do_sub);
+}
+
static __always_inline unsigned int __irq_exit_rcu(bool keep_hardirq_count)
{
unsigned int wake_flags = 0;
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 09/12] arm64: Use the pre-counted hardirq lifecycle
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (7 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 08/12] entry: Classify ordinary hardirq return-tail work as hardirq context Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 10/12] docs: entry: Describe " Karl Mehltretter
` (2 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
Ordinary EL0 and EL1 interrupt paths establish generic entry state before
irq_enter_rcu() adds HARDIRQ_OFFSET and lockdep nesting. Callbacks in that
early window are therefore not classified as hardirq work. Their exit paths
also drop the classification before function-graph return callbacks and the
kernel return tail have completed.
After the EL1 pseudo-NMI decision, enter ordinary interrupts through
irqentry_hardirq_enter(). Complete delayed timer and accounting work with
irq_enter_rcu_preempt_counted(). Use the retained exit preparation followed
immediately by raw count and lockdep teardown.
For kernel returns, restore return-tail classification after masking DAIF
and before MTE exit tracing. Keep the pseudo-NMI lifecycle unchanged. Force
arm64 preempt_count() inline so noinstr callers cannot be outlined into
instrumentable text.
The noinstr path also depends on the raw preempt-count and
__kern_my_cpu_offset() inlining changes carried as patches 1 and 2. Keep
them separate because they are externally authored fixes.
Link: https://lore.kernel.org/all/c3a90368dd6c096af9d918e036a27728696fb8a4.1786603168.git.hongyan.xia@transsion.com/
Link: https://lore.kernel.org/r/20260918161407.2300-12-will@kernel.org
Assisted-by: LLM
---
arch/arm64/include/asm/preempt.h | 2 +-
arch/arm64/kernel/entry-common.c | 23 +++++++++++++++++------
2 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/arch/arm64/include/asm/preempt.h b/arch/arm64/include/asm/preempt.h
index 4f917596f387d..3d61be0f825fb 100644
--- a/arch/arm64/include/asm/preempt.h
+++ b/arch/arm64/include/asm/preempt.h
@@ -7,7 +7,7 @@
#define PREEMPT_NEED_RESCHED BIT(32)
#define PREEMPT_ENABLED (PREEMPT_NEED_RESCHED)
-static inline int preempt_count(void)
+static __always_inline int preempt_count(void)
{
return READ_ONCE(current_thread_info()->preempt.count);
}
diff --git a/arch/arm64/kernel/entry-common.c b/arch/arm64/kernel/entry-common.c
index 93056a766fb20..fda807c61ff27 100644
--- a/arch/arm64/kernel/entry-common.c
+++ b/arch/arm64/kernel/entry-common.c
@@ -56,6 +56,8 @@ static void noinstr __arm64_exit_to_kernel_mode(struct pt_regs *regs,
irqentry_state_t state)
{
local_daif_mask();
+ /* arm64 uses the split exit API, so enter the tail before MTE tracing. */
+ irqentry_hardirq_tail_enter(state);
mte_check_tfsr_exit();
irqentry_exit_to_kernel_mode_after_preempt(regs, state);
}
@@ -523,12 +525,16 @@ static __always_inline void __el1_irq(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
irqentry_state_t state;
+ bool do_sub;
- state = arm64_enter_from_kernel_mode(regs);
+ state = irqentry_hardirq_enter(regs);
+ mte_check_tfsr_entry();
+ mte_disable_tco_entry(current);
- irq_enter_rcu();
+ irq_enter_rcu_preempt_counted();
do_interrupt_handler(regs, handler);
- irq_exit_rcu();
+ do_sub = irq_exit_rcu_preempt_counted_prepare();
+ irqentry_hardirq_exit_finish(do_sub);
arm64_exit_to_kernel_mode_preempt(regs, state);
}
@@ -839,16 +845,21 @@ asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
static void noinstr el0_interrupt(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
- arm64_enter_from_user_mode(regs);
+ bool do_sub;
+
+ (void)irqentry_hardirq_enter(regs);
+ mte_disable_tco_entry(current);
+ sme_enter_from_user_mode();
write_sysreg(DAIF_PROCCTX_NOIRQ, daif);
if (regs->pc & BIT(55))
arm64_apply_bp_hardening();
- irq_enter_rcu();
+ irq_enter_rcu_preempt_counted();
do_interrupt_handler(regs, handler);
- irq_exit_rcu();
+ do_sub = irq_exit_rcu_preempt_counted_prepare();
+ irqentry_hardirq_exit_finish(do_sub);
arm64_exit_to_user_mode(regs);
}
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 10/12] docs: entry: Describe the pre-counted hardirq lifecycle
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (8 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 09/12] arm64: Use the pre-counted hardirq lifecycle Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 11/12] locking/selftest: Exercise hardirq return-tail preparation Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 12/12] tracing: Pin recursion state for its complete lifetime Karl Mehltretter
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
The entry documentation covers only the traditional irq_enter_rcu()
path. It does not explain the early-counted entry contract or the retained
and synthetic hardirq exit phases.
Document the raw count and lockdep entry, delayed traceable bookkeeping,
hardirq accounting, hardirq-to-softirq count swap, retained real IRQ exit,
possible preemption and migration, and the classification-only return tail.
Also cover late worker closure, raw teardown and context-tracking order.
Record that arm64 excludes pseudo-NMIs from this lifecycle and identify its
external noinstr inlining prerequisites.
Assisted-by: LLM
---
Documentation/core-api/entry.rst | 75 +++++++++++++++++++++++++++-----
1 file changed, 65 insertions(+), 10 deletions(-)
diff --git a/Documentation/core-api/entry.rst b/Documentation/core-api/entry.rst
index 09942135aad18..e02c463dcb08f 100644
--- a/Documentation/core-api/entry.rst
+++ b/Documentation/core-api/entry.rst
@@ -193,21 +193,14 @@ Note that the invocation of the actual interrupt handler is within a
irq_enter_rcu() and irq_exit_rcu() pair.
irq_enter_rcu() updates the preemption count which makes in_hardirq()
-return true, handles NOHZ tick state and interrupt time accounting. This
-means that up to the point where irq_enter_rcu() is invoked in_hardirq()
-returns false.
+return true, handles NOHZ tick state and interrupt time accounting. On this
+entry path, in_hardirq() therefore returns false until irq_enter_rcu() is
+invoked.
irq_exit_rcu() handles interrupt time accounting, handles soft interrupts if
possible, handles the NOHZ tick state and finally undoes the preemption count
update.
-In theory, the preemption count could be updated in irqentry_enter(). In
-practice, deferring this update to irq_enter_rcu() allows the preemption-count
-code to be traced, while also maintaining symmetry with irq_exit_rcu() and
-irqentry_exit(), which are described in the next paragraph. The only downside
-is that the early entry code up to irq_enter_rcu() must be aware that the
-preemption count has not yet been updated with the HARDIRQ_OFFSET state.
-
Note that soft interrupt handlers must run in BH context rather than in hard
interrupt context. irq_exit_rcu() therefore replaces HARDIRQ_OFFSET with
SOFTIRQ_OFFSET in the preemption count while it handles soft interrupts and
@@ -216,6 +209,68 @@ is still attributed to the interrupt. HARDIRQ_OFFSET is removed before
irq_exit_rcu() returns because irqentry_exit() might schedule, which requires
that HARDIRQ_OFFSET has been removed from the preemption count.
+Pre-counted hardirq lifecycle
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Architectures which need hardirq state before early context-tracking or
+tracing callbacks can use the pre-counted lifecycle. It applies only to an
+ordinary hard interrupt after the architecture has distinguished it from an
+NMI-like event:
+
+ * irqentry_hardirq_enter() adds HARDIRQ_OFFSET with a raw update and
+ establishes IRQ-disabled and lockdep hardirq state before instrumentable
+ entry work. It then runs the generic entry and context-tracking path,
+ including any required transition out of an RCU extended quiescent state.
+
+ * irq_enter_rcu_preempt_counted() later performs the traceable debug and
+ latency bookkeeping for that raw update. It also performs the deferred
+ timer repair, NOHZ tick handling and interrupt accounting from
+ irq_enter_rcu().
+
+ * irq_exit_rcu_preempt_counted_prepare() performs interrupt accounting,
+ softirq handling and NOHZ tick handling while retaining the hardirq count
+ and lockdep nesting through every traceable exit callback. Softirq
+ handling uses the same temporary HARDIRQ_OFFSET to SOFTIRQ_OFFSET and back
+ transition described above. The architecture immediately follows the
+ preparation with irqentry_hardirq_exit_finish(), which applies the raw
+ count and lockdep teardown.
+
+The real interrupt is complete after that teardown. Return-to-kernel
+preemption can schedule and migrate the task before the architecture-specific
+tail runs, so the current preemption count cannot identify the original entry
+class. irqentry_state_t retains that provenance instead.
+
+For an ordinary hardirq returning to kernel mode,
+irqentry_hardirq_tail_enter() restores only HARDIRQ_OFFSET and lockdep hardirq
+nesting after the preemption decision. This synthetic classification covers
+traceable architecture tail work, deferred timer rearming, IRQ-on preparation
+and context-tracking preparation. It does not repeat interrupt accounting,
+RCU or tick entry. IRQ-on preparation accepts exactly this one synthetic
+lockdep nesting level.
+
+irqentry_hardirq_tail_exit() performs the final traceable teardown preparation
+and then removes the raw synthetic state. Both retained exit points recheck
+regular softirq work and forced-thread timer work raised by late callbacks and
+wake the corresponding workers before dropping hardirq state. If the
+interrupted context was in an RCU extended quiescent state,
+ct_irq_exit_prepare() runs before the raw tail teardown and
+ct_irq_exit_finish() completes the transition afterwards.
+
+On arm64 the ordinary lifecycle is selected only after the pseudo-NMI
+decision. Pseudo-NMIs continue to use irqentry_nmi_enter() and
+irqentry_nmi_exit(). All raw transitions are in noinstr paths, so
+preempt_count(), the raw preempt-count update helpers and
+__kern_my_cpu_offset() must remain inline. The arm64 conversion includes the
+first requirement and depends on the `arm64 raw preempt-count inlining
+prerequisite`_ and `arm64 per-CPU offset inlining prerequisite`_ for the
+others.
+
+.. _`arm64 raw preempt-count inlining prerequisite`:
+ https://lore.kernel.org/all/c3a90368dd6c096af9d918e036a27728696fb8a4.1786603168.git.hongyan.xia@transsion.com/
+
+.. _`arm64 per-CPU offset inlining prerequisite`:
+ https://lore.kernel.org/r/20260918161407.2300-12-will@kernel.org
+
Even though interrupt handlers are expected to run with local interrupts
disabled, interrupt nesting is common from an entry/exit perspective. For
example, softirq handling happens within an irqentry_{enter,exit}() block with
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 11/12] locking/selftest: Exercise hardirq return-tail preparation
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (9 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 10/12] docs: entry: Describe " Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 12/12] tracing: Pin recursion state for its complete lifetime Karl Mehltretter
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
The return-tail lockdep preparation must accept exactly one live
hardirq context. Zero and nested contexts must fail, and the ordinary
preparation helper must continue to reject a live hardirq context.
Add focused boot-time lockdep cases for those four states. Skip the
final IRQ-state transition after an expected preparation failure so
that each failure remains attributable to the preparation invariant.
Assisted-by: LLM
---
lib/locking-selftest.c | 53 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/lib/locking-selftest.c b/lib/locking-selftest.c
index c3d976c801bb4..1575a04f65d46 100644
--- a/lib/locking-selftest.c
+++ b/lib/locking-selftest.c
@@ -222,6 +222,55 @@ static void init_shared_classes(void)
local_irq_enable(); \
local_bh_enable();
+static void irqtail_prepare_test_enter(unsigned int nr)
+{
+ unsigned int i;
+
+ local_irq_disable();
+ for (i = 0; i < nr; i++)
+ __irq_enter_raw();
+ trace_hardirqs_on_prepare();
+}
+
+static void irqtail_prepare_test_exit(unsigned int nr)
+{
+ unsigned int i;
+
+ for (i = 0; i < nr; i++)
+ __irq_exit_raw();
+ if (debug_locks)
+ lockdep_hardirqs_on(_THIS_IP_);
+ raw_local_irq_enable();
+}
+
+static void prepare_irqtail_0(void)
+{
+ irqtail_prepare_test_enter(0);
+ lockdep_hardirqs_on_prepare_irqtail();
+ irqtail_prepare_test_exit(0);
+}
+
+static void prepare_irqtail_1(void)
+{
+ irqtail_prepare_test_enter(1);
+ lockdep_hardirqs_on_prepare_irqtail();
+ irqtail_prepare_test_exit(1);
+}
+
+static void prepare_irqtail_2(void)
+{
+ irqtail_prepare_test_enter(2);
+ lockdep_hardirqs_on_prepare_irqtail();
+ irqtail_prepare_test_exit(2);
+}
+
+static void prepare_regular_1(void)
+{
+ irqtail_prepare_test_enter(1);
+ lockdep_hardirqs_on_prepare();
+ irqtail_prepare_test_exit(1);
+}
+
/*
* Shortcuts for lock/unlock API variants, to keep
* the testcases compact:
@@ -2934,6 +2983,10 @@ void locking_selftest(void)
DO_TESTCASE_6x2x2RW("irq read-recursion", irq_read_recursion);
DO_TESTCASE_6x2x2RW("irq read-recursion #2", irq_read_recursion2);
DO_TESTCASE_6x2x2RW("irq read-recursion #3", irq_read_recursion3);
+ DO_TESTCASE_1B("IRQ tail prepare", prepare_irqtail, 0);
+ DO_TESTCASE_1("IRQ tail prepare", prepare_irqtail, 1);
+ DO_TESTCASE_1B("IRQ tail prepare", prepare_irqtail, 2);
+ DO_TESTCASE_1B("regular IRQ prepare", prepare_regular, 1);
ww_tests();
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [RFC PATCH 12/12] tracing: Pin recursion state for its complete lifetime
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
` (10 preceding siblings ...)
2026-10-22 5:16 ` [RFC PATCH 11/12] locking/selftest: Exercise hardirq return-tail preparation Karl Mehltretter
@ 2026-10-22 5:16 ` Karl Mehltretter
11 siblings, 0 replies; 13+ messages in thread
From: Karl Mehltretter @ 2026-10-22 5:16 UTC (permalink / raw)
To: Steven Rostedt, Peter Zijlstra, Thomas Gleixner
Cc: Karl Mehltretter, Catalin Marinas, Will Deacon, Mark Rutland,
Ingo Molnar, Andy Lutomirski, Frederic Weisbecker,
Paul E . McKenney, Sebastian Andrzej Siewior, Boqun Feng,
Waiman Long, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Hongyan Xia, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Andrew Morton, Nathan Chancellor, Nick Desaulniers, Bill Wendling,
Justin Stitt, linux-arm-kernel, linux-trace-kernel,
linux-rt-devel, linux-doc, llvm, linux-kernel
trace_test_and_set_recursion() publishes the task recursion bit before it
disables preemption. Scheduler tracing can therefore run while acquisition
is in progress. Conversely, trace_clear_recursion() enables preemption
before clearing the bit, so a reschedule can observe stale recursion state
during release.
CONFIG_FTRACE_VALIDATE_RCU_IS_WATCHING also validates RCU state before the
outer preemption disable. rcu_is_watching() drops its internal preemption
reference before returning, and the warning path accesses
TRACE_RECORD_RECURSION_BIT outside the protected lifetime.
Disable preemption before validation and retain that reference until the
recursion bit is cleared. Balance both rejection paths. This keeps
validation and published recursion state inside one preemption-disabled
interval.
Keep the transition slot unchanged. It remains necessary for
interrupt-entry intervals whose context is not yet reflected in
preempt_count().
Use preempt_enable_notrace() because this inline API is also used by
modules. The no-reschedule variant is unavailable there.
Assisted-by: LLM
---
include/linux/trace_recursion.h | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/include/linux/trace_recursion.h b/include/linux/trace_recursion.h
index e6ca052b2a85a..fd14380751404 100644
--- a/include/linux/trace_recursion.h
+++ b/include/linux/trace_recursion.h
@@ -126,11 +126,16 @@ extern void ftrace_record_recursion(unsigned long ip, unsigned long parent_ip);
static __always_inline int trace_test_and_set_recursion(unsigned long ip, unsigned long pip,
int start)
{
- unsigned int val = READ_ONCE(current->trace_recursion);
+ unsigned int val;
int bit;
- if (trace_warn_on_no_rcu(ip))
+ preempt_disable_notrace();
+ if (trace_warn_on_no_rcu(ip)) {
+ preempt_enable_notrace();
return -1;
+ }
+
+ val = READ_ONCE(current->trace_recursion);
bit = trace_get_context_bit() + start;
if (unlikely(val & (1 << bit))) {
@@ -145,6 +150,7 @@ static __always_inline int trace_test_and_set_recursion(unsigned long ip, unsign
bit = TRACE_CTX_TRANSITION + start;
if (val & (1 << bit)) {
do_ftrace_record_recursion(ip, pip);
+ preempt_enable_notrace();
return -1;
}
}
@@ -153,8 +159,6 @@ static __always_inline int trace_test_and_set_recursion(unsigned long ip, unsign
current->trace_recursion = val;
barrier();
- preempt_disable_notrace();
-
return bit;
}
@@ -163,9 +167,9 @@ static __always_inline int trace_test_and_set_recursion(unsigned long ip, unsign
*/
static __always_inline void trace_clear_recursion(int bit)
{
- preempt_enable_notrace();
barrier();
trace_recursion_clear(bit);
+ preempt_enable_notrace();
}
/**
--
2.39.5 (Apple Git-154)
^ permalink raw reply related [flat|nested] 13+ messages in thread
end of thread, other threads:[~2026-10-22 5:17 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-22 5:16 [RFC PATCH 00/12] entry, tracing: Classify early hardirq entry and return tails Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 01/12] sched: Split preempt-count bookkeeping from raw updates Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 02/12] lockdep: Use raw access for pinned hardirq state Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 03/12] entry: Add the pre-counted ordinary-hardirq entry/exit contract Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 04/12] entry: Skip inactive IRQ-return reschedule calls Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 05/12] context_tracking: Split IRQ-exit preparation from the EQS transition Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 06/12] lockdep: Prepare IRQ-on state from a hardirq return tail Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 07/12] entry: Carry ordinary-hardirq provenance through kernel return Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 08/12] entry: Classify ordinary hardirq return-tail work as hardirq context Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 09/12] arm64: Use the pre-counted hardirq lifecycle Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 10/12] docs: entry: Describe " Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 11/12] locking/selftest: Exercise hardirq return-tail preparation Karl Mehltretter
2026-10-22 5:16 ` [RFC PATCH 12/12] tracing: Pin recursion state for its complete lifetime Karl Mehltretter
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox