* [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec
@ 2026-10-14 17:11 Konstantin Khorenko
2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 1/8] oracle/mm: fix VM_BUG_ON hit when preserving memory Konstantin Khorenko
` (7 more replies)
0 siblings, 8 replies; 17+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw)
To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel
VSTOR-149847: memory preserved across exec with MADV_DOEXEC loses its
connection to long-term pins (VFIO DMA, io_uring fixed buffers) after a
live-update exec, because the copy into the new mm write protects the
pages and the first write in the new image copies them away from the
pins. Applies on top of "mm, binfmt_elf: two fixes also posted to
mainline".
http://lore.virtuozzo.com/20261014-b4-prep-khorenko-vstor-149847-1-upstream-fixes-v1-0-7fde9ffbebe9@virtuozzo.com
Patches 1-3 are Oracle's fixes for that path, taken from UEK8 (uek8/u2)
as they are: the copy runs under the write lock of the old mm with
write_protect_seq held, and preserved PTEs of COW mappings are no longer
write protected.
Patches 4-6 fix what patch 3 leaves:
- 4: the preserved PTE stayed writable in both mms, which
page_table_check catches on a debug kernel (kernel BUG at
mm/page_table_check.c). Write protect both copies but keep
PageAnonExclusive, so the first write in the new image reuses the
page.
- 5: MMF_HAS_PINNED is carried to the new mm, otherwise a fork in the
new image treats pinned pages as unpinned and separates them from
their pins.
- 6: the preserved ranges of the old mm are zapped right after the copy;
while the old mm lived, a page was exclusive in two mms and
UFFDIO_MOVE could move its rmap to an anon_vma that is later freed.
The zap runs on several threads, as the teardown of these ranges
did before.
Patch 7 (Andrey) refuses a preserving exec while a CLONE_VM task outside
the thread group shares the old mm, and patch 8 is its selftest, which
also covers the pinned cases above. A kernel thread on the mm counts
too: with vhost in kthread mode, the vz10 default, QEMU has to reset the
vhost owner before a preserving exec.
Tested with the whole stack on release and debug kernels: the
MADV_DOEXEC selftests (including the allocation-failure sweep on the
debug kernel), gup_test, a Container start/exec/stop; no BUG, WARNING
or KASAN.
https://virtuozzo.atlassian.net/browse/VSTOR-150231
---
Andrey Drobyshev (2):
exec: protect preserved memory against surviving shared-mm tasks
selftests/mm: test memory preservation across exec
Anthony Yznaga (3):
oracle/mm: fix VM_BUG_ON hit when preserving memory
oracle/mm: hold the source mmap write lock when copying PTEs
oracle/mm: do not write protect COW mappings when preserving across exec
Konstantin Khorenko (3):
oracle/mm: write protect preserved PTEs but keep them exclusive
oracle/mm: set MMF_HAS_PINNED on the mm receiving preserved pages
oracle/mm: zap the preserved ranges of the old mm after copying them
fs/exec.c | 111 +++-
include/linux/binfmts.h | 4 +-
include/linux/mm.h | 1 +
include/linux/mm_types.h | 4 +
include/linux/rmap.h | 8 +-
kernel/fork.c | 37 ++
mm/memory.c | 93 +++-
mm/mmap.c | 7 +-
tools/testing/selftests/mm/.gitignore | 1 +
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/doexec.c | 814 ++++++++++++++++++++++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 4 +
12 files changed, 1050 insertions(+), 35 deletions(-)
---
base-commit: 975b3d461d014d75dbc775f44a4758fb41996d09
change-id: 20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-51b0e4b50c5e
Best regards,
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 17+ messages in thread* [Devel] [PATCH hci-8.1 1/8] oracle/mm: fix VM_BUG_ON hit when preserving memory 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:16 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 2/8] oracle/mm: hold the source mmap write lock when copying PTEs Konstantin Khorenko ` (6 subsequent siblings) 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel From: Anthony Yznaga <anthony.yznaga@oracle.com> copy_page_range_mt() is a stripped down version of copy_page_range(). Unlike copy_page_range(), it is called during exec with the mmap read lock held on the outgoing mm. Assert that the mmap lock is held, and hold the write on the write_protect_seq seqcount_t section. The latter is needed to avoid a VM_BUG_ON in folio_needs_cow_for_dma(). Orabug: 37588319 Signed-off-by: Anthony Yznaga <anthony.yznaga@oracle.com> Reviewed-by: Liam R. Howlett <liam.howlett@oracle.com> (cherry picked from commit b4c299d320c22ed30b03c7e665389c9870dd9bab) Taken from Oracle linux-uek branch uek8/u2. Applied cleanly. Same Orabug as 90339648df65 ("oracle/mm: fix vma iterator misconfiguration that triggers VM_WARN_ONs"), which we already have; this is the second half of that pair. Fixes: d674aa9506ea ("oracle/mm: use padata for copying page ranges in vma_dup()") https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- mm/memory.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mm/memory.c b/mm/memory.c index 5e414c68f3a9c..d4821379529bf 100644 --- a/mm/memory.c +++ b/mm/memory.c @@ -1489,10 +1489,16 @@ int copy_page_range_mt(struct vm_area_struct *dst_vma, struct vm_area_struct *sr .min_chunk = max(1ul << 27, PMD_SIZE), .max_threads = 16, }; + int ret; BUG_ON(!(src_vma->vm_flags & VM_EXEC_KEEP)); - return padata_do_multithreaded(&job); + mmap_assert_locked(src_vma->vm_mm); + raw_write_seqcount_begin(&src_vma->vm_mm->write_protect_seq); + ret = padata_do_multithreaded(&job); + raw_write_seqcount_end(&src_vma->vm_mm->write_protect_seq); + + return ret; } #endif /* CONFIG_PADATA */ -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 1/8] oracle/mm: fix VM_BUG_ON hit when preserving memory 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 1/8] oracle/mm: fix VM_BUG_ON hit when preserving memory Konstantin Khorenko @ 2026-10-14 17:16 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:16 UTC (permalink / raw) To: Andrey Drobyshev, Pavel Tikhomirov, Konstantin Khorenko, Vasileios Almpanis Cc: devel > From: Anthony Yznaga <anthony.yznaga@oracle.com> > > copy_page_range_mt() is a stripped down version of copy_page_range(). > Unlike copy_page_range(), it is called during exec with the mmap read > lock held on the outgoing mm. Assert that the mmap lock is held, and > hold the write on the write_protect_seq seqcount_t section. The latter > is needed to avoid a VM_BUG_ON in folio_needs_cow_for_dma(). > > Orabug: 37588319 > Signed-off-by: Anthony Yznaga <anthony.yznaga@oracle.com> > Reviewed-by: Liam R. Howlett <liam.howlett@oracle.com> > (cherry picked from commit b4c299d320c22ed30b03c7e665389c9870dd9bab) > > Taken from Oracle linux-uek branch uek8/u2. Applied cleanly. Same > Orabug as 90339648df65 ("oracle/mm: fix vma iterator misconfiguration > that triggers VM_WARN_ONs"), which we already have; this is the second > half of that pair. > > Fixes: d674aa9506ea ("oracle/mm: use padata for copying page ranges in vma_dup()") > https://virtuozzo.atlassian.net/browse/VSTOR-150231 > > Feature: oracle/mm: MADV_DOEXEC madvise() flag > Assisted-by: LLM > Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> Sashiko has reviewed this patch and found no issues. It looks great! -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=1 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* [Devel] [PATCH hci-8.1 2/8] oracle/mm: hold the source mmap write lock when copying PTEs 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 1/8] oracle/mm: fix VM_BUG_ON hit when preserving memory Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:15 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 3/8] oracle/mm: do not write protect COW mappings when preserving across exec Konstantin Khorenko ` (5 subsequent siblings) 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel From: Anthony Yznaga <anthony.yznaga@oracle.com> The copy_p*_range() routines assume the mmap write lock is held for both the source and destination and therefore there will be no unexpected THP transitions while PTEs are copied (for example, due to khugepaged). The write lock is already held on the destination mm, but until now only the read lock was held on the source. Orabug: 38029049 Signed-off-by: Anthony Yznaga <anthony.yznaga@oracle.com> Reviewed-by: Harry Yoo <harry.yoo@oracle.com> (cherry picked from commit 285a140316fa44f0cdc6608c109ba525e52d6c3f) Taken from Oracle linux-uek branch uek8/u2. Applied cleanly; the only change is a trailing space dropped from the new comment in exec_mmap(). Fixes: 9e6d16a6c0a1 ("oracle/mm: introduce VM_EXEC_KEEP") https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- fs/exec.c | 19 ++++++++++++++++--- mm/mmap.c | 7 ++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/fs/exec.c b/fs/exec.c index e861a54b491f8..62010a6d63aab 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1028,10 +1028,20 @@ static int exec_mmap(struct linux_binprm *bprm) up_write(&tsk->signal->exec_update_lock); return ret; } - if (bprm->accepts_preserved_mem) { + if (unlikely(bprm->accepts_preserved_mem)) { + /* + * Acquire the write lock to avoid changes to page + * table entries while they are copied. + */ + mmap_read_unlock(old_mm); + ret = mmap_write_lock_killable(old_mm); + if (ret) { + up_write(&tsk->signal->exec_update_lock); + return ret; + } ret = vma_dup_some(old_mm, mm); if (ret) { - mmap_read_unlock(old_mm); + mmap_write_unlock(old_mm); up_write(&tsk->signal->exec_update_lock); return ret; } @@ -1063,7 +1073,10 @@ static int exec_mmap(struct linux_binprm *bprm) task_unlock(tsk); lru_gen_use_mm(mm); if (old_mm) { - mmap_read_unlock(old_mm); + if (unlikely(bprm->accepts_preserved_mem)) + mmap_write_unlock(old_mm); + else + mmap_read_unlock(old_mm); BUG_ON(active_mm != old_mm); setmax_mm_hiwater_rss(&tsk->signal->maxrss, old_mm); mm_update_next_owner(old_mm); diff --git a/mm/mmap.c b/mm/mmap.c index 320f397816df9..bc508c6bd934b 100644 --- a/mm/mmap.c +++ b/mm/mmap.c @@ -1800,12 +1800,9 @@ int vma_dup(struct vm_area_struct *old_vma, struct mm_struct *mm) * from old_vma. Since vma_dup() is only called during exec to * duplicate a vma from the outgoing mm into the mm of the new * process, this effectively transfers the accounting from the old - * vma to new one. - * exec holds the mmap read lock on the outgoing mm at this point - * so __vm_flags_mod() is used here to avoid an assert that the - * mmap write lock is held which is not needed in this instance.. + * vma to the new one. */ - __vm_flags_mod(old_vma, 0, VM_ACCOUNT); + vm_flags_clear(old_vma, VM_ACCOUNT); #ifdef CONFIG_PADATA ret = copy_page_range_mt(vma, old_vma); -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 2/8] oracle/mm: hold the source mmap write lock when copying PTEs 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 2/8] oracle/mm: hold the source mmap write lock when copying PTEs Konstantin Khorenko @ 2026-10-14 17:15 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:15 UTC (permalink / raw) To: Pavel Tikhomirov, Konstantin Khorenko, Vasileios Almpanis, Andrey Drobyshev Cc: devel > From: Anthony Yznaga <anthony.yznaga@oracle.com> > > The copy_p*_range() routines assume the mmap write lock is held > for both the source and destination and therefore there will be > no unexpected THP transitions while PTEs are copied (for example, > due to khugepaged). The write lock is already held on the destination > mm, but until now only the read lock was held on the source. > > Orabug: 38029049 > Signed-off-by: Anthony Yznaga <anthony.yznaga@oracle.com> > Reviewed-by: Harry Yoo <harry.yoo@oracle.com> > (cherry picked from commit 285a140316fa44f0cdc6608c109ba525e52d6c3f) > > Taken from Oracle linux-uek branch uek8/u2. Applied cleanly; the only > change is a trailing space dropped from the new comment in exec_mmap(). > > Fixes: 9e6d16a6c0a1 ("oracle/mm: introduce VM_EXEC_KEEP") > https://virtuozzo.atlassian.net/browse/VSTOR-150231 > > Feature: oracle/mm: MADV_DOEXEC madvise() flag > Assisted-by: LLM > Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> Sashiko has reviewed this patch and found no issues. It looks great! -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=2 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* [Devel] [PATCH hci-8.1 3/8] oracle/mm: do not write protect COW mappings when preserving across exec 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 1/8] oracle/mm: fix VM_BUG_ON hit when preserving memory Konstantin Khorenko 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 2/8] oracle/mm: hold the source mmap write lock when copying PTEs Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:20 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 4/8] oracle/mm: write protect preserved PTEs but keep them exclusive Konstantin Khorenko ` (4 subsequent siblings) 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel From: Anthony Yznaga <anthony.yznaga@oracle.com> A mapping that is pinned for DMA and preserved across exec can incur a COW if write protected. The elevated reference count prevents reuse of the mapped folio. Orabug: 38029049 Suggested-by: Steve Sistare <steven.sistare@oracle.com> Signed-off-by: Anthony Yznaga <anthony.yznaga@oracle.com> Reviewed-by: Harry Yoo <harry.yoo@oracle.com> (cherry picked from commit f2e56cc70e62fe07efb71a1e1c2360a78cba5aff) Taken from Oracle linux-uek branch uek8/u2. Applied cleanly. This is the fix for the ticket: a FOLL_LONGTERM-pinned anonymous page preserved with MADV_DOEXEC was write-protected and lost PageAnonExclusive in vma_dup(), so the first write in the new image COWed it and the pin kept the old page. The pinned pages now stay exclusive and writable in the new mm, and the old mm keeps its own. The fixes that follow write protect both again and zap the old mm's mappings right after the copy. Fixes: 9e6d16a6c0a1 ("oracle/mm: introduce VM_EXEC_KEEP") https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- include/linux/rmap.h | 8 ++++---- mm/memory.c | 13 ++++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/include/linux/rmap.h b/include/linux/rmap.h index 5dc4cd6e4e2c9..01e46123ab2dc 100644 --- a/include/linux/rmap.h +++ b/include/linux/rmap.h @@ -431,22 +431,22 @@ static __always_inline int __folio_try_dup_anon_rmap(struct folio *folio, } if (!folio_test_large(folio)) { - if (PageAnonExclusive(page)) + if (PageAnonExclusive(page) && !is_exec_keep) ClearPageAnonExclusive(page); atomic_inc(&folio->_mapcount); break; } do { - if (PageAnonExclusive(page)) + if (PageAnonExclusive(page) && !is_exec_keep) ClearPageAnonExclusive(page); atomic_inc(&page->_mapcount); } while (page++, --nr_pages > 0); atomic_add(orig_nr_pages, &folio->_large_mapcount); break; case RMAP_LEVEL_PMD: - if (PageAnonExclusive(page)) { - if (unlikely(maybe_pinned) && likely(!is_exec_keep)) + if (PageAnonExclusive(page) && !is_exec_keep) { + if (unlikely(maybe_pinned)) return -EBUSY; ClearPageAnonExclusive(page); } diff --git a/mm/memory.c b/mm/memory.c index d4821379529bf..a240246762751 100644 --- a/mm/memory.c +++ b/mm/memory.c @@ -956,9 +956,15 @@ static __always_inline void __copy_present_ptes(struct vm_area_struct *dst_vma, pte_t pte, unsigned long addr, int nr) { struct mm_struct *src_mm = src_vma->vm_mm; + bool is_exec_keep = !!(dst_vma->vm_flags & VM_EXEC_KEEP); /* If it's a COW mapping, write protect it both processes. */ - if (is_cow_mapping(src_vma->vm_flags) && pte_write(pte)) { + /* + * But don't protect it if PTEs are being copied as part of preserving + * memory across exec since the page may be pinned for DMA and must not + * be COW'd. + */ + if (is_cow_mapping(src_vma->vm_flags) && pte_write(pte) && !is_exec_keep) { wrprotect_ptes(src_mm, addr, src_pte, nr); pte = pte_wrprotect(pte); } @@ -991,6 +997,7 @@ copy_present_ptes(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma bool any_writable; fpb_t flags = 0; int err, nr; + bool is_exec_keep = !!(dst_vma->vm_flags & VM_EXEC_KEEP); page = vm_normal_page(src_vma, addr, pte); if (unlikely(!page)) @@ -1019,7 +1026,7 @@ copy_present_ptes(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma return -EAGAIN; } rss[MM_ANONPAGES] += nr; - VM_WARN_ON_FOLIO(PageAnonExclusive(page), folio); + VM_WARN_ON_FOLIO(PageAnonExclusive(page) && !is_exec_keep, folio); } else { folio_dup_file_rmap_ptes(folio, page, nr); rss[mm_counter_file(folio)] += nr; @@ -1047,7 +1054,7 @@ copy_present_ptes(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma return err ? err : 1; } rss[MM_ANONPAGES]++; - VM_WARN_ON_FOLIO(PageAnonExclusive(page), folio); + VM_WARN_ON_FOLIO(PageAnonExclusive(page) && !is_exec_keep, folio); } else { folio_dup_file_rmap_pte(folio, page); rss[mm_counter_file(folio)]++; -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 3/8] oracle/mm: do not write protect COW mappings when preserving across exec 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 3/8] oracle/mm: do not write protect COW mappings when preserving across exec Konstantin Khorenko @ 2026-10-14 17:20 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:20 UTC (permalink / raw) To: Pavel Tikhomirov, Konstantin Khorenko, Vasileios Almpanis, Andrey Drobyshev Cc: devel > From: Anthony Yznaga <anthony.yznaga@oracle.com> > > A mapping that is pinned for DMA and preserved across exec can incur > a COW if write protected. The elevated reference count prevents reuse > of the mapped folio. > > Orabug: 38029049 > Suggested-by: Steve Sistare <steven.sistare@oracle.com> > Signed-off-by: Anthony Yznaga <anthony.yznaga@oracle.com> > Reviewed-by: Harry Yoo <harry.yoo@oracle.com> > (cherry picked from commit f2e56cc70e62fe07efb71a1e1c2360a78cba5aff) > > Taken from Oracle linux-uek branch uek8/u2. Applied cleanly. > > This is the fix for the ticket: a FOLL_LONGTERM-pinned anonymous page > preserved with MADV_DOEXEC was write-protected and lost > PageAnonExclusive in vma_dup(), so the first write in the new image > COWed it and the pin kept the old page. The pinned pages now stay > exclusive and writable in the new mm, and the old mm keeps its own. The > fixes that follow write protect both again and zap the old mm's mappings > right after the copy. > > Fixes: 9e6d16a6c0a1 ("oracle/mm: introduce VM_EXEC_KEEP") > https://virtuozzo.atlassian.net/browse/VSTOR-150231 > > Feature: oracle/mm: MADV_DOEXEC madvise() flag > Assisted-by: LLM > Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> Sashiko has reviewed this patch and found no issues. It looks great! -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=3 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* [Devel] [PATCH hci-8.1 4/8] oracle/mm: write protect preserved PTEs but keep them exclusive 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko ` (2 preceding siblings ...) 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 3/8] oracle/mm: do not write protect COW mappings when preserving across exec Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:18 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 5/8] oracle/mm: set MMF_HAS_PINNED on the mm receiving preserved pages Konstantin Khorenko ` (3 subsequent siblings) 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel Commit 37d9f55655ea ("oracle/mm: do not write protect COW mappings when preserving across exec") leaves the PTE of a preserved anonymous page writable in both the outgoing and the new mm. With CONFIG_PAGE_TABLE_CHECK, which refuses to install a writable PTE for an anonymous page that is mapped already, the first preserving exec panics: kernel BUG at mm/page_table_check.c:118! RIP: 0010:page_table_check_set+0x484/0x520 __page_table_check_ptes_set copy_present_ptes copy_pte_range ... copy_page_range_mt vma_dup exec_mmap The writable PTE is not what prevents the COW of a pinned page: the same patch keeps PageAnonExclusive set on the preserved page, and do_wp_page() reuses an exclusive anonymous page instead of copying it. Write protect the PTE in both mms again, as fork does. The first write in the new image takes a fault that reuses the page, which is what the PMD path already does since copy_huge_pmd() write protects both PMDs. Unlike fork, the copy does not flush the TLB of the old mm after write protecting it; the zap of the old ranges later in this series does. Fixes: 37d9f55655ea ("oracle/mm: do not write protect COW mappings when preserving across exec") https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- mm/memory.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mm/memory.c b/mm/memory.c index a240246762751..1c128162f5cda 100644 --- a/mm/memory.c +++ b/mm/memory.c @@ -956,15 +956,13 @@ static __always_inline void __copy_present_ptes(struct vm_area_struct *dst_vma, pte_t pte, unsigned long addr, int nr) { struct mm_struct *src_mm = src_vma->vm_mm; - bool is_exec_keep = !!(dst_vma->vm_flags & VM_EXEC_KEEP); - /* If it's a COW mapping, write protect it both processes. */ /* - * But don't protect it if PTEs are being copied as part of preserving - * memory across exec since the page may be pinned for DMA and must not - * be COW'd. + * If it's a COW mapping, write protect it both processes. A page + * preserved across exec keeps PageAnonExclusive, so the new process + * reuses it on the first write instead of copying it. */ - if (is_cow_mapping(src_vma->vm_flags) && pte_write(pte) && !is_exec_keep) { + if (is_cow_mapping(src_vma->vm_flags) && pte_write(pte)) { wrprotect_ptes(src_mm, addr, src_pte, nr); pte = pte_wrprotect(pte); } -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 4/8] oracle/mm: write protect preserved PTEs but keep them exclusive 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 4/8] oracle/mm: write protect preserved PTEs but keep them exclusive Konstantin Khorenko @ 2026-10-14 17:18 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:18 UTC (permalink / raw) To: Andrey Drobyshev, Vasileios Almpanis, Pavel Tikhomirov, Konstantin Khorenko Cc: devel > Commit 37d9f55655ea ("oracle/mm: do not write protect COW mappings when > preserving across exec") leaves the PTE of a preserved anonymous page > writable in both the outgoing and the new mm. With > CONFIG_PAGE_TABLE_CHECK, which refuses to install a writable PTE for an > anonymous page that is mapped already, the first preserving exec panics: > > kernel BUG at mm/page_table_check.c:118! > RIP: 0010:page_table_check_set+0x484/0x520 > __page_table_check_ptes_set > copy_present_ptes > copy_pte_range > ... > copy_page_range_mt > vma_dup > exec_mmap > [ ... ] > https://virtuozzo.atlassian.net/browse/VSTOR-150231 > > Feature: oracle/mm: MADV_DOEXEC madvise() flag > Assisted-by: LLM > Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> Sashiko has reviewed this patch and found no issues. It looks great! -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=4 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* [Devel] [PATCH hci-8.1 5/8] oracle/mm: set MMF_HAS_PINNED on the mm receiving preserved pages 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko ` (3 preceding siblings ...) 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 4/8] oracle/mm: write protect preserved PTEs but keep them exclusive Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:19 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them Konstantin Khorenko ` (2 subsequent siblings) 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel fork decides whether an anonymous page may be pinned with folio_needs_cow_for_dma(), which looks at MMF_HAS_PINNED of the parent mm before folio_maybe_dma_pinned(). Only GUP sets that flag, in the mm whose pages it pins. Pages preserved across exec keep a long-term pin taken through the old mm, but the new mm never has the flag, so a fork in the new image clears PageAnonExclusive of a pinned page and maps it read-only in both processes. The next write in the parent copies the page and the device keeps the old one. A debug kernel catches the shared page when the pin is dropped: page dumped because: VM_BUG_ON_PAGE(!PageAnonExclusive(&folio->page)) kernel BUG at mm/gup.c:66! RIP: 0010:sanity_check_pinned_pages+0x237/0x960 unpin_user_pages gup_test_release __fput Set MMF_HAS_PINNED on the new mm when the old one has it. Fixes: 37d9f55655ea ("oracle/mm: do not write protect COW mappings when preserving across exec") https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- fs/exec.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/exec.c b/fs/exec.c index 62010a6d63aab..8677347963e20 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -984,6 +984,10 @@ static int vma_dup_some(struct mm_struct *old_mm, struct mm_struct *new_mm) VMA_ITERATOR(vmi, old_mm, 0); int ret = 0; + /* Preserved pages may be pinned, fork has to keep copying them. */ + if (test_bit(MMF_HAS_PINNED, &old_mm->flags)) + set_bit(MMF_HAS_PINNED, &new_mm->flags); + mmap_write_lock_nested(new_mm, SINGLE_DEPTH_NESTING); for_each_vma(vmi, vma) if (vma->vm_flags & VM_EXEC_KEEP) { -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 5/8] oracle/mm: set MMF_HAS_PINNED on the mm receiving preserved pages 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 5/8] oracle/mm: set MMF_HAS_PINNED on the mm receiving preserved pages Konstantin Khorenko @ 2026-10-14 17:19 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:19 UTC (permalink / raw) To: Andrey Drobyshev, Vasileios Almpanis, Pavel Tikhomirov, Konstantin Khorenko Cc: devel > fork decides whether an anonymous page may be pinned with > folio_needs_cow_for_dma(), which looks at MMF_HAS_PINNED of the parent > mm before folio_maybe_dma_pinned(). Only GUP sets that flag, in the mm > whose pages it pins. Pages preserved across exec keep a long-term pin > taken through the old mm, but the new mm never has the flag, so a fork > in the new image clears PageAnonExclusive of a pinned page and maps it > read-only in both processes. The next write in the parent copies the > page and the device keeps the old one. A debug kernel catches the > shared page when the pin is dropped: > > page dumped because: VM_BUG_ON_PAGE(!PageAnonExclusive(&folio->page)) > kernel BUG at mm/gup.c:66! > RIP: 0010:sanity_check_pinned_pages+0x237/0x960 > unpin_user_pages > gup_test_release > __fput > > Set MMF_HAS_PINNED on the new mm when the old one has it. > > Fixes: 37d9f55655ea ("oracle/mm: do not write protect COW mappings when preserving across exec") > https://virtuozzo.atlassian.net/browse/VSTOR-150231 > > Feature: oracle/mm: MADV_DOEXEC madvise() flag > Assisted-by: LLM > Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> Sashiko has reviewed this patch and found no issues. It looks great! -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=5 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko ` (4 preceding siblings ...) 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 5/8] oracle/mm: set MMF_HAS_PINNED on the mm receiving preserved pages Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:24 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 7/8] exec: protect preserved memory against surviving shared-mm tasks Konstantin Khorenko 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec Konstantin Khorenko 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel A preserved page keeps PageAnonExclusive in both the old and the new mm after the copy. exec_mmap() only drops a reference to the old mm, and anything that holds mm_users keeps it mapped there, such as a /proc/pid/mem reader stuck in copy_to_user() or process_vm_readv(). Code that trusts PageAnonExclusive to mean a single mapping then goes wrong. UFFDIO_MOVE in either mm calls folio_move_anon_rmap() on a folio the other mm still maps, and once the destination anon_vma is freed the next rmap walk reads freed memory. try_to_migrate_one() hits VM_BUG_ON_PAGE() for a writable PTE of a page that the other mapping made shared, and a writable set_pte_at() in the new mm trips PAGE_TABLE_CHECK. A write through the old mm reuses the page and lands in the new image. Zap the VM_EXEC_KEEP ranges of the old mm right after the copy, under its write lock, so that the copies are the only mappings left. The pages are not freed, the new mm holds them; a later access through the old mm faults in fresh memory there. The unmap work moves from the teardown of the old mm, which ran it with several threads, into the zap, so zap_vma_mt() spreads an anonymous VMA over several threads as well, each with an mmu_gather of its own as concurrent MADV_DONTNEED calls have; the teardown finds these VMAs empty and no longer treats VM_EXEC_KEEP specially. Fixes: 37d9f55655ea ("oracle/mm: do not write protect COW mappings when preserving across exec") https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- fs/exec.c | 10 +++++++- include/linux/mm.h | 1 + mm/memory.c | 74 +++++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/fs/exec.c b/fs/exec.c index 8677347963e20..32567010a2f27 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -997,7 +997,15 @@ static int vma_dup_some(struct mm_struct *old_mm, struct mm_struct *new_mm) } mmap_write_unlock(new_mm); - return ret; + if (ret) + return ret; + + /* Whoever keeps old_mm alive past the exec must not reach the pages through it. */ + vma_iter_set(&vmi, 0); + for_each_vma(vmi, vma) + if (vma->vm_flags & VM_EXEC_KEEP) + zap_vma_mt(vma); + return 0; } /* diff --git a/include/linux/mm.h b/include/linux/mm.h index 75b8bcd9f4428..9ebf3c6dcb978 100644 --- a/include/linux/mm.h +++ b/include/linux/mm.h @@ -2462,6 +2462,7 @@ struct page *vm_normal_page_pmd(struct vm_area_struct *vma, unsigned long addr, void zap_vma_ptes(struct vm_area_struct *vma, unsigned long address, unsigned long size); +void zap_vma_mt(struct vm_area_struct *vma); void zap_page_range_single(struct vm_area_struct *vma, unsigned long address, unsigned long size, struct zap_details *details); static inline void zap_vma_pages(struct vm_area_struct *vma) diff --git a/mm/memory.c b/mm/memory.c index 1c128162f5cda..2566f02fb7d5e 100644 --- a/mm/memory.c +++ b/mm/memory.c @@ -2043,30 +2043,20 @@ static void unmap_page_range_mt(struct mmu_gather *tlb, BUG_ON(addr >= end); - /* - * Assumptions made below about VM_EXEC_KEEP VMAs only hold true - * if the entire address space is being unmapped. - */ if (!tlb->fullmm) { unmap_page_range(tlb, vma, addr, end, details); return; } /* - * Pages in VMAs being preserved will have an additional reference - * due to the mappings having been copied prior to unmapping the full - * address space. Since lock contention due to freeing pages will not - * be an issue, use additional threads to unmap them. - * Otherwise for sufficiently large, private anon VMAs, using - * multiple threads can result in severe lock contention the swap - * subsystem if a signficant portion is swapped out. Since the number - * of swapped pages is tracked per MM and not per VMA, err on the - * side of caution and assume the number applies to the VMA and revert - * to unmapping single threaded if necessary. + * For sufficiently large, private anon VMAs, using multiple threads + * can result in severe lock contention in the swap subsystem if a + * significant portion is swapped out. Since the number of swapped + * pages is tracked per MM and not per VMA, err on the side of caution + * and assume the number applies to the VMA and revert to unmapping + * single threaded if necessary. */ - if (vma->vm_flags & VM_EXEC_KEEP) { - job.max_threads = 16; - } else if (!(vma->vm_flags & VM_SHARED) && total_sz >= min_chunk_sz * 3) { + if (!(vma->vm_flags & VM_SHARED) && total_sz >= min_chunk_sz * 3) { unsigned long swapped_sz; swapped_sz = get_mm_counter(tlb->mm, MM_SWAPENTS) << PAGE_SHIFT; @@ -2079,6 +2069,17 @@ static void unmap_page_range_mt(struct mmu_gather *tlb, padata_do_multithreaded(&job); } +static int zap_vma_chunk(unsigned long addr, unsigned long end, void *arg) +{ + struct vm_area_struct *vma = arg; + struct mmu_gather tlb; + + tlb_gather_mmu(&tlb, vma->vm_mm); + unmap_page_range(&tlb, vma, addr, end, NULL); + tlb_finish_mmu(&tlb); + return 0; +} + #endif /* CONFIG_PADATA */ static void unmap_single_vma(struct mmu_gather *tlb, @@ -2214,6 +2215,45 @@ void zap_page_range_single(struct vm_area_struct *vma, unsigned long address, hugetlb_zap_end(vma, details); } +/** + * zap_vma_mt - remove all ptes of a vma, with several threads if it is large + * @vma: vm_area_struct holding ptes to be zapped + * + * The caller holds the mmap lock for writing. An anonymous vma is zapped in + * PMD-aligned parts, each with an mmu_gather of its own, by several threads + * when it is large; other vmas, and all of them without CONFIG_PADATA, are + * zapped by the caller alone. + */ +void zap_vma_mt(struct vm_area_struct *vma) +{ +#ifdef CONFIG_PADATA + struct padata_mt_job job = { + .thread_fn = zap_vma_chunk, + .fn_arg = vma, + .start = vma->vm_start, + .size = vma->vm_end - vma->vm_start, + .align = PMD_SIZE, + .min_chunk = max(1ul << 27, PMD_SIZE), + .max_threads = 16, + }; + struct mmu_notifier_range range; + + mmap_assert_write_locked(vma->vm_mm); + if (vma_is_anonymous(vma)) { + lru_add_drain(); + mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm, + vma->vm_start, vma->vm_end); + update_hiwater_rss(vma->vm_mm); + mmu_notifier_invalidate_range_start(&range); + padata_do_multithreaded(&job); + mmu_notifier_invalidate_range_end(&range); + return; + } +#endif + zap_page_range_single(vma, vma->vm_start, vma->vm_end - vma->vm_start, + NULL); +} + /** * zap_vma_ptes - remove ptes mapping the vma * @vma: vm_area_struct holding ptes to be zapped -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them Konstantin Khorenko @ 2026-10-14 17:24 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:24 UTC (permalink / raw) To: Vasileios Almpanis, Konstantin Khorenko, Pavel Tikhomirov, Andrey Drobyshev Cc: devel Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] mm: zap_vma_mt() clears old_mm PTEs without vma_start_write() -- commit f34df816bcbe51e2402a6b8ae419dea8ef679f49 Author: Konstantin Khorenko <khorenko@virtuozzo.com> oracle/mm: zap the preserved ranges of the old mm after copying them After the VM_EXEC_KEEP ranges are copied into the new mm, this patch zaps them from the old mm under its write lock. This is meant to leave the copies as the only mappings of the preserved pages. A new zap_vma_mt() helper spreads the zap of an anonymous VMA over several threads. Link: https://virtuozzo.atlassian.net/browse/VSTOR-150231 > diff --git a/fs/exec.c b/fs/exec.c > --- a/fs/exec.c > +++ b/fs/exec.c [ ... ] > @@ -997,7 +997,15 @@ static int vma_dup_some(struct mm_struct *old_mm, struct mm_struct *new_mm) > } > > mmap_write_unlock(new_mm); > - return ret; > + if (ret) > + return ret; > + > + /* Whoever keeps old_mm alive past the exec must not reach the pages through it. */ > + vma_iter_set(&vmi, 0); > + for_each_vma(vmi, vma) > + if (vma->vm_flags & VM_EXEC_KEEP) > + zap_vma_mt(vma); > + return 0; > } [Severity: Medium] Can this zap race with a per-VMA lock holder on old_mm? The loop calls zap_vma_mt() on old_mm VMAs without calling vma_start_write(vma) first: vma_dup_some() for_each_vma(vmi, vma) zap_vma_mt(vma) mmap_write_lock() alone does not drain holders that already took the per-VMA read lock through lock_vma_under_rcu(). One such holder is UFFDIO_MOVE: uffd_move_lock() *src_vmap = lock_vma_under_rcu(mm, src_start); This path never takes mmap_lock. A task that owns a userfaultfd context bound to old_mm has mm_users on old_mm, and its own t->mm is a different mm. reserve_preserved_mem() only rejects tasks whose t->mm == mm, so such a task is not excluded. It could therefore run folio_move_anon_rmap() or a fault in old_mm while the zap clears the same PTEs. That would reintroduce the problems the commit message describes, since the preserved pages still carry PageAnonExclusive in both mms. Would it make sense to call vma_start_write(vma) before zapping each VM_EXEC_KEEP VMA, either in vma_dup_some() or at the start of zap_vma_mt()? > diff --git a/mm/memory.c b/mm/memory.c > --- a/mm/memory.c > +++ b/mm/memory.c [ ... ] > +void zap_vma_mt(struct vm_area_struct *vma) > +{ > +#ifdef CONFIG_PADATA > + struct padata_mt_job job = { [ ... ] > + mmap_assert_write_locked(vma->vm_mm); > + if (vma_is_anonymous(vma)) { > + lru_add_drain(); > + mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm, > + vma->vm_start, vma->vm_end); > + update_hiwater_rss(vma->vm_mm); > + mmu_notifier_invalidate_range_start(&range); > + padata_do_multithreaded(&job); > + mmu_notifier_invalidate_range_end(&range); > + return; > + } > +#endif > + zap_page_range_single(vma, vma->vm_start, vma->vm_end - vma->vm_start, > + NULL); > +} [Severity: Medium] The same concern applies here. zap_vma_mt() only asserts the mmap write lock with mmap_assert_write_locked() before clearing PTEs, either in the padata worker threads through zap_vma_chunk() or in zap_page_range_single(). It never calls vma_start_write(vma). For the anonymous case, can PTEs be cleared while a per-VMA locked fault or UFFDIO_MOVE is still operating on the same VMA? Both the padata path and the zap_page_range_single() path appear to be exposed. -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=6 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* [Devel] [PATCH hci-8.1 7/8] exec: protect preserved memory against surviving shared-mm tasks 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko ` (5 preceding siblings ...) 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:24 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec Konstantin Khorenko 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com> exec_mmap() copies the MADV_DOEXEC ranges into the new mm and then zaps them in the old one, so that the copies are the only mappings left. de_thread() removes the thread group, but a task sharing the mm via CLONE_VM without CLONE_THREAD survives exec. It races with the copy, writing into pages the new image takes over, and afterwards finds the ranges empty and silently goes on with fresh memory. Refuse such an exec before the point of no return: - begin_new_exec(): when the binary accepts preserved memory and the mm has a VM_EXEC_KEEP VMA, mark the mm as being preserved and return EBUSY if a task outside the thread group has it as its mm. Sibling threads, which de_thread() removes, and plain mm references such as /proc readers are allowed. - copy_mm(): refuse a CLONE_VM clone that is not a thread while the mm is marked, and count such clones in flight until the child is on the task list, so the check above cannot miss one. The check walks the task list under RCU; it sees every child whose count was dropped, since the drop follows the insertion and both are ordered by page_table_lock. - exec_mmap() and free_bprm(): clear the mark once the mm is switched or the exec fails. - exec_mmap(): take the preserve path, including the source mmap write lock, only when the mark was set (bprm->preserving_mem), so an accepting binary without a VM_EXEC_KEEP VMA takes only the read lock, as an exec without preserved memory does. A kernel thread running on the mm through kthread_use_mm() is outside the thread group as well, so it makes the exec fail the same way. This covers a vhost worker in kthread mode, the default here, for as long as its device has an owner: the caller has to reset the owner first, as the worker would otherwise go on writing guest memory into the old mm. A kernel thread that takes the mm only after the check, for instance to complete an AIO or fence request made before the exec, is not refused: a write before the zap of the old ranges reaches the preserved pages, as one made just before the exec would, and a later one lands in fresh memory of the old mm and is lost, as after any exec. Fixes: 37d9f55655ea ("oracle/mm: do not write protect COW mappings when preserving across exec") https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com> [khorenko: rationale after the zap of the old ranges, count only clones that are not threads, walk the task list under RCU, keep the upstream begin_new_exec() comment, note kthread_use_mm() users] Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- fs/exec.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++-- include/linux/binfmts.h | 4 ++- include/linux/mm_types.h | 4 +++ kernel/fork.c | 37 ++++++++++++++++++++++ 4 files changed, 124 insertions(+), 3 deletions(-) diff --git a/fs/exec.c b/fs/exec.c index 32567010a2f27..5bf856e829af3 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -978,6 +978,78 @@ ssize_t read_code(struct file *file, unsigned long addr, loff_t pos, size_t len) EXPORT_SYMBOL(read_code); #endif +/* The reservation owns no mm reference; release it before dropping old_mm. */ +static void release_preserved_mem(struct linux_binprm *bprm, struct mm_struct *mm) +{ + if (!bprm->preserving_mem) + return; + spin_lock(&mm->page_table_lock); + mm->exec_keep_active = false; + spin_unlock(&mm->page_table_lock); + bprm->preserving_mem = false; +} + +/* + * Reserve the marked address space before exec becomes irreversible. Tasks + * outside our thread group survive de_thread() and would find the preserved + * ranges empty once exec_mmap() zaps them in this mm. + */ +static int reserve_preserved_mem(struct linux_binprm *bprm) +{ + struct mm_struct *mm = current->mm; + struct vm_area_struct *vma; + struct task_struct *p, *t; + bool preserve = false; + int ret = 0; + + if (!bprm->accepts_preserved_mem || !mm) + return 0; + + if (mmap_read_lock_killable(mm)) + return -EINTR; + { + VMA_ITERATOR(vmi, mm, 0); + + for_each_vma(vmi, vma) { + if (vma->vm_flags & VM_EXEC_KEEP) { + preserve = true; + break; + } + } + } + mmap_read_unlock(mm); + if (!preserve) + return 0; + + /* A clone is either counted here or already visible in the task list. */ + spin_lock(&mm->page_table_lock); + if (mm->exec_keep_active || mm->exec_keep_clones) + ret = -EBUSY; + else + mm->exec_keep_active = true; + spin_unlock(&mm->page_table_lock); + if (ret) + return ret; + bprm->preserving_mem = true; + + rcu_read_lock(); + for_each_process_thread(p, t) { + if (same_thread_group(current, t)) + continue; + task_lock(t); + if (t->mm == mm) + ret = -EBUSY; + task_unlock(t); + if (ret) + goto out; + } +out: + rcu_read_unlock(); + if (ret) + release_preserved_mem(bprm, mm); + return ret; +} + static int vma_dup_some(struct mm_struct *old_mm, struct mm_struct *new_mm) { struct vm_area_struct *vma; @@ -1040,7 +1112,7 @@ static int exec_mmap(struct linux_binprm *bprm) up_write(&tsk->signal->exec_update_lock); return ret; } - if (unlikely(bprm->accepts_preserved_mem)) { + if (unlikely(bprm->preserving_mem)) { /* * Acquire the write lock to avoid changes to page * table entries while they are copied. @@ -1085,13 +1157,14 @@ static int exec_mmap(struct linux_binprm *bprm) task_unlock(tsk); lru_gen_use_mm(mm); if (old_mm) { - if (unlikely(bprm->accepts_preserved_mem)) + if (unlikely(bprm->preserving_mem)) mmap_write_unlock(old_mm); else mmap_read_unlock(old_mm); BUG_ON(active_mm != old_mm); setmax_mm_hiwater_rss(&tsk->signal->maxrss, old_mm); mm_update_next_owner(old_mm); + release_preserved_mem(bprm, old_mm); mmput(old_mm); return 0; } @@ -1300,6 +1373,10 @@ int begin_new_exec(struct linux_binprm * bprm) if (retval) return retval; + retval = reserve_preserved_mem(bprm); + if (retval) + return retval; + /* * This tracepoint marks the point before flushing the old exec where * the current task is still unchanged, but errors are fatal (point of @@ -1552,6 +1629,7 @@ static void do_close_execat(struct file *file) static void free_bprm(struct linux_binprm *bprm) { + release_preserved_mem(bprm, current->mm); if (bprm->mm) { acct_arg_size(bprm, 0); mmput(bprm->mm); diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index 9f03c39498e92..f42487c043178 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -47,7 +47,9 @@ struct linux_binprm { * Set if the binary being exec'd will accept memory marked * for preservation by the outgoing process. */ - accepts_preserved_mem:1; + accepts_preserved_mem:1, + /* Outgoing mm has VM_EXEC_KEEP VMAs and is reserved for exec. */ + preserving_mem:1; #ifdef __alpha__ unsigned int taso:1; #endif diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h index cf50c74516b09..befdf38ef855b 100644 --- a/include/linux/mm_types.h +++ b/include/linux/mm_types.h @@ -994,6 +994,10 @@ struct mm_struct { unsigned long flags; /* Must use atomic bitops to access */ unsigned int vps_dumpable:2; + + /* Shared-mm clone publication, serialized by page_table_lock. */ + unsigned int exec_keep_clones; + bool exec_keep_active; #ifdef CONFIG_AIO spinlock_t ioctx_lock; struct kioctx_table __rcu *ioctx_table; diff --git a/kernel/fork.c b/kernel/fork.c index 00d9b4febd85b..92a7e9aae9278 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -1270,6 +1270,8 @@ static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p, mt_init_flags(&mm->mm_mt, MM_MT_FLAGS); mt_set_external_lock(&mm->mm_mt, &mm->mmap_lock); atomic_set(&mm->mm_users, 1); + mm->exec_keep_clones = 0; + mm->exec_keep_active = false; atomic_set(&mm->mm_count, 1); seqcount_init(&mm->write_protect_seq); mmap_init_lock(mm); @@ -1710,9 +1712,32 @@ static struct mm_struct *dup_mm(struct task_struct *tsk, return NULL; } +/* Keep unpublished CLONE_VM tasks visible to preserving-exec admission. */ +static int exec_keep_clone_begin(struct mm_struct *mm) +{ + int ret = 0; + + spin_lock(&mm->page_table_lock); + if (mm->exec_keep_active) + ret = -EBUSY; + else + mm->exec_keep_clones++; + spin_unlock(&mm->page_table_lock); + return ret; +} + +static void exec_keep_clone_end(struct mm_struct *mm) +{ + spin_lock(&mm->page_table_lock); + if (!WARN_ON_ONCE(!mm->exec_keep_clones)) + mm->exec_keep_clones--; + spin_unlock(&mm->page_table_lock); +} + static int copy_mm(unsigned long clone_flags, struct task_struct *tsk) { struct mm_struct *mm, *oldmm; + int ret; tsk->min_flt = tsk->maj_flt = 0; tsk->nvcsw = tsk->nivcsw = 0; @@ -1734,6 +1759,12 @@ static int copy_mm(unsigned long clone_flags, struct task_struct *tsk) return 0; if (clone_flags & CLONE_VM) { + /* de_thread() takes care of our own threads. */ + if (!(clone_flags & CLONE_THREAD)) { + ret = exec_keep_clone_begin(oldmm); + if (ret) + return ret; + } mmget(oldmm); mm = oldmm; } else { @@ -2638,6 +2669,10 @@ __latent_entropy struct task_struct *copy_process( syscall_tracepoint_update(p); write_unlock_irq(&tasklist_lock); + /* Admission can now find this shared-mm task in the task list. */ + if ((clone_flags & (CLONE_VM | CLONE_THREAD)) == CLONE_VM && p->mm) + exec_keep_clone_end(p->mm); + if (pidfile) fd_install(pidfd, pidfile); @@ -2679,6 +2714,8 @@ __latent_entropy struct task_struct *copy_process( exit_ve_namespace(p); bad_fork_cleanup_mm: if (p->mm) { + if ((clone_flags & (CLONE_VM | CLONE_THREAD)) == CLONE_VM) + exec_keep_clone_end(p->mm); mm_clear_owner(p->mm, p); mmput(p->mm); } -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 7/8] exec: protect preserved memory against surviving shared-mm tasks 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 7/8] exec: protect preserved memory against surviving shared-mm tasks Konstantin Khorenko @ 2026-10-14 17:24 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:24 UTC (permalink / raw) To: Pavel Tikhomirov, Vasileios Almpanis, Andrey Drobyshev, Konstantin Khorenko Cc: devel Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Low] fork: copy_mm()/copy_process() take page_table_lock twice per shared-mm clone - [Low] exec: VM_EXEC_KEEP scan in reserve_preserved_mem() is stale by exec_mmap() -- commit 3fae5b8a82902f91e53a5e8e729322cb7b034228 Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com> exec: protect preserved memory against surviving shared-mm tasks This patch makes an exec that accepts preserved memory fail with EBUSY when a task outside the thread group still uses the mm and the mm has a VM_EXEC_KEEP VMA. copy_mm() refuses and counts CLONE_VM, non-thread clones while the mm is marked. exec_mmap() takes the preserve path only when the mm was marked. Link: https://virtuozzo.atlassian.net/browse/VSTOR-150231 > diff --git a/fs/exec.c b/fs/exec.c > --- a/fs/exec.c > +++ b/fs/exec.c [ ... ] > +static int reserve_preserved_mem(struct linux_binprm *bprm) > +{ > + struct mm_struct *mm = current->mm; [ ... ] > + mmap_read_unlock(mm); > + if (!preserve) > + return 0; [Severity: Low] Can the result of this scan be stale by the time exec_mmap() runs? When no VM_EXEC_KEEP VMA is found here, nothing is reserved: exec_keep_active and bprm->preserving_mem both stay unset. Sibling threads have not yet been removed by de_thread(), and copy_mm() only refuses clones while exec_keep_active is set. So a sibling thread or a CLONE_VM task can still call madvise(MADV_DOEXEC) after mmap_read_unlock() and set VM_EXEC_KEEP on a range. > @@ -1040,7 +1112,7 @@ static int exec_mmap(struct linux_binprm *bprm) > up_write(&tsk->signal->exec_update_lock); > return ret; > } > - if (unlikely(bprm->accepts_preserved_mem)) { > + if (unlikely(bprm->preserving_mem)) { Here the preserve path (write lock plus vma_dup_some()) is now selected by the early sample rather than by the VMAs present under the lock at copy time. A VM_EXEC_KEEP VMA created after the scan in reserve_preserved_mem() would not be copied, and the range would be lost with the old mm without any error. Before this patch, accepts_preserved_mem alone selected the path, and vma_dup_some() copied any VM_EXEC_KEEP VMA present under mmap_write_lock. Should the decision be revalidated, or the mark taken before the scan, so that a range marked during begin_new_exec() is not silently dropped? [ ... ] > diff --git a/kernel/fork.c b/kernel/fork.c > --- a/kernel/fork.c > +++ b/kernel/fork.c [ ... ] > +/* Keep unpublished CLONE_VM tasks visible to preserving-exec admission. */ > +static int exec_keep_clone_begin(struct mm_struct *mm) > +{ > + int ret = 0; > + > + spin_lock(&mm->page_table_lock); > + if (mm->exec_keep_active) > + ret = -EBUSY; > + else > + mm->exec_keep_clones++; > + spin_unlock(&mm->page_table_lock); > + return ret; > +} > + > +static void exec_keep_clone_end(struct mm_struct *mm) > +{ > + spin_lock(&mm->page_table_lock); > + if (!WARN_ON_ONCE(!mm->exec_keep_clones)) > + mm->exec_keep_clones--; > + spin_unlock(&mm->page_table_lock); > +} [Severity: Low] This isn't a correctness problem, but is the cost of this accounting intended for every spawn? exec_keep_clone_begin() in copy_mm() and exec_keep_clone_end() in copy_process() each take and release the parent's mm->page_table_lock. This happens on every CLONE_VM clone that is not a thread (vfork(), posix_spawn(), clone(CLONE_VM)), whether or not the mm has any VM_EXEC_KEEP VMA and whether or not an exec is in progress. page_table_lock also protects page table operations, so a multithreaded parent taking page faults can contend with it. Would an atomic counter, or skipping the accounting until the mm has ever had MADV_DOEXEC applied, avoid the two lock round trips per spawn? [ ... ] -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=7 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko ` (6 preceding siblings ...) 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 7/8] exec: protect preserved memory against surviving shared-mm tasks Konstantin Khorenko @ 2026-10-14 17:11 ` Konstantin Khorenko 2026-10-14 17:33 ` no-reply 7 siblings, 1 reply; 17+ messages in thread From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw) To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com> Add doexec, a MADV_DOEXEC test with the ELF opt-in note, and wire it into run_vmtests.sh. The pin cases need CONFIG_GUP_TEST. What it checks: - CPU writes match what a long-term pin held through gup_test reads back, across two execs and a later fork, for base pages, PMD huge pages, mlocked pages, a read-only mapping, read-only pins and fast GUP pins. - A COW mapping shared with a child forked before exec stays intact on both sides. - exec is refused with EBUSY while a CLONE_VM task outside the thread group has the mm, and succeeds once that task is gone. - Own threads, a non-leader thread calling exec, /proc readers during repeated exec and a vhost device whose owner was reset before exec do not block it. - A CLONE_VM clone racing with exec does not end up sharing the preserved memory. - Optionally, a sweep over task-local allocation failures. https://virtuozzo.atlassian.net/browse/VSTOR-150231 Feature: oracle/mm: MADV_DOEXEC madvise() flag Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com> [khorenko: exec through an fd opened before fail-nth is armed, as /proc fails the path lookup with ENOENT; accept E2BIG, ELIBBAD and EIO, which exec returns for some failed allocations; report an unexpected child status; add the binary to .gitignore; use %note, which every architecture's assembler accepts; accept EINVAL from MADV_NOHUGEPAGE on kernels without THP; run it in a doexec category of its own; let the blocked CLONE_VM child drop its copy of the pipe write end] Assisted-by: LLM Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- tools/testing/selftests/mm/.gitignore | 1 + tools/testing/selftests/mm/Makefile | 1 + tools/testing/selftests/mm/doexec.c | 814 ++++++++++++++++++++++++++++++ tools/testing/selftests/mm/run_vmtests.sh | 4 + 4 files changed, 820 insertions(+) diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore index 8f01f4da1c0de..2bdaebdbb87ea 100644 --- a/tools/testing/selftests/mm/.gitignore +++ b/tools/testing/selftests/mm/.gitignore @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-only cow +doexec hugepage-mmap hugepage-mremap hugepage-shm diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile index c837b0549c892..7f9efb0e0a386 100644 --- a/tools/testing/selftests/mm/Makefile +++ b/tools/testing/selftests/mm/Makefile @@ -74,6 +74,7 @@ endif TEST_GEN_FILES = cow TEST_GEN_FILES += compaction_test +TEST_GEN_FILES += doexec TEST_GEN_FILES += gup_longterm TEST_GEN_FILES += gup_test TEST_GEN_FILES += hmm-tests diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c new file mode 100644 index 0000000000000..33529aa41367b --- /dev/null +++ b/tools/testing/selftests/mm/doexec.c @@ -0,0 +1,814 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Verify that exec preserves the memory backing long-term DMA pins. */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <linux/vhost.h> +#include <poll.h> +#include <pthread.h> +#include <sched.h> +#include <signal.h> +#include <stdbool.h> +#include <stdatomic.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <sys/resource.h> +#include <sys/syscall.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../../../../mm/gup_test.h" +#include "../kselftest.h" + +#ifndef MADV_DOEXEC +#define MADV_DOEXEC 201 +#endif +#ifndef MADV_COLLAPSE +#define MADV_COLLAPSE 25 +#endif + +/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */ +asm(".pushsection .note.doexec,\"a\",%note\n" + ".balign 4\n" + ".long 17, 0, 0x07c1feed\n" + ".asciz \"preserved-mem-ok\"\n" + ".balign 4\n" + ".popsection\n"); + +enum test_kind { + BASE, + HUGE, + LOCKED, + PROTECTED, + READ_PIN, + FAST_PIN, + NR_TESTS, +}; + +static const char * const names[] = { + "base pages", + "PMD huge pages", + "mlocked pages", + "read-only mapping", + "read-only pins", + "fast GUP pins", +}; + +static int wait_child(pid_t pid) +{ + int status; + pid_t ret; + + do { + ret = waitpid(pid, &status, 0); + } while (ret < 0 && errno == EINTR); + if (ret < 0 || !WIFEXITED(status)) + return KSFT_FAIL; + return WEXITSTATUS(status); +} + +static int check_pin(int fd, char *ram, size_t size, unsigned char value) +{ + char *copy = malloc(size); + uint64_t addr = (uintptr_t)copy; + size_t i; + int ret = KSFT_FAIL; + + if (!copy) + return ret; + if (ioctl(fd, PIN_LONGTERM_TEST_READ, &addr)) { + ksft_print_msg("read pin: %s\n", strerror(errno)); + goto out; + } + for (i = 0; i < size; i++) { + if ((unsigned char)ram[i] != value || + (unsigned char)copy[i] != value) { + ksft_print_msg("CPU/pin mismatch at byte %zu\n", i); + goto out; + } + } + ret = KSFT_PASS; +out: + free(copy); + return ret; +} + +static int check_bytes(char *ram, size_t size, unsigned char value) +{ + size_t i; + + for (i = 0; i < size; i++) + if ((unsigned char)ram[i] != value) + return KSFT_FAIL; + return KSFT_PASS; +} + +static int after_cow_exec(char *ram, size_t size, pid_t sibling, int pipe_fd) +{ + char ready = 1; + int ret = check_bytes(ram, size, 0x31); + + memset(ram, 0x42, size); + if (write(pipe_fd, &ready, 1) != 1) + ret = KSFT_FAIL; + close(pipe_fd); + if (wait_child(sibling)) + ret = KSFT_FAIL; + return ret; +} + +/* Pages shared with a separate mm must retain their pre-existing COW state. */ +static int before_cow_exec(void) +{ + size_t size = 16 * sysconf(_SC_PAGESIZE); + char addr_arg[32], size_arg[32], pid_arg[32], fd_arg[32], ready; + int pipe_fd[2]; + char *ram; + pid_t pid; + + ram = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (ram == MAP_FAILED) + return KSFT_FAIL; + memset(ram, 0x31, size); + if (madvise(ram, size, MADV_DOEXEC)) + return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL; + if (pipe(pipe_fd)) + return KSFT_FAIL; + pid = fork(); + if (pid < 0) + return KSFT_FAIL; + if (!pid) { + close(pipe_fd[1]); + if (read(pipe_fd[0], &ready, 1) != 1) + _exit(KSFT_FAIL); + _exit(check_bytes(ram, size, 0x31)); + } + close(pipe_fd[0]); + snprintf(addr_arg, sizeof(addr_arg), "%llu", + (unsigned long long)(uintptr_t)ram); + snprintf(size_arg, sizeof(size_arg), "%zu", size); + snprintf(pid_arg, sizeof(pid_arg), "%d", pid); + snprintf(fd_arg, sizeof(fd_arg), "%d", pipe_fd[1]); + execl("/proc/self/exe", "doexec", "--cow", addr_arg, size_arg, + pid_arg, fd_arg, NULL); + close(pipe_fd[1]); + wait_child(pid); + return KSFT_FAIL; +} + +static void exec_stage(int fd, char *ram, size_t size, int kind, int stage) +{ + char fd_arg[32], addr_arg[32], size_arg[32], kind_arg[32], stage_arg[32]; + + snprintf(fd_arg, sizeof(fd_arg), "%d", fd); + snprintf(addr_arg, sizeof(addr_arg), "%llu", + (unsigned long long)(uintptr_t)ram); + snprintf(size_arg, sizeof(size_arg), "%zu", size); + snprintf(kind_arg, sizeof(kind_arg), "%d", kind); + snprintf(stage_arg, sizeof(stage_arg), "%d", stage); + execl("/proc/self/exe", "doexec", "--exec", fd_arg, addr_arg, size_arg, + kind_arg, stage_arg, NULL); + ksft_print_msg("exec: %s\n", strerror(errno)); + _exit(KSFT_FAIL); +} + +static int after_exec(int fd, char *ram, size_t size, int kind, int stage) +{ + unsigned char before = stage == 1 ? 0x31 : 0x42; + unsigned char after = stage == 1 ? 0x42 : 0x53; + pid_t pid; + + if (check_pin(fd, ram, size, before)) + return KSFT_FAIL; + if (kind == PROTECTED && mprotect(ram, size, PROT_READ | PROT_WRITE)) + return KSFT_FAIL; + memset(ram, after, size); + if (check_pin(fd, ram, size, after)) + return KSFT_FAIL; + if (stage == 1) { + /* VM_EXEC_KEEP is consumed, so mark again for the second exec. */ + if (madvise(ram, size, MADV_DOEXEC)) + return KSFT_FAIL; + if (kind == PROTECTED && mprotect(ram, size, PROT_READ)) + return KSFT_FAIL; + exec_stage(fd, ram, size, kind, 2); + } + + /* Fork must copy into the child and retain the parent's pinned pages. */ + pid = fork(); + if (pid < 0) + return KSFT_FAIL; + if (!pid) { + memset(ram, 0x64, size); + _exit(KSFT_PASS); + } + if (wait_child(pid) || check_pin(fd, ram, size, after)) + return KSFT_FAIL; + memset(ram, 0x75, size); + if (check_pin(fd, ram, size, 0x75)) + return KSFT_FAIL; + return ioctl(fd, PIN_LONGTERM_TEST_STOP) ? KSFT_FAIL : KSFT_PASS; +} + +static size_t huge_page_size(void) +{ + FILE *file = fopen("/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", "r"); + size_t size = 0; + + if (file) { + if (fscanf(file, "%zu", &size) != 1) + size = 0; + fclose(file); + } + return size; +} + +static int before_exec(int kind) +{ + size_t size = 16 * sysconf(_SC_PAGESIZE); + struct pin_longterm_test pin; + char *ram, *mapping; + uintptr_t aligned; + int fd; + + if (kind == HUGE) { + size = huge_page_size(); + if (!size) + return KSFT_SKIP; + } + fd = open("/sys/kernel/debug/gup_test", O_RDWR); + if (fd < 0) + return KSFT_SKIP; + mapping = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapping == MAP_FAILED) + return KSFT_FAIL; + aligned = ((uintptr_t)mapping + size - 1) & ~(size - 1); + ram = (char *)aligned; + if (ram != mapping) + munmap(mapping, ram - mapping); + if (ram + size != mapping + 2 * size) + munmap(ram + size, mapping + 2 * size - (ram + size)); + if (madvise(ram, size, kind == HUGE ? MADV_HUGEPAGE : MADV_NOHUGEPAGE) && + (kind == HUGE || errno != EINVAL)) + return KSFT_FAIL; + memset(ram, 0x31, size); + if (kind == HUGE && madvise(ram, size, MADV_COLLAPSE)) + return KSFT_SKIP; + if (kind == LOCKED && mlock(ram, size)) + return KSFT_SKIP; + if (madvise(ram, size, MADV_DOEXEC)) { + ksft_print_msg("MADV_DOEXEC: %s\n", strerror(errno)); + return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL; + } + pin = (struct pin_longterm_test) { + .addr = (uintptr_t)ram, + .size = size, + .flags = kind == READ_PIN ? 0 : PIN_LONGTERM_TEST_FLAG_USE_WRITE, + }; + if (kind == FAST_PIN) + pin.flags |= PIN_LONGTERM_TEST_FLAG_USE_FAST; + if (ioctl(fd, PIN_LONGTERM_TEST_START, &pin)) { + ksft_print_msg("pin: %s\n", strerror(errno)); + return errno == ENOTTY ? KSFT_SKIP : KSFT_FAIL; + } + if (kind == PROTECTED && mprotect(ram, size, PROT_READ)) + return KSFT_FAIL; + exec_stage(fd, ram, size, kind, 1); + return KSFT_FAIL; +} + +static int check_exec(char *ram, size_t size, size_t stride, bool reject) +{ + char addr_arg[32], size_arg[32], stride_arg[32]; + + snprintf(addr_arg, sizeof(addr_arg), "%llu", + (unsigned long long)(uintptr_t)ram); + snprintf(size_arg, sizeof(size_arg), "%zu", size); + snprintf(stride_arg, sizeof(stride_arg), "%zu", stride); + execl("/proc/self/exe", "doexec", reject ? "--reject" : "--check", addr_arg, size_arg, + stride_arg, NULL); + return errno; +} + +static char *preserved_ram(size_t size) +{ + char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + + if (ram == MAP_FAILED) + return NULL; + memset(ram, 0x31, size); + if (madvise(ram, size, MADV_DOEXEC)) { + munmap(ram, size); + return NULL; + } + return ram; +} + +static int blocked_task(void *arg) +{ + int fd = *(int *)arg; + char ready; + + return read(fd, &ready, 1) == 1 ? KSFT_PASS : KSFT_FAIL; +} + +static void *blocked_thread(void *arg) +{ + blocked_task(arg); + return NULL; +} + +/* Without its own copy of the write end, the clone ends with its parent. */ +static int blocked_clone(void *arg) +{ + close(((int *)arg)[1]); + return blocked_task(arg); +} + +/* Own threads may be killed by exec; a separate CLONE_VM task may not. */ +static int shared_mm_exec(bool own_thread) +{ + size_t size = sysconf(_SC_PAGESIZE); + char *ram = preserved_ram(size), *stack; + int pipe_fd[2], ret; + pthread_t thread; + pid_t pid; + + if (!ram) + return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL; + if (pipe(pipe_fd)) + return KSFT_FAIL; + if (own_thread) { + if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0])) + return KSFT_FAIL; + check_exec(ram, size, 1, false); + write(pipe_fd[1], "x", 1); + pthread_join(thread, NULL); + return KSFT_FAIL; + } + stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); + if (stack == MAP_FAILED) + return KSFT_FAIL; + pid = clone(blocked_clone, stack + 65536, CLONE_VM | SIGCHLD, pipe_fd); + if (pid < 0) + return KSFT_FAIL; + ret = check_exec(ram, size, 1, true); + /* Exec must return EBUSY with the old mapping intact. */ + if (ret != EBUSY || check_bytes(ram, size, 0x31)) + ret = KSFT_FAIL; + else + ret = KSFT_PASS; + write(pipe_fd[1], "x", 1); + if (wait_child(pid)) + ret = KSFT_FAIL; + if (ret) + return ret; + /* Releasing the outside user must allow a retry. */ + check_exec(ram, size, 1, false); + return KSFT_FAIL; +} + +/* The caller must release its backend owner before preserving exec. */ +static int vhost_owner_exec(void) +{ + size_t size = sysconf(_SC_PAGESIZE); + char *ram; + int fd = open("/dev/vhost-net", O_RDWR); + + if (fd < 0) + return KSFT_SKIP; + ram = preserved_ram(size); + if (!ram) + return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL; + if (ioctl(fd, VHOST_SET_OWNER)) + return KSFT_SKIP; + if (ioctl(fd, VHOST_RESET_OWNER)) + return KSFT_FAIL; + close(fd); + check_exec(ram, size, 1, false); + return KSFT_FAIL; +} + +static void *exec_thread(void *arg) +{ + check_exec(arg, sysconf(_SC_PAGESIZE), 1, false); + return NULL; +} + +static int nonleader_exec(void) +{ + char *ram = preserved_ram(sysconf(_SC_PAGESIZE)); + pthread_t thread; + + if (!ram) + return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL; + if (pthread_create(&thread, NULL, exec_thread, ram)) + return KSFT_FAIL; + pthread_join(thread, NULL); + return KSFT_FAIL; +} + +static int proc_exec(char *ram, size_t size, int stage) +{ + char addr_arg[32], size_arg[32], stage_arg[32]; + + if (check_bytes(ram, size, 0x31)) + return KSFT_FAIL; + if (stage == 32) + return KSFT_PASS; + if (madvise(ram, size, MADV_DOEXEC)) + return KSFT_FAIL; + snprintf(addr_arg, sizeof(addr_arg), "%llu", + (unsigned long long)(uintptr_t)ram); + snprintf(size_arg, sizeof(size_arg), "%zu", size); + snprintf(stage_arg, sizeof(stage_arg), "%d", stage + 1); + execl("/proc/self/exe", "doexec", "--proc", addr_arg, size_arg, + stage_arg, NULL); + return KSFT_FAIL; +} + +/* Read both pre-opened and freshly opened proc files during repeated exec. */ +static int proc_readers_exec(void) +{ + static const char * const files[] = { "maps", "smaps_rollup", "pagemap" }; + int ready[2], go[2], fds[3] = { -1, -1, -1 }, status, ret = KSFT_FAIL; + char paths[3][80], buffer[4096]; + unsigned int round = 0; + pid_t pid, waited; + int i; + + if (pipe(ready) || pipe(go)) + return KSFT_FAIL; + pid = fork(); + if (pid < 0) + return KSFT_FAIL; + if (!pid) { + size_t size = 16 * sysconf(_SC_PAGESIZE); + char *ram = preserved_ram(size); + + close(ready[0]); + close(go[1]); + if (!ram) + _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL); + if (write(ready[1], "x", 1) != 1 || read(go[0], buffer, 1) != 1) + _exit(KSFT_FAIL); + close(ready[1]); + close(go[0]); + _exit(proc_exec(ram, size, 0)); + } + close(ready[1]); + close(go[0]); + if (read(ready[0], buffer, 1) != 1) { + close(ready[0]); + close(go[1]); + return wait_child(pid); + } + close(ready[0]); + for (i = 0; i < 3; i++) { + snprintf(paths[i], sizeof(paths[i]), "/proc/%d/%s", pid, files[i]); + fds[i] = open(paths[i], O_RDONLY); + if (fds[i] < 0) { + ret = KSFT_SKIP; + goto stop; + } + } + if (write(go[1], "x", 1) != 1) + goto stop; + close(go[1]); + go[1] = -1; + for (;;) { + waited = waitpid(pid, &status, WNOHANG); + if (waited == pid) { + ret = WIFEXITED(status) ? WEXITSTATUS(status) : KSFT_FAIL; + break; + } + if (waited < 0 && errno != EINTR) + break; + for (i = 0; i < 3; i++) { + lseek(fds[i], 0, SEEK_SET); + if (read(fds[i], buffer, sizeof(buffer)) < 0 && + errno != EINTR && errno != ESRCH) + goto stop; + } + /* Keep two old handles while refreshing the third. */ + i = round++ % 3; + close(fds[i]); + fds[i] = open(paths[i], O_RDONLY); + if (fds[i] < 0) { + ret = wait_child(pid); + break; + } + } + for (i = 0; i < 3; i++) + if (fds[i] >= 0) + close(fds[i]); + return ret; +stop: + if (go[1] >= 0) + close(go[1]); + kill(pid, SIGKILL); + wait_child(pid); + for (i = 0; i < 3; i++) + if (fds[i] >= 0) + close(fds[i]); + return ret; +} + +struct clone_race { + char *ram; + int report; + atomic_bool stop; + atomic_uint rounds; +}; + +static int clone_race_child(void *arg) +{ + struct clone_race *race = arg; + unsigned int i; + + for (i = 0; i < 8; i++) { + if (__atomic_load_n(race->ram, __ATOMIC_RELAXED) != 0x31) { + write(race->report, "x", 1); + return KSFT_FAIL; + } + usleep(1000); + } + return KSFT_PASS; +} + +static void *clone_race_thread(void *arg) +{ + struct clone_race *race = arg; + char *stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); + pid_t pid; + + if (stack == MAP_FAILED) { + write(race->report, "x", 1); + atomic_store(&race->rounds, 4); + return NULL; + } + while (!atomic_load(&race->stop)) { + pid = clone(clone_race_child, stack + 65536, + CLONE_VM | SIGCHLD, race); + if (pid >= 0) { + if (wait_child(pid)) + write(race->report, "x", 1); + } else if (errno != EBUSY && errno != EINTR) { + write(race->report, "x", 1); + } + atomic_fetch_add(&race->rounds, 1); + usleep(1000); + } + munmap(stack, 65536); + return NULL; +} + +/* A supervisor collects errors from any shared-mm child surviving exec. */ +static int clone_exec_race(void) +{ + int report[2], ret; + struct pollfd pfd; + char failed; + pid_t pid; + + if (pipe(report)) + return KSFT_FAIL; + pid = fork(); + if (pid < 0) + return KSFT_FAIL; + if (!pid) { + size_t size = sysconf(_SC_PAGESIZE); + struct clone_race race = { .report = report[1] }; + pthread_t thread; + unsigned int attempt; + + close(report[0]); + race.ram = preserved_ram(size); + if (!race.ram) + _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL); + if (pthread_create(&thread, NULL, clone_race_thread, &race)) + _exit(KSFT_FAIL); + while (atomic_load(&race.rounds) < 4) + usleep(1000); + for (attempt = 0; attempt < 500; attempt++) { + if (check_exec(race.ram, size, 1, false) != EBUSY) + break; + if (check_bytes(race.ram, size, 0x31)) + break; + usleep(1000); + } + atomic_store(&race.stop, true); + pthread_join(thread, NULL); + _exit(KSFT_FAIL); + } + close(report[1]); + ret = wait_child(pid); + pfd = (struct pollfd) { .fd = report[0], .events = POLLIN | POLLHUP }; + if (poll(&pfd, 1, 3000) <= 0 || read(report[0], &failed, 1) != 0) + ret = KSFT_FAIL; + close(report[0]); + return ret; +} + +static int check_exec_fd(int exe, char *ram, size_t size, size_t stride) +{ + char addr_arg[32], size_arg[32], stride_arg[32]; + char *args[] = { "doexec", "--check", addr_arg, size_arg, stride_arg, NULL }; + + snprintf(addr_arg, sizeof(addr_arg), "%llu", + (unsigned long long)(uintptr_t)ram); + snprintf(size_arg, sizeof(size_arg), "%zu", size); + snprintf(stride_arg, sizeof(stride_arg), "%zu", stride); + syscall(__NR_execveat, exe, "", args, environ, AT_EMPTY_PATH); + return errno; +} + +static int unexpected_status(unsigned int nth, int status) +{ + ksft_print_msg("fail-nth %u: wait status %#x\n", nth, status); + return KSFT_FAIL; +} + +/* Opt-in sweep of allocation failures, using only the child's fail-nth. */ +static int allocation_failures(void) +{ + const size_t size = 256ul << 20, stride = 2ul << 20; + struct rlimit no_core = { 0, 0 }; + unsigned int early = 0, fatal = 0, success = 0, loader = 0; + unsigned int nth; + pid_t pid; + int status; + pid_t waited; + + if (!getenv("DOEXEC_FAULT_INJECTION") || access("/proc/self/fail-nth", W_OK)) + return KSFT_SKIP; + for (nth = 1; nth <= 256; nth++) { + pid = fork(); + if (pid < 0) + return KSFT_FAIL; + if (!pid) { + char *ram, number[32]; + size_t i; + int fd, len, err, exe; + + setrlimit(RLIMIT_CORE, &no_core); + ram = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (ram == MAP_FAILED || + (madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL)) + _exit(KSFT_FAIL); + for (i = 0; i < size; i += stride) + ram[i] = 0x31; + if (madvise(ram, size, MADV_DOEXEC)) + _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL); + /* Keep the path lookup, which /proc fails with ENOENT, out of the sweep. */ + exe = open("/proc/self/exe", O_RDONLY | O_CLOEXEC); + fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC); + len = snprintf(number, sizeof(number), "%u", nth); + if (exe < 0 || fd < 0 || write(fd, number, len) != len) + _exit(KSFT_FAIL); + err = check_exec_fd(exe, ram, size, stride); + /* Disarm before checking mappings or exiting. */ + pwrite(fd, "0", 1, 0); + for (i = 0; i < size; i += stride) + if (ram[i] != 0x31) + _exit(KSFT_FAIL); + /* + * Failed allocations also surface as E2BIG from + * copy_strings(), and as ELIBBAD or EIO from reading the + * ELF interpreter. + */ + if (err == ENOMEM || err == EFAULT || err == EAGAIN || + err == E2BIG || err == ELIBBAD || err == EIO) + _exit(10); + ksft_print_msg("fail-nth %u: exec: %s\n", nth, strerror(err)); + _exit(KSFT_FAIL); + } + do { + waited = waitpid(pid, &status, 0); + } while (waited < 0 && errno == EINTR); + if (waited < 0) + return KSFT_FAIL; + if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV) + fatal++; + else if (WIFEXITED(status) && WEXITSTATUS(status) == 10) + early++; + else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_PASS) + success++; + /* A dynamic loader may exit when a post-exec allocation fails. */ + else if (WIFEXITED(status) && WEXITSTATUS(status) == 127) + loader++; + else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_SKIP) + return KSFT_SKIP; + else + return unexpected_status(nth, status); + } + ksft_print_msg("faults: %u recoverable, %u SIGSEGV, %u success, %u loader exits\n", + early, fatal, success, loader); + /* A sweep that injected no recoverable/fatal errors provides no coverage. */ + return early && fatal ? KSFT_PASS : KSFT_SKIP; +} + +int main(int argc, char **argv) +{ + pid_t pid; + int kind, ret; + + if (argc == 7 && !strcmp(argv[1], "--exec")) + return after_exec(atoi(argv[2]), + (char *)(uintptr_t)strtoull(argv[3], NULL, 10), + strtoull(argv[4], NULL, 10), atoi(argv[5]), atoi(argv[6])); + if (argc == 6 && !strcmp(argv[1], "--cow")) + return after_cow_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10), + strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5])); + if (argc == 5 && !strcmp(argv[1], "--reject")) + return KSFT_FAIL; + if (argc == 5 && !strcmp(argv[1], "--proc")) + return proc_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10), + strtoull(argv[3], NULL, 10), atoi(argv[4])); + if (argc == 5 && !strcmp(argv[1], "--check")) { + char *ram = (char *)(uintptr_t)strtoull(argv[2], NULL, 10); + size_t size = strtoull(argv[3], NULL, 10); + size_t stride = strtoull(argv[4], NULL, 10), i; + int fd = open("/proc/self/fail-nth", O_WRONLY); + + if (fd >= 0) { + write(fd, "0", 1); + close(fd); + } + for (i = 0; i < size; i += stride) + if (ram[i] != 0x31) + return KSFT_FAIL; + return KSFT_PASS; + } + ksft_print_header(); + ksft_set_plan(NR_TESTS + 8); + for (kind = 0; kind < NR_TESTS; kind++) { + fflush(stdout); + pid = fork(); + if (!pid) + _exit(before_exec(kind)); + ret = pid < 0 ? KSFT_FAIL : wait_child(pid); + if (ret == KSFT_SKIP) + ksft_test_result_skip("%s: DOEXEC, GUP test or mapping unavailable\n", + names[kind]); + else + ksft_test_result(ret == KSFT_PASS, "%s: repeated exec and fork\n", + names[kind]); + } + fflush(stdout); + pid = fork(); + if (!pid) + _exit(before_cow_exec()); + ret = pid < 0 ? KSFT_FAIL : wait_child(pid); + if (ret == KSFT_SKIP) + ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n"); + else + ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n"); + for (kind = 0; kind < 7; kind++) { + static const char * const extra_names[] = { + "CLONE_VM: recoverable EBUSY and retry", + "own threads: preserving exec allowed", + "vhost owner: reset before preserving exec", + "allocation failure sweep (opt-in)", + "non-leader thread: preserving exec allowed", + "proc readers: repeated preserving exec allowed", + "concurrent CLONE_VM: shared memory is not stolen", + }; + + fflush(stdout); + pid = fork(); + if (!pid) { + switch (kind) { + case 0: + case 1: + _exit(shared_mm_exec(kind)); + case 2: + _exit(vhost_owner_exec()); + case 3: + _exit(allocation_failures()); + case 4: + _exit(nonleader_exec()); + case 5: + _exit(proc_readers_exec()); + case 6: + _exit(clone_exec_race()); + } + } + ret = pid < 0 ? KSFT_FAIL : wait_child(pid); + if (ret == KSFT_SKIP) + ksft_test_result_skip("%s: unavailable or disabled\n", extra_names[kind]); + else + ksft_test_result(ret == KSFT_PASS, "%s\n", extra_names[kind]); + } + ksft_finished(); +} diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh index 6ac8ba7e24471..5604fc8939a33 100755 --- a/tools/testing/selftests/mm/run_vmtests.sh +++ b/tools/testing/selftests/mm/run_vmtests.sh @@ -77,6 +77,8 @@ to the -t argument containing one or more of the following categories test prctl(PR_SET_MDWE, ...) - page_frag test handling of page fragment allocation and freeing +- doexec + test memory preserved across exec with MADV_DOEXEC example: ./run_vmtests.sh -t "hmm mmap ksm" EOF @@ -297,6 +299,8 @@ fi CATEGORY="gup_test" run_test ./gup_test -ct -F 0x1 0 19 0x1000 CATEGORY="gup_test" run_test ./gup_longterm +CATEGORY="doexec" run_test ./doexec + CATEGORY="userfaultfd" run_test ./uffd-unit-tests uffd_stress_bin=./uffd-stress CATEGORY="userfaultfd" run_test ${uffd_stress_bin} anon 20 16 -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec Konstantin Khorenko @ 2026-10-14 17:33 ` no-reply 0 siblings, 0 replies; 17+ messages in thread From: no-reply @ 2026-10-14 17:33 UTC (permalink / raw) To: Konstantin Khorenko, Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis Cc: devel Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Low] selftests/mm: doexec child diagnostics are lost by _exit()/exec - [Low] selftests/mm: doexec ignores write() return values - [Low] selftests/mm: doexec treats any SIGSEGV in allocation sweep as expected -- commit eee32e73516b760bb3007e1b97633a4999581fec Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com> selftests/mm: test memory preservation across exec This commit adds doexec, a selftest for MADV_DOEXEC. It checks that memory backing long-term GUP pins and other mappings survives repeated exec and fork. It also covers CLONE_VM handling, threads, /proc readers and an optional allocation-failure sweep. The test is wired into run_vmtests.sh. Link: https://virtuozzo.atlassian.net/browse/VSTOR-150231 > diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c > new file mode 100644 > --- /dev/null > +++ b/tools/testing/selftests/mm/doexec.c [ ... ] > +static void exec_stage(int fd, char *ram, size_t size, int kind, int stage) > +{ > + char fd_arg[32], addr_arg[32], size_arg[32], kind_arg[32], stage_arg[32]; > + > [ ... ] > + execl("/proc/self/exe", "doexec", "--exec", fd_arg, addr_arg, size_arg, > + kind_arg, stage_arg, NULL); > + ksft_print_msg("exec: %s\n", strerror(errno)); > + _exit(KSFT_FAIL); > +} [Severity: Low] Can the diagnostics printed by the forked children get lost here? ksft_print_msg() writes through buffered stdio. When stdout is a pipe or a file, as under run_vmtests.sh or a CI harness, the "exec: ..." message is still in the buffer when _exit(KSFT_FAIL) runs, and _exit() does not flush it. The same applies to the other messages printed from the per-test children, for example: before_exec() -> "pin: ...", "MADV_DOEXEC: ..." check_pin() -> "read pin: ...", "CPU/pin mismatch at byte N" allocation_failures() -> "fail-nth N: ..." In main(), the fflush(stdout) calls run in the parent before fork(). They prevent duplicated output but do not flush what the child prints itself. Messages printed before the execl() into the --exec stage are likewise discarded by the exec. A failing run would then show only a bare "not ok" line. Would an fflush(stdout) before each _exit() and execl() in the children be appropriate? > +static int shared_mm_exec(bool own_thread) > +{ [ ... ] > + if (own_thread) { > + if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0])) > + return KSFT_FAIL; > + check_exec(ram, size, 1, false); > + write(pipe_fd[1], "x", 1); > + pthread_join(thread, NULL); > + return KSFT_FAIL; > + } [ ... ] > + write(pipe_fd[1], "x", 1); > + if (wait_child(pid)) > + ret = KSFT_FAIL; [Severity: Low] Is it intended that the write() results are ignored here? write() is warn_unused_result under glibc _FORTIFY_SOURCE, so this can produce -Wunused-result warnings. If this write ever failed, the CLONE_VM child or thread would stay blocked in read(), and wait_child() or pthread_join() would hang because the test has no timeout. > +static int clone_race_child(void *arg) > +{ > + struct clone_race *race = arg; > + unsigned int i; > + > + for (i = 0; i < 8; i++) { > + if (__atomic_load_n(race->ram, __ATOMIC_RELAXED) != 0x31) { > + write(race->report, "x", 1); > + return KSFT_FAIL; > + } > + usleep(1000); > + } > + return KSFT_PASS; > +} [Severity: Low] Should the result of this write() be checked as well? This write is the only signal that the shared memory was stolen. If it fails, clone_exec_race() sees EOF on the report pipe and passes despite the detected failure. clone_race_thread() and the --check handler in main() also drop write() results. > + if (argc == 5 && !strcmp(argv[1], "--check")) { > + char *ram = (char *)(uintptr_t)strtoull(argv[2], NULL, 10); > + size_t size = strtoull(argv[3], NULL, 10); > + size_t stride = strtoull(argv[4], NULL, 10), i; > + int fd = open("/proc/self/fail-nth", O_WRONLY); > + > + if (fd >= 0) { > + write(fd, "0", 1); > + close(fd); > + } > + for (i = 0; i < size; i += stride) > + if (ram[i] != 0x31) > + return KSFT_FAIL; > + return KSFT_PASS; > + } [ ... ] > + if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV) > + fatal++; [ ... ] > + /* A sweep that injected no recoverable/fatal errors provides no coverage. */ > + return early && fatal ? KSFT_PASS : KSFT_SKIP; > +} [Severity: Low] Can the allocation_failures() sweep pass even when the preserved mapping has been lost? do_execveat_common() deliberately raises SIGSEGV when exec fails after the point of no return: if (bprm->point_of_no_return && !fatal_signal_pending(current)) force_fatal_sig(SIGSEGV); So SIGSEGV is a legitimate outcome of the sweep. However, the test cannot tell that kernel-forced SIGSEGV apart from a user-space fault. After a successful preserving exec, the --check image dereferences ram[i] in main(). If a kernel bug dropped or unmapped the preserved range, that load would also raise SIGSEGV. allocation_failures() would count it as fatal++. The final check is "early && fatal ? KSFT_PASS : KSFT_SKIP", and it requires at least one SIGSEGV. A regression that loses the mapping can therefore help the test pass. Is there a way to distinguish the two cases? One option is to have the --check image report a lost mapping without faulting, for example by probing the range with mincore() or msync() before reading it. -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=8 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-10-14 17:35 UTC | newest] Thread overview: 17+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 1/8] oracle/mm: fix VM_BUG_ON hit when preserving memory Konstantin Khorenko 2026-10-14 17:16 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 2/8] oracle/mm: hold the source mmap write lock when copying PTEs Konstantin Khorenko 2026-10-14 17:15 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 3/8] oracle/mm: do not write protect COW mappings when preserving across exec Konstantin Khorenko 2026-10-14 17:20 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 4/8] oracle/mm: write protect preserved PTEs but keep them exclusive Konstantin Khorenko 2026-10-14 17:18 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 5/8] oracle/mm: set MMF_HAS_PINNED on the mm receiving preserved pages Konstantin Khorenko 2026-10-14 17:19 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them Konstantin Khorenko 2026-10-14 17:24 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 7/8] exec: protect preserved memory against surviving shared-mm tasks Konstantin Khorenko 2026-10-14 17:24 ` no-reply 2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec Konstantin Khorenko 2026-10-14 17:33 ` no-reply
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.