632 lines
13 KiB
C
632 lines
13 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (c) 2025 Samsung Electronics Co., Ltd.
|
|
* http://www.samsung.com/
|
|
*/
|
|
|
|
#include <linux/anon_inodes.h>
|
|
#include <linux/file.h>
|
|
#include <linux/kdev_t.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/module.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/types.h>
|
|
#include <linux/debugfs.h>
|
|
|
|
#include "exynos-hvc.h"
|
|
#include "hvm_drv.h"
|
|
|
|
static DEFINE_MUTEX(hvm_list_lock);
|
|
static LIST_HEAD(hvm_list);
|
|
|
|
/**
|
|
* register_memslot_addr_range() - Register memory region to Halla
|
|
* @hvm: Pointer to struct hvm
|
|
* @memslot: Pointer to struct hvm_memslot
|
|
*
|
|
* Return: 0 for success, negative number for error
|
|
*/
|
|
static int
|
|
register_memslot_addr_range(struct hvm *hvm, struct hvm_memslot *memslot)
|
|
{
|
|
struct hvm_memory_region_ranges *region;
|
|
u32 buf_size = PAGE_SIZE * 2;
|
|
u64 gfn;
|
|
unsigned long ret;
|
|
|
|
region = alloc_pages_exact(buf_size, GFP_KERNEL);
|
|
if (!region)
|
|
return -ENOMEM;
|
|
|
|
region->slot = memslot->slot_id;
|
|
region->total_pages = memslot->npages;
|
|
gfn = memslot->base_gfn;
|
|
region->gpa = PFN_PHYS(gfn);
|
|
|
|
ret = exynos_hvc(HVC_FID_HVM_SET_MEMREGION,
|
|
hvm->vm_id, buf_size,
|
|
virt_to_phys(region), 0);
|
|
if (ret) {
|
|
free_pages_exact(region, buf_size);
|
|
return -EFAULT;
|
|
}
|
|
|
|
free_pages_exact(region, buf_size);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* memregion_validity_check() - Preliminary check for memory region
|
|
* @hvm: Pointer to struct hvm.
|
|
* @mem: Input memory region from user.
|
|
*
|
|
* Return: true for check passed, false for invalid input.
|
|
*/
|
|
static bool
|
|
memregion_validity_check(struct hvm *hvm,
|
|
struct hvm_userspace_memory_region *mem)
|
|
{
|
|
if (mem->slot >= HVM_MAX_MEM_REGION)
|
|
return false;
|
|
|
|
if (!PAGE_ALIGNED(mem->guest_phys_addr) ||
|
|
!PAGE_ALIGNED(mem->memory_size))
|
|
return false;
|
|
|
|
if (mem->guest_phys_addr + mem->memory_size < mem->guest_phys_addr)
|
|
return false;
|
|
|
|
if ((mem->memory_size >> PAGE_SHIFT) > HVM_MEM_MAX_NR_PAGES)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* hvm_vm_ioctl_set_memory_region() - Set memory region of guest
|
|
* @hvm: Pointer to struct hvm.
|
|
* @mem: Input memory region from user.
|
|
*
|
|
* Return: 0 for success, negative number for error
|
|
*
|
|
* -EXIO - The memslot is out-of-range
|
|
* -EFAULT - Cannot find corresponding vma
|
|
* -EINVAL - Region size and VMA size mismatch
|
|
*/
|
|
static int
|
|
hvm_vm_ioctl_set_memory_region(struct hvm *hvm,
|
|
struct hvm_userspace_memory_region *mem)
|
|
{
|
|
int ret;
|
|
struct vm_area_struct *vma;
|
|
struct hvm_memslot *memslot;
|
|
unsigned long size;
|
|
|
|
if (memregion_validity_check(hvm, mem) != true)
|
|
return -EINVAL;
|
|
|
|
memslot = &hvm->memslot[mem->slot];
|
|
|
|
vma = vma_lookup(hvm->mm, mem->userspace_addr);
|
|
if (!vma)
|
|
return -EFAULT;
|
|
|
|
size = vma->vm_end - vma->vm_start;
|
|
if (size != mem->memory_size)
|
|
return -EINVAL;
|
|
|
|
memslot->base_gfn = __phys_to_pfn(mem->guest_phys_addr);
|
|
memslot->npages = size >> PAGE_SHIFT;
|
|
memslot->userspace_addr = mem->userspace_addr;
|
|
memslot->vma = vma;
|
|
memslot->flags = mem->flags;
|
|
memslot->slot_id = mem->slot;
|
|
|
|
ret = (int)exynos_hvc(HVC_FID_HVM_MEMREGION_PURPOSE,
|
|
hvm->vm_id, mem->guest_phys_addr,
|
|
mem->memory_size, mem->flags);
|
|
if (ret)
|
|
return -EFAULT;
|
|
|
|
return register_memslot_addr_range(hvm, memslot);
|
|
}
|
|
|
|
/**
|
|
* hvm_inject_irq() - Inject virtual interrupt to a VM
|
|
* @hvm: Pointer to struct hvm
|
|
* @vcpu_idx: vcpu index, only valid if PPI
|
|
* @irq: *SPI* irq number (excluding offset value `32`)
|
|
* @level: 1 if true else 0
|
|
*
|
|
* Return:
|
|
* * 0 - Success.
|
|
* * Negative - Failure.
|
|
*/
|
|
int hvm_inject_irq(struct hvm *hvm, unsigned int vcpu_idx,
|
|
u32 irq, bool level)
|
|
{
|
|
unsigned long ret;
|
|
u64 ids;
|
|
struct hvm_vcpu *vcpu = hvm->vcpus[vcpu_idx];
|
|
|
|
ids = (u32)hvm->vm_id;
|
|
ids = (ids << 16) | vcpu_idx;
|
|
|
|
ret = exynos_hvc(HVC_FID_HVM_IRQ_LINE,
|
|
ids, irq, level, 0);
|
|
|
|
hvm_vcpu_wakeup(vcpu);
|
|
|
|
return (int)ret;
|
|
}
|
|
|
|
static int hvm_vm_ioctl_irq_line(struct hvm *hvm,
|
|
struct hvm_irq_level *irq_level)
|
|
{
|
|
u32 irq = irq_level->irq;
|
|
u32 vcpu_idx, vcpu2_idx, irq_num;
|
|
bool level = irq_level->level;
|
|
|
|
vcpu_idx = FIELD_GET(HVM_IRQ_LINE_VCPU, irq);
|
|
vcpu2_idx = FIELD_GET(HVM_IRQ_LINE_VCPU2, irq) * (HVM_IRQ_VCPU_MASK + 1);
|
|
irq_num = FIELD_GET(HVM_IRQ_LINE_NUM, irq);
|
|
|
|
return hvm_inject_irq(hvm, vcpu_idx + vcpu2_idx, irq_num,
|
|
level);
|
|
}
|
|
|
|
static int hvm_vm_ioctl_create_device(struct hvm *hvm, void __user *argp)
|
|
{
|
|
struct hvm_create_device *hvm_dev;
|
|
void *dev_data = NULL;
|
|
int ret;
|
|
|
|
hvm_dev = alloc_pages_exact(PAGE_SIZE, GFP_KERNEL);
|
|
if (!hvm_dev)
|
|
return -ENOMEM;
|
|
|
|
if (copy_from_user(hvm_dev, argp, sizeof(struct hvm_create_device))) {
|
|
ret = -EFAULT;
|
|
goto err_free_dev;
|
|
}
|
|
|
|
if (hvm_dev->attr_addr != 0 && hvm_dev->attr_size != 0) {
|
|
size_t attr_size = hvm_dev->attr_size;
|
|
void __user *attr_addr = u64_to_user_ptr(hvm_dev->attr_addr);
|
|
|
|
/* Size of device specific data should not be over a page. */
|
|
if (attr_size > PAGE_SIZE) {
|
|
ret = -EINVAL;
|
|
goto err_free_dev;
|
|
}
|
|
|
|
dev_data = alloc_pages_exact(attr_size, GFP_KERNEL);
|
|
if (!dev_data) {
|
|
ret = -ENOMEM;
|
|
goto err_free_dev;
|
|
}
|
|
|
|
if (copy_from_user(dev_data, attr_addr, attr_size)) {
|
|
ret = -EFAULT;
|
|
goto err_free_dev_data;
|
|
}
|
|
hvm_dev->attr_addr = virt_to_phys(dev_data);
|
|
}
|
|
|
|
ret = (int)exynos_hvc(HVC_FID_HVM_CREATE_DEVICE,
|
|
hvm->vm_id, virt_to_phys(hvm_dev), 0, 0);
|
|
err_free_dev_data:
|
|
if (dev_data)
|
|
free_pages_exact(dev_data, 0);
|
|
err_free_dev:
|
|
free_pages_exact(hvm_dev, 0);
|
|
return ret;
|
|
}
|
|
|
|
static int hvm_vm_enable_cap(struct hvm *hvm,
|
|
struct hvm_enable_cap *cap,
|
|
struct arm_smccc_res *res)
|
|
{
|
|
arm_smccc_hvc(HVC_FID_HVM_ENABLE_CAP,
|
|
hvm->vm_id, cap->cap, cap->args[0], cap->args[1], 0, 0, 0, res);
|
|
|
|
return res->a0;
|
|
}
|
|
|
|
static int hvm_vm_ioctl_enable_cap(struct hvm *hvm,
|
|
struct hvm_enable_cap *cap,
|
|
void __user *argp)
|
|
{
|
|
struct arm_smccc_res res = {0};
|
|
int ret;
|
|
|
|
/* Currently, only pvm-related capabilities exist */
|
|
if (cap->cap != HVM_CAP_PROTECTED_VM)
|
|
return -EINVAL;
|
|
|
|
switch (cap->args[0]) {
|
|
case HVM_CAP_PVM_SET_PVMFW_GPA:
|
|
fallthrough;
|
|
case HVM_CAP_PVM_SET_PROTECTED_VM:
|
|
ret = hvm_vm_enable_cap(hvm, cap, &res);
|
|
return ret;
|
|
case HVM_CAP_PVM_GET_PVMFW_SIZE: {
|
|
ret = hvm_vm_enable_cap(hvm, cap, &res);
|
|
if (ret)
|
|
return -EINVAL;
|
|
cap->args[1] = res.a1;
|
|
if (copy_to_user(argp, cap, sizeof(*cap)))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int set_dtb_config(struct hvm *hvm, struct hvm_dtb_config *cfg)
|
|
{
|
|
unsigned long ret;
|
|
|
|
ret = exynos_hvc(HVC_FID_HVM_SET_DTB_CONFIG,
|
|
hvm->vm_id, cfg->dtb_addr, cfg->dtb_size, 0);
|
|
|
|
return (int)ret;
|
|
}
|
|
|
|
static unsigned long hvm_pre_destory_vm(struct hvm *hvm)
|
|
{
|
|
return exynos_hvc(HVC_FID_HVM_PRE_DESTROY_VM,
|
|
hvm->vm_id, 0, 0, 0);
|
|
}
|
|
|
|
static unsigned long
|
|
__hvm_reclaim_guest_memory(u16 vmid, u64 gfn, unsigned long nr_pages)
|
|
{
|
|
phys_addr_t ipa = PFN_PHYS(gfn);
|
|
unsigned long left_pages = nr_pages;
|
|
unsigned long min_pages = HVM_RECLAIM_MIN_SIZE >> PAGE_SHIFT;
|
|
unsigned long order = min_pages;
|
|
unsigned long ret = 0;
|
|
|
|
while (left_pages > 0) {
|
|
if (order > left_pages)
|
|
order = left_pages;
|
|
|
|
ret = exynos_hvc(HVC_FID_HVM_RECLAIM_GUEST_MEMORY,
|
|
vmid, ipa, order, 0);
|
|
if (ret)
|
|
break;
|
|
|
|
left_pages -= order;
|
|
ipa += (order << PAGE_SHIFT);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static unsigned long hvm_memslot_reclaim_guest_memory(struct hvm *hvm)
|
|
{
|
|
struct hvm_memslot *slot;
|
|
unsigned long ret = 0;
|
|
|
|
for (unsigned int i = 0; i < HVM_MAX_MEM_REGION; i++) {
|
|
slot = &hvm->memslot[i];
|
|
|
|
if (slot->npages == 0)
|
|
continue;
|
|
|
|
ret = __hvm_reclaim_guest_memory(hvm->vm_id,
|
|
slot->base_gfn,
|
|
slot->npages);
|
|
if (ret)
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static unsigned long hvm_reclaim_guest_memory(struct hvm *hvm)
|
|
{
|
|
return hvm_memslot_reclaim_guest_memory(hvm);
|
|
}
|
|
|
|
static void hvm_reclaim_guest_stage2_pgtable(struct hvm *hvm)
|
|
{
|
|
unsigned long ret;
|
|
|
|
ret = exynos_hvc(HVC_FID_HVM_RECLAIM_GUEST_S2_PGTABLE,
|
|
hvm->vm_id, 0, 0, 0);
|
|
if (ret)
|
|
return;
|
|
|
|
hvm_free_pages(hvm->s2table_buf, PAGE_SIZE);
|
|
|
|
for (unsigned int i = 0; i < hvm->pgtable_num; i++)
|
|
hvm_free_pages(hvm->vm_pgtable[i], PAGE_SIZE * 2);
|
|
}
|
|
|
|
/* Invoker of this function is responsible for locking */
|
|
static void hvm_destroy_all_ppage(struct hvm *hvm)
|
|
{
|
|
struct hvm_pinned_page *ppage;
|
|
struct rb_node *node;
|
|
|
|
node = rb_first(&hvm->pinned_pages);
|
|
while (node) {
|
|
ppage = rb_entry(node, struct hvm_pinned_page, node);
|
|
unpin_user_pages_dirty_lock(&ppage->page, 1, true);
|
|
node = rb_next(node);
|
|
rb_erase(&ppage->node, &hvm->pinned_pages);
|
|
kfree(ppage);
|
|
}
|
|
}
|
|
|
|
static void hvm_destroy_vm(struct hvm *hvm)
|
|
{
|
|
unsigned long ret = 0;
|
|
|
|
mutex_lock(&hvm->lock);
|
|
|
|
hvm_pre_destory_vm(hvm);
|
|
|
|
hvm_vm_irqfd_release(hvm);
|
|
|
|
ret = hvm_reclaim_guest_memory(hvm);
|
|
hvm_reclaim_guest_stage2_pgtable(hvm);
|
|
|
|
hvm_destroy_vcpus(hvm);
|
|
|
|
exynos_hvc(HVC_FID_HVM_DESTROY_VM,
|
|
hvm->vm_id, 0, 0, 0);
|
|
|
|
hvm_destroy_vm_debugfs(hvm);
|
|
|
|
mutex_lock(&hvm_list_lock);
|
|
list_del(&hvm->vm_list);
|
|
mutex_unlock(&hvm_list_lock);
|
|
|
|
mutex_unlock(&hvm->lock);
|
|
|
|
/**
|
|
* Have to leave the page pinned as we don't know whether its
|
|
* save to access
|
|
*/
|
|
if (!ret) {
|
|
/**
|
|
* No need to lock here becauese it's single-threaded
|
|
* execution
|
|
*/
|
|
hvm_destroy_all_ppage(hvm);
|
|
}
|
|
|
|
kfree(hvm);
|
|
}
|
|
|
|
void hvm_destroy_all_vms(void)
|
|
{
|
|
struct hvm *hvm, *tmp;
|
|
|
|
mutex_lock(&hvm_list_lock);
|
|
if (list_empty(&hvm_list))
|
|
goto out;
|
|
|
|
list_for_each_entry_safe(hvm, tmp, &hvm_list, vm_list)
|
|
hvm_destroy_vm(hvm);
|
|
|
|
out:
|
|
mutex_unlock(&hvm_list_lock);
|
|
}
|
|
|
|
int hvm_check_extension(struct hvm *hvm, u64 cap, void __user *argp)
|
|
{
|
|
switch (cap) {
|
|
case HVM_CAP_PROTECTED_VM: {
|
|
u64 success = 1;
|
|
|
|
if (copy_to_user(argp, &success, sizeof(__u64)))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
case HVM_CAP_VM_GPA_SIZE: {
|
|
u64 value = CONFIG_ARM64_PA_BITS;
|
|
|
|
if (copy_to_user(argp, &value, sizeof(__u64)))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
/* hvm_vm_ioctl() - Ioctl handler of VM FD */
|
|
static long hvm_vm_ioctl(struct file *filp, unsigned int ioctl,
|
|
unsigned long arg)
|
|
{
|
|
long ret;
|
|
void __user *argp = (void __user *)arg;
|
|
struct hvm *hvm = filp->private_data;
|
|
|
|
switch (ioctl) {
|
|
case HVM_CHECK_EXTENSION: {
|
|
u64 cap;
|
|
|
|
if (copy_from_user(&cap, argp, sizeof(__u64)))
|
|
return -EFAULT;
|
|
|
|
ret = hvm_check_extension(hvm, cap, argp);
|
|
break;
|
|
}
|
|
case HVM_CREATE_VCPU: {
|
|
ret = hvm_vm_ioctl_create_vcpu(hvm, arg);
|
|
break;
|
|
}
|
|
case HVM_SET_USER_MEMORY_REGION: {
|
|
struct hvm_userspace_memory_region userspace_mem;
|
|
|
|
if (copy_from_user(&userspace_mem, argp, sizeof(userspace_mem)))
|
|
return -EFAULT;
|
|
|
|
ret = hvm_vm_ioctl_set_memory_region(hvm, &userspace_mem);
|
|
break;
|
|
}
|
|
case HVM_IRQ_LINE: {
|
|
struct hvm_irq_level irq_level;
|
|
|
|
if (copy_from_user(&irq_level, argp, sizeof(irq_level))) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
ret = hvm_vm_ioctl_irq_line(hvm, &irq_level);
|
|
break;
|
|
}
|
|
case HVM_CREATE_DEVICE: {
|
|
ret = hvm_vm_ioctl_create_device(hvm, argp);
|
|
break;
|
|
}
|
|
case HVM_IRQFD: {
|
|
struct hvm_irqfd data;
|
|
|
|
if (copy_from_user(&data, argp, sizeof(data))) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
ret = hvm_irqfd(hvm, &data);
|
|
break;
|
|
}
|
|
case HVM_IOEVENTFD: {
|
|
struct hvm_ioeventfd data;
|
|
|
|
if (copy_from_user(&data, argp, sizeof(data))) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
ret = hvm_ioeventfd(hvm, &data);
|
|
break;
|
|
}
|
|
case HVM_ENABLE_CAP: {
|
|
struct hvm_enable_cap cap;
|
|
|
|
if (copy_from_user(&cap, argp, sizeof(cap))) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
ret = hvm_vm_ioctl_enable_cap(hvm, &cap, argp);
|
|
break;
|
|
}
|
|
case HVM_SET_DTB_CONFIG: {
|
|
struct hvm_dtb_config cfg;
|
|
|
|
if (copy_from_user(&cfg, argp, sizeof(cfg))) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
ret = set_dtb_config(hvm, &cfg);
|
|
break;
|
|
}
|
|
default:
|
|
ret = -ENOTTY;
|
|
}
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static int hvm_vm_release(struct inode *inode, struct file *filp)
|
|
{
|
|
struct hvm *hvm = filp->private_data;
|
|
|
|
hvm_destroy_vm(hvm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations hvm_vm_fops = {
|
|
.release = hvm_vm_release,
|
|
.unlocked_ioctl = hvm_vm_ioctl,
|
|
};
|
|
|
|
static int hvm_arch_drv_init(struct hvm *hvm)
|
|
{
|
|
clocks_calc_mult_shift(&hvm->clock_scale_factor.mult,
|
|
&hvm->clock_scale_factor.shift,
|
|
arch_timer_get_cntfrq(),
|
|
NSEC_PER_SEC,
|
|
0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* hvm_dev_ioctl_create_vm - Create vm fd
|
|
* @vm_type: VM type. Only supports Linux VM now.
|
|
*
|
|
* Return: fd of vm, negative if error
|
|
*/
|
|
int hvm_dev_ioctl_create_vm(unsigned long vm_type)
|
|
{
|
|
struct hvm *hvm;
|
|
int ret;
|
|
|
|
hvm = kzalloc(sizeof(struct hvm), GFP_KERNEL);
|
|
if (!hvm)
|
|
return -ENOMEM;
|
|
|
|
ret = exynos_hvc(HVC_FID_HVM_CREATE_VM, vm_type, 0, 0, 0);
|
|
if (ret == 0) {
|
|
kfree(hvm);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
hvm->vm_id = ret;
|
|
hvm->mm = current->mm;
|
|
mutex_init(&hvm->lock);
|
|
mutex_init(&hvm->mem_lock);
|
|
hvm->pinned_pages = RB_ROOT;
|
|
|
|
ret = hvm_vm_irqfd_init(hvm);
|
|
if (ret) {
|
|
kfree(hvm);
|
|
return ret;
|
|
}
|
|
|
|
ret = hvm_init_ioeventfd(hvm);
|
|
if (ret) {
|
|
kfree(hvm);
|
|
return ret;
|
|
}
|
|
|
|
ret = hvm_init_vm_stage2_mmu(hvm);
|
|
if (ret) {
|
|
kfree(hvm);
|
|
return ret;
|
|
}
|
|
|
|
ret = hvm_create_vm_debugfs(hvm);
|
|
if (ret) {
|
|
kfree(hvm);
|
|
return ret;
|
|
}
|
|
|
|
ret = hvm_arch_drv_init(hvm);
|
|
if (ret) {
|
|
kfree(hvm);
|
|
return ret;
|
|
}
|
|
|
|
mutex_lock(&hvm_list_lock);
|
|
list_add(&hvm->vm_list, &hvm_list);
|
|
mutex_unlock(&hvm_list_lock);
|
|
|
|
return anon_inode_getfd("hvm-vm", &hvm_vm_fops, hvm,
|
|
O_RDWR | O_CLOEXEC);
|
|
}
|