all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH storage/qemu 0/13]: Custom UEFI firmware in PVE
@ 2026-08-17 11:59 ` Christian Ludwig
  0 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 1126 bytes --]

Hi,

this series brings initial support for custom UEFI firmware to PVE.
This is useful for confidential computing workloads, where VMs may
bring their own firmware and not rely on the hypervisor's. There is
also some other software around that ships it's own VM firmware. The
firmware needs to be compatible to KVM/Qemu, of course. See the design
discussion earlier at [1].

The first part of the series brings a new storage content type for EFI
firmware, which can be set on directory-based storage. There is no
restriction on the actual firmware file's name.

The second part brings new 'efi-firmware' VM config key allows Qemu to
use a file from a storage with that new content type, instead of the
default firmware. To reduce complexity, this only works with bios=ovmf.

This feature is only configurable from the API. The GUI parts were left
out on purpose. I am unsure how much we want/need to expose there in the
first place.

We have tested this with custom UEFI firmware in AMD SEV/SNP
confidential VMs.


 - Christian

[1] https://lore.proxmox.com/pve-devel/10513e3f2c0d94bc938a540b4a0a18749eb5ed96.camel@genua.de/

^ permalink raw reply	[flat|nested] 28+ messages in thread

* [PATCH pve-storage 1/13] Add efi-firmware content type
  2026-08-17 11:59 ` Christian Ludwig
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 2393 bytes --]

This is meant to hold custom OVMF code files. It is stored in the
efi-firmware/ subdirectory of the storage path. There is no restriction
on the file name. But stick with the safe character set, basically for
safety reasons.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/Storage/Plugin.pm | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 4f69f9b..f6ca140 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -372,7 +372,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
 
@@ -831,6 +831,8 @@ 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}+)$!) {
+        return ('efi-firmware', $1, undef, undef, undef, undef, 'raw');
     }
 
     die "unable to parse directory volume name '$volname'\n";
@@ -844,6 +846,7 @@ my $vtype_subdirs = {
     backup => 'dump',
     snippets => 'snippets',
     import => 'import',
+    'efi-firmware' => 'efi-firmware',
 };
 
 sub get_vtype_subdirs {
@@ -1753,6 +1756,10 @@ 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}+)$!i;
+
+            $info = { volid => "$sid:efi-firmware/$1", format => 'raw' };
         }
 
         $info->{size} = $st->size;
@@ -1789,6 +1796,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] 28+ messages in thread

