blob: 972c2a18ca1b0b9819a8e6ac4f00a710d3386047 [file] [log] [blame] [edit]
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2023-2024 Qualcomm Innovation Center, Inc. All rights reserved.
*/
#define pr_fmt(fmt) "gunyah_vm_mgr: " fmt
#include <asm/gunyah.h>
#include <linux/mm.h>
#include <linux/pagemap.h>
#include "rsc_mgr.h"
#include "vm_mgr.h"
#define WRITE_TAG (1 << 0)
#define SHARE_TAG (1 << 1)
static inline struct gunyah_resource *
__first_resource(struct gunyah_vm_resource_ticket *ticket)
{
return list_first_entry_or_null(&ticket->resources,
struct gunyah_resource, list);
}
int gunyah_vm_parcel_to_paged(struct gunyah_vm *ghvm,
struct gunyah_rm_mem_parcel *parcel, u64 gfn,
u64 nr)
{
struct gunyah_rm_mem_entry *entry;
unsigned long i, tag = 0;
struct folio *folio;
pgoff_t off = 0;
int ret;
if (parcel->n_acl_entries > 1)
tag |= SHARE_TAG;
if (parcel->acl_entries[0].perms & GUNYAH_RM_ACL_W)
tag |= WRITE_TAG;
for (i = 0; i < parcel->n_mem_entries; i++) {
entry = &parcel->mem_entries[i];
folio = pfn_folio(PHYS_PFN(le64_to_cpu(entry->phys_addr)));
ret = mtree_insert_range(&ghvm->mm, gfn + off,
gfn + off + folio_nr_pages(folio) - 1,
xa_tag_pointer(folio, tag),
GFP_KERNEL);
if (ret) {
WARN_ON(ret != -ENOMEM);
gunyah_vm_mm_erase_range(ghvm, gfn, off - 1);
return ret;
}
off += folio_nr_pages(folio);
}
BUG_ON(off != nr);
return 0;
}
/**
* gunyah_vm_mm_erase_range() - Erases a range of folios from ghvm's mm
* @ghvm: gunyah vm
* @gfn: start guest frame number
* @nr: number of pages to erase
*
* Do not use this function unless rolling back gunyah_vm_parcel_to_paged.
*/
void gunyah_vm_mm_erase_range(struct gunyah_vm *ghvm, u64 gfn, u64 nr)
{
struct folio *folio;
u64 off = gfn;
while (off < gfn + nr) {
folio = xa_untag_pointer(mtree_erase(&ghvm->mm, off));
if (!folio)
return;
off += folio_nr_pages(folio);
}
}
static inline u32 donate_flags(bool share)
{
if (share)
return FIELD_PREP_CONST(GUNYAH_MEMEXTENT_OPTION_TYPE_MASK,
GUNYAH_MEMEXTENT_DONATE_TO_SIBLING);
else
return FIELD_PREP_CONST(GUNYAH_MEMEXTENT_OPTION_TYPE_MASK,
GUNYAH_MEMEXTENT_DONATE_TO_PROTECTED);
}
static inline u32 reclaim_flags(bool share, bool sync)
{
u32 flags = 0;
if (share)
flags |= FIELD_PREP_CONST(GUNYAH_MEMEXTENT_OPTION_TYPE_MASK,
GUNYAH_MEMEXTENT_DONATE_TO_SIBLING);
else
flags |= FIELD_PREP_CONST(
GUNYAH_MEMEXTENT_OPTION_TYPE_MASK,
GUNYAH_MEMEXTENT_DONATE_FROM_PROTECTED);
if (!sync)
flags |= GUNYAH_MEMEXTENT_OPTION_NOSYNC;
return flags;
}
int gunyah_vm_provide_folio(struct gunyah_vm *ghvm, struct folio *folio,
u64 gfn, bool share, bool write)
{
struct gunyah_resource *guest_extent, *host_extent, *addrspace;
u32 map_flags = BIT(GUNYAH_ADDRSPACE_MAP_FLAG_PARTIAL);
u64 extent_attrs, gpa = gunyah_gfn_to_gpa(gfn);
phys_addr_t pa = PFN_PHYS(folio_pfn(folio));
enum gunyah_pagetable_access access;
size_t size = folio_size(folio);
enum gunyah_error gunyah_error;
unsigned long tag = 0;
int ret, tmp;
/* clang-format off */
if (share) {
guest_extent = __first_resource(&ghvm->guest_shared_extent_ticket);
host_extent = __first_resource(&ghvm->host_shared_extent_ticket);
} else {
guest_extent = __first_resource(&ghvm->guest_private_extent_ticket);
host_extent = __first_resource(&ghvm->host_private_extent_ticket);
}
/* clang-format on */
addrspace = __first_resource(&ghvm->addrspace_ticket);
if (!addrspace || !guest_extent || !host_extent) {
return -ENODEV;
}
if (share) {
map_flags |= BIT(GUNYAH_ADDRSPACE_MAP_FLAG_VMMIO);
tag |= SHARE_TAG;
} else {
map_flags |= BIT(GUNYAH_ADDRSPACE_MAP_FLAG_PRIVATE);
}
if (write)
tag |= WRITE_TAG;
ret = mtree_insert_range(&ghvm->mm, gfn,
gfn + folio_nr_pages(folio) - 1,
xa_tag_pointer(folio, tag), GFP_KERNEL);
if (ret == -EEXIST)
ret = -EAGAIN;
if (ret)
return ret;
if (share && write)
access = GUNYAH_PAGETABLE_ACCESS_RW;
else if (share && !write)
access = GUNYAH_PAGETABLE_ACCESS_R;
else if (!share && write)
access = GUNYAH_PAGETABLE_ACCESS_RWX;
else /* !share && !write */
access = GUNYAH_PAGETABLE_ACCESS_RX;
ret = gunyah_rm_platform_pre_demand_page(ghvm->rm, ghvm->vmid, access,
folio);
if (ret)
goto reclaim_host;
gunyah_error = gunyah_hypercall_memextent_donate(donate_flags(share),
host_extent->capid,
guest_extent->capid,
pa, size);
if (gunyah_error != GUNYAH_ERROR_OK) {
pr_err("Failed to donate memory for guest address 0x%016llx: %d\n",
gpa, gunyah_error);
ret = gunyah_error_remap(gunyah_error);
goto platform_release;
}
extent_attrs =
FIELD_PREP_CONST(GUNYAH_MEMEXTENT_MAPPING_TYPE,
ARCH_GUNYAH_DEFAULT_MEMTYPE) |
FIELD_PREP(GUNYAH_MEMEXTENT_MAPPING_USER_ACCESS, access) |
FIELD_PREP(GUNYAH_MEMEXTENT_MAPPING_KERNEL_ACCESS, access);
gunyah_error = gunyah_hypercall_addrspace_map(addrspace->capid,
guest_extent->capid, gpa,
extent_attrs, map_flags,
pa, size);
if (gunyah_error != GUNYAH_ERROR_OK) {
pr_err("Failed to map guest address 0x%016llx: %d\n", gpa,
gunyah_error);
ret = gunyah_error_remap(gunyah_error);
goto memextent_reclaim;
}
folio_get(folio);
return 0;
memextent_reclaim:
gunyah_error = gunyah_hypercall_memextent_donate(
reclaim_flags(share, true), guest_extent->capid,
host_extent->capid, pa, size);
if (gunyah_error != GUNYAH_ERROR_OK)
pr_err("Failed to reclaim memory donation for guest address 0x%016llx: %d\n",
gpa, gunyah_error);
platform_release:
tmp = gunyah_rm_platform_reclaim_demand_page(ghvm->rm, ghvm->vmid,
access, folio);
if (tmp) {
pr_err("Platform failed to reclaim memory for guest address 0x%016llx: %d",
gpa, tmp);
return ret;
}
reclaim_host:
gunyah_folio_host_reclaim(folio);
mtree_erase(&ghvm->mm, gfn);
return ret;
}
static int __gunyah_vm_reclaim_folio_locked(struct gunyah_vm *ghvm, void *entry,
u64 gfn, const bool sync)
{
u32 map_flags = BIT(GUNYAH_ADDRSPACE_MAP_FLAG_PARTIAL);
struct gunyah_resource *guest_extent, *host_extent, *addrspace;
enum gunyah_pagetable_access access;
enum gunyah_error gunyah_error;
struct folio *folio;
bool write, share;
phys_addr_t pa;
size_t size;
int ret;
addrspace = __first_resource(&ghvm->addrspace_ticket);
if (!addrspace)
return -ENODEV;
share = !!(xa_pointer_tag(entry) & SHARE_TAG);
write = !!(xa_pointer_tag(entry) & WRITE_TAG);
folio = xa_untag_pointer(entry);
if (!sync)
map_flags |= BIT(GUNYAH_ADDRSPACE_MAP_FLAG_NOSYNC);
/* clang-format off */
if (share) {
guest_extent = __first_resource(&ghvm->guest_shared_extent_ticket);
host_extent = __first_resource(&ghvm->host_shared_extent_ticket);
map_flags |= BIT(GUNYAH_ADDRSPACE_MAP_FLAG_VMMIO);
} else {
guest_extent = __first_resource(&ghvm->guest_private_extent_ticket);
host_extent = __first_resource(&ghvm->host_private_extent_ticket);
map_flags |= BIT(GUNYAH_ADDRSPACE_MAP_FLAG_PRIVATE);
}
/* clang-format on */
pa = PFN_PHYS(folio_pfn(folio));
size = folio_size(folio);
gunyah_error = gunyah_hypercall_addrspace_unmap(addrspace->capid,
guest_extent->capid,
gunyah_gfn_to_gpa(gfn),
map_flags, pa, size);
if (gunyah_error != GUNYAH_ERROR_OK) {
pr_err_ratelimited(
"Failed to unmap guest address 0x%016llx: %d\n",
gunyah_gfn_to_gpa(gfn), gunyah_error);
ret = gunyah_error_remap(gunyah_error);
goto err;
}
gunyah_error = gunyah_hypercall_memextent_donate(
reclaim_flags(share, sync), guest_extent->capid,
host_extent->capid, pa, size);
if (gunyah_error != GUNYAH_ERROR_OK) {
pr_err_ratelimited(
"Failed to reclaim memory donation for guest address 0x%016llx: %d\n",
gunyah_gfn_to_gpa(gfn), gunyah_error);
ret = gunyah_error_remap(gunyah_error);
goto err;
}
if (share && write)
access = GUNYAH_PAGETABLE_ACCESS_RW;
else if (share && !write)
access = GUNYAH_PAGETABLE_ACCESS_R;
else if (!share && write)
access = GUNYAH_PAGETABLE_ACCESS_RWX;
else /* !share && !write */
access = GUNYAH_PAGETABLE_ACCESS_RX;
ret = gunyah_rm_platform_reclaim_demand_page(ghvm->rm, ghvm->vmid,
access, folio);
if (ret) {
pr_err_ratelimited(
"Platform failed to reclaim memory for guest address 0x%016llx: %d",
gunyah_gfn_to_gpa(gfn), ret);
goto err;
}
BUG_ON(mtree_erase(&ghvm->mm, gfn) != entry);
unpin_user_page(folio_page(folio, 0));
account_locked_vm(current->mm, 1, false);
return 0;
err:
return ret;
}
int gunyah_vm_reclaim_folio(struct gunyah_vm *ghvm, u64 gfn, struct folio *folio)
{
void *entry;
entry = mtree_load(&ghvm->mm, gfn);
if (!entry)
return 0;
if (folio != xa_untag_pointer(entry))
return -EAGAIN;
return __gunyah_vm_reclaim_folio_locked(ghvm, entry, gfn, true);
}
int gunyah_vm_reclaim_range(struct gunyah_vm *ghvm, u64 gfn, u64 nr)
{
unsigned long next = gfn, g;
struct folio *folio;
int ret, ret2 = 0;
void *entry;
bool sync;
mt_for_each(&ghvm->mm, entry, next, gfn + nr) {
folio = xa_untag_pointer(entry);
g = next;
sync = !!mt_find_after(&ghvm->mm, &g, gfn + nr);
g = next - folio_nr_pages(folio);
folio_get(folio);
folio_lock(folio);
if (mtree_load(&ghvm->mm, g) == entry)
ret = __gunyah_vm_reclaim_folio_locked(ghvm, entry, g, sync);
else
ret = -EAGAIN;
folio_unlock(folio);
folio_put(folio);
if (ret && ret2 != -EAGAIN)
ret2 = ret;
}
return ret2;
}
int gunyah_vm_binding_alloc(struct gunyah_vm *ghvm,
struct gunyah_userspace_memory_region *region,
bool lend)
{
struct gunyah_vm_gup_binding *binding;
int ret = 0;
if (!region->memory_size || !PAGE_ALIGNED(region->memory_size) ||
!PAGE_ALIGNED(region->userspace_addr) ||
!PAGE_ALIGNED(region->guest_phys_addr))
return -EINVAL;
if (overflows_type(region->guest_phys_addr + region->memory_size, u64))
return -EOVERFLOW;
binding = kzalloc(sizeof(*binding), GFP_KERNEL_ACCOUNT);
if (!binding) {
return -ENOMEM;
}
binding->userspace_addr = region->userspace_addr;
binding->guest_phys_addr = region->guest_phys_addr;
binding->size = region->memory_size;
binding->flags = region->flags;
if (lend) {
binding->share_type = VM_MEM_LEND;
} else {
binding->share_type = VM_MEM_SHARE;
}
down_write(&ghvm->bindings_lock);
ret = mtree_insert_range(&ghvm->bindings,
gunyah_gpa_to_gfn(binding->guest_phys_addr),
gunyah_gpa_to_gfn(binding->guest_phys_addr + region->memory_size - 1),
binding, GFP_KERNEL);
if(ret != 0)
kfree(binding);
up_write(&ghvm->bindings_lock);
return ret;
}
int gunyah_gup_demand_page(struct gunyah_vm *ghvm, u64 gpa, bool write)
{
unsigned long gfn = gunyah_gpa_to_gfn(gpa);
struct gunyah_vm_gup_binding *b;
unsigned int gup_flags;
u64 offset;
int pinned, ret;
struct page *page;
struct folio *folio;
down_read(&ghvm->bindings_lock);
b = mtree_load(&ghvm->bindings, gfn);
if (!b) {
ret = -ENOENT;
goto unlock;
}
if (write && !(b->flags & GUNYAH_MEM_ALLOW_WRITE)) {
ret = -EPERM;
goto unlock;
}
gup_flags = FOLL_LONGTERM;
if (b->flags & GUNYAH_MEM_ALLOW_WRITE)
gup_flags |= FOLL_WRITE;
offset = (gunyah_gfn_to_gpa(gfn) - b->guest_phys_addr);
ret = account_locked_vm(current->mm, 1, true);
if (ret)
goto unlock;
pinned = pin_user_pages_fast(b->userspace_addr + offset, 1,
gup_flags, &page);
if (pinned != 1) {
ret = pinned;
goto unlock_page;
}
folio = page_folio(page);
if (!PageSwapBacked(page)) {
ret = -EIO;
goto unpin_page;
}
folio_lock(folio);
ret = gunyah_vm_provide_folio(ghvm, folio, gfn - folio_page_idx(folio, page),
!(b->share_type == VM_MEM_LEND),
!!(b->flags & GUNYAH_MEM_ALLOW_WRITE));
folio_unlock(folio);
if (ret) {
if (ret != -EAGAIN)
pr_err_ratelimited(
"Failed to provide folio for guest addr: %016llx: %d\n",
gpa, ret);
goto unpin_page;
}
goto unlock;
unpin_page:
unpin_user_page(page);
unlock_page:
account_locked_vm(current->mm, 1, false);
unlock:
up_read(&ghvm->bindings_lock);
return ret;
}
EXPORT_SYMBOL_GPL(gunyah_gup_demand_page);
int gunyah_gup_share_parcel(struct gunyah_vm *ghvm, struct gunyah_rm_mem_parcel *parcel,
u64 *gfn, u64 *nr)
{
struct gunyah_vm_gup_binding *b;
bool lend = false;
struct page **pages;
int pinned, ret;
u16 vmid;
struct folio *folio;
unsigned int gup_flags;
unsigned long i, offset;
parcel->mem_handle = GUNYAH_MEM_HANDLE_INVAL;
if (!*nr)
return -EINVAL;
down_read(&ghvm->bindings_lock);
b = mtree_load(&ghvm->bindings, *gfn);
if (!b) {
ret = -ENOENT;
goto unlock;
}
offset = gunyah_gfn_to_gpa(*gfn) - b->guest_phys_addr;
pages = kcalloc(*nr, sizeof(*pages), GFP_KERNEL_ACCOUNT);
if (!pages) {
ret = -ENOMEM;
goto unlock;
}
gup_flags = FOLL_LONGTERM;
if (b->flags & GUNYAH_MEM_ALLOW_WRITE)
gup_flags |= FOLL_WRITE;
pinned = pin_user_pages_fast(b->userspace_addr + offset, *nr,
gup_flags, pages);
if (pinned < 0) {
ret = pinned;
goto free_pages;
} else if (pinned != *nr) {
ret = -EFAULT;
goto unpin_pages;
}
ret = account_locked_vm(current->mm, pinned, true);
if (ret)
goto unpin_pages;
if (b->share_type == VM_MEM_LEND) {
parcel->n_acl_entries = 1;
lend = true;
} else {
lend = false;
parcel->n_acl_entries = 2;
}
parcel->acl_entries = kcalloc(parcel->n_acl_entries,
sizeof(*parcel->acl_entries), GFP_KERNEL);
if (!parcel->acl_entries) {
ret = -ENOMEM;
goto unaccount_pages;
}
/* acl_entries[0].vmid will be this VM's vmid. We'll fill it when the
* VM is starting and we know the VM's vmid.
*/
parcel->acl_entries[0].vmid = cpu_to_le16(ghvm->vmid);
if (b->flags & GUNYAH_MEM_ALLOW_READ)
parcel->acl_entries[0].perms |= GUNYAH_RM_ACL_R;
if (b->flags & GUNYAH_MEM_ALLOW_WRITE)
parcel->acl_entries[0].perms |= GUNYAH_RM_ACL_W;
if (b->flags & GUNYAH_MEM_ALLOW_EXEC)
parcel->acl_entries[0].perms |= GUNYAH_RM_ACL_X;
if (!lend) {
ret = gunyah_rm_get_vmid(ghvm->rm, &vmid);
if (ret)
goto free_acl;
parcel->acl_entries[1].vmid = cpu_to_le16(vmid);
/* Host assumed to have all these permissions. Gunyah will not
* grant new permissions if host actually had less than RWX
*/
parcel->acl_entries[1].perms = GUNYAH_RM_ACL_R | GUNYAH_RM_ACL_W | GUNYAH_RM_ACL_X;
}
parcel->mem_entries = kcalloc(pinned, sizeof(parcel->mem_entries[0]),
GFP_KERNEL_ACCOUNT);
if (!parcel->mem_entries) {
ret = -ENOMEM;
goto free_acl;
}
folio = page_folio(pages[0]);
*gfn -= folio_page_idx(folio, pages[0]);
parcel->mem_entries[0].size = cpu_to_le64(folio_size(folio));
parcel->mem_entries[0].phys_addr = cpu_to_le64(PFN_PHYS(folio_pfn(folio)));
for (i = 1; i < pinned; i++) {
folio = page_folio(pages[i]);
if (pages[i] == folio_page(folio, 0)) {
parcel->mem_entries[i].size = cpu_to_le64(folio_size(folio));
parcel->mem_entries[i].phys_addr = cpu_to_le64(PFN_PHYS(folio_pfn(folio)));
} else {
unpin_user_page(pages[i]);
account_locked_vm(current->mm, 1, false);
}
}
parcel->n_mem_entries = i;
ret = gunyah_rm_mem_share(ghvm->rm, parcel);
goto free_pages;
free_acl:
kfree(parcel->acl_entries);
parcel->acl_entries = NULL;
kfree(parcel->mem_entries);
parcel->mem_entries = NULL;
parcel->n_mem_entries = 0;
unaccount_pages:
account_locked_vm(current->mm, pinned, false);
unpin_pages:
unpin_user_pages(pages, pinned);
free_pages:
kfree(pages);
unlock:
up_read(&ghvm->bindings_lock);
return ret;
}