* [PATCH v3 00/19] Support for custom EFI firmware
@ 2026-10-27 10:12 Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 01/19] plugin: add efi-firmware content type Christian Ludwig
` (18 more replies)
0 siblings, 19 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Hi,
here is an updated patch series that brings support for custom UEFI
firmware images. You can find the v2 series at [1] and v1 series at [2]
for reference.
In Confidential Computing the aim is to not trust the hypervisor, yet it
runs its bundled firmware in each VM. Some VM appliances also ship their
own firmware images. This series allows to bring your own firmware for
OVMF based VMs.
Today the Proxmox-VE API allows to import a custom efidisk0 (EFIVAR)
already. EFI firmware and EFIVAR data have to match. Otherwise, the VM
might not boot anymore. Therefore, with this series you can set a custom
EFI firmware via the API, along with a custom efidisk. But these are two
steps. And there is no safety net. The GUI is missing a way to set a
custom EFIVAR. So this series only allows to set a custom firmware image
for confidential computing VMs that do not need EFIVAR storage. The
defaults do not change.
The changes in detail:
pve-storage:
- declares the new 'efi-firmware' content type for file based storage
- provides upload/download API. File names are restricted to the safe
character set, because they are used as sub-properties in the GUI.
File extensions are restricted to '.fd'. So the efi-firmware storage
can host other types as well in the future.
qemu-server:
- extends the 'bios' property with an optional 'firmware' sub-property
with an efi-firmware storage location.
- the 'firmware' sub-property is only allowed to be set for OVMF based
VMs.
- adds API plumbing
pve-manager:
- adds UI plumbing for the 'efi-firmware' storage content type
- extends the BIOS chooser dialog with an 'OVMF (custom)' option to
select the firmware sub-property.
- restricts custom firmware selection to confidential computing VMs.
pve-docs:
- adds a custom firmware subsection in the BIOS section
Changes in v3:
- rebased on current master for all repositories
- turn 'bios' into a property-string and make the custom firmware its
'firmware' sub-property (bios: ovmf,firmware=<volid>), instead of a
separate 'efi-firmware' VM config key (suggested by Fiona)
- restrict firmware file names to the '.fd' extension (suggested by
Fiona)
- validate uploaded and downloaded images as raw (suggested by Fiona)
- support efi-firmware volumes in volume export/import (suggested by
Fiona)
- clearer errors when a configured firmware image is not usable on the
node, e.g. for templates or restored VMs
- UI: refuse migration with a local custom firmware image
- docs: describe migration, replication, templates and backups
Tests and feedback welcome.
- Christian
[1] https://lore.proxmox.com/pve-devel/cover.1790337423.git@genua.de/
[2] https://lore.proxmox.com/pve-devel/20260817115919.abQyMsVBJeHupthBjG6HbiJWe2o8RfIy9CmAttembto@z/
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 01/19] plugin: add efi-firmware content type
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
@ 2026-10-27 10:12 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 02/19] test: get_subdir: cover the " Christian Ludwig
` (17 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Holds custom OVMF code images in the efi-firmware/ subdirectory of a
storage. File names are restricted to the safe character class and must
end with '.fd'. Requiring the extension avoids confusion with other
image formats and leaves room for supporting other firmware formats
later on. Access is granted like for ISO images.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/PVE/Storage.pm | 23 +++++++++++++++++++----
src/PVE/Storage/Plugin.pm | 15 ++++++++++++++-
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index cfbbabd..03ec6a1 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -123,6 +123,8 @@ our $IMPORT_EXT_RE_1 = qr/\.(ova|ovf|qcow2|raw|vmdk)/;
our $UPLOAD_IMPORT_EXT_RE_1 = qr/\.(ova|qcow2|raw|vmdk)/;
+our $EFI_FIRMWARE_EXT_RE_1 = qr/\.(fd)/;
+
our $SAFE_CHAR_CLASS_RE = qr/[a-zA-Z0-9\-\.\+\=\_]/;
our $SAFE_CHAR_WITH_WHITESPACE_CLASS_RE = qr/[ a-zA-Z0-9\-\.\+\=\_]/;
@@ -629,7 +631,12 @@ sub check_volume_access {
return if $rpcenv->check($user, "/storage/$sid", ['Datastore.Allocate'], 1);
- if ($vtype eq 'iso' || $vtype eq 'vztmpl' || $vtype eq 'import') {
+ if (
+ $vtype eq 'iso'
+ || $vtype eq 'vztmpl'
+ || $vtype eq 'import'
+ || $vtype eq 'efi-firmware'
+ ) {
# require at least read access to storage, (custom) templates/ISOs could be sensitive
$rpcenv->check_any(
$user,
@@ -719,6 +726,7 @@ sub path_to_volume_id {
my $backupdir = $plugin->get_subdir($scfg, 'backup');
my $snippetsdir = $plugin->get_subdir($scfg, 'snippets');
my $importdir = $plugin->get_subdir($scfg, 'import');
+ my $firmwaredir = $plugin->get_subdir($scfg, 'efi-firmware');
if ($path =~ m!^\Q$imagedir\E/(\d+)/([^/\s]+)$!) {
my $vmid = $1;
@@ -747,6 +755,9 @@ sub path_to_volume_id {
} elsif ($path =~ m!^\Q$importdir\E/(${SAFE_CHAR_CLASS_RE}+${IMPORT_EXT_RE_1})$!) {
my $name = $1;
return ('import', "$sid:import/$name");
+ } elsif ($path =~ m!^\Q$firmwaredir\E/(${SAFE_CHAR_CLASS_RE}+${EFI_FIRMWARE_EXT_RE_1})$!) {
+ my $name = $1;
+ return ('efi-firmware', "$sid:efi-firmware/$name");
}
}
@@ -1269,7 +1280,11 @@ sub template_list {
my ($cfg, $storeid, $tt) = @_;
die "unknown template type '$tt'\n"
- if !($tt eq 'iso' || $tt eq 'vztmpl' || $tt eq 'backup' || $tt eq 'snippets');
+ if !($tt eq 'iso'
+ || $tt eq 'vztmpl'
+ || $tt eq 'backup'
+ || $tt eq 'snippets'
+ || $tt eq 'efi-firmware');
my $ids = $cfg->{ids};
@@ -1297,7 +1312,7 @@ sub template_list {
sub volume_list {
my ($cfg, $storeid, $vmid, $content) = @_;
- my @ctypes = qw(rootdir images vztmpl iso backup snippets import);
+ my @ctypes = qw(rootdir images vztmpl iso backup snippets import efi-firmware);
my $cts = $content ? [$content] : [@ctypes];
@@ -2264,7 +2279,7 @@ sub complete_storage_enabled {
sub complete_content_type {
my ($cmdname, $pname, $cvalue) = @_;
- return [qw(rootdir images vztmpl iso backup snippets import)];
+ return [qw(rootdir images vztmpl iso backup snippets import efi-firmware)];
}
sub complete_volume {
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 8318a68..43af559 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -384,7 +384,7 @@ PVE::JSONSchema::register_format('pve-storage-content', \&verify_content);
sub verify_content {
my ($ct, $noerr) = @_;
- return $ct if $ct eq 'import';
+ return $ct if $ct eq 'import' || $ct eq 'efi-firmware';
my $valid_content = valid_content_types('dir'); # dir includes all other types
@@ -843,6 +843,10 @@ sub parse_volname {
m!^import/(${PVE::Storage::SAFE_CHAR_WITH_WHITESPACE_CLASS_RE}+$PVE::Storage::IMPORT_EXT_RE_1)$!
) {
return ('import', $1, undef, undef, undef, undef, $2);
+ } elsif ($volname =~
+ m!^efi-firmware/(${PVE::Storage::SAFE_CHAR_CLASS_RE}+$PVE::Storage::EFI_FIRMWARE_EXT_RE_1)$!
+ ) {
+ return ('efi-firmware', $1, undef, undef, undef, undef, 'raw');
}
die "unable to parse directory volume name '$volname'\n";
@@ -948,6 +952,7 @@ my $vtype_subdirs = {
backup => 'dump',
snippets => 'snippets',
import => 'import',
+ 'efi-firmware' => 'efi-firmware',
};
sub get_vtype_subdirs {
@@ -1855,6 +1860,12 @@ my $get_subdir_files = sub {
m!/(${PVE::Storage::SAFE_CHAR_CLASS_RE}+$PVE::Storage::IMPORT_EXT_RE_1)$!i;
$info = { volid => "$sid:import/$1", format => "$2" };
+ } elsif ($tt eq 'efi-firmware') {
+ next
+ if $fn !~
+ m!/(${PVE::Storage::SAFE_CHAR_CLASS_RE}+$PVE::Storage::EFI_FIRMWARE_EXT_RE_1)$!;
+
+ $info = { volid => "$sid:efi-firmware/$1", format => 'raw' };
}
$info->{size} = $st->size;
@@ -1891,6 +1902,8 @@ sub list_volumes {
$data = $get_subdir_files->($storeid, $path, 'snippets');
} elsif ($type eq 'import') {
$data = $get_subdir_files->($storeid, $path, 'import');
+ } elsif ($type eq 'efi-firmware' && !defined($vmid)) {
+ $data = $get_subdir_files->($storeid, $path, 'efi-firmware');
}
}
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 02/19] test: get_subdir: cover the efi-firmware content type
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 ` 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
` (16 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Check that the vtype maps to the efi-firmware/ subdirectory.
Reviewed-by: Fiona Ebner <f.ebner@proxmox.com>
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/test/get_subdir_test.pm | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/test/get_subdir_test.pm b/src/test/get_subdir_test.pm
index 5fb5445..5be6de8 100644
--- a/src/test/get_subdir_test.pm
+++ b/src/test/get_subdir_test.pm
@@ -19,6 +19,8 @@ my $tests = [
# failed matches
[$scfg_with_path, 'none', "unknown vtype 'none'\n"],
[{}, 'iso', "storage definition has no path\n"],
+ # efi-firmware vtype returns <path>/efi-firmware
+ [$scfg_with_path, 'efi-firmware', "$scfg_with_path->{path}/efi-firmware"],
];
# creates additional positive tests
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 03/19] plugins: allow the efi-firmware content type on file based storages
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 ` 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
` (15 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Enable it for dir, NFS, CIFS, BTRFS and CephFS.
Reviewed-by: Fiona Ebner <f.ebner@proxmox.com>
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/PVE/Storage/BTRFSPlugin.pm | 1 +
src/PVE/Storage/CIFSPlugin.pm | 1 +
src/PVE/Storage/CephFSPlugin.pm | 13 +++++++++++--
src/PVE/Storage/DirPlugin.pm | 1 +
src/PVE/Storage/NFSPlugin.pm | 1 +
5 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/PVE/Storage/BTRFSPlugin.pm b/src/PVE/Storage/BTRFSPlugin.pm
index fa28592..13a208e 100644
--- a/src/PVE/Storage/BTRFSPlugin.pm
+++ b/src/PVE/Storage/BTRFSPlugin.pm
@@ -41,6 +41,7 @@ sub plugindata {
snippets => 1,
none => 1,
import => 1,
+ 'efi-firmware' => 1,
},
{ images => 1, rootdir => 1 },
],
diff --git a/src/PVE/Storage/CIFSPlugin.pm b/src/PVE/Storage/CIFSPlugin.pm
index 54f0f4e..724e5f0 100644
--- a/src/PVE/Storage/CIFSPlugin.pm
+++ b/src/PVE/Storage/CIFSPlugin.pm
@@ -121,6 +121,7 @@ sub plugindata {
backup => 1,
snippets => 1,
import => 1,
+ 'efi-firmware' => 1,
},
{ images => 1 },
],
diff --git a/src/PVE/Storage/CephFSPlugin.pm b/src/PVE/Storage/CephFSPlugin.pm
index fbc9711..fe7975e 100644
--- a/src/PVE/Storage/CephFSPlugin.pm
+++ b/src/PVE/Storage/CephFSPlugin.pm
@@ -116,8 +116,17 @@ sub type {
sub plugindata {
return {
- content =>
- [{ vztmpl => 1, iso => 1, backup => 1, snippets => 1, import => 1 }, { backup => 1 }],
+ content => [
+ {
+ vztmpl => 1,
+ iso => 1,
+ backup => 1,
+ snippets => 1,
+ import => 1,
+ 'efi-firmware' => 1,
+ },
+ { backup => 1 },
+ ],
'sensitive-properties' => { keyring => 1 },
};
}
diff --git a/src/PVE/Storage/DirPlugin.pm b/src/PVE/Storage/DirPlugin.pm
index 80c4a03..ab7911d 100644
--- a/src/PVE/Storage/DirPlugin.pm
+++ b/src/PVE/Storage/DirPlugin.pm
@@ -34,6 +34,7 @@ sub plugindata {
snippets => 1,
none => 1,
import => 1,
+ 'efi-firmware' => 1,
},
{ images => 1, rootdir => 1 },
],
diff --git a/src/PVE/Storage/NFSPlugin.pm b/src/PVE/Storage/NFSPlugin.pm
index 4cc02c9..e8a3661 100644
--- a/src/PVE/Storage/NFSPlugin.pm
+++ b/src/PVE/Storage/NFSPlugin.pm
@@ -62,6 +62,7 @@ sub plugindata {
backup => 1,
snippets => 1,
import => 1,
+ 'efi-firmware' => 1,
},
{ images => 1 },
],
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 04/19] plugin: support efi-firmware volumes in volume export and import
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (2 preceding siblings ...)
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 ` 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
` (14 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Allow moving firmware images between nodes, e.g. with 'pvesm export' and
'pvesm import'. Firmware images are raw files, so handle them like ISO
images.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/PVE/Storage/Plugin.pm | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 43af559..3ceadc0 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -2367,7 +2367,7 @@ sub volume_export_formats {
return ();
}
return ('tar+size') if $format eq 'subvol';
- return ('raw+size') if $vtype =~ /^(images|iso|snippets|vztmpl|import)$/;
+ return ('raw+size') if $vtype =~ /^(images|iso|snippets|vztmpl|import|efi-firmware)$/;
}
return ();
}
@@ -2452,7 +2452,7 @@ sub volume_import {
warn $@ if $@;
die $err;
}
- } elsif (grep { $vtype eq $_ } qw(import iso snippets vztmpl)) {
+ } elsif (grep { $vtype eq $_ } qw(import iso snippets vztmpl efi-firmware)) {
eval {
run_command(['dd', "of=$file", 'conv=excl', 'bs=64k'], input => '<&' . fileno($fh));
};
@@ -2480,7 +2480,7 @@ sub volume_import_formats {
return ();
}
return ('tar+size') if $format eq 'subvol';
- return ('raw+size') if $vtype =~ /^(images|iso|snippets|vztmpl|import)$/;
+ return ('raw+size') if $vtype =~ /^(images|iso|snippets|vztmpl|import|efi-firmware)$/;
}
return ();
}
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 05/19] api: status: support efi-firmware in upload and download-url
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (3 preceding siblings ...)
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 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 06/19] test: volume access: cover efi-firmware volumes Christian Ludwig
` (13 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Allow the content type in both endpoints. File names must consist of the
safe character class and end with '.fd'. Validate the image after the
transfer, like for other content types, and reject anything that is not
detected as a raw image.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/PVE/API2/Storage/Status.pm | 22 ++++++++++++++++++++--
src/PVE/Storage.pm | 19 +++++++++++++++++++
2 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/src/PVE/API2/Storage/Status.pm b/src/PVE/API2/Storage/Status.pm
index 741d514..fd91639 100644
--- a/src/PVE/API2/Storage/Status.pm
+++ b/src/PVE/API2/Storage/Status.pm
@@ -533,7 +533,7 @@ __PACKAGE__->register_method({
description => "Content type.",
type => 'string',
format => 'pve-storage-content',
- enum => ['iso', 'vztmpl', 'import'],
+ enum => ['iso', 'vztmpl', 'import', 'efi-firmware'],
},
filename => {
description =>
@@ -618,6 +618,13 @@ __PACKAGE__->register_method({
}
$path = PVE::Storage::get_import_dir($cfg, $storage);
+ } elsif ($content eq 'efi-firmware') {
+ if ($filename !~
+ m!^${PVE::Storage::SAFE_CHAR_CLASS_RE}+$PVE::Storage::EFI_FIRMWARE_EXT_RE_1$!
+ ) {
+ raise_param_exc({ filename => "invalid filename or wrong extension" });
+ }
+ $path = PVE::Storage::get_efi_firmware_dir($cfg, $storage);
} else {
raise_param_exc({ content => "upload content type '$content' not allowed" });
}
@@ -693,6 +700,8 @@ __PACKAGE__->register_method({
if ($content eq 'iso') {
PVE::Storage::assert_iso_content($tmpfilename);
+ } elsif ($content eq 'efi-firmware') {
+ PVE::Storage::assert_efi_firmware_content($tmpfilename);
}
if ($is_ova) {
@@ -770,7 +779,7 @@ __PACKAGE__->register_method({
description => "Content type.", # TODO: could be optional & detected in most cases
type => 'string',
format => 'pve-storage-content',
- enum => ['iso', 'vztmpl', 'import'],
+ enum => ['iso', 'vztmpl', 'import', 'efi-firmware'],
},
filename => {
description =>
@@ -859,6 +868,13 @@ __PACKAGE__->register_method({
}
$path = PVE::Storage::get_import_dir($cfg, $storage);
+ } elsif ($content eq 'efi-firmware') {
+ if ($filename !~
+ m!^${PVE::Storage::SAFE_CHAR_CLASS_RE}+$PVE::Storage::EFI_FIRMWARE_EXT_RE_1$!
+ ) {
+ raise_param_exc({ filename => "invalid filename or wrong extension" });
+ }
+ $path = PVE::Storage::get_efi_firmware_dir($cfg, $storage);
} else {
raise_param_exc({ content => "upload content-type '$content' is not allowed" });
}
@@ -885,6 +901,8 @@ __PACKAGE__->register_method({
if ($content eq 'iso') {
PVE::Storage::assert_iso_content($tmp_path);
+ } elsif ($content eq 'efi-firmware') {
+ PVE::Storage::assert_efi_firmware_content($tmp_path);
}
if ($is_ova) {
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 03ec6a1..fb37bdb 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -557,6 +557,15 @@ sub get_iso_dir {
return $plugin->get_subdir($scfg, 'iso');
}
+sub get_efi_firmware_dir {
+ my ($cfg, $storeid) = @_;
+
+ my $scfg = storage_config($cfg, $storeid);
+ my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+ return $plugin->get_subdir($scfg, 'efi-firmware');
+}
+
sub get_import_dir {
my ($cfg, $storeid) = @_;
@@ -2514,4 +2523,14 @@ sub assert_iso_content {
return 1;
}
+sub assert_efi_firmware_content {
+ my ($path) = @_;
+
+ my (undef, $format) = file_size_info($path, 10, 'auto-detect', 1);
+ die "efi-firmware image is not in raw format, detected '" . ($format // 'unknown') . "'\n"
+ if !defined($format) || $format ne 'raw';
+
+ return 1;
+}
+
1;
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 06/19] test: volume access: cover efi-firmware volumes
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (4 preceding siblings ...)
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 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-storage v3 07/19] test: list volumes: " Christian Ludwig
` (12 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Check that access is granted based on the content type.
Reviewed-by: Fiona Ebner <f.ebner@proxmox.com>
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/test/run_volume_access_tests.pl | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/test/run_volume_access_tests.pl b/src/test/run_volume_access_tests.pl
index 3448708..72331e5 100755
--- a/src/test/run_volume_access_tests.pl
+++ b/src/test/run_volume_access_tests.pl
@@ -15,7 +15,7 @@ use PVE::Storage::Plugin;
my $storage_cfg = <<'EOF';
dir: dir
path /mnt/pve/dir
- content vztmpl,snippets,iso,backup,rootdir,images
+ content vztmpl,snippets,iso,backup,rootdir,images,efi-firmware
EOF
my $user_cfg = <<'EOF';
@@ -103,6 +103,13 @@ my @tests = (
'iso' => 1,
},
},
+ {
+ volid => 'dir:efi-firmware/custom-uefi.fd',
+ denied_users => {},
+ allowed_types => {
+ 'efi-firmware' => 1,
+ },
+ },
{
volid => 'dir:111/subvol-111-disk-0.subvol',
denied_users => {
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 07/19] test: list volumes: cover efi-firmware volumes
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (5 preceding siblings ...)
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 ` 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
` (11 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Check that files in efi-firmware/ are listed as efi-firmware volumes, and
that only files ending in '.fd' are considered.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/test/list_volumes_test.pm | 47 ++++++++++++++++++++++++++++++++++-
1 file changed, 46 insertions(+), 1 deletion(-)
diff --git a/src/test/list_volumes_test.pm b/src/test/list_volumes_test.pm
index 0876902..4a1ac45 100644
--- a/src/test/list_volumes_test.pm
+++ b/src/test/list_volumes_test.pm
@@ -72,6 +72,7 @@ my $scfg = {
'images' => 1,
'snippets' => 1,
'backup' => 1,
+ 'efi-firmware' => 1,
},
};
@@ -462,6 +463,50 @@ my @tests = (
],
expected => [], # returns empty list
},
+ {
+ description => 'VMID: none, efi-firmware .fd files listed',
+ vmid => undef,
+ files => [
+ "$storage_dir/efi-firmware/custom.fd",
+ "$storage_dir/efi-firmware/vendor-bios-v2.fd",
+ ],
+ expected => [
+ {
+ 'content' => 'efi-firmware',
+ 'ctime' => DEFAULT_CTIME,
+ 'format' => 'raw',
+ 'size' => DEFAULT_SIZE,
+ 'volid' => 'local:efi-firmware/custom.fd',
+ },
+ {
+ 'content' => 'efi-firmware',
+ 'ctime' => DEFAULT_CTIME,
+ 'format' => 'raw',
+ 'size' => DEFAULT_SIZE,
+ 'volid' => 'local:efi-firmware/vendor-bios-v2.fd',
+ },
+ ],
+ },
+ {
+ description => 'VMID: none, only efi-firmware files ending in .fd listed',
+ vmid => undef,
+ files => [
+ "$storage_dir/efi-firmware/custom.fd",
+ "$storage_dir/efi-firmware/custom.FD",
+ "$storage_dir/efi-firmware/custom.iso",
+ "$storage_dir/efi-firmware/custom.raw",
+ "$storage_dir/efi-firmware/custom",
+ ],
+ expected => [
+ {
+ 'content' => 'efi-firmware',
+ 'ctime' => DEFAULT_CTIME,
+ 'format' => 'raw',
+ 'size' => DEFAULT_SIZE,
+ 'volid' => 'local:efi-firmware/custom.fd',
+ },
+ ],
+ },
);
# provide static vmlist for tests
@@ -520,7 +565,7 @@ plan tests => $plan + 1;
{
my $sid = 'local';
- my $types = ['rootdir', 'images', 'vztmpl', 'iso', 'backup', 'snippets'];
+ my $types = ['rootdir', 'images', 'vztmpl', 'iso', 'backup', 'snippets', 'efi-firmware'];
my @suffixes = ('qcow2', 'raw', 'vmdk', 'vhdx');
# run through test cases
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-storage v3 08/19] test: cover efi-firmware volumes in parse_volname and path_to_volume_id
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (6 preceding siblings ...)
2026-10-27 10:12 ` [PATCH pve-storage v3 07/19] test: list volumes: " Christian Ludwig
@ 2026-10-27 10:12 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH qemu-server v3 09/19] config: define bios schema as property-string Christian Ludwig
` (10 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Check that only names with the safe character class and the '.fd'
extension are recognized as efi-firmware volumes.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/test/parse_volname_test.pm | 26 ++++++++++++++++++++++++++
src/test/path_to_volume_id_test.pm | 13 +++++++++++++
2 files changed, 39 insertions(+)
diff --git a/src/test/parse_volname_test.pm b/src/test/parse_volname_test.pm
index 7e937aa..7c6b4a0 100644
--- a/src/test/parse_volname_test.pm
+++ b/src/test/parse_volname_test.pm
@@ -166,6 +166,14 @@ my $tests = [
expected => ['import', 'import.ova/disk.raw', undef, undef, undef, undef, 'ova+raw'],
},
#
+ # EFI firmware
+ #
+ {
+ description => 'EFI firmware, fd',
+ volname => 'efi-firmware/custom-ovmf-code.fd',
+ expected => ['efi-firmware', 'custom-ovmf-code.fd', undef, undef, undef, undef, 'raw'],
+ },
+ #
# failed matches
#
{
@@ -207,6 +215,24 @@ my $tests = [
volname => "import/test.foo",
expected => "unable to parse directory volume name 'import/test.foo'\n",
},
+ {
+ description => 'Failed match: EFI firmware, wrong extension',
+ volname => 'efi-firmware/custom-ovmf-code.raw',
+ expected =>
+ "unable to parse directory volume name 'efi-firmware/custom-ovmf-code.raw'\n",
+ },
+ {
+ description => 'Failed match: EFI firmware, upper case extension',
+ volname => 'efi-firmware/custom-ovmf-code.FD',
+ expected =>
+ "unable to parse directory volume name 'efi-firmware/custom-ovmf-code.FD'\n",
+ },
+ {
+ description => 'Failed match: EFI firmware, unsafe characters',
+ volname => 'efi-firmware/custom ovmf code.fd',
+ expected =>
+ "unable to parse directory volume name 'efi-firmware/custom ovmf code.fd'\n",
+ },
];
# create more test cases for VM disk images matches
diff --git a/src/test/path_to_volume_id_test.pm b/src/test/path_to_volume_id_test.pm
index dfa51e6..10beb5e 100644
--- a/src/test/path_to_volume_id_test.pm
+++ b/src/test/path_to_volume_id_test.pm
@@ -29,6 +29,7 @@ my $scfg = {
'iso' => 1,
'backup' => 1,
'vztmpl' => 1,
+ 'efi-firmware' => 1,
},
},
},
@@ -185,6 +186,13 @@ my @tests = (
'import', 'local:import/import.ovf',
],
},
+ {
+ description => 'EFI firmware, fd',
+ volname => "$storage_dir/efi-firmware/custom-ovmf-code.fd",
+ expected => [
+ 'efi-firmware', 'local:efi-firmware/custom-ovmf-code.fd',
+ ],
+ },
# no matches, path or files with failures
{
@@ -232,6 +240,11 @@ my @tests = (
volname => "$storage_dir/import/test.foo",
expected => [''],
},
+ {
+ description => 'EFI firmware, wrong extension',
+ volname => "$storage_dir/efi-firmware/custom-ovmf-code.raw",
+ expected => [''],
+ },
);
plan tests => scalar @tests + 1;
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH qemu-server v3 09/19] config: define bios schema as property-string
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (7 preceding siblings ...)
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 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH qemu-server v3 10/19] bios: add a custom firmware sub-property Christian Ludwig
` (9 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Convert the bios parameter to a property-string and use the BIOS type as
the default key for backward compatibility. This allows adding further
BIOS related options, like a custom firmware image, as sub-properties.
Add helpers to parse and print the property-string and to query the
BIOS type, and use the latter everywhere the bios option was compared
directly. No functional change intended.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/PVE/QemuConfig.pm | 3 ++-
src/PVE/QemuServer.pm | 15 ++++++-----
src/PVE/QemuServer/BIOS.pm | 48 +++++++++++++++++++++++++++++++++
src/PVE/QemuServer/CPUConfig.pm | 5 ++--
src/PVE/QemuServer/Makefile | 1 +
src/PVE/QemuServer/PCI.pm | 3 ++-
6 files changed, 64 insertions(+), 11 deletions(-)
create mode 100644 src/PVE/QemuServer/BIOS.pm
diff --git a/src/PVE/QemuConfig.pm b/src/PVE/QemuConfig.pm
index 2ef6c075..d83a474c 100644
--- a/src/PVE/QemuConfig.pm
+++ b/src/PVE/QemuConfig.pm
@@ -10,6 +10,7 @@ use PVE::INotify;
use PVE::JSONSchema;
use PVE::QemuMigrate::Helpers;
use PVE::QemuServer::Agent;
+use PVE::QemuServer::BIOS;
use PVE::QemuServer::Blockdev;
use PVE::QemuServer::CPUConfig;
use PVE::QemuServer::Drive;
@@ -192,7 +193,7 @@ sub get_backup_volumes {
my $reason = "backup=";
$reason .= defined($drive->{backup}) ? 'no' : 'yes';
- if ($key =~ m/^efidisk/ && (!defined($conf->{bios}) || $conf->{bios} ne 'ovmf')) {
+ if ($key =~ m/^efidisk/ && PVE::QemuServer::BIOS::get_bios_type($conf) ne 'ovmf') {
$included = 0;
$reason = "efidisk but no OVMF BIOS";
}
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 74772fdd..c288ba22 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -53,6 +53,7 @@ use PVE::QemuConfig;
use PVE::QemuConfig::NoWrite;
use PVE::QemuMigrate::Helpers;
use PVE::QemuServer::Agent qw(get_qga_key parse_guest_agent qga_check_running);
+use PVE::QemuServer::BIOS qw(get_bios_type);
use PVE::QemuServer::Blockdev;
use PVE::QemuServer::BlockJob;
use PVE::QemuServer::Cfg2Cmd;
@@ -659,7 +660,7 @@ EODESCR
bios => {
optional => 1,
type => 'string',
- enum => [qw(seabios ovmf)],
+ format => 'pve-qm-bios',
description => "Select BIOS implementation.",
default => 'seabios',
},
@@ -1535,7 +1536,7 @@ sub print_vga_device {
my $edidoff = "";
if ($type eq 'VGA' && windows_version($conf->{ostype})) {
- $edidoff = ",edid=off" if (!defined($conf->{bios}) || $conf->{bios} ne 'ovmf');
+ $edidoff = ",edid=off" if get_bios_type($conf) ne 'ovmf';
}
my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
@@ -3075,7 +3076,7 @@ my sub should_disable_smm {
return if $machine =~ m/^virt/; # there is no smm flag that could be disabled
return
- (!defined($conf->{bios}) || $conf->{bios} eq 'seabios')
+ get_bios_type($conf) eq 'seabios'
&& $vga->{type}
&& $vga->{type} =~ m/^(serial\d+|none)$/;
}
@@ -3258,7 +3259,7 @@ sub config_to_command {
push @$cmd, '-smbios', "type=1" . $smbios_string;
}
- if ($conf->{bios} && $conf->{bios} eq 'ovmf') {
+ 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;
@@ -3771,10 +3772,10 @@ sub config_to_command {
}
if ($conf->{'amd-sev'}) {
- push @$devices, '-object', get_amd_sev_object($conf->{'amd-sev'}, $conf->{bios});
+ push @$devices, '-object', get_amd_sev_object($conf->{'amd-sev'}, get_bios_type($conf));
push @$machineFlags, 'confidential-guest-support=sev0';
} elsif ($conf->{'intel-tdx'}) {
- my $tdx_object = get_intel_tdx_object($conf->{'intel-tdx'}, $conf->{bios});
+ my $tdx_object = get_intel_tdx_object($conf->{'intel-tdx'}, get_bios_type($conf));
push @$devices, '-object', to_json($tdx_object, { canonical => 1 });
push @$machineFlags, 'confidential-guest-support=tdx0';
push @$machineFlags, 'kernel_irqchip=split';
@@ -5631,7 +5632,7 @@ sub vm_start_nolock {
my $storage_hints = generate_storage_hints($conf, 1);
PVE::Storage::activate_volumes($storecfg, $vollist, undef, $storage_hints);
- check_efi_vars($storecfg, $vmid, $conf) if $conf->{bios} && $conf->{bios} eq 'ovmf';
+ check_efi_vars($storecfg, $vmid, $conf) if get_bios_type($conf) eq 'ovmf';
PVE::QemuServer::Drive::warn_about_virtio_win_issues_in_config($conf);
diff --git a/src/PVE/QemuServer/BIOS.pm b/src/PVE/QemuServer/BIOS.pm
new file mode 100644
index 00000000..4e1a3936
--- /dev/null
+++ b/src/PVE/QemuServer/BIOS.pm
@@ -0,0 +1,48 @@
+package PVE::QemuServer::BIOS;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(parse_property_string print_property_string);
+
+use base 'Exporter';
+
+our @EXPORT_OK = qw(
+ parse_bios
+ print_bios
+ get_bios_type
+);
+
+my $bios_fmt = {
+ type => {
+ type => 'string',
+ enum => [qw(seabios ovmf)],
+ default_key => 1,
+ description => "Select BIOS implementation.",
+ },
+};
+PVE::JSONSchema::register_format('pve-qm-bios', $bios_fmt);
+
+sub parse_bios {
+ my ($value) = @_;
+
+ return {} if !defined($value);
+
+ return parse_property_string($bios_fmt, $value);
+}
+
+sub print_bios {
+ my ($bios) = @_;
+
+ return print_property_string($bios, $bios_fmt);
+}
+
+# Returns the configured BIOS implementation, 'seabios' or 'ovmf'.
+sub get_bios_type {
+ my ($conf) = @_;
+
+ # SeaBIOS is the default if the bios option is not set
+ return parse_bios($conf->{bios})->{type} // 'seabios';
+}
+
+1;
diff --git a/src/PVE/QemuServer/CPUConfig.pm b/src/PVE/QemuServer/CPUConfig.pm
index 2bae55b2..c672302b 100644
--- a/src/PVE/QemuServer/CPUConfig.pm
+++ b/src/PVE/QemuServer/CPUConfig.pm
@@ -11,6 +11,7 @@ use PVE::ProcFSTools;
use PVE::RESTEnvironment qw(log_warn);
use PVE::Tools qw(run_command);
+use PVE::QemuServer::BIOS qw(get_bios_type);
use PVE::QemuServer::Helpers qw(min_version get_host_arch);
use PVE::QemuServer::CPUFlags qw(cpu_flag_supported_re cpu_flag_any_re supported_cpu_flags_names);
@@ -910,7 +911,7 @@ sub get_cpu_options(
$hv_flags = get_hyperv_enlightenments(
$winversion,
$version_guard,
- $conf->{bios},
+ get_bios_type($conf),
$gpu_passthrough,
$hv_vendor_id,
);
@@ -991,7 +992,7 @@ sub get_cpu_options(
$cpu_str .= $phys_bits;
}
- check_phys_bits_above_40_compat($conf->{bios}, $cputype, $resolved_flags)
+ check_phys_bits_above_40_compat(get_bios_type($conf), $cputype, $resolved_flags)
if $using_phys_bits_above_40;
return ('-cpu', $cpu_str);
diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile
index 060fac23..242f1155 100644
--- a/src/PVE/QemuServer/Makefile
+++ b/src/PVE/QemuServer/Makefile
@@ -3,6 +3,7 @@ PREFIX=/usr
PERLDIR=$(PREFIX)/share/perl5
SOURCES=Agent.pm \
+ BIOS.pm \
Blockdev.pm \
BlockJob.pm \
Cfg2Cmd.pm \
diff --git a/src/PVE/QemuServer/PCI.pm b/src/PVE/QemuServer/PCI.pm
index d7dc6121..38640ff0 100644
--- a/src/PVE/QemuServer/PCI.pm
+++ b/src/PVE/QemuServer/PCI.pm
@@ -10,6 +10,7 @@ use PVE::Mapping::PCI;
use PVE::SysFSTools;
use PVE::Tools;
+use PVE::QemuServer::BIOS qw(get_bios_type);
use PVE::QemuServer::Helpers;
use PVE::QemuServer::Machine;
use PVE::QemuServer::PCI::Mdev;
@@ -666,7 +667,7 @@ sub print_hostpci_devices {
my $xvga = '';
if ($d->{'x-vga'}) {
- $xvga = ',x-vga=on' if !($conf->{bios} && $conf->{bios} eq 'ovmf');
+ $xvga = ',x-vga=on' if get_bios_type($conf) ne 'ovmf';
$kvm_off = 1;
$vga->{type} = 'none' if !defined($conf->{vga});
$gpu_passthrough = 1;
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH qemu-server v3 10/19] bios: add a custom firmware sub-property
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (8 preceding siblings ...)
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
2026-10-27 10:12 ` [PATCH qemu-server v3 11/19] api: check the custom firmware of the bios option Christian Ludwig
` (8 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
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
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH qemu-server v3 11/19] api: check the custom firmware of the bios option
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (9 preceding siblings ...)
2026-10-27 10:12 ` [PATCH qemu-server v3 10/19] bios: add a custom firmware sub-property Christian Ludwig
@ 2026-10-27 10:12 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH qemu-server v3 12/19] test: custom firmware in " Christian Ludwig
` (7 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
When the bios option is set via the POST/PUT {vmid}/config endpoints,
check that the user may access the custom firmware image, that it is an
efi-firmware volume, and that the BIOS type is 'ovmf'. Like for ISO
images, whether the image is usable on the node is only checked when the
VM is started. The option keeps requiring the VM.Config.Options
permission.
A cloned VM references the same firmware image, like an ISO image, so a
clone is only considered shared if the image is on a shared storage.
Report custom firmware images in the local disks of the migration
precondition check, so that clients can tell why a migration is not
possible.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/PVE/API2/Qemu.pm | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 922c6599..3338edc2 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -29,6 +29,7 @@ use PVE::GuestImport;
use PVE::QemuConfig;
use PVE::QemuServer;
use PVE::QemuServer::Agent;
+use PVE::QemuServer::BIOS;
use PVE::QemuServer::Blockdev;
use PVE::QemuServer::BlockJob;
use PVE::QemuServer::Cloudinit;
@@ -107,6 +108,20 @@ my $foreach_volume_with_alloc = sub {
}
};
+my $check_bios_param = sub {
+ my ($param) = @_;
+
+ return if !defined($param->{bios});
+
+ eval {
+ PVE::QemuServer::BIOS::check_bios(PVE::QemuServer::BIOS::parse_bios($param->{bios}));
+ };
+ if (my $err = $@) {
+ chomp($err);
+ raise_param_exc({ bios => $err });
+ }
+};
+
my $check_drive_param = sub {
my ($param, $storecfg, $extra_checks) = @_;
@@ -277,6 +292,13 @@ my $check_storage_access = sub {
"/storage/$settings->{vmstatestorage}",
['Datastore.AllocateSpace'],
) if defined($settings->{vmstatestorage});
+
+ if (defined($settings->{bios})) {
+ my $firmware = PVE::QemuServer::BIOS::parse_bios($settings->{bios})->{firmware};
+ PVE::Storage::check_volume_access(
+ $rpcenv, $authuser, $storecfg, $vmid, $firmware, 'efi-firmware',
+ ) if defined($firmware);
+ }
};
my $check_storage_access_clone = sub {
@@ -319,6 +341,13 @@ my $check_storage_access_clone = sub {
$rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
if defined($conf->{vmstatestorage});
+ # like ISO images, a custom firmware image is simply referenced by the clone
+ if (my $firmware = PVE::QemuServer::BIOS::get_bios_firmware($conf)) {
+ my ($sid) = PVE::Storage::parse_volume_id($firmware);
+ my $scfg = PVE::Storage::storage_config($storecfg, $sid);
+ $sharedvm = 0 if !$scfg->{shared};
+ }
+
return $sharedvm;
};
@@ -1365,6 +1394,7 @@ __PACKAGE__->register_method({
&$check_cpu_model_access($rpcenv, $authuser, $param);
$check_drive_param->($param, $storecfg);
+ $check_bios_param->($param);
PVE::QemuServer::Network::add_random_macs($param);
}
@@ -2203,6 +2233,8 @@ my $update_vm_api = sub {
$rpcenv, $authuser, $storecfg, $vmid, $param, undef, $extraction_storage,
);
+ $check_bios_param->($param);
+
PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
my $load_and_check_config = sub {
@@ -5348,6 +5380,11 @@ __PACKAGE__->register_method({
type => 'boolean',
description => 'True if the disk is a cdrom.',
},
+ is_firmware => {
+ type => 'boolean',
+ description => 'True if the volume is a custom firmware image.',
+ optional => 1,
+ },
is_unused => {
type => 'boolean',
description => 'True if the disk is unused.',
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH qemu-server v3 12/19] test: custom firmware in the bios option
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (10 preceding siblings ...)
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 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH qemu-server v3 13/19] test: custom firmware volumes in replication and migration Christian Ludwig
` (6 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Add test cases for different custom firmware settings in the VM config,
including the errors for a missing image, a BIOS type other than 'ovmf',
a volume of another content type, a storage that does not exist and a
storage without the efi-firmware content type. Also enhance the
PVE::QemuServer::OVMF test mock to handle nonexistent files.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
.../efi-custom-firmware-content-disabled.conf | 5 +++
.../cfg2cmd/efi-custom-firmware-legacy.conf | 13 ++++++++
.../efi-custom-firmware-legacy.conf.cmd | 28 ++++++++++++++++
.../efi-custom-firmware-no-storage.conf | 5 +++
.../efi-custom-firmware-not-found.conf | 5 +++
.../cfg2cmd/efi-custom-firmware-seabios.conf | 4 +++
src/test/cfg2cmd/efi-custom-firmware-sev.conf | 6 ++++
.../cfg2cmd/efi-custom-firmware-sev.conf.cmd | 32 +++++++++++++++++++
src/test/cfg2cmd/efi-custom-firmware-snp.conf | 5 +++
.../cfg2cmd/efi-custom-firmware-snp.conf.cmd | 30 +++++++++++++++++
src/test/cfg2cmd/efi-custom-firmware-tdx.conf | 5 +++
.../cfg2cmd/efi-custom-firmware-tdx.conf.cmd | 30 +++++++++++++++++
.../efi-custom-firmware-wrong-content.conf | 5 +++
src/test/cfg2cmd/efi-custom-firmware.conf | 4 +++
src/test/cfg2cmd/efi-custom-firmware.conf.cmd | 31 ++++++++++++++++++
src/test/parse-config-input/efi-firmware.conf | 12 +++++++
.../regular-vm-efifirmware.conf | 16 ++++++++++
src/test/run_config2command_tests.pl | 2 ++
src/test/run_parse_config_tests.pl | 2 +-
19 files changed, 239 insertions(+), 1 deletion(-)
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-content-disabled.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-legacy.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-legacy.conf.cmd
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-no-storage.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-not-found.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-seabios.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-sev.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-sev.conf.cmd
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-snp.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-snp.conf.cmd
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-tdx.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-tdx.conf.cmd
create mode 100644 src/test/cfg2cmd/efi-custom-firmware-wrong-content.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware.conf
create mode 100644 src/test/cfg2cmd/efi-custom-firmware.conf.cmd
create mode 100644 src/test/parse-config-input/efi-firmware.conf
create mode 100644 src/test/parse-config-input/regular-vm-efifirmware.conf
diff --git a/src/test/cfg2cmd/efi-custom-firmware-content-disabled.conf b/src/test/cfg2cmd/efi-custom-firmware-content-disabled.conf
new file mode 100644
index 00000000..d8071d74
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-content-disabled.conf
@@ -0,0 +1,5 @@
+# TEST: Custom firmware on a storage without the efi-firmware content type causes die
+# EXPECT_ERROR: bios: custom firmware 'noimages:efi-firmware/custom.fd' - storage 'noimages' does not support content type 'efi-firmware'
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=noimages:efi-firmware/custom.fd
+efidisk0: local:100/vm-100-disk-0.raw
diff --git a/src/test/cfg2cmd/efi-custom-firmware-legacy.conf b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf
new file mode 100644
index 00000000..5f0cb899
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf
@@ -0,0 +1,13 @@
+# TEST: Custom firmware replaces system OVMF_CODE path in legacy -drive command line
+bios: ovmf,firmware=local:efi-firmware/custom.fd
+bootdisk: scsi0
+cores: 2
+efidisk0: local:100/vm-100-disk-1.qcow2,size=128K
+machine: pc-q35-7.0
+meta: creation-qemu=6.1
+memory: 512
+net0: virtio=2E:01:68:F9:9C:87,bridge=vmbr0
+ostype: l26
+scsihw: virtio-scsi-pci
+smbios1: uuid=3dd750ce-d910-44d0-9493-525c0be4e687
+vmgenid: 54d1c06c-8f5b-440f-b5b2-6eab1380e13d
diff --git a/src/test/cfg2cmd/efi-custom-firmware-legacy.conf.cmd b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf.cmd
new file mode 100644
index 00000000..c20ff00c
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf.cmd
@@ -0,0 +1,28 @@
+/usr/bin/kvm \
+ -id 8006 \
+ -name vm8006 \
+ -no-shutdown \
+ -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+ -object 'monitor-qmp,chardev=qmp,id=monitor-qmp' \
+ -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+ -object 'monitor-qmp,chardev=qmp-event,id=monitor-qmp-event' \
+ -pidfile /var/run/qemu-server/8006.pid \
+ -daemonize \
+ -smbios 'type=1,uuid=3dd750ce-d910-44d0-9493-525c0be4e687' \
+ -drive 'if=pflash,unit=0,format=raw,readonly=on,file=/var/lib/vz/efi-firmware/custom.fd' \
+ -drive 'if=pflash,unit=1,id=drive-efidisk0,format=qcow2,file=/var/lib/vz/images/100/vm-100-disk-1.qcow2' \
+ -smp '2,sockets=1,cores=2,maxcpus=2' \
+ -nodefaults \
+ -boot 'menu=on,strict=on,reboot-timeout=1000,splash=/usr/share/qemu-server/bootsplash.jpg' \
+ -vnc 'unix:/var/run/qemu-server/8006.vnc,password=on' \
+ -cpu kvm64,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+lahf_lm,+sep \
+ -m 512 \
+ -readconfig /usr/share/qemu-server/pve-q35-4.0.cfg \
+ -device 'vmgenid,guid=54d1c06c-8f5b-440f-b5b2-6eab1380e13d' \
+ -device 'usb-tablet,id=tablet,bus=ehci.0,port=1' \
+ -device 'VGA,id=vga,bus=pcie.0,addr=0x1' \
+ -device 'virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3,free-page-reporting=on' \
+ -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+ -netdev 'type=tap,id=net0,ifname=tap8006i0,script=/usr/libexec/qemu-server/pve-bridge,downscript=/usr/libexec/qemu-server/pve-bridgedown,vhost=on' \
+ -device 'virtio-net-pci,mac=2E:01:68:F9:9C:87,netdev=net0,bus=pci.0,addr=0x12,id=net0,bootindex=300' \
+ -machine 'type=pc-q35-7.0+pve0'
diff --git a/src/test/cfg2cmd/efi-custom-firmware-no-storage.conf b/src/test/cfg2cmd/efi-custom-firmware-no-storage.conf
new file mode 100644
index 00000000..1bab97ae
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-no-storage.conf
@@ -0,0 +1,5 @@
+# TEST: Custom firmware on a storage that does not exist causes die
+# EXPECT_ERROR: bios: custom firmware 'nostorage:efi-firmware/custom.fd' - storage 'nostorage' does not exist
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=nostorage:efi-firmware/custom.fd
+efidisk0: local:100/vm-100-disk-0.raw
diff --git a/src/test/cfg2cmd/efi-custom-firmware-not-found.conf b/src/test/cfg2cmd/efi-custom-firmware-not-found.conf
new file mode 100644
index 00000000..67e9d4f5
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-not-found.conf
@@ -0,0 +1,5 @@
+# TEST: Custom firmware pointing to a nonexistent file causes die
+# EXPECT_ERROR: custom firmware image '/var/lib/vz/efi-firmware/nonexistent.fd' not found
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=local:efi-firmware/nonexistent.fd
+efidisk0: local:100/vm-100-disk-0.raw
diff --git a/src/test/cfg2cmd/efi-custom-firmware-seabios.conf b/src/test/cfg2cmd/efi-custom-firmware-seabios.conf
new file mode 100644
index 00000000..d4435400
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-seabios.conf
@@ -0,0 +1,4 @@
+# TEST: Custom firmware without the ovmf BIOS type causes die
+# EXPECT_ERROR: a custom firmware image requires the 'ovmf' BIOS type
+bios: seabios,firmware=local:efi-firmware/custom.fd
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
diff --git a/src/test/cfg2cmd/efi-custom-firmware-sev.conf b/src/test/cfg2cmd/efi-custom-firmware-sev.conf
new file mode 100644
index 00000000..237ae030
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-sev.conf
@@ -0,0 +1,6 @@
+# TEST: Custom firmware replaces system OVMF_SEV_CODE path in SEV CVM blockdev command line
+# HW_CAPABILITIES: amd-turin-9005
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=local:efi-firmware/custom.fd
+efidisk0: local:100/vm-100-disk-0.raw,efitype=4m,pre-enrolled-keys=1,size=528K
+amd-sev: type=std
diff --git a/src/test/cfg2cmd/efi-custom-firmware-sev.conf.cmd b/src/test/cfg2cmd/efi-custom-firmware-sev.conf.cmd
new file mode 100644
index 00000000..35d1fd91
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-sev.conf.cmd
@@ -0,0 +1,32 @@
+/usr/bin/kvm \
+ -id 8006 \
+ -name vm8006 \
+ -no-shutdown \
+ -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+ -object 'monitor-qmp,chardev=qmp,id=monitor-qmp' \
+ -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+ -object 'monitor-qmp,chardev=qmp-event,id=monitor-qmp-event' \
+ -pidfile /var/run/qemu-server/8006.pid \
+ -daemonize \
+ -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+ -object '{"id":"throttle-drive-efidisk0","limits":{},"qom-type":"throttle-group"}' \
+ -blockdev '{"driver":"raw","file":{"driver":"file","filename":"/var/lib/vz/efi-firmware/custom.fd"},"node-name":"pflash0","read-only":true}' \
+ -blockdev '{"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":false,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"raw","file":{"aio":"io_uring","cache":{"direct":false,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/100/vm-100-disk-0.raw","node-name":"e1175f2a490414e7c53337589fde17a","read-only":false},"node-name":"f1175f2a490414e7c53337589fde17a","read-only":false,"size":540672},"node-name":"drive-efidisk0","read-only":false,"throttle-group":"throttle-drive-efidisk0"}' \
+ -smp '1,sockets=1,cores=1,maxcpus=1' \
+ -nodefaults \
+ -boot 'menu=on,strict=on,reboot-timeout=1000,splash=/usr/share/qemu-server/bootsplash.jpg' \
+ -vnc 'unix:/var/run/qemu-server/8006.vnc,password=on' \
+ -cpu kvm64,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+lahf_lm,+sep \
+ -m 512 \
+ -global 'PIIX4_PM.disable_s3=1' \
+ -global 'PIIX4_PM.disable_s4=1' \
+ -device 'pci-bridge,id=pci.1,chassis_nr=1,bus=pci.0,addr=0x1e' \
+ -device 'pci-bridge,id=pci.2,chassis_nr=2,bus=pci.0,addr=0x1f' \
+ -device 'pci-bridge,id=pci.4,chassis_nr=4,bus=pci.1,addr=0x1c' \
+ -device 'piix3-usb-uhci,id=uhci,bus=pci.0,addr=0x1.0x2' \
+ -device 'usb-tablet,id=tablet,bus=uhci.0,port=1' \
+ -device 'VGA,id=vga,bus=pci.0,addr=0x2' \
+ -device 'virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3,free-page-reporting=on' \
+ -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+ -object 'sev-guest,id=sev0,cbitpos=51,reduced-phys-bits=6,policy=0x8' \
+ -machine 'pflash0=pflash0,pflash1=drive-efidisk0,type=pc+pve0,confidential-guest-support=sev0'
diff --git a/src/test/cfg2cmd/efi-custom-firmware-snp.conf b/src/test/cfg2cmd/efi-custom-firmware-snp.conf
new file mode 100644
index 00000000..094a8e89
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-snp.conf
@@ -0,0 +1,5 @@
+# TEST: Custom firmware replaces default SNP firmware in -bios argument
+# HW_CAPABILITIES: amd-turin-9005
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=local:efi-firmware/custom.fd
+amd-sev: type=snp
diff --git a/src/test/cfg2cmd/efi-custom-firmware-snp.conf.cmd b/src/test/cfg2cmd/efi-custom-firmware-snp.conf.cmd
new file mode 100644
index 00000000..ca1268c0
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-snp.conf.cmd
@@ -0,0 +1,30 @@
+/usr/bin/kvm \
+ -id 8006 \
+ -name vm8006 \
+ -no-shutdown \
+ -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+ -object 'monitor-qmp,chardev=qmp,id=monitor-qmp' \
+ -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+ -object 'monitor-qmp,chardev=qmp-event,id=monitor-qmp-event' \
+ -pidfile /var/run/qemu-server/8006.pid \
+ -daemonize \
+ -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+ -bios /var/lib/vz/efi-firmware/custom.fd \
+ -smp '1,sockets=1,cores=1,maxcpus=1' \
+ -nodefaults \
+ -boot 'menu=on,strict=on,reboot-timeout=1000,splash=/usr/share/qemu-server/bootsplash.jpg' \
+ -vnc 'unix:/var/run/qemu-server/8006.vnc,password=on' \
+ -cpu kvm64,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+lahf_lm,+sep \
+ -m 512 \
+ -global 'PIIX4_PM.disable_s3=1' \
+ -global 'PIIX4_PM.disable_s4=1' \
+ -device 'pci-bridge,id=pci.1,chassis_nr=1,bus=pci.0,addr=0x1e' \
+ -device 'pci-bridge,id=pci.2,chassis_nr=2,bus=pci.0,addr=0x1f' \
+ -device 'pci-bridge,id=pci.4,chassis_nr=4,bus=pci.1,addr=0x1c' \
+ -device 'piix3-usb-uhci,id=uhci,bus=pci.0,addr=0x1.0x2' \
+ -device 'usb-tablet,id=tablet,bus=uhci.0,port=1' \
+ -device 'VGA,id=vga,bus=pci.0,addr=0x2' \
+ -device 'virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3,free-page-reporting=on' \
+ -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+ -object 'sev-snp-guest,id=sev0,cbitpos=51,reduced-phys-bits=6,policy=0xb0000' \
+ -machine 'type=pc+pve0,confidential-guest-support=sev0'
diff --git a/src/test/cfg2cmd/efi-custom-firmware-tdx.conf b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf
new file mode 100644
index 00000000..62167eb9
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf
@@ -0,0 +1,5 @@
+# TEST: Custom firmware replaces default TDX firmware in -bios argument
+# HW_CAPABILITIES: {"intel-tdx":{"tdx-support":true},"amd-sev":{"cbitpos":0,"reduced-phys-bits":0,"sev-support":false,"sev-support-es":false,"sev-support-snp":false}}
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=local:efi-firmware/custom.fd
+intel-tdx: tdx,attestation=0
diff --git a/src/test/cfg2cmd/efi-custom-firmware-tdx.conf.cmd b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf.cmd
new file mode 100644
index 00000000..ca615654
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf.cmd
@@ -0,0 +1,30 @@
+/usr/bin/kvm \
+ -id 8006 \
+ -name vm8006 \
+ -no-shutdown \
+ -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+ -object 'monitor-qmp,chardev=qmp,id=monitor-qmp' \
+ -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+ -object 'monitor-qmp,chardev=qmp-event,id=monitor-qmp-event' \
+ -pidfile /var/run/qemu-server/8006.pid \
+ -daemonize \
+ -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+ -bios /var/lib/vz/efi-firmware/custom.fd \
+ -smp '1,sockets=1,cores=1,maxcpus=1' \
+ -nodefaults \
+ -boot 'menu=on,strict=on,reboot-timeout=1000,splash=/usr/share/qemu-server/bootsplash.jpg' \
+ -vnc 'unix:/var/run/qemu-server/8006.vnc,password=on' \
+ -cpu kvm64,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+lahf_lm,+sep \
+ -m 512 \
+ -global 'PIIX4_PM.disable_s3=1' \
+ -global 'PIIX4_PM.disable_s4=1' \
+ -device 'pci-bridge,id=pci.1,chassis_nr=1,bus=pci.0,addr=0x1e' \
+ -device 'pci-bridge,id=pci.2,chassis_nr=2,bus=pci.0,addr=0x1f' \
+ -device 'pci-bridge,id=pci.4,chassis_nr=4,bus=pci.1,addr=0x1c' \
+ -device 'piix3-usb-uhci,id=uhci,bus=pci.0,addr=0x1.0x2' \
+ -device 'usb-tablet,id=tablet,bus=uhci.0,port=1' \
+ -device 'VGA,id=vga,bus=pci.0,addr=0x2' \
+ -device 'virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3,free-page-reporting=on' \
+ -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+ -object '{"id":"tdx0","qom-type":"tdx-guest"}' \
+ -machine 'type=pc+pve0,confidential-guest-support=tdx0,kernel_irqchip=split'
diff --git a/src/test/cfg2cmd/efi-custom-firmware-wrong-content.conf b/src/test/cfg2cmd/efi-custom-firmware-wrong-content.conf
new file mode 100644
index 00000000..e4d11f58
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-wrong-content.conf
@@ -0,0 +1,5 @@
+# TEST: Custom firmware referencing a volume of another content type causes die
+# EXPECT_ERROR: bios: custom firmware 'local:iso/custom.iso' - volume has content type 'iso', expected 'efi-firmware'
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=local:iso/custom.iso
+efidisk0: local:100/vm-100-disk-0.raw
diff --git a/src/test/cfg2cmd/efi-custom-firmware.conf b/src/test/cfg2cmd/efi-custom-firmware.conf
new file mode 100644
index 00000000..709ff748
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware.conf
@@ -0,0 +1,4 @@
+# TEST: Custom firmware replaces system OVMF_CODE path in blockdev command line
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf,firmware=local:efi-firmware/custom.fd
+efidisk0: local:100/vm-100-disk-0.raw
diff --git a/src/test/cfg2cmd/efi-custom-firmware.conf.cmd b/src/test/cfg2cmd/efi-custom-firmware.conf.cmd
new file mode 100644
index 00000000..90ab2556
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware.conf.cmd
@@ -0,0 +1,31 @@
+/usr/bin/kvm \
+ -id 8006 \
+ -name vm8006 \
+ -no-shutdown \
+ -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+ -object 'monitor-qmp,chardev=qmp,id=monitor-qmp' \
+ -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+ -object 'monitor-qmp,chardev=qmp-event,id=monitor-qmp-event' \
+ -pidfile /var/run/qemu-server/8006.pid \
+ -daemonize \
+ -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+ -object '{"id":"throttle-drive-efidisk0","limits":{},"qom-type":"throttle-group"}' \
+ -blockdev '{"driver":"raw","file":{"driver":"file","filename":"/var/lib/vz/efi-firmware/custom.fd"},"node-name":"pflash0","read-only":true}' \
+ -blockdev '{"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":false,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"raw","file":{"aio":"io_uring","cache":{"direct":false,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/100/vm-100-disk-0.raw","node-name":"e1175f2a490414e7c53337589fde17a","read-only":false},"node-name":"f1175f2a490414e7c53337589fde17a","read-only":false,"size":131072},"node-name":"drive-efidisk0","read-only":false,"throttle-group":"throttle-drive-efidisk0"}' \
+ -smp '1,sockets=1,cores=1,maxcpus=1' \
+ -nodefaults \
+ -boot 'menu=on,strict=on,reboot-timeout=1000,splash=/usr/share/qemu-server/bootsplash.jpg' \
+ -vnc 'unix:/var/run/qemu-server/8006.vnc,password=on' \
+ -cpu kvm64,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+lahf_lm,+sep \
+ -m 512 \
+ -global 'PIIX4_PM.disable_s3=1' \
+ -global 'PIIX4_PM.disable_s4=1' \
+ -device 'pci-bridge,id=pci.1,chassis_nr=1,bus=pci.0,addr=0x1e' \
+ -device 'pci-bridge,id=pci.2,chassis_nr=2,bus=pci.0,addr=0x1f' \
+ -device 'pci-bridge,id=pci.4,chassis_nr=4,bus=pci.1,addr=0x1c' \
+ -device 'piix3-usb-uhci,id=uhci,bus=pci.0,addr=0x1.0x2' \
+ -device 'usb-tablet,id=tablet,bus=uhci.0,port=1' \
+ -device 'VGA,id=vga,bus=pci.0,addr=0x2' \
+ -device 'virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3,free-page-reporting=on' \
+ -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+ -machine 'pflash0=pflash0,pflash1=drive-efidisk0,type=pc+pve0'
diff --git a/src/test/parse-config-input/efi-firmware.conf b/src/test/parse-config-input/efi-firmware.conf
new file mode 100644
index 00000000..7c4f04cf
--- /dev/null
+++ b/src/test/parse-config-input/efi-firmware.conf
@@ -0,0 +1,12 @@
+bios: ovmf,firmware=local:efi-firmware/custom.fd
+boot: order=scsi0
+cores: 2
+efidisk0: local-lvm:vm-100-disk-0,efitype=4m,pre-enrolled-keys=1
+memory: 1024
+name: test-vm
+net0: virtio=BC:24:11:2C:69:EC,bridge=vmbr0
+numa: 0
+ostype: l26
+scsi0: local-lvm:vm-100-disk-1,size=8G
+scsihw: virtio-scsi-pci
+sockets: 1
diff --git a/src/test/parse-config-input/regular-vm-efifirmware.conf b/src/test/parse-config-input/regular-vm-efifirmware.conf
new file mode 100644
index 00000000..4b76ccf8
--- /dev/null
+++ b/src/test/parse-config-input/regular-vm-efifirmware.conf
@@ -0,0 +1,16 @@
+# regular VM with an EFI disk and custom firmware
+bios: ovmf,firmware=mydir:efi-firmware/custom.fd
+boot: order=scsi0;ide2;net0
+cores: 1
+efidisk0: mydir:139/vm-139-disk-0.qcow2,size=128K
+ide2: local:iso/debian-10.6.0-amd64-netinst.iso,media=cdrom
+memory: 2048
+name: eficloneclone
+net0: virtio=7A:6C:A5:8B:11:93,bridge=vmbr0,firewall=1
+numa: 0
+ostype: l26
+scsi0: rbdkvm:vm-139-disk-1,size=4G
+scsihw: virtio-scsi-pci
+smbios1: uuid=21a7e7bc-3cd2-4232-a009-a41f4ee992ae
+sockets: 1
+vmgenid: 0
diff --git a/src/test/run_config2command_tests.pl b/src/test/run_config2command_tests.pl
index 28f538d3..34193549 100755
--- a/src/test/run_config2command_tests.pl
+++ b/src/test/run_config2command_tests.pl
@@ -31,6 +31,7 @@ my $base_env = {
content => {
images => 1,
iso => 1,
+ 'efi-firmware' => 1,
},
path => '/var/lib/vz',
type => 'dir',
@@ -286,6 +287,7 @@ my $qemu_server_ovmf_module = Test::MockModule->new("PVE::QemuServer::OVMF");
$qemu_server_ovmf_module->mock(
file_exists => sub {
my ($path) = @_;
+ return 0 if $path =~ m/nonexistent/;
return 1;
},
file_get_size => sub {
diff --git a/src/test/run_parse_config_tests.pl b/src/test/run_parse_config_tests.pl
index 62e36ee0..b2a02374 100755
--- a/src/test/run_parse_config_tests.pl
+++ b/src/test/run_parse_config_tests.pl
@@ -26,7 +26,7 @@ my $OUTPUT_DIR = './parse-config-output';
my $EXPECTED_DIR = './parse-config-expected';
# NOTE update when you add/remove tests
-plan tests => 2 * 10;
+plan tests => 2 * 12;
sub run_tests {
my ($strict) = @_;
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH qemu-server v3 13/19] test: custom firmware volumes in replication and migration
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (11 preceding siblings ...)
2026-10-27 10:12 ` [PATCH qemu-server v3 12/19] test: custom firmware in " Christian Ludwig
@ 2026-10-27 10:12 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-manager v3 14/19] ui: storage: add efi-firmware content type support Christian Ludwig
` (5 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Test that custom firmware images are excluded from volume replication,
and that migration is refused for images on a local storage but works
for images on a shared storage.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
src/test/run_qemu_migrate_tests.pl | 61 +++++++++++++++++++++++
src/test/test_get_replicatable_volumes.pl | 10 ++++
2 files changed, 71 insertions(+)
diff --git a/src/test/run_qemu_migrate_tests.pl b/src/test/run_qemu_migrate_tests.pl
index 05eed1d9..69f2cc1f 100755
--- a/src/test/run_qemu_migrate_tests.pl
+++ b/src/test/run_qemu_migrate_tests.pl
@@ -77,6 +77,7 @@ my $storage_config = {
"local-dir" => {
content => {
images => 1,
+ 'efi-firmware' => 1,
},
path => "/some/dir/",
type => "dir",
@@ -88,6 +89,15 @@ my $storage_config = {
path => "/some/other/dir/",
type => "dir",
},
+ "shared-dir" => {
+ content => {
+ images => 1,
+ 'efi-firmware' => 1,
+ },
+ path => "/mnt/pve/shared-dir/",
+ type => "dir",
+ shared => 1,
+ },
"zfs-alias-1" => {
content => {
images => 1,
@@ -1259,6 +1269,57 @@ my $tests = [
},
},
},
+ {
+ name => '105_local_firmware',
+ target => 'pve1',
+ vmid => 105,
+ vm_status => {
+ running => 0,
+ },
+ config_patch => {
+ bios => 'ovmf,firmware=local-dir:efi-firmware/custom.fd',
+ },
+ expected_calls => {},
+ expect_die => "local custom firmware image - use a shared storage",
+ expected => {
+ source_volids => local_volids_for_vm(105),
+ target_volids => {},
+ vm_config => get_patched_config(
+ 105,
+ {
+ bios => 'ovmf,firmware=local-dir:efi-firmware/custom.fd',
+ },
+ ),
+ vm_status => {
+ running => 0,
+ },
+ },
+ },
+ {
+ name => '105_shared_firmware',
+ target => 'pve1',
+ vmid => 105,
+ vm_status => {
+ running => 0,
+ },
+ config_patch => {
+ bios => 'ovmf,firmware=shared-dir:efi-firmware/custom.fd',
+ },
+ expected_calls => $default_expected_calls_offline,
+ expected => {
+ source_volids => {},
+ target_volids => local_volids_for_vm(105),
+ vm_config => get_patched_config(
+ 105,
+ {
+ bios => 'ovmf,firmware=shared-dir:efi-firmware/custom.fd',
+ },
+ ),
+ vm_status => {
+ running => 0,
+ },
+ },
+ },
{
name => '149_running_missing_option_withlocaldisks',
target => 'pve1',
diff --git a/src/test/test_get_replicatable_volumes.pl b/src/test/test_get_replicatable_volumes.pl
index 6a3d0338..c693ee06 100755
--- a/src/test/test_get_replicatable_volumes.pl
+++ b/src/test/test_get_replicatable_volumes.pl
@@ -22,6 +22,7 @@ my $storecfg = {
'backup' => 1,
'images' => 1,
'rootdir' => 1,
+ 'efi-firmware' => 1,
},
path => "/var/lib/vz",
},
@@ -159,5 +160,14 @@ $conf = PVE::QemuServer::parse_vm_config("/qemu-server/$vmid.conf", $rawconf);
eval { $volumes = PVE::QemuConfig->get_replicatable_volumes($storecfg, $vmid, $conf, 0, 0); };
is($@, "missing replicate feature on volume 'local:900/vm-900-disk-2.raw'\n", $test_name);
+$test_name = "custom firmware is shared, not owned by VM";
+$rawconf = <<__EOD__;
+bios: ovmf,firmware=local:efi-firmware/custom.fd
+__EOD__
+
+$conf = PVE::QemuServer::parse_vm_config("/qemu-server/$vmid.conf", $rawconf);
+$volumes = PVE::QemuConfig->get_replicatable_volumes($storecfg, $vmid, $conf, 0, 0);
+is_deeply($volumes, {}, $test_name);
+
done_testing();
exit(0);
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-manager v3 14/19] ui: storage: add efi-firmware content type support
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (12 preceding siblings ...)
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 ` 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
` (4 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Add the 'efi-firmware' storage content type to the Web UI:
- Utils.js: add display name for the content type
- ContentTypeSelector: include in the default content type list
- CephFSEdit: include in its explicit content type list, the backend
CephFS plugin allows the type as well
- Browser: add a tab for browsing and uploading firmware images; the
format is always 'raw' for this type, so drop that column
- UploadToStorage: only accept files with the '.fd' extension, like the
backend does
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
www/manager6/Utils.js | 1 +
www/manager6/form/ContentTypeSelector.js | 11 ++++++++++-
www/manager6/storage/Browser.js | 14 ++++++++++++++
www/manager6/storage/CephFSEdit.js | 2 +-
www/manager6/window/UploadToStorage.js | 1 +
5 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 8b99371d..e83d87ff 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -740,6 +740,7 @@ Ext.define('PVE.Utils', {
rootdir: gettext('Container'),
snippets: gettext('Snippets'),
import: gettext('Import'),
+ 'efi-firmware': gettext('EFI Firmware'),
},
// volume can be a full volume info object, in which case the format parameter is ignored, or
diff --git a/www/manager6/form/ContentTypeSelector.js b/www/manager6/form/ContentTypeSelector.js
index 60532251..b33eb124 100644
--- a/www/manager6/form/ContentTypeSelector.js
+++ b/www/manager6/form/ContentTypeSelector.js
@@ -10,7 +10,16 @@ Ext.define('PVE.form.ContentTypeSelector', {
me.comboItems = [];
if (me.cts === undefined) {
- me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets', 'import'];
+ me.cts = [
+ 'images',
+ 'iso',
+ 'vztmpl',
+ 'backup',
+ 'rootdir',
+ 'snippets',
+ 'import',
+ 'efi-firmware',
+ ];
}
Ext.Array.each(me.cts, function (ct) {
diff --git a/www/manager6/storage/Browser.js b/www/manager6/storage/Browser.js
index d0237948..611c0009 100644
--- a/www/manager6/storage/Browser.js
+++ b/www/manager6/storage/Browser.js
@@ -189,6 +189,20 @@ Ext.define('PVE.storage.Browser', {
pluginType: plugin,
});
}
+ if (contents.includes('efi-firmware')) {
+ me.items.push({
+ xtype: 'pveStorageContentView',
+ title: gettext('EFI Firmware'),
+ iconCls: 'fa fa-microchip',
+ itemId: 'contentEfiFirmware',
+ content: 'efi-firmware',
+ showColumns: ['name', 'date', 'size'],
+ pluginType: plugin,
+ enableUploadButton: enableUpload,
+ enableDownloadUrlButton: enableDownloadUrl,
+ useUploadButton: true,
+ });
+ }
}
if (caps.storage['Permissions.Modify']) {
diff --git a/www/manager6/storage/CephFSEdit.js b/www/manager6/storage/CephFSEdit.js
index db54df87..3b7a49f3 100644
--- a/www/manager6/storage/CephFSEdit.js
+++ b/www/manager6/storage/CephFSEdit.js
@@ -95,7 +95,7 @@ Ext.define('PVE.storage.CephFSInputPanel', {
me.column2 = [
{
xtype: 'pveContentTypeSelector',
- cts: ['backup', 'iso', 'vztmpl', 'snippets', 'import'],
+ cts: ['backup', 'iso', 'vztmpl', 'snippets', 'import', 'efi-firmware'],
fieldLabel: gettext('Content'),
name: 'content',
value: 'backup',
diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
index a5fa1773..5b75555d 100644
--- a/www/manager6/window/UploadToStorage.js
+++ b/www/manager6/window/UploadToStorage.js
@@ -12,6 +12,7 @@ Ext.define('PVE.window.UploadToStorage', {
import: ['.ova', '.qcow2', '.raw', '.vmdk'],
iso: ['.img', '.iso'],
vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
+ 'efi-firmware': ['.fd'],
},
// accepted for file selection, but the extension does not tell the image format, so the user
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-manager v3 15/19] ui: form: support other content types in the ISO selector
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (13 preceding siblings ...)
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 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-manager v3 16/19] ui: qemu: allow selecting a custom EFI firmware image Christian Ludwig
` (3 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
The selector pairs a storage selector with a file selector and keeps
both in sync, which is useful for any content type, not just ISO images.
Filter both lists by the 'storageContent' config. The file selector
label gets configured via 'fileLabel'.
The defaults keep the existing ISO behavior.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
www/manager6/form/IsoSelector.js | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/www/manager6/form/IsoSelector.js b/www/manager6/form/IsoSelector.js
index b2d94ed3..508c0eaf 100644
--- a/www/manager6/form/IsoSelector.js
+++ b/www/manager6/form/IsoSelector.js
@@ -13,11 +13,17 @@ Ext.define('PVE.form.IsoSelector', {
labelWidth: undefined,
labelAlign: 'right',
+ // Storage content type to select from, default: iso.
+ storageContent: 'iso',
+ fileLabel: gettext('ISO image'),
+
cbindData: function () {
let me = this;
return {
nodename: me.nodename,
insideWizard: me.insideWizard,
+ storageContent: me.storageContent,
+ fieldLabel: me.fileLabel,
};
},
@@ -71,9 +77,9 @@ Ext.define('PVE.form.IsoSelector', {
reference: 'storage',
isFormField: false,
fieldLabel: gettext('Storage'),
- storageContent: 'iso',
allowBlank: false,
cbind: {
+ storageContent: '{storageContent}',
nodename: '{nodename}',
autoSelect: '{insideWizard}',
insideWizard: '{insideWizard}',
@@ -94,10 +100,10 @@ Ext.define('PVE.form.IsoSelector', {
xtype: 'pveFileSelector',
reference: 'file',
isFormField: false,
- storageContent: 'iso',
- fieldLabel: gettext('ISO image'),
labelAlign: 'right',
cbind: {
+ storageContent: '{storageContent}',
+ fieldLabel: '{fieldLabel}',
nodename: '{nodename}',
disabled: '{disabled}',
labelWidth: '{labelWidth}',
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-manager v3 16/19] ui: qemu: allow selecting a custom EFI firmware image
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (14 preceding siblings ...)
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 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-manager v3 17/19] ui: migrate: refuse migration with a local custom " Christian Ludwig
` (2 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Offer a custom EFI firmware image in the BIOS editor, as it is the
'firmware' sub-property of the 'bios' setting. The BIOS selector gains
an 'ovmf-custom' pseudo value, which maps to 'bios=ovmf,firmware=<volid>'
and reveals a storage and image selector. Choosing any other BIOS drops
the sub-property. The 'bios' hardware row shows a custom image as
'OVMF (custom), firmware=<volid>'.
Add helpers to parse and print the 'bios' property-string and use them
wherever the BIOS type is compared.
The image replaces the OVMF code image without touching the EFI vars
store, so only offer it for guests that load the firmware read-only
via '-bios' and have no vars store, i.e. confidential VMs. The API
stays general, so an image that is already set is always shown and can
be cleared, with a hint about the vars store when an EFI disk exists.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
www/manager6/Parser.js | 8 ++
www/manager6/Utils.js | 2 +
www/manager6/form/QemuBiosSelector.js | 22 ++++-
www/manager6/qemu/Architecture.js | 4 +-
www/manager6/qemu/BootOrderEdit.js | 2 +-
www/manager6/qemu/HardwareView.js | 36 ++++++-
www/manager6/qemu/QemuBiosEdit.js | 131 ++++++++++++++++++++------
7 files changed, 164 insertions(+), 41 deletions(-)
diff --git a/www/manager6/Parser.js b/www/manager6/Parser.js
index 36df8e9d..ab4a04e7 100644
--- a/www/manager6/Parser.js
+++ b/www/manager6/Parser.js
@@ -457,6 +457,14 @@ Ext.define('PVE.Parser', {
return arr.join(',');
},
+ parseQemuBios: function (value) {
+ return PVE.Parser.parsePropertyString(value, 'type') ?? {};
+ },
+
+ printQemuBios: function (bios) {
+ return PVE.Parser.printPropertyString(bios, 'type');
+ },
+
parseQemuSmbios1: function (value) {
let res = value.split(',').reduce((acc, currentValue) => {
const [k, v] = currentValue.split(/[=](.+)/);
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index e83d87ff..11f6d15c 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -552,6 +552,8 @@ Ext.define('PVE.Utils', {
return 'SeaBIOS';
} else if (value === 'ovmf') {
return 'OVMF (UEFI)';
+ } else if (value === 'ovmf-custom') {
+ return gettext('OVMF (custom)');
} else {
return value;
}
diff --git a/www/manager6/form/QemuBiosSelector.js b/www/manager6/form/QemuBiosSelector.js
index 40abb589..62f67e32 100644
--- a/www/manager6/form/QemuBiosSelector.js
+++ b/www/manager6/form/QemuBiosSelector.js
@@ -2,13 +2,25 @@ Ext.define('PVE.form.QemuBiosSelector', {
extend: 'PVE.form.FilteredKVComboBox',
alias: ['widget.pveQemuBiosSelector'],
- comboItems: [
- ['__default__', PVE.Utils.render_qemu_bios('')],
- ['seabios', PVE.Utils.render_qemu_bios('seabios')],
- ['ovmf', PVE.Utils.render_qemu_bios('ovmf')],
- ],
+ withCustomFirmware: false,
allowedValuesPerCategory: PVE.qemu.Architecture.allowedFirmware,
setDefaultDisplay: (arch) => PVE.Utils.render_qemu_bios('', arch),
+
+ initComponent: function () {
+ let me = this;
+
+ me.comboItems = [
+ ['__default__', PVE.Utils.render_qemu_bios('')],
+ ['seabios', PVE.Utils.render_qemu_bios('seabios')],
+ ['ovmf', PVE.Utils.render_qemu_bios('ovmf')],
+ ];
+
+ if (me.withCustomFirmware) {
+ me.comboItems.push(['ovmf-custom', PVE.Utils.render_qemu_bios('ovmf-custom')]);
+ }
+
+ me.callParent();
+ },
});
diff --git a/www/manager6/qemu/Architecture.js b/www/manager6/qemu/Architecture.js
index 7b6ef402..6db8f02e 100644
--- a/www/manager6/qemu/Architecture.js
+++ b/www/manager6/qemu/Architecture.js
@@ -63,8 +63,8 @@ Ext.define('PVE.qemu.Architecture', {
},
allowedFirmware: {
- x86_64: ['__default__', 'seabios', 'ovmf'], // default is seabios
- aarch64: ['ovmf'],
+ x86_64: ['__default__', 'seabios', 'ovmf', 'ovmf-custom'], // default is seabios
+ aarch64: ['ovmf', 'ovmf-custom'],
},
render_vcpu_architecture: function (value) {
diff --git a/www/manager6/qemu/BootOrderEdit.js b/www/manager6/qemu/BootOrderEdit.js
index 6239aeca..1df37243 100644
--- a/www/manager6/qemu/BootOrderEdit.js
+++ b/www/manager6/qemu/BootOrderEdit.js
@@ -46,7 +46,7 @@ Ext.define('PVE.qemu.BootOrderPanel', {
emptyWarning.setHidden(val !== '');
let showRngPxeWarning =
- view.vmconfig?.bios === 'ovmf' &&
+ PVE.Parser.parseQemuBios(view.vmconfig?.bios).type === 'ovmf' &&
!view.vmconfig?.rng0 &&
val?.includes('net');
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index f7f4b46f..356b3c98 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -173,7 +173,17 @@ Ext.define('PVE.qemu.HardwareView', {
editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
defaultValue: '',
iconCls: 'microchip',
- renderer: PVE.Utils.render_qemu_bios,
+ renderer: function (value) {
+ let bios = PVE.Parser.parseQemuBios(value);
+ if (bios.firmware) {
+ return (
+ PVE.Utils.render_qemu_bios('ovmf-custom') +
+ ', firmware=' +
+ Ext.htmlEncode(bios.firmware)
+ );
+ }
+ return PVE.Utils.render_qemu_bios(bios.type);
+ },
},
vga: {
header: gettext('Display'),
@@ -280,6 +290,12 @@ Ext.define('PVE.qemu.HardwareView', {
affinity: {
visible: false,
},
+ 'amd-sev': {
+ visible: false,
+ },
+ 'intel-tdx': {
+ visible: false,
+ },
};
PVE.Utils.forEachBus(undefined, function (type, id) {
@@ -461,6 +477,20 @@ Ext.define('PVE.qemu.HardwareView', {
},
};
+ if (rec.data.key === 'bios') {
+ // A custom EFI firmware image replaces the OVMF code image without touching
+ // the EFI vars store. Only offer when EFI vars are not necessary, i.e. for
+ // confidential VMs. Also offer when one is set already.
+ let value = (key) => me.getObjectValue(key, undefined, true);
+ let cvmType =
+ PVE.Parser.parsePropertyString(value('amd-sev'), 'type')?.type ??
+ PVE.Parser.parsePropertyString(value('intel-tdx'), 'type')?.type;
+
+ commonOpts.withCustomFirmware =
+ ['snp', 'tdx'].includes(cvmType) ||
+ !!PVE.Parser.parseQemuBios(value('bios')).firmware;
+ }
+
if (Ext.isString(editor)) {
Ext.create(editor, commonOpts);
} else {
@@ -709,7 +739,9 @@ Ext.define('PVE.qemu.HardwareView', {
autoShow: true,
url: '/api2/extjs/' + baseurl,
pveSelNode: me.pveSelNode,
- usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf',
+ usesEFI: [bios?.value, bios?.pending].some(
+ (value) => PVE.Parser.parseQemuBios(value).type === 'ovmf',
+ ),
listeners: {
destroy: () => me.reload(),
},
diff --git a/www/manager6/qemu/QemuBiosEdit.js b/www/manager6/qemu/QemuBiosEdit.js
index a637ed25..3602c1f5 100644
--- a/www/manager6/qemu/QemuBiosEdit.js
+++ b/www/manager6/qemu/QemuBiosEdit.js
@@ -1,9 +1,12 @@
-Ext.define('PVE.qemu.BiosEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveQemuBiosEdit',
+Ext.define('PVE.qemu.BiosInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveQemuBiosInputPanel',
onlineHelp: 'qm_bios_and_uefi',
- subject: 'BIOS',
+
+ nodename: undefined,
+
+ withCustomFirmware: false,
viewModel: {
data: {
@@ -12,38 +15,91 @@ Ext.define('PVE.qemu.BiosEdit', {
},
formulas: {
showEFIDiskHint: (get) => get('bios') === 'ovmf' && !get('efidisk0'),
+ customFirmware: (get) => get('bios') === 'ovmf-custom',
+ // the vars store of an existing EFI disk is kept as it is, so it can go out of sync
+ showVarStoreHint: (get) => get('bios') === 'ovmf-custom' && !!get('efidisk0'),
},
},
- items: [
- {
- xtype: 'pveQemuBiosSelector',
- onlineHelp: 'qm_bios_and_uefi',
- name: 'bios',
- value: '__default__',
- bind: {
- value: '{bios}',
- category: '{arch}',
+ onGetValues: function (values) {
+ // the custom firmware is a sub-property of 'bios', so it is dropped with any other choice
+ if (values.bios === 'ovmf-custom') {
+ values.bios = PVE.Parser.printQemuBios({ type: 'ovmf', firmware: values.firmware });
+ }
+ delete values.firmware;
+
+ return values;
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ me.items = [
+ {
+ xtype: 'pveQemuBiosSelector',
+ onlineHelp: 'qm_bios_and_uefi',
+ name: 'bios',
+ value: '__default__',
+ withCustomFirmware: me.withCustomFirmware,
+ bind: {
+ value: '{bios}',
+ category: '{arch}',
+ },
+ fieldLabel: 'BIOS',
},
- fieldLabel: 'BIOS',
- },
- {
- xtype: 'displayfield',
- name: 'efidisk0',
- bind: '{efidisk0}',
- hidden: true,
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- 'You need to add an EFI disk for storing the EFI settings. See the online help for details.',
- ),
- bind: {
- hidden: '{!showEFIDiskHint}',
+ {
+ xtype: 'displayfield',
+ name: 'efidisk0',
+ bind: '{efidisk0}',
+ hidden: true,
},
- },
- ],
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext(
+ 'You need to add an EFI disk for storing the EFI settings. See the online help for details.',
+ ),
+ bind: {
+ hidden: '{!showEFIDiskHint}',
+ },
+ },
+ {
+ xtype: 'pveIsoSelector',
+ name: 'firmware',
+ storageContent: 'efi-firmware',
+ fileLabel: gettext('Firmware Image'),
+ nodename: me.nodename,
+ hidden: true,
+ disabled: true,
+ bind: {
+ hidden: '{!customFirmware}',
+ disabled: '{!customFirmware}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext(
+ 'Note: The EFI disk keeps its vars store, which needs to match the custom firmware, otherwise the guest might not boot.',
+ ),
+ bind: {
+ hidden: '{!showVarStoreHint}',
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.BiosEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveQemuBiosEdit',
+
+ onlineHelp: 'qm_bios_and_uefi',
+ subject: 'BIOS',
+
+ withCustomFirmware: false,
initComponent: function () {
let me = this;
@@ -54,6 +110,14 @@ Ext.define('PVE.qemu.BiosEdit', {
throw 'no nodename given';
}
+ me.items = [
+ {
+ xtype: 'pveQemuBiosInputPanel',
+ nodename: me.nodename,
+ withCustomFirmware: me.withCustomFirmware,
+ },
+ ];
+
me.callParent();
if (!me.isCreate) {
@@ -61,6 +125,11 @@ Ext.define('PVE.qemu.BiosEdit', {
success: function ({ result }) {
let values = result.data;
let arch = PVE.qemu.Architecture.getGuestArchitecture(values.arch, me.nodename);
+ if (values.bios) {
+ let bios = PVE.Parser.parseQemuBios(values.bios);
+ values.bios = bios.firmware ? 'ovmf-custom' : bios.type;
+ values.firmware = bios.firmware;
+ }
me.setValues(values);
me.down('pveQemuBiosSelector').setCategory(arch);
},
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-manager v3 17/19] ui: migrate: refuse migration with a local custom firmware image
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (15 preceding siblings ...)
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 ` 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
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
A custom firmware image is not owned by the VM and never copied to the
target node, so the backend refuses the migration unless the image is on
a shared storage. Show this as an error in the preconditions, rather than
offering to migrate it like a local disk.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
www/manager6/window/Migrate.js | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/www/manager6/window/Migrate.js b/www/manager6/window/Migrate.js
index 8cac54ea..89026d73 100644
--- a/www/manager6/window/Migrate.js
+++ b/www/manager6/window/Migrate.js
@@ -364,7 +364,16 @@ Ext.define('PVE.window.Migrate', {
if (migrateStats.local_disks.length) {
migrateStats.local_disks.forEach(function (disk) {
- if (disk.cdrom && disk.cdrom === 1) {
+ if (disk.is_firmware) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: Ext.String.format(
+ gettext('Cannot migrate VM with local custom firmware image: {0}'),
+ disk.volid,
+ ),
+ severity: 'error',
+ });
+ } else if (disk.cdrom && disk.cdrom === 1) {
if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) {
migration.possible = false;
migration.preconditions.push({
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-docs v3 18/19] pvesm: document the efi-firmware content type
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (16 preceding siblings ...)
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 ` Christian Ludwig
2026-10-27 10:12 ` [PATCH pve-docs v3 19/19] qm: document the custom firmware of the bios option Christian Ludwig
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Describe the content type and list it for the backends that support it.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
pve-storage-cephfs.adoc | 4 ++--
pve-storage-cifs.adoc | 4 ++--
pve-storage-dir.adoc | 5 +++--
pve-storage-nfs.adoc | 4 ++--
pvesm.adoc | 5 +++++
5 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/pve-storage-cephfs.adoc b/pve-storage-cephfs.adoc
index 7df0613..6d83be7 100644
--- a/pve-storage-cephfs.adoc
+++ b/pve-storage-cephfs.adoc
@@ -124,8 +124,8 @@ The `cephfs` backend is a POSIX-compliant filesystem, on top of a Ceph cluster.
.Storage features for backend `cephfs`
[width="100%",cols="m,m,3*d",options="header"]
|==============================================================================
-|Content types |Image formats |Shared |Snapshots |Clones
-|vztmpl iso backup snippets |none |yes |yes^[1]^ |no
+|Content types |Image formats |Shared |Snapshots |Clones
+|vztmpl iso backup snippets efi-firmware |none |yes |yes^[1]^ |no
|==============================================================================
^[1]^ While no known bugs exist, snapshots are not yet guaranteed to be stable,
as they lack sufficient testing.
diff --git a/pve-storage-cifs.adoc b/pve-storage-cifs.adoc
index 1664764..53ef141 100644
--- a/pve-storage-cifs.adoc
+++ b/pve-storage-cifs.adoc
@@ -98,8 +98,8 @@ features available.
.Storage features for backend `cifs`
[width="100%",cols="m,m,3*d",options="header"]
|==============================================================================
-|Content types |Image formats |Shared |Snapshots |Clones
-|images rootdir vztmpl iso backup snippets |raw qcow2 vmdk |yes |qcow2 |qcow2
+|Content types |Image formats |Shared |Snapshots |Clones
+|images rootdir vztmpl iso backup snippets efi-firmware |raw qcow2 vmdk |yes |qcow2 |qcow2
|==============================================================================
Examples
diff --git a/pve-storage-dir.adoc b/pve-storage-dir.adoc
index 9905017..250ef58 100644
--- a/pve-storage-dir.adoc
+++ b/pve-storage-dir.adoc
@@ -41,6 +41,7 @@ storage backends.
|Backup files |`dump/`
|Snippets |`snippets/`
|Import |`import/`
+|EFI firmware |`efi-firmware/`
|===========================================================
@@ -119,8 +120,8 @@ feature to create clones.
.Storage features for backend `dir`
[width="100%",cols="m,m,3*d",options="header"]
|==============================================================================
-|Content types |Image formats |Shared |Snapshots |Clones
-|images rootdir vztmpl iso backup snippets |raw qcow2 vmdk subvol |no |qcow2 |qcow2
+|Content types |Image formats |Shared |Snapshots |Clones
+|images rootdir vztmpl iso backup snippets efi-firmware |raw qcow2 vmdk subvol |no |qcow2 |qcow2
|==============================================================================
diff --git a/pve-storage-nfs.adoc b/pve-storage-nfs.adoc
index ea5bbdc..33793b3 100644
--- a/pve-storage-nfs.adoc
+++ b/pve-storage-nfs.adoc
@@ -73,8 +73,8 @@ to implement snapshots and cloning.
.Storage features for backend `nfs`
[width="100%",cols="m,m,3*d",options="header"]
|==============================================================================
-|Content types |Image formats |Shared |Snapshots |Clones
-|images rootdir vztmpl iso backup snippets |raw qcow2 vmdk |yes |qcow2 |qcow2
+|Content types |Image formats |Shared |Snapshots |Clones
+|images rootdir vztmpl iso backup snippets efi-firmware |raw qcow2 vmdk |yes |qcow2 |qcow2
|==============================================================================
Examples
diff --git a/pvesm.adoc b/pvesm.adoc
index 5bd24b2..b1e2b73 100644
--- a/pvesm.adoc
+++ b/pvesm.adoc
@@ -250,6 +250,11 @@ import:::
OVAs and VM disk images that can be imported from this storage
+efi-firmware:::
+
+Custom EFI firmware images (raw images with the `.fd` extension) for use with
+OVMF-based VMs
+
shared::
Indicate that this is a single storage with the same contents on all nodes (or
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH pve-docs v3 19/19] qm: document the custom firmware of the bios option
2026-10-27 10:12 [PATCH v3 00/19] Support for custom EFI firmware Christian Ludwig
` (17 preceding siblings ...)
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 ` Christian Ludwig
18 siblings, 0 replies; 20+ messages in thread
From: Christian Ludwig @ 2026-10-27 10:12 UTC (permalink / raw)
To: pve-devel
Describe how to set and remove a custom EFI firmware image, and what to
consider for migration, replication, templates and backups.
Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
qm.adoc | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/qm.adoc b/qm.adoc
index 18a4fbe..81d97e7 100644
--- a/qm.adoc
+++ b/qm.adoc
@@ -1250,6 +1250,61 @@ NOTE: The markers `ms-cert=2023` and `ms-cert=2023w` may indicate partial
enrollment. The VM start task log will warn about this. You should apply the
enrollment procedure for such EFI disks too.
+[[qm_custom_efi_firmware]]
+Custom EFI Firmware
+^^^^^^^^^^^^^^^^^^^
+
+By default, {pve} uses the system-provided OVMF firmware images. If you need a
+custom or vendor-specific EFI firmware code image, you can override the default
+firmware with the `firmware` property of the `bios` VM option.
+
+The firmware image must first be uploaded to a storage that has the
+`efi-firmware` content type enabled. It must be a raw image and its file name
+must end with `.fd`.
+
+To configure a VM to use a custom firmware image:
+
+----
+# qm set <vmid> -bios ovmf,firmware=<storage>:efi-firmware/<name>.fd
+----
+
+For example:
+
+----
+# qm set 100 -bios ovmf,firmware=local:efi-firmware/custom-ovmf-code.fd
+----
+
+NOTE: A custom firmware image is only supported with the `ovmf` BIOS type.
+
+The custom image replaces the firmware code (pflash0) only. The EFI vars disk
+(`efidisk0`) keeps its contents, which are created from the vars template of
+the system-provided firmware. If they do not match the custom image, the guest
+may fail to boot. A matching vars image can be imported with:
+
+----
+# qm set <vmid> -efidisk0 <storage>:0,efitype=4m,pre-enrolled-keys=0,import-from=<source>
+----
+
+Confidential VMs (AMD SEV-SNP, Intel TDX) load the firmware read-only via
+`-bios` and use no EFI vars disk at all, so this does not apply to them. For
+that reason, the web interface only offers custom firmware images for such VMs,
+through the 'BIOS' entry in the 'Hardware' panel of a VM.
+
+A VM that references a custom firmware image can only start if the image is
+available on the node. This also applies to templates and to backups of stopped
+VMs, which start the VM for the duration of the backup, and to VMs restored on
+another node or cluster.
+
+The firmware image is neither copied when a VM migrates nor replicated. To be
+able to migrate a VM, put the image on a shared storage, so that it is available
+on all nodes.
+
+To remove a custom firmware assignment and revert to the default OVMF image:
+
+----
+# qm set <vmid> -bios ovmf
+----
+
[[qm_tpm]]
Trusted Platform Module (TPM)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-10-27 10:15 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH qemu-server v3 10/19] bios: add a custom firmware sub-property Christian Ludwig
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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox