Skip to content

Instantly share code, notes, and snippets.

@lhotari
Last active April 11, 2026 14:01
Show Gist options
  • Select an option

  • Save lhotari/907e903f3360af340aac42b2f3c458ea to your computer and use it in GitHub Desktop.

Select an option

Save lhotari/907e903f3360af340aac42b2f3c458ea to your computer and use it in GitHub Desktop.

Migrating a QuickEmu Windows 11 VM to libvirt with nested Hyper-V support

This guide documents migrating a Windows 11 VM created with QuickEmu to libvirt/virt-manager, with nested virtualization support for running Hyper-V, WSL2, and nested VMs inside Windows.

Tested on Pop!_OS 24.04 with an Intel Core i9-9980HK (Coffee Lake-H), Dell XPS 7590 (2019).

Prerequisites

Install quickemu and quickgui

sudo apt-add-repository ppa:flexiondotorg/quickemu
sudo apt update
sudo apt install quickemu quickgui qemu-system-modules-spice

Install libvirt and virt-manager

sudo apt install qemu-kvm libvirt-daemon-system virt-manager passt ovmf swtpm

QuickEmu for automated Windows installation

mkdir ~/QuickEmu
cd ~/QuickEmu
quickget windows 11
# quickget download will likely fail with error "WARNING! Microsoft blocked the automated download request based on your IP address."
# go to https://www.microsoft.com/en-us/software-download/windows11 with the browser and download the ISO file, store to ~/QuickEmu/windows-11/windows-11.iso
# when start 
[ -f windows-11/windows-11.iso ] && quickemu --vm windows-11.conf || echo "Did you download the windows 11 iso file manually from https://www.microsoft.com/en-us/software-download/windows11? Download it to $PWD/windows-11/windows-11.iso."

Enable nested virtualization on the host

# Check current state
cat /sys/module/kvm_intel/parameters/nested

# Enable persistently
sudo tee /etc/modprobe.d/kvm-nested.conf <<EOF
options kvm-intel nested=1
EOF

# Reload (no VMs running)
sudo modprobe -r kvm_intel
sudo modprobe kvm_intel

# Verify
cat /sys/module/kvm_intel/parameters/nested
# Should output: Y

Step 1: Copy VM files to libvirt storage

# Create directory
sudo mkdir -p /var/lib/libvirt/images/windows-11

# Copy disk image and virtio drivers ISO from QuickEmu directory
sudo cp ~/QuickEmu/windows-11/disk.qcow2 /var/lib/libvirt/images/windows-11/
sudo cp ~/QuickEmu/windows-11/virtio-win.iso /var/lib/libvirt/images/windows-11/

# Set ownership
sudo chown -R libvirt-qemu:kvm /var/lib/libvirt/images/windows-11/

Step 2: Define the VM

Download the libvirt XML configuration and define the VM:

sudo virsh define windows-11-wsl2.xml

Step 3: Preserve TPM state (optional)

If your existing Windows install has BitLocker or other TPM-bound secrets, preserve the TPM state from your QuickEmu setup:

#!/bin/bash
set -euo pipefail
domuuid=$(sudo virsh domuuid windows-11)
sudo mkdir -p "/var/lib/libvirt/swtpm/${domuuid}/tpm2"
sudo cp ~/QuickEmu/windows-11/tpm2-00.permall "/var/lib/libvirt/swtpm/${domuuid}/tpm2/tpm2-00.permall"
sudo chown -R swtpm:swtpm "/var/lib/libvirt/swtpm/${domuuid}"
sudo chmod 700 "/var/lib/libvirt/swtpm/${domuuid}"
sudo chmod 600 "/var/lib/libvirt/swtpm/${domuuid}/tpm2/tpm2-00.permall"
echo "TPM state restored for VM ${domuuid}"

Step 4: Start the VM

sudo virsh start windows-11

Or open virt-manager and double-click the VM. You can open the console in virt-manager.

Step 5: Enable nested Hyper-V inside Windows

Open an elevated PowerShell and enable the required features:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -NoRestart
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart
Restart-Computer

After reboot, set the classic scheduler (required for nested KVM environments):

bcdedit /set hypervisorschedulertype classic
Restart-Computer

Step 6: Verify nested virtualization

# Check if the hypervisor is running (should return True)
(Get-CimInstance Win32_ComputerSystem).HypervisorPresent

# Check all features are enabled
Get-WindowsOptionalFeature -Online | Where-Object {$_.State -eq 'Enabled'} |
  Where-Object {$_.FeatureName -match 'Hyper|Virtual|WSL|Subsystem'} |
  Format-Table FeatureName, State

# Check Hyper-V services are running
Get-Service vmcompute, vmms | Format-Table Name, Status

# Check boot configuration
bcdedit /enum "{current}" | findstr hypervisor

Step 7: Install WSL2 (optional)

wsl --update --web-download
wsl --set-default-version 2
wsl --install Ubuntu

Step 8: Enable SSH access to the Windows VM

Install OpenSSH Server on Windows

