[PATCH 0/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA

Deepak Gupta posted 1 patch 6 days, 14 hours ago
There is a newer version of this series
target/arm/kvm.c        | 164 +++++++++++++++++++++++++++++++++++-----
target/arm/trace-events |   1 +
2 files changed, 146 insertions(+), 19 deletions(-)
[PATCH 0/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA
Posted by Deepak Gupta 6 days, 14 hours ago
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
[PATCH v2 0/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA
Posted by Deepak Gupta 8 hours ago
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.

Changes in v2 (review feedback received internally):
 * Do not enable the capability for VMs that expose EL2 (nested
   virtualization): the injection always targets EL1, which is wrong
   for a guest at EL2 or with HCR_EL2.TGE set. Such VMs keep KVM's
   SError. The enable moved from kvm_arch_init() to
   kvm_arch_init_vcpu(), where has_el2 is known; the cap is per VM and
   enabling it repeatedly is harmless.
 * The GED-notified GHES source exists for virt machine versions after
   10.1, not 10.0 (hw_compat_10_1 clears x-has-hest-addr). Comment and
   commit message corrected, virt-10.1 added to the tests.
 * Document that BUS_MCEERR_AO delivery is best effort: a record is
   dropped with a warning while the guest has not acknowledged the
   previous one.
 * Clarify the kvm_inject_arm_sea() comment on the EC variant choice.

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.1 (the AR path still delivers the synchronous record, the AO
record is dropped with the warning), check-unit on the host. The EL2
skip is compile-tested only: the test host has no KVM nested support.

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

v1: https://lore.kernel.org/qemu-devel/cover.1792191544.git.deepagupta@nvidia.com/

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

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

-- 
2.53.0
[PATCH v2 1/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA and report BUS_MCEERR_AO
Posted by Deepak Gupta 8 hours ago
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.1 have no such source; warn and drop the record there.
   Delivery is best effort: a record is also dropped, with a warning,
   while the guest has not acknowledged the previous one.

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. The injection targets EL1, so
the capability is not enabled for VMs that expose EL2: a guest running
at EL2, or with HCR_EL2.TGE set, keeps KVM's SError until the injection
learns to target EL2. 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>
---
v2: do not enable the cap for VMs that expose EL2 (enable moved to
    kvm_arch_init_vcpu()); "after 10.1" not "10.0" for the GED-notified
    GHES source; document best-effort BUS_MCEERR_AO delivery; clarify
    the kvm_inject_arm_sea() comment.

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

diff --git a/target/arm/kvm.c b/target/arm/kvm.c
index 3d905f8565..63d67511fb 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)
@@ -1599,6 +1602,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,
@@ -2047,6 +2054,27 @@ int kvm_arch_init_vcpu(CPUState *cs)
         cpu->kvm_init_features[0] |= 1 << KVM_ARM_VCPU_HAS_EL2;
     }
 
+    /*
+     * 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().
+     *
+     * Not when the VM exposes EL2: kvm_inject_arm_sea() delivers the abort
+     * to EL1, which is wrong for a guest running at EL2 or with HCR_EL2.TGE
+     * set. Such VMs keep KVM's SError until the injection learns to
+     * target EL2.
+     */
+    if (!cpu->has_el2 &&
+        kvm_check_extension(cs->kvm_state, KVM_CAP_ARM_SEA_TO_USER)) {
+        if (kvm_vm_enable_cap(cs->kvm_state, KVM_CAP_ARM_SEA_TO_USER, 0)) {
+            warn_report_once("Failed to enable KVM_CAP_ARM_SEA_TO_USER cap");
+        }
+    }
+
     /* Do KVM_ARM_VCPU_INIT ioctl */
     ret = kvm_arm_vcpu_init(cpu);
     if (ret) {
@@ -2106,29 +2134,109 @@ 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 EC variant is chosen here: the same-EL one when the
+ *       guest is already at the target EL, the lower-EL one otherwise.
+ *       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 +2633,49 @@ 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.1); 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.
+             *
+             * Delivery is best effort: if the guest has not acknowledged
+             * the previous record on that source yet, this one is dropped
+             * with a warning rather than queued.
+             */
+            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
Re: [PATCH v2 1/1] target/arm/kvm: handle KVM_EXIT_ARM_SEA and report BUS_MCEERR_AO
Posted by Peter Maydell an hour ago
On Fri, 23 Oct 2026 at 06:22, Deepak Gupta <deepagupta@nvidia.com> wrote:
>
> 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.

So we already have a mechanism for the kernel to tell us about
memory errors -- the BUS_MCEERR_AR SIGBUS code path. Why do
we need to have a second mechanism that needs more code in QEMU
to wire up to have the same effect? Can't the kernel tell us
about memory errors in just one way?

-- PMM