Re: [RFC] KVM: mm: fd-based approach for supporting KVM guest private memory
From: Sean Christopherson
Date: Thu Sep 02 2021 - 16:33:41 EST
On Thu, Sep 02, 2021, Kirill A. Shutemov wrote:
> Hi folks,
>
> I try to sketch how the memfd changes would look like.
>
> I've added F_SEAL_GUEST. The new seal is only allowed if there's no
> pre-existing pages in the fd (i_mapping->nrpages check) and there's
> no existing mapping of the file (RB_EMPTY_ROOT(&i_mapping->i_mmap.rb_root check).
>
> After the seal is set, no read/write/mmap from userspace is allowed.
>
> Although it's not clear how to serialize read check vs. seal setup: seal
> is protected with inode_lock() which we don't hold in read path because it
> is expensive. I don't know yet how to get it right. For TDX, it's okay to
> allow read as it cannot trigger #MCE. Maybe we can allow it?
Would requiring the size to be '0' at F_SEAL_GUEST time solve that problem?
> Truncate and punch hole are tricky.
>
> We want to allow it to save memory if substantial range is converted to
> shared. Partial truncate and punch hole effectively writes zeros to
> partially truncated page and may lead to #MCE. We can reject any partial
> truncate/punch requests, but it doesn't help the situation with THPs.
>
> If we truncate to the middle of THP page, we try to split it into small
> pages and proceed as usual for small pages. But split is allowed to fail.
> If it happens we zero part of THP.
> I guess we may reject truncate if split fails. It should work fine if we
> only use it for saving memory.
FWIW, splitting a THP will also require a call into KVM to demote the huge page
to the equivalent small pages.
> We need to modify truncation/punch path to notify kvm that pages are about
> to be freed. I think we will register callback in the memfd on adding the
> fd to KVM memslot that going to be called for the notification. That means
> 1:1 between memfd and memslot. I guess it's okay.
Hmm, 1:1 memfd to memslot will be problematic as that would prevent punching a
hole in KVM's memslots, e.g. to convert a subset to shared. It would also
disallow backing guest memory with a single memfd that's split across two
memslots for <4gb and >4gb.
But I don't think we need a 1:1 relationship. To keep KVM sane, we can require
each private memslot to be wholly contained in a single memfd, I can't think of
any reason that would be problematic for userspace.
For the callbacks, I believe the rule should be 1:1 between memfd and KVM instance.
That would allow mapping multiple memslots to a single memfd so long as they're
all coming from the same KVM instance.
> Migration going to always fail on F_SEAL_GUEST for now. Can be modified to
> use a callback in the future.
>
> Swapout will also always fail on F_SEAL_GUEST. It seems trivial. Again, it
> can be a callback in the future.
>
> For GPA->PFN translation KVM could use vm_ops->fault(). Semantically it is
> a good fit, but we don't have any VMAs around and ->mmap is forbidden for
> F_SEAL_GUEST.
> Other option is call shmem_getpage() directly, but it looks like a
> layering violation to me. And it's not available to modules :/
My idea for this was to have the memfd:KVM exchange callbacks, i.e. memfd would
have callbacks into KVM, but KVM would also have callbacks into memfd. To avoid
circular refcounts, KVM would hold a reference to the memfd (since it's the
instigator) and KVM would be responsible for unregistering itself before freeing
it's reference to the memfd.
The memfd callbacks would be tracked per private memslot, which meshes nicely
without how KVM uses memslots to translate gfn->pfn. In effect, the ops pointer
in the memslots replaces the host virtual address that's used to get the pfn for
non-private memslots.
@@ -2428,8 +2453,12 @@ kvm_pfn_t __gfn_to_pfn_memslot(struct kvm_memory_slot *slot, gfn_t gfn,
bool atomic, bool *async, bool write_fault,
bool *writable, hva_t *hva)
{
- unsigned long addr = __gfn_to_hva_many(slot, gfn, NULL, write_fault);
+ unsigned long addr;
+ if (memslot_is_private(slot))
+ return slot->private_ops->gfn_to_pfn(...);
+
+ addr = __gfn_to_hva_many(slot, gfn, NULL, write_fault);
if (hva)
*hva = addr;
>
> Any comments?
>
> --
> Kirill A. Shutemov