Open an elevated PowerShell and install the OpenSSH Server feature:

# Install the OpenSSH Server capability
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*' | Add-WindowsCapability -Online

# Start the SSH service
Start-Service sshd

# Set it to start automatically on boot
Set-Service -Name sshd -StartupType Automatic

# Verify it's running
Get-Service sshd

# Allow SSH through Windows Firewall
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -DisplayName "OpenSSH Server (sshd)" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

Configure SSH public key authentication

When the Windows user (e.g. quickemu) is a member of the Administrators group, Windows OpenSSH ignores ~/.ssh/authorized_keys and instead reads from C:\ProgramData\ssh\administrators_authorized_keys.

Set your public key in the correct file:

$authorizedKeysFile = "C:\ProgramData\ssh\administrators_authorized_keys"
$publicKey = "ssh-ed25519 AAAA... your-key-comment"

# Make the file writable before modifying
icacls $authorizedKeysFile /grant "BUILTIN\Administrators:(F)"

# Set a key (overwrites existing content)
Set-Content -Path $authorizedKeysFile -Value $publicKey -Encoding UTF8

# Or append a key to an existing file
Add-Content -Path $authorizedKeysFile -Value $publicKey -Encoding UTF8

# Lock down permissions again (required for sshd to use the file)
icacls $authorizedKeysFile /inheritance:r /grant "SYSTEM:(R)" /grant "BUILTIN\Administrators:(R)"

Verify file permissions

The administrators_authorized_keys file must be owned by SYSTEM and Administrators, with no inherited permissions. If permissions are wrong, sshd silently ignores the file.

icacls "C:\ProgramData\ssh\administrators_authorized_keys"

Connect from the host

Since the VM uses passt networking with port forwarding (port 22220 → 22):

ssh -p 22220 quickemu@localhost

Libvirt XML key decisions

CPU model

Uses a named CPU model (Skylake-Client-IBRS) instead of host-passthrough. This is the key setting that makes nested Hyper-V work — host-passthrough causes Windows to crash when Hyper-V is enabled, while a named CPU model avoids the conflict and still exposes VMX for nested virtualization.

<cpu mode='custom' match='exact' check='partial'>
  <model fallback='allow'>Skylake-Client-IBRS</model>
  <topology sockets='1' dies='1' cores='4' threads='2'/>
  <feature policy='require' name='vmx'/>
  <feature policy='disable' name='mpx'/>
</cpu>
  • vmx — exposes Intel VT-x to the guest for nested virtualization
  • mpx disabled — Memory Protection Extensions removed, known to cause issues with nested Hyper-V

Adjust the CPU model to match your host. Check available models with:

virsh cpu-models x86_64

Note: hiding KVM from the guest (<kvm><hidden state='on'/>) and disabling the hypervisor CPUID bit are not necessary — the named CPU model is what resolves the conflict with nested Hyper-V.

Hyper-V enlightenments

<hyperv mode='passthrough'>

Passes through all compatible Hyper-V enlightenments. The following features were explicitly excluded because they require APICv/posted interrupts which may not be available on all CPUs:

  • evmcs — Enlightened VMCS, requires APICv
  • emsr-bitmap — requires APICv
  • reenlightenment — only needed for live migration

evmcs would improve performance of nested virtualization if it's available. It's not available for mobile CPUs such as Intel Core i9-9980HK. For desktop / server CPUs, it's most likely available.

Memory

Static memory allocation with balloon disabled. Memory ballooning conflicts with Hyper-V's memory management:

<memory unit='GiB'>10</memory>
<currentMemory unit='GiB'>10</currentMemory>
<!-- ... -->
<memballoon model='none'/>

Networking

Uses passt backend for user-mode networking with port forwarding. IPv6 is disabled to avoid errors on hosts with IPv6 turned off. SSH into the guest:

ssh -p 22220 localhost

Hyper-V scheduler

The classic scheduler must be used instead of the default core scheduler. The core scheduler requires accurate SMT topology information which may not be properly exposed in a nested environment:

bcdedit /set hypervisorschedulertype classic

Troubleshooting

VM fails to define with firmware error

Check which OVMF files are available:

ls /usr/share/OVMF/

Update the <loader> path in the XML to match. Common variants:

  • /usr/share/OVMF/OVMF_CODE_4M.secboot.fd
  • /usr/share/OVMF/OVMF_CODE.secboot.fd
  • /usr/share/edk2/ovmf/OVMF_CODE.secboot.fd

Or let libvirt auto-select by removing the <loader> line entirely.

passt permission errors

sudo apt install passt

If port forwarding uses a privileged port (below 1024), change to a high port. If IPv6 is disabled on the host, add <options ipv6='no'/> to the passt backend.

Windows enters automatic repair after XML changes

Reset NVRAM and redefine:

sudo virsh destroy windows-11
sudo virsh undefine windows-11 --nvram
sudo virsh define windows-11-wsl2.xml
sudo virsh start windows-11

HypervisorPresent returns False

The Hyper-V hypervisor isn't launching. Check event logs:

Get-WinEvent -LogName "Microsoft-Windows-Hyper-V-Hypervisor-Admin" -MaxEvents 20 |
  Format-Table TimeCreated, LevelDisplayName, Message -Wrap

Verify boot configuration:

bcdedit /enum "{current}" | findstr hypervisor

hypervisorlaunchtype should be Auto and hypervisorschedulertype should be Classic.

Backup NVRAM and TPM before changes

# Backup NVRAM
sudo cp /var/lib/libvirt/qemu/nvram/windows-11_VARS.fd \
  /var/lib/libvirt/qemu/nvram/windows-11_VARS.fd.bak

# Backup TPM
domuuid=$(sudo virsh domuuid windows-11)
sudo cp -a "/var/lib/libvirt/swtpm/${domuuid}" \
  "/var/lib/libvirt/swtpm/${domuuid}.bak"

References

<domain type='kvm'>
<name>windows-11</name>
<metadata/>
<memory unit='GiB'>10</memory>
<currentMemory unit='GiB'>10</currentMemory>
<memoryBacking>
<nosharepages/>
</memoryBacking>
<vcpu placement='static'>8</vcpu>
<cpu mode='custom' match='exact' check='partial'>
<model fallback='allow'>Skylake-Client-IBRS</model>
<topology sockets='1' dies='1' cores='4' threads='2'/>
<feature policy='require' name='vmx'/>
<feature policy='disable' name='mpx'/>
</cpu>
<features>
<acpi/>
<apic/>
<hyperv mode='passthrough'>
<relaxed state='on'/>
<vapic state='on'/>
<spinlocks state='on' retries='8191'/>
<vpindex state='on'/>
<synic state='on'/>
<stimer state='on'>
<direct state='on'/>
</stimer>
<reset state='on'/>
<frequencies state='on'/>
<tlbflush state='on'/>
</hyperv>
<vmport state='off'/>
<smm state='on'>
<tseg unit='MiB'>48</tseg>
</smm>
</features>
<os firmware='efi'>
<type arch='x86_64' machine='q35'>hvm</type>
<firmware>
<feature enabled='yes' name='secure-boot'/>
</firmware>
<loader readonly='yes' type='pflash' secure='yes'>/usr/share/OVMF/OVMF_CODE_4M.secboot.fd</loader>
</os>
<clock offset='localtime'>
<timer name='rtc' tickpolicy='catchup'/>
<timer name='pit' tickpolicy='discard'/>
<timer name='hpet' present='no'/>
<timer name='hypervclock' present='yes'/>
<timer name='tsc' present='yes' mode='native'/>
</clock>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<pm>
<suspend-to-mem enabled='no'/>
<suspend-to-disk enabled='no'/>
</pm>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' cache='writeback' io='threads'
discard='unmap' detect_zeroes='unmap'/>
<source file='/var/lib/libvirt/images/windows-11/disk.qcow2'/>
<target dev='vda' bus='virtio'/>
</disk>
<disk type='file' device='cdrom'>
<driver name='qemu' type='raw'/>
<source file='/var/lib/libvirt/images/windows-11/virtio-win.iso'/>
<target dev='sda' bus='sata'/>
<readonly/>
</disk>
<interface type='user'>
<mac address='52:54:00:00:00:01'/>
<model type='virtio'/>
<backend type='passt'>
<options ipv6='no'/>
</backend>
<portForward proto='tcp'>
<range start='22220' to='22'/>
</portForward>
</interface>
<video>
<model type='qxl' ram='65536' vram='65536' vgamem='65536' heads='1' primary='yes'>
<resolution x='1920' y='1080'/>
</model>
</video>
<graphics type='spice' autoport='yes'/>
<!-- Clipboard, mouse integration, dynamic resolution -->
<channel type='spicevmc'>
<target type='virtio' name='com.redhat.spice.0'/>
</channel>
<!-- Graceful shutdown, snapshots, guest info queries -->
<channel type='unix'>
<source mode='bind' path='/var/lib/libvirt/images/windows-11/windows-11-agent.sock'/>
<target type='virtio' name='org.qemu.guest_agent.0'/>
</channel>
<!-- Folder sharing -->
<channel type='spiceport'>
<source channel='org.spice-space.webdav.0'/>
<target type='virtio' name='org.spice-space.webdav.0'/>
</channel>
<controller type='usb' model='qemu-xhci'/>
<input type='tablet' bus='usb'/>
<sound model='ich9'>
<audio id='1'/>
</sound>
<audio id='1' type='spice'/>
<memballoon model='none'/>
<rng model='virtio'>
<backend model='random'>/dev/urandom</backend>
</rng>
<tpm model='tpm-tis'>
<backend type='emulator' version='2.0'>
<active_pcr_banks>
<sha256/>
</active_pcr_banks>
</backend>
</tpm>
</devices>
</domain>
@lhotari

lhotari commented Apr 11, 2026

Copy link
Copy Markdown
Author

VM was created with QuickEmu and then moved to libvirt / virt-manager.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment