From: Christian Ludwig <christian_ludwig@genua.de>
To: <pve-devel@lists.proxmox.com>
Subject: [PATCH qemu-server v3 10/19] bios: add a custom firmware sub-property
Date: Tue, 27 Oct 2026 11:12:23 +0100 [thread overview]
Message-ID: <fd50e4438008341d57d9d369e88c24954474d196.1793029144.git@genua.de> (raw)
In-Reply-To: <cover.1793029144.git@genua.de>
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 <christian_ludwig@genua.de>
---
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
next prev parent reply other threads:[~2026-10-27 10:13 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 01/19] plugin: add efi-firmware content type Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 02/19] test: get_subdir: cover the " Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 03/19] plugins: allow the efi-firmware content type on file based storages Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 04/19] plugin: support efi-firmware volumes in volume export and import Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 05/19] api: status: support efi-firmware in upload and download-url Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 06/19] test: volume access: cover efi-firmware volumes Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 07/19] test: list volumes: " Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 08/19] test: cover efi-firmware volumes in parse_volname and path_to_volume_id Christian Ludwig
2026-10-27 10:12 ` [PATCH qemu-server v3 09/19] config: define bios schema as property-string Christian Ludwig
2026-10-27 10:12 ` Christian Ludwig [this message]
2026-10-27 10:12 ` [PATCH qemu-server v3 11/19] api: check the custom firmware of the bios option Christian Ludwig
2026-10-27 10:12 ` [PATCH qemu-server v3 12/19] test: custom firmware in " Christian Ludwig
2026-10-27 10:12 ` [PATCH qemu-server v3 13/19] test: custom firmware volumes in replication and migration Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-manager v3 14/19] ui: storage: add efi-firmware content type support Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-manager v3 15/19] ui: form: support other content types in the ISO selector Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-manager v3 16/19] ui: qemu: allow selecting a custom EFI firmware image Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-manager v3 17/19] ui: migrate: refuse migration with a local custom " Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-docs v3 18/19] pvesm: document the efi-firmware content type Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-docs v3 19/19] qm: document the custom firmware of the bios option Christian Ludwig
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=fd50e4438008341d57d9d369e88c24954474d196.1793029144.git@genua.de \
--to=christian_ludwig@genua.de \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox