[pve-devel] [PATCH qemu-server v2 1/2] QEMU AMD SEV enable

Fiona Ebner f.ebner at proxmox.com
Mon Nov 14 14:06:12 CET 2022


Am 11.11.22 um 15:27 schrieb Markus Frank:
> This Patch is for enabling AMD SEV (Secure Encrypted
> Virtualization) support in QEMU and enabling future
> memory encryption technologies like INTEL MKTME
> (Multi-key Total Memory Encryption) and SEV-SNP.
> 
> Config-Example:
> memory_encryption: type=sev,cbitpos=47,policy=0x0001,reduced-phys-bits=1
> 
> reduced-phys-bios, cbitpos and policy correspond to the varibles with the
> same name in qemu.
> 
> reduced-phys-bios and cbitpos are system specific and can be read out
> with QMP. If not set by the user, a dummy-vm gets started to read QMP
> for these variables out and save them to config. Afterwards the dummy-vm gets
> stopped.

Why even allow the user to set them if they are system-specific values?
Or are there multiple possible values on some systems? If not, it should
be a node-specific configuration, rather than a VM-specific one. That
would also only require starting the dummy VM once per node, or we could
require the user to set the values in some node config (of course
mentioning how in the docs :))

> 
> policy can be calculated with the links in comments & description.
> To test SEV-ES (CPU register encryption) the policy should be set
> somewhere between 0x4 and 0x7 or 0xC and 0xF, etc.
> (Bit-2 has to be set 1 (LSB 0 bit numbering))
> 
> SEV needs edk2-OVMF to work.
> 
> Signed-off-by: Markus Frank <m.frank at proxmox.com>
> ---
>  PVE/QemuServer.pm | 133 ++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 133 insertions(+)
> 
> diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm
> index 513a248..2ea8abd 100644
> --- a/PVE/QemuServer.pm
> +++ b/PVE/QemuServer.pm
> @@ -175,6 +175,58 @@ my $agent_fmt = {
>      },
>  };
>  
> +my $memory_encryption_fmt = {
> +    type => {
> +	type => 'string',
> +	default_key => 1,
> +	description => "Memory Encryption Type:"

Nit: I'd rather have the description be a sentence or two, what it's all
about and add a verbose_description to describe the individual variants.

> +	    ." for AMD SEV -> 'memory_encryption: type=sev';"
> +	    ." for AMD SEV-ES -> use 'sev' and change policy to between 0x4 and 0x7;"
> +	    ." (Bit-2 has to be set 1 (LSB 0 bit numbering))"

Nit: better to use 0x0004 and 0x0007, because 0x4 and 0x7 are not valid
values for 'policy' below.

> +	    #. "for AMD SEV-SNP -> 'memory_encryption: type=sev-snp'"
> +	    ." (sev requires edk2-ovmf & sev kernel support by guest operating system &"
> +	    ." on host: add kernel-parameters 'mem_encrypt=on kvm_amd.sev=1')"
> +	    ." see https://github.com/AMDESE/AMDSEV &"
> +	    ." https://documentation.suse.com/sles/15-SP1/html/SLES-amd-sev/index.html",
> +	format_description => "qemu-memory-encryption-type",
> +	# TODO enable sev-snp option when feature can be tested on 3rd-gen EPYC
> +	# https://www.phoronix.com/news/AMD-SEV-SNP-Arrives-Linux-5.19
> +	# enum => ['sev','sev-snp','mktme'],

Nit: I feel like these comments don't really belong in the patch. Maybe
just add a single high-level TODO comment? The rest should be done by
the patch actually adding sev-snp ;)

Also, the many links might be better left to the documentation patch.

Is the rest of the format even compatible with Intel's MKTME? I.e.
does/will that also have reduced-phys-bits, 4 policy bits and cbitpos?
If there is some overlap or if we expect to be easily able to translate
certain settings, we can still keep a general memory_encryption_fmt, but
otherwise, it might be better to have completely distinct formats for
Intel and AMD?

> +	enum => ['sev'],
> +	maxLength => 10,
> +    },
> +    'reduced-phys-bits' => {
> +	description => "Number of bits the physical address space is reduced by. System dependent",
> +	type => 'integer',
> +	default => 1,

The default is system-dependent and automatically figured out by the
dummy VM. Also the kvm man pages states

> On EPYC, the value should be 5.

so why 1?

> +	optional => 1,
> +	minimum => 0,
> +	maximum => 100,
> +    },
> +    cbitpos => {
> +	description => "C-bit: marks if a memory page is protected. System dependent",
> +	type => 'integer',
> +	default => 47,

Same here with regards to auto-magic.

> +	optional => 1,
> +	minimum => 0,
> +	maximum => 100,
> +    },
> +    policy => {
> +	description => "SEV Guest Policy"
> +	    . "see Capter 3:"

Nit: typo

> +	    . "https://www.amd.com/system/files/TechDocs/55766_SEV-KM_API_Specification.pdf"
> +	    . "& https://www.qemu.org/docs/master/system/i386/amd-memory-encryption.html",
> +

Could have a verbose_description for each bit rather than the links. Or
should we go one step further and add explicit flags for the
relevant-for-us bits instead? Would be more user-friendly, but the
$memory_encryption_fmt would be AMD-specific of course.

> +	format_description => "qemu-memory-encryption-policy",
> +	type => 'string',
> +	default => '0x0000',
> +	pattern => '0[xX][0-9a-fA-F]{1,4}',
> +	optional => 1,
> +	maxLength => 6,
> +    },
> +};
> +PVE::JSONSchema::register_format('pve-qemu-memory-encryption-fmt', $memory_encryption_fmt);
> +
>  my $vga_fmt = {
>      type => {
>  	description => "Select the VGA type.",
> @@ -349,6 +401,12 @@ my $confdesc = {
>  	minimum => 16,
>  	default => 512,
>      },
> +    memory_encryption => {
> +	description => "Memory Encryption",
> +	optional => 1,
> +	format => 'pve-qemu-memory-encryption-fmt',
> +	type => 'string',
> +    },
>      balloon => {
>  	optional => 1,
>  	type => 'integer',
> @@ -2113,6 +2171,17 @@ sub parse_guest_agent {
>      return $res;
>  }
>  
> +sub parse_memory_encryption {
> +    my ($value) = @_;
> +
> +    return if !$value;
> +
> +    my $res = eval { parse_property_string($memory_encryption_fmt, $value) };
> +    warn $@ if $@;
> +    return $res;
> +}

Why not fail if parsing fails?

> +
> +
>  sub get_qga_key {
>      my ($conf, $key) = @_;
>      return undef if !defined($conf->{agent});
> @@ -4085,6 +4154,70 @@ sub config_to_command {
>      }
>      push @$machineFlags, "type=${machine_type_min}";
>  
> +    # Memory Encryption

Nit: comment contains no additional information.

> +    my $memory_encryption = parse_memory_encryption($conf->{'memory_encryption'});
> +
> +    # Die if bios is not ovmf

Nit: Same here.

> +    if (
> +	$memory_encryption->{'type'}
> +	&& $memory_encryption->{type} eq 'sev'
> +	&& $conf->{bios} ne 'ovmf'

I think $conf->{bios} could be undef here? That would cause an ugly Perl
warning. At least other place check for
    !defined($conf->{bios}) || $conf->{bios} ne 'ovmf'

> +    ) {
> +	die "SEV needs ovmf\n";

Nit: maybe a bit too concise of a message, could mention the settings at
least

> +    }
> +

All of the below should rather live in helper functions, especially the
dummy VM stuff. config_to_command is already much too bloated.

> +    # AMD SEV
> +    if ($memory_encryption->{'type'} && $memory_encryption->{type} =~ /(sev|sev-snp)/) {

Nit: I'd rather have explicit equality testing for the type (or at least
add a leading ^ to the regex). The sev-snp type does not exist yet and
should be added by a later patch.

> +	# Get reduced-phys-bits & cbitpos from QMP, if not set
> +	if (
> +	    !$memory_encryption->{'reduced-phys-bits'}
> +	    || !$memory_encryption->{cbitpos}
> +	) {
> +	    my $fakevmid = -1;
> +	    my $qemu_cmd = get_command_for_arch($arch);
> +	    my $pidfile = PVE::QemuServer::Helpers::pidfile_name($fakevmid);
> +	    my $default_machine = $default_machines->{$arch};
> +	    my $cmd = [
> +		$qemu_cmd,
> +		'-machine', $default_machine,
> +		'-display', 'none',
> +		'-chardev', "socket,id=qmp,path=/var/run/qemu-server/$fakevmid.qmp,server=on,wait=off",
> +		'-mon', 'chardev=qmp,mode=control',
> +		'-pidfile', $pidfile,
> +		'-S', '-daemonize'
> +	    ];

Instead of daemonizing, pidfile etc. we could also use --qmp stdio and
pass the commands via stdin like:
{"execute": "qmp_capabilities"}
{"execute": "query-sev-capabilities"}
{"execute": "quit"}
which might be a bit more straight-forward. But maybe we prefer re-using
the existing infrastructure with the fake ID, not sure?

> +	    my $rc = run_command($cmd, noerr => 1, quiet => 0);
> +	    die "QEMU flag querying VM exited with code " . $rc . "\n" if $rc;
> +	    my $res = mon_cmd($fakevmid, 'query-sev-capabilities');
> +	    $memory_encryption->{'reduced-phys-bits'} = $res->{'reduced-phys-bits'};
> +	    $memory_encryption->{cbitpos} = $res->{cbitpos};
> +	    $conf->{'memory_encryption'} = PVE::JSONSchema::print_property_string(
> +		$memory_encryption,
> +		$memory_encryption_fmt
> +	    );
> +	    PVE::QemuConfig->write_config($vmid, $conf);

config_to_command should not write the config. Hope that those settings
are truly node-specific and not VM-specific :)

> +	    vm_stop(undef, $fakevmid, 1, 1, 10, 0, 1);
> +	}
> +	# qemu-Example: -object 'sev-guest,id=sev0,cbitpos=47,reduced-phys-bits=1';
> +	# see https://www.qemu.org/docs/master/system/i386/amd-memory-encryption.html
> +	my $memobjcmd = "";
> +	if ($memory_encryption->{type} eq 'sev-snp') {
> +	    # future feature: cannot be reached
> +	    $memobjcmd = 'sev-snp-guest';

Nit: should be added by a later patch

> +	} else {
> +	    $memobjcmd = 'sev-guest';
> +	}
> +	$memobjcmd .= ',id=sev0,cbitpos='.$memory_encryption->{cbitpos}
> +	    .',reduced-phys-bits='.$memory_encryption->{'reduced-phys-bits'};
> +	if ($memory_encryption->{type} eq 'sev' && $memory_encryption->{policy}) {
> +	    $memobjcmd .= ',policy='.$memory_encryption->{policy}
> +	}
> +	push @$devices, '-object' , $memobjcmd;
> +	# old qemu-Example: -machine 'type=q35+pve0,memory-encryption=sev0'
> +	# https://fossies.org/linux/qemu/docs/system/confidential-guest-support.rst

Nit: I mean the QEMU docs also mention this so no need for that link and
why the "old" example?

> +	push @$machineFlags, 'confidential-guest-support=sev0';
> +    }
> +
>      push @$cmd, @$devices;
>      push @$cmd, '-rtc', join(',', @$rtcFlags) if scalar(@$rtcFlags);
>      push @$cmd, '-machine', join(',', @$machineFlags) if scalar(@$machineFlags);





More information about the pve-devel mailing list