[PATCH v6 6/6] target/arm/hvf, whpx: wire ISV=0 emulation for data aborts

Lucas Amaral posted 6 patches 1 day, 19 hours ago
Maintainers: Peter Maydell <peter.maydell@linaro.org>, Alexander Graf <agraf@csgraf.de>, Pedro Barbuda <pbarbuda@microsoft.com>, Mohamed Mediouni <mohamed@unpredictable.fr>
[PATCH v6 6/6] target/arm/hvf, whpx: wire ISV=0 emulation for data aborts
Posted by Lucas Amaral 1 day, 19 hours ago
When a data abort with ISV=0 occurs during MMIO emulation, the
syndrome register does not carry the access size or target register.
Previously this hit an assert(isv) and killed the VM.

Replace the assert with instruction fetch + decode + emulate using the
shared library in target/arm/emulate/.  The faulting instruction is read
from guest memory via cpu_memory_rw_debug(), decoded by the decodetree-
generated decoder, and emulated against the vCPU register file.

Both HVF (macOS) and WHPX (Windows Hyper-V) use the same pattern:
  1. cpu_synchronize_state() to flush hypervisor registers
  2. Fetch 4-byte instruction at env->pc
  3. arm_emul_insn(env, insn)
  4. On success, advance PC past the emulated instruction

If the instruction is unhandled or a memory error occurs, a synchronous
external abort is injected into the guest via syn_data_abort_no_iss()
with fnv=1 and fsc=0x10, matching the syndrome that KVM uses in
kvm_inject_arm_sea().  The guest kernel's fault handler then reports
the error through its normal data abort path.

WHPX adds a whpx_inject_data_abort() helper and adjusts the
whpx_handle_mmio() return convention so the caller skips PC advancement
when an exception has been injected.

Signed-off-by: Lucas Amaral <lucaaamaral@gmail.com>
---
 target/arm/hvf/hvf.c       | 46 ++++++++++++++++++++++++++--
 target/arm/whpx/whpx-all.c | 61 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 103 insertions(+), 4 deletions(-)

diff --git a/target/arm/hvf/hvf.c b/target/arm/hvf/hvf.c
index 5fc8f6bb..000e54bd 100644
--- a/target/arm/hvf/hvf.c
+++ b/target/arm/hvf/hvf.c
@@ -32,6 +32,7 @@
 #include "arm-powerctl.h"
 #include "target/arm/cpu.h"
 #include "target/arm/internals.h"
+#include "emulate/arm_emulate.h"
 #include "target/arm/multiprocessing.h"
 #include "target/arm/gtimer.h"
 #include "target/arm/trace.h"
@@ -2175,10 +2176,49 @@ static int hvf_handle_exception(CPUState *cpu, hv_vcpu_exit_exception_t *excp)
         assert(!s1ptw);
 
         /*
-         * TODO: ISV will be 0 for SIMD or SVE accesses.
-         * Inject the exception into the guest.
+         * ISV=0: syndrome doesn't carry access size/register info.
+         * Fetch and emulate via target/arm/emulate/.
          */
