Kernel KVM virtualization development
 help / color / mirror / Atom feed
* [PATCH 0/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA
@ 2026-10-16 23:05 Deepak Gupta
  2026-10-16 23:05 ` [PATCH 1/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA and report BUS_MCEERR_AO Deepak Gupta
  0 siblings, 1 reply; 2+ messages in thread
From: Deepak Gupta @ 2026-10-16 23:05 UTC (permalink / raw)
  To: qemu-devel
  Cc: qemu-arm, kvm, kvmarm, peter.maydell, pbonzini, mchehab+huawei,
	jiaqiyan, oliver.upton

Hi,

Linux 6.19 added KVM_CAP_ARM_SEA_TO_USER [1]: with the capability
enabled, KVM returns a guest synchronous external abort that the host
could not claim through APEI to userspace as KVM_EXIT_ARM_SEA, instead
of injecting a virtual SError. QEMU does not consume the exit yet, so on
a host whose HEST has no SEA-notified GHES source every guest-consumed
memory error still ends in a guest "Asynchronous SError Interrupt"
panic.

This patch enables the capability, handles the exit by recording a GHES
memory error (when the exit carries an address backed by guest RAM) and
injecting a synchronous external abort that mirrors the original one,
and wires up the BUS_MCEERR_AO SIGBUS case so the guest learns about the
poisoned page asynchronously when the exit itself carried no address.

Points I would like feedback on:

 * Injection: I reuse the existing kvm_inject_arm_sea() route
   (arm_cpu_do_interrupt() after kvm_cpu_synchronize_state()) and let it
   pass the sanitised ESR fields (EC, IL, FnV, EA, CM, WnR, xFSC, SET)
   and the FAR through to the guest. KVM_SET_VCPU_EVENTS with
   ext_dabt_pending can only produce a data abort with FAR taken from
   the last exit, and the documentation restricts it to MMIO/NISV/
   LDST64B exits, so it did not seem a good fit; happy to switch if that
   is preferred.

 * The BUS_MCEERR_AO record goes on ACPI_HEST_SRC_ID_QMP, which ghes.h
   documents as being for QMP-injected errors, because it is the only
   GED-notified source the virt machine has. If a dedicated source id
   for host-originated asynchronous records is preferred I can add one.

 * With the pre-10.1 single-block HEST layout (no GED-notified source)
   the AO record is dropped with a warning rather than written into the
   synchronous source's block.

Testing: arm64 host whose HEST has only GSIV/SDEI-notified sources,
EINJ uncorrectable memory error into a page mapped by a guest, consumed
by a guest process (-machine virt,ras=on). Before: guest panic. After:
SIGBUS to the process, GHES record and hwpoison in the guest, guest
stays up. Also checked: the pre-existing BUS_MCEERR_AR SIGBUS path still
works (it is taken when the host's memory_failure() wins the race),
ras=off (bare external abort to the guest, no record), -machine
virt-10.0 (the AR path still delivers the synchronous record, the AO
record is dropped with the warning),
check-unit and check-qtest-aarch64 on the host.

[1] https://lore.kernel.org/r/20251013185903.1372553-1-jiaqiyan@google.com


Deepak Gupta (1):
  target/arm/kvm: handle KVM_EXIT_ARM_SEA and report BUS_MCEERR_AO

 target/arm/kvm.c        | 164 +++++++++++++++++++++++++++++++++++-----
 target/arm/trace-events |   1 +
 2 files changed, 146 insertions(+), 19 deletions(-)

-- 
2.53.0


^ permalink raw reply	[flat|nested] 2+ messages in thread

* [PATCH 1/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA and report BUS_MCEERR_AO
  2026-10-16 23:05 [PATCH 0/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA Deepak Gupta
@ 2026-10-16 23:05 ` Deepak Gupta
  0 siblings, 0 replies; 2+ messages in thread
From: Deepak Gupta @ 2026-10-16 23:05 UTC (permalink / raw)
  To: qemu-devel
  Cc: qemu-arm, kvm, kvmarm, peter.maydell, pbonzini, mchehab+huawei,
	jiaqiyan, oliver.upton

Linux 6.19 added KVM_CAP_ARM_SEA_TO_USER: a guest synchronous external
abort (SEA) that the host cannot attribute to a firmware-first (APEI)
error record is returned to userspace as KVM_EXIT_ARM_SEA instead of
being injected as a virtual SError. Linux guests treat that SError as
fatal, so on a host whose HEST has no SEA-notified GHES source every
guest-consumed memory error currently panics the guest.

Enable the capability when KVM offers it and handle the exit:

 - If the exit carries a guest physical address backed by guest RAM,
   mark the page for remapping on reset and record a memory error on
   the synchronous GHES source, as the BUS_MCEERR_AR SIGBUS path does.

 - Inject a synchronous external abort into the guest. KVM hands us
   ESR_EL2 sanitised to EC, IL, FnV, EA, CM, WnR, xFSC and SET, and
   FAR_EL2 when FnV is clear. Generalise kvm_inject_arm_sea() to take
   syndrome and FAR and pass them through, so the guest sees an
   instruction or data abort with the real fault status code and a
   valid FAR when the hardware provided one. The SIGBUS path keeps
   injecting the data abort with FnV set.

 - The exit may carry no address at all (FnV set, no HPFAR_EL2). The
   host's memory_failure() tells us the page later via BUS_MCEERR_AO,
   so far only acknowledged with a TODO: report it on the GED-notified
   GHES source so the guest offlines the page too. Machine types up to
   virt-10.0 have no such source; warn and drop the record there.

The exception is emulated with arm_cpu_do_interrupt() rather than via
KVM_SET_VCPU_EVENTS' ext_dabt_pending, which can only produce a data
abort with FAR taken from the last exit, while the exit may be an
instruction abort or have no valid FAR. With ras=off the guest only
gets the external abort, still better than the SError. A host without
the capability is unaffected. A trace event records every exit.

Tested on an arm64 host whose HEST has only GSIV- and SDEI-notified
sources, injecting an uncorrectable memory error with EINJ into a page
mapped by a guest and consuming it from a guest process: without this
patch the guest panics with an SError; with it the process gets SIGBUS,
the guest marks the page hwpoison and stays up.

Link: https://lore.kernel.org/r/20251013185903.1372553-2-jiaqiyan@google.com
Signed-off-by: Deepak Gupta <deepagupta@nvidia.com>
---
 target/arm/kvm.c        | 164 +++++++++++++++++++++++++++++++++++-----
 target/arm/trace-events |   1 +
 2 files changed, 146 insertions(+), 19 deletions(-)

diff --git a/target/arm/kvm.c b/target/arm/kvm.c
index 3d905f8565..8f18bee285 100644
--- a/target/arm/kvm.c
+++ b/target/arm/kvm.c
@@ -38,6 +38,7 @@
 #include "hw/core/irq.h"
 #include "qapi/visitor.h"
 #include "qemu/log.h"
+#include "qemu/rcu.h"
 #include "hw/acpi/acpi.h"
 #include "hw/acpi/ghes.h"
 #include "target/arm/gtimer.h"
@@ -52,6 +53,8 @@ static bool cap_has_mp_state;
 static bool cap_has_inject_serror_esr;
 static bool cap_has_inject_ext_dabt;
 
+static int kvm_arm_handle_sea(ARMCPU *cpu, struct kvm_run *run);
+
 /**
  * ARMHostCPUFeatures: information about the host CPU (identified
  * by asking the host kernel)
@@ -674,6 +677,21 @@ int kvm_arch_init(MachineState *ms, KVMState *s)
         }
     }
 
+    /*
+     * Ask KVM to return a guest synchronous external abort that the host
+     * could not attribute to a firmware-first (APEI) error record as
+     * KVM_EXIT_ARM_SEA, instead of injecting an SError with an
+     * IMPLEMENTATION DEFINED syndrome into the guest. Linux guests treat
+     * that SError as fatal, so on a host whose HEST has no SEA-notified
+     * GHES source every guest-consumed memory error would otherwise be a
+     * guest kernel panic. See kvm_arm_handle_sea().
+     */
+    if (kvm_check_extension(s, KVM_CAP_ARM_SEA_TO_USER)) {
+        if (kvm_vm_enable_cap(s, KVM_CAP_ARM_SEA_TO_USER, 0)) {
+            warn_report("Failed to enable KVM_CAP_ARM_SEA_TO_USER cap");
+        }
+    }
+
     if (s->kvm_eager_split_size) {
         uint32_t sizes;
 
@@ -1599,6 +1617,10 @@ int kvm_arch_handle_exit(CPUState *cs, struct kvm_run *run)
             ret = EXCP_DEBUG;
         } /* otherwise return to guest */
         break;
+    case KVM_EXIT_ARM_SEA:
+        /* Guest SEA that the host could not claim through APEI */
+        ret = kvm_arm_handle_sea(cpu, run);
+        break;
     case KVM_EXIT_ARM_NISV:
         /* External DABT with no valid iss to decode */
         ret = kvm_arm_handle_dabt_nisv(cpu, run->arm_nisv.esr_iss,
@@ -2106,29 +2128,107 @@ int kvm_arch_destroy_vcpu(CPUState *cs)
     return 0;
 }
 
-/* Callers must hold the iothread mutex lock */
-static void kvm_inject_arm_sea(CPUState *c)
+/*
+ * Inject a synchronous external abort into the guest, targeting EL1.
+ *
+ * @esr: the syndrome to present. Its EC selects an instruction or a data
+ *       abort (the same-EL variant is chosen here); IL and the ISS bits
+ *       (FnV, EA, CM, WnR, xFSC, SET) are passed through to the guest.
+ * @far: the faulting virtual address, only meaningful when ISS.FnV is 0.
+ *
+ * Callers must hold the BQL.
+ */
+static void kvm_inject_arm_sea(CPUState *c, uint32_t esr, uint64_t far)
 {
     ARMCPU *cpu = ARM_CPU(c);
     CPUARMState *env = &cpu->env;
-    uint32_t esr;
+    uint32_t ec = syn_get_ec(esr);
+    bool iabt = ec == EC_INSNABORT || ec == EC_INSNABORT_SAME_EL;
     bool same_el;
 
-    c->exception_index = EXCP_DATA_ABORT;
+    c->exception_index = iabt ? EXCP_PREFETCH_ABORT : EXCP_DATA_ABORT;
     env->exception.target_el = 1;
+    env->exception.vaddress = far;
 
-    /*
-     * Set the DFSC to synchronous external abort and set FnV to not valid,
-     * this will tell guest the FAR_ELx is UNKNOWN for this abort.
-     */
     same_el = arm_current_el(env) == env->exception.target_el;
-    esr = syn_data_abort_no_iss(same_el, 1, 0, 0, 0, 0, 0x10);
-
-    env->exception.syndrome = esr;
+    ec = (iabt ? EC_INSNABORT : EC_DATAABORT) + same_el;
+    env->exception.syndrome = syn_set_ec(esr, ec);
 
     arm_cpu_do_interrupt(c);
 }
 
+/*
+ * Handle KVM_EXIT_ARM_SEA: the guest took a synchronous external abort,
+ * typically by consuming a poisoned memory location, and the host kernel
+ * found no APEI error record to claim it with. Without
+ * KVM_CAP_ARM_SEA_TO_USER, KVM would inject a virtual SError here, which a
+ * Linux guest treats as fatal.
+ *
+ * Do what the BUS_MCEERR_AR SIGBUS path does: when the exit carries a guest
+ * physical address that is backed by guest RAM, remember the page so it is
+ * remapped on reset and record a memory error on the synchronous GHES
+ * source (if the machine has one, i.e. ras=on), so the guest's
+ * memory_failure() can isolate the page and kill only the affected task.
+ * Then inject a synchronous external abort that reflects the original one:
+ * the kernel hands us ESR_EL2 sanitised to EC, IL, FnV, EA, CM, WnR, xFSC
+ * and SET, which are exactly the fields that are meaningful at EL1, and the
+ * faulting virtual address when FnV is clear.
+ *
+ * kvm_arch_handle_exit() runs without the BQL; both the GHES update and
+ * arm_cpu_do_interrupt() need it.
+ */
+static int kvm_arm_handle_sea(ARMCPU *cpu, struct kvm_run *run)
+{
+    CPUState *cs = CPU(cpu);
+    uint64_t flags = run->arm_sea.flags;
+    uint64_t esr = run->arm_sea.esr;
+    uint64_t gva = run->arm_sea.gva;
+    hwaddr gpa = run->arm_sea.gpa;
+    ram_addr_t ram_addr = RAM_ADDR_INVALID;
+    AcpiGhesState *ags;
+
+    trace_kvm_arm_handle_sea(flags, esr, gva, gpa);
+
+    if (flags & KVM_EXIT_ARM_SEA_FLAG_GPA_VALID) {
+        MemoryRegion *mr;
+        hwaddr xlat, len = 1;
+
+        WITH_RCU_READ_LOCK_GUARD() {
+            mr = address_space_translate(&address_space_memory, gpa, &xlat,
+                                         &len, false, MEMTXATTRS_UNSPECIFIED);
+            if (memory_region_is_ram(mr)) {
+                ram_addr = memory_region_get_ram_addr(mr) + xlat;
+            }
+        }
+    }
+
+    bql_lock();
+    kvm_cpu_synchronize_state(cs);
+
+    ags = acpi_ghes_get_state();
+    if (ram_addr != RAM_ADDR_INVALID) {
+        kvm_hwpoison_page_add(ram_addr);
+        if (ags) {
+            Error *err = NULL;
+
+            if (!acpi_ghes_memory_errors(ags, ACPI_HEST_SRC_ID_SYNC, gpa,
+                                         &err)) {
+                warn_reportf_err(err, "KVM_EXIT_ARM_SEA at GPA 0x%" HWADDR_PRIx
+                                 ": ", gpa);
+            }
+        } else {
+            warn_report_once("KVM_EXIT_ARM_SEA at GPA 0x%" HWADDR_PRIx
+                             " but the machine has no GHES (ras=off): the "
+                             "guest only gets an external abort", gpa);
+        }
+    }
+
+    kvm_inject_arm_sea(cs, esr, gva);
+    bql_unlock();
+
+    return 0;
+}
+
 #define AARCH64_CORE_REG(x)   (KVM_REG_ARM64 | KVM_REG_SIZE_U64 | \
                  KVM_REG_ARM_CORE | KVM_REG_ARM_CORE_REG(x))
 
@@ -2525,19 +2625,45 @@ void kvm_arch_on_sigbus_vcpu(CPUState *c, int code, void *addr)
             /*
              * If this is a BUS_MCEERR_AR, we know we have been called
              * synchronously from the vCPU thread, so we can easily
-             * synchronize the state and inject an error.
-             *
-             * TODO: we currently don't tell the guest at all about
-             * BUS_MCEERR_AO. In that case we might either be being
-             * called synchronously from the vCPU thread, or a bit
-             * later from the main thread, so doing the injection of
-             * the error would be more complicated.
+             * synchronize the state and inject an error: a data abort
+             * with DFSC = synchronous external abort and FnV set, which
+             * tells the guest that FAR_ELx is UNKNOWN for this abort.
              */
             if (code == BUS_MCEERR_AR) {
                 kvm_cpu_synchronize_state(c);
                 acpi_ghes_memory_errors(ags, ACPI_HEST_SRC_ID_SYNC,
                                         paddr, &error_fatal);
-                kvm_inject_arm_sea(c);
+                kvm_inject_arm_sea(c, syn_data_abort_no_iss(0, 1, 0, 0, 0, 0,
+                                                            0x10), 0);
+                return;
+            }
+
+            /*
+             * BUS_MCEERR_AO: the host has retired a page of guest RAM that
+             * no vCPU was accessing at the time. We may be on a vCPU thread
+             * or on the main thread, in both cases under the BQL, so there
+             * is no vCPU to inject into. Instead record an action-optional
+             * memory error on the GED-notified GHES source, so the guest
+             * can offline the page as well. That source only exists with
+             * the HEST layout that reports its address to QEMU (virt
+             * machine versions after 10.0); with the older single-block
+             * layout the record would land in the synchronous source's
+             * block, unnoticed by the guest, and block the next
+             * synchronous record.
+             */
+            if (!ags->use_hest_addr) {
+                warn_report_once("BUS_MCEERR_AO at GPA 0x%" HWADDR_PRIx
+                                 ": this machine type has no GED-notified "
+                                 "GHES source, the guest is not told",
+                                 paddr);
+            } else {
+                Error *err = NULL;
+
+                if (!acpi_ghes_memory_errors(ags, ACPI_HEST_SRC_ID_QMP,
+                                             paddr, &err)) {
+                    warn_reportf_err(err, "BUS_MCEERR_AO at GPA 0x%"
+                                     HWADDR_PRIx ": ", paddr);
+                }
             }
             return;
         }
diff --git a/target/arm/trace-events b/target/arm/trace-events
index 8502fb3265..5c78d1255c 100644
--- a/target/arm/trace-events
+++ b/target/arm/trace-events
@@ -13,6 +13,7 @@ arm_gt_update_irq(int timer, int irqstate) "gt_update_irq: timer %d irqstate %d"
 
 # kvm.c
 kvm_arm_fixup_msi_route(uint64_t iova, uint64_t gpa) "MSI iova = 0x%"PRIx64" is translated into 0x%"PRIx64
+kvm_arm_handle_sea(uint64_t flags, uint64_t esr, uint64_t gva, uint64_t gpa) "KVM_EXIT_ARM_SEA flags 0x%"PRIx64" esr 0x%"PRIx64" gva 0x%"PRIx64" gpa 0x%"PRIx64
 
 # cpu.c
 arm_cpu_reset(uint64_t mp_aff) "cpu %" PRIu64
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-10-16 23:05 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-16 23:05 [PATCH 0/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA Deepak Gupta
2026-10-16 23:05 ` [PATCH 1/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA and report BUS_MCEERR_AO Deepak Gupta

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox