From: Alexandru Elisei <alexandru.elisei@arm.com>
To: Sean Christopherson <seanjc@google.com>
Cc: pbonzini@redhat.com, kvm@vger.kernel.org,
david.hildenbrand@arm.com, maz@kernel.org, oupton@kernel.org,
joey.gouly@arm.com, seiden@linux.ibm.com, suzuki.poulose@arm.com,
yuzenghui@huawei.com, linux-arm-kernel@lists.infradead.org,
kvmarm@lists.linux.dev, fuad.tabba@linux.dev,
mark.rutland@arm.com
Subject: Re: [RFC PATCH 2/3] KVM: Implement dirty page logging for guest_memfd-only memslots
Date: Tue, 7 Jul 2026 18:12:20 +0100 [thread overview]
Message-ID: <ak0zdNr1LvuF7gvm@raptor> (raw)
In-Reply-To: <akxWZ1NTX4DHnUzf@google.com>
Hi Sean,
On Mon, Jul 06, 2026 at 06:29:11PM -0700, Sean Christopherson wrote:
> On Thu, Jul 02, 2026, Alexandru Elisei wrote:
> > The entire memory represented by guest_memfd-only memslot is shared and
> > accessible by userspace.
>
> ...
>
> > +8.48 KVM_CAP_GUEST_MEMFD_MMAP_LOG_DIRTY_PAGES
> > +---------------------------------------------
> > +
> > +:Architectures: all
> > +
> > +The presence of this capability indicates that memslots backed by a guest_memfd
> > +file descriptor created with the GUEST_MEMFD_FLAG_MMAP flag can have dirty
> > +page logging enabled.
>
> What does mmap() have to do with anything? Supporting mmap() doesn't guarantee
> the memory is shared, and I can't think of any dependency on memory actually
> being mapped into userspace.
My bad, it should have been GUEST_MEMFD_FLAG_MMAP +
GUEST_MEMFD_FLAG_INIT_SHARED. I'm not sure what you mean by "dependency on
memory actually being mapped into userspace".
From my point of view, it only makes sense to enable dirty page logging if
the contents of the memory is accessible to userspace, hence I made dirty
page logging depend on userspace having the option to access the memory.
This can only happen if the guest_memfd file is mmap'able and accessible by
userspace. But it doesn't force userspace to actually have the memory
mapped to allow the log dirty pages flag to be set for a guest_memfd backed
memslot. Hm.. now that I think about it, maybe I should have made depend on
guest_memfd also having been created as shared? Though I think that can be
changed with KVM_SET_MEMORY_ATTRIBUTES on x86.
Does that answer your question?
>
> > diff --git a/virt/kvm/guest_memfd.c b/virt/kvm/guest_memfd.c
> > index 43ef8e908aaf..210bdd76f0aa 100644
> > --- a/virt/kvm/guest_memfd.c
> > +++ b/virt/kvm/guest_memfd.c
> > @@ -622,6 +622,11 @@ bool __weak kvm_arch_supports_gmem_init_shared(struct kvm *kvm)
> > return true;
> > }
> >
> > +bool __weak kvm_arch_supports_gmem_mmap_dirty_logging(struct kvm *kvm)
> > +{
> > + return false;
> > +}
> > +
> > static int __kvm_gmem_create(struct kvm *kvm, loff_t size, u64 flags)
> > {
> > static const char *name = "[kvm-gmem]";
> > @@ -705,6 +710,66 @@ int kvm_gmem_create(struct kvm *kvm, struct kvm_create_guest_memfd *args)
> > return __kvm_gmem_create(kvm, size, flags);
> > }
> >
> > +static int __kvm_gmem_check_no_change(struct kvm *kvm, struct kvm_memory_slot *old,
> > + struct file *old_file, unsigned int fd,
> > + loff_t offset)
> > +{
> > + struct file *new_file;
> > +
> > + new_file = fget(fd);
> > + if (!new_file)
> > + return -EBADF;
> > + if (new_file != old_file) {
>
> There's a TOCTOU issue here, no? Nothing prevents userspace from deleting and
> replacing the old guest_memfd instance between now and when the re-binding
> happens.
When userspace closes a guest_memfd file, for all memslots that use that
file, slot->gmem.file is set to NULL in kvm_gmem_release().
kvm_gmem_release() takes the kvm->slots_lock(), so I don't think changes to
memslots can run concurrently with a guest_memfd instance being closed -
i.e, if a guest_memfd file has been closed then the subsequent memslot
update will see slot->gmem.file = NULL. Or if a memslot update is under
way, slot->gmem.file won't be made NULL until the memslot update completes.
Unless I misunderstood what you were saying.
> Ah, no, because the check in kvm_set_memory_region() is only to check
> for a "nop" update. I think we should continue to disallow such "updates", I
Sure, I added this here because calling the legacy
KVM_SET_USER_MEMORY_REGION with the same values for the struct
kvm_user_memory_region fields does not return an error.
> can't think of any reasonable use case, and then we can fold this helper into
> its sole remaining caller.
>
> > + fput(new_file);
> > + return -EBADF;
> > + }
> > + fput(new_file);
> > +
> > + if (old->gmem.pgoff != offset >> PAGE_SHIFT)
>
> This can and should be handled in common KVM, not in guest_memfd. It's a
> property of the memslot, not of the gmem instance.
Sure.
>
> > + return -EINVAL;
> > +
> > + return 0;
> > +}
> > +int kvm_gmem_change_flags(struct kvm *kvm, struct kvm_memory_slot *old,
>
> Hmm, if we use a separate helper, I think this should be phrased in terms of
> commands to guest_memfd, not in terms of why common KVM is making changes.
> guest_memfd shouldn't have to care *why* it's being asked to re-bind to a
> different memslot.
>
> Alternatively, provide kvm_gmem_commit_memory_region() and pass in a
> kvm_mr_change param, but that gets weird since the unbind() case needs to be
> handled even without an explicit DELETE.
The way I see it, guest_memfd is not rebinding to a new memslot, since
f->bindings are not changed at any point in the handling of the
KVM_SET_USER_MEMORY_REGION2 call if only the flags are changed.
The function was initially supposed to validate that this indeed is a flag
only change, which means that the guest_memfd fd and file offset are
unchanged. But then I added the update to new->flags to set the
KVM_MEMSLOT_GMEM_ONLY flag. I guess I could split it into
kvm_gmem_check_flags_update() and kvm_gmem_copy_flags()?
Also, I think the best place for both functions would be in
kvm_set_memory_region():
* That's where the validity of a change to a memslot is checked.
* That's where new->flags is copied from kvm_userspace_memory_region2->flags.
What do you think? Does that make sense?
>
> > @@ -734,6 +799,11 @@ int kvm_gmem_bind(struct kvm *kvm, struct kvm_memory_slot *slot,
> > if (!PAGE_ALIGNED(offset) || offset + size > i_size_read(inode))
> > goto err;
> >
> > + if (slot->flags & KVM_MEM_LOG_DIRTY_PAGES &&
> > + (!kvm_gmem_supports_mmap(inode) ||
> > + !kvm_arch_supports_gmem_mmap_dirty_logging(kvm)))
>
> I think I would rather handle this in kvm_arch_prepare_memory_region(). AFAIK,
> arm64 and x86 are the only architectures that support using gmem for private
> memory, and so are the only architectures that would need to restrict dirty
> logging (TDX needs additional plumbiong). It'd mean updating x86 at the same
> time, but that should be relatively straighforward.
Sure, I'll give it a go.
>
> That would mean we couldn't handle the check in check_memory_region_flags(), but
> that should be a non-issue, e.g. arm64 and RISC-V already put additional
> restrictions on what can regions be dirty-logged.
>
> > @@ -1739,16 +1739,6 @@ static void kvm_commit_memory_region(struct kvm *kvm,
> > */
> > if (old->dirty_bitmap && !new->dirty_bitmap)
> > kvm_destroy_dirty_bitmap(old);
> > -
> > - /*
> > - * Unbind the guest_memfd instance as needed; the @new slot has
> > - * already created its own binding. TODO: Drop the WARN when
> > - * dirty logging guest_memfd memslots is supported. Until then,
> > - * flags-only changes on guest_memfd slots should be impossible.
> > - */
> > - if (WARN_ON_ONCE(old->flags & KVM_MEM_GUEST_MEMFD))
> > - kvm_gmem_unbind(old);
> > -
> > /*
> > * The final quirk. Free the detached, old slot, but only its
> > * memory, not any metadata. Metadata, including arch specific
> > @@ -2073,22 +2063,27 @@ static int kvm_set_memory_region(struct kvm *kvm,
> > if ((kvm->nr_memslot_pages + npages) < kvm->nr_memslot_pages)
> > return -EINVAL;
> > } else { /* Modify an existing slot. */
> > - /* Private memslots are immutable, they can only be deleted. */
> > - if (mem->flags & KVM_MEM_GUEST_MEMFD)
> > - return -EINVAL;
> > if ((mem->userspace_addr != old->userspace_addr) ||
> > (npages != old->npages) ||
> > ((mem->flags ^ old->flags) & (KVM_MEM_READONLY | KVM_MEM_GUEST_MEMFD)))
> > return -EINVAL;
> >
> > - if (base_gfn != old->base_gfn)
> > + if (base_gfn != old->base_gfn) {
> > change = KVM_MR_MOVE;
> > - else if (mem->flags != old->flags)
> > + } else if (mem->flags != (old->flags & MEMSLOT_USER_FLAGS_MASK)) {
> > change = KVM_MR_FLAGS_ONLY;
> > - else /* Nothing to change. */
> > + } else if (mem->flags & KVM_MEM_GUEST_MEMFD) {
> > + return kvm_gmem_check_no_change(kvm, old, mem->guest_memfd,
> > + mem->guest_memfd_offset);
>
> As above, just return -EINVAL.
Sure.
>
> > + } else {
> > return 0;
> > + }
> > }
> >
> > + if (mem->flags & KVM_MEM_GUEST_MEMFD &&
> > + change != KVM_MR_CREATE && change != KVM_MR_FLAGS_ONLY)
>
> This is a *very* convoluted way of disallowing MOVE. Handle this above. E.g.o
>
> if (base_gfn != old->base_gfn) {
> /* KVM doesn't support moving guest_memfd bindings. */
> if (mem->flags & KVM_MEM_GUEST_MEMFD)
> return -EINVAL;
>
> change = KVM_MR_MOVE;
> } else if (mem->flags != (old->flags & MEMSLOT_USER_FLAGS_MASK)) {
> if (mem->flags & KVM_MEM_GUEST_MEMFD &&
> old->gmem.pgoff != mem->guest_memfd_offset)
> change = KVM_MR_FLAGS_ONLY;
> } else if (mem->flags & KVM_MEM_GUEST_MEMFD) {
> return -EINVAL;
> } else {
> return 0;
> }
Sure, looks better.
>
> > + return -EINVAL;
> > +
> > if ((change == KVM_MR_CREATE || change == KVM_MR_MOVE) &&
> > kvm_check_memslot_overlap(slots, id, base_gfn, base_gfn + npages))
> > return -EEXIST;
> > @@ -2105,7 +2100,12 @@ static int kvm_set_memory_region(struct kvm *kvm,
> > new->flags = mem->flags;
> > new->userspace_addr = mem->userspace_addr;
> > if (mem->flags & KVM_MEM_GUEST_MEMFD) {
> > - r = kvm_gmem_bind(kvm, new, mem->guest_memfd, mem->guest_memfd_offset);
> > + if (change == KVM_MR_CREATE) {
>
> Curly braces aren't needed.
Sure, I'll remove them, I put them there because the block for
KVM_MR_FLAGS_ONLY, which, even though has only an instruction, spans two
lines.
>
> > + r = kvm_gmem_bind(kvm, new, mem->guest_memfd, mem->guest_memfd_offset);
> > + } else if (change == KVM_MR_FLAGS_ONLY) {
> > + r = kvm_gmem_change_flags(kvm, old, new, mem->guest_memfd,
> > + mem->guest_memfd_offset);
> > + }
> > if (r)
> > goto out;
> > }
> > @@ -2117,7 +2117,7 @@ static int kvm_set_memory_region(struct kvm *kvm,
> > return 0;
> >
> > out_unbind:
> > - if (mem->flags & KVM_MEM_GUEST_MEMFD)
> > + if ((mem->flags & KVM_MEM_GUEST_MEMFD) && change == KVM_MR_CREATE)
> > kvm_gmem_unbind(new);
>
> This is wrong. If kvm_set_memslot() failed, the old memslot needs to be bound
> back to the guest_memfd instance. Hmm, but KVM can't guarantee success. So
The old memslot is still bound to guest_memfd in the sense that f->bindings
still has the same memslot id+as_id assigned to the memslot's gpa range.
There's no rebinding happening when flags are changed because the memslot's
id + as_id are not changed.
I think we might not be on the same page regarding what the previous patch
does, that is where I've changed how guest_memfd keeps track of the
memslots. Instead of storing a pointer, it now stores the memslot id and
as_id, and uses a search in the active memslots (kvm->memslots) to get the
active memslot by using id and as_id:
slot = id_to_memslot(__kvm_memslots(kvm, as_id), id)
If userspace punches a hole or kvm_gmem_error_folio() is called while a
memslot flags update is in progress, __kvm_gmem_invalidate_{start,end}()
might observe the old memslot (with the old flags), but I believe that's
ok, because that's also what happens when the MMU notifiers trigger
invalidation during a memslot update.
> unless there's reason why the bind() can't happen under slots_arch_lock, I think
> the way to handle this is to only bind once success is guaranteed. It'll require
> plumbing the fd+offset into kvm_set_memslot().
>
> Or I guess add the "fd" to kvm_memory_slot.gmem? I kinda like that, because then
> we can require that userspace really is just updating flags, and not switching
> the fd (to a the same file).
>
>
> E.g. something like this?
>
> diff --git include/linux/kvm_host.h include/linux/kvm_host.h
> index ab8cfaec82d3..82385eb9a82e 100644
> --- include/linux/kvm_host.h
> +++ include/linux/kvm_host.h
> @@ -610,6 +610,7 @@ struct kvm_memory_slot {
> * reference via kvm_gmem_get_file() is protected by
> * either kvm->slots_lock or kvm->srcu.
> */
> + int fd;
> struct file *file;
> pgoff_t pgoff;
> } gmem;
> diff --git virt/kvm/kvm_main.c virt/kvm/kvm_main.c
> index e44c20c04961..0729e7c94816 100644
> --- virt/kvm/kvm_main.c
> +++ virt/kvm/kvm_main.c
> @@ -1946,6 +1946,20 @@ static int kvm_set_memslot(struct kvm *kvm,
> return r;
> }
>
> + if (new->flags & KVM_MEM_GUEST_MEMFD) {
> + if (change == KVM_MR_CREATE)
> + r = kvm_gmem_bind(...);
> + else if (WARN_ON_ONCE(change == KVM_MR_MOVE))
> + r = -EINVAL;
> + else if (change == KVM_MR_FLAGS_ONLY)
> + r = kvm_gmem_rebind(...);
> +
> + if (r) {
> + mutex_unlock(&kvm->slots_arch_lock);
> + return r;
> + }
> + }
> +
> /*
> * For DELETE and MOVE, the working slot is now active as the INVALID
> * version of the old slot. MOVE is particularly special as it reuses
> @@ -2104,21 +2118,14 @@ static int kvm_set_memory_region(struct kvm *kvm,
> new->npages = npages;
> new->flags = mem->flags;
> new->userspace_addr = mem->userspace_addr;
> - if (mem->flags & KVM_MEM_GUEST_MEMFD) {
> - r = kvm_gmem_bind(kvm, new, mem->guest_memfd, mem->guest_memfd_offset);
> - if (r)
> - goto out;
> - }
> + new->gmem.fd = mem->guest_memfd;
> + new->gmem.pgoff = mem->guest_memfd_offset >> PAGE_SHIFT;
>
> r = kvm_set_memslot(kvm, old, new, change);
> if (r)
> - goto out_unbind;
> + goto out;
>
> return 0;
> -
> -out_unbind:
> - if (mem->flags & KVM_MEM_GUEST_MEMFD)
> - kvm_gmem_unbind(new);
> out:
> kfree(new);
> return r;
I can explore that, for sure, thank you for the review!
Alex
next prev parent reply other threads:[~2026-07-07 17:12 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-02 14:29 [RFC PATCH 0/3] KVM: Dirty page logging for guest_memfd-only memslots Alexandru Elisei
2026-07-02 14:29 ` [RFC PATCH 1/3] KVM: guest_memfd: Use memslot id to keep track of associated memslots Alexandru Elisei
2026-07-02 14:47 ` sashiko-bot
2026-07-02 16:09 ` Alexandru Elisei
2026-07-06 7:14 ` David Hildenbrand
2026-07-06 13:45 ` Alexandru Elisei
2026-07-06 21:46 ` Sean Christopherson
2026-07-07 17:05 ` Alexandru Elisei
2026-07-06 21:43 ` Sean Christopherson
2026-07-07 17:05 ` Alexandru Elisei
2026-07-02 14:29 ` [RFC PATCH 2/3] KVM: Implement dirty page logging for guest_memfd-only memslots Alexandru Elisei
2026-07-07 1:29 ` Sean Christopherson
2026-07-07 17:12 ` Alexandru Elisei [this message]
2026-07-02 14:29 ` [RFC PATCH 3/3] KVM: arm64: Allow " Alexandru Elisei
2026-07-07 0:56 ` [RFC PATCH 0/3] KVM: Dirty " Sean Christopherson
2026-07-07 16:58 ` Alexandru Elisei
2026-07-07 17:12 ` Sean Christopherson
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=ak0zdNr1LvuF7gvm@raptor \
--to=alexandru.elisei@arm.com \
--cc=david.hildenbrand@arm.com \
--cc=fuad.tabba@linux.dev \
--cc=joey.gouly@arm.com \
--cc=kvm@vger.kernel.org \
--cc=kvmarm@lists.linux.dev \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=mark.rutland@arm.com \
--cc=maz@kernel.org \
--cc=oupton@kernel.org \
--cc=pbonzini@redhat.com \
--cc=seanjc@google.com \
--cc=seiden@linux.ibm.com \
--cc=suzuki.poulose@arm.com \
--cc=yuzenghui@huawei.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.