From: Mayank Rungta <mrungta@google.com>
To: Wim Van Sebroeck <wim@linux-watchdog.org>,
Guenter Roeck <linux@roeck-us.net>,
Thomas Gleixner <tglx@kernel.org>, Radu Rendec <radu@rendec.net>
Cc: Marc Zyngier <maz@kernel.org>,
Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>,
linux-watchdog@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-arm-msm@vger.kernel.org,
"Kirill A . Shutemov" <kas@kernel.org>,
Douglas Anderson <dianders@chromium.org>,
Mayank Rungta <mrungta@google.com>
Subject: [PATCH v4 1/7] genirq: Synchronize in-flight handlers during NMI teardown
Date: Wed, 11 Nov 2026 19:16:23 -0800 [thread overview]
Message-ID: <20261112031629.396819-2-mrungta@google.com> (raw)
In-Reply-To: <20261112031629.396819-1-mrungta@google.com>
When freeing or disabling an interrupt requested via request_nmi(),
__cleanup_nmi() and __synchronize_hardirq() currently do not wait for
in-flight instances of the NMI handler on other CPUs to complete, which
can lead to use-after-free conditions on module unload or driver unbind.
Regular IRQs and NMIs require different synchronization mechanisms:
- Regular IRQ flow handlers already acquire desc->lock, so tracking
in-flight execution via IRQD_IRQ_INPROGRESS under desc->lock avoids
extra atomic operations in the interrupt fast path and extra memory
barriers during synchronization.
- NMI handlers cannot acquire desc->lock, and querying hardware
IRQCHIP_STATE_ACTIVE alone does not cover handler execution after EOI.
Track in-flight NMI handlers locklessly via an atomic counter
(desc->nmi_active) in handle_fasteoi_nmi(), paired with a dedicated
__synchronize_nmi() helper that waits for both nmi_active and
(when supported) IRQCHIP_STATE_ACTIVE to drain.
Because handle_fasteoi_nmi() runs locklessly and cannot mask lazily
disabled interrupts in hardware, force unlazy disable
(_IRQ_DISABLE_UNLAZY) in request_nmi() and mask the interrupt in
hardware before setting IRQD_IRQ_DISABLED in __irq_disable() (matching
irq_shutdown()). This prevents a level-triggered NMI from observing
IRQD_IRQ_DISABLED while still unmasked in hardware and livelocking on
EOI, and is safe for regular IRQs because __irq_disable() runs under
desc->lock and no chip disable/mask callbacks inspect
irqd_irq_disabled().
Update __cleanup_nmi() to serialize teardown, shut down and synchronize
the NMI before clearing desc->action and NMI state, and warn if
free_nmi() is called from interrupt context.
Signed-off-by: Mayank Rungta <mrungta@google.com>
---
include/linux/irqdesc.h | 4 ++
kernel/irq/chip.c | 35 +++++++++----
kernel/irq/manage.c | 111 ++++++++++++++++++++++++++++++++++------
kernel/irq/settings.h | 5 ++
4 files changed, 130 insertions(+), 25 deletions(-)
diff --git a/include/linux/irqdesc.h b/include/linux/irqdesc.h
index 779d6023c5c2..f1fabddf9650 100644
--- a/include/linux/irqdesc.h
+++ b/include/linux/irqdesc.h
@@ -65,6 +65,9 @@ struct irq_redirect {
* @threads_oneshot: bitfield to handle shared oneshot threads
* @threads_active: number of irqaction threads currently running
* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
+ * @nmi_active: number of NMI handlers currently running (normally 0
+ * or 1, but can briefly reach 2 if an NMI re-triggers
+ * between hardware EOI and atomic_dec())
* @nr_actions: number of installed actions on this descriptor
* @no_suspend_depth: number of irqactions on a irq descriptor with
* IRQF_NO_SUSPEND set
@@ -107,6 +110,7 @@ struct irq_desc {
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
+ atomic_t nmi_active;
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
diff --git a/kernel/irq/chip.c b/kernel/irq/chip.c
index de754db414d1..565cb0233fff 100644
--- a/kernel/irq/chip.c
+++ b/kernel/irq/chip.c
@@ -364,13 +364,19 @@ static void __irq_disable(struct irq_desc *desc, bool mask)
if (mask)
mask_irq(desc);
} else {
- irq_state_set_disabled(desc);
if (desc->irq_data.chip->irq_disable) {
desc->irq_data.chip->irq_disable(&desc->irq_data);
irq_state_set_masked(desc);
} else if (mask) {
mask_irq(desc);
}
+ /*
+ * Mask in hardware before setting IRQD_IRQ_DISABLED so a
+ * lockless NMI firing before hardware masking completes does
+ * not observe IRQD_IRQ_DISABLED, skip its handler, and EOI an
+ * unmasked level-triggered line into an infinite NMI storm.
+ */
+ irq_state_set_disabled(desc);
}
}
@@ -792,21 +798,32 @@ EXPORT_SYMBOL_GPL(handle_fasteoi_irq);
void handle_fasteoi_nmi(struct irq_desc *desc)
{
struct irq_chip *chip = irq_desc_get_chip(desc);
- struct irqaction *action = desc->action;
unsigned int irq = irq_desc_get_irq(desc);
+ struct irqaction *action;
irqreturn_t res;
- __kstat_incr_irqs_this_cpu(desc);
+ atomic_inc(&desc->nmi_active);
+ /* Ensure nmi_active is visible before reading action or disabled state */
+ smp_mb__after_atomic();
- trace_irq_handler_entry(irq, action);
- /*
- * NMIs cannot be shared, there is only one action.
- */
- res = action->handler(irq, action->dev_id);
- trace_irq_handler_exit(irq, action, res);
+ action = READ_ONCE(desc->action);
+ if (likely(action && !irqd_irq_disabled(&desc->irq_data))) {
+ __kstat_incr_irqs_this_cpu(desc);
+
+ trace_irq_handler_entry(irq, action);
+ /*
+ * NMIs cannot be shared, there is only one action.
+ */
+ res = action->handler(irq, action->dev_id);
+ trace_irq_handler_exit(irq, action, res);
+ }
if (chip->irq_eoi)
chip->irq_eoi(&desc->irq_data);
+
+ /* Ensure all handler accesses complete before decrementing nmi_active */
+ smp_mb__before_atomic();
+ atomic_dec(&desc->nmi_active);
}
EXPORT_SYMBOL_GPL(handle_fasteoi_nmi);
diff --git a/kernel/irq/manage.c b/kernel/irq/manage.c
index ec3ecb0e48ce..3fb77a2d954d 100644
--- a/kernel/irq/manage.c
+++ b/kernel/irq/manage.c
@@ -48,11 +48,72 @@ static inline void synchronize_irqwork(struct irq_desc *desc) { }
static int __irq_get_irqchip_state(struct irq_data *d, enum irqchip_irq_state which, bool *state);
+static void __synchronize_nmi(struct irq_desc *desc, bool sync_chip)
+{
+ struct irq_data *irqd = irq_desc_get_irq_data(desc);
+ bool inprogress;
+
+ do {
+ /*
+ * Wait until we're out of the NMI handler before taking
+ * desc->lock or polling the irqchip. This might give the
+ * wrong answer due to the lack of memory barriers.
+ */
+ while (atomic_read(&desc->nmi_active))
+ cpu_relax();
+
+ guard(raw_spinlock_irqsave)(&desc->lock);
+ /*
+ * Ok, that indicated we're done: double-check carefully.
+ *
+ * Holding desc->lock serializes with prior state updates (e.g.
+ * IRQD_IRQ_DISABLED set under desc->lock) and protects
+ * __irq_get_irqchip_state(). Together with
+ * smp_mb__after_spinlock(), this provides a full memory
+ * barrier pairing with smp_mb__after_atomic() in
+ * handle_fasteoi_nmi() so those updates are visible before
+ * reading nmi_active.
+ */
+ smp_mb__after_spinlock();
+
+ /*
+ * Pair with smp_mb__before_atomic() + atomic_dec() in
+ * handle_fasteoi_nmi() so all handler memory accesses complete
+ * before __synchronize_nmi() returns.
+ */
+ inprogress = atomic_read_acquire(&desc->nmi_active);
+
+ /*
+ * Also check hardware active state when supported to bridge the
+ * window between hardware acknowledge and atomic_inc() in
+ * handle_fasteoi_nmi() on non-percpu_devid SPIs. Ignore the
+ * return code; inprogress is only updated when the chip
+ * supports it. Note that if a chip does not implement this,
+ * disable_nmi() is still safe, and free_nmi() is as safe as we
+ * can make it (though an NMI acknowledged just before masking
+ * that has not yet entered handle_fasteoi_nmi() could race with
+ * irq_nmi_teardown() resetting desc->handle_irq).
+ */
+ if (!inprogress && sync_chip)
+ __irq_get_irqchip_state(irqd, IRQCHIP_STATE_ACTIVE,
+ &inprogress);
+ } while (inprogress);
+}
+
static void __synchronize_hardirq(struct irq_desc *desc, bool sync_chip)
{
struct irq_data *irqd = irq_desc_get_irq_data(desc);
bool inprogress;
+ /*
+ * Generic paths such as synchronize_irq() (e.g. suspend_device_irqs())
+ * and synchronize_hardirq() can be called on NMIs; redirect them.
+ */
+ if (unlikely(irq_is_nmi(desc) && !irq_settings_is_per_cpu_devid(desc))) {
+ __synchronize_nmi(desc, sync_chip);
+ return;
+ }
+
do {
/*
* Wait until we're out of the critical section. This might
@@ -2044,29 +2105,38 @@ EXPORT_SYMBOL(free_irq);
static const void *__cleanup_nmi(unsigned int irq, struct irq_desc *desc)
{
- struct irqaction *action = NULL;
- const char *devname = NULL;
+ struct irqaction *action;
+ const char *devname;
- scoped_guard(raw_spinlock_irqsave, &desc->lock) {
- irq_nmi_teardown(desc);
+ guard(mutex)(&desc->request_mutex);
- desc->istate &= ~IRQS_NMI;
-
- if (!WARN_ON(desc->action == NULL)) {
- action = desc->action;
- irq_pm_remove_action(desc, action);
- devname = action->name;
- }
- desc->action = NULL;
+ scoped_guard(raw_spinlock_irqsave, &desc->lock) {
+ if (WARN_ON(!desc->action))
+ return NULL;
irq_settings_clr_disable_unlazy(desc);
- irq_shutdown_and_deactivate(desc);
+ irq_shutdown(desc);
}
- irq_proc_update_valid(desc);
+ /*
+ * Ensure all in-flight NMI handlers on other CPUs complete before
+ * clearing desc->action or tearing down NMI state.
+ */
+ __synchronize_nmi(desc, true);
+
+ scoped_guard(raw_spinlock_irqsave, &desc->lock) {
+ action = desc->action;
+ irq_pm_remove_action(desc, action);
+ devname = action->name;
+ WRITE_ONCE(desc->action, NULL);
- if (action)
- unregister_handler_proc(irq, action);
+ irq_nmi_teardown(desc);
+ desc->istate &= ~IRQS_NMI;
+ irq_domain_deactivate_irq(&desc->irq_data);
+ irq_proc_update_valid(desc);
+ }
+
+ unregister_handler_proc(irq, action);
kfree(action);
irq_release_resources(desc);
@@ -2081,6 +2151,8 @@ const void *free_nmi(unsigned int irq, void *dev_id)
{
struct irq_desc *desc = irq_to_desc(irq);
+ WARN(in_interrupt(), "Trying to free NMI %d from IRQ context!\n", irq);
+
if (!desc || WARN_ON(!irq_is_nmi(desc)))
return NULL;
@@ -2337,6 +2409,13 @@ int request_nmi(unsigned int irq, irq_handler_t handler,
goto err_irq_setup;
scoped_guard(raw_spinlock_irqsave, &desc->lock) {
+ /*
+ * Force unlazy disable so disable_nmi() / disable_nmi_nosync()
+ * always mask the interrupt in hardware; handle_fasteoi_nmi()
+ * runs locklessly and cannot mask lazily disabled interrupts.
+ */
+ irq_settings_set_disable_unlazy(desc);
+
/* Setup NMI state */
desc->istate |= IRQS_NMI;
retval = irq_nmi_setup(desc);
diff --git a/kernel/irq/settings.h b/kernel/irq/settings.h
index 0a0c027a5d34..4fefdf062dbd 100644
--- a/kernel/irq/settings.h
+++ b/kernel/irq/settings.h
@@ -163,6 +163,11 @@ static inline bool irq_settings_disable_unlazy(struct irq_desc *desc)
return desc->status_use_accessors & _IRQ_DISABLE_UNLAZY;
}
+static inline void irq_settings_set_disable_unlazy(struct irq_desc *desc)
+{
+ desc->status_use_accessors |= _IRQ_DISABLE_UNLAZY;
+}
+
static inline void irq_settings_clr_disable_unlazy(struct irq_desc *desc)
{
desc->status_use_accessors &= ~_IRQ_DISABLE_UNLAZY;
--
2.56.0.611.g1030f0b940-goog
next prev parent reply other threads:[~2026-11-12 3:16 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-12 3:16 [PATCH v4 0/7] watchdog: qcom: Support NMI pretimeout warnings Mayank Rungta
2026-11-12 3:16 ` Mayank Rungta [this message]
2026-11-12 3:16 ` [PATCH v4 2/7] genirq: Allow global interrupts in request_nmi() Mayank Rungta
2026-11-12 3:16 ` [PATCH v4 3/7] genirq: Implement synchronous disable_nmi() Mayank Rungta
2026-11-12 3:16 ` [PATCH v4 4/7] genirq: Export NMI APIs for qcom-wdt Mayank Rungta
2026-11-12 3:16 ` [PATCH v4 5/7] watchdog: pretimeout: Protect governor access with RCU for NMI safety Mayank Rungta
2026-11-12 3:16 ` [PATCH v4 6/7] watchdog: pretimeout_panic: Use nmi_panic() when invoked from NMI context Mayank Rungta
2026-11-12 3:16 ` [PATCH v4 7/7] watchdog: qcom: Register pretimeout interrupt as NMI Mayank Rungta
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261112031629.396819-2-mrungta@google.com \
--to=mrungta@google.com \
--cc=dianders@chromium.org \
--cc=kas@kernel.org \
--cc=konrad.dybcio@oss.qualcomm.com \
--cc=linux-arm-msm@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-watchdog@vger.kernel.org \
--cc=linux@roeck-us.net \
--cc=maz@kernel.org \
--cc=radu@rendec.net \
--cc=tglx@kernel.org \
--cc=wim@linux-watchdog.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®