* [PATCH pve-storage 2/13] Test for efi-firmware content type
  2026-08-17 11:59 ` Christian Ludwig
  (?)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 658 bytes --]

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] 28+ messages in thread

* [PATCH pve-storage 3/13] Allow efi-firmware in file-based storage
  2026-08-17 11:59 ` Christian Ludwig
                   ` (2 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 2569 bytes --]

Enable efi-firmware files to be placed in file-based storage backends.

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 | 2 +-
 src/PVE/Storage/DirPlugin.pm    | 1 +
 src/PVE/Storage/NFSPlugin.pm    | 1 +
 5 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Storage/BTRFSPlugin.pm b/src/PVE/Storage/BTRFSPlugin.pm
index fb47aa0..2f999b2 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..1a4747a 100644
--- a/src/PVE/Storage/CephFSPlugin.pm
+++ b/src/PVE/Storage/CephFSPlugin.pm
@@ -117,7 +117,7 @@ sub type {
 sub plugindata {
     return {
         content =>
-            [{ vztmpl => 1, iso => 1, backup => 1, snippets => 1, import => 1 }, { backup => 1 }],
+            [{ 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] 28+ messages in thread

* [PATCH pve-storage 4/13] Extend storage API endpoints for efi-firmware
  2026-08-17 11:59 ` Christian Ludwig
                   ` (3 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 3790 bytes --]

Extend the storage upload and download-url API endpoints for the
efi-firmware content type. Provide an error message if the file name
does not match the safe character set.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/API2/Storage/Status.pm | 14 ++++++++++++--
 src/PVE/Storage.pm             | 13 +++++++++++--
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/src/PVE/API2/Storage/Status.pm b/src/PVE/API2/Storage/Status.pm
index 741d514..a6fc317 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,11 @@ __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}+$!) {
+                raise_param_exc({ filename => "invalid file name" });
+            }
+            $path = PVE::Storage::get_efi_firmware_dir($cfg, $storage);
         } else {
             raise_param_exc({ content => "upload content type '$content' not allowed" });
         }
@@ -770,7 +775,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 +864,11 @@ __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}+$!) {
+                raise_param_exc({ filename => "invalid file name" });
+            }
+            $path = PVE::Storage::get_efi_firmware_dir($cfg, $storage);
         } else {
             raise_param_exc({ content => "upload content-type '$content' is not allowed" });
         }
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 64ea9da..1083b1d 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -555,6 +555,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) = @_;
 
@@ -629,7 +638,7 @@ 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,
@@ -1297,7 +1306,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];
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH pve-storage 5/13] Volume access check test for efi-firmware
  2026-08-17 11:59 ` Christian Ludwig
                   ` (4 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 1035 bytes --]

Add a test case for an efi-firmware volume to check for it's type.

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] 28+ messages in thread

* [PATCH pve-storage 6/13] efi-firmware storage path to volume conversion test
  2026-08-17 11:59 ` Christian Ludwig
                   ` (5 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 2798 bytes --]

Add test cases to check that storage paths under the efi-firmware/
subdirectory are correctly listed as efi-firmware content type volumes,
regardless of file extension.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/test/list_volumes_test.pm | 51 ++++++++++++++++++++++++++++++++++-
 1 file changed, 50 insertions(+), 1 deletion(-)

diff --git a/src/test/list_volumes_test.pm b/src/test/list_volumes_test.pm
index 0876902..a1e98d1 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,54 @@ 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, all efi-firmware files listed regardless of extension',
+        vmid => undef,
+        files => [
+            "$storage_dir/efi-firmware/custom.fd",
+            "$storage_dir/efi-firmware/custom.iso",
+        ],
+        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/custom.iso',
+            },
+        ],
+    },
 );
 
 # provide static vmlist for tests
@@ -520,7 +569,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] 28+ messages in thread

* [PATCH qemu-server 07/13] Add efi-firmware key to VM config schema
  2026-08-17 11:59 ` Christian Ludwig
                   ` (6 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 3354 bytes --]

Add a new config key 'efi-firmware' to the VM config schema. That key
points to a firmware content type file.

It is restricted to OVMF bios settings. A custom efi-firmware image does
not make sense for VMs with legacy BIOS.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/QemuConfig.pm |  7 +++++++
 src/PVE/QemuServer.pm | 30 ++++++++++++++++++++++++++++--
 2 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/src/PVE/QemuConfig.pm b/src/PVE/QemuConfig.pm
index 26f0fda2..d25f2bbb 100644
--- a/src/PVE/QemuConfig.pm
+++ b/src/PVE/QemuConfig.pm
@@ -112,6 +112,13 @@ sub parse_volume {
             die $err;
         }
         $volume = { 'file' => $volume_string };
+    } elsif ($key eq 'efi-firmware') {
+        eval { PVE::JSONSchema::check_format('pve-volume-id', $volume_string) };
+        if (my $err = $@) {
+            return if $noerr;
+            die $err;
+        }
+        $volume = { 'file' => $volume_string };
     } else {
         $volume = PVE::QemuServer::Drive::parse_drive($key, $volume_string);
     }
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 2f43faa7..b95fcb8c 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -663,6 +663,14 @@ EODESCR
         description => "Select BIOS implementation.",
         default => 'seabios',
     },
+    'efi-firmware' => {
+        optional => 1,
+        type => 'string',
+        format => 'pve-volume-id',
+        description => "Custom EFI firmware code image (pflash0). Must be a volid "
+            . "referencing a 'efi-firmware' content type volume (e.g. "
+            . "'local:efi-firmware/custom.fd'). Requires bios=ovmf.",
+    },
     vmgenid => {
         type => 'string',
         pattern => '(?:[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}|[01])',
@@ -2078,6 +2086,21 @@ sub parse_vm_config {
 
     $res->{pending} = {} if !defined($res->{pending});
 
+    # config sanity checks
+    if ($res->{'efi-firmware'}) {
+        if (!$res->{bios} || $res->{bios} ne 'ovmf') {
+            $handle_error->("vm $vmid - efi-firmware requires bios=ovmf\n");
+        } else {
+            my ($sid, $volname) = PVE::Storage::parse_volume_id($res->{'efi-firmware'}, 1);
+            if (!$sid || $volname !~ m!^efi-firmware/[^/]+$!) {
+                $handle_error->(
+                    "vm $vmid - efi-firmware: invalid volid format,"
+                    . " expected <storeid>:efi-firmware/<name>\n"
+                );
+            }
+        }
+    }
+
     return $res;
 }
 
@@ -4568,11 +4591,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 'efi-firmware';
+
         $volhash->{$volid}->{drivename} = $key if is_valid_drivename($key);
     };
 
     my $include_opts = {
-        extra_keys => ['vmstate'],
+        extra_keys => ['vmstate', 'efi-firmware'],
         include_unused => 1,
     };
 
@@ -6123,7 +6149,7 @@ sub get_current_vm_volumes {
 
     PVE::QemuConfig->foreach_volume_full(
         $conf,
-        { extra_keys => ['vmstate'] },
+        { extra_keys => ['vmstate', 'efi-firmware'] },
         sub {
             my ($ds, $drive) = @_;
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH qemu-server 08/13] Add efi-firmware support to the API
  2026-08-17 11:59 ` Christian Ludwig
                   ` (7 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 1474 bytes --]

Add an efi-firmware key to the POST/PUT {vmid}/config API endpoint.
It needs VM.Config.HWType permission. And deleting efi-firmware from the
config does not trigger volume cleanup, firmware images are shared.

Note that changing the bios type requires VM.Config.Options permissions.
That should probably move to the VM.Config.HWType permission, too.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/API2/Qemu.pm | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 3320313c..6fc7c1f7 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -818,6 +818,7 @@ my $hwtypeoptions = {
     'watchdog' => 1,
     'audio0' => 1,
     'rng0' => 1,
+    'efi-firmware' => 1,
 };
 
 my $generaloptions = {
@@ -2523,6 +2524,11 @@ my $update_vm_api = sub {
                     print "automatic pinning of machine version failed - $@" if $@;
                 }
                 $conf->{pending}->{$opt} = $param->{$opt};
+            } elsif ($opt eq 'efi-firmware') {
+                PVE::Storage::check_volume_access(
+                    $rpcenv, $authuser, $storecfg, $vmid, $param->{$opt},
+                );
+                $conf->{pending}->{$opt} = $param->{$opt};
             } elsif ($opt eq 'cipassword') {
                 if (!PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
                     # Same logic as in cloud-init (but with the regex fixed...)
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH qemu-server 09/13] Generate efi-firmware Qemu command line
  2026-08-17 11:59 ` Christian Ludwig
                   ` (8 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 2354 bytes --]

If efi-firmware is set, use the correct EFI code file to create the Qemu
command line.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/QemuServer/OVMF.pm | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/PVE/QemuServer/OVMF.pm b/src/PVE/QemuServer/OVMF.pm
index 67665c7c..fe301353 100644
--- a/src/PVE/QemuServer/OVMF.pm
+++ b/src/PVE/QemuServer/OVMF.pm
@@ -123,6 +123,9 @@ my sub print_ovmf_drive_commandlines {
         if $cvm_type && $cvm_type eq 'tdx';
 
     my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d, $q35, $cvm_type);
+    if ($conf->{'efi-firmware'}) {
+        $ovmf_code = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+    }
     my $ovmf_vars_size = file_get_size($ovmf_vars);
 
     my $var_drive_str = "if=pflash,unit=1,id=drive-efidisk0";
@@ -219,6 +222,9 @@ my sub generate_ovmf_blockdev {
         if $cvm_type && $cvm_type eq 'snp';
 
     my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $drive, $q35, $cvm_type);
+    if ($conf->{'efi-firmware'}) {
+        $ovmf_code = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+    }
 
     my $ovmf_code_blockdev = {
         driver => 'raw',
@@ -268,6 +274,12 @@ sub print_ovmf_commandline {
 
     my $cvm_type = $hw_info->{'cvm-type'};
 
+    if ($conf->{'efi-firmware'}) {
+        my $fw_path = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+        die "efi-firmware volume '$conf->{'efi-firmware'}' not found at '$fw_path'\n"
+            if !file_exists($fw_path);
+    }
+
     my $cmd = [];
     my $machine_flags = [];
 
@@ -277,7 +289,13 @@ 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 $bios_path;
+        if ($conf->{'efi-firmware'}) {
+            $bios_path = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+        } else {
+            ($bios_path) = get_ovmf_files($hw_info->{arch}, undef, undef, $cvm_type);
+        }
+        push $cmd->@*, '-bios', $bios_path;
     } 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] 28+ messages in thread

* [PATCH qemu-server 10/13] test: efi-firmware key in VM config
  2026-08-17 11:59 ` Christian Ludwig
                   ` (9 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 22577 bytes --]

Add test cases for different efi-firmware settings in VM config.
Also enhance the PVE::QemuServer::OVMF test mock to trigger to handle
nonexistent files.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 .../cfg2cmd/efi-custom-firmware-legacy.conf   |  6 ++++
 .../efi-custom-firmware-legacy.conf.cmd       | 27 ++++++++++++++++
 .../efi-custom-firmware-not-found.conf        |  6 ++++
 src/test/cfg2cmd/efi-custom-firmware-old.conf |  6 ++++
 .../cfg2cmd/efi-custom-firmware-old.conf.cmd  | 27 ++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware-sev.conf |  7 +++++
 .../cfg2cmd/efi-custom-firmware-sev.conf.cmd  | 31 +++++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware-snp.conf |  6 ++++
 .../cfg2cmd/efi-custom-firmware-snp.conf.cmd  | 29 +++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware-tdx.conf |  6 ++++
 .../cfg2cmd/efi-custom-firmware-tdx.conf.cmd  | 29 +++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware.conf     |  5 +++
 src/test/cfg2cmd/efi-custom-firmware.conf.cmd | 30 ++++++++++++++++++
 .../efi-firmware-bad-volid.conf.strict.error  |  1 +
 .../efi-firmware-no-ovmf.conf.strict.error    |  1 +
 .../efi-firmware-bad-volid.conf               | 13 ++++++++
 .../efi-firmware-no-ovmf.conf                 | 11 +++++++
 src/test/parse-config-input/efi-firmware.conf | 13 ++++++++
 .../regular-vm-efifirmware.conf               | 17 ++++++++++
 src/test/run_config2command_tests.pl          |  2 ++
 src/test/run_parse_config_tests.pl            |  2 +-
 21 files changed, 274 insertions(+), 1 deletion(-)
 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-not-found.conf
 create mode 100644 src/test/cfg2cmd/efi-custom-firmware-old.conf
 create mode 100644 src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd
 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.conf
 create mode 100644 src/test/cfg2cmd/efi-custom-firmware.conf.cmd
 create mode 100644 src/test/parse-config-expected/efi-firmware-bad-volid.conf.strict.error
 create mode 100644 src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error
 create mode 100644 src/test/parse-config-input/efi-firmware-bad-volid.conf
 create mode 100644 src/test/parse-config-input/efi-firmware-no-ovmf.conf
 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-legacy.conf b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf
new file mode 100644
index 00000000..27d2a59d
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-firmware replaces system OVMF_CODE path in legacy -drive command line
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+machine: pc-i440fx-4.1+pve0
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/custom.fd
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..e7e870a5
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf.cmd
@@ -0,0 +1,27 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -pidfile /var/run/qemu-server/8006.pid \
+  -daemonize \
+  -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+  -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=raw,file=/var/lib/vz/images/100/vm-100-disk-0.raw' \
+  -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 \
+  -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 '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' \
+  -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+  -machine 'type=pc-i440fx-4.1+pve0'
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..fcc8774a
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-not-found.conf
@@ -0,0 +1,6 @@
+# TEST: efi-firmware pointing to a nonexistent file causes die
+# EXPECT_ERROR: efi-firmware volume 'local:efi-firmware/nonexistent.fd' not found at '/var/lib/vz/efi-firmware/nonexistent.fd'
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/nonexistent.fd
diff --git a/src/test/cfg2cmd/efi-custom-firmware-old.conf b/src/test/cfg2cmd/efi-custom-firmware-old.conf
new file mode 100644
index 00000000..5ac13d9c
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-old.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-firmware replaces system OVMF_CODE path in legacy -drive command line (old naming)
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+machine: pc-i440fx-4.1+pve0
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/custom.fd
diff --git a/src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd b/src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd
new file mode 100644
index 00000000..e7e870a5
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd
@@ -0,0 +1,27 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -pidfile /var/run/qemu-server/8006.pid \
+  -daemonize \
+  -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+  -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=raw,file=/var/lib/vz/images/100/vm-100-disk-0.raw' \
+  -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 \
+  -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 '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' \
+  -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+  -machine 'type=pc-i440fx-4.1+pve0'
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..cb0d6a9e
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-sev.conf
@@ -0,0 +1,7 @@
+# TEST: Custom efi-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
+efidisk0: local:100/vm-100-disk-0.raw,efitype=4m,pre-enrolled-keys=1,size=528K
+amd-sev: type=std
+efi-firmware: local:efi-firmware/custom.fd
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..4b3a67dc
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-sev.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' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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..07a80a29
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-snp.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-firmware replaces default SNP firmware in -bios argument
+# HW_CAPABILITIES: amd-turin-9005
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+amd-sev: type=snp
+efi-firmware: local:efi-firmware/custom.fd
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..678d1356
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-snp.conf.cmd
@@ -0,0 +1,29 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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..8219dad3
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-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
+intel-tdx: tdx,attestation=0
+efi-firmware: local:efi-firmware/custom.fd
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..7902aaef
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf.cmd
@@ -0,0 +1,29 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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.conf b/src/test/cfg2cmd/efi-custom-firmware.conf
new file mode 100644
index 00000000..25ca7bc4
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware.conf
@@ -0,0 +1,5 @@
+# TEST: Custom efi-firmware replaces system OVMF_CODE path in blockdev command line
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/custom.fd
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..526d99c3
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware.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' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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-expected/efi-firmware-bad-volid.conf.strict.error b/src/test/parse-config-expected/efi-firmware-bad-volid.conf.strict.error
new file mode 100644
index 00000000..35672ff1
--- /dev/null
+++ b/src/test/parse-config-expected/efi-firmware-bad-volid.conf.strict.error
@@ -0,0 +1 @@
+vm 8006 - efi-firmware: invalid volid format, expected <storeid>:efi-firmware/<name>
diff --git a/src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error b/src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error
new file mode 100644
index 00000000..3a56d271
--- /dev/null
+++ b/src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error
@@ -0,0 +1 @@
+vm 8006 - efi-firmware requires bios=ovmf
diff --git a/src/test/parse-config-input/efi-firmware-bad-volid.conf b/src/test/parse-config-input/efi-firmware-bad-volid.conf
new file mode 100644
index 00000000..c91afb69
--- /dev/null
+++ b/src/test/parse-config-input/efi-firmware-bad-volid.conf
@@ -0,0 +1,13 @@
+bios: ovmf
+boot: order=scsi0
+cores: 2
+efi-firmware: local:iso/custom.fd
+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/efi-firmware-no-ovmf.conf b/src/test/parse-config-input/efi-firmware-no-ovmf.conf
new file mode 100644
index 00000000..3739d8f5
--- /dev/null
+++ b/src/test/parse-config-input/efi-firmware-no-ovmf.conf
@@ -0,0 +1,11 @@
+boot: order=scsi0
+cores: 2
+efi-firmware: local:efi-firmware/custom.fd
+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-0,size=8G
+scsihw: virtio-scsi-pci
+sockets: 1
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..c39570d9
--- /dev/null
+++ b/src/test/parse-config-input/efi-firmware.conf
@@ -0,0 +1,13 @@
+bios: ovmf
+boot: order=scsi0
+cores: 2
+efi-firmware: local:efi-firmware/custom.fd
+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..78a03e71
--- /dev/null
+++ b/src/test/parse-config-input/regular-vm-efifirmware.conf
@@ -0,0 +1,17 @@
+# regular VM with an EFI disk and custom firmware
+bios: ovmf
+boot: order=scsi0;ide2;net0
+cores: 1
+efi-firmware: mydir:efi-firmware/custom.fd
+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 47250c67..a2e95add 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',
@@ -285,6 +286,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..3e31bc30 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 * 14;
 
 sub run_tests {
     my ($strict) = @_;
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH qemu-server 11/13] test: efi-firmware volumes replication
  2026-08-17 11:59 ` Christian Ludwig
                   ` (10 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 1372 bytes --]

Test that efi-firmware volumes are excluded from volume replication.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/test/test_get_replicatable_volumes.pl | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/test/test_get_replicatable_volumes.pl b/src/test/test_get_replicatable_volumes.pl
index 6a3d0338..7ecda208 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,15 @@ $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 = "efi-firmware is shared, not owned by VM";
+$rawconf = <<__EOD__;
+bios: ovmf
+efi-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] 28+ messages in thread

* [PATCH pve-docs 12/13] pvesm: Document efi-firmware content type
  2026-08-17 11:59 ` Christian Ludwig
                   ` (11 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 4319 bytes --]

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              | 4 ++++
 5 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/pve-storage-cephfs.adoc b/pve-storage-cephfs.adoc
index ab8d850..5ae22e3 100644
--- a/pve-storage-cephfs.adoc
+++ b/pve-storage-cephfs.adoc
@@ -128,8 +128,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 6b8f35d..2eda2ff 100644
--- a/pve-storage-cifs.adoc
+++ b/pve-storage-cifs.adoc
@@ -97,8 +97,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..ebbd0bb 100644
--- a/pvesm.adoc
+++ b/pvesm.adoc
@@ -250,6 +250,10 @@ import:::
 
 OVAs and VM disk images that can be imported from this storage
 
+efi-firmware:::
+
+Custom EFI firmware images 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] 28+ messages in thread

* [PATCH pve-docs 13/13] qm: Document efi-firmware VM option
  2026-08-17 11:59 ` Christian Ludwig
                   ` (12 preceding siblings ...)
  (?)
@ 2026-08-17  9:29 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17  9:29 UTC (permalink / raw)
  To: pve-devel, -b, cludwig

[-- Attachment #1: Type: text/plain, Size: 1508 bytes --]

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 qm.adoc | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/qm.adoc b/qm.adoc
index 5b46cdc..141187b 100644
--- a/qm.adoc
+++ b/qm.adoc
@@ -1246,6 +1246,39 @@ 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 `efi-firmware` VM option.
+
+The firmware image must first be uploaded to a storage that has the
+`efi-firmware` content type enabled.
+
+To configure a VM to use a custom firmware image:
+
+----
+# qm set <vmid> -efi-firmware <storage>:efi-firmware/<name>
+----
+
+For example:
+
+----
+# qm set 100 -efi-firmware local:efi-firmware/custom-ovmf-code.fd
+----
+
+NOTE: The `efi-firmware` option requires `bios` to be set to `ovmf`. The
+custom image replaces only the firmware code (pflash0); the EFI vars disk
+(`efidisk0`) is still used as normal for storing UEFI variables.
+
+To remove a custom firmware assignment and revert to the default OVMF image:
+
+----
+# qm set <vmid> -delete efi-firmware
+----
+
 [[qm_tpm]]
 Trusted Platform Module (TPM)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH storage/qemu 0/13]: Custom UEFI firmware in PVE
@ 2026-08-17 11:59 ` Christian Ludwig
  0 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 1126 bytes --]

Hi,

this series brings initial support for custom UEFI firmware to PVE.
This is useful for confidential computing workloads, where VMs may
bring their own firmware and not rely on the hypervisor's. There is
also some other software around that ships it's own VM firmware. The
firmware needs to be compatible to KVM/Qemu, of course. See the design
discussion earlier at [1].

The first part of the series brings a new storage content type for EFI
firmware, which can be set on directory-based storage. There is no
restriction on the actual firmware file's name.

The second part brings new 'efi-firmware' VM config key allows Qemu to
use a file from a storage with that new content type, instead of the
default firmware. To reduce complexity, this only works with bios=ovmf.

This feature is only configurable from the API. The GUI parts were left
out on purpose. I am unsure how much we want/need to expose there in the
first place.

We have tested this with custom UEFI firmware in AMD SEV/SNP
confidential VMs.


 - Christian

[1] https://lore.proxmox.com/pve-devel/10513e3f2c0d94bc938a540b4a0a18749eb5ed96.camel@genua.de/

^ permalink raw reply	[flat|nested] 28+ messages in thread

* [PATCH pve-storage 1/13] Add efi-firmware content type
  2026-08-17 11:59 ` Christian Ludwig
                   ` (13 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 2393 bytes --]

This is meant to hold custom OVMF code files. It is stored in the
efi-firmware/ subdirectory of the storage path. There is no restriction
on the file name. But stick with the safe character set, basically for
safety reasons.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/Storage/Plugin.pm | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 4f69f9b..f6ca140 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -372,7 +372,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
 
@@ -831,6 +831,8 @@ 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}+)$!) {
+        return ('efi-firmware', $1, undef, undef, undef, undef, 'raw');
     }
 
     die "unable to parse directory volume name '$volname'\n";
@@ -844,6 +846,7 @@ my $vtype_subdirs = {
     backup => 'dump',
     snippets => 'snippets',
     import => 'import',
+    'efi-firmware' => 'efi-firmware',
 };
 
 sub get_vtype_subdirs {
@@ -1753,6 +1756,10 @@ 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}+)$!i;
+
+            $info = { volid => "$sid:efi-firmware/$1", format => 'raw' };
         }
 
         $info->{size} = $st->size;
@@ -1789,6 +1796,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] 28+ messages in thread

* [PATCH pve-storage 2/13] Test for efi-firmware content type
  2026-08-17 11:59 ` Christian Ludwig
                   ` (14 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 658 bytes --]

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] 28+ messages in thread

* [PATCH pve-storage 3/13] Allow efi-firmware in file-based storage
  2026-08-17 11:59 ` Christian Ludwig
                   ` (15 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 2569 bytes --]

Enable efi-firmware files to be placed in file-based storage backends.

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 | 2 +-
 src/PVE/Storage/DirPlugin.pm    | 1 +
 src/PVE/Storage/NFSPlugin.pm    | 1 +
 5 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Storage/BTRFSPlugin.pm b/src/PVE/Storage/BTRFSPlugin.pm
index fb47aa0..2f999b2 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..1a4747a 100644
--- a/src/PVE/Storage/CephFSPlugin.pm
+++ b/src/PVE/Storage/CephFSPlugin.pm
@@ -117,7 +117,7 @@ sub type {
 sub plugindata {
     return {
         content =>
-            [{ vztmpl => 1, iso => 1, backup => 1, snippets => 1, import => 1 }, { backup => 1 }],
+            [{ 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] 28+ messages in thread

* [PATCH pve-storage 4/13] Extend storage API endpoints for efi-firmware
  2026-08-17 11:59 ` Christian Ludwig
                   ` (16 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 3790 bytes --]

Extend the storage upload and download-url API endpoints for the
efi-firmware content type. Provide an error message if the file name
does not match the safe character set.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/API2/Storage/Status.pm | 14 ++++++++++++--
 src/PVE/Storage.pm             | 13 +++++++++++--
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/src/PVE/API2/Storage/Status.pm b/src/PVE/API2/Storage/Status.pm
index 741d514..a6fc317 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,11 @@ __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}+$!) {
+                raise_param_exc({ filename => "invalid file name" });
+            }
+            $path = PVE::Storage::get_efi_firmware_dir($cfg, $storage);
         } else {
             raise_param_exc({ content => "upload content type '$content' not allowed" });
         }
@@ -770,7 +775,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 +864,11 @@ __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}+$!) {
+                raise_param_exc({ filename => "invalid file name" });
+            }
+            $path = PVE::Storage::get_efi_firmware_dir($cfg, $storage);
         } else {
             raise_param_exc({ content => "upload content-type '$content' is not allowed" });
         }
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 64ea9da..1083b1d 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -555,6 +555,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) = @_;
 
@@ -629,7 +638,7 @@ 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,
@@ -1297,7 +1306,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];
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH pve-storage 5/13] Volume access check test for efi-firmware
  2026-08-17 11:59 ` Christian Ludwig
                   ` (17 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 1035 bytes --]

Add a test case for an efi-firmware volume to check for it's type.

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] 28+ messages in thread

* [PATCH pve-storage 6/13] efi-firmware storage path to volume conversion test
  2026-08-17 11:59 ` Christian Ludwig
                   ` (18 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 2798 bytes --]

Add test cases to check that storage paths under the efi-firmware/
subdirectory are correctly listed as efi-firmware content type volumes,
regardless of file extension.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/test/list_volumes_test.pm | 51 ++++++++++++++++++++++++++++++++++-
 1 file changed, 50 insertions(+), 1 deletion(-)

diff --git a/src/test/list_volumes_test.pm b/src/test/list_volumes_test.pm
index 0876902..a1e98d1 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,54 @@ 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, all efi-firmware files listed regardless of extension',
+        vmid => undef,
+        files => [
+            "$storage_dir/efi-firmware/custom.fd",
+            "$storage_dir/efi-firmware/custom.iso",
+        ],
+        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/custom.iso',
+            },
+        ],
+    },
 );
 
 # provide static vmlist for tests
@@ -520,7 +569,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] 28+ messages in thread

* [PATCH qemu-server 07/13] Add efi-firmware key to VM config schema
  2026-08-17 11:59 ` Christian Ludwig
                   ` (19 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 3354 bytes --]

Add a new config key 'efi-firmware' to the VM config schema. That key
points to a firmware content type file.

It is restricted to OVMF bios settings. A custom efi-firmware image does
not make sense for VMs with legacy BIOS.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/QemuConfig.pm |  7 +++++++
 src/PVE/QemuServer.pm | 30 ++++++++++++++++++++++++++++--
 2 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/src/PVE/QemuConfig.pm b/src/PVE/QemuConfig.pm
index 26f0fda2..d25f2bbb 100644
--- a/src/PVE/QemuConfig.pm
+++ b/src/PVE/QemuConfig.pm
@@ -112,6 +112,13 @@ sub parse_volume {
             die $err;
         }
         $volume = { 'file' => $volume_string };
+    } elsif ($key eq 'efi-firmware') {
+        eval { PVE::JSONSchema::check_format('pve-volume-id', $volume_string) };
+        if (my $err = $@) {
+            return if $noerr;
+            die $err;
+        }
+        $volume = { 'file' => $volume_string };
     } else {
         $volume = PVE::QemuServer::Drive::parse_drive($key, $volume_string);
     }
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 2f43faa7..b95fcb8c 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -663,6 +663,14 @@ EODESCR
         description => "Select BIOS implementation.",
         default => 'seabios',
     },
+    'efi-firmware' => {
+        optional => 1,
+        type => 'string',
+        format => 'pve-volume-id',
+        description => "Custom EFI firmware code image (pflash0). Must be a volid "
+            . "referencing a 'efi-firmware' content type volume (e.g. "
+            . "'local:efi-firmware/custom.fd'). Requires bios=ovmf.",
+    },
     vmgenid => {
         type => 'string',
         pattern => '(?:[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}|[01])',
@@ -2078,6 +2086,21 @@ sub parse_vm_config {
 
     $res->{pending} = {} if !defined($res->{pending});
 
+    # config sanity checks
+    if ($res->{'efi-firmware'}) {
+        if (!$res->{bios} || $res->{bios} ne 'ovmf') {
+            $handle_error->("vm $vmid - efi-firmware requires bios=ovmf\n");
+        } else {
+            my ($sid, $volname) = PVE::Storage::parse_volume_id($res->{'efi-firmware'}, 1);
+            if (!$sid || $volname !~ m!^efi-firmware/[^/]+$!) {
+                $handle_error->(
+                    "vm $vmid - efi-firmware: invalid volid format,"
+                    . " expected <storeid>:efi-firmware/<name>\n"
+                );
+            }
+        }
+    }
+
     return $res;
 }
 
@@ -4568,11 +4591,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 'efi-firmware';
+
         $volhash->{$volid}->{drivename} = $key if is_valid_drivename($key);
     };
 
     my $include_opts = {
-        extra_keys => ['vmstate'],
+        extra_keys => ['vmstate', 'efi-firmware'],
         include_unused => 1,
     };
 
@@ -6123,7 +6149,7 @@ sub get_current_vm_volumes {
 
     PVE::QemuConfig->foreach_volume_full(
         $conf,
-        { extra_keys => ['vmstate'] },
+        { extra_keys => ['vmstate', 'efi-firmware'] },
         sub {
             my ($ds, $drive) = @_;
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH qemu-server 08/13] Add efi-firmware support to the API
  2026-08-17 11:59 ` Christian Ludwig
                   ` (20 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 1474 bytes --]

Add an efi-firmware key to the POST/PUT {vmid}/config API endpoint.
It needs VM.Config.HWType permission. And deleting efi-firmware from the
config does not trigger volume cleanup, firmware images are shared.

Note that changing the bios type requires VM.Config.Options permissions.
That should probably move to the VM.Config.HWType permission, too.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/API2/Qemu.pm | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 3320313c..6fc7c1f7 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -818,6 +818,7 @@ my $hwtypeoptions = {
     'watchdog' => 1,
     'audio0' => 1,
     'rng0' => 1,
+    'efi-firmware' => 1,
 };
 
 my $generaloptions = {
@@ -2523,6 +2524,11 @@ my $update_vm_api = sub {
                     print "automatic pinning of machine version failed - $@" if $@;
                 }
                 $conf->{pending}->{$opt} = $param->{$opt};
+            } elsif ($opt eq 'efi-firmware') {
+                PVE::Storage::check_volume_access(
+                    $rpcenv, $authuser, $storecfg, $vmid, $param->{$opt},
+                );
+                $conf->{pending}->{$opt} = $param->{$opt};
             } elsif ($opt eq 'cipassword') {
                 if (!PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
                     # Same logic as in cloud-init (but with the regex fixed...)
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH qemu-server 09/13] Generate efi-firmware Qemu command line
  2026-08-17 11:59 ` Christian Ludwig
                   ` (21 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 2354 bytes --]

If efi-firmware is set, use the correct EFI code file to create the Qemu
command line.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/PVE/QemuServer/OVMF.pm | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/PVE/QemuServer/OVMF.pm b/src/PVE/QemuServer/OVMF.pm
index 67665c7c..fe301353 100644
--- a/src/PVE/QemuServer/OVMF.pm
+++ b/src/PVE/QemuServer/OVMF.pm
@@ -123,6 +123,9 @@ my sub print_ovmf_drive_commandlines {
         if $cvm_type && $cvm_type eq 'tdx';
 
     my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d, $q35, $cvm_type);
+    if ($conf->{'efi-firmware'}) {
+        $ovmf_code = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+    }
     my $ovmf_vars_size = file_get_size($ovmf_vars);
 
     my $var_drive_str = "if=pflash,unit=1,id=drive-efidisk0";
@@ -219,6 +222,9 @@ my sub generate_ovmf_blockdev {
         if $cvm_type && $cvm_type eq 'snp';
 
     my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $drive, $q35, $cvm_type);
+    if ($conf->{'efi-firmware'}) {
+        $ovmf_code = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+    }
 
     my $ovmf_code_blockdev = {
         driver => 'raw',
@@ -268,6 +274,12 @@ sub print_ovmf_commandline {
 
     my $cvm_type = $hw_info->{'cvm-type'};
 
+    if ($conf->{'efi-firmware'}) {
+        my $fw_path = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+        die "efi-firmware volume '$conf->{'efi-firmware'}' not found at '$fw_path'\n"
+            if !file_exists($fw_path);
+    }
+
     my $cmd = [];
     my $machine_flags = [];
 
@@ -277,7 +289,13 @@ 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 $bios_path;
+        if ($conf->{'efi-firmware'}) {
+            $bios_path = PVE::Storage::path($storecfg, $conf->{'efi-firmware'});
+        } else {
+            ($bios_path) = get_ovmf_files($hw_info->{arch}, undef, undef, $cvm_type);
+        }
+        push $cmd->@*, '-bios', $bios_path;
     } 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] 28+ messages in thread

* [PATCH qemu-server 10/13] test: efi-firmware key in VM config
  2026-08-17 11:59 ` Christian Ludwig
                   ` (22 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 22577 bytes --]

Add test cases for different efi-firmware settings in VM config.
Also enhance the PVE::QemuServer::OVMF test mock to trigger to handle
nonexistent files.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 .../cfg2cmd/efi-custom-firmware-legacy.conf   |  6 ++++
 .../efi-custom-firmware-legacy.conf.cmd       | 27 ++++++++++++++++
 .../efi-custom-firmware-not-found.conf        |  6 ++++
 src/test/cfg2cmd/efi-custom-firmware-old.conf |  6 ++++
 .../cfg2cmd/efi-custom-firmware-old.conf.cmd  | 27 ++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware-sev.conf |  7 +++++
 .../cfg2cmd/efi-custom-firmware-sev.conf.cmd  | 31 +++++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware-snp.conf |  6 ++++
 .../cfg2cmd/efi-custom-firmware-snp.conf.cmd  | 29 +++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware-tdx.conf |  6 ++++
 .../cfg2cmd/efi-custom-firmware-tdx.conf.cmd  | 29 +++++++++++++++++
 src/test/cfg2cmd/efi-custom-firmware.conf     |  5 +++
 src/test/cfg2cmd/efi-custom-firmware.conf.cmd | 30 ++++++++++++++++++
 .../efi-firmware-bad-volid.conf.strict.error  |  1 +
 .../efi-firmware-no-ovmf.conf.strict.error    |  1 +
 .../efi-firmware-bad-volid.conf               | 13 ++++++++
 .../efi-firmware-no-ovmf.conf                 | 11 +++++++
 src/test/parse-config-input/efi-firmware.conf | 13 ++++++++
 .../regular-vm-efifirmware.conf               | 17 ++++++++++
 src/test/run_config2command_tests.pl          |  2 ++
 src/test/run_parse_config_tests.pl            |  2 +-
 21 files changed, 274 insertions(+), 1 deletion(-)
 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-not-found.conf
 create mode 100644 src/test/cfg2cmd/efi-custom-firmware-old.conf
 create mode 100644 src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd
 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.conf
 create mode 100644 src/test/cfg2cmd/efi-custom-firmware.conf.cmd
 create mode 100644 src/test/parse-config-expected/efi-firmware-bad-volid.conf.strict.error
 create mode 100644 src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error
 create mode 100644 src/test/parse-config-input/efi-firmware-bad-volid.conf
 create mode 100644 src/test/parse-config-input/efi-firmware-no-ovmf.conf
 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-legacy.conf b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf
new file mode 100644
index 00000000..27d2a59d
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-firmware replaces system OVMF_CODE path in legacy -drive command line
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+machine: pc-i440fx-4.1+pve0
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/custom.fd
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..e7e870a5
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-legacy.conf.cmd
@@ -0,0 +1,27 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -pidfile /var/run/qemu-server/8006.pid \
+  -daemonize \
+  -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+  -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=raw,file=/var/lib/vz/images/100/vm-100-disk-0.raw' \
+  -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 \
+  -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 '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' \
+  -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+  -machine 'type=pc-i440fx-4.1+pve0'
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..fcc8774a
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-not-found.conf
@@ -0,0 +1,6 @@
+# TEST: efi-firmware pointing to a nonexistent file causes die
+# EXPECT_ERROR: efi-firmware volume 'local:efi-firmware/nonexistent.fd' not found at '/var/lib/vz/efi-firmware/nonexistent.fd'
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/nonexistent.fd
diff --git a/src/test/cfg2cmd/efi-custom-firmware-old.conf b/src/test/cfg2cmd/efi-custom-firmware-old.conf
new file mode 100644
index 00000000..5ac13d9c
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-old.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-firmware replaces system OVMF_CODE path in legacy -drive command line (old naming)
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+machine: pc-i440fx-4.1+pve0
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/custom.fd
diff --git a/src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd b/src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd
new file mode 100644
index 00000000..e7e870a5
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-old.conf.cmd
@@ -0,0 +1,27 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -pidfile /var/run/qemu-server/8006.pid \
+  -daemonize \
+  -smbios 'type=1,uuid=7b10d7af-b932-4c66-b2c3-3996152ec465' \
+  -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=raw,file=/var/lib/vz/images/100/vm-100-disk-0.raw' \
+  -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 \
+  -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 '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' \
+  -iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff' \
+  -machine 'type=pc-i440fx-4.1+pve0'
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..cb0d6a9e
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-sev.conf
@@ -0,0 +1,7 @@
+# TEST: Custom efi-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
+efidisk0: local:100/vm-100-disk-0.raw,efitype=4m,pre-enrolled-keys=1,size=528K
+amd-sev: type=std
+efi-firmware: local:efi-firmware/custom.fd
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..4b3a67dc
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-sev.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' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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..07a80a29
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-snp.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-firmware replaces default SNP firmware in -bios argument
+# HW_CAPABILITIES: amd-turin-9005
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+amd-sev: type=snp
+efi-firmware: local:efi-firmware/custom.fd
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..678d1356
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-snp.conf.cmd
@@ -0,0 +1,29 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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..8219dad3
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf
@@ -0,0 +1,6 @@
+# TEST: Custom efi-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
+intel-tdx: tdx,attestation=0
+efi-firmware: local:efi-firmware/custom.fd
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..7902aaef
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware-tdx.conf.cmd
@@ -0,0 +1,29 @@
+/usr/bin/kvm \
+  -id 8006 \
+  -name vm8006 \
+  -no-shutdown \
+  -chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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.conf b/src/test/cfg2cmd/efi-custom-firmware.conf
new file mode 100644
index 00000000..25ca7bc4
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware.conf
@@ -0,0 +1,5 @@
+# TEST: Custom efi-firmware replaces system OVMF_CODE path in blockdev command line
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+bios: ovmf
+efidisk0: local:100/vm-100-disk-0.raw
+efi-firmware: local:efi-firmware/custom.fd
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..526d99c3
--- /dev/null
+++ b/src/test/cfg2cmd/efi-custom-firmware.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' \
+  -mon 'chardev=qmp,mode=control' \
+  -chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000' \
+  -mon 'chardev=qmp-event,mode=control' \
+  -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 '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-expected/efi-firmware-bad-volid.conf.strict.error b/src/test/parse-config-expected/efi-firmware-bad-volid.conf.strict.error
new file mode 100644
index 00000000..35672ff1
--- /dev/null
+++ b/src/test/parse-config-expected/efi-firmware-bad-volid.conf.strict.error
@@ -0,0 +1 @@
+vm 8006 - efi-firmware: invalid volid format, expected <storeid>:efi-firmware/<name>
diff --git a/src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error b/src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error
new file mode 100644
index 00000000..3a56d271
--- /dev/null
+++ b/src/test/parse-config-expected/efi-firmware-no-ovmf.conf.strict.error
@@ -0,0 +1 @@
+vm 8006 - efi-firmware requires bios=ovmf
diff --git a/src/test/parse-config-input/efi-firmware-bad-volid.conf b/src/test/parse-config-input/efi-firmware-bad-volid.conf
new file mode 100644
index 00000000..c91afb69
--- /dev/null
+++ b/src/test/parse-config-input/efi-firmware-bad-volid.conf
@@ -0,0 +1,13 @@
+bios: ovmf
+boot: order=scsi0
+cores: 2
+efi-firmware: local:iso/custom.fd
+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/efi-firmware-no-ovmf.conf b/src/test/parse-config-input/efi-firmware-no-ovmf.conf
new file mode 100644
index 00000000..3739d8f5
--- /dev/null
+++ b/src/test/parse-config-input/efi-firmware-no-ovmf.conf
@@ -0,0 +1,11 @@
+boot: order=scsi0
+cores: 2
+efi-firmware: local:efi-firmware/custom.fd
+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-0,size=8G
+scsihw: virtio-scsi-pci
+sockets: 1
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..c39570d9
--- /dev/null
+++ b/src/test/parse-config-input/efi-firmware.conf
@@ -0,0 +1,13 @@
+bios: ovmf
+boot: order=scsi0
+cores: 2
+efi-firmware: local:efi-firmware/custom.fd
+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..78a03e71
--- /dev/null
+++ b/src/test/parse-config-input/regular-vm-efifirmware.conf
@@ -0,0 +1,17 @@
+# regular VM with an EFI disk and custom firmware
+bios: ovmf
+boot: order=scsi0;ide2;net0
+cores: 1
+efi-firmware: mydir:efi-firmware/custom.fd
+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 47250c67..a2e95add 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',
@@ -285,6 +286,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..3e31bc30 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 * 14;
 
 sub run_tests {
     my ($strict) = @_;
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH qemu-server 11/13] test: efi-firmware volumes replication
  2026-08-17 11:59 ` Christian Ludwig
                   ` (23 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 1372 bytes --]

Test that efi-firmware volumes are excluded from volume replication.

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 src/test/test_get_replicatable_volumes.pl | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/test/test_get_replicatable_volumes.pl b/src/test/test_get_replicatable_volumes.pl
index 6a3d0338..7ecda208 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,15 @@ $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 = "efi-firmware is shared, not owned by VM";
+$rawconf = <<__EOD__;
+bios: ovmf
+efi-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] 28+ messages in thread

* [PATCH pve-docs 12/13] pvesm: Document efi-firmware content type
  2026-08-17 11:59 ` Christian Ludwig
                   ` (24 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 4319 bytes --]

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              | 4 ++++
 5 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/pve-storage-cephfs.adoc b/pve-storage-cephfs.adoc
index ab8d850..5ae22e3 100644
--- a/pve-storage-cephfs.adoc
+++ b/pve-storage-cephfs.adoc
@@ -128,8 +128,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 6b8f35d..2eda2ff 100644
--- a/pve-storage-cifs.adoc
+++ b/pve-storage-cifs.adoc
@@ -97,8 +97,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..ebbd0bb 100644
--- a/pvesm.adoc
+++ b/pvesm.adoc
@@ -250,6 +250,10 @@ import:::
 
 OVAs and VM disk images that can be imported from this storage
 
+efi-firmware:::
+
+Custom EFI firmware images 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] 28+ messages in thread

* [PATCH pve-docs 13/13] qm: Document efi-firmware VM option
  2026-08-17 11:59 ` Christian Ludwig
                   ` (25 preceding siblings ...)
  (?)
@ 2026-08-17 11:59 ` Christian Ludwig
  -1 siblings, 0 replies; 28+ messages in thread
From: Christian Ludwig @ 2026-08-17 11:59 UTC (permalink / raw)
  To: pve-devel

[-- Attachment #1: Type: text/plain, Size: 1508 bytes --]

Signed-off-by: Christian Ludwig <christian_ludwig@genua.de>
---
 qm.adoc | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/qm.adoc b/qm.adoc
index 5b46cdc..141187b 100644
--- a/qm.adoc
+++ b/qm.adoc
@@ -1246,6 +1246,39 @@ 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 `efi-firmware` VM option.
+
+The firmware image must first be uploaded to a storage that has the
+`efi-firmware` content type enabled.
+
+To configure a VM to use a custom firmware image:
+
+----
+# qm set <vmid> -efi-firmware <storage>:efi-firmware/<name>
+----
+
+For example:
+
+----
+# qm set 100 -efi-firmware local:efi-firmware/custom-ovmf-code.fd
+----
+
+NOTE: The `efi-firmware` option requires `bios` to be set to `ovmf`. The
+custom image replaces only the firmware code (pflash0); the EFI vars disk
+(`efidisk0`) is still used as normal for storing UEFI variables.
+
+To remove a custom firmware assignment and revert to the default OVMF image:
+
+----
+# qm set <vmid> -delete efi-firmware
+----
+
 [[qm_tpm]]
 Trusted Platform Module (TPM)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 28+ messages in thread

end of thread, other threads:[~2026-08-17 12:01 UTC | newest]

Thread overview: 28+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-17  9:29 [PATCH storage/qemu 0/13]: Custom UEFI firmware in PVE Christian Ludwig
2026-08-17 11:59 ` Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-storage 1/13] Add efi-firmware content type Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-storage 2/13] Test for " Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-storage 3/13] Allow efi-firmware in file-based storage Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-storage 4/13] Extend storage API endpoints for efi-firmware Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-storage 5/13] Volume access check test " Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-storage 6/13] efi-firmware storage path to volume conversion test Christian Ludwig
2026-08-17  9:29 ` [PATCH qemu-server 07/13] Add efi-firmware key to VM config schema Christian Ludwig
2026-08-17  9:29 ` [PATCH qemu-server 08/13] Add efi-firmware support to the API Christian Ludwig
2026-08-17  9:29 ` [PATCH qemu-server 09/13] Generate efi-firmware Qemu command line Christian Ludwig
2026-08-17  9:29 ` [PATCH qemu-server 10/13] test: efi-firmware key in VM config Christian Ludwig
2026-08-17  9:29 ` [PATCH qemu-server 11/13] test: efi-firmware volumes replication Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-docs 12/13] pvesm: Document efi-firmware content type Christian Ludwig
2026-08-17  9:29 ` [PATCH pve-docs 13/13] qm: Document efi-firmware VM option Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-storage 1/13] Add efi-firmware content type Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-storage 2/13] Test for " Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-storage 3/13] Allow efi-firmware in file-based storage Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-storage 4/13] Extend storage API endpoints for efi-firmware Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-storage 5/13] Volume access check test " Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-storage 6/13] efi-firmware storage path to volume conversion test Christian Ludwig
2026-08-17 11:59 ` [PATCH qemu-server 07/13] Add efi-firmware key to VM config schema Christian Ludwig
2026-08-17 11:59 ` [PATCH qemu-server 08/13] Add efi-firmware support to the API Christian Ludwig
2026-08-17 11:59 ` [PATCH qemu-server 09/13] Generate efi-firmware Qemu command line Christian Ludwig
2026-08-17 11:59 ` [PATCH qemu-server 10/13] test: efi-firmware key in VM config Christian Ludwig
2026-08-17 11:59 ` [PATCH qemu-server 11/13] test: efi-firmware volumes replication Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-docs 12/13] pvesm: Document efi-firmware content type Christian Ludwig
2026-08-17 11:59 ` [PATCH pve-docs 13/13] qm: Document efi-firmware VM option Christian Ludwig

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal