All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support
@ 2026-07-31 18:46 Doru Blânzeanu
  2026-07-31 18:46 ` [PATCH v2 1/4] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu
                   ` (4 more replies)
  0 siblings, 5 replies; 8+ messages in thread
From: Doru Blânzeanu @ 2026-07-31 18:46 UTC (permalink / raw)
  To: qemu-devel
  Cc: Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu,
	Doru Blânzeanu, Magnus Kulke

This series brings guest debugging to the MSHV accelerator by
implementing software breakpoints and single-stepping, so that gdb can
attach to an MSHV guest through QEMU's gdbstub.

It follows the KVM gdbstub implementation closely, and borrows the WHPX
approach where the MSHV interface differs from KVM:

  - it installs a partition-wide intercept for the #DB exception
    (HVCALL_INSTALL_INTERCEPT). #BP (INT3) is deliberately left to the
    guest's own IDT and is never intercepted, as WHPX does.

  - only software breakpoints are supported. A breakpoint is an INT1
    (opcode 0xf1) patched into guest memory, which raises a #DB when
    executed. Hardware breakpoints and watchpoints are not supported.

  - it never touches the guest debug registers (DR0-DR7), so guest state
    is not clobbered. The only guest register used is RFLAGS.TF, toggled
    around the vCPU run for single-stepping and kept out of env->eflags
    so it cannot be read back and re-armed.

  - it re-injects the #DB back into the guest (via
    HV_REGISTER_PENDING_EVENT0) when the faulting RIP does not match a
    debugger breakpoint, so a guest-owned #DB is delivered through the
    guest's own IDT.

When a vCPU is created we register the accel ops that gdb drives once it
is attached:

  - mshv_update_guest_debug
  - mshv_insert_gdbstub_breakpoint
  - mshv_remove_gdbstub_breakpoint
  - mshv_remove_all_gdbstub_breakpoints

For each insert request from gdb the ops save the original byte at the
target address and patch in 0xf1; on a remove request the saved byte is
restored, after which gdb single-steps over the original instruction and
re-inserts the breakpoint. When the vCPU executes the 0xf1 byte a #DB
vmexits to QEMU. mshv_handle_debug() then checks whether the RIP where
the vCPU stopped matches a breakpoint installed by gdb, or whether
single-stepping is active, and if so reports the stop to gdb. If the RIP
does not match any debugger breakpoint the #DB is assumed to be
guest-owned and is re-injected so the guest can handle it.

Known limitations:

  - Once installed, the #DB intercept stays for the lifetime
    of the VM and every #DB - including those the guest raises itself -
    vmexits to QEMU.

  - Because attribution relies on matching the faulting RIP against the
    debugger's breakpoint list, a guest that legitimately executes INT1 or
    raises its own #DB at an address that also holds a debugger breakpoint
    cannot be disambiguated; such overlaps are not expected in practice.

The series is organised as:

  1. include/hw/hyperv: add ABI for exception intercepts and pending
     events
  2. accel/mshv: add gdbstub software breakpoint support
  3. target/i386/mshv: support single-stepping
  4. accel/mshv: break the vCPU run loop on exit_request
     This commit fixes a race condition encountered during testing with guests
     that run infinite loops with some debugging opcodes.

I have tested this patch series by using both a guest that doesn't use debugging
and one that triggers the #DB and #BP exceptions (#DB is re-injected and #BP is
not intercepted at all).

Changes since v1:
- refactored the HVMSG_X64_EXCEPTION_INTERCEPT vmexit handle to be contained in
  a standalone function handle_exception_interrupt
- add new commit that checks the mshv vcpu run loop for exit requests before
  re-entering MSHV_RUN_VP. This ensures a busy vCPU doesn't ignore gdb commands.

Doru Blânzeanu (4):
  include/hw/hyperv: add ABI for exception intercepts and pending events
  accel/mshv: add gdbstub software breakpoint support
  target/i386/mshv: support single-stepping
  accel/mshv: break the vCPU run loop on exit_request

 accel/mshv/mshv-all.c          | 175 ++++++++++++++++++++++++++++++++-
 include/hw/hyperv/hvgdk_mini.h |  56 +++++++++++
 include/system/mshv_int.h      |  14 +++
 target/i386/mshv/mshv-cpu.c    | 154 +++++++++++++++++++++++++++++
 4 files changed, 398 insertions(+), 1 deletion(-)

-- 
2.53.0



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