-        assert(isv);
+        if (!isv) {
+            ARMCPU *arm_cpu = ARM_CPU(cpu);
+            CPUARMState *env = &arm_cpu->env;
+            uint32_t insn;
+            ArmEmulResult r;
+
+            cpu_synchronize_state(cpu);
+
+            if (cpu_memory_rw_debug(cpu, env->pc,
+                                    (uint8_t *)&insn, 4, false) != 0) {
+                bool same_el = arm_current_el(env) == 1;
+                uint32_t esr = syn_data_abort_no_iss(same_el,
+                    1, 0, 0, 0, iswrite, 0x10);
+
+                error_report("HVF: cannot read insn at pc=0x%" PRIx64,
+                             (uint64_t)env->pc);
+                env->exception.vaddress = excp->virtual_address;
+                hvf_raise_exception(cpu, EXCP_DATA_ABORT, esr, 1);
+                break;
+            }
+
+            r = arm_emul_insn(env, insn);
+            if (r == ARM_EMUL_UNHANDLED || r == ARM_EMUL_ERR_MEM) {
+                bool same_el = arm_current_el(env) == 1;
+                uint32_t esr = syn_data_abort_no_iss(same_el,
+                    1, 0, 0, 0, iswrite, 0x10);
+
+                error_report("HVF: ISV=0 %s insn 0x%08x at "
+                             "pc=0x%" PRIx64 ", injecting data abort",
+                             r == ARM_EMUL_UNHANDLED ? "unhandled"
+                                                     : "memory error",
+                             insn, (uint64_t)env->pc);
+                env->exception.vaddress = excp->virtual_address;
+                hvf_raise_exception(cpu, EXCP_DATA_ABORT, esr, 1);
+                break;
+            }
+
+            advance_pc = true;
+            break;
+        }
 
         /*
          * Emulate MMIO.
diff --git a/target/arm/whpx/whpx-all.c b/target/arm/whpx/whpx-all.c
index 513551be..0c04073e 100644
--- a/target/arm/whpx/whpx-all.c
+++ b/target/arm/whpx/whpx-all.c
@@ -29,6 +29,7 @@
 #include "syndrome.h"
 #include "target/arm/cpregs.h"
 #include "internals.h"
+#include "emulate/arm_emulate.h"
 
 #include "system/whpx-internal.h"
 #include "system/whpx-accel-ops.h"
@@ -352,6 +353,27 @@ static void whpx_set_gp_reg(CPUState *cpu, int rt, uint64_t val)
     whpx_set_reg(cpu, reg, reg_val);
 }
 
+/*
+ * Inject a synchronous external abort (data abort) into the guest.
+ * Used when ISV=0 instruction emulation fails.  Matches the syndrome
+ * that KVM uses in kvm_inject_arm_sea().
+ */
+static void whpx_inject_data_abort(CPUState *cpu, bool iswrite)
+{
+    ARMCPU *arm_cpu = ARM_CPU(cpu);
+    CPUARMState *env = &arm_cpu->env;
+    bool same_el = arm_current_el(env) == 1;
+    uint32_t esr = syn_data_abort_no_iss(same_el, 1, 0, 0, 0, iswrite, 0x10);
+
+    cpu->exception_index = EXCP_DATA_ABORT;
+    env->exception.target_el = 1;
+    env->exception.syndrome = esr;
+
+    bql_lock();
+    arm_cpu_do_interrupt(cpu);
+    bql_unlock();
+}
+
 static int whpx_handle_mmio(CPUState *cpu, WHV_MEMORY_ACCESS_CONTEXT *ctx)
 {
     uint64_t syndrome = ctx->Syndrome;
@@ -366,7 +388,40 @@ static int whpx_handle_mmio(CPUState *cpu, WHV_MEMORY_ACCESS_CONTEXT *ctx)
     uint64_t val = 0;
 
     assert(!cm);
-    assert(isv);
+
+    /*
+     * ISV=0: syndrome doesn't carry access size/register info.
+     * Fetch and decode the faulting instruction via the emulation library.
+     */
+    if (!isv) {
+        ARMCPU *arm_cpu = ARM_CPU(cpu);
+        CPUARMState *env = &arm_cpu->env;
+        uint32_t insn;
+        ArmEmulResult r;
+
+        cpu_synchronize_state(cpu);
+
+        if (cpu_memory_rw_debug(cpu, env->pc,
+                                (uint8_t *)&insn, 4, false) != 0) {
+            error_report("WHPX: cannot read insn at pc=0x%" PRIx64,
+                         (uint64_t)env->pc);
+            whpx_inject_data_abort(cpu, iswrite);
+            return 1;
+        }
+
+        r = arm_emul_insn(env, insn);
+        if (r == ARM_EMUL_UNHANDLED || r == ARM_EMUL_ERR_MEM) {
+            error_report("WHPX: ISV=0 %s insn 0x%08x at "
+                         "pc=0x%" PRIx64 ", injecting data abort",
+                         r == ARM_EMUL_UNHANDLED ? "unhandled"
+                                                 : "memory error",
+                         insn, (uint64_t)env->pc);
+            whpx_inject_data_abort(cpu, iswrite);
+            return 1;
+        }
+
+        return 0;
+    }
 
     if (iswrite) {
         val = whpx_get_gp_reg(cpu, srt);
@@ -451,6 +506,10 @@ int whpx_vcpu_run(CPUState *cpu)
             }
 
             ret = whpx_handle_mmio(cpu, &vcpu->exit_ctx.MemoryAccess);
+            if (ret > 0) {
+                advance_pc = false;
+                ret = 0;
+            }
             break;
         case WHvRunVpExitReasonCanceled:
             cpu->exception_index = EXCP_INTERRUPT;
-- 
2.52.0
Re: [PATCH v6 6/6] target/arm/hvf, whpx: wire ISV=0 emulation for data aborts
Posted by Mohamed Mediouni via qemu development 1 day, 18 hours ago

> On 10. Apr 2026, at 00:06, Lucas Amaral <lucaaamaral@gmail.com> wrote:
> 
> When a data abort with ISV=0 occurs during MMIO emulation, the
> syndrome register does not carry the access size or target register.
> Previously this hit an assert(isv) and killed the VM.
> 
> Replace the assert with instruction fetch + decode + emulate using the
> shared library in target/arm/emulate/.  The faulting instruction is read
> from guest memory via cpu_memory_rw_debug(), decoded by the decodetree-
> generated decoder, and emulated against the vCPU register file.
> 
> Both HVF (macOS) and WHPX (Windows Hyper-V) use the same pattern:
>  1. cpu_synchronize_state() to flush hypervisor registers
>  2. Fetch 4-byte instruction at env->pc
>  3. arm_emul_insn(env, insn)
>  4. On success, advance PC past the emulated instruction
> 
> If the instruction is unhandled or a memory error occurs, a synchronous
> external abort is injected into the guest via syn_data_abort_no_iss()
> with fnv=1 and fsc=0x10, matching the syndrome that KVM uses in
> kvm_inject_arm_sea().  The guest kernel's fault handler then reports
> the error through its normal data abort path.
> 
> WHPX adds a whpx_inject_data_abort() helper and adjusts the
> whpx_handle_mmio() return convention so the caller skips PC advancement
> when an exception has been injected.
> 
> Signed-off-by: Lucas Amaral <lucaaamaral@gmail.com>
> ---
> target/arm/hvf/hvf.c       | 46 ++++++++++++++++++++++++++--
> target/arm/whpx/whpx-all.c | 61 +++++++++++++++++++++++++++++++++++++-
> 2 files changed, 103 insertions(+), 4 deletions(-)
> 
> diff --git a/target/arm/hvf/hvf.c b/target/arm/hvf/hvf.c
> index 5fc8f6bb..000e54bd 100644
> --- a/target/arm/hvf/hvf.c
> +++ b/target/arm/hvf/hvf.c
> @@ -32,6 +32,7 @@
> #include "arm-powerctl.h"
> #include "target/arm/cpu.h"
> #include "target/arm/internals.h"
> +#include "emulate/arm_emulate.h"
> #include "target/arm/multiprocessing.h"
> #include "target/arm/gtimer.h"
> #include "target/arm/trace.h"
> @@ -2175,10 +2176,49 @@ static int hvf_handle_exception(CPUState *cpu, hv_vcpu_exit_exception_t *excp)
>         assert(!s1ptw);
> 
>         /*
> -         * TODO: ISV will be 0 for SIMD or SVE accesses.
> -         * Inject the exception into the guest.
> +         * ISV=0: syndrome doesn't carry access size/register info.
> +         * Fetch and emulate via target/arm/emulate/.
>          */
> -        assert(isv);
> +        if (!isv) {
> +            ARMCPU *arm_cpu = ARM_CPU(cpu);
> +            CPUARMState *env = &arm_cpu->env;
> +            uint32_t insn;
> +            ArmEmulResult r;
> +
> +            cpu_synchronize_state(cpu);
> +
> +            if (cpu_memory_rw_debug(cpu, env->pc,
> +                                    (uint8_t *)&insn, 4, false) != 0) {
Hello,

Having a version of mem_access exported and used there too to read the instruction
to get rid of cpu_memory_rw_debug there too

(For both WHPX and HVF)

> +                bool same_el = arm_current_el(env) == 1;
> +                uint32_t esr = syn_data_abort_no_iss(same_el,
> +                    1, 0, 0, 0, iswrite, 0x10);
> +
> +                error_report("HVF: cannot read insn at pc=0x%" PRIx64,
> +                             (uint64_t)env->pc);
> +                env->exception.vaddress = excp->virtual_address;
> +                hvf_raise_exception(cpu, EXCP_DATA_ABORT, esr, 1);
> +                break;
> +            }
> +
> +            r = arm_emul_insn(env, insn);
> +            if (r == ARM_EMUL_UNHANDLED || r == ARM_EMUL_ERR_MEM) {
> +                bool same_el = arm_current_el(env) == 1;
> +                uint32_t esr = syn_data_abort_no_iss(same_el,
> +                    1, 0, 0, 0, iswrite, 0x10);
> +
> +                error_report("HVF: ISV=0 %s insn 0x%08x at "
> +                             "pc=0x%" PRIx64 ", injecting data abort",
> +                             r == ARM_EMUL_UNHANDLED ? "unhandled"
> +                                                     : "memory error",
> +                             insn, (uint64_t)env->pc);
> +                env->exception.vaddress = excp->virtual_address;
> +                hvf_raise_exception(cpu, EXCP_DATA_ABORT, esr, 1);
> +                break;
> +            }
> +
> +            advance_pc = true;
> +            break;
> +        }
> 
>         /*
>          * Emulate MMIO.
> diff --git a/target/arm/whpx/whpx-all.c b/target/arm/whpx/whpx-all.c
> index 513551be..0c04073e 100644
> --- a/target/arm/whpx/whpx-all.c
> +++ b/target/arm/whpx/whpx-all.c
> @@ -29,6 +29,7 @@
> #include "syndrome.h"
> #include "target/arm/cpregs.h"
> #include "internals.h"
> +#include "emulate/arm_emulate.h"
> 
> #include "system/whpx-internal.h"
> #include "system/whpx-accel-ops.h"
> @@ -352,6 +353,27 @@ static void whpx_set_gp_reg(CPUState *cpu, int rt, uint64_t val)
>     whpx_set_reg(cpu, reg, reg_val);
> }
> 
> +/*
> + * Inject a synchronous external abort (data abort) into the guest.
> + * Used when ISV=0 instruction emulation fails.  Matches the syndrome
> + * that KVM uses in kvm_inject_arm_sea().
> + */
> +static void whpx_inject_data_abort(CPUState *cpu, bool iswrite)
> +{
> +    ARMCPU *arm_cpu = ARM_CPU(cpu);
> +    CPUARMState *env = &arm_cpu->env;
> +    bool same_el = arm_current_el(env) == 1;
> +    uint32_t esr = syn_data_abort_no_iss(same_el, 1, 0, 0, 0, iswrite, 0x10);
> +
> +    cpu->exception_index = EXCP_DATA_ABORT;
> +    env->exception.target_el = 1;
> +    env->exception.syndrome = esr;
> +
> +    bql_lock();
> +    arm_cpu_do_interrupt(cpu);
> +    bql_unlock();
> +}
> +
> static int whpx_handle_mmio(CPUState *cpu, WHV_MEMORY_ACCESS_CONTEXT *ctx)
> {
>     uint64_t syndrome = ctx->Syndrome;
> @@ -366,7 +388,40 @@ static int whpx_handle_mmio(CPUState *cpu, WHV_MEMORY_ACCESS_CONTEXT *ctx)
>     uint64_t val = 0;
> 
>     assert(!cm);
> -    assert(isv);
> +
> +    /*
> +     * ISV=0: syndrome doesn't carry access size/register info.
> +     * Fetch and decode the faulting instruction via the emulation library.
> +     */
> +    if (!isv) {
> +        ARMCPU *arm_cpu = ARM_CPU(cpu);
> +        CPUARMState *env = &arm_cpu->env;
> +        uint32_t insn;
> +        ArmEmulResult r;
> +
> +        cpu_synchronize_state(cpu);
> +
> +        if (cpu_memory_rw_debug(cpu, env->pc,
> +                                (uint8_t *)&insn, 4, false) != 0) {
> +            error_report("WHPX: cannot read insn at pc=0x%" PRIx64,
> +                         (uint64_t)env->pc);
> +            whpx_inject_data_abort(cpu, iswrite);
> +            return 1;
> +        }
> +
> +        r = arm_emul_insn(env, insn);
> +        if (r == ARM_EMUL_UNHANDLED || r == ARM_EMUL_ERR_MEM) {
> +            error_report("WHPX: ISV=0 %s insn 0x%08x at "
> +                         "pc=0x%" PRIx64 ", injecting data abort",
> +                         r == ARM_EMUL_UNHANDLED ? "unhandled"
> +                                                 : "memory error",
> +                         insn, (uint64_t)env->pc);
> +            whpx_inject_data_abort(cpu, iswrite);
> +            return 1;
> +        }
> +
> +        return 0;
> +    }
> 
>     if (iswrite) {
>         val = whpx_get_gp_reg(cpu, srt);
> @@ -451,6 +506,10 @@ int whpx_vcpu_run(CPUState *cpu)
>             }
> 
>             ret = whpx_handle_mmio(cpu, &vcpu->exit_ctx.MemoryAccess);
> +            if (ret > 0) {
> +                advance_pc = false;
> +                ret = 0;
> +            }
>             break;
>         case WHvRunVpExitReasonCanceled:
>             cpu->exception_index = EXCP_INTERRUPT;
> -- 
> 2.52.0
> 
Re: [PATCH v6 0/6] target/arm: ISV=0 data abort emulation library
Posted by Lucas Amaral 1 day, 11 hours ago
Hi Mohamed,

Thanks for the thorough review and the Reviewed-by tags on all six
patches.

On the observations:

> [1/6] Perhaps having a common version of this for fetching the
> instruction

Good point. The ISV=0 wiring in target/arm/hvf/hvf.c and
target/arm/whpx/whpx-all.c still uses cpu_memory_rw_debug() to fetch
the faulting instruction at env->pc — Henderson flagged this for the
library code in v5, but the wiring code was out of scope for v6. I'll
investigate factoring out a common get_phys_addr() + address_space_rw
helper covering both call sites and send it as a follow-up.

> [4/6] Do people actually use [exclusive load/store on MMIO]? And if
> so I wonder if that's an application bug...

I had the impression that virglrenderer hit this early on, but I'm
not 100% sure of the exact case anymore. The v4 emulation library
tried to cover the most likely ISV=0 triggers — load/store pair,
register-offset, exclusives, atomics, CAS — without fully exhausting
the ISA. I might double-check what actually exercises this path in
practice.

> [5/6] Maybe there should be a restriction to put a warning message
> somewhere if an op happens on a non-MMIO range...

Agreed, that would help debug guest issues. I'll look into adding a
warn_report_once() when get_phys_addr() resolves to a RAM region (vs
MMIO/Device).

I'll send these as a follow-up series once v6 lands.

Thanks,
Lucas