| /* |
| * QEMU MSHV support |
| * |
| * Copyright Microsoft, Corp. 2025 |
| * |
| * Authors: Ziqiao Zhou <ziqiaozhou@microsoft.com> |
| * Magnus Kulke <magnuskulke@microsoft.com> |
| * Stanislav Kinsburskii <skinsburskii@microsoft.com> |
| * |
| * SPDX-License-Identifier: GPL-2.0-or-later |
| */ |
| |
| #include "qemu/osdep.h" |
| #include "linux/mshv.h" |
| #include "qemu/error-report.h" |
| #include "hw/hyperv/hvhdk_mini.h" |
| #include "hw/hyperv/hvgdk_mini.h" |
| #include "hw/intc/ioapic.h" |
| #include "hw/pci/msi.h" |
| #include "system/mshv.h" |
| #include "system/mshv_int.h" |
| #include "trace.h" |
| #include <sys/ioctl.h> |
| |
| #define MSHV_IRQFD_RESAMPLE_FLAG (1 << MSHV_IRQFD_BIT_RESAMPLE) |
| #define MSHV_IRQFD_BIT_DEASSIGN_FLAG (1 << MSHV_IRQFD_BIT_DEASSIGN) |
| |
| static MshvMsiControl *msi_control; |
| static QemuMutex msi_control_mutex; |
| |
| void mshv_init_msicontrol(void) |
| { |
| qemu_mutex_init(&msi_control_mutex); |
| msi_control = g_new0(MshvMsiControl, 1); |
| msi_control->gsi_routes = g_hash_table_new(g_direct_hash, g_direct_equal); |
| msi_control->updated = false; |
| } |
| |
| static int set_msi_routing(uint32_t gsi, uint64_t addr, uint32_t data) |
| { |
| struct mshv_user_irq_entry *entry; |
| uint32_t high_addr = addr >> 32; |
| uint32_t low_addr = addr & 0xFFFFFFFF; |
| GHashTable *gsi_routes; |
| |
| trace_mshv_set_msi_routing(gsi, addr, data); |
| |
| if (gsi >= MSHV_MAX_MSI_ROUTES) { |
| error_report("gsi >= MSHV_MAX_MSI_ROUTES"); |
| return -1; |
| } |
| |
| assert(msi_control); |
| |
| WITH_QEMU_LOCK_GUARD(&msi_control_mutex) { |
| gsi_routes = msi_control->gsi_routes; |
| entry = g_hash_table_lookup(gsi_routes, GINT_TO_POINTER(gsi)); |
| |
| if (entry |
| && entry->address_hi == high_addr |
| && entry->address_lo == low_addr |
| && entry->data == data) |
| { |
| /* nothing to update */ |
| return 0; |
| } |
| |
| /* free old entry */ |
| g_free(entry); |
| |
| /* create new entry */ |
| entry = g_new0(struct mshv_user_irq_entry, 1); |
| entry->gsi = gsi; |
| entry->address_hi = high_addr; |
| entry->address_lo = low_addr; |
| entry->data = data; |
| |
| g_hash_table_insert(gsi_routes, GINT_TO_POINTER(gsi), entry); |
| msi_control->updated = true; |
| } |
| |
| return 0; |
| } |
| |
| static int add_msi_routing(uint64_t addr, uint32_t data) |
| { |
| struct mshv_user_irq_entry *route_entry; |
| uint32_t high_addr = addr >> 32; |
| uint32_t low_addr = addr & 0xFFFFFFFF; |
| int gsi; |
| GHashTable *gsi_routes; |
| |
| trace_mshv_add_msi_routing(addr, data); |
| |
| assert(msi_control); |
| |
| WITH_QEMU_LOCK_GUARD(&msi_control_mutex) { |
| /* find an empty slot */ |
| gsi = 0; |
| gsi_routes = msi_control->gsi_routes; |
| while (gsi < MSHV_MAX_MSI_ROUTES) { |
| route_entry = g_hash_table_lookup(gsi_routes, GINT_TO_POINTER(gsi)); |
| if (!route_entry) { |
| break; |
| } |
| gsi++; |
| } |
| if (gsi >= MSHV_MAX_MSI_ROUTES) { |
| error_report("No empty gsi slot available"); |
| return -1; |
| } |
| |
| /* create new entry */ |
| route_entry = g_new0(struct mshv_user_irq_entry, 1); |
| route_entry->gsi = gsi; |
| route_entry->address_hi = high_addr; |
| route_entry->address_lo = low_addr; |
| route_entry->data = data; |
| |
| g_hash_table_insert(gsi_routes, GINT_TO_POINTER(gsi), route_entry); |
| msi_control->updated = true; |
| } |
| |
| return gsi; |
| } |
| |
| static int commit_msi_routing_table(int vm_fd) |
| { |
| guint len; |
| int i, ret; |
| size_t table_size; |
| struct mshv_user_irq_table *table; |
| GHashTableIter iter; |
| gpointer key, value; |
| |
| assert(msi_control); |
| |
| WITH_QEMU_LOCK_GUARD(&msi_control_mutex) { |
| if (!msi_control->updated) { |
| /* nothing to update */ |
| return 0; |
| } |
| |
| /* Calculate the size of the table */ |
| len = g_hash_table_size(msi_control->gsi_routes); |
| table_size = sizeof(struct mshv_user_irq_table) |
| + len * sizeof(struct mshv_user_irq_entry); |
| table = g_malloc0(table_size); |
| |
| g_hash_table_iter_init(&iter, msi_control->gsi_routes); |
| i = 0; |
| while (g_hash_table_iter_next(&iter, &key, &value)) { |
| struct mshv_user_irq_entry *entry = value; |
| table->entries[i] = *entry; |
| i++; |
| } |
| table->nr = i; |
| |
| trace_mshv_commit_msi_routing_table(vm_fd, len); |
| |
| ret = ioctl(vm_fd, MSHV_SET_MSI_ROUTING, table); |
| g_free(table); |
| if (ret < 0) { |
| error_report("Failed to commit msi routing table"); |
| return -1; |
| } |
| msi_control->updated = false; |
| } |
| return 0; |
| } |
| |
| static int remove_msi_routing(uint32_t gsi) |
| { |
| struct mshv_user_irq_entry *route_entry; |
| GHashTable *gsi_routes; |
| |
| trace_mshv_remove_msi_routing(gsi); |
| |
| if (gsi >= MSHV_MAX_MSI_ROUTES) { |
| error_report("Invalid GSI: %u", gsi); |
| return -1; |
| } |
| |
| assert(msi_control); |
| |
| WITH_QEMU_LOCK_GUARD(&msi_control_mutex) { |
| gsi_routes = msi_control->gsi_routes; |
| route_entry = g_hash_table_lookup(gsi_routes, GINT_TO_POINTER(gsi)); |
| if (route_entry) { |
| g_hash_table_remove(gsi_routes, GINT_TO_POINTER(gsi)); |
| g_free(route_entry); |
| msi_control->updated = true; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* Pass an eventfd which is to be used for injecting interrupts from userland */ |
| static int irqfd(int vm_fd, int fd, int resample_fd, uint32_t gsi, |
| uint32_t flags) |
| { |
| int ret; |
| struct mshv_user_irqfd arg = { |
| .fd = fd, |
| .resamplefd = resample_fd, |
| .gsi = gsi, |
| .flags = flags, |
| }; |
| |
| ret = ioctl(vm_fd, MSHV_IRQFD, &arg); |
| if (ret < 0) { |
| error_report("Failed to set irqfd: gsi=%u, fd=%d", gsi, fd); |
| return -1; |
| } |
| return ret; |
| } |
| |
| static int register_irqfd(int vm_fd, int event_fd, uint32_t gsi) |
| { |
| int ret; |
| |
| trace_mshv_register_irqfd(vm_fd, event_fd, gsi); |
| |
| ret = irqfd(vm_fd, event_fd, 0, gsi, 0); |
| if (ret < 0) { |
| error_report("Failed to register irqfd: gsi=%u", gsi); |
| return -1; |
| } |
| return 0; |
| } |
| |
| static int register_irqfd_with_resample(int vm_fd, int event_fd, |
| int resample_fd, uint32_t gsi) |
| { |
| int ret; |
| uint32_t flags = MSHV_IRQFD_RESAMPLE_FLAG; |
| |
| ret = irqfd(vm_fd, event_fd, resample_fd, gsi, flags); |
| if (ret < 0) { |
| error_report("Failed to register irqfd with resample: gsi=%u", gsi); |
| return -errno; |
| } |
| return 0; |
| } |
| |
| static int unregister_irqfd(int vm_fd, int event_fd, uint32_t gsi) |
| { |
| int ret; |
| uint32_t flags = MSHV_IRQFD_BIT_DEASSIGN_FLAG; |
| |
| ret = irqfd(vm_fd, event_fd, 0, gsi, flags); |
| if (ret < 0) { |
| error_report("Failed to unregister irqfd: gsi=%u", gsi); |
| return -errno; |
| } |
| return 0; |
| } |
| |
| static int irqchip_update_irqfd_notifier_gsi(const EventNotifier *event, |
| const EventNotifier *resample, |
| int virq, bool add) |
| { |
| int fd = event_notifier_get_fd(event); |
| int rfd = resample ? event_notifier_get_fd(resample) : -1; |
| int vm_fd = mshv_state->vm; |
| |
| trace_mshv_irqchip_update_irqfd_notifier_gsi(fd, rfd, virq, add); |
| |
| if (!add) { |
| return unregister_irqfd(vm_fd, fd, virq); |
| } |
| |
| if (rfd > 0) { |
| return register_irqfd_with_resample(vm_fd, fd, rfd, virq); |
| } |
| |
| return register_irqfd(vm_fd, fd, virq); |
| } |
| |
| |
| int mshv_irqchip_add_msi_route(int vector, PCIDevice *dev) |
| { |
| MSIMessage msg = { 0, 0 }; |
| int virq = 0; |
| |
| if (pci_available && dev) { |
| msg = pci_get_msi_message(dev, vector); |
| virq = add_msi_routing(msg.address, le32_to_cpu(msg.data)); |
| } |
| |
| return virq; |
| } |
| |
| void mshv_irqchip_release_virq(int virq) |
| { |
| remove_msi_routing(virq); |
| } |
| |
| int mshv_irqchip_update_msi_route(int virq, MSIMessage msg, PCIDevice *dev) |
| { |
| int ret; |
| |
| ret = set_msi_routing(virq, msg.address, le32_to_cpu(msg.data)); |
| if (ret < 0) { |
| error_report("Failed to set msi routing"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| int mshv_request_interrupt(MshvState *mshv_state, uint32_t interrupt_type, uint32_t vector, |
| uint32_t vp_index, bool logical_dest_mode, |
| bool level_triggered) |
| { |
| int ret; |
| int vm_fd = mshv_state->vm; |
| |
| if (vector == 0) { |
| warn_report("Ignoring request for interrupt vector 0"); |
| return 0; |
| } |
| |
| union hv_interrupt_control control = { |
| .interrupt_type = interrupt_type, |
| .level_triggered = level_triggered, |
| .logical_dest_mode = logical_dest_mode, |
| .rsvd = 0, |
| }; |
| |
| struct hv_input_assert_virtual_interrupt arg = {0}; |
| arg.control = control; |
| arg.dest_addr = (uint64_t)vp_index; |
| arg.vector = vector; |
| |
| struct mshv_root_hvcall args = {0}; |
| args.code = HVCALL_ASSERT_VIRTUAL_INTERRUPT; |
| args.in_sz = sizeof(arg); |
| args.in_ptr = (uint64_t)&arg; |
| |
| ret = mshv_hvcall(vm_fd, &args); |
| if (ret < 0) { |
| error_report("Failed to request interrupt"); |
| return -errno; |
| } |
| return 0; |
| } |
| |
| void mshv_irqchip_commit_routes(void) |
| { |
| int ret; |
| int vm_fd = mshv_state->vm; |
| |
| ret = commit_msi_routing_table(vm_fd); |
| if (ret < 0) { |
| error_report("Failed to commit msi routing table"); |
| abort(); |
| } |
| } |
| |
| int mshv_irqchip_add_irqfd_notifier_gsi(const EventNotifier *event, |
| const EventNotifier *resample, |
| int virq) |
| { |
| return irqchip_update_irqfd_notifier_gsi(event, resample, virq, true); |
| } |
| |
| int mshv_irqchip_remove_irqfd_notifier_gsi(const EventNotifier *event, |
| int virq) |
| { |
| return irqchip_update_irqfd_notifier_gsi(event, NULL, virq, false); |
| } |
| |
| int mshv_reserve_ioapic_msi_routes(int vm_fd) |
| { |
| int ret, gsi; |
| |
| /* |
| * Reserve GSI 0-23 for IOAPIC pins, to avoid conflicts of legacy |
| * peripherals with MSI-X devices |
| */ |
| for (gsi = 0; gsi < IOAPIC_NUM_PINS; gsi++) { |
| ret = add_msi_routing(0, 0); |
| if (ret < 0) { |
| error_report("Failed to reserve GSI %d", gsi); |
| return -1; |
| } |
| } |
| |
| ret = commit_msi_routing_table(vm_fd); |
| if (ret < 0) { |
| error_report("Failed to commit reserved IOAPIC MSI routes"); |
| return -1; |
| } |
| |
| return 0; |
| } |