* [PATCH v2 1/4] include/hw/hyperv: add ABI for exception intercepts and pending events
  2026-07-31 18:46 [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu
@ 2026-07-31 18:46 ` Doru Blânzeanu
  2026-07-31 18:46 ` [PATCH v2 2/4] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 8+ messages in thread
From: Doru Blânzeanu @ 2026-07-31 18:46 UTC (permalink / raw)
  To: qemu-devel
  Cc: Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu,
	Doru Blânzeanu, Magnus Kulke

Import the hypervisor definitions needed to intercept guest exceptions
and to inject events back into a guest, in preparation for gdbstub
debugging support in the MSHV accelerator.

Add:
  - union hv_intercept_parameters, struct hv_input_install_intercept and
    HVCALL_INSTALL_INTERCEPT, used to install an exception intercept on
    the partition;
  - struct hv_x64_exception_intercept_message, the message delivered on
    an intercepted exception, carrying the faulting RIP, the exception
    vector and the instruction bytes;
  - HV_X64_PENDING_EVENT_EXCEPTION, the pending-event type for an
    exception.

These definitions mirror the hypervisor headers and have no functional
effect on their own.

Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com>
Reviewed-by: Magnus Kulke <magnuskulke@linux.microsoft.com>
---
 include/hw/hyperv/hvgdk_mini.h | 56 ++++++++++++++++++++++++++++++++++
 1 file changed, 56 insertions(+)

diff --git a/include/hw/hyperv/hvgdk_mini.h b/include/hw/hyperv/hvgdk_mini.h
index f8838a31bb..c81928d0c2 100644
--- a/include/hw/hyperv/hvgdk_mini.h
+++ b/include/hw/hyperv/hvgdk_mini.h
@@ -22,6 +22,9 @@
 #define HV_X64_MSR_TSC_FREQUENCY    0x40000022
 #define HV_X64_MSR_APIC_FREQUENCY   0x40000023
 
+/* event_type values for hv_x64_pending_exception_event */
+#define HV_X64_PENDING_EVENT_EXCEPTION 0
+
 typedef enum hv_register_name {
     /* Pending Interruption Register */
     HV_REGISTER_PENDING_INTERRUPTION = 0x00010002,
@@ -236,6 +239,29 @@ enum hv_intercept_type {
     HV_INTERCEPT_TYPE_INVALID               = 0XFFFFFFFF,
 };
 
+union hv_intercept_parameters {
+    /* HV_INTERCEPT_PARAMETERS is defined to be an 8-byte field. */
+    uint64_t as_uint64;
+    /* HV_INTERCEPT_TYPE_X64_IO_PORT */
+    uint16_t io_port;
+    /* HV_INTERCEPT_TYPE_X64_CPUID */
+    uint32_t cpuid_index;
+    /* HV_INTERCEPT_TYPE_X64_APIC_WRITE */
+    uint32_t apic_write_mask;
+    /* HV_INTERCEPT_TYPE_EXCEPTION */
+    uint16_t exception_vector;
+    /* HV_INTERCEPT_TYPE_X64_MSR_INDEX */
+    uint32_t msr_index;
+    /* N.B. Other intercept types do not have any parameters. */
+};
+
+struct hv_input_install_intercept {
+    uint64_t partition_id;
+    uint32_t access_type; /* mask */
+    uint32_t intercept_type; /* enum hv_intercept_type */
+    union hv_intercept_parameters intercept_parameter;
+} QEMU_PACKED;
+
 struct hv_u128 {
     uint64_t low_part;
     uint64_t high_part;
@@ -836,6 +862,35 @@ struct hv_x64_memory_intercept_message {
     uint8_t instruction_bytes[16];
 } QEMU_PACKED;
 
+struct hv_x64_exception_intercept_message {
+    struct hv_x64_intercept_message_header header;
+    uint16_t exception_vector;
+    uint8_t exception_info;
+    uint8_t instruction_byte_count;
+    uint32_t error_code;
+    uint64_t exception_parameter; /* DR6 for #DB, CR2 for #PF */
+    uint64_t reserved;
+    uint8_t instruction_bytes[16];
+    struct hv_x64_segment_register ds_segment;
+    struct hv_x64_segment_register ss_segment;
+    uint64_t rax;
+    uint64_t rcx;
+    uint64_t rdx;
+    uint64_t rbx;
+    uint64_t rsp;
+    uint64_t rbp;
+    uint64_t rsi;
+    uint64_t rdi;
+    uint64_t r8;
+    uint64_t r9;
+    uint64_t r10;
+    uint64_t r11;
+    uint64_t r12;
+    uint64_t r13;
+    uint64_t r14;
+    uint64_t r15;
+} QEMU_PACKED;
+
 union hv_message_flags {
     uint8_t asu8;
     struct {
@@ -943,6 +998,7 @@ struct hv_cpuid {
 
 #define HVCALL_GET_PARTITION_PROPERTY    0x0044
 #define HVCALL_SET_PARTITION_PROPERTY    0x0045
+#define HVCALL_INSTALL_INTERCEPT         0x004d
 #define HVCALL_GET_VP_REGISTERS          0x0050
 #define HVCALL_SET_VP_REGISTERS          0x0051
 #define HVCALL_TRANSLATE_VIRTUAL_ADDRESS 0x0052
-- 
2.53.0



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

* [PATCH v2 2/4] accel/mshv: add gdbstub software breakpoint support
  2026-07-31 18:46 [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu
  2026-07-31 18:46 ` [PATCH v2 1/4] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu
@ 2026-07-31 18:46 ` Doru Blânzeanu
  2026-09-23 11:20   ` Magnus Kulke
  2026-07-31 18:46 ` [PATCH v2 3/4] target/i386/mshv: support single-stepping Doru Blânzeanu
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 8+ messages in thread
From: Doru Blânzeanu @ 2026-07-31 18:46 UTC (permalink / raw)
  To: qemu-devel
  Cc: Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu,
	Doru Blânzeanu, Magnus Kulke

Implement software breakpoint based guest debugging for the MSHV
accelerator, modeled on the WHPX backend. The guest-visible debug
registers are not used: a breakpoint is an INT1 (opcode 0xf1)
patched into guest memory, which raises a #DB when executed.

A partition-wide intercept on #DB is installed the first time a
breakpoint (or single step) is requested. The intercept is not uninstalled,
so it stays for the lifetime of the VM and every vmexit mshv_handle_debug()
decides whether the #DB belongs to gdb (it hit one of our INT1 breakpoints) or
to the guest; a guest-owned #DB is handed back through a pending exception
event so the guest takes it through its own IDT.
Guest INT3 (#BP) is deliberately left to the guest's own IDT and is never
intercepted.

Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com>
---
 accel/mshv/mshv-all.c       | 165 +++++++++++++++++++++++++++++++++++-
 include/system/mshv_int.h   |  14 +++
 target/i386/mshv/mshv-cpu.c | 104 +++++++++++++++++++++++
 3 files changed, 282 insertions(+), 1 deletion(-)

diff --git a/accel/mshv/mshv-all.c b/accel/mshv/mshv-all.c
index 72721d0f0d..afcb87b3b8 100644
--- a/accel/mshv/mshv-all.c
+++ b/accel/mshv/mshv-all.c
@@ -29,6 +29,7 @@
 
 #include "qemu/accel.h"
 #include "qemu/guest-random.h"
+#include "gdbstub/enums.h"
 #include "accel/accel-ops.h"
 #include "accel/accel-cpu-ops.h"
 #include "exec/cpu-common.h"
@@ -583,6 +584,10 @@ static int mshv_init(AccelState *as, MachineState *ms)
     s->nr_as = 1;
     s->as = g_new0(MshvAddressSpace, s->nr_as);
 
+    QTAILQ_INIT(&s->sw_breakpoints);
+
+    as->gdbstub.sstep_flags = SSTEP_ENABLE;
+
     mshv_state = s;
 
     mshv_init_irq_routing(s);
@@ -607,6 +612,153 @@ static int mshv_destroy_vcpu(CPUState *cpu)
     return 0;
 }
 
+struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc)
+{
+    struct MshvSwBreakpoint *bp;
+
+    QTAILQ_FOREACH(bp, &mshv_state->sw_breakpoints, entry) {
+        if (bp->pc == pc) {
+            return bp;
+        }
+    }
+    return NULL;
+}
+
+static int mshv_install_exception_intercept(int vm_fd, uint16_t vector)
+{
+    struct hv_input_install_intercept in = {0};
+    struct mshv_root_hvcall args = {0};
+    int ret;
+
+    in.access_type = 1 << HV_X64_INTERCEPT_ACCESS_TYPE_EXECUTE;
+    in.intercept_type = HV_INTERCEPT_TYPE_EXCEPTION;
+    in.intercept_parameter.as_uint64 = vector;
+
+    args.code = HVCALL_INSTALL_INTERCEPT;
+    args.in_sz = sizeof(in);
+    args.in_ptr = (uint64_t)&in;
+
+    ret = mshv_hvcall(vm_fd, &args);
+    if (ret < 0) {
+        error_report("Failed to install exception intercept for vector %u",
+                     vector);
+    }
+    return ret;
+}
+
+/*
+ * Install the #DB intercept. This is a permanent, partition-wide latch: it is
+ * installed once on the first debug use and stays for the lifetime of the VM.
+ */
+static int mshv_init_exception_intercept(void)
+{
+    int ret;
+
+    if (mshv_state->exception_intercepts_installed) {
+        return 0;
+    }
+
+    /* Only #DB is needed: gdb breakpoints are INT1 (0xf1) */
+    ret = mshv_install_exception_intercept(mshv_state->vm, 1); /* #DB */
+    if (ret < 0) {
+        return ret;
+    }
+
+    mshv_state->exception_intercepts_installed = true;
+    return 0;
+}
+
+static int mshv_update_guest_debug(CPUState *cpu)
+{
+    if (cpu_single_stepping(cpu)) {
+        /* Installs exception intercept only on the first call */
+        return mshv_init_exception_intercept();
+    }
+    return 0;
+}
+
+static int mshv_insert_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type,
+                                          vaddr addr, vaddr len)
+{
+    struct MshvSwBreakpoint *bp;
+    int err;
+
+    /* We don't use the guest's debug registers. */
+    if (type != GDB_BREAKPOINT_SW) {
+        return -ENOSYS;
+    }
+
+    bp = mshv_find_sw_breakpoint(cpu, addr);
+    if (bp) {
+        bp->use_count++;
+        return 0;
+    }
+
+    bp = g_new(struct MshvSwBreakpoint, 1);
+    bp->pc = addr;
+    bp->use_count = 1;
+    err = mshv_arch_insert_sw_breakpoint(cpu, bp);
+    if (err) {
+        g_free(bp);
+        return err;
+    }
+
+    QTAILQ_INSERT_HEAD(&mshv_state->sw_breakpoints, bp, entry);
+
+    /* Installs exception intercept only on the first call */
+    return mshv_init_exception_intercept();
+}
+
+static int mshv_remove_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type,
+                                          vaddr addr, vaddr len)
+{
+    struct MshvSwBreakpoint *bp;
+    int err;
+
+    if (type != GDB_BREAKPOINT_SW) {
+        return -ENOSYS;
+    }
+
+    bp = mshv_find_sw_breakpoint(cpu, addr);
+    if (!bp) {
+        return -ENOENT;
+    }
+
+    if (bp->use_count > 1) {
+        bp->use_count--;
+        return 0;
+    }
+
+    err = mshv_arch_remove_sw_breakpoint(cpu, bp);
+    if (err) {
+        return err;
+    }
+
+    QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry);
+    g_free(bp);
+
+    return 0;
+}
+
+static void mshv_remove_all_gdbstub_breakpoints(CPUState *cpu)
+{
+    struct MshvSwBreakpoint *bp, *next;
+    CPUState *tmpcpu;
+
+    QTAILQ_FOREACH_SAFE(bp, &mshv_state->sw_breakpoints, entry, next) {
+        if (mshv_arch_remove_sw_breakpoint(cpu, bp) != 0) {
+            /* Fall back to whichever CPU still has it mapped. */
+            CPU_FOREACH(tmpcpu) {
+                if (mshv_arch_remove_sw_breakpoint(tmpcpu, bp) == 0) {
+                    break;
+                }
+            }
+        }
+        QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry);
+        g_free(bp);
+    }
+}
+
 static int mshv_cpu_exec(CPUState *cpu)
 {
     hv_message mshv_msg;
@@ -637,6 +789,9 @@ static int mshv_cpu_exec(CPUState *cpu)
         switch (exit_reason) {
         case MshvVmExitIgnore:
             break;
+        case MshvVmExitDebug:
+            ret = EXCP_DEBUG;
+            break;
         default:
             ret = EXCP_INTERRUPT;
             break;
@@ -708,7 +863,10 @@ static void *mshv_vcpu_thread(void *arg)
     do {
         qemu_process_cpu_events(cpu);
         if (cpu_can_run(cpu)) {
-            mshv_cpu_exec(cpu);
+            ret = mshv_cpu_exec(cpu);
+            if (ret == EXCP_DEBUG) {
+                cpu_handle_guest_debug(cpu);
+            }
         }
     } while (!cpu->unplug || cpu_can_run(cpu));
 
@@ -852,6 +1010,11 @@ static void mshv_accel_ops_class_init(ObjectClass *oc, const void *data)
     ops->synchronize_pre_loadvm = mshv_cpu_synchronize_pre_loadvm;
     ops->cpus_are_resettable = mshv_cpus_are_resettable;
     ops->handle_interrupt = generic_handle_interrupt;
+
+    ops->update_guest_debug = mshv_update_guest_debug;
+    ops->insert_gdbstub_breakpoint = mshv_insert_gdbstub_breakpoint;
+    ops->remove_gdbstub_breakpoint = mshv_remove_gdbstub_breakpoint;
+    ops->remove_all_gdbstub_breakpoints = mshv_remove_all_gdbstub_breakpoints;
 }
 
 static const TypeInfo mshv_accel_ops_type = {
diff --git a/include/system/mshv_int.h b/include/system/mshv_int.h
index b91c4d661a..9244915eb6 100644
--- a/include/system/mshv_int.h
+++ b/include/system/mshv_int.h
@@ -15,6 +15,7 @@
 #define QEMU_MSHV_INT_H
 
 #include "hw/hyperv/hvhdk.h"
+#include "exec/vaddr.h"
 
 #define MSHV_MSR_ENTRIES_COUNT 64
 
@@ -56,6 +57,13 @@ typedef struct MshvAddressSpace {
     AddressSpace *as;
 } MshvAddressSpace;
 
+struct MshvSwBreakpoint {
+    vaddr pc;
+    vaddr saved_insn;
+    int use_count;
+    QTAILQ_ENTRY(MshvSwBreakpoint) entry;
+};
+
 struct MshvState {
     AccelState parent_obj;
     int vm;
@@ -70,6 +78,8 @@ struct MshvState {
     unsigned long *used_gsi_bitmap;
     unsigned int gsi_count;
     union hv_partition_processor_features processor_features;
+    QTAILQ_HEAD(, MshvSwBreakpoint) sw_breakpoints;
+    bool exception_intercepts_installed;
 };
 
 typedef struct MshvMsiControl {
@@ -85,6 +95,7 @@ typedef enum MshvVmExit {
     MshvVmExitIgnore   = 0,
     MshvVmExitShutdown = 1,
     MshvVmExitSpecial  = 2,
+    MshvVmExitDebug    = 3,
 } MshvVmExit;
 
 void mshv_init_mmio_emu(void);
@@ -98,6 +109,9 @@ int mshv_get_generic_regs(CPUState *cpu, hv_register_assoc *assocs,
                           size_t n_regs);
 int mshv_arch_store_vcpu_state(const CPUState *cpu);
 int mshv_arch_load_vcpu_state(CPUState *cpu);
+int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp);
+int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp);
+struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc);
 void mshv_arch_init_vcpu(CPUState *cpu);
 void mshv_arch_destroy_vcpu(CPUState *cpu);
 void mshv_arch_amend_proc_features(
diff --git a/target/i386/mshv/mshv-cpu.c b/target/i386/mshv/mshv-cpu.c
index 1c433c408c..b15776b9bd 100644
--- a/target/i386/mshv/mshv-cpu.c
+++ b/target/i386/mshv/mshv-cpu.c
@@ -12,6 +12,7 @@
 
 #include "qemu/osdep.h"
 #include "qemu/error-report.h"
+#include "qemu/main-loop.h"
 #include "qemu/memalign.h"
 
 #include "system/mshv.h"
@@ -28,6 +29,7 @@
 #include "emulate/x86_decode.h"
 #include "emulate/x86_emu.h"
 #include "emulate/x86_flags.h"
+#include "gdbstub/enums.h"
 
 #include "accel/accel-cpu-target.h"
 
@@ -1932,6 +1934,72 @@ static int handle_pio(CPUState *cpu, const struct hyperv_message *msg)
     return handle_pio_non_str(cpu, &info);
 }
 
+/* Re-inject a guest-owned #DB via a pending event */
+static int reinject_exception(CPUState *cpu,
+    const struct hv_x64_exception_intercept_message *info)
+{
+    hv_register_assoc assoc = { .name = HV_REGISTER_PENDING_EVENT0 };
+
+    assoc.value.pending_exception_event.event_pending = 1;
+    assoc.value.pending_exception_event.event_type =
+        HV_X64_PENDING_EVENT_EXCEPTION;
+    assoc.value.pending_exception_event.exception_parameter =
+        info->exception_parameter;
+    assoc.value.pending_exception_event.vector = info->exception_vector;
+
+    return mshv_set_generic_regs(cpu, &assoc, 1);
+}
+
+/* Check if the intercepted #DB has been set by gdb. */
+static bool is_debug_exception(CPUState *cpu, const hv_message *msg)
+{
+    struct hv_x64_exception_intercept_message *info = (void *)msg->payload;
+
+    /* Only #DB is intercepted */
+    if (info->exception_vector != EXCP01_DB) {
+        return false;
+    }
+
+    /* INT1 hit: RIP points at the breakpoint */
+    if (mshv_find_sw_breakpoint(cpu, info->header.rip) != NULL) {
+        return true;
+    }
+
+    /* Check if single stepping */
+    if (cpu_single_stepping(cpu)) {
+        return true;
+    }
+
+    /* The guest's own #DB; caller re-injects it. */
+    return false;
+}
+
+static int handle_exception_interrupt(CPUState *cpu,
+                                       const struct hyperv_message *msg,
+                                       MshvVmExit *exit_reason)
+{
+    int ret;
+
+    if (is_debug_exception(cpu, msg)) {
+        /* Exit early reporting debug exception */
+        *exit_reason = MshvVmExitDebug;
+        return 0;
+    }
+
+    /* Not ours - hand the #DB back to the guest and keep running. */
+    ret = reinject_exception(cpu,
+        (struct hv_x64_exception_intercept_message *) msg->payload);
+    if (ret < 0) {
+        error_report("failed to reinject exception on vcpu %d",
+                     cpu->cpu_index);
+        return -1;
+    }
+
+    *exit_reason = MshvVmExitIgnore;
+
+    return 0;
+}
+
 int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit)
 {
     int ret;
@@ -1960,6 +2028,16 @@ int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit)
             return MshvVmExitSpecial;
         }
         return MshvVmExitIgnore;
+    case HVMSG_X64_EXCEPTION_INTERCEPT:
+        bql_lock();
+        ret = handle_exception_interrupt(cpu, msg, &exit_reason);
+        bql_unlock();
+        if (ret < 0) {
+            error_report("failed to handle exception intercept");
+            return -1;
+        }
+        *exit = exit_reason;
+        return exit_reason;
     default:
         break;
     }
