From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
To: Konstantin Khorenko <khorenko@virtuozzo.com>,
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>,
Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
Cc: devel@openvz.org
Subject: Re: [Devel] [PATCH hci-8.1 1/2] mm: allow MADV_DOEXEC on shared PFN mappings of device memory
Date: Fri, 16 Oct 2026 15:57:50 +0300 [thread overview]
Message-ID: <61fe3d17-c8b9-4a01-b3b1-b5d2cda767f1@virtuozzo.com> (raw)
In-Reply-To: <20261014-b4-prep-khorenko-vstor-103860-4-doexec-vfio-bars-v1-1-7be6c4d68114@virtuozzo.com>
On 10/14/26 8:14 PM, Konstantin Khorenko wrote:
> From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
>
> QEMU live update (cpr-exec) keeps guest RAM across exec with MADV_DOEXEC,
> but VFIO device BARs mapped into QEMU are lost on exec, and the new QEMU
> has to mmap them again on the same device fd.
>
> Some drivers don't allow that. The NVIDIA vGPU VFIO driver records each
> BAR mapping and refuses an overlapping one with -EINVAL. For VF BARs the
> record is dropped only when the device fd is closed, so after exec the
> new QEMU can't map BAR1/BAR3 and the vGPU becomes unusable.
>
> Let MADV_DOEXEC also preserve shared VM_PFNMAP mappings of a VFIO device
> file, the BARs. vma_dup() duplicates them the way dup_mmap() duplicates a
> shared file mapping: clear the inherited VM_PAT so that track_pfn_copy()
> reserves the memtype for the copy, fork the anon_vma if the old VMA
> ever got one, link the VMA into the file's i_mmap (so
> unmap_mapping_range() on device reset still reaches it), call
> vm_ops->open() and copy the special PTEs with copy_page_range().
> vma_link() places the copy after the original in i_mmap, since an equal
> key goes right in the interval tree, which is the order dup_mmap() gets
> from vma_interval_tree_insert_after().
>
> Admit only a VFIO file, checked with vfio_file_is_valid() as KVM does;
> of those only a device file can be mapped. Other drivers keep per-mm
> state in such mappings and rely on VM_DONTCOPY to keep fork away: perf
> charges a ring buffer to the mm's pinned_vm, so a copy would drive the
> counter of the new mm negative and lift RLIMIT_MEMLOCK, and Xen privcmd
> frees per-mapping state in ->close() without an ->open(). VM_DONTCOPY
> itself cannot be the test, because QEMU marks every RAM block
> MADV_DONTFORK, VFIO BARs included. A VFIO mapping whose vm_ops has
> ->close() but no ->open() is refused as well, since the copy would run
> ->close() a second time.
>
> No in-tree VFIO driver keeps per-VMA state, and the vfio-pci family
> revokes BAR mappings through the device file's mapping, which reaches
> the copy. An out-of-tree driver that tracks its mappings has to count
> the copy in ->open(). One that maps with remap_pfn_range(), which sets
> VM_PAT, and revokes with unmap_mapping_range() inherits the windows fork
> has between track_pfn_copy() and a concurrent zap, and the one munmap has
> between the zap of the old VMA and a device reset; no in-tree VFIO
> driver does both.
>
> Dropping the mark gives VM_MAYEXEC back only to anonymous memory, where
> do_mmap() decides it. A marked PFN map does not get it back, since there
> the driver had the last word.
>
> Once the copy is in i_mmap, a zap from a device reset may clear VM_PAT
> of either VMA holding only i_mmap_rwsem. So clear VM_EXEC_KEEP of the
> copy before linking it, and touch VM_ACCOUNT of the old VMA only when it
> is set. The source VMA is write-locked explicitly, as in dup_mmap(),
> instead of through that vm_flags_clear().
>
> Such a VMA may have NULL vm_ops, in which case vma_is_anonymous()
> returns true for it. So the new vma_is_shared_pfnmap() helper is checked
> first, also in zap_vma_mt(), whose threaded path for anonymous memory
> would skip untrack_pfn().
>
> https://virtuozzo.atlassian.net/browse/VSTOR-150233
> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> Feature: oracle/mm: MADV_DOEXEC madvise() flag
> (cherry picked from commit f1903df83b065cd59ebe30cfcf5fde6391fcc4e3)
>
> Changes while porting: added the untrack_pfn_clear() call, the
> unconditional anon_vma_fork(), the VFIO-only admission, the VM_MAYEXEC
> handling and the flag ordering above; the file reference now comes from
> an earlier fix that takes it for every file-backed VMA.
>
> Assisted-by: LLM
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
> ---
> include/linux/mm.h | 12 ++++++++++--
> mm/madvise.c | 30 +++++++++++++++++++++++++-----
> mm/memory.c | 2 +-
> mm/mmap.c | 53 ++++++++++++++++++++++++++++++++++++++---------------
> 4 files changed, 74 insertions(+), 23 deletions(-)
>
> diff --git a/include/linux/mm.h b/include/linux/mm.h
> index 91f305fd5c025..496cbdfc7141a 100644
> --- a/include/linux/mm.h
> +++ b/include/linux/mm.h
> @@ -1018,6 +1018,16 @@ static inline bool vma_is_anonymous(struct vm_area_struct *vma)
> return !vma->vm_ops;
> }
>
> +/*
> + * Shared PFN mapping of a file, such as a device BAR mapped through VFIO.
> + * Its vm_ops may be NULL, so it must be tested before vma_is_anonymous().
> + */
> +static inline bool vma_is_shared_pfnmap(struct vm_area_struct *vma)
> +{
> + return vma->vm_file &&
> + (vma->vm_flags & (VM_SHARED | VM_PFNMAP)) == (VM_SHARED | VM_PFNMAP);
> +}
> +
> /*
> * Indicate if the VMA is a heap for the given task; for
> * /proc/PID/maps that is the heap of the main task.
> @@ -2481,10 +2491,8 @@ void free_pgd_range(struct mmu_gather *tlb, unsigned long addr,
> unsigned long end, unsigned long floor, unsigned long ceiling);
> int
> copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma);
> -#ifdef CONFIG_PADATA
> int copy_page_range_mt(struct vm_area_struct *dst_vma,
> struct vm_area_struct *src_vma);
> -#endif
> int generic_access_phys(struct vm_area_struct *vma, unsigned long addr,
> void *buf, int len, int write);
>
> diff --git a/mm/madvise.c b/mm/madvise.c
> index e6972bcfc9749..2b33d41b6ccf4 100644
> --- a/mm/madvise.c
> +++ b/mm/madvise.c
> @@ -31,6 +31,7 @@
> #include <linux/swapops.h>
> #include <linux/shmem_fs.h>
> #include <linux/mmu_notifier.h>
> +#include <linux/vfio.h>
>
> #include <asm/tlb.h>
>
> @@ -1249,6 +1250,22 @@ static long madvise_guard_remove(struct vm_area_struct *vma,
> &guard_remove_walk_ops, NULL);
> }
>
> +/* Only VFIO device mappings are known to cope with a copy in another mm. */
> +static bool vma_doexec_pfnmap_ok(struct vm_area_struct *vma)
> +{
> + bool (*is_vfio)(struct file *file);
> + bool ret;
> +
> + if (vma->vm_ops && vma->vm_ops->close && !vma->vm_ops->open)
> + return false;
This breaks qemu-update with vGPU directly when attempting
madvise(MADV_DOEXEC):
MADV_DOEXEC failed on memory region 0000:bd:00.4 region 0 mmaps[2]:
addr=0x7f5fc006c000 length=192512: Invalid argument
The reason being: NVIDIA maps the BAR0 pieces with vm_ops which has no
.open():
static const struct vm_operations_struct vgpu_sparse_ops = {
.close = vgpu_mmio_close,
};
My previous commit didn't have this check and it worked.
Andrey
> + is_vfio = symbol_get(vfio_file_is_valid);
> + if (!is_vfio)
> + return false;
> + ret = is_vfio(vma->vm_file);
> + symbol_put(vfio_file_is_valid);
> + return ret;
> +}
> +
> /*
> * Apply an madvise behavior to a region of a vma. madvise_update_vma
> * will handle splitting a vm area into separate areas, each area with its own
> @@ -1309,12 +1326,15 @@ static int madvise_vma_behavior(struct vm_area_struct *vma,
> break;
> case MADV_DOEXEC:
> /*
> - * MADV_DOEXEC is only supported on private, non-executable,
> - * non-stack, non-special anonymous memory and if the
> - * VM_EXEC_KEEP flag is available.
> + * MADV_DOEXEC is supported on private, non-executable,
> + * non-stack, non-special anonymous memory, and on shared
> + * PFN mappings of VFIO devices, if the VM_EXEC_KEEP flag
> + * is available.
> */
> - if (!VM_EXEC_KEEP || !vma_is_anonymous(vma) ||
> - vma->vm_flags & (VM_EXEC | VM_SHARED | VM_STACK | VM_SPECIAL)) {
> + if (!VM_EXEC_KEEP || vma->vm_flags & (VM_EXEC | VM_STACK) ||
> + (vma_is_shared_pfnmap(vma) ? !vma_doexec_pfnmap_ok(vma) :
> + !vma_is_anonymous(vma) ||
> + vma->vm_flags & (VM_SHARED | VM_SPECIAL))) {
> error = -EINVAL;
> goto out;
> }
> diff --git a/mm/memory.c b/mm/memory.c
> index 2566f02fb7d5e..2f15bc06ccf90 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -2239,7 +2239,7 @@ void zap_vma_mt(struct vm_area_struct *vma)
> struct mmu_notifier_range range;
>
> mmap_assert_write_locked(vma->vm_mm);
> - if (vma_is_anonymous(vma)) {
> + if (vma_is_anonymous(vma) && !vma_is_shared_pfnmap(vma)) {
> lru_add_drain();
> mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
> vma->vm_start, vma->vm_end);
> diff --git a/mm/mmap.c b/mm/mmap.c
> index 69dd1514d750e..959810d8047a8 100644
> --- a/mm/mmap.c
> +++ b/mm/mmap.c
> @@ -1752,31 +1752,41 @@ int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
>
> /*
> * The VM_MAYEXEC that MADV_DOEXEC took from @vma, to give back when the mark
> - * goes: what do_mmap() grants such a mapping.
> + * goes: what do_mmap() grants such a mapping. A driver may have withheld it
> + * from a PFN map, so leave that one without it.
> */
> vm_flags_t vma_doexec_mayexec(struct vm_area_struct *vma)
> {
> - if (vma->vm_file && path_noexec(&vma->vm_file->f_path))
> + if (vma_is_shared_pfnmap(vma) ||
> + (vma->vm_file && path_noexec(&vma->vm_file->f_path)))
> return 0;
> return VM_MAYEXEC;
> }
>
> /*
> - * Duplicate an anonymous VMA into a new mm as part of preserving
> - * it across exec.
> + * Duplicate a VMA into a new mm as part of preserving it across exec.
> + *
> + * Anonymous VMAs get their pages shared with the new mm fork-style.
> + * Shared PFN mappings (e.g. VFIO BARs) are duplicated the way fork
> + * duplicates a shared file mapping: same file, same special PTEs,
> + * nothing to copy-on-write.
> */
> int vma_dup(struct vm_area_struct *old_vma, struct mm_struct *mm)
> {
> struct vm_area_struct *vma;
> unsigned long npages;
> + bool pfnmap = vma_is_shared_pfnmap(old_vma);
> int ret = -ENOMEM;
>
> - if (WARN_ON(!vma_is_anonymous(old_vma)))
> + if (WARN_ON(!pfnmap && !vma_is_anonymous(old_vma)))
> return -EINVAL;
>
> if (find_vma_intersection(mm, old_vma->vm_start, old_vma->vm_end))
> return -EEXIST;
>
> + /* As in dup_mmap(), keep per-VMA lock users out of the source. */
> + vma_start_write(old_vma);
> +
> npages = vma_pages(old_vma);
> vm_stat_account(mm, old_vma->vm_flags, npages);
>
> @@ -1784,6 +1794,10 @@ int vma_dup(struct vm_area_struct *old_vma, struct mm_struct *mm)
> if (!vma)
> goto fail_nomem;
>
> + /* track_pfn_copy() will later take care of copying internal state. */
> + if (pfnmap)
> + untrack_pfn_clear(vma);
> +
> ret = vma_dup_policy(old_vma, vma);
> if (ret)
> goto fail_nomem_policy;
> @@ -1797,17 +1811,24 @@ int vma_dup(struct vm_area_struct *old_vma, struct mm_struct *mm)
>
> /*
> * Clear functionality that should not carry over to the new
> - * process. Note that VM_EXEC_KEEP is cleared later to allow
> - * code called by copy_page_range to infer that the copying is
> - * for preserving over exec and not for process forking.
> + * process. Note that VM_EXEC_KEEP of an anonymous copy is cleared
> + * later to allow code called by copy_page_range to infer that the
> + * copying is for preserving over exec and not for process forking.
> */
> vm_flags_clear(vma, VM_LOCKED|VM_LOCKONFAULT|VM_UFFD_MISSING|VM_UFFD_WP|VM_UFFD_MINOR);
> vma->vm_userfaultfd_ctx = NULL_VM_UFFD_CTX;
> + /* Once linked, unmap_mapping_range() may modify the flags concurrently. */
> + if (pfnmap)
> + vm_flags_clear(vma, VM_EXEC_KEEP);
>
> + /* Also links a file VMA into the file's i_mmap, for unmap_mapping_range(). */
> ret = vma_link(mm, vma);
> if (ret)
> goto fail_nomem_vma_link;
>
> + if (pfnmap && vma->vm_ops && vma->vm_ops->open)
> + vma->vm_ops->open(vma);
> +
> /*
> * Now that the dup vma is inserted into the mm, clear VM_ACCOUNT
> * from old_vma. Since vma_dup() is only called during exec to
> @@ -1815,14 +1836,16 @@ int vma_dup(struct vm_area_struct *old_vma, struct mm_struct *mm)
> * process, this effectively transfers the accounting from the old
> * vma to the new one.
> */
> - vm_flags_clear(old_vma, VM_ACCOUNT);
> + if (old_vma->vm_flags & VM_ACCOUNT)
> + vm_flags_clear(old_vma, VM_ACCOUNT);
>
> -#ifdef CONFIG_PADATA
> - ret = copy_page_range_mt(vma, old_vma);
> -#else
> - ret = copy_page_range(vma, old_vma);
> -#endif
> - vm_flags_mod(vma, vma_doexec_mayexec(vma), VM_EXEC_KEEP);
> + /* For PFN maps this copies the device PTEs and the PAT memtype */
> + if (pfnmap || !IS_ENABLED(CONFIG_PADATA))
> + ret = copy_page_range(vma, old_vma);
> + else
> + ret = copy_page_range_mt(vma, old_vma);
> + if (!pfnmap)
> + vm_flags_mod(vma, vma_doexec_mayexec(vma), VM_EXEC_KEEP);
>
> return ret;
>
>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
next prev parent reply other threads:[~2026-10-16 12:59 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-14 17:13 [Devel] [PATCH hci-8.1 0/2] mm: allow MADV_DOEXEC on VFIO BAR mappings Konstantin Khorenko
2026-10-14 17:14 ` [Devel] [PATCH hci-8.1 1/2] mm: allow MADV_DOEXEC on shared PFN mappings of device memory Konstantin Khorenko
2026-10-16 12:57 ` Andrey Drobyshev [this message]
2026-10-14 17:14 ` [Devel] [PATCH hci-8.1 2/2] selftests/mm: check MADV_DOEXEC on VFIO BARs and other PFN mappings Konstantin Khorenko
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=61fe3d17-c8b9-4a01-b3b1-b5d2cda767f1@virtuozzo.com \
--to=andrey.drobyshev@virtuozzo.com \
--cc=devel@openvz.org \
--cc=khorenko@virtuozzo.com \
--cc=ptikhomirov@virtuozzo.com \
--cc=vasileios.almpanis@virtuozzo.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox