| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * ACRN: Memory mapping management |
| * |
| * Copyright (C) 2020 Intel Corporation. All rights reserved. |
| * |
| * Authors: |
| * Fei Li <lei1.li@intel.com> |
| * Shuo Liu <shuo.a.liu@intel.com> |
| */ |
| |
| #include <linux/io.h> |
| #include <linux/mm.h> |
| #include <linux/slab.h> |
| #include <linux/vmalloc.h> |
| |
| #include "acrn_drv.h" |
| |
| static int modify_region(struct acrn_vm *vm, struct vm_memory_region_op *region) |
| { |
| struct vm_memory_region_batch *regions; |
| int ret; |
| |
| regions = kzalloc(sizeof(*regions), GFP_KERNEL); |
| if (!regions) |
| return -ENOMEM; |
| |
| regions->vmid = vm->vmid; |
| regions->regions_num = 1; |
| regions->regions_gpa = virt_to_phys(region); |
| |
| ret = hcall_set_memory_regions(virt_to_phys(regions)); |
| if (ret < 0) |
| dev_dbg(acrn_dev.this_device, |
| "Failed to set memory region for VM[%u]!\n", vm->vmid); |
| |
| kfree(regions); |
| return ret; |
| } |
| |
| /** |
| * acrn_mm_region_add() - Set up the EPT mapping of a memory region. |
| * @vm: User VM. |
| * @user_gpa: A GPA of User VM. |
| * @service_gpa: A GPA of Service VM. |
| * @size: Size of the region. |
| * @mem_type: Combination of ACRN_MEM_TYPE_*. |
| * @mem_access_right: Combination of ACRN_MEM_ACCESS_*. |
| * |
| * Return: 0 on success, <0 on error. |
| */ |
| int acrn_mm_region_add(struct acrn_vm *vm, u64 user_gpa, u64 service_gpa, |
| u64 size, u32 mem_type, u32 mem_access_right) |
| { |
| struct vm_memory_region_op *region; |
| int ret = 0; |
| |
| region = kzalloc(sizeof(*region), GFP_KERNEL); |
| if (!region) |
| return -ENOMEM; |
| |
| region->type = ACRN_MEM_REGION_ADD; |
| region->user_vm_pa = user_gpa; |
| region->service_vm_pa = service_gpa; |
| region->size = size; |
| region->attr = ((mem_type & ACRN_MEM_TYPE_MASK) | |
| (mem_access_right & ACRN_MEM_ACCESS_RIGHT_MASK)); |
| ret = modify_region(vm, region); |
| |
| dev_dbg(acrn_dev.this_device, |
| "%s: user-GPA[%pK] service-GPA[%pK] size[0x%llx].\n", |
| __func__, (void *)user_gpa, (void *)service_gpa, size); |
| kfree(region); |
| return ret; |
| } |
| |
| /** |
| * acrn_mm_region_del() - Del the EPT mapping of a memory region. |
| * @vm: User VM. |
| * @user_gpa: A GPA of the User VM. |
| * @size: Size of the region. |
| * |
| * Return: 0 on success, <0 for error. |
| */ |
| int acrn_mm_region_del(struct acrn_vm *vm, u64 user_gpa, u64 size) |
| { |
| struct vm_memory_region_op *region; |
| int ret = 0; |
| |
| region = kzalloc(sizeof(*region), GFP_KERNEL); |
| if (!region) |
| return -ENOMEM; |
| |
| region->type = ACRN_MEM_REGION_DEL; |
| region->user_vm_pa = user_gpa; |
| region->service_vm_pa = 0UL; |
| region->size = size; |
| region->attr = 0U; |
| |
| ret = modify_region(vm, region); |
| |
| dev_dbg(acrn_dev.this_device, "%s: user-GPA[%pK] size[0x%llx].\n", |
| __func__, (void *)user_gpa, size); |
| kfree(region); |
| return ret; |
| } |
| |
| int acrn_vm_memseg_map(struct acrn_vm *vm, struct acrn_vm_memmap *memmap) |
| { |
| int ret; |
| |
| if (memmap->type == ACRN_MEMMAP_RAM) |
| return acrn_vm_ram_map(vm, memmap); |
| |
| if (memmap->type != ACRN_MEMMAP_MMIO) { |
| dev_dbg(acrn_dev.this_device, |
| "Invalid memmap type: %u\n", memmap->type); |
| return -EINVAL; |
| } |
| |
| ret = acrn_mm_region_add(vm, memmap->user_vm_pa, |
| memmap->service_vm_pa, memmap->len, |
| ACRN_MEM_TYPE_UC, memmap->attr); |
| if (ret < 0) |
| dev_dbg(acrn_dev.this_device, |
| "Add memory region failed, VM[%u]!\n", vm->vmid); |
| |
| return ret; |
| } |
| |
| int acrn_vm_memseg_unmap(struct acrn_vm *vm, struct acrn_vm_memmap *memmap) |
| { |
| int ret; |
| |
| if (memmap->type != ACRN_MEMMAP_MMIO) { |
| dev_dbg(acrn_dev.this_device, |
| "Invalid memmap type: %u\n", memmap->type); |
| return -EINVAL; |
| } |
| |
| ret = acrn_mm_region_del(vm, memmap->user_vm_pa, memmap->len); |
| if (ret < 0) |
| dev_dbg(acrn_dev.this_device, |
| "Del memory region failed, VM[%u]!\n", vm->vmid); |
| |
| return ret; |
| } |
| |
| /** |
| * acrn_vm_ram_map() - Create a RAM EPT mapping of User VM. |
| * @vm: The User VM pointer |
| * @memmap: Info of the EPT mapping |
| * |
| * Return: 0 on success, <0 for error. |
| */ |
| int acrn_vm_ram_map(struct acrn_vm *vm, struct acrn_vm_memmap *memmap) |
| { |
| struct vm_memory_region_batch *regions_info; |
| int nr_pages, i, order, nr_regions = 0; |
| struct vm_memory_mapping *region_mapping; |
| struct vm_memory_region_op *vm_region; |
| struct page **pages = NULL, *page; |
| void *remap_vaddr; |
| int ret, pinned; |
| u64 user_vm_pa; |
| struct vm_area_struct *vma; |
| |
| if (!vm || !memmap) |
| return -EINVAL; |
| |
| /* Get the page number of the map region */ |
| nr_pages = memmap->len >> PAGE_SHIFT; |
| if (!nr_pages) |
| return -EINVAL; |
| |
| mmap_read_lock(current->mm); |
| vma = vma_lookup(current->mm, memmap->vma_base); |
| if (vma && ((vma->vm_flags & VM_PFNMAP) != 0)) { |
| unsigned long start_pfn, cur_pfn; |
| bool writable; |
| |
| if ((memmap->vma_base + memmap->len) > vma->vm_end) { |
| mmap_read_unlock(current->mm); |
| return -EINVAL; |
| } |
| |
| for (i = 0; i < nr_pages; i++) { |
| struct follow_pfnmap_args args = { |
| .vma = vma, |
| .address = memmap->vma_base + i * PAGE_SIZE, |
| }; |
| |
| ret = follow_pfnmap_start(&args); |
| if (ret) |
| break; |
| |
| cur_pfn = args.pfn; |
| if (i == 0) |
| start_pfn = cur_pfn; |
| writable = args.writable; |
| follow_pfnmap_end(&args); |
| |
| /* Disallow write access if the PTE is not writable. */ |
| if (!writable && |
| (memmap->attr & ACRN_MEM_ACCESS_WRITE)) { |
| ret = -EFAULT; |
| break; |
| } |
| |
| /* Disallow refcounted pages. */ |
| if (pfn_valid(cur_pfn) && |
| !PageReserved(pfn_to_page(cur_pfn))) { |
| ret = -EFAULT; |
| break; |
| } |
| |
| /* Disallow non-contiguous ranges. */ |
| if (cur_pfn != start_pfn + i) { |
| ret = -EINVAL; |
| break; |
| } |
| } |
| mmap_read_unlock(current->mm); |
| |
| if (ret) { |
| dev_dbg(acrn_dev.this_device, |
| "Failed to lookup PFN at VMA:%pK.\n", (void *)memmap->vma_base); |
| return ret; |
| } |
| |
| return acrn_mm_region_add(vm, memmap->user_vm_pa, |
| PFN_PHYS(start_pfn), memmap->len, |
| ACRN_MEM_TYPE_WB, memmap->attr); |
| } |
| mmap_read_unlock(current->mm); |
| |
| pages = vzalloc(array_size(nr_pages, sizeof(*pages))); |
| if (!pages) |
| return -ENOMEM; |
| |
| /* Lock the pages of user memory map region */ |
| pinned = pin_user_pages_fast(memmap->vma_base, |
| nr_pages, FOLL_WRITE | FOLL_LONGTERM, |
| pages); |
| if (pinned < 0) { |
| ret = pinned; |
| goto free_pages; |
| } else if (pinned != nr_pages) { |
| ret = -EFAULT; |
| goto put_pages; |
| } |
| |
| /* Create a kernel map for the map region */ |
| remap_vaddr = vmap(pages, nr_pages, VM_MAP, PAGE_KERNEL); |
| if (!remap_vaddr) { |
| ret = -ENOMEM; |
| goto put_pages; |
| } |
| |
| /* Record Service VM va <-> User VM pa mapping */ |
| mutex_lock(&vm->regions_mapping_lock); |
| region_mapping = &vm->regions_mapping[vm->regions_mapping_count]; |
| if (vm->regions_mapping_count < ACRN_MEM_MAPPING_MAX) { |
| region_mapping->pages = pages; |
| region_mapping->npages = nr_pages; |
| region_mapping->size = memmap->len; |
| region_mapping->service_vm_va = remap_vaddr; |
| region_mapping->user_vm_pa = memmap->user_vm_pa; |
| vm->regions_mapping_count++; |
| } else { |
| dev_warn(acrn_dev.this_device, |
| "Run out of memory mapping slots!\n"); |
| ret = -ENOMEM; |
| mutex_unlock(&vm->regions_mapping_lock); |
| goto unmap_no_count; |
| } |
| mutex_unlock(&vm->regions_mapping_lock); |
| |
| /* Calculate count of vm_memory_region_op */ |
| for (i = 0; i < nr_pages; i += 1 << order) { |
| page = pages[i]; |
| VM_BUG_ON_PAGE(PageTail(page), page); |
| order = compound_order(page); |
| nr_regions++; |
| } |
| |
| /* Prepare the vm_memory_region_batch */ |
| regions_info = kzalloc(struct_size(regions_info, regions_op, |
| nr_regions), GFP_KERNEL); |
| if (!regions_info) { |
| ret = -ENOMEM; |
| goto unmap_kernel_map; |
| } |
| regions_info->regions_num = nr_regions; |
| |
| /* Fill each vm_memory_region_op */ |
| vm_region = regions_info->regions_op; |
| regions_info->vmid = vm->vmid; |
| regions_info->regions_gpa = virt_to_phys(vm_region); |
| user_vm_pa = memmap->user_vm_pa; |
| for (i = 0; i < nr_pages; i += 1 << order) { |
| u32 region_size; |
| |
| page = pages[i]; |
| VM_BUG_ON_PAGE(PageTail(page), page); |
| order = compound_order(page); |
| region_size = PAGE_SIZE << order; |
| vm_region->type = ACRN_MEM_REGION_ADD; |
| vm_region->user_vm_pa = user_vm_pa; |
| vm_region->service_vm_pa = page_to_phys(page); |
| vm_region->size = region_size; |
| vm_region->attr = (ACRN_MEM_TYPE_WB & ACRN_MEM_TYPE_MASK) | |
| (memmap->attr & ACRN_MEM_ACCESS_RIGHT_MASK); |
| |
| vm_region++; |
| user_vm_pa += region_size; |
| } |
| |
| /* Inform the ACRN Hypervisor to set up EPT mappings */ |
| ret = hcall_set_memory_regions(virt_to_phys(regions_info)); |
| if (ret < 0) { |
| dev_dbg(acrn_dev.this_device, |
| "Failed to set regions, VM[%u]!\n", vm->vmid); |
| goto unset_region; |
| } |
| kfree(regions_info); |
| |
| dev_dbg(acrn_dev.this_device, |
| "%s: VM[%u] service-GVA[%pK] user-GPA[%pK] size[0x%llx]\n", |
| __func__, vm->vmid, |
| remap_vaddr, (void *)memmap->user_vm_pa, memmap->len); |
| return ret; |
| |
| unset_region: |
| kfree(regions_info); |
| unmap_kernel_map: |
| mutex_lock(&vm->regions_mapping_lock); |
| vm->regions_mapping_count--; |
| mutex_unlock(&vm->regions_mapping_lock); |
| unmap_no_count: |
| vunmap(remap_vaddr); |
| put_pages: |
| for (i = 0; i < pinned; i++) |
| unpin_user_page(pages[i]); |
| free_pages: |
| vfree(pages); |
| return ret; |
| } |
| |
| /** |
| * acrn_vm_all_ram_unmap() - Destroy a RAM EPT mapping of User VM. |
| * @vm: The User VM |
| */ |
| void acrn_vm_all_ram_unmap(struct acrn_vm *vm) |
| { |
| struct vm_memory_mapping *region_mapping; |
| int i, j; |
| |
| mutex_lock(&vm->regions_mapping_lock); |
| for (i = 0; i < vm->regions_mapping_count; i++) { |
| region_mapping = &vm->regions_mapping[i]; |
| vunmap(region_mapping->service_vm_va); |
| for (j = 0; j < region_mapping->npages; j++) |
| unpin_user_page(region_mapping->pages[j]); |
| vfree(region_mapping->pages); |
| } |
| mutex_unlock(&vm->regions_mapping_lock); |
| } |