From: Sasha Levin <sashal@kernel.org>
To: stable@vger.kernel.org
Cc: EJ Campbell <ejc3@meta.com>, Paolo Bonzini <pbonzini@redhat.com>,
Sasha Levin <sashal@kernel.org>
Subject: [PATCH 5.15.y 3/3] x86/kvm: Don't consume async #PF flags on a kernel #PF in NMI context
Date: Thu, 22 Oct 2026 20:28:51 -0400 [thread overview]
Message-ID: <20261023002045.2270144-3-sashal@kernel.org> (raw)
In-Reply-To: <20261023002045.2270144-1-sashal@kernel.org>
From: EJ Campbell <ejc3@meta.com>
[ Upstream commit 81d99abe2bd81379abfb595e403bbbee6fbfc624 ]
An async #PF "page not present" event arrives through the #PF vector,
and its reason sits in the per-CPU apf_reason.flags until
__kvm_handle_async_pf() reads and clears it. If an NMI arrives after
the event is injected and before the #PF handler reads the flags, and
the NMI handler itself takes a #PF, that fault reads and clears the
flags belonging to the #PF it interrupted. It then finds interrupts
disabled in the NMI context it interrupted, and panics:
Kernel panic - not syncing: Host injected async #PF in interrupt
disabled region
Call Trace:
<NMI>
panic+0x52/0x60
__kvm_handle_async_pf+0xb0/0xb0
exc_page_fault+0x71/0xb0
asm_exc_page_fault+0x27/0x30
RIP: 0010:__get_user_nocheck_8+0x6/0x20
perf_callchain_user+0x147/0x1a0
get_perf_callchain+0x12d/0x1c0
...
perf_event_nmi_handler+0x26/0x50
exc_nmi+0xe8/0x110
RIP: 0010:asm_exc_page_fault+0x0/0x30
</NMI>
The last RIP line shows the NMI landing on the first instruction of
the #PF entry. perf with user call chains takes such a fault: the
sampling NMI walks the user stack with __get_user(), which faults when
a saved frame pointer is not a valid user address or the walk runs off
the top of the stack. The same window exists when KVM, running as an
L1 hypervisor with DELIVERY_AS_PF_VMEXIT, reads the flags itself after
a #PF VM-exit.
The NMI entry code saves and restores CR2 for this nesting, but not the
async #PF reason. Andy Lutomirski described the race, with perf as the
example, when async #PF delivery was reworked in 2020 [1]. Saving and
clearing the flags across the NMI, next to CR2, was discussed there but
not merged.
Since that rework, Linux has stopped setting KVM_ASYNC_PF_SEND_ALWAYS,
so the host delivers "page not present" only for user mode faults and
a kernel-mode fault in NMI context (including kernel-mode #MC and #DB)
cannot be an async page fault. In that case, return without touching
the flags so that the interrupted handler finds them intact.
Tested on v7.3-rc5 in a 4-vCPU, 2 GiB guest restored from a snapshot
whose memory is served on demand through userfaultfd, so that reading
it back takes async #PFs, while every thread samples hardware cycles
with user call chains and points its frame pointer at an unmapped page.
Without this patch the guest panicked as above in 3 of 3 runs. With it,
the guest survived 3 of 3 runs of 60 seconds, and the host counted
148,441 to 153,334 kvm_async_pf_not_present events in the first 20
seconds of each. On 6.18.50 the results were the same: the guest
panicked without the patch and survived with it.
Link: https://lore.kernel.org/all/ed71d0967113a35f670a9625a058b8e6e0b2f104.1583547991.git.luto@kernel.org/ [1]
Link: https://lore.kernel.org/all/20211126123145.2772-1-jiangshanlai@gmail.com/ [2]
Cc: stable@vger.kernel.org # v5.9+
Signed-off-by: EJ Campbell <ejc3@meta.com>
Link: https://patch.msgid.link/CADkxfh-uQTca_6jB1JPDq1MsKffF_UKGQR339oQSJNcoV75GNQ@mail.gmail.com
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
arch/x86/kernel/kvm.c | 32 +++++++++++++++++++++++++++++---
1 file changed, 29 insertions(+), 3 deletions(-)
diff --git a/arch/x86/kernel/kvm.c b/arch/x86/kernel/kvm.c
index 8d34f8ceca015..04b7123d3270b 100644
--- a/arch/x86/kernel/kvm.c
+++ b/arch/x86/kernel/kvm.c
@@ -252,11 +252,39 @@ noinstr u32 kvm_read_and_reset_apf_flags(void)
}
EXPORT_SYMBOL_GPL(kvm_read_and_reset_apf_flags);
+static __always_inline bool kvm_apf_reason_pending(void)
+{
+ return __this_cpu_read(async_pf_enabled) &&
+ __this_cpu_read(apf_reason.flags);
+}
+
noinstr bool __kvm_handle_async_pf(struct pt_regs *regs, u32 token)
{
- u32 flags = kvm_read_and_reset_apf_flags();
irqentry_state_t state;
+ u32 flags;
+
+ if (!kvm_apf_reason_pending())
+ return false;
+
+ /*
+ * The guest does not enable KVM_ASYNC_PF_SEND_ALWAYS, so the host
+ * delivers async #PF only to user mode (or as a #PF VM-exit, which KVM
+ * reads itself). A #PF from kernel mode can still arrive between that
+ * delivery and the read of the reason flags, inside an NMI that
+ * interrupted it, for instance perf reading user memory for a call
+ * chain. Such a fault is not async, so leave the async PF flags
+ * untouched for the outer handler. Outside NMI context, on the other
+ * hand, async PF flags on a kernel-mode fault mean the host is broken.
+ */
+ if (!user_mode(regs)) {
+ if (likely(in_nmi()))
+ return false;
+ state = irqentry_enter(regs);
+ instrumentation_begin();
+ panic("Host injected async #PF in kernel mode\n");
+ }
+ flags = kvm_read_and_reset_apf_flags();
if (!flags)
return false;
@@ -272,8 +300,6 @@ noinstr bool __kvm_handle_async_pf(struct pt_regs *regs, u32 token)
panic("Host injected async #PF in interrupt disabled region\n");
if (flags & KVM_PV_REASON_PAGE_NOT_PRESENT) {
- if (unlikely(!(user_mode(regs))))
- panic("Host injected async #PF in kernel mode\n");
/* Page is swapped out by the host. */
kvm_async_pf_task_wait_schedule(token);
} else {
--
2.53.0
prev parent reply other threads:[~2026-10-23 0:28 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 18:50 FAILED: patch "[PATCH] x86/kvm: Don't consume async #PF flags on a kernel #PF in NMI" failed to apply to 5.15-stable tree gregkh
2026-10-23 0:28 ` [PATCH 5.15.y 1/3] x86/kvm: Silence per-cpu pr_info noise about KVM clocks and steal time Sasha Levin
2026-10-23 0:28 ` [PATCH 5.15.y 2/3] x86/kvm: Use separate percpu variable to track the enabling of asyncpf Sasha Levin
2026-10-23 0:28 ` Sasha Levin [this message]
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=20261023002045.2270144-3-sashal@kernel.org \
--to=sashal@kernel.org \
--cc=ejc3@meta.com \
--cc=pbonzini@redhat.com \
--cc=stable@vger.kernel.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.