@@ -1973,6 +2051,32 @@ void mshv_remove_vcpu(int vm_fd, int cpu_fd)
     close(cpu_fd);
 }
 
+int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp)
+{
+    static const uint8_t int1 = 0xf1;
+
+    if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 0) ||
+        cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&int1, 1, 1)) {
+        return -EINVAL;
+    }
+    return 0;
+}
+
+int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp)
+{
+    uint8_t int1;
+
+    if (cpu_memory_rw_debug(cpu, bp->pc, &int1, 1, 0)) {
+        return -EINVAL;
+    }
+    if (int1 != 0xf1) {
+        return 0;
+    }
+    if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 1)) {
+        return -EINVAL;
+    }
+    return 0;
+}
 
 int mshv_create_vcpu(int vm_fd, uint8_t vp_index, int *cpu_fd)
 {
-- 
2.53.0



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

* [PATCH v2 3/4] target/i386/mshv: support single-stepping
  2026-07-31 18:46 [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu
  2026-07-31 18:46 ` [PATCH v2 1/4] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu
  2026-07-31 18:46 ` [PATCH v2 2/4] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu
@ 2026-07-31 18:46 ` Doru Blânzeanu
  2026-07-31 18:46 ` [PATCH v2 4/4] accel/mshv: break the vCPU run loop on exit_request Doru Blânzeanu
  2026-10-12 12:35 ` [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Magnus Kulke
  4 siblings, 0 replies; 8+ messages in thread
From: Doru Blânzeanu @ 2026-07-31 18:46 UTC (permalink / raw)
  To: qemu-devel
  Cc: Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu,
	Doru Blânzeanu, Magnus Kulke

Single-step by toggling RFLAGS.TF around the vCPU run, as WHPX does.
TF is set only on the live register, never in env->eflags, so it is not read
back and re-applied by a later register store.
The resulting #DB is reported to gdb by mshv_handle_debug() whenever the vCPU
is single-stepping.

Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com>
Reviewed-by: Magnus Kulke <magnuskulke@linux.microsoft.com>
---
 target/i386/mshv/mshv-cpu.c | 50 +++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/target/i386/mshv/mshv-cpu.c b/target/i386/mshv/mshv-cpu.c
index b15776b9bd..af4f90385e 100644
--- a/target/i386/mshv/mshv-cpu.c
+++ b/target/i386/mshv/mshv-cpu.c
@@ -2000,17 +2000,67 @@ static int handle_exception_interrupt(CPUState *cpu,
     return 0;
 }
 
+/*
+ * Flip RFLAGS.TF like WHPX. Set it only on the live register, not env->eflags,
+ * so a later store won't put it back.
+ */
+static int arch_set_single_step(CPUState *cpu, bool enable)
+{
+    X86CPU *x86cpu = X86_CPU(cpu);
+    CPUX86State *env = &x86cpu->env;
+    hv_register_assoc assoc = { .name = HV_X64_REGISTER_RFLAGS };
+    uint64_t rflags;
+    int ret;
+
+    if (env->regs_page && env->regs_page->isvalid != 0) {
+        rflags = env->regs_page->rflags;
+        rflags = enable ? (rflags | TF_MASK) : (rflags & ~TF_MASK);
+        env->regs_page->rflags = rflags;
+        env->regs_page->dirty |= (1u << HV_X64_REGISTER_CLASS_FLAGS);
+        return 0;
+    }
+
+    ret = mshv_get_generic_regs(cpu, &assoc, 1);
+    if (ret < 0) {
+        return ret;
+    }
+    rflags = assoc.value.reg64;
+    rflags = enable ? (rflags | TF_MASK) : (rflags & ~TF_MASK);
+    assoc.value.reg64 = rflags;
+    return mshv_set_generic_regs(cpu, &assoc, 1);
+}
+
 int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit)
 {
     int ret;
     enum MshvVmExit exit_reason;
     int cpu_fd = mshv_vcpufd(cpu);
+    bool single_step;
+
+    /* enable single stepping by flipping RFLAGS.TF */
+    single_step = cpu_single_stepping(cpu);
+    if (single_step) {
+        ret = arch_set_single_step(cpu, true);
+        if (ret < 0) {
+            error_report("Failed to arm single-step (TF) on vcpu %d: %s",
+                         cpu->cpu_index, strerror(-ret));
+            *exit = MshvVmExitShutdown;
+            return -1;
+        }
+    }
 
     ret = ioctl(cpu_fd, MSHV_RUN_VP, msg);
     if (ret < 0) {
         return MshvVmExitShutdown;
     }
 
+    /* disable single stepping by flipping RFLAGS.TF */
+    if (single_step && arch_set_single_step(cpu, false) < 0) {
+        error_report("Failed to clear single-step (TF) on vcpu %d",
+                     cpu->cpu_index);
+        return -1;
+    }
+
     switch (msg->header.message_type) {
     case HVMSG_UNRECOVERABLE_EXCEPTION:
         return MshvVmExitShutdown;
-- 
2.53.0



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

* [PATCH v2 4/4] accel/mshv: break the vCPU run loop on exit_request
  2026-07-31 18:46 [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu
                   ` (2 preceding siblings ...)
  2026-07-31 18:46 ` [PATCH v2 3/4] target/i386/mshv: support single-stepping Doru Blânzeanu
@ 2026-07-31 18:46 ` Doru Blânzeanu
  2026-09-23 11:27   ` Magnus Kulke
  2026-10-12 12:35 ` [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Magnus Kulke
  4 siblings, 1 reply; 8+ messages in thread
From: Doru Blânzeanu @ 2026-07-31 18:46 UTC (permalink / raw)
  To: qemu-devel
  Cc: Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu,
	Doru Blânzeanu, Magnus Kulke

Ignored VM exits can cause mshv_cpu_exec() to re-enter MSHV_RUN_VP
without returning to qemu_process_cpu_events(). A busy vCPU may
prevent pause_all_vcpus() from completing.

Check exit_request before re-entering MSHV_RUN_VP and return
EXCP_INTERRUPT so pending stop requests can be processed.

Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com>
---
 accel/mshv/mshv-all.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/accel/mshv/mshv-all.c b/accel/mshv/mshv-all.c
index afcb87b3b8..b7c1126fe0 100644
--- a/accel/mshv/mshv-all.c
+++ b/accel/mshv/mshv-all.c
@@ -780,6 +780,16 @@ static int mshv_cpu_exec(CPUState *cpu)
             cpu->vcpu_dirty = false;
         }
 
+        /*
+         * A kick/stop can't reliably break MSHV_RUN_VP out, so leave the run
+         * loop here too; otherwise a busy vCPU never reaches
+         * qemu_process_cpu_events() and pause_all_vcpus() hangs under the BQL.
+         */
+        if (cpu->stop || qatomic_load_acquire(&cpu->exit_request)) {
+            ret = EXCP_INTERRUPT;
+            break;
+        }
+
         ret = mshv_run_vcpu(mshv_state->vm, cpu, &mshv_msg, &exit_reason);
         if (ret < 0) {
             error_report("Failed to run on vcpu %d", cpu->cpu_index);
-- 
2.53.0



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

* Re: [PATCH v2 2/4] accel/mshv: add gdbstub software breakpoint support
  2026-07-31 18:46 ` [PATCH v2 2/4] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu
@ 2026-09-23 11:20   ` Magnus Kulke
  0 siblings, 0 replies; 8+ messages in thread
From: Magnus Kulke @ 2026-09-23 11:20 UTC (permalink / raw)
  To: Doru Blânzeanu
  Cc: qemu-devel, Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu

On Fri, Jul 31, 2026 at 09:46:40PM +0300, Doru Blânzeanu wrote:
> Implement software breakpoint based guest debugging for the MSHV
> accelerator, modeled on the WHPX backend. The guest-visible debug
> registers are not used: a breakpoint is an INT1 (opcode 0xf1)
> patched into guest memory, which raises a #DB when executed.
> 
> A partition-wide intercept on #DB is installed the first time a
> breakpoint (or single step) is requested. The intercept is not uninstalled,
> so it stays for the lifetime of the VM and every vmexit mshv_handle_debug()
> decides whether the #DB belongs to gdb (it hit one of our INT1 breakpoints) or
> to the guest; a guest-owned #DB is handed back through a pending exception
> event so the guest takes it through its own IDT.
> Guest INT3 (#BP) is deliberately left to the guest's own IDT and is never
> intercepted.
> 
> Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com>
> ---
>  accel/mshv/mshv-all.c       | 165 +++++++++++++++++++++++++++++++++++-
>  include/system/mshv_int.h   |  14 +++
>  target/i386/mshv/mshv-cpu.c | 104 +++++++++++++++++++++++
>  3 files changed, 282 insertions(+), 1 deletion(-)
> 
> diff --git a/accel/mshv/mshv-all.c b/accel/mshv/mshv-all.c
> index 72721d0f0d..afcb87b3b8 100644
> --- a/accel/mshv/mshv-all.c
> +++ b/accel/mshv/mshv-all.c
> @@ -29,6 +29,7 @@
>  
>  #include "qemu/accel.h"
>  #include "qemu/guest-random.h"
> +#include "gdbstub/enums.h"
>  #include "accel/accel-ops.h"
>  #include "accel/accel-cpu-ops.h"
>  #include "exec/cpu-common.h"
> @@ -583,6 +584,10 @@ static int mshv_init(AccelState *as, MachineState *ms)
>      s->nr_as = 1;
>      s->as = g_new0(MshvAddressSpace, s->nr_as);
>  
> +    QTAILQ_INIT(&s->sw_breakpoints);
> +
> +    as->gdbstub.sstep_flags = SSTEP_ENABLE;
> +
>      mshv_state = s;
>  
>      mshv_init_irq_routing(s);
> @@ -607,6 +612,153 @@ static int mshv_destroy_vcpu(CPUState *cpu)
>      return 0;
>  }
>  
> +struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc)
> +{
> +    struct MshvSwBreakpoint *bp;
> +
> +    QTAILQ_FOREACH(bp, &mshv_state->sw_breakpoints, entry) {
> +        if (bp->pc == pc) {
> +            return bp;
> +        }
> +    }
> +    return NULL;
> +}
> +
> +static int mshv_install_exception_intercept(int vm_fd, uint16_t vector)
> +{
> +    struct hv_input_install_intercept in = {0};
> +    struct mshv_root_hvcall args = {0};
> +    int ret;
> +
> +    in.access_type = 1 << HV_X64_INTERCEPT_ACCESS_TYPE_EXECUTE;
> +    in.intercept_type = HV_INTERCEPT_TYPE_EXCEPTION;
> +    in.intercept_parameter.as_uint64 = vector;
> +
> +    args.code = HVCALL_INSTALL_INTERCEPT;
> +    args.in_sz = sizeof(in);
> +    args.in_ptr = (uint64_t)&in;
> +
> +    ret = mshv_hvcall(vm_fd, &args);
> +    if (ret < 0) {
> +        error_report("Failed to install exception intercept for vector %u",
> +                     vector);
> +    }
> +    return ret;
> +}
> +
> +/*
> + * Install the #DB intercept. This is a permanent, partition-wide latch: it is
> + * installed once on the first debug use and stays for the lifetime of the VM.
> + */
> +static int mshv_init_exception_intercept(void)
> +{
> +    int ret;
> +
> +    if (mshv_state->exception_intercepts_installed) {
> +        return 0;
> +    }
> +
> +    /* Only #DB is needed: gdb breakpoints are INT1 (0xf1) */
> +    ret = mshv_install_exception_intercept(mshv_state->vm, 1); /* #DB */
> +    if (ret < 0) {
> +        return ret;
> +    }
> +
> +    mshv_state->exception_intercepts_installed = true;
> +    return 0;
> +}
> +
> +static int mshv_update_guest_debug(CPUState *cpu)
> +{
> +    if (cpu_single_stepping(cpu)) {
> +        /* Installs exception intercept only on the first call */
> +        return mshv_init_exception_intercept();
> +    }
> +    return 0;
> +}
> +
> +static int mshv_insert_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type,
> +                                          vaddr addr, vaddr len)
> +{
> +    struct MshvSwBreakpoint *bp;
> +    int err;
> +
> +    /* We don't use the guest's debug registers. */
> +    if (type != GDB_BREAKPOINT_SW) {
> +        return -ENOSYS;
> +    }
> +
> +    bp = mshv_find_sw_breakpoint(cpu, addr);
> +    if (bp) {
> +        bp->use_count++;
> +        return 0;
> +    }
> +
> +    bp = g_new(struct MshvSwBreakpoint, 1);
> +    bp->pc = addr;
> +    bp->use_count = 1;
> +    err = mshv_arch_insert_sw_breakpoint(cpu, bp);
> +    if (err) {
> +        g_free(bp);
> +        return err;
> +    }
> +
> +    QTAILQ_INSERT_HEAD(&mshv_state->sw_breakpoints, bp, entry);
> +
> +    /* Installs exception intercept only on the first call */
> +    return mshv_init_exception_intercept();
> +}
> +
> +static int mshv_remove_gdbstub_breakpoint(CPUState *cpu, GdbBreakpointType type,
> +                                          vaddr addr, vaddr len)
> +{
> +    struct MshvSwBreakpoint *bp;
> +    int err;
> +
> +    if (type != GDB_BREAKPOINT_SW) {
> +        return -ENOSYS;
> +    }
> +
> +    bp = mshv_find_sw_breakpoint(cpu, addr);
> +    if (!bp) {
> +        return -ENOENT;
> +    }
> +
> +    if (bp->use_count > 1) {
> +        bp->use_count--;
> +        return 0;
> +    }
> +
> +    err = mshv_arch_remove_sw_breakpoint(cpu, bp);
> +    if (err) {
> +        return err;
> +    }
> +
> +    QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry);
> +    g_free(bp);
> +
> +    return 0;
> +}
> +
> +static void mshv_remove_all_gdbstub_breakpoints(CPUState *cpu)
> +{
> +    struct MshvSwBreakpoint *bp, *next;
> +    CPUState *tmpcpu;
> +
> +    QTAILQ_FOREACH_SAFE(bp, &mshv_state->sw_breakpoints, entry, next) {
> +        if (mshv_arch_remove_sw_breakpoint(cpu, bp) != 0) {
> +            /* Fall back to whichever CPU still has it mapped. */
> +            CPU_FOREACH(tmpcpu) {
> +                if (mshv_arch_remove_sw_breakpoint(tmpcpu, bp) == 0) {
> +                    break;
> +                }
> +            }
> +        }
> +        QTAILQ_REMOVE(&mshv_state->sw_breakpoints, bp, entry);
> +        g_free(bp);
> +    }
> +}
> +
>  static int mshv_cpu_exec(CPUState *cpu)
>  {
>      hv_message mshv_msg;
> @@ -637,6 +789,9 @@ static int mshv_cpu_exec(CPUState *cpu)
>          switch (exit_reason) {
>          case MshvVmExitIgnore:
>              break;
> +        case MshvVmExitDebug:
> +            ret = EXCP_DEBUG;
> +            break;
>          default:
>              ret = EXCP_INTERRUPT;
>              break;
> @@ -708,7 +863,10 @@ static void *mshv_vcpu_thread(void *arg)
>      do {
>          qemu_process_cpu_events(cpu);
>          if (cpu_can_run(cpu)) {
> -            mshv_cpu_exec(cpu);
> +            ret = mshv_cpu_exec(cpu);
> +            if (ret == EXCP_DEBUG) {
> +                cpu_handle_guest_debug(cpu);
> +            }
>          }
>      } while (!cpu->unplug || cpu_can_run(cpu));
>  
> @@ -852,6 +1010,11 @@ static void mshv_accel_ops_class_init(ObjectClass *oc, const void *data)
>      ops->synchronize_pre_loadvm = mshv_cpu_synchronize_pre_loadvm;
>      ops->cpus_are_resettable = mshv_cpus_are_resettable;
>      ops->handle_interrupt = generic_handle_interrupt;
> +
> +    ops->update_guest_debug = mshv_update_guest_debug;
> +    ops->insert_gdbstub_breakpoint = mshv_insert_gdbstub_breakpoint;
> +    ops->remove_gdbstub_breakpoint = mshv_remove_gdbstub_breakpoint;
> +    ops->remove_all_gdbstub_breakpoints = mshv_remove_all_gdbstub_breakpoints;
>  }
>  
>  static const TypeInfo mshv_accel_ops_type = {
> diff --git a/include/system/mshv_int.h b/include/system/mshv_int.h
> index b91c4d661a..9244915eb6 100644
> --- a/include/system/mshv_int.h
> +++ b/include/system/mshv_int.h
> @@ -15,6 +15,7 @@
>  #define QEMU_MSHV_INT_H
>  
>  #include "hw/hyperv/hvhdk.h"
> +#include "exec/vaddr.h"
>  
>  #define MSHV_MSR_ENTRIES_COUNT 64
>  
> @@ -56,6 +57,13 @@ typedef struct MshvAddressSpace {
>      AddressSpace *as;
>  } MshvAddressSpace;
>  
> +struct MshvSwBreakpoint {
> +    vaddr pc;
> +    vaddr saved_insn;
> +    int use_count;
> +    QTAILQ_ENTRY(MshvSwBreakpoint) entry;
> +};
> +
>  struct MshvState {
>      AccelState parent_obj;
>      int vm;
> @@ -70,6 +78,8 @@ struct MshvState {
>      unsigned long *used_gsi_bitmap;
>      unsigned int gsi_count;
>      union hv_partition_processor_features processor_features;
> +    QTAILQ_HEAD(, MshvSwBreakpoint) sw_breakpoints;
> +    bool exception_intercepts_installed;
>  };
>  
>  typedef struct MshvMsiControl {
> @@ -85,6 +95,7 @@ typedef enum MshvVmExit {
>      MshvVmExitIgnore   = 0,
>      MshvVmExitShutdown = 1,
>      MshvVmExitSpecial  = 2,
> +    MshvVmExitDebug    = 3,
>  } MshvVmExit;
>  
>  void mshv_init_mmio_emu(void);
> @@ -98,6 +109,9 @@ int mshv_get_generic_regs(CPUState *cpu, hv_register_assoc *assocs,
>                            size_t n_regs);
>  int mshv_arch_store_vcpu_state(const CPUState *cpu);
>  int mshv_arch_load_vcpu_state(CPUState *cpu);
> +int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp);
> +int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp);
> +struct MshvSwBreakpoint *mshv_find_sw_breakpoint(CPUState *cpu, vaddr pc);
>  void mshv_arch_init_vcpu(CPUState *cpu);
>  void mshv_arch_destroy_vcpu(CPUState *cpu);
>  void mshv_arch_amend_proc_features(
> diff --git a/target/i386/mshv/mshv-cpu.c b/target/i386/mshv/mshv-cpu.c
> index 1c433c408c..b15776b9bd 100644
> --- a/target/i386/mshv/mshv-cpu.c
> +++ b/target/i386/mshv/mshv-cpu.c
> @@ -12,6 +12,7 @@
>  
>  #include "qemu/osdep.h"
>  #include "qemu/error-report.h"
> +#include "qemu/main-loop.h"
>  #include "qemu/memalign.h"
>  
>  #include "system/mshv.h"
> @@ -28,6 +29,7 @@
>  #include "emulate/x86_decode.h"
>  #include "emulate/x86_emu.h"
>  #include "emulate/x86_flags.h"
> +#include "gdbstub/enums.h"
>  
>  #include "accel/accel-cpu-target.h"
>  
> @@ -1932,6 +1934,72 @@ static int handle_pio(CPUState *cpu, const struct hyperv_message *msg)
>      return handle_pio_non_str(cpu, &info);
>  }
>  
> +/* Re-inject a guest-owned #DB via a pending event */
> +static int reinject_exception(CPUState *cpu,
> +    const struct hv_x64_exception_intercept_message *info)
> +{
> +    hv_register_assoc assoc = { .name = HV_REGISTER_PENDING_EVENT0 };
> +
> +    assoc.value.pending_exception_event.event_pending = 1;
> +    assoc.value.pending_exception_event.event_type =
> +        HV_X64_PENDING_EVENT_EXCEPTION;
> +    assoc.value.pending_exception_event.exception_parameter =
> +        info->exception_parameter;
> +    assoc.value.pending_exception_event.vector = info->exception_vector;
> +
> +    return mshv_set_generic_regs(cpu, &assoc, 1);
> +}
> +
> +/* Check if the intercepted #DB has been set by gdb. */
> +static bool is_debug_exception(CPUState *cpu, const hv_message *msg)
> +{
> +    struct hv_x64_exception_intercept_message *info = (void *)msg->payload;
> +
> +    /* Only #DB is intercepted */
> +    if (info->exception_vector != EXCP01_DB) {
> +        return false;
> +    }
> +
> +    /* INT1 hit: RIP points at the breakpoint */
> +    if (mshv_find_sw_breakpoint(cpu, info->header.rip) != NULL) {
> +        return true;
> +    }
> +
> +    /* Check if single stepping */
> +    if (cpu_single_stepping(cpu)) {
> +        return true;
> +    }
> +
> +    /* The guest's own #DB; caller re-injects it. */
> +    return false;
> +}
> +
> +static int handle_exception_interrupt(CPUState *cpu,
> +                                       const struct hyperv_message *msg,
> +                                       MshvVmExit *exit_reason)
> +{
> +    int ret;
> +
> +    if (is_debug_exception(cpu, msg)) {
> +        /* Exit early reporting debug exception */
> +        *exit_reason = MshvVmExitDebug;
> +        return 0;
> +    }
> +
> +    /* Not ours - hand the #DB back to the guest and keep running. */
> +    ret = reinject_exception(cpu,
> +        (struct hv_x64_exception_intercept_message *) msg->payload);
> +    if (ret < 0) {
> +        error_report("failed to reinject exception on vcpu %d",
> +                     cpu->cpu_index);
> +        return -1;
> +    }
> +
> +    *exit_reason = MshvVmExitIgnore;
> +
> +    return 0;
> +}
> +
>  int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit)
>  {
>      int ret;
> @@ -1960,6 +2028,16 @@ int mshv_run_vcpu(int vm_fd, CPUState *cpu, hv_message *msg, MshvVmExit *exit)
>              return MshvVmExitSpecial;
>          }
>          return MshvVmExitIgnore;
> +    case HVMSG_X64_EXCEPTION_INTERCEPT:
> +        bql_lock();
> +        ret = handle_exception_interrupt(cpu, msg, &exit_reason);
> +        bql_unlock();
> +        if (ret < 0) {
> +            error_report("failed to handle exception intercept");
> +            return -1;
> +        }
> +        *exit = exit_reason;
> +        return exit_reason;
>      default:
>          break;
>      }
> @@ -1973,6 +2051,32 @@ void mshv_remove_vcpu(int vm_fd, int cpu_fd)
>      close(cpu_fd);
>  }
>  
> +int mshv_arch_insert_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp)
> +{
> +    static const uint8_t int1 = 0xf1;
> +
> +    if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 0) ||
> +        cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&int1, 1, 1)) {
> +        return -EINVAL;
> +    }
> +    return 0;
> +}
> +
> +int mshv_arch_remove_sw_breakpoint(CPUState *cpu, struct MshvSwBreakpoint *bp)
> +{
> +    uint8_t int1;
> +
> +    if (cpu_memory_rw_debug(cpu, bp->pc, &int1, 1, 0)) {
> +        return -EINVAL;
> +    }
> +    if (int1 != 0xf1) {
> +        return 0;
> +    }
> +    if (cpu_memory_rw_debug(cpu, bp->pc, (uint8_t *)&bp->saved_insn, 1, 1)) {
> +        return -EINVAL;
> +    }
> +    return 0;
> +}
>  
>  int mshv_create_vcpu(int vm_fd, uint8_t vp_index, int *cpu_fd)
>  {
> -- 
> 2.53.0

Reviewed-by: Magnus Kulke <magnuskulke@linux.microsoft.com>


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

* Re: [PATCH v2 4/4] accel/mshv: break the vCPU run loop on exit_request
  2026-07-31 18:46 ` [PATCH v2 4/4] accel/mshv: break the vCPU run loop on exit_request Doru Blânzeanu
@ 2026-09-23 11:27   ` Magnus Kulke
  0 siblings, 0 replies; 8+ messages in thread
From: Magnus Kulke @ 2026-09-23 11:27 UTC (permalink / raw)
  To: Doru Blânzeanu
  Cc: qemu-devel, Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu

On Fri, Jul 31, 2026 at 09:46:42PM +0300, Doru Blânzeanu wrote:
> Ignored VM exits can cause mshv_cpu_exec() to re-enter MSHV_RUN_VP
> without returning to qemu_process_cpu_events(). A busy vCPU may
> prevent pause_all_vcpus() from completing.
> 
> Check exit_request before re-entering MSHV_RUN_VP and return
> EXCP_INTERRUPT so pending stop requests can be processed.
> 
> Signed-off-by: Doru Blânzeanu <dblanzeanu@linux.microsoft.com>
> ---
>  accel/mshv/mshv-all.c | 10 ++++++++++
>  1 file changed, 10 insertions(+)
> 
> diff --git a/accel/mshv/mshv-all.c b/accel/mshv/mshv-all.c
> index afcb87b3b8..b7c1126fe0 100644
> --- a/accel/mshv/mshv-all.c
> +++ b/accel/mshv/mshv-all.c
> @@ -780,6 +780,16 @@ static int mshv_cpu_exec(CPUState *cpu)
>              cpu->vcpu_dirty = false;
>          }
>  
> +        /*
> +         * A kick/stop can't reliably break MSHV_RUN_VP out, so leave the run
> +         * loop here too; otherwise a busy vCPU never reaches
> +         * qemu_process_cpu_events() and pause_all_vcpus() hangs under the BQL.
> +         */
> +        if (cpu->stop || qatomic_load_acquire(&cpu->exit_request)) {
> +            ret = EXCP_INTERRUPT;
> +            break;
> +        }
> +
>          ret = mshv_run_vcpu(mshv_state->vm, cpu, &mshv_msg, &exit_reason);
>          if (ret < 0) {
>              error_report("Failed to run on vcpu %d", cpu->cpu_index);
> -- 
> 2.53.0

This is already fixed in upstream. checking the cpu->stop flag doesn't
seem to be required and other accelerators do not seem to check it
either. We agreed to drop this patch for now.


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

* Re: [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support
  2026-07-31 18:46 [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu
                   ` (3 preceding siblings ...)
  2026-07-31 18:46 ` [PATCH v2 4/4] accel/mshv: break the vCPU run loop on exit_request Doru Blânzeanu
@ 2026-10-12 12:35 ` Magnus Kulke
  4 siblings, 0 replies; 8+ messages in thread
From: Magnus Kulke @ 2026-10-12 12:35 UTC (permalink / raw)
  To: Doru Blânzeanu
  Cc: qemu-devel, Wei Liu, Wei Liu, Magnus Kulke, Doru Blânzeanu

On Fri, Jul 31, 2026 at 09:46:38PM +0300, Doru Blânzeanu wrote:
> This series brings guest debugging to the MSHV accelerator by
> implementing software breakpoints and single-stepping, so that gdb can
> attach to an MSHV guest through QEMU's gdbstub.
> 
> It follows the KVM gdbstub implementation closely, and borrows the WHPX
> approach where the MSHV interface differs from KVM:
> 
>   - it installs a partition-wide intercept for the #DB exception
>     (HVCALL_INSTALL_INTERCEPT). #BP (INT3) is deliberately left to the
>     guest's own IDT and is never intercepted, as WHPX does.
> 
>   - only software breakpoints are supported. A breakpoint is an INT1
>     (opcode 0xf1) patched into guest memory, which raises a #DB when
>     executed. Hardware breakpoints and watchpoints are not supported.
> 
>   - it never touches the guest debug registers (DR0-DR7), so guest state
>     is not clobbered. The only guest register used is RFLAGS.TF, toggled
>     around the vCPU run for single-stepping and kept out of env->eflags
>     so it cannot be read back and re-armed.
> 
>   - it re-injects the #DB back into the guest (via
>     HV_REGISTER_PENDING_EVENT0) when the faulting RIP does not match a
>     debugger breakpoint, so a guest-owned #DB is delivered through the
>     guest's own IDT.
> 
> When a vCPU is created we register the accel ops that gdb drives once it
> is attached:
> 
>   - mshv_update_guest_debug
>   - mshv_insert_gdbstub_breakpoint
>   - mshv_remove_gdbstub_breakpoint
>   - mshv_remove_all_gdbstub_breakpoints
> 
> For each insert request from gdb the ops save the original byte at the
> target address and patch in 0xf1; on a remove request the saved byte is
> restored, after which gdb single-steps over the original instruction and
> re-inserts the breakpoint. When the vCPU executes the 0xf1 byte a #DB
> vmexits to QEMU. mshv_handle_debug() then checks whether the RIP where
> the vCPU stopped matches a breakpoint installed by gdb, or whether
> single-stepping is active, and if so reports the stop to gdb. If the RIP
> does not match any debugger breakpoint the #DB is assumed to be
> guest-owned and is re-injected so the guest can handle it.
> 
> Known limitations:
> 
>   - Once installed, the #DB intercept stays for the lifetime
>     of the VM and every #DB - including those the guest raises itself -
>     vmexits to QEMU.
> 
>   - Because attribution relies on matching the faulting RIP against the
>     debugger's breakpoint list, a guest that legitimately executes INT1 or
>     raises its own #DB at an address that also holds a debugger breakpoint
>     cannot be disambiguated; such overlaps are not expected in practice.
> 
> The series is organised as:
> 
>   1. include/hw/hyperv: add ABI for exception intercepts and pending
>      events
>   2. accel/mshv: add gdbstub software breakpoint support
>   3. target/i386/mshv: support single-stepping
>   4. accel/mshv: break the vCPU run loop on exit_request
>      This commit fixes a race condition encountered during testing with guests
>      that run infinite loops with some debugging opcodes.
> 
> I have tested this patch series by using both a guest that doesn't use debugging
> and one that triggers the #DB and #BP exceptions (#DB is re-injected and #BP is
> not intercepted at all).
> 
> Changes since v1:
> - refactored the HVMSG_X64_EXCEPTION_INTERCEPT vmexit handle to be contained in
>   a standalone function handle_exception_interrupt
> - add new commit that checks the mshv vcpu run loop for exit requests before
>   re-entering MSHV_RUN_VP. This ensures a busy vCPU doesn't ignore gdb commands.
> 
> Doru Blânzeanu (4):
>   include/hw/hyperv: add ABI for exception intercepts and pending events
>   accel/mshv: add gdbstub software breakpoint support
>   target/i386/mshv: support single-stepping
>   accel/mshv: break the vCPU run loop on exit_request
> 
>  accel/mshv/mshv-all.c          | 175 ++++++++++++++++++++++++++++++++-
>  include/hw/hyperv/hvgdk_mini.h |  56 +++++++++++
>  include/system/mshv_int.h      |  14 +++
>  target/i386/mshv/mshv-cpu.c    | 154 +++++++++++++++++++++++++++++
>  4 files changed, 398 insertions(+), 1 deletion(-)
> 
> -- 
> 2.53.0

included in mkulke-pull-20261006, skipped 4/4



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

end of thread, other threads:[~2026-10-12 12:36 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31 18:46 [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Doru Blânzeanu
2026-07-31 18:46 ` [PATCH v2 1/4] include/hw/hyperv: add ABI for exception intercepts and pending events Doru Blânzeanu
2026-07-31 18:46 ` [PATCH v2 2/4] accel/mshv: add gdbstub software breakpoint support Doru Blânzeanu
2026-09-23 11:20   ` Magnus Kulke
2026-07-31 18:46 ` [PATCH v2 3/4] target/i386/mshv: support single-stepping Doru Blânzeanu
2026-07-31 18:46 ` [PATCH v2 4/4] accel/mshv: break the vCPU run loop on exit_request Doru Blânzeanu
2026-09-23 11:27   ` Magnus Kulke
2026-10-12 12:35 ` [PATCH v2 0/4] accel/mshv: add gdbstub guest debugging support Magnus Kulke

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.