/* * QEMU MSHV support * * Copyright Microsoft, Corp. 2025 * * Authors: * Magnus Kulke * * SPDX-License-Identifier: GPL-2.0-or-later * */ #include "qemu/osdep.h" #include "qemu/error-report.h" #include "qapi/error.h" #include "linux/mshv.h" #include "system/address-spaces.h" #include "system/mshv.h" #include "system/mshv_int.h" #include "hw/hyperv/hvhdk_mini.h" #include "system/physmem.h" #include "exec/memattrs.h" #include #include "trace.h" static int set_guest_memory(int vm_fd, const struct mshv_user_mem_region *region) { int ret; ret = ioctl(vm_fd, MSHV_SET_GUEST_MEMORY, region); if (ret < 0) { error_report("failed to set guest memory"); return -errno; } return 0; } static int map_or_unmap(int vm_fd, const MshvMemoryRegion *mr, bool map) { struct mshv_user_mem_region region = {0}; region.guest_pfn = mr->guest_phys_addr >> MSHV_PAGE_SHIFT; region.size = mr->memory_size; region.userspace_addr = mr->userspace_addr; if (!map) { region.flags |= (1 << MSHV_SET_MEM_BIT_UNMAP); trace_mshv_unmap_memory(mr->userspace_addr, mr->guest_phys_addr, mr->memory_size); return set_guest_memory(vm_fd, ®ion); } region.flags = BIT(MSHV_SET_MEM_BIT_EXECUTABLE); if (!mr->readonly) { region.flags |= BIT(MSHV_SET_MEM_BIT_WRITABLE); } trace_mshv_map_memory(mr->userspace_addr, mr->guest_phys_addr, mr->memory_size); return set_guest_memory(vm_fd, ®ion); } static int handle_unmapped_mmio_region_read(uint64_t gpa, uint64_t size, uint8_t *data) { warn_report("read from unmapped mmio region gpa=0x%lx size=%lu", gpa, size); if (size == 0 || size > 8) { error_report("invalid size %lu for reading from unmapped mmio region", size); return -1; } memset(data, 0xFF, size); return 0; } int mshv_guest_mem_read(uint64_t gpa, uint8_t *data, uintptr_t size, bool is_secure_mode, bool instruction_fetch) { int ret; MemTxAttrs memattr = { .secure = is_secure_mode }; if (instruction_fetch) { trace_mshv_insn_fetch(gpa, size); } else { trace_mshv_mem_read(gpa, size); } ret = address_space_rw(&address_space_memory, gpa, memattr, (void *)data, size, false); if (ret == MEMTX_OK) { return 0; } if (ret == MEMTX_DECODE_ERROR) { return handle_unmapped_mmio_region_read(gpa, size, data); } error_report("failed to read guest memory at 0x%lx", gpa); return -1; } int mshv_guest_mem_write(uint64_t gpa, const uint8_t *data, uintptr_t size, bool is_secure_mode) { int ret; MemTxAttrs memattr = { .secure = is_secure_mode }; trace_mshv_mem_write(gpa, size); ret = address_space_rw(&address_space_memory, gpa, memattr, (void *)data, size, true); if (ret == MEMTX_OK) { return 0; } if (ret == MEMTX_DECODE_ERROR) { warn_report("write to unmapped mmio region gpa=0x%lx size=%lu", gpa, size); return 0; } error_report("Failed to write guest memory"); return -1; } static int set_memory(const MshvMemoryRegion *mshv_mr, bool add) { int ret = 0; if (!mshv_mr) { error_report("Invalid mshv_mr"); return -1; } trace_mshv_set_memory(add, mshv_mr->guest_phys_addr, mshv_mr->memory_size, mshv_mr->userspace_addr, mshv_mr->readonly, ret); return map_or_unmap(mshv_state->vm, mshv_mr, add); } /* * Calculate and align the start address and the size of the section. * Return the size. If the size is 0, the aligned section is empty. */ static hwaddr align_section(MemoryRegionSection *section, hwaddr *start) { hwaddr size = int128_get64(section->size); hwaddr delta, aligned; /* * works in page size chunks, but the function may be called * with sub-page size and unaligned start address. Pad the start * address to next and truncate size to previous page boundary. */ aligned = ROUND_UP(section->offset_within_address_space, qemu_real_host_page_size()); delta = aligned - section->offset_within_address_space; *start = aligned; if (delta > size) { return 0; } return (size - delta) & qemu_real_host_page_mask(); } void mshv_set_phys_mem(MshvMemoryListener *mml, MemoryRegionSection *section, bool add) { int ret = 0; MemoryRegion *area = section->mr; bool writable = !area->readonly && !area->rom_device; hwaddr start_addr, mr_offset, size; void *ram; MshvMemoryRegion mshv_mr = {0}; size = align_section(section, &start_addr); trace_mshv_set_phys_mem(add, section->mr->name, start_addr); /* * If the memory device is a writable non-ram area, we do not * want to map it into the guest memory. If it is not a ROM device, * we want to remove mshv memory mapping, so accesses will trap. */ if (!memory_region_is_ram(area)) { if (writable) { return; } else if (!area->romd_mode) { add = false; } } if (!size) { return; } mr_offset = section->offset_within_region + start_addr - section->offset_within_address_space; ram = memory_region_get_ram_ptr(area) + mr_offset; mshv_mr.guest_phys_addr = start_addr; mshv_mr.memory_size = size; mshv_mr.readonly = !writable; mshv_mr.userspace_addr = (uint64_t)ram; ret = set_memory(&mshv_mr, add); if (ret < 0) { error_report("Failed to set memory region"); abort(); } } static int enable_dirty_page_tracking(int vm_fd) { int ret; struct hv_input_set_partition_property in = {0}; struct mshv_root_hvcall args = {0}; in.property_code = HV_PARTITION_PROPERTY_GPA_PAGE_ACCESS_TRACKING; in.property_value = 1; args.code = HVCALL_SET_PARTITION_PROPERTY; args.in_sz = sizeof(in); args.in_ptr = (uint64_t)∈ ret = mshv_hvcall(vm_fd, &args); if (ret < 0) { error_report("Failed to enable dirty page tracking: %s", strerror(errno)); return -1; } return 0; } /* * Retrieve dirty page bitmap for a GPA range, clearing the dirty bits * atomically. Large ranges are handled in batches. */ static int get_dirty_log(int vm_fd, uint64_t base_pfn, uint64_t page_count, unsigned long *bitmap, size_t bitmap_size) { uint64_t batch, bitmap_offset, completed = 0; struct mshv_gpap_access_bitmap args = {0}; int ret; QEMU_BUILD_BUG_ON(MSHV_DIRTY_PAGES_BATCH_SIZE % BITS_PER_LONG != 0); assert(bitmap_size >= ROUND_UP(page_count, BITS_PER_LONG) / 8); while (completed < page_count) { batch = MIN(MSHV_DIRTY_PAGES_BATCH_SIZE, page_count - completed); bitmap_offset = completed / BITS_PER_LONG; args.access_type = MSHV_GPAP_ACCESS_TYPE_DIRTY; args.access_op = MSHV_GPAP_ACCESS_OP_CLEAR; args.page_count = batch; args.gpap_base = base_pfn + completed; args.bitmap_ptr = (uint64_t)(bitmap + bitmap_offset); ret = ioctl(vm_fd, MSHV_GET_GPAP_ACCESS_BITMAP, &args); if (ret < 0) { error_report("Failed to get dirty log (base_pfn=0x%" PRIx64 " batch=%" PRIu64 "): %s", base_pfn + completed, batch, strerror(errno)); return -1; } completed += batch; } return 0; } bool mshv_log_global_start(MemoryListener *listener, Error **errp) { int ret; ret = enable_dirty_page_tracking(mshv_state->vm); if (ret < 0) { error_setg_errno(errp, -ret, "Failed to enable dirty page tracking"); return false; } return true; } static int disable_dirty_page_tracking(int vm_fd) { int ret; struct hv_input_set_partition_property in = {0}; struct mshv_root_hvcall args = {0}; in.property_code = HV_PARTITION_PROPERTY_GPA_PAGE_ACCESS_TRACKING; in.property_value = 0; args.code = HVCALL_SET_PARTITION_PROPERTY; args.in_sz = sizeof(in); args.in_ptr = (uint64_t)∈ ret = mshv_hvcall(vm_fd, &args); if (ret < 0) { error_report("Failed to disable dirty page tracking: %s", strerror(errno)); return -1; } return 0; } static int set_dirty_pages(int vm_fd, uint64_t base_pfn, uint64_t page_count) { uint64_t batch, completed = 0; unsigned long bitmap[MSHV_DIRTY_PAGES_BATCH_SIZE / BITS_PER_LONG]; struct mshv_gpap_access_bitmap args = {0}; int ret; while (completed < page_count) { batch = MIN(MSHV_DIRTY_PAGES_BATCH_SIZE, page_count - completed); args.access_type = MSHV_GPAP_ACCESS_TYPE_DIRTY; args.access_op = MSHV_GPAP_ACCESS_OP_SET; args.page_count = batch; args.gpap_base = base_pfn + completed; args.bitmap_ptr = (uint64_t)bitmap; ret = ioctl(vm_fd, MSHV_GET_GPAP_ACCESS_BITMAP, &args); if (ret < 0) { error_report("Failed to set dirty pages (base_pfn=0x%" PRIx64 " batch=%" PRIu64 "): %s", base_pfn + completed, batch, strerror(errno)); return -1; } completed += batch; } return 0; } static bool set_dirty_bits_cb(Int128 start, Int128 len, const MemoryRegion *mr, hwaddr offset_in_region, void *opaque) { int ret, *errp = opaque; hwaddr gpa, size; uint64_t page_count, base_pfn; gpa = int128_get64(start); size = int128_get64(len); page_count = size >> MSHV_PAGE_SHIFT; base_pfn = gpa >> MSHV_PAGE_SHIFT; if (!mr->ram || mr->readonly) { return false; } if (page_count == 0) { return false; } ret = set_dirty_pages(mshv_state->vm, base_pfn, page_count); /* true aborts the iteration, which is what we want if there's an error */ if (ret < 0) { *errp = ret; return true; } return false; } void mshv_log_global_stop(MemoryListener *listener) { int err = 0; /* MSHV requires all dirty bits to be set before disabling tracking. */ FlatView *fv = address_space_to_flatview(&address_space_memory); flatview_for_each_range(fv, set_dirty_bits_cb, &err); if (err < 0) { error_report("Failed to set dirty bits before disabling tracking"); } disable_dirty_page_tracking(mshv_state->vm); } void mshv_log_sync(MemoryListener *listener, MemoryRegionSection *section) { hwaddr size, start_addr, mr_offset; uint64_t page_count, base_pfn; size_t bitmap_size; unsigned long *bitmap; ram_addr_t ram_addr; int ret; MemoryRegion *mr = section->mr; if (!memory_region_is_ram(mr) || memory_region_is_rom(mr)) { return; } size = align_section(section, &start_addr); if (!size) { return; } page_count = size >> MSHV_PAGE_SHIFT; base_pfn = start_addr >> MSHV_PAGE_SHIFT; bitmap_size = ROUND_UP(page_count, BITS_PER_LONG) / 8; bitmap = g_malloc0(bitmap_size); ret = get_dirty_log(mshv_state->vm, base_pfn, page_count, bitmap, bitmap_size); if (ret < 0) { g_free(bitmap); return; } mr_offset = section->offset_within_region + start_addr - section->offset_within_address_space; ram_addr = memory_region_get_ram_addr(mr) + mr_offset; physical_memory_set_dirty_lebitmap(bitmap, ram_addr, page_count); g_free(bitmap); }