]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
use non SMM ovmf code file for i440fx machines
[qemu-server.git] / PVE / QemuServer.pm
index 3c0ecf5946a8753d5b7dbb34501ce10002db792d..794558bff2471a0e3dabbc4adf49f25618097695 100644 (file)
@@ -22,7 +22,7 @@ use JSON;
 use MIME::Base64;
 use POSIX;
 use Storable qw(dclone);
-use Time::HiRes qw(gettimeofday);
+use Time::HiRes qw(gettimeofday usleep);
 use URI::Escape;
 use UUID;
 
@@ -64,6 +64,14 @@ eval {
 my $EDK2_FW_BASE = '/usr/share/pve-edk2-firmware/';
 my $OVMF = {
     x86_64 => {
+       '4m-no-smm' => [
+           "$EDK2_FW_BASE/OVMF_CODE_4M.fd",
+           "$EDK2_FW_BASE/OVMF_VARS_4M.fd",
+       ],
+       '4m-no-smm-ms' => [
+           "$EDK2_FW_BASE/OVMF_CODE_4M.fd",
+           "$EDK2_FW_BASE/OVMF_VARS_4M.ms.fd",
+       ],
        '4m' => [
            "$EDK2_FW_BASE/OVMF_CODE_4M.secboot.fd",
            "$EDK2_FW_BASE/OVMF_VARS_4M.fd",
@@ -87,12 +95,10 @@ my $OVMF = {
 
 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
 
-# Note about locking: we use flock on the config file protect
-# against concurent actions.
-# Aditionaly, we have a 'lock' setting in the config file. This
-# can be set to 'migrate', 'backup', 'snapshot' or 'rollback'. Most actions are not
-# allowed when such lock is set. But you can ignore this kind of
-# lock with the --skiplock flag.
+# Note about locking: we use flock on the config file protect against concurent actions.
+# Aditionaly, we have a 'lock' setting in the config file. This  can be set to 'migrate',
+# 'backup', 'snapshot' or 'rollback'. Most actions are not allowed when such lock is set.
+# But you can ignore this kind of lock with the --skiplock flag.
 
 cfs_register_file('/qemu-server/',
                  \&parse_vm_config,
@@ -163,7 +169,7 @@ PVE::JSONSchema::register_format('pve-qm-watchdog', $watchdog_fmt);
 
 my $agent_fmt = {
     enabled => {
-       description => "Enable/disable Qemu GuestAgent.",
+       description => "Enable/disable communication with a Qemu Guest Agent (QGA) running in the VM.",
        type => 'boolean',
        default => 0,
        default_key => 1,
@@ -252,33 +258,29 @@ my $rng_fmt = {
        type => 'string',
        enum => ['/dev/urandom', '/dev/random', '/dev/hwrng'],
        default_key => 1,
-       description => "The file on the host to gather entropy from. In most"
-                    . " cases /dev/urandom should be preferred over /dev/random"
-                    . " to avoid entropy-starvation issues on the host. Using"
-                    . " urandom does *not* decrease security in any meaningful"
-                    . " way, as it's still seeded from real entropy, and the"
-                    . " bytes provided will most likely be mixed with real"
-                    . " entropy on the guest as well. /dev/hwrng can be used"
-                    . " to pass through a hardware RNG from the host.",
+       description => "The file on the host to gather entropy from. In most cases '/dev/urandom'"
+           ." should be preferred over '/dev/random' to avoid entropy-starvation issues on the"
+           ." host. Using urandom does *not* decrease security in any meaningful way, as it's"
+           ." still seeded from real entropy, and the bytes provided will most likely be mixed"
+           ." with real entropy on the guest as well. '/dev/hwrng' can be used to pass through"
+           ." a hardware RNG from the host.",
     },
     max_bytes => {
        type => 'integer',
-       description => "Maximum bytes of entropy injected into the guest every"
-                    . " 'period' milliseconds. Prefer a lower value when using"
-                    . " /dev/random as source. Use 0 to disable limiting"
-                    . " (potentially dangerous!).",
+       description => "Maximum bytes of entropy allowed to get injected into the guest every"
+           ." 'period' milliseconds. Prefer a lower value when using '/dev/random' as source. Use"
+           ." `0` to disable limiting (potentially dangerous!).",
        optional => 1,
 
-       # default is 1 KiB/s, provides enough entropy to the guest to avoid
-       # boot-starvation issues (e.g. systemd etc...) while allowing no chance
-       # of overwhelming the host, provided we're reading from /dev/urandom
+       # default is 1 KiB/s, provides enough entropy to the guest to avoid boot-starvation issues
+       # (e.g. systemd etc...) while allowing no chance of overwhelming the host, provided we're
+       # reading from /dev/urandom
        default => 1024,
     },
     period => {
        type => 'integer',
-       description => "Every 'period' milliseconds the entropy-injection quota"
-                    . " is reset, allowing the guest to retrieve another"
-                    . " 'max_bytes' of entropy.",
+       description => "Every 'period' milliseconds the entropy-injection quota is reset, allowing"
+           ." the guest to retrieve another 'max_bytes' of entropy.",
        optional => 1,
        default => 1000,
     },
@@ -298,9 +300,11 @@ my $confdesc = {
        default => 0,
     },
     hotplug => {
-        optional => 1,
-        type => 'string', format => 'pve-hotplug-features',
-        description => "Selectively enable hotplug features. This is a comma separated list of hotplug features: 'network', 'disk', 'cpu', 'memory' and 'usb'. Use '0' to disable hotplug completely. Value '1' is an alias for the default 'network,disk,usb'.",
+       optional => 1,
+       type => 'string', format => 'pve-hotplug-features',
+       description => "Selectively enable hotplug features. This is a comma separated list of"
+           ." hotplug features: 'network', 'disk', 'cpu', 'memory' and 'usb'. Use '0' to disable"
+           ." hotplug completely. Using '1' as value is an alias for the default `network,disk,usb`.",
         default => 'network,disk,usb',
     },
     reboot => {
@@ -319,15 +323,16 @@ my $confdesc = {
        optional => 1,
        type => 'number',
        description => "Limit of CPU usage.",
-        verbose_description => "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has total of '2' CPU time. Value '0' indicates no CPU limit.",
+       verbose_description => "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has"
+           ." total of '2' CPU time. Value '0' indicates no CPU limit.",
        minimum => 0,
        maximum => 128,
-        default => 0,
+       default => 0,
     },
     cpuunits => {
        optional => 1,
        type => 'integer',
-        description => "CPU weight for a VM, will be clamped to [1, 10000] in cgroup v2.",
+       description => "CPU weight for a VM, will be clamped to [1, 10000] in cgroup v2.",
        verbose_description => "CPU weight for a VM. Argument is used in the kernel fair scheduler."
            ." The larger the number is, the more CPU time this VM gets. Number is relative to"
            ." weights of all the other running VMs.",
@@ -338,20 +343,23 @@ my $confdesc = {
     memory => {
        optional => 1,
        type => 'integer',
-       description => "Amount of RAM for the VM in MB. This is the maximum available memory when you use the balloon device.",
+       description => "Amount of RAM for the VM in MB. This is the maximum available memory when"
+           ." you use the balloon device.",
        minimum => 16,
        default => 512,
     },
     balloon => {
-        optional => 1,
-        type => 'integer',
-        description => "Amount of target RAM for the VM in MB. Using zero disables the ballon driver.",
+       optional => 1,
+       type => 'integer',
+       description => "Amount of target RAM for the VM in MB. Using zero disables the ballon driver.",
        minimum => 0,
     },
     shares => {
-        optional => 1,
-        type => 'integer',
-        description => "Amount of memory shares for auto-ballooning. The larger the number is, the more memory this VM gets. Number is relative to weights of all other running VMs. Using zero disables auto-ballooning. Auto-ballooning is done by pvestatd.",
+       optional => 1,
+       type => 'integer',
+       description => "Amount of memory shares for auto-ballooning. The larger the number is, the"
+           ." more memory this VM gets. Number is relative to weights of all other running VMs."
+           ." Using zero disables auto-ballooning. Auto-ballooning is done by pvestatd.",
        minimum => 0,
        maximum => 50000,
        default => 1000,
@@ -359,8 +367,8 @@ my $confdesc = {
     keyboard => {
        optional => 1,
        type => 'string',
-       description => "Keybord layout for vnc server. Default is read from the '/etc/pve/datacenter.cfg' configuration file.".
-                      "It should not be necessary to set it.",
+       description => "Keyboard layout for VNC server. The default is read from the"
+           ."'/etc/pve/datacenter.cfg' configuration file. It should not be necessary to set it.",
        enum => PVE::Tools::kvmkeymaplist(),
        default => undef,
     },
@@ -386,7 +394,7 @@ my $confdesc = {
     ostype => {
        optional => 1,
        type => 'string',
-        enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 win8 win10 l24 l26 solaris)],
+       enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 win8 win10 win11 l24 l26 solaris)],
        description => "Specify guest operating system.",
        verbose_description => <<EODESC,
 Specify guest operating system. This is used to enable special
@@ -402,6 +410,7 @@ wvista;; Microsoft Windows Vista
 win7;; Microsoft Windows 7
 win8;; Microsoft Windows 8/2012/2012r2
 win10;; Microsoft Windows 10/2016/2019
+win11;; Microsoft Windows 11/2022
 l24;; Linux 2.4 Kernel
 l26;; Linux 2.6 - 5.X Kernel
 solaris;; Solaris/OpenSolaris/OpenIndiania kernel
@@ -410,8 +419,8 @@ EODESC
     boot => {
        optional => 1,
        type => 'string', format => 'pve-qm-boot',
-       description => "Specify guest boot order. Use with 'order=', usage with"
-                    . " no key or 'legacy=' is deprecated.",
+       description => "Specify guest boot order. Use the 'order=' sub-property as usage with no"
+           ." key or 'legacy=' is deprecated.",
     },
     bootdisk => {
        optional => 1,
@@ -474,7 +483,7 @@ EODESC
     },
     agent => {
        optional => 1,
-       description => "Enable/disable Qemu GuestAgent and its properties.",
+       description => "Enable/disable communication with the Qemu Guest Agent and its properties.",
        type => 'string',
        format => $agent_fmt,
     },
@@ -493,8 +502,8 @@ EODESC
     localtime => {
        optional => 1,
        type => 'boolean',
-       description => "Set the real time clock to local time. This is enabled by default if ostype"
-           ." indicates a Microsoft OS.",
+       description => "Set the real time clock (RTC) to local time. This is enabled by default if"
+           ." the `ostype` indicates a Microsoft Windows OS.",
     },
     freeze => {
        optional => 1,
@@ -1843,7 +1852,6 @@ sub print_vga_device {
     my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
     my $vgaid = "vga" . ($id // '');
     my $pciaddr;
-
     if ($q35 && $vgaid eq 'vga') {
        # the first display uses pcie.0 bus on q35 machines
        $pciaddr = print_pcie_addr($vgaid, $bridges, $arch, $machine);
@@ -2111,9 +2119,16 @@ sub verify_usb_device {
 sub json_config_properties {
     my $prop = shift;
 
+    my $skip_json_config_opts = {
+       parent => 1,
+       snaptime => 1,
+       vmstate => 1,
+       runningmachine => 1,
+       runningcpu => 1,
+    };
+
     foreach my $opt (keys %$confdesc) {
-       next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate' ||
-           $opt eq 'runningmachine' || $opt eq 'runningcpu';
+       next if $skip_json_config_opts->{$opt};
        $prop->{$opt} = $confdesc->{$opt};
     }
 
@@ -3058,6 +3073,12 @@ sub start_swtpm {
     push @$emulator_cmd, "--tpm2" if $tpm->{version} eq 'v2.0';
     run_command($emulator_cmd, outfunc => sub { print $1; });
 
+    my $tries = 100; # swtpm may take a bit to start before daemonizing, wait up to 5s for pid
+    while (! -e $paths->{pid}) {
+       die "failed to start swtpm: pid file '$paths->{pid}' wasn't created.\n" if --$tries == 0;
+       usleep(50_000);
+    }
+
     # return untainted PID of swtpm daemon so it can be killed on error
     file_read_firstline($paths->{pid}) =~ m/(\d+)/;
     return $1;
@@ -3152,15 +3173,16 @@ sub get_vm_machine {
     return $machine;
 }
 
-sub get_ovmf_files($$) {
-    my ($arch, $efidisk) = @_;
+sub get_ovmf_files($$$) {
+    my ($arch, $efidisk, $smm) = @_;
 
     my $types = $OVMF->{$arch}
        or die "no OVMF images known for architecture '$arch'\n";
 
     my $type = 'default';
     if (defined($efidisk->{efitype}) && $efidisk->{efitype} eq '4m') {
-       $type = $efidisk->{'ms-keys'} ? "4m-ms" : "4m";
+       $type = $smm ? "4m" : "4m-no-smm";
+       $type .= '-ms' if $efidisk->{'pre-enrolled-keys'};
     }
 
     return $types->{$type}->@*;
@@ -3259,8 +3281,7 @@ sub query_supported_cpu_flags {
        };
        my $err = $@;
 
-       # force stop with 10 sec timeout and 'nocheck'
-       # always stop, even if QMP failed
+       # force stop with 10 sec timeout and 'nocheck', always stop, even if QMP failed
        vm_stop(undef, $fakevmid, 1, 1, 10, 0, 1);
 
        die $err if $err;
@@ -3307,11 +3328,8 @@ sub config_to_command {
         $pbs_backing) = @_;
 
     my $cmd = [];
-    my $globalFlags = [];
-    my $machineFlags = [];
-    my $rtcFlags = [];
+    my ($globalFlags, $machineFlags, $rtcFlags) = ([], [], []);
     my $devices = [];
-    my $pciaddr = '';
     my $bridges = {};
     my $ostype = $conf->{ostype};
     my $winversion = windows_version($ostype);
@@ -3427,7 +3445,7 @@ sub config_to_command {
            $d = parse_drive('efidisk0', $efidisk);
        }
 
-       my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d);
+       my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d, $q35);
        die "uefi base image '$ovmf_code' not found\n" if ! -f $ovmf_code;
 
        my ($path, $format);
@@ -3472,8 +3490,7 @@ sub config_to_command {
        push @$cmd, '-drive', "if=pflash,unit=1$cache,format=$format,id=drive-efidisk0$size_str,file=${path}${read_only_str}";
     }
 
-    # load q35 config
-    if ($q35) {
+    if ($q35) { # tell QEMU to load q35 config early
        # we use different pcie-port hardware for qemu >= 4.0 for passthrough
        if (min_version($machine_version, 4, 0)) {
            push @$devices, '-readconfig', '/usr/share/qemu-server/pve-q35-4.0.cfg';
@@ -3506,10 +3523,8 @@ sub config_to_command {
     }
 
     # enable absolute mouse coordinates (needed by vnc)
-    my $tablet;
-    if (defined($conf->{tablet})) {
-       $tablet = $conf->{tablet};
-    } else {
+    my $tablet = $conf->{tablet};
+    if (!defined($tablet)) {
        $tablet = $defaults->{tablet};
        $tablet = 0 if $qxlnum; # disable for spice because it is not needed
        $tablet = 0 if $vga->{type} =~ m/^serial\d+$/; # disable if we use serial terminal (no vga card)
@@ -3537,23 +3552,22 @@ sub config_to_command {
 
     # serial devices
     for (my $i = 0; $i < $MAX_SERIAL_PORTS; $i++)  {
-       if (my $path = $conf->{"serial$i"}) {
-           if ($path eq 'socket') {
-               my $socket = "/var/run/qemu-server/${vmid}.serial$i";
-               push @$devices, '-chardev', "socket,id=serial$i,path=$socket,server=on,wait=off";
-               # On aarch64, serial0 is the UART device. Qemu only allows
-               # connecting UART devices via the '-serial' command line, as
-               # the device has a fixed slot on the hardware...
-               if ($arch eq 'aarch64' && $i == 0) {
-                   push @$devices, '-serial', "chardev:serial$i";
-               } else {
-                   push @$devices, '-device', "isa-serial,chardev=serial$i";
-               }
+       my $path = $conf->{"serial$i"} or next;
+       if ($path eq 'socket') {
+           my $socket = "/var/run/qemu-server/${vmid}.serial$i";
+           push @$devices, '-chardev', "socket,id=serial$i,path=$socket,server=on,wait=off";
+           # On aarch64, serial0 is the UART device. Qemu only allows
+           # connecting UART devices via the '-serial' command line, as
+           # the device has a fixed slot on the hardware...
+           if ($arch eq 'aarch64' && $i == 0) {
+               push @$devices, '-serial', "chardev:serial$i";
            } else {
-               die "no such serial device\n" if ! -c $path;
-               push @$devices, '-chardev', "tty,id=serial$i,path=$path";
                push @$devices, '-device', "isa-serial,chardev=serial$i";
            }
+       } else {
+           die "no such serial device\n" if ! -c $path;
+           push @$devices, '-chardev', "tty,id=serial$i,path=$path";
+           push @$devices, '-device', "isa-serial,chardev=serial$i";
        }
     }
 
@@ -3587,11 +3601,9 @@ sub config_to_command {
 
     my $allowed_vcpus = $cpuinfo->{cpus};
 
-    die "MAX $allowed_vcpus vcpus allowed per VM on this node\n"
-       if ($allowed_vcpus < $maxcpus);
-
-    if($hotplug_features->{cpu} && min_version($machine_version, 2, 7)) {
+    die "MAX $allowed_vcpus vcpus allowed per VM on this node\n" if ($allowed_vcpus < $maxcpus);
 
+    if ($hotplug_features->{cpu} && min_version($machine_version, 2, 7)) {
        push @$cmd, '-smp', "1,sockets=$sockets,cores=$cores,maxcpus=$maxcpus";
         for (my $i = 2; $i <= $vcpus; $i++)  {
            my $cpustr = print_cpu_device($conf,$i);
@@ -3739,13 +3751,13 @@ sub config_to_command {
 
     # enable balloon by default, unless explicitly disabled
     if (!defined($conf->{balloon}) || $conf->{balloon}) {
-       $pciaddr = print_pci_addr("balloon0", $bridges, $arch, $machine_type);
+       my $pciaddr = print_pci_addr("balloon0", $bridges, $arch, $machine_type);
        push @$devices, '-device', "virtio-balloon-pci,id=balloon0$pciaddr";
     }
 
     if ($conf->{watchdog}) {
        my $wdopts = parse_watchdog($conf->{watchdog});
-       $pciaddr = print_pci_addr("watchdog", $bridges, $arch, $machine_type);
+       my $pciaddr = print_pci_addr("watchdog", $bridges, $arch, $machine_type);
        my $watchdog = $wdopts->{model} || 'i6300esb';
        push @$devices, '-device', "$watchdog$pciaddr";
        push @$devices, '-watchdog-action', $wdopts->{action} if $wdopts->{action};
@@ -3789,7 +3801,7 @@ sub config_to_command {
            die "scsi$drive->{index}: machine version 4.1~pve2 or higher is required to use more than 14 SCSI disks\n"
                if $drive->{index} > 13 && !&$version_guard(4, 1, 2);
 
-           $pciaddr = print_pci_addr("$controller_prefix$controller", $bridges, $arch, $machine_type);
+           my $pciaddr = print_pci_addr("$controller_prefix$controller", $bridges, $arch, $machine_type);
            my $scsihw_type = $scsihw =~ m/^virtio-scsi-single/ ? "virtio-scsi-pci" : $scsihw;
 
            my $iothread = '';
@@ -3812,7 +3824,7 @@ sub config_to_command {
 
         if ($drive->{interface} eq 'sata') {
            my $controller = int($drive->{index} / $PVE::QemuServer::Drive::MAX_SATA_DISKS);
-           $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type);
+           my $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type);
            push @$devices, '-device', "ahci,id=ahci$controller,multifunction=on$pciaddr"
                if !$ahcicontroller->{$controller};
            $ahcicontroller->{$controller}=1;
@@ -3877,15 +3889,12 @@ sub config_to_command {
     # pci.4 is nested in pci.1
     $bridges->{1} = 1 if $bridges->{4};
 
-    if (!$q35) {
-       # add pci bridges
-        if (min_version($machine_version, 2, 3)) {
+    if (!$q35) { # add pci bridges
+       if (min_version($machine_version, 2, 3)) {
           $bridges->{1} = 1;
           $bridges->{2} = 1;
        }
-
        $bridges->{3} = 1 if $scsihw =~ m/^virtio-scsi-single/;
-
     }
 
     for my $k (sort {$b cmp $a} keys %$bridges) {
@@ -3895,11 +3904,10 @@ sub config_to_command {
        if ($k == 2 && $legacy_igd) {
            $k_name = "$k-igd";
        }
-       $pciaddr = print_pci_addr("pci.$k_name", undef, $arch, $machine_type);
-
+       my $pciaddr = print_pci_addr("pci.$k_name", undef, $arch, $machine_type);
        my $devstr = "pci-bridge,id=pci.$k,chassis_nr=$k$pciaddr";
-       if ($q35) {
-           # add after -readconfig pve-q35.cfg
+
+       if ($q35) { # add after -readconfig pve-q35.cfg
            splice @$devices, 2, 0, '-device', $devstr;
        } else {
            unshift @$devices, '-device', $devstr if $k > 0;
@@ -4030,43 +4038,33 @@ sub vm_deviceplug {
     qemu_add_pci_bridge($storecfg, $conf, $vmid, $deviceid, $arch, $machine_type);
 
     if ($deviceid eq 'tablet') {
-
        qemu_deviceadd($vmid, print_tabletdevice_full($conf, $arch));
-
     } elsif ($deviceid eq 'keyboard') {
-
        qemu_deviceadd($vmid, print_keyboarddevice_full($conf, $arch));
-
     } elsif ($deviceid =~ m/^usb(\d+)$/) {
-
        die "usb hotplug currently not reliable\n";
        # since we can't reliably hot unplug all added usb devices and usb
        # passthrough breaks live migration we disable usb hotplugging for now
        #qemu_deviceadd($vmid, PVE::QemuServer::USB::print_usbdevice_full($conf, $deviceid, $device));
-
     } elsif ($deviceid =~ m/^(virtio)(\d+)$/) {
-
        qemu_iothread_add($vmid, $deviceid, $device);
 
-        qemu_driveadd($storecfg, $vmid, $device);
-        my $devicefull = print_drivedevice_full($storecfg, $conf, $vmid, $device, undef, $arch, $machine_type);
+       qemu_driveadd($storecfg, $vmid, $device);
+       my $devicefull = print_drivedevice_full($storecfg, $conf, $vmid, $device, undef, $arch, $machine_type);
 
-        qemu_deviceadd($vmid, $devicefull);
+       qemu_deviceadd($vmid, $devicefull);
        eval { qemu_deviceaddverify($vmid, $deviceid); };
        if (my $err = $@) {
            eval { qemu_drivedel($vmid, $deviceid); };
            warn $@ if $@;
            die $err;
         }
-
     } elsif ($deviceid =~ m/^(virtioscsi|scsihw)(\d+)$/) {
-
-
-        my $scsihw = defined($conf->{scsihw}) ? $conf->{scsihw} : "lsi";
-        my $pciaddr = print_pci_addr($deviceid, undef, $arch, $machine_type);
+       my $scsihw = defined($conf->{scsihw}) ? $conf->{scsihw} : "lsi";
+       my $pciaddr = print_pci_addr($deviceid, undef, $arch, $machine_type);
        my $scsihw_type = $scsihw eq 'virtio-scsi-single' ? "virtio-scsi-pci" : $scsihw;
 
-        my $devicefull = "$scsihw_type,id=$deviceid$pciaddr";
+       my $devicefull = "$scsihw_type,id=$deviceid$pciaddr";
 
        if($deviceid =~ m/^virtioscsi(\d+)$/ && $device->{iothread}) {
            qemu_iothread_add($vmid, $deviceid, $device);
@@ -4077,11 +4075,9 @@ sub vm_deviceplug {
            $devicefull .= ",num_queues=$device->{queues}";
        }
 
-        qemu_deviceadd($vmid, $devicefull);
-        qemu_deviceaddverify($vmid, $deviceid);
-
+       qemu_deviceadd($vmid, $devicefull);
+       qemu_deviceaddverify($vmid, $deviceid);
     } elsif ($deviceid =~ m/^(scsi)(\d+)$/) {
-
         qemu_findorcreatescsihw($storecfg,$conf, $vmid, $device, $arch, $machine_type);
         qemu_driveadd($storecfg, $vmid, $device);
 
@@ -4092,9 +4088,7 @@ sub vm_deviceplug {
            warn $@ if $@;
            die $err;
         }
-
     } elsif ($deviceid =~ m/^(net)(\d+)$/) {
-
        return if !qemu_netdevadd($vmid, $conf, $arch, $device, $deviceid);
 
        my $machine_type = PVE::QemuServer::Machine::qemu_machine_pxe($vmid, $conf);
@@ -4113,16 +4107,13 @@ sub vm_deviceplug {
            warn $@ if $@;
            die $err;
        }
-
     } elsif (!$q35 && $deviceid =~ m/^(pci\.)(\d+)$/) {
-
        my $bridgeid = $2;
        my $pciaddr = print_pci_addr($deviceid, undef, $arch, $machine_type);
        my $devicefull = "pci-bridge,id=pci.$bridgeid,chassis_nr=$bridgeid$pciaddr";
 
        qemu_deviceadd($vmid, $devicefull);
        qemu_deviceaddverify($vmid, $deviceid);
-
     } else {
        die "can't hotplug device '$deviceid'\n";
     }
@@ -4541,7 +4532,7 @@ sub qemu_volume_snapshot {
 
     my $running = check_running($vmid);
 
-    if ($running && do_snapshots_with_qemu($storecfg, $volid)){
+    if ($running && do_snapshots_with_qemu($storecfg, $volid, $deviceid)) {
        mon_cmd($vmid, 'blockdev-snapshot-internal-sync', device => $deviceid, name => $snap);
     } else {
        PVE::Storage::volume_snapshot($storecfg, $volid, $snap);
@@ -4563,7 +4554,7 @@ sub qemu_volume_snapshot_delete {
        });
     }
 
-    if ($running && do_snapshots_with_qemu($storecfg, $volid)){
+    if ($running && do_snapshots_with_qemu($storecfg, $volid, $deviceid)) {
        mon_cmd($vmid, 'blockdev-snapshot-delete-internal-sync', device => $deviceid, name => $snap);
     } else {
        PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, $running);
@@ -5381,35 +5372,36 @@ sub vm_start_nolock {
        push @$cmd, '-S';
     }
 
-    # host pci devices
+    my $start_timeout = $params->{timeout} // config_aware_timeout($conf, $resume);
+
+    my $pci_devices = {}; # host pci devices
     for (my $i = 0; $i < $PVE::QemuServer::PCI::MAX_HOSTPCI_DEVICES; $i++)  {
-      my $d = parse_hostpci($conf->{"hostpci$i"});
-      next if !$d;
-      my $pcidevices = $d->{pciid};
-      foreach my $pcidevice (@$pcidevices) {
-           my $pciid = $pcidevice->{id};
-
-           my $info = PVE::SysFSTools::pci_device_info("$pciid");
-           die "IOMMU not present\n" if !PVE::SysFSTools::check_iommu_support();
-           die "no pci device info for device '$pciid'\n" if !$info;
-
-           if ($d->{mdev}) {
-               my $uuid = PVE::SysFSTools::generate_mdev_uuid($vmid, $i);
-               PVE::SysFSTools::pci_create_mdev_device($pciid, $uuid, $d->{mdev});
-           } else {
-               die "can't unbind/bind PCI group to VFIO '$pciid'\n"
-                   if !PVE::SysFSTools::pci_dev_group_bind_to_vfio($pciid);
-               die "can't reset PCI device '$pciid'\n"
-                   if $info->{has_fl_reset} && !PVE::SysFSTools::pci_dev_reset($info);
+       my $dev = $conf->{"hostpci$i"} or next;
+       $pci_devices->{$i} = parse_hostpci($dev);
+    }
+
+    my $pci_id_list = [ map { $_->{id} } map { $_->{pciid}->@* } values $pci_devices->%* ];
+    # reserve all PCI IDs before actually doing anything with them
+    PVE::QemuServer::PCI::reserve_pci_usage($pci_id_list, $vmid, $start_timeout);
+
+    eval {
+       for my $id (sort keys %$pci_devices) {
+           my $d = $pci_devices->{$id};
+           for my $dev ($d->{pciid}->@*) {
+               PVE::QemuServer::PCI::prepare_pci_device($vmid, $dev->{id}, $id, $d->{mdev});
            }
-      }
+       }
+    };
+    if (my $err = $@) {
+       eval { PVE::QemuServer::PCI::remove_pci_reservation($pci_id_list) };
+       warn $@ if $@;
+       die $err;
     }
 
     PVE::Storage::activate_volumes($storecfg, $vollist);
 
     eval {
-       run_command(['/bin/systemctl', 'stop', "$vmid.scope"],
-           outfunc => sub {}, errfunc => sub {});
+       run_command(['/bin/systemctl', 'stop', "$vmid.scope"], outfunc => sub{}, errfunc => sub{});
     };
     # Issues with the above 'stop' not being fully completed are extremely rare, a very low
     # timeout should be more than enough here...
@@ -5417,7 +5409,6 @@ sub vm_start_nolock {
 
     my $cpuunits = get_cpuunits($conf);
 
-    my $start_timeout = $params->{timeout} // config_aware_timeout($conf, $resume);
     my %run_params = (
        timeout => $statefile ? undef : $start_timeout,
        umask => 0077,
@@ -5431,7 +5422,7 @@ sub vm_start_nolock {
        $run_params{logfunc} = sub { print "QEMU: $_[0]\n" };
     }
 
-    my %properties = (
+    my %systemd_properties = (
        Slice => 'qemu.slice',
        KillMode => 'process',
        SendSIGKILL => 0,
@@ -5440,19 +5431,19 @@ sub vm_start_nolock {
 
     if (PVE::CGroup::cgroup_mode() == 2) {
        $cpuunits = 10000 if $cpuunits >= 10000; # else we get an error
-       $properties{CPUWeight} = $cpuunits;
+       $systemd_properties{CPUWeight} = $cpuunits;
     } else {
-       $properties{CPUShares} = $cpuunits;
+       $systemd_properties{CPUShares} = $cpuunits;
     }
 
     if (my $cpulimit = $conf->{cpulimit}) {
-       $properties{CPUQuota} = int($cpulimit * 100);
+       $systemd_properties{CPUQuota} = int($cpulimit * 100);
     }
-    $properties{timeout} = 10 if $statefile; # setting up the scope shoul be quick
+    $systemd_properties{timeout} = 10 if $statefile; # setting up the scope shoul be quick
 
     my $run_qemu = sub {
        PVE::Tools::run_fork sub {
-           PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
+           PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %systemd_properties);
 
            my $tpmpid;
            if (my $tpm = $conf->{tpmstate0}) {
@@ -5497,9 +5488,16 @@ sub vm_start_nolock {
     if (my $err = $@) {
        # deactivate volumes if start fails
        eval { PVE::Storage::deactivate_volumes($storecfg, $vollist); };
+       eval { PVE::QemuServer::PCI::remove_pci_reservation($pci_id_list) };
+
        die "start failed: $err";
     }
 
+    # re-reserve all PCI IDs now that we can know the actual VM PID
+    my $pid = PVE::QemuServer::Helpers::vm_running_locally($vmid);
+    eval { PVE::QemuServer::PCI::reserve_pci_usage($pci_id_list, $vmid, undef, $pid) };
+    warn $@ if $@;
+
     print "migration listens on $migrate_uri\n" if $migrate_uri;
     $res->{migrate_uri} = $migrate_uri;
 
@@ -5688,6 +5686,7 @@ sub vm_stop_cleanup {
            unlink '/dev/shm/pve-shm-' . ($ivshmem->{name} // $vmid);
        }
 
+       my $ids = [];
        foreach my $key (keys %$conf) {
            next if $key !~ m/^hostpci(\d+)$/;
            my $hostpciindex = $1;
@@ -5696,9 +5695,11 @@ sub vm_stop_cleanup {
 
            foreach my $pci (@{$d->{pciid}}) {
                my $pciid = $pci->{id};
+               push @$ids, $pci->{id};
                PVE::SysFSTools::pci_cleanup_mdev_device($pciid, $uuid);
            }
        }
+       PVE::QemuServer::PCI::remove_pci_reservation($ids);
 
        vmconfig_apply_pending($vmid, $conf, $storecfg) if $apply_pending_changes;
     };
@@ -7031,7 +7032,9 @@ my $qemu_snap_storage = {
     rbd => 1,
 };
 sub do_snapshots_with_qemu {
-    my ($storecfg, $volid) = @_;
+    my ($storecfg, $volid, $deviceid) = @_;
+
+    return if $deviceid =~ m/tpmstate0/;
 
     my $storage_name = PVE::Storage::parse_volume_id($volid);
     my $scfg = $storecfg->{ids}->{$storage_name};
@@ -7538,7 +7541,8 @@ sub get_efivars_size {
     my ($conf) = @_;
     my $arch = get_vm_arch($conf);
     my $efidisk = $conf->{efidisk0} ? parse_drive('efidisk0', $conf->{efidisk0}) : undef;
-    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk);
+    my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
+    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk, $smm);
     die "uefi vars image '$ovmf_vars' not found\n" if ! -f $ovmf_vars;
     return -s $ovmf_vars;
 }
@@ -7563,10 +7567,10 @@ sub update_tpmstate_size {
     $conf->{tpmstate0} = print_drive($disk);
 }
 
-sub create_efidisk($$$$$$) {
-    my ($storecfg, $storeid, $vmid, $fmt, $arch, $efidisk) = @_;
+sub create_efidisk($$$$$$$) {
+    my ($storecfg, $storeid, $vmid, $fmt, $arch, $efidisk, $smm) = @_;
 
-    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk);
+    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk, $smm);
     die "EFI vars default image not found\n" if ! -f $ovmf_vars;
 
     my $vars_size_b = -s $ovmf_vars;