arm64: entry: Avoid unnecessary local_irq_disable() on kernel exit
authorVladimir Murzin <vladimir.murzin@arm.com>
Mon, 27 Jul 2026 16:34:17 +0000 (17:34 +0100)
committerWill Deacon <will@kernel.org>
Tue, 11 Aug 2026 11:35:08 +0000 (11:35 +0000)
Currently, when exiting to kernel mode, we attempt involuntary
preemption. The preemption logic expects IRQs to be disabled, which is
why we call local_irq_disable() before attempting preemption.

However, depending on the context, local_irq_disable() may be
unnecessary:

- __el1_irq(), the non-NMI EL1 IRQ path, already has IRQs disabled, so
  local_irq_disable() is redundant.

- irqentry_exit_to_kernel_mode_preempt() immediately returns when
  exiting from an NMI-like context, so calling local_irq_disable()
  beforehand is unnecessary work.

Furthermore, it confuses the pNMI state tracking when we are in a
context with interrupts disabled and the GIC_PRIO_PSR_I_SET bit is set
in the PMR, leading to a warning when
CONFIG_ARM64_DEBUG_PRIORITY_MASKING=y:

     WARNING: ./arch/arm64/include/asm/irqflags.h:63 at arm64_exit_to_kernel_mode+0xb8/0xc0, CPU#40: retsnoop/31805
     CPU: 40 UID: 0 PID: 31805 Comm: retsnoop Not tainted 7.2.0-rc6-next-20260805 #7 PREEMPTLAZY
     pstate: 234013c9 (nzCv DAIF +PAN -UAO +TCO +DIT +SSBS BTYPE=--)
     pc : arm64_exit_to_kernel_mode (arch/arm64/kernel/entry-common.c:63)
     lr : el1_abort (arch/arm64/kernel/entry-common.c:323)
     pmr: 000000f0
     Call trace:
      arm64_exit_to_kernel_mode (arch/arm64/kernel/entry-common.c:63) (P)
      el1_abort (arch/arm64/kernel/entry-common.c:323)
      el1h_64_sync_handler (arch/arm64/kernel/entry-common.c:449)
      el1h_64_sync (arch/arm64/kernel/entry.S:589)
      [...]

Split arm64_exit_to_kernel_mode() into preempt, non-preempt, and
dispatch parts so that we can avoid this extra work where it is not
needed and avoid breaking the pNMI tracking logic.

Reported-by: Breno Leitao <leitao@debian.org>
Fixes: ae654112eac0 ("arm64: entry: Use split preemption logic")
Link: https://lore.kernel.org/all/20260807-arm64_fix-v1-1-d069ccf9d71b@debian.org/
Reviewed-by: Jinjie Ruan <ruanjinjie@huawei.com>
Signed-off-by: Vladimir Murzin <vladimir.murzin@arm.com>
Signed-off-by: Will Deacon <will@kernel.org>
arch/arm64/kernel/entry-common.c

index 2be42d7..72c03cc 100644 (file)
@@ -52,16 +52,36 @@ static noinstr irqentry_state_t arm64_enter_from_kernel_mode(struct pt_regs *reg
  * After this function returns it is not safe to call regular kernel code,
  * instrumentable code, or any code which may trigger an exception.
  */
-static void noinstr arm64_exit_to_kernel_mode(struct pt_regs *regs,
-                                             irqentry_state_t state)
+static void noinstr __arm64_exit_to_kernel_mode(struct pt_regs *regs,
+                                               irqentry_state_t state)
 {
-       local_irq_disable();
-       irqentry_exit_to_kernel_mode_preempt(regs, state);
        local_daif_mask();
        mte_check_tfsr_exit();
        irqentry_exit_to_kernel_mode_after_preempt(regs, state);
 }
 
+/*
+ * We are returning from the context which allows involuntary kernel preemption
+ */
+static void noinstr arm64_exit_to_kernel_mode_preempt(struct pt_regs *regs,
+                                                     irqentry_state_t state)
+{
+       irqentry_exit_to_kernel_mode_preempt(regs, state);
+       __arm64_exit_to_kernel_mode(regs, state);
+}
+
+static void noinstr arm64_exit_to_kernel_mode(struct pt_regs *regs,
+                                             irqentry_state_t state)
+{
+       if (!regs_irqs_disabled(regs)) {
+               local_irq_disable();
+               arm64_exit_to_kernel_mode_preempt(regs, state);
+               return;
+       }
+
+       __arm64_exit_to_kernel_mode(regs, state);
+}
+
 static __always_inline void arm64_syscall_enter_from_user_mode(struct pt_regs *regs)
 {
        enter_from_user_mode(regs);
@@ -510,7 +530,7 @@ static __always_inline void __el1_irq(struct pt_regs *regs,
        do_interrupt_handler(regs, handler);
        irq_exit_rcu();
 
-       arm64_exit_to_kernel_mode(regs, state);
+       arm64_exit_to_kernel_mode_preempt(regs, state);
 }
 static void noinstr el1_interrupt(struct pt_regs *regs,
                                  void (*handler)(struct pt_regs *))