[PATCH AUTOSEL 6.19-6.18] irqchip/riscv-imsic: Add a CPU pm notifier to restore the IMSIC on exit

Sasha Levin
2026-02-12T01:10:31Z

From: Nick Hu <nick.hu@sifive.com>

[ Upstream commit f48b4bd0915bf61ac12b8c65c7939ebd03bc8abf ]

The IMSIC might be reset when the system enters a low power state, but on
exit nothing restores the registers, which prevents interrupt delivery.

Solve this by registering a CPU power management notifier, which restores
the IMSIC on exit.

Signed-off-by: Nick Hu <nick.hu@sifive.com>
Signed-off-by: Thomas Gleixner <tglx@linutronix.de>
Reviewed-by: Yong-Xuan Wang <yongxuan.wang@sifive.com>
Reviewed-by: Cyan Yang <cyan.yang@sifive.com>
Reviewed-by: Anup Patel <anup@brainfault.org>
Reviewed-by: Nutty Liu <liujingqi@lanxincomputing.com>
Link: https://patch.msgid.link/20251202-preserve-aplic-imsic-v3-1-1844fbf1fe92@sifive.com
Signed-off-by: Sasha Levin <sashal@kernel.org>
---

LLM Generated explanations, may be completely bogus:

Now I have a comprehensive understanding. Let me summarize the analysis.

---

## Detailed Analysis

### 1. Commit Message Analysis

The commit message is clear and directly describes a **bug fix**: "The
IMSIC might be reset when the system enters a low power state, but on
exit nothing restores the registers, which prevents interrupt delivery."
This is unambiguous — without the fix, **interrupt delivery is broken**
after returning from a low power state.

The patch is from Nick Hu at SiFive (a major RISC-V silicon vendor),
signed-off by Thomas Gleixner (the overall irqchip maintainer), and
reviewed by:
- Anup Patel (original IMSIC driver author/maintainer at Ventana Micro)
- Yong-Xuan Wang, Cyan Yang (SiFive reviewers)
- Nutty Liu (Lanxin Computing)

This level of review by domain experts demonstrates the fix is well-
vetted.

### 2. Code Change Analysis

The change is minimal (31 lines added, 8 removed, single file) and
consists of three logical parts:

**a) Refactoring — extracting `imsic_hw_states_init()`:**

The existing code from `imsic_starting_cpu()` that handles hardware
register initialization is moved into a new helper:

```126:147:drivers/irqchip/irq-riscv-imsic-early.c
static int imsic_starting_cpu(unsigned int cpu)
{
/* Mark per-CPU IMSIC state as online */
imsic_state_online();

/* Enable per-CPU parent interrupt */
enable_percpu_irq(imsic_parent_irq,
irq_get_trigger_type(imsic_parent_irq));

/* Setup IPIs */
imsic_ipi_starting_cpu();

/*
- Interrupts identities might have been enabled/disabled while
- this CPU was not running so sync-up local enable/disable state.
*/
imsic_local_sync_all(true);

/* Enable local interrupt delivery */
imsic_local_delivery(true);

return 0;
}
```

The three operations (`imsic_ipi_starting_cpu()`,
`imsic_local_sync_all(true)`, `imsic_local_delivery(true)`) are
extracted into `imsic_hw_states_init()`, which is then called from both
`imsic_starting_cpu()` and the new PM notifier.

**b) Adding the CPU PM notifier:**

A new `imsic_pm_notifier` function handles `CPU_PM_EXIT` by calling
`imsic_hw_states_init()`. This is the **exact same pattern** used by
GICv3 (`irq-gic-v3.c:1482`) and GIC (`irq-gic.c`), which have been
stable for years.

**c) Registering the notifier:**

The `imsic_early_probe()` return is changed from `return 0` to `return
cpu_pm_register_notifier(&imsic_pm_notifier_block)`.

### 3. Bug Mechanism and Severity

**The bug**: On RISC-V systems with SBI-based cpuidle (the standard CPU
idle mechanism), when a CPU enters a deep idle state, the SBI firmware
may power down the IMSIC. The `cpuidle-riscv-sbi.c` driver calls
`cpu_pm_enter()` before and `cpu_pm_exit()` after the idle transition.
`cpu_pm_exit()` fires `CPU_PM_EXIT` notifications to all registered
handlers. Without this patch, no handler exists for IMSIC, so:

1. `imsic_local_delivery` — the EIDELIVERY/EITHRESHOLD CSRs may be
reset, disabling all interrupt delivery
2. `imsic_local_sync_all` — individual interrupt enable bits may be
reset, leaving all interrupt sources disabled
3. `imsic_ipi_starting_cpu` — the IPI enable bit may be reset, breaking
inter-processor interrupts

**Impact**: The CPU effectively becomes deaf to all interrupts — IPIs,
device interrupts, timer interrupts routed through IMSIC — after
returning from a deep idle state. This can cause:
- System hangs (no timer interrupts)
- IPI failures (scheduler hangs, RCU stalls)
- Device interrupt loss (I/O failures, network timeouts)

This bug affects any RISC-V system using IMSIC with cpuidle deep states
— which includes SiFive boards and other production RISC-V hardware.

### 4. Dependency Check

The patch is **self-contained**. It depends only on:
- `imsic_ipi_starting_cpu()` — exists in all stable trees with IMSIC
(6.12.y+)
- `imsic_local_sync_all()` — exists in all stable trees with IMSIC
(6.12.y+)
- `imsic_local_delivery()` — exists in all stable trees with IMSIC
(6.12.y+)
- `cpu_pm_register_notifier()` — exists in all kernel versions (core
kernel PM API)
- `cpuidle-riscv-sbi.c` — exists in all stable trees with IMSIC, and
calls `cpu_pm_enter()`/`cpu_pm_exit()`

The companion APLIC patch (95a8ddde36601) touches a different driver
entirely and is not a dependency.

### 5. Backport Applicability

- **IMSIC driver exists**: 6.12.y through 6.19.y (added in v6.10)
- **Clean backport**: The `imsic_starting_cpu` function is byte-
identical between the mainline pre-image and 6.12.y. Only minor
context conflict in the `#include` section (6.12.y lacks
`<linux/export.h>` that mainline has), trivially resolved.
- **Bug triggerable**: The cpuidle-riscv-sbi driver in 6.12.y already
calls `cpu_pm_enter()`/`cpu_pm_exit()`, meaning the IMSIC registers
get reset with no restoration.

### 6. Risk Assessment

**Very low risk**:
- The refactored code (`imsic_hw_states_init`) performs exactly the same
operations as before — just from an additional call site
- The GICv3 driver has used this exact pattern (CPU PM notifier
restoring interrupt controller state) for over a decade
- No behavior changes to the existing CPU hotplug path
- Well-reviewed by all key stakeholders

### 7. Conclusion

This is a clear-cut bug fix for a critical issue: **complete loss of
interrupt delivery** after RISC-V CPUs return from deep idle states. The
fix is:
- Small and surgical (31 lines added to a single file)
- Based on a well-established pattern (identical to GICv3/GIC CPU PM
handling)
- Thoroughly reviewed by domain experts
- Self-contained with no dependencies on other patches
- Cleanly backportable to all stable trees containing IMSIC (6.12.y+)

**YES**

drivers/irqchip/irq-riscv-imsic-early.c | 39 ++++++++++++++++++++-----
1 file changed, 31 insertions(+), 8 deletions(-)

diff --git a/drivers/irqchip/irq-riscv-imsic-early.c b/drivers/irqchip/irq-riscv-imsic-early.c
index 6bac67cc0b6d9..ba903fa689bd5 100644
--- a/drivers/irqchip/irq-riscv-imsic-early.c
+++ b/drivers/irqchip/irq-riscv-imsic-early.c
@@ -7,6 +7,7 @@
#define pr_fmt(fmt) "riscv-imsic: " fmt
#include <linux/acpi.h>
#include <linux/cpu.h>
+#include <linux/cpu_pm.h>
#include <linux/export.h>
#include <linux/interrupt.h>
#include <linux/init.h>
@@ -123,14 +124,8 @@ static void imsic_handle_irq(struct irq_desc *desc)
chained_irq_exit(chip, desc);
}

-static int imsic_starting_cpu(unsigned int cpu)
+static void imsic_hw_states_init(void)
{
- /* Mark per-CPU IMSIC state as online */
- imsic_state_online();
-
- /* Enable per-CPU parent interrupt */
- enable_percpu_irq(imsic_parent_irq, irq_get_trigger_type(imsic_parent_irq));
-
/* Setup IPIs */
imsic_ipi_starting_cpu();

@@ -142,6 +137,18 @@ static int imsic_starting_cpu(unsigned int cpu)

/* Enable local interrupt delivery */
imsic_local_delivery(true);
+}
+
+static int imsic_starting_cpu(unsigned int cpu)
+{
+ /* Mark per-CPU IMSIC state as online */
+ imsic_state_online();
+
+ /* Enable per-CPU parent interrupt */
+ enable_percpu_irq(imsic_parent_irq, irq_get_trigger_type(imsic_parent_irq));
+
+ /* Initialize the IMSIC registers to enable the interrupt delivery */
+ imsic_hw_states_init();

return 0;
}
@@ -157,6 +164,22 @@ static int imsic_dying_cpu(unsigned int cpu)
return 0;
}

+static int imsic_pm_notifier(struct notifier_block *self, unsigned long cmd, void *v)
+{
+ switch (cmd) {
+ case CPU_PM_EXIT:
+ /* Initialize the IMSIC registers to enable the interrupt delivery */
+ imsic_hw_states_init();
+ break;
+ }
+
+ return NOTIFY_OK;
+}
+
+static struct notifier_block imsic_pm_notifier_block = {
+ .notifier_call = imsic_pm_notifier,
+};
+
static int __init imsic_early_probe(struct fwnode_handle *fwnode)
{
struct irq_domain *domain;
@@ -194,7 +217,7 @@ static int __init imsic_early_probe(struct fwnode_handle *fwnode)
cpuhp_setup_state(CPUHP_AP_IRQ_RISCV_IMSIC_STARTING, "irqchip/riscv/imsic:starting",
imsic_starting_cpu, imsic_dying_cpu);

- return 0;
+ return cpu_pm_register_notifier(&imsic_pm_notifier_block);
}

static int __init imsic_early_dt_init(struct device_node *node, struct device_node *parent)
--
2.51.0