From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 323A21FF0B0 for ; Tue, 27 Oct 2026 11:13:58 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id E7D42216C2; Tue, 27 Oct 2026 11:13:31 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=genua.de; s=202307; t=1793095944; bh=UlqhfscmojYVzAQ9jciQtgqRdqQSVAs2ayi7TECVCYY=; h=Date:From:To:Subject:References:In-Reply-To:From; b=GVjmN3UcZWOULui4LjcAiFH/rgD4Rvc5pTVnYDY1bp0pZ5eJofJUdQ14FzKBq0f7N zkCVeqUuwiDqxD6F45vsFRkpYTRkCbUsT37D/doL/g5ZL0uhiF6SLkI8yiGkIAT+BZ SA1iYfx3i02BsLtLi6omno8QZioL70kY2nZPRv/471jdSiGhoTNwj4OsLD9IK4xrKw XbX6acDB8LU/fc7hSKF116YtdYZlXchLxDwJbOvVn3SiuybCV7bk/4x7Q3beQXc9ve ThZgQ0doY7ljz+nTfePqLdCWRm79zr8ee2wyAYPJValBF4ROjQxRT7xsFiJTKrLHXC VWOkun0qiKyFw== Date: Tue, 27 Oct 2026 11:12:23 +0100 From: Christian Ludwig To: Subject: [PATCH qemu-server v3 10/19] bios: add a custom firmware sub-property Message-ID: References: MIME-Version: 1.0 In-Reply-To: X-Originating-IP: [192.168.217.185] X-ClientProxiedBy: kch1-mta09.win.genua.de (10.208.16.109) To kch1-mta07.win.genua.de (10.208.16.107) Content-Type: text/plain; charset="us-ascii" Content-Disposition: inline X-SPAM-LEVEL: Spam detection results: 0 AWL 0.066 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's domain DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from domain DMARC_PASS -0.1 DMARC pass policy SPF_HELO_PASS -0.001 SPF: HELO matches SPF record SPF_PASS -0.001 SPF: sender matches SPF record UNPARSEABLE_RELAY 0.001 Informational: message has unparseable relay lines Message-ID-Hash: MNBVDIA7JENYK3TNHEW2BRZQGRZNHDX5 X-Message-ID-Hash: MNBVDIA7JENYK3TNHEW2BRZQGRZNHDX5 X-MailFrom: christian_ludwig@genua.de X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add the 'firmware' sub-property to the bios option, pointing to a volume of the efi-firmware content type: bios: ovmf,firmware=local:efi-firmware/custom.fd Custom firmware images are only supported with OVMF for now, so require the 'ovmf' BIOS type when generating the command line. The image replaces the system OVMF code image for the pflash, blockdev and '-bios' variants. Include the volume when activating a VM's volumes. Check the volume's storage and content type before that, so that errors are reported in the context of the custom firmware, e.g. when a template or a restored VM references an image that is not available on the node. Firmware images are content and not owned by the VM, like ISO images, so refuse migration unless they are on a shared storage. They are never copied, so refuse remote migration too. Signed-off-by: Christian Ludwig --- src/PVE/QemuConfig.pm | 17 ++++++++++ src/PVE/QemuMigrate.pm | 6 ++++ src/PVE/QemuServer.pm | 20 ++++++++--- src/PVE/QemuServer/BIOS.pm | 28 ++++++++++++++++ src/PVE/QemuServer/OVMF.pm | 68 ++++++++++++++++++++++++++++++++------ 5 files changed, 125 insertions(+), 14 deletions(-) diff --git a/src/PVE/QemuConfig.pm b/src/PVE/QemuConfig.pm index d83a474c..c2b2cab7 100644 --- a/src/PVE/QemuConfig.pm +++ b/src/PVE/QemuConfig.pm @@ -113,6 +113,17 @@ sub parse_volume { die $err; } $volume = { 'file' => $volume_string }; + } elsif ($key eq 'bios') { + my $bios = eval { PVE::QemuServer::BIOS::parse_bios($volume_string) }; + if (my $err = $@) { + return if $noerr; + die $err; + } + # only a custom firmware image is a volume, plain BIOS settings are not + return if !defined($bios->{firmware}); + + $bios->{file} = delete($bios->{firmware}); + $volume = $bios; } else { $volume = PVE::QemuServer::Drive::parse_drive($key, $volume_string); } @@ -125,6 +136,12 @@ sub parse_volume { sub print_volume { my ($class, $key, $volume) = @_; + if ($key eq 'bios') { + my $bios = { $volume->%* }; + $bios->{firmware} = delete($bios->{file}); + return PVE::QemuServer::BIOS::print_bios($bios); + } + return PVE::QemuServer::Drive::print_drive($volume); } diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm index 8da6f15d..78416896 100644 --- a/src/PVE/QemuMigrate.pm +++ b/src/PVE/QemuMigrate.pm @@ -431,6 +431,10 @@ sub scan_local_volumes { # check if storage is available on both nodes my $scfg = PVE::Storage::storage_check_enabled($storecfg, $sid); + # the custom firmware image is not owned by the VM, so it is never copied over + die "custom firmware image is not available on the remote cluster\n" + if $attr->{is_firmware} && $self->{opts}->{remote}; + my $targetsid = $sid; # NOTE: local ignores shared mappings, remote maps them if (!$scfg->{shared} || $self->{opts}->{remote}) { @@ -440,6 +444,8 @@ sub scan_local_volumes { $self->target_storage_check_available($storecfg, $targetsid, $volid); return if $scfg->{shared} && !$self->{opts}->{remote}; + die "local custom firmware image - use a shared storage\n" if $attr->{is_firmware}; + $local_volumes->{$volid}->{ref} = 'pending' if $attr->{referenced_in_pending}; $local_volumes->{$volid}->{ref} = 'snapshot' if $attr->{referenced_in_snapshot}; $local_volumes->{$volid}->{ref} = 'unused' if $attr->{is_unused}; diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm index c288ba22..87baa1f0 100644 --- a/src/PVE/QemuServer.pm +++ b/src/PVE/QemuServer.pm @@ -3259,6 +3259,9 @@ sub config_to_command { push @$cmd, '-smbios', "type=1" . $smbios_string; } + PVE::QemuServer::BIOS::check_bios(PVE::QemuServer::BIOS::parse_bios($conf->{bios})); + PVE::QemuServer::OVMF::check_custom_firmware($storecfg, $conf); + if (get_bios_type($conf) eq 'ovmf') { die "OVMF (UEFI) BIOS is not supported on 32-bit CPU types\n" if !$forcecpu && get_cpu_bitness($conf->{cpu}, $arch) == 32; @@ -4519,11 +4522,14 @@ sub foreach_volid { $volhash->{$volid}->{is_tpmstate} //= 0; $volhash->{$volid}->{is_tpmstate} = 1 if $key eq 'tpmstate0'; + $volhash->{$volid}->{is_firmware} //= 0; + $volhash->{$volid}->{is_firmware} = 1 if $key eq 'bios'; + $volhash->{$volid}->{drivename} = $key if is_valid_drivename($key); }; my $include_opts = { - extra_keys => ['vmstate'], + extra_keys => ['vmstate', 'bios'], include_unused => 1, }; @@ -6074,7 +6080,8 @@ sub get_vm_volumes { return $vollist; } -# Get volumes defined in the current VM configuration, including the VM state file. +# Get volumes defined in the current VM configuration, including the VM state file and the custom +# firmware image. sub get_current_vm_volumes { my ($storecfg, $conf) = @_; @@ -6082,12 +6089,17 @@ sub get_current_vm_volumes { PVE::QemuConfig->foreach_volume_full( $conf, - { extra_keys => ['vmstate'] }, + { extra_keys => ['vmstate', 'bios'] }, sub { my ($ds, $drive) = @_; if (PVE::Storage::parse_volume_id($drive->{file}, 1)) { - check_volume_storage_type($storecfg, $drive->{file}); + if ($ds eq 'bios') { + # report errors in the context of the custom firmware + PVE::QemuServer::OVMF::check_custom_firmware($storecfg, $conf); + } else { + check_volume_storage_type($storecfg, $drive->{file}); + } push $volumes->@*, $drive->{file}; } }, diff --git a/src/PVE/QemuServer/BIOS.pm b/src/PVE/QemuServer/BIOS.pm index 4e1a3936..eecf877f 100644 --- a/src/PVE/QemuServer/BIOS.pm +++ b/src/PVE/QemuServer/BIOS.pm @@ -11,6 +11,8 @@ our @EXPORT_OK = qw( parse_bios print_bios get_bios_type + get_bios_firmware + check_bios ); my $bios_fmt = { @@ -20,6 +22,15 @@ my $bios_fmt = { default_key => 1, description => "Select BIOS implementation.", }, + firmware => { + type => 'string', + format => 'pve-volume-id', + format_description => 'volume', + description => "Custom firmware image to use instead of the one provided by the system." + . " Must reference a volume with content type 'efi-firmware', for example" + . " 'local:efi-firmware/custom.fd'. Only supported with the 'ovmf' BIOS type.", + optional => 1, + }, }; PVE::JSONSchema::register_format('pve-qm-bios', $bios_fmt); @@ -45,4 +56,21 @@ sub get_bios_type { return parse_bios($conf->{bios})->{type} // 'seabios'; } +# Returns the volume ID of the custom firmware image, if one is configured. +sub get_bios_firmware { + my ($conf) = @_; + + return parse_bios($conf->{bios})->{firmware}; +} + +# Dies if the combination of settings in the parsed bios property-string is not supported. +sub check_bios { + my ($bios) = @_; + + die "a custom firmware image requires the 'ovmf' BIOS type\n" + if defined($bios->{firmware}) && $bios->{type} ne 'ovmf'; + + return; +} + 1; diff --git a/src/PVE/QemuServer/OVMF.pm b/src/PVE/QemuServer/OVMF.pm index 67665c7c..f387d894 100644 --- a/src/PVE/QemuServer/OVMF.pm +++ b/src/PVE/QemuServer/OVMF.pm @@ -11,6 +11,7 @@ use PVE::RESTEnvironment qw(log_warn); use PVE::Storage; use PVE::Tools; +use PVE::QemuServer::BIOS; use PVE::QemuServer::Blockdev; use PVE::QemuServer::Drive qw(checked_volume_format parse_drive print_drive); use PVE::QemuServer::Helpers; @@ -60,26 +61,69 @@ my $OVMF = { my $temporary_efidisk_dir = '/run/qemu-server/efidisk'; -my sub get_ovmf_files($$$$) { - my ($arch, $efidisk, $smm, $cvm_type) = @_; +# Check that the custom firmware image configured for the VM, if any, can be used on this node. +# The image itself is not checked, because the storage might not be activated yet. +sub check_custom_firmware { + my ($storecfg, $conf) = @_; + + my $volid = PVE::QemuServer::BIOS::get_bios_firmware($conf) // return; + + eval { + my ($storeid) = PVE::Storage::parse_volume_id($volid); + my $scfg = PVE::Storage::storage_check_enabled($storecfg, $storeid); + + my ($vtype) = PVE::Storage::parse_volname($storecfg, $volid); + die "volume has content type '$vtype', expected 'efi-firmware'\n" + if $vtype ne 'efi-firmware'; + + die "storage '$storeid' does not support content type 'efi-firmware'\n" + if !$scfg->{content}->{'efi-firmware'}; + }; + die "bios: custom firmware '$volid' - $@" if $@; + + return; +} + +my sub get_custom_firmware_path { + my ($storecfg, $conf) = @_; + + my $volid = PVE::QemuServer::BIOS::get_bios_firmware($conf) // return; + + return PVE::Storage::path($storecfg, $volid); +} + +# If $custom_code is set, it is used instead of the system-provided code image. +my sub get_ovmf_files($$$$;$) { + my ($arch, $efidisk, $smm, $cvm_type, $custom_code) = @_; my $types = $OVMF->{$arch} or die "no OVMF images known for architecture '$arch'\n"; + my $check_code_image = sub { + my ($ovmf_code) = @_; + + if (defined($custom_code)) { + die "custom firmware image '$custom_code' not found\n" + if !file_exists($custom_code); + return $custom_code; + } + + die "EFI base image '$ovmf_code' not found\n" if !file_exists($ovmf_code); + return $ovmf_code; + }; + my $type = 'default'; if ($arch eq 'x86_64') { if ($cvm_type && $cvm_type eq 'snp') { $type = "4m-snp"; my ($ovmf) = $types->{$type}->@*; - die "EFI base image '$ovmf' not found\n" if !file_exists($ovmf); - return ($ovmf); + return ($check_code_image->($ovmf)); } elsif ($cvm_type && ($cvm_type eq 'std' || $cvm_type eq 'es')) { $type = "4m-sev"; } elsif ($cvm_type && $cvm_type eq 'tdx') { $type = "4m-tdx"; my ($ovmf) = $types->{$type}->@*; - die "EFI base image '$ovmf' not found\n" if !file_exists($ovmf); - return ($ovmf); + return ($check_code_image->($ovmf)); } elsif (defined($efidisk->{efitype}) && $efidisk->{efitype} eq '4m') { $type = $smm ? "4m" : "4m-no-smm"; $type .= '-ms' if $efidisk->{'pre-enrolled-keys'}; @@ -103,7 +147,7 @@ my sub get_ovmf_files($$$$) { } my ($ovmf_code, $ovmf_vars) = $types->{$type}->@*; - die "EFI base image '$ovmf_code' not found\n" if !file_exists($ovmf_code); + $ovmf_code = $check_code_image->($ovmf_code); die "EFI vars image '$ovmf_vars' not found\n" if !file_exists($ovmf_vars); return ($ovmf_code, $ovmf_vars); @@ -122,7 +166,8 @@ my sub print_ovmf_drive_commandlines { die "Attempting to configure TDX with pflash devices instead of using `-bios`\n" if $cvm_type && $cvm_type eq 'tdx'; - my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d, $q35, $cvm_type); + my $custom_code = get_custom_firmware_path($storecfg, $conf); + my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d, $q35, $cvm_type, $custom_code); my $ovmf_vars_size = file_get_size($ovmf_vars); my $var_drive_str = "if=pflash,unit=1,id=drive-efidisk0"; @@ -218,7 +263,8 @@ my sub generate_ovmf_blockdev { die "Attempting to configure SEV-SNP with pflash devices instead of using `-bios`\n" if $cvm_type && $cvm_type eq 'snp'; - my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $drive, $q35, $cvm_type); + my $custom_code = get_custom_firmware_path($storecfg, $conf); + my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $drive, $q35, $cvm_type, $custom_code); my $ovmf_code_blockdev = { driver => 'raw', @@ -277,7 +323,9 @@ sub print_ovmf_commandline { "EFI disks are not supported with Confidential Virtual Machines and will be ignored" ); } - push $cmd->@*, '-bios', get_ovmf_files($hw_info->{arch}, undef, undef, $cvm_type); + my $custom_code = get_custom_firmware_path($storecfg, $conf); + push $cmd->@*, '-bios', + get_ovmf_files($hw_info->{arch}, undef, undef, $cvm_type, $custom_code); } else { if ($version_guard->(10, 0, 0)) { # for the switch to -blockdev my ($code_blockdev, $vars_blockdev, $throttle_group) = -- 2.34.1