[tip:,x86/core] x86: Fix CPUIDLE_FLAG_IRQ_ENABLE leaking timer reprogram
Commit Message
The following commit has been merged into the x86/core branch of tip:
Commit-ID: edc8fc01f608108b0b7580cb2c29dfb5135e5f0e
Gitweb: https://git.kernel.org/tip/edc8fc01f608108b0b7580cb2c29dfb5135e5f0e
Author: Peter Zijlstra <peterz@infradead.org>
AuthorDate: Wed, 15 Nov 2023 10:13:23 -05:00
Committer: Peter Zijlstra <peterz@infradead.org>
CommitterDate: Wed, 29 Nov 2023 15:44:01 +01:00
x86: Fix CPUIDLE_FLAG_IRQ_ENABLE leaking timer reprogram
intel_idle_irq() re-enables IRQs very early. As a result, an interrupt
may fire before mwait() is eventually called. If such an interrupt queues
a timer, it may go unnoticed until mwait returns and the idle loop
handles the tick re-evaluation. And monitoring TIF_NEED_RESCHED doesn't
help because a local timer enqueue doesn't set that flag.
The issue is mitigated by the fact that this idle handler is only invoked
for shallow C-states when, presumably, the next tick is supposed to be
close enough. There may still be rare cases though when the next tick
is far away and the selected C-state is shallow, resulting in a timer
getting ignored for a while.
Fix this with using sti_mwait() whose IRQ-reenablement only triggers
upon calling mwait(), dealing with the race while keeping the interrupt
latency within acceptable bounds.
Fixes: c227233ad64c (intel_idle: enable interrupts before C1 on Xeons)
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Signed-off-by: Frederic Weisbecker <frederic@kernel.org>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Acked-by: Rafael J. Wysocki <rafael@kernel.org>
Link: https://lkml.kernel.org/r/20231115151325.6262-3-frederic@kernel.org
---
arch/x86/include/asm/mwait.h | 11 +++++++++--
drivers/idle/intel_idle.c | 19 +++++++------------
2 files changed, 16 insertions(+), 14 deletions(-)
Comments
On Wed, Nov 29, 2023 at 02:55:55PM -0000, tip-bot2 for Peter Zijlstra wrote:
> diff --git a/arch/x86/include/asm/mwait.h b/arch/x86/include/asm/mwait.h
> index 341ee4f..920426d 100644
> --- a/arch/x86/include/asm/mwait.h
> +++ b/arch/x86/include/asm/mwait.h
> @@ -124,8 +124,15 @@ static __always_inline void mwait_idle_with_hints(unsigned long eax, unsigned lo
> }
>
> __monitor((void *)¤t_thread_info()->flags, 0, 0);
> - if (!need_resched())
> - __mwait(eax, ecx);
> +
> + if (!need_resched()) {
> + if (ecx & 1) {
> + __mwait(eax, ecx);
> + } else {
> + __sti_mwait(eax, ecx);
> + raw_local_irq_disable();
> + }
> + }
Andrew noted that this is only safe if it precludes #DB from happening
on mwait, because #DB can wreck the STI shadow thing.
> @@ -159,19 +160,13 @@ static __always_inline int __intel_idle(struct cpuidle_device *dev,
> static __cpuidle int intel_idle(struct cpuidle_device *dev,
> struct cpuidle_driver *drv, int index)
> {
> + return __intel_idle(dev, drv, index, true);
> }
>
> static __cpuidle int intel_idle_irq(struct cpuidle_device *dev,
> struct cpuidle_driver *drv, int index)
> {
> + return __intel_idle(dev, drv, index, false);
> }
>
> static __cpuidle int intel_idle_ibrs(struct cpuidle_device *dev,
> @@ -184,7 +179,7 @@ static __cpuidle int intel_idle_ibrs(struct cpuidle_device *dev,
> if (smt_active)
> __update_spec_ctrl(0);
>
> + ret = __intel_idle(dev, drv, index, true);
>
> if (smt_active)
> __update_spec_ctrl(spec_ctrl);
> @@ -196,7 +191,7 @@ static __cpuidle int intel_idle_xstate(struct cpuidle_device *dev,
> struct cpuidle_driver *drv, int index)
> {
> fpu_idle_fpregs();
> + return __intel_idle(dev, drv, index, true);
> }
This is so, because all mwait users should be in __cpuidle section,
which itself is part of the noinstr section and as such
kprobes/hw-breakpoints etc.. are disallowed.
Notable vmlinux.lds.h has:
#define NOINSTR_TEXT \
ALIGN_FUNCTION(); \
__noinstr_text_start = .; \
*(.noinstr.text) \
__cpuidle_text_start = .; \
*(.cpuidle.text) \
__cpuidle_text_end = .; \
__noinstr_text_end = .;
On Thu, Nov 30, 2023 at 12:15:19PM +0100, Peter Zijlstra wrote:
> This is so, because all mwait users should be in __cpuidle section,
> which itself is part of the noinstr section and as such
> kprobes/hw-breakpoints etc.. are disallowed.
>
> Notable vmlinux.lds.h has:
>
> #define NOINSTR_TEXT \
> ALIGN_FUNCTION(); \
> __noinstr_text_start = .; \
> *(.noinstr.text) \
> __cpuidle_text_start = .; \
> *(.cpuidle.text) \
> __cpuidle_text_end = .; \
> __noinstr_text_end = .;
So #DB aren't supposed to happen then, right? Or you noticed an mwait
user that doesn't have __cpuidle?
Thanks.
@@ -124,8 +124,15 @@ static __always_inline void mwait_idle_with_hints(unsigned long eax, unsigned lo
}
__monitor((void *)¤t_thread_info()->flags, 0, 0);
- if (!need_resched())
- __mwait(eax, ecx);
+
+ if (!need_resched()) {
+ if (ecx & 1) {
+ __mwait(eax, ecx);
+ } else {
+ __sti_mwait(eax, ecx);
+ raw_local_irq_disable();
+ }
+ }
}
current_clr_polling();
}
@@ -131,11 +131,12 @@ static unsigned int mwait_substates __initdata;
#define MWAIT2flg(eax) ((eax & 0xFF) << 24)
static __always_inline int __intel_idle(struct cpuidle_device *dev,
- struct cpuidle_driver *drv, int index)
+ struct cpuidle_driver *drv,
+ int index, bool irqoff)
{
struct cpuidle_state *state = &drv->states[index];
unsigned long eax = flg2MWAIT(state->flags);
- unsigned long ecx = 1; /* break on interrupt flag */
+ unsigned long ecx = 1*irqoff; /* break on interrupt flag */
mwait_idle_with_hints(eax, ecx);
@@ -159,19 +160,13 @@ static __always_inline int __intel_idle(struct cpuidle_device *dev,
static __cpuidle int intel_idle(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int index)
{
- return __intel_idle(dev, drv, index);
+ return __intel_idle(dev, drv, index, true);
}
static __cpuidle int intel_idle_irq(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int index)
{
- int ret;
-
- raw_local_irq_enable();
- ret = __intel_idle(dev, drv, index);
- raw_local_irq_disable();
-
- return ret;
+ return __intel_idle(dev, drv, index, false);
}
static __cpuidle int intel_idle_ibrs(struct cpuidle_device *dev,
@@ -184,7 +179,7 @@ static __cpuidle int intel_idle_ibrs(struct cpuidle_device *dev,
if (smt_active)
__update_spec_ctrl(0);
- ret = __intel_idle(dev, drv, index);
+ ret = __intel_idle(dev, drv, index, true);
if (smt_active)
__update_spec_ctrl(spec_ctrl);
@@ -196,7 +191,7 @@ static __cpuidle int intel_idle_xstate(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int index)
{
fpu_idle_fpregs();
- return __intel_idle(dev, drv, index);
+ return __intel_idle(dev, drv, index, true);
}
/**