vmspawn: initial support for SEV-SNP guests (#42193)
This commit is contained in:
@@ -385,6 +385,30 @@
|
||||
<xi:include href="version-info.xml" xpointer="v255"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--coco=</option></term>
|
||||
|
||||
<listitem><para>Caveat: This feature is experimental, and is likely to be changed (or removed in
|
||||
its current form) in a future version of systemd.</para>
|
||||
|
||||
<para>Configures whether to run the guest as a confidential VM. Takes one of
|
||||
<literal>no</literal> or <literal>sev-snp</literal>. Defaults to <literal>no</literal>.</para>
|
||||
|
||||
<para><literal>sev-snp</literal> enables AMD SEV-SNP. This requires KVM on an x86_64 host with
|
||||
SNP-capable hardware and firmware. <option>--firmware=</option> must point to a raw SNP-built
|
||||
OVMF <filename>.fd</filename> image; the standard pflash + NVRAM split is not supported under
|
||||
SNP, so the firmware is loaded via QEMU's <option>-bios</option> and Secure Boot is
|
||||
unavailable. SMBIOS credentials passed via <option>--set-credential=</option> or
|
||||
<option>--load-credential=</option> are rejected because they are outside the SNP launch
|
||||
measurement. Direct kernel boot via <option>--linux=</option> is required so that the
|
||||
kernel, initrd and command line are hashed into the launch measurement
|
||||
(<literal>kernel-hashes=on</literal>); booting the kernel off the disk image via the
|
||||
firmware would leave it outside the measurement. A vTPM, if attached via
|
||||
<option>--tpm=</option>, must be treated as untrusted by the guest.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--grow-image=<replaceable>BYTES</replaceable></option></term>
|
||||
<term><option>-G <replaceable>BYTES</replaceable></option></term>
|
||||
|
||||
@@ -36,3 +36,10 @@ static const char *const firmware_table[_FIRMWARE_MAX] = {
|
||||
};
|
||||
|
||||
DEFINE_STRING_TABLE_LOOKUP(firmware, Firmware);
|
||||
|
||||
static const char *const confidential_computing_table[_COCO_MAX] = {
|
||||
[COCO_NO] = "no",
|
||||
[COCO_AMD_SEV_SNP] = "sev-snp",
|
||||
};
|
||||
|
||||
DEFINE_STRING_TABLE_LOOKUP(confidential_computing, ConfidentialComputing);
|
||||
|
||||
@@ -42,6 +42,13 @@ typedef enum Firmware {
|
||||
_FIRMWARE_INVALID = -EINVAL,
|
||||
} Firmware;
|
||||
|
||||
typedef enum ConfidentialComputing {
|
||||
COCO_NO,
|
||||
COCO_AMD_SEV_SNP,
|
||||
_COCO_MAX,
|
||||
_COCO_INVALID = -EINVAL,
|
||||
} ConfidentialComputing;
|
||||
|
||||
typedef enum SettingsMask {
|
||||
SETTING_START_MODE = UINT64_C(1) << 0,
|
||||
SETTING_MACHINE_ID = UINT64_C(1) << 6,
|
||||
@@ -55,3 +62,4 @@ typedef enum SettingsMask {
|
||||
DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode);
|
||||
DECLARE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport);
|
||||
DECLARE_STRING_TABLE_LOOKUP(firmware, Firmware);
|
||||
DECLARE_STRING_TABLE_LOOKUP(confidential_computing, ConfidentialComputing);
|
||||
|
||||
+104
-15
@@ -162,6 +162,7 @@ static Firmware arg_firmware_type = _FIRMWARE_INVALID;
|
||||
static bool arg_firmware_describe = false;
|
||||
static Set *arg_firmware_features_include = NULL;
|
||||
static Set *arg_firmware_features_exclude = NULL;
|
||||
static ConfidentialComputing arg_confidential_computing = COCO_NO;
|
||||
static char *arg_forward_journal = NULL;
|
||||
static uint64_t arg_forward_journal_max_use = UINT64_MAX;
|
||||
static uint64_t arg_forward_journal_keep_free = UINT64_MAX;
|
||||
@@ -637,6 +638,14 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
break;
|
||||
}
|
||||
|
||||
OPTION_LONG("coco", "no|sev-snp", "Run the guest as a confidential VM"): {
|
||||
ConfidentialComputing cc = confidential_computing_from_string(opts.arg);
|
||||
if (cc < 0)
|
||||
return log_error_errno(cc, "Unknown --coco= value: %s", opts.arg);
|
||||
arg_confidential_computing = cc;
|
||||
break;
|
||||
}
|
||||
|
||||
OPTION_LONG("discard-disk", "BOOL", "Control processing of discard requests"):
|
||||
r = parse_boolean_argument("--discard-disk=", opts.arg, &arg_discard_disk);
|
||||
if (r < 0)
|
||||
@@ -2589,7 +2598,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
|
||||
use_kvm = r;
|
||||
}
|
||||
|
||||
if (arg_firmware_type == FIRMWARE_UEFI) {
|
||||
if (arg_confidential_computing == COCO_AMD_SEV_SNP && !use_kvm)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
|
||||
"--coco=sev-snp requires KVM, but KVM is not available.");
|
||||
|
||||
if (arg_firmware_type == FIRMWARE_UEFI && arg_confidential_computing != COCO_AMD_SEV_SNP) {
|
||||
if (arg_firmware)
|
||||
r = load_ovmf_config(arg_firmware, &ovmf_config);
|
||||
else
|
||||
@@ -2663,13 +2676,19 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
|
||||
r = qemu_config_key(config_file, "kernel-irqchip", "split");
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM) {
|
||||
r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb));
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (ARCHITECTURE_SUPPORTS_CXL) {
|
||||
if (ARCHITECTURE_SUPPORTS_CXL && arg_confidential_computing == COCO_NO) {
|
||||
r = qemu_config_key(config_file, "cxl", "on");
|
||||
if (r < 0)
|
||||
return r;
|
||||
@@ -2687,6 +2706,12 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
|
||||
return r;
|
||||
}
|
||||
|
||||
if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
|
||||
r = qemu_config_key(config_file, "confidential-guest-support", "snp0");
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
r = qemu_config_section(config_file, "smp-opts", /* id= */ NULL,
|
||||
"cpus", arg_cpus ?: "1");
|
||||
if (r < 0)
|
||||
@@ -2719,11 +2744,13 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = qemu_config_section(config_file, "device", "balloon0",
|
||||
"driver", "virtio-balloon",
|
||||
"free-page-reporting", "on");
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (arg_confidential_computing == COCO_NO) {
|
||||
r = qemu_config_section(config_file, "device", "balloon0",
|
||||
"driver", "virtio-balloon",
|
||||
"free-page-reporting", "on");
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (ARCHITECTURE_SUPPORTS_VMGENID) {
|
||||
sd_id128_t vmgenid;
|
||||
@@ -2877,6 +2904,22 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
|
||||
return r;
|
||||
}
|
||||
|
||||
if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
|
||||
/* SNP marks encrypted guest pages via the "C-bit" in the page table entry. On all
|
||||
* SNP-capable processors (Milan and later) the C-bit lives at bit 51, which reduces
|
||||
* the usable guest physical address space by one bit.
|
||||
* Embed the hashes of kernel, initrd and cmdline into the firmware
|
||||
* so they are covered by the launch measurement and the guest's
|
||||
* boot chain starts from a measured state. */
|
||||
r = qemu_config_section(config_file, "object", "snp0",
|
||||
"qom-type", "sev-snp-guest",
|
||||
"cbitpos", "51",
|
||||
"reduced-phys-bits", "1",
|
||||
"kernel-hashes", "on");
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
unsigned child_cid = arg_vsock_cid;
|
||||
if (use_vsock) {
|
||||
config.vsock.fd = TAKE_FD(vhost_device_fd);
|
||||
@@ -2894,14 +2937,17 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
|
||||
config.vsock.cid = child_cid;
|
||||
}
|
||||
|
||||
/* -cpu stays on cmdline since not all flags are supported in config */
|
||||
r = strv_extend_many(&cmdline, "-cpu",
|
||||
/* -cpu stays on cmdline since not all flags are supported in config. SNP needs a stable,
|
||||
* named CPU model so the launch measurement is reproducible across hosts; EPYC-v4 is the
|
||||
* baseline that covers all SNP-capable processors (Milan and later). */
|
||||
const char *cpu_model =
|
||||
#ifdef __x86_64__
|
||||
"max,hv_relaxed,hv-vapic,hv-time"
|
||||
arg_confidential_computing == COCO_AMD_SEV_SNP ? "EPYC-v4"
|
||||
: "max,hv_relaxed,hv-vapic,hv-time";
|
||||
#else
|
||||
"max"
|
||||
"max";
|
||||
#endif
|
||||
);
|
||||
r = strv_extend_many(&cmdline, "-cpu", cpu_model);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
@@ -3032,9 +3078,15 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
|
||||
}
|
||||
|
||||
_cleanup_(unlink_and_freep) char *ovmf_vars = NULL;
|
||||
r = cmdline_add_ovmf(config_file, ovmf_config, &ovmf_vars);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (arg_confidential_computing != COCO_NO) {
|
||||
r = strv_extend_many(&cmdline, "-bios", arg_firmware);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
r = cmdline_add_ovmf(config_file, ovmf_config, &ovmf_vars);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (arg_linux) {
|
||||
r = strv_extend_many(&cmdline, "-kernel", arg_linux);
|
||||
@@ -4009,6 +4061,43 @@ static int verify_arguments(void) {
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'.");
|
||||
|
||||
if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
|
||||
if (native_architecture() != ARCHITECTURE_X86_64)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
|
||||
"--coco=sev-snp is only supported on x86_64.");
|
||||
if (arg_kvm == 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"--coco=sev-snp requires KVM, remove --kvm=no.");
|
||||
if (arg_firmware_type != FIRMWARE_UEFI)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"--coco can't be used with %s firmware",
|
||||
firmware_to_string(arg_firmware_type));
|
||||
/* SNP can't use pflash + NVRAM split, so the firmware-descriptor
|
||||
* machinery doesn't apply. Require an explicit raw .fd path and
|
||||
* use it verbatim with -bios later. */
|
||||
if (!arg_firmware)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"--coco=sev-snp requires --firmware=PATH "
|
||||
"pointing at a raw SNP-built OVMF .fd binary.");
|
||||
log_debug("Using raw SNP firmware at %s (no NVRAM, no Secure Boot).", arg_firmware);
|
||||
if (set_contains(arg_firmware_features_include, "secure-boot"))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"--secure-boot=yes cannot be combined with --coco.");
|
||||
if (arg_credentials.n_credentials != 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"SMBIOS credentials aren't trusted by the confidential computing guest and will be rejected.");
|
||||
if (arg_tpm > 0)
|
||||
log_warning("TPM can't be trusted by the confidential computing guest");
|
||||
/* kernel-hashes=on only covers what QEMU itself loads via -kernel/-initrd/-append.
|
||||
* Without --linux= the kernel and initrd come off disk via OVMF and aren't part
|
||||
* of the launch measurement, leaving the guest unattestable in any meaningful
|
||||
* way. Require direct kernel boot so the boot chain starts from a measured state. */
|
||||
if (!arg_linux)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"--coco=sev-snp requires --linux= "
|
||||
"so kernel, initrd and cmdline are covered by the launch measurement.");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user