mirror of
https://github.com/torvalds/linux.git
synced 2024-11-22 12:11:40 +00:00
KVM: SEV: Add KVM_SEV_SNP_LAUNCH_UPDATE command
A key aspect of a launching an SNP guest is initializing it with a known/measured payload which is then encrypted into guest memory as pre-validated private pages and then measured into the cryptographic launch context created with KVM_SEV_SNP_LAUNCH_START so that the guest can attest itself after booting. Since all private pages are provided by guest_memfd, make use of the kvm_gmem_populate() interface to handle this. The general flow is that guest_memfd will handle allocating the pages associated with the GPA ranges being initialized by each particular call of KVM_SEV_SNP_LAUNCH_UPDATE, copying data from userspace into those pages, and then the post_populate callback will do the work of setting the RMP entries for these pages to private and issuing the SNP firmware calls to encrypt/measure them. For more information see the SEV-SNP specification. Signed-off-by: Brijesh Singh <brijesh.singh@amd.com> Co-developed-by: Michael Roth <michael.roth@amd.com> Signed-off-by: Michael Roth <michael.roth@amd.com> Signed-off-by: Ashish Kalra <ashish.kalra@amd.com> Message-ID: <20240501085210.2213060-7-michael.roth@amd.com> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
This commit is contained in:
parent
136d8bc931
commit
dee5a47cc7
@ -490,6 +490,60 @@ Returns: 0 on success, -negative on error
|
||||
See SNP_LAUNCH_START in the SEV-SNP specification [snp-fw-abi]_ for further
|
||||
details on the input parameters in ``struct kvm_sev_snp_launch_start``.
|
||||
|
||||
19. KVM_SEV_SNP_LAUNCH_UPDATE
|
||||
-----------------------------
|
||||
|
||||
The KVM_SEV_SNP_LAUNCH_UPDATE command is used for loading userspace-provided
|
||||
data into a guest GPA range, measuring the contents into the SNP guest context
|
||||
created by KVM_SEV_SNP_LAUNCH_START, and then encrypting/validating that GPA
|
||||
range so that it will be immediately readable using the encryption key
|
||||
associated with the guest context once it is booted, after which point it can
|
||||
attest the measurement associated with its context before unlocking any
|
||||
secrets.
|
||||
|
||||
It is required that the GPA ranges initialized by this command have had the
|
||||
KVM_MEMORY_ATTRIBUTE_PRIVATE attribute set in advance. See the documentation
|
||||
for KVM_SET_MEMORY_ATTRIBUTES for more details on this aspect.
|
||||
|
||||
Upon success, this command is not guaranteed to have processed the entire
|
||||
range requested. Instead, the ``gfn_start``, ``uaddr``, and ``len`` fields of
|
||||
``struct kvm_sev_snp_launch_update`` will be updated to correspond to the
|
||||
remaining range that has yet to be processed. The caller should continue
|
||||
calling this command until those fields indicate the entire range has been
|
||||
processed, e.g. ``len`` is 0, ``gfn_start`` is equal to the last GFN in the
|
||||
range plus 1, and ``uaddr`` is the last byte of the userspace-provided source
|
||||
buffer address plus 1. In the case where ``type`` is KVM_SEV_SNP_PAGE_TYPE_ZERO,
|
||||
``uaddr`` will be ignored completely.
|
||||
|
||||
Parameters (in): struct kvm_sev_snp_launch_update
|
||||
|
||||
Returns: 0 on success, < 0 on error, -EAGAIN if caller should retry
|
||||
|
||||
::
|
||||
|
||||
struct kvm_sev_snp_launch_update {
|
||||
__u64 gfn_start; /* Guest page number to load/encrypt data into. */
|
||||
__u64 uaddr; /* Userspace address of data to be loaded/encrypted. */
|
||||
__u64 len; /* 4k-aligned length in bytes to copy into guest memory.*/
|
||||
__u8 type; /* The type of the guest pages being initialized. */
|
||||
__u8 pad0;
|
||||
__u16 flags; /* Must be zero. */
|
||||
__u32 pad1;
|
||||
__u64 pad2[4];
|
||||
|
||||
};
|
||||
|
||||
where the allowed values for page_type are #define'd as::
|
||||
|
||||
KVM_SEV_SNP_PAGE_TYPE_NORMAL
|
||||
KVM_SEV_SNP_PAGE_TYPE_ZERO
|
||||
KVM_SEV_SNP_PAGE_TYPE_UNMEASURED
|
||||
KVM_SEV_SNP_PAGE_TYPE_SECRETS
|
||||
KVM_SEV_SNP_PAGE_TYPE_CPUID
|
||||
|
||||
See the SEV-SNP spec [snp-fw-abi]_ for further details on how each page type is
|
||||
used/measured.
|
||||
|
||||
Device attribute API
|
||||
====================
|
||||
|
||||
|
@ -699,6 +699,7 @@ enum sev_cmd_id {
|
||||
|
||||
/* SNP-specific commands */
|
||||
KVM_SEV_SNP_LAUNCH_START = 100,
|
||||
KVM_SEV_SNP_LAUNCH_UPDATE,
|
||||
|
||||
KVM_SEV_NR_MAX,
|
||||
};
|
||||
@ -835,6 +836,24 @@ struct kvm_sev_snp_launch_start {
|
||||
__u64 pad1[4];
|
||||
};
|
||||
|
||||
/* Kept in sync with firmware values for simplicity. */
|
||||
#define KVM_SEV_SNP_PAGE_TYPE_NORMAL 0x1
|
||||
#define KVM_SEV_SNP_PAGE_TYPE_ZERO 0x3
|
||||
#define KVM_SEV_SNP_PAGE_TYPE_UNMEASURED 0x4
|
||||
#define KVM_SEV_SNP_PAGE_TYPE_SECRETS 0x5
|
||||
#define KVM_SEV_SNP_PAGE_TYPE_CPUID 0x6
|
||||
|
||||
struct kvm_sev_snp_launch_update {
|
||||
__u64 gfn_start;
|
||||
__u64 uaddr;
|
||||
__u64 len;
|
||||
__u8 type;
|
||||
__u8 pad0;
|
||||
__u16 flags;
|
||||
__u32 pad1;
|
||||
__u64 pad2[4];
|
||||
};
|
||||
|
||||
#define KVM_X2APIC_API_USE_32BIT_IDS (1ULL << 0)
|
||||
#define KVM_X2APIC_API_DISABLE_BROADCAST_QUIRK (1ULL << 1)
|
||||
|
||||
|
@ -259,6 +259,45 @@ static void sev_decommission(unsigned int handle)
|
||||
sev_guest_decommission(&decommission, NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
* Certain page-states, such as Pre-Guest and Firmware pages (as documented
|
||||
* in Chapter 5 of the SEV-SNP Firmware ABI under "Page States") cannot be
|
||||
* directly transitioned back to normal/hypervisor-owned state via RMPUPDATE
|
||||
* unless they are reclaimed first.
|
||||
*
|
||||
* Until they are reclaimed and subsequently transitioned via RMPUPDATE, they
|
||||
* might not be usable by the host due to being set as immutable or still
|
||||
* being associated with a guest ASID.
|
||||
*/
|
||||
static int snp_page_reclaim(u64 pfn)
|
||||
{
|
||||
struct sev_data_snp_page_reclaim data = {0};
|
||||
int err, rc;
|
||||
|
||||
data.paddr = __sme_set(pfn << PAGE_SHIFT);
|
||||
rc = sev_do_cmd(SEV_CMD_SNP_PAGE_RECLAIM, &data, &err);
|
||||
if (WARN_ONCE(rc, "Failed to reclaim PFN %llx", pfn))
|
||||
snp_leak_pages(pfn, 1);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
* Transition a page to hypervisor-owned/shared state in the RMP table. This
|
||||
* should not fail under normal conditions, but leak the page should that
|
||||
* happen since it will no longer be usable by the host due to RMP protections.
|
||||
*/
|
||||
static int host_rmp_make_shared(u64 pfn, enum pg_level level)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = rmp_make_shared(pfn, level);
|
||||
if (WARN_ON_ONCE(rc))
|
||||
snp_leak_pages(pfn, page_level_size(level) >> PAGE_SHIFT);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void sev_unbind_asid(struct kvm *kvm, unsigned int handle)
|
||||
{
|
||||
struct sev_data_deactivate deactivate;
|
||||
@ -2121,6 +2160,194 @@ e_free_context:
|
||||
return rc;
|
||||
}
|
||||
|
||||
struct sev_gmem_populate_args {
|
||||
__u8 type;
|
||||
int sev_fd;
|
||||
int fw_error;
|
||||
};
|
||||
|
||||
static int sev_gmem_post_populate(struct kvm *kvm, gfn_t gfn_start, kvm_pfn_t pfn,
|
||||
void __user *src, int order, void *opaque)
|
||||
{
|
||||
struct sev_gmem_populate_args *sev_populate_args = opaque;
|
||||
struct kvm_sev_info *sev = &to_kvm_svm(kvm)->sev_info;
|
||||
int n_private = 0, ret, i;
|
||||
int npages = (1 << order);
|
||||
gfn_t gfn;
|
||||
|
||||
if (WARN_ON_ONCE(sev_populate_args->type != KVM_SEV_SNP_PAGE_TYPE_ZERO && !src))
|
||||
return -EINVAL;
|
||||
|
||||
for (gfn = gfn_start, i = 0; gfn < gfn_start + npages; gfn++, i++) {
|
||||
struct sev_data_snp_launch_update fw_args = {0};
|
||||
bool assigned;
|
||||
int level;
|
||||
|
||||
if (!kvm_mem_is_private(kvm, gfn)) {
|
||||
pr_debug("%s: Failed to ensure GFN 0x%llx has private memory attribute set\n",
|
||||
__func__, gfn);
|
||||
ret = -EINVAL;
|
||||
goto err;
|
||||
}
|
||||
|
||||
ret = snp_lookup_rmpentry((u64)pfn + i, &assigned, &level);
|
||||
if (ret || assigned) {
|
||||
pr_debug("%s: Failed to ensure GFN 0x%llx RMP entry is initial shared state, ret: %d assigned: %d\n",
|
||||
__func__, gfn, ret, assigned);
|
||||
ret = -EINVAL;
|
||||
goto err;
|
||||
}
|
||||
|
||||
if (src) {
|
||||
void *vaddr = kmap_local_pfn(pfn + i);
|
||||
|
||||
ret = copy_from_user(vaddr, src + i * PAGE_SIZE, PAGE_SIZE);
|
||||
if (ret)
|
||||
goto err;
|
||||
kunmap_local(vaddr);
|
||||
}
|
||||
|
||||
ret = rmp_make_private(pfn + i, gfn << PAGE_SHIFT, PG_LEVEL_4K,
|
||||
sev_get_asid(kvm), true);
|
||||
if (ret)
|
||||
goto err;
|
||||
|
||||
n_private++;
|
||||
|
||||
fw_args.gctx_paddr = __psp_pa(sev->snp_context);
|
||||
fw_args.address = __sme_set(pfn_to_hpa(pfn + i));
|
||||
fw_args.page_size = PG_LEVEL_TO_RMP(PG_LEVEL_4K);
|
||||
fw_args.page_type = sev_populate_args->type;
|
||||
|
||||
ret = __sev_issue_cmd(sev_populate_args->sev_fd, SEV_CMD_SNP_LAUNCH_UPDATE,
|
||||
&fw_args, &sev_populate_args->fw_error);
|
||||
if (ret)
|
||||
goto fw_err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
fw_err:
|
||||
/*
|
||||
* If the firmware command failed handle the reclaim and cleanup of that
|
||||
* PFN specially vs. prior pages which can be cleaned up below without
|
||||
* needing to reclaim in advance.
|
||||
*
|
||||
* Additionally, when invalid CPUID function entries are detected,
|
||||
* firmware writes the expected values into the page and leaves it
|
||||
* unencrypted so it can be used for debugging and error-reporting.
|
||||
*
|
||||
* Copy this page back into the source buffer so userspace can use this
|
||||
* information to provide information on which CPUID leaves/fields
|
||||
* failed CPUID validation.
|
||||
*/
|
||||
if (!snp_page_reclaim(pfn + i) && !host_rmp_make_shared(pfn + i, PG_LEVEL_4K) &&
|
||||
sev_populate_args->type == KVM_SEV_SNP_PAGE_TYPE_CPUID &&
|
||||
sev_populate_args->fw_error == SEV_RET_INVALID_PARAM) {
|
||||
void *vaddr = kmap_local_pfn(pfn + i);
|
||||
|
||||
if (copy_to_user(src + i * PAGE_SIZE, vaddr, PAGE_SIZE))
|
||||
pr_debug("Failed to write CPUID page back to userspace\n");
|
||||
|
||||
kunmap_local(vaddr);
|
||||
}
|
||||
|
||||
/* pfn + i is hypervisor-owned now, so skip below cleanup for it. */
|
||||
n_private--;
|
||||
|
||||
err:
|
||||
pr_debug("%s: exiting with error ret %d (fw_error %d), restoring %d gmem PFNs to shared.\n",
|
||||
__func__, ret, sev_populate_args->fw_error, n_private);
|
||||
for (i = 0; i < n_private; i++)
|
||||
host_rmp_make_shared(pfn + i, PG_LEVEL_4K);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int snp_launch_update(struct kvm *kvm, struct kvm_sev_cmd *argp)
|
||||
{
|
||||
struct kvm_sev_info *sev = &to_kvm_svm(kvm)->sev_info;
|
||||
struct sev_gmem_populate_args sev_populate_args = {0};
|
||||
struct kvm_sev_snp_launch_update params;
|
||||
struct kvm_memory_slot *memslot;
|
||||
long npages, count;
|
||||
void __user *src;
|
||||
int ret = 0;
|
||||
|
||||
if (!sev_snp_guest(kvm) || !sev->snp_context)
|
||||
return -EINVAL;
|
||||
|
||||
if (copy_from_user(¶ms, u64_to_user_ptr(argp->data), sizeof(params)))
|
||||
return -EFAULT;
|
||||
|
||||
pr_debug("%s: GFN start 0x%llx length 0x%llx type %d flags %d\n", __func__,
|
||||
params.gfn_start, params.len, params.type, params.flags);
|
||||
|
||||
if (!PAGE_ALIGNED(params.len) || params.flags ||
|
||||
(params.type != KVM_SEV_SNP_PAGE_TYPE_NORMAL &&
|
||||
params.type != KVM_SEV_SNP_PAGE_TYPE_ZERO &&
|
||||
params.type != KVM_SEV_SNP_PAGE_TYPE_UNMEASURED &&
|
||||
params.type != KVM_SEV_SNP_PAGE_TYPE_SECRETS &&
|
||||
params.type != KVM_SEV_SNP_PAGE_TYPE_CPUID))
|
||||
return -EINVAL;
|
||||
|
||||
npages = params.len / PAGE_SIZE;
|
||||
|
||||
/*
|
||||
* For each GFN that's being prepared as part of the initial guest
|
||||
* state, the following pre-conditions are verified:
|
||||
*
|
||||
* 1) The backing memslot is a valid private memslot.
|
||||
* 2) The GFN has been set to private via KVM_SET_MEMORY_ATTRIBUTES
|
||||
* beforehand.
|
||||
* 3) The PFN of the guest_memfd has not already been set to private
|
||||
* in the RMP table.
|
||||
*
|
||||
* The KVM MMU relies on kvm->mmu_invalidate_seq to retry nested page
|
||||
* faults if there's a race between a fault and an attribute update via
|
||||
* KVM_SET_MEMORY_ATTRIBUTES, and a similar approach could be utilized
|
||||
* here. However, kvm->slots_lock guards against both this as well as
|
||||
* concurrent memslot updates occurring while these checks are being
|
||||
* performed, so use that here to make it easier to reason about the
|
||||
* initial expected state and better guard against unexpected
|
||||
* situations.
|
||||
*/
|
||||
mutex_lock(&kvm->slots_lock);
|
||||
|
||||
memslot = gfn_to_memslot(kvm, params.gfn_start);
|
||||
if (!kvm_slot_can_be_private(memslot)) {
|
||||
ret = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
sev_populate_args.sev_fd = argp->sev_fd;
|
||||
sev_populate_args.type = params.type;
|
||||
src = params.type == KVM_SEV_SNP_PAGE_TYPE_ZERO ? NULL : u64_to_user_ptr(params.uaddr);
|
||||
|
||||
count = kvm_gmem_populate(kvm, params.gfn_start, src, npages,
|
||||
sev_gmem_post_populate, &sev_populate_args);
|
||||
if (count < 0) {
|
||||
argp->error = sev_populate_args.fw_error;
|
||||
pr_debug("%s: kvm_gmem_populate failed, ret %ld (fw_error %d)\n",
|
||||
__func__, count, argp->error);
|
||||
ret = -EIO;
|
||||
} else {
|
||||
params.gfn_start += count;
|
||||
params.len -= count * PAGE_SIZE;
|
||||
if (params.type != KVM_SEV_SNP_PAGE_TYPE_ZERO)
|
||||
params.uaddr += count * PAGE_SIZE;
|
||||
|
||||
ret = 0;
|
||||
if (copy_to_user(u64_to_user_ptr(argp->data), ¶ms, sizeof(params)))
|
||||
ret = -EFAULT;
|
||||
}
|
||||
|
||||
out:
|
||||
mutex_unlock(&kvm->slots_lock);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sev_mem_enc_ioctl(struct kvm *kvm, void __user *argp)
|
||||
{
|
||||
struct kvm_sev_cmd sev_cmd;
|
||||
@ -2220,6 +2447,9 @@ int sev_mem_enc_ioctl(struct kvm *kvm, void __user *argp)
|
||||
case KVM_SEV_SNP_LAUNCH_START:
|
||||
r = snp_launch_start(kvm, &sev_cmd);
|
||||
break;
|
||||
case KVM_SEV_SNP_LAUNCH_UPDATE:
|
||||
r = snp_launch_update(kvm, &sev_cmd);
|
||||
break;
|
||||
default:
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
|
Loading…
Reference in New Issue
Block a user