acrn-hypervisor/hypervisor/arch/x86/guest/vmcall.c
Junjie Mao ea4eadf0a5 hv: hypercalls: refactor permission-checking and dispatching logic
The current permission-checking and dispatching mechanism of hypercalls is
not unified because:

  1. Some hypercalls require the exact vCPU initiating the call, while the
     others only need to know the VM.
  2. Different hypercalls have different permission requirements: the
     trusty-related ones are enabled by a guest flag, while the others
     require the initiating VM to be the Service OS.

Without a unified logic it could be hard to scale when more kinds of
hypercalls are added later.

The objectives of this patch are as follows.

  1. All hypercalls have the same prototype and are dispatched by a unified
     logic.
  2. Permissions are checked by a unified logic without consulting the
     hypercall ID.

To achieve the first objective, this patch modifies the type of the first
parameter of hcall_* functions (which are the callbacks implementing the
hypercalls) from `struct acrn_vm *` to `struct acrn_vcpu *`. The
doxygen-style documentations are updated accordingly.

To achieve the second objective, this patch adds to `struct hc_dispatch` a
`permission_flags` field which specifies the guest flags that must ALL be
set for a VM to be able to invoke the hypercall. The default value (which
is 0UL) indicates that this hypercall is for SOS only. Currently only the
`permission_flag` of trusty-related hypercalls have the non-zero value
GUEST_FLAG_SECURE_WORLD_ENABLED.

With `permission_flag`, the permission checking logic of hypercalls is
unified as follows.

  1. General checks
     i. If the VM is neither SOS nor having any guest flag that allows
        certain hypercalls, it gets #UD upon executing the `vmcall`
        instruction.
    ii. If the VM is allowed to execute the `vmcall` instruction, but
        attempts to execute it in ring 1, 2 or 3, the VM gets #GP(0).
  2. Hypercall-specific checks
     i. If the hypercall is for SOS (i.e. `permission_flag` is 0), the
        initiating VM must be SOS and the specified target VM cannot be a
        pre-launched VM. Otherwise the hypercall returns -EINVAL without
        further actions.
    ii. If the hypercall requires certain guest flags, the initiating VM
        must have all the required flags. Otherwise the hypercall returns
        -EINVAL without further actions.
   iii. A hypercall with an unknown hypercall ID makes the hypercall
        returns -EINVAL without further actions.

The logic above is different from the current implementation in the
following aspects.

  1. A pre-launched VM now gets #UD (rather than #GP(0)) when it attempts
     to execute `vmcall` in ring 1, 2 or 3.
  2. A pre-launched VM now gets #UD (rather than the return value -EPERM)
     when it attempts to execute a trusty hypercall in ring 0.
  3. The SOS now gets the return value -EINVAL (rather than -EPERM) when it
     attempts to invoke a trusty hypercall.
  4. A post-launched VM with trusty support now gets the return value
     -EINVAL (rather than #UD) when it attempts to invoke a non-trusty
     hypercall or an invalid hypercall.

v1 -> v2:
 - Update documentation that describe hypercall behavior.
 - Fix Doxygen warnings

Tracked-On: #5924
Signed-off-by: Junjie Mao <junjie.mao@intel.com>
Acked-by: Eddie Dong <eddie.dong@intel.com>
2021-05-12 13:43:41 +08:00

244 lines
7.9 KiB
C

/*
* Copyright (C) 2018 Intel Corporation. All rights reserved.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <types.h>
#include <errno.h>
#include <asm/lib/spinlock.h>
#include <asm/guest/vcpu.h>
#include <asm/guest/vm.h>
#include <asm/guest/virq.h>
#include <acrn_hv_defs.h>
#include <hypercall.h>
#include <trace.h>
#include <logmsg.h>
struct hc_dispatch {
int32_t (*handler)(struct acrn_vcpu *vcpu, struct acrn_vm *target_vm, uint64_t param1, uint64_t param2);
/* The permission_flags is a bitmap of guest flags indicating whether a VM can invoke this hypercall:
*
* - If permission_flags == 0UL (which is the default value), this hypercall can only be invoked by the SOS.
* - Otherwise, this hypercall can only be invoked by a VM whose guest flags have ALL set bits in
* permission_flags.
*/
uint64_t permission_flags;
};
/* VM Dispatch table for Exit condition handling */
static const struct hc_dispatch hc_dispatch_table[] = {
[HC_IDX(HC_GET_API_VERSION)] = {
.handler = hcall_get_api_version},
[HC_IDX(HC_SOS_OFFLINE_CPU)] = {
.handler = hcall_sos_offline_cpu},
[HC_IDX(HC_SET_CALLBACK_VECTOR)] = {
.handler = hcall_set_callback_vector},
[HC_IDX(HC_GET_PLATFORM_INFO)] = {
.handler = hcall_get_platform_info},
[HC_IDX(HC_CREATE_VM)] = {
.handler = hcall_create_vm},
[HC_IDX(HC_DESTROY_VM)] = {
.handler = hcall_destroy_vm},
[HC_IDX(HC_START_VM)] = {
.handler = hcall_start_vm},
[HC_IDX(HC_RESET_VM)] = {
.handler = hcall_reset_vm},
[HC_IDX(HC_PAUSE_VM)] = {
.handler = hcall_pause_vm},
[HC_IDX(HC_SET_VCPU_REGS)] = {
.handler = hcall_set_vcpu_regs},
[HC_IDX(HC_CREATE_VCPU)] = {
.handler = hcall_create_vcpu},
[HC_IDX(HC_SET_IRQLINE)] = {
.handler = hcall_set_irqline},
[HC_IDX(HC_INJECT_MSI)] = {
.handler = hcall_inject_msi},
[HC_IDX(HC_SET_IOREQ_BUFFER)] = {
.handler = hcall_set_ioreq_buffer},
[HC_IDX(HC_NOTIFY_REQUEST_FINISH)] = {
.handler = hcall_notify_ioreq_finish},
[HC_IDX(HC_VM_SET_MEMORY_REGIONS)] = {
.handler = hcall_set_vm_memory_regions},
[HC_IDX(HC_VM_WRITE_PROTECT_PAGE)] = {
.handler = hcall_write_protect_page},
[HC_IDX(HC_VM_GPA2HPA)] = {
.handler = hcall_gpa_to_hpa},
[HC_IDX(HC_ASSIGN_PCIDEV)] = {
.handler = hcall_assign_pcidev},
[HC_IDX(HC_DEASSIGN_PCIDEV)] = {
.handler = hcall_deassign_pcidev},
[HC_IDX(HC_ASSIGN_MMIODEV)] = {
.handler = hcall_assign_mmiodev},
[HC_IDX(HC_DEASSIGN_MMIODEV)] = {
.handler = hcall_deassign_mmiodev},
[HC_IDX(HC_ADD_VDEV)] = {
.handler = hcall_add_vdev},
[HC_IDX(HC_REMOVE_VDEV)] = {
.handler = hcall_remove_vdev},
[HC_IDX(HC_SET_PTDEV_INTR_INFO)] = {
.handler = hcall_set_ptdev_intr_info},
[HC_IDX(HC_RESET_PTDEV_INTR_INFO)] = {
.handler = hcall_reset_ptdev_intr_info},
[HC_IDX(HC_PM_GET_CPU_STATE)] = {
.handler = hcall_get_cpu_pm_state},
[HC_IDX(HC_VM_INTR_MONITOR)] = {
.handler = hcall_vm_intr_monitor},
[HC_IDX(HC_SETUP_SBUF)] = {
.handler = hcall_setup_sbuf},
[HC_IDX(HC_SETUP_HV_NPK_LOG)] = {
.handler = hcall_setup_hv_npk_log},
[HC_IDX(HC_PROFILING_OPS)] = {
.handler = hcall_profiling_ops},
[HC_IDX(HC_GET_HW_INFO)] = {
.handler = hcall_get_hw_info},
[HC_IDX(HC_INITIALIZE_TRUSTY)] = {
.handler = hcall_initialize_trusty,
.permission_flags = GUEST_FLAG_SECURE_WORLD_ENABLED},
[HC_IDX(HC_WORLD_SWITCH)] = {
.handler = hcall_world_switch,
.permission_flags = GUEST_FLAG_SECURE_WORLD_ENABLED},
[HC_IDX(HC_SAVE_RESTORE_SWORLD_CTX)] = {
.handler = hcall_save_restore_sworld_ctx,
.permission_flags = GUEST_FLAG_SECURE_WORLD_ENABLED},
};
#define GUEST_FLAGS_ALLOWING_HYPERCALLS GUEST_FLAG_SECURE_WORLD_ENABLED
struct acrn_vm *parse_target_vm(struct acrn_vm *sos_vm, uint64_t hcall_id, uint64_t param1, __unused uint64_t param2)
{
struct acrn_vm *target_vm = NULL;
uint16_t vm_id = ACRN_INVALID_VMID;
struct acrn_create_vm cv;
struct set_regions regions;
uint16_t relative_vm_id;
switch (hcall_id) {
case HC_CREATE_VM:
if (copy_from_gpa(sos_vm, &cv, param1, sizeof(cv)) == 0) {
vm_id = get_vmid_by_uuid(&cv.uuid[0]);
}
break;
case HC_PM_GET_CPU_STATE:
vm_id = rel_vmid_2_vmid(sos_vm->vm_id, (uint16_t)((param1 & PMCMD_VMID_MASK) >> PMCMD_VMID_SHIFT));
break;
case HC_VM_SET_MEMORY_REGIONS:
if (copy_from_gpa(sos_vm, &regions, param1, sizeof(regions)) == 0) {
/* the vmid in regions is a relative vm id, need to convert to absolute vm id */
vm_id = rel_vmid_2_vmid(sos_vm->vm_id, regions.vmid);
}
break;
case HC_GET_API_VERSION:
case HC_SOS_OFFLINE_CPU:
case HC_SET_CALLBACK_VECTOR:
case HC_GET_PLATFORM_INFO:
case HC_SETUP_SBUF:
case HC_SETUP_HV_NPK_LOG:
case HC_PROFILING_OPS:
case HC_GET_HW_INFO:
target_vm = sos_vm;
break;
default:
relative_vm_id = (uint16_t)param1;
vm_id = rel_vmid_2_vmid(sos_vm->vm_id, relative_vm_id);
break;
}
if ((target_vm == NULL) && (vm_id < CONFIG_MAX_VM_NUM)) {
target_vm = get_vm_from_vmid(vm_id);
if (hcall_id == HC_CREATE_VM) {
target_vm->vm_id = vm_id;
}
}
return target_vm;
}
static int32_t dispatch_hypercall(struct acrn_vcpu *vcpu)
{
int32_t ret = -EINVAL;
struct acrn_vm *vm = vcpu->vm;
uint64_t guest_flags = get_vm_config(vm->vm_id)->guest_flags; /* hypercall ID from guest */
uint64_t hcall_id = vcpu_get_gpreg(vcpu, CPU_REG_R8); /* hypercall ID from guest */
if (HC_IDX(hcall_id) < ARRAY_SIZE(hc_dispatch_table)) {
const struct hc_dispatch *dispatch = &(hc_dispatch_table[HC_IDX(hcall_id)]);
uint64_t permission_flags = dispatch->permission_flags;
if (dispatch->handler != NULL) {
uint64_t param1 = vcpu_get_gpreg(vcpu, CPU_REG_RDI); /* hypercall param1 from guest */
uint64_t param2 = vcpu_get_gpreg(vcpu, CPU_REG_RSI); /* hypercall param2 from guest */
if ((permission_flags == 0UL) && is_sos_vm(vm)) {
/* A permission_flags of 0 indicates that this hypercall is for SOS to manage
* post-launched VMs.
*/
struct acrn_vm *target_vm = parse_target_vm(vm, hcall_id, param1, param2);
if ((target_vm != NULL) && !is_prelaunched_vm(target_vm)) {
get_vm_lock(target_vm);
ret = dispatch->handler(vcpu, target_vm, param1, param2);
put_vm_lock(target_vm);
}
} else if ((permission_flags != 0UL) &&
((guest_flags & permission_flags) == permission_flags)) {
ret = dispatch->handler(vcpu, vcpu->vm, param1, param2);
} else {
/* The vCPU is not allowed to invoke the given hypercall. Keep `ret` as -EINVAL and no
* further actions required.
*/
}
}
}
return ret;
}
/*
* Pass return value to SOS by register rax.
* This function should always return 0 since we shouldn't
* deal with hypercall error in hypervisor.
*/
int32_t vmcall_vmexit_handler(struct acrn_vcpu *vcpu)
{
int32_t ret;
struct acrn_vm *vm = vcpu->vm;
/* hypercall ID from guest*/
uint64_t hypcall_id = vcpu_get_gpreg(vcpu, CPU_REG_R8);
uint64_t guest_flags = get_vm_config(vm->vm_id)->guest_flags;
/*
* The following permission checks are applied to hypercalls.
*
* 1. Only SOS and VMs with specific guest flags (referred to as 'allowed VMs' hereinafter) can invoke
* hypercalls by executing the `vmcall` instruction. Attempts to execute the `vmcall` instruction in the
* other VMs will trigger #UD.
* 2. Attempts to execute the `vmcall` instruction from ring 1, 2 or 3 in an allowed VM will trigger #GP(0).
* 3. An allowed VM is permitted to only invoke some of the supported hypercalls depending on its load order and
* guest flags. Attempts to invoke an unpermitted hypercall will make a vCPU see -EINVAL as the return
* value. No exception is triggered in this case.
*/
if (!is_sos_vm(vm) && ((guest_flags & GUEST_FLAGS_ALLOWING_HYPERCALLS) == 0UL)) {
vcpu_inject_ud(vcpu);
ret = -ENODEV;
} else if (!is_hypercall_from_ring0()) {
vcpu_inject_gp(vcpu, 0U);
ret = -EACCES;
} else {
ret = dispatch_hypercall(vcpu);
}
if ((ret != -EACCES) && (ret != -ENODEV)) {
vcpu_set_gpreg(vcpu, CPU_REG_RAX, (uint64_t)ret);
}
if (ret < 0) {
pr_err("ret=%d hypercall=0x%lx failed in %s\n", ret, hypcall_id, __func__);
}
TRACE_2L(TRACE_VMEXIT_VMCALL, vm->vm_id, hypcall_id);
return 0;
}