* [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order
@ 2026-10-28 12:22 Konstantin Khorenko
2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko
` (3 more replies)
0 siblings, 4 replies; 13+ messages in thread
From: Konstantin Khorenko @ 2026-10-28 12:22 UTC (permalink / raw)
To: Pavel Tikhomirov; +Cc: devel
memory.numa_migrate, our memcg file that vcmmd uses to move a cgroup's
memory between NUMA nodes, allocates a 2 MB destination for every large
folio it migrates. Page cache folios of a smaller order get a
destination of the wrong size, and the page cache falls apart around
it. Patch 1 is the fix, patch 2 a selftest that catches it without
crashing the node.
What the customer hit
=====================
A two-node VMware host runs VMs with vcmmd's NUMA policy enabled, so
every few minutes vcmmd writes memory.numa_migrate for a VM's cgroup.
qemu writes its backup scratch file on NFS, and NFS sizes each page
cache folio to the write, so that file is full of order-1 to order-5
folios. Closing the scratch file dies in nfs4_evict_inode():
kernel BUG at fs/inode.c:737!
RIP: 0010:clear_inode+0x75/0x80
mapping->nrpages of the inode is negative, and the vmcore holds
order-9 folios of that file with an index not aligned to 512, the
refcount of a small folio, and whole xarray ranges of live folios that
are no longer in the tree. Reads of the migrated ranges return garbage,
so the backups taken on that host while the policy was active should
be verified. The same crash was filed as VSTOR-151676.
Where the 2 MB comes from
=========================
memcg_numa_migrate_new_folio() is the migrate_pages() allocation
callback. Its large folio branch dates from vz8 and copies the 4.18
new_page() in mm/mempolicy.c:
if (thp_migration_supported() && PageTransHuge(page))
alloc ... HPAGE_PMD_ORDER
A compound page on the LRU could only be a PMD-sized THP then, so the
test implied the order. Upstream replaced that with folio_order() of
the source when its allocators moved to folios, ffe06786b540
("mm/migrate: Use a folio in alloc_migration_target()"). Our copy kept
the shape, only PageTransHuge() became folio_test_large(), which is
folio_test_head() and is true for any order from 1 up. NFS and XFS
large folios, shmem and anonymous mTHP now all reach it.
Why an oversized destination breaks the page cache
==================================================
The two sides of the page cache size the folio differently.
On insertion __folio_migrate_mapping() takes everything from the
source: nr = folio_nr_pages(old), the xarray state at the old index
with no xas_set_order(), newfolio->index = old->index,
folio_ref_add(newfolio, nr), and xas_store() replaces exactly the slots
the source occupied. folio_copy() copies the source pages only. So a
2 MB folio enters the cache as, say, a 4-page one: 4 xarray slots, 4
extra references, nrpages unchanged, 4 pages of valid data.
Everything afterwards asks the folio itself, and it answers order 9:
- page_cache_delete(): xas_set_order(index, 9) and nrpages -= 512.
512 slots wiped instead of 4, with the index rounded down to 512 so
that other live folios lose their slots, and 512 subtracted instead
of 4. That is the negative nrpages and the orphaned folios.
- __remove_mapping(): expects 1 + 512 references and finds 1 + 4.
Reclaim can never free the folio.
- filemap_free_folio(): drops 512 references from a handful at
eviction. The refcount goes negative and the folio leaks.
- reads: folio_file_page() picks the page by its offset inside the
folio, but only the first pages were copied.
While the folio sits in the cache and is read through its own slots
nothing is visible. It breaks the first time something applies
folio_nr_pages() or folio_order() to it: invalidation, truncate,
reclaim, inode eviction. On NFS cache invalidation is routine (an
attribute change), so the bug shows up there instead of staying a
silent 2 MB leak per folio.
In short: inserted by the source size, removed by the destination
size. That only works when the two match, which for a real THP they
always did. A debug kernel says the same on the first migration, in
mem_cgroup_migrate():
VM_BUG_ON_FOLIO(folio_nr_pages(old) != folio_nr_pages(new))
The patches
===========
Patch 1 allocates the destination with folio_order() of the source,
GFP_TRANSHUGE_LIGHT for large folios as before. The
thp_migration_supported() test goes: migrate_pages() splits PMD-mappable
folios itself when THP migration is unsupported, and every other large
folio still needs a matching destination.
Patch 2 adds tools/testing/selftests/ve/ve_numa_migrate_test.c. It
binds a cgroup to one NUMA node, writes a file on a loop-mounted XFS
image in 64K chunks so that the page cache holds large folios, migrates
the cgroup to the other node through memory.numa_migrate and compares
what reads back. memory.current cannot serve as the detector:
mem_cgroup_migrate() only transfers the charge, whatever the size
mismatch. On an unfixed kernel the comparison fails and the test leaves
the image mounted on purpose, since evicting the inode would hit the
BUG_ON. It needs root, two NUMA nodes with memory (numa=fake=2 is
enough), cgroup2 with memory and cpuset, and mkfs.xfs; it skips
otherwise.
Testing
=======
On a VM booted with numa=fake=2:
- the reproducer (64K and 8K writes on XFS from a node-bound cgroup,
memory.numa_migrate to the other node, unlink) kills the unfixed
debug kernel on the VM_BUG_ON above and the release kernel in
clear_inode(); with patch 1 migration succeeds and unlink and
eviction complete;
- the selftest passes on the fixed kernel and fails on the comparison
on 22.11 without taking the node down.
Other branches
==============
The same patch applies as is to the 20.x branch (the customer's
kernel) and is on pr/khorenko-VSTOR-151607-numa-migrate-order-20.x;
the function is a plain callback, so it is a ReadyKernel candidate.
vz9 carries the same PageTransHuge() code, but 5.14 has no non-PMD
large folios on the LRU, so it is not reachable there.
https://virtuozzo.atlassian.net/browse/VSTOR-151607
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Konstantin Khorenko (2):
mm/memcontrol: allocate numa_migrate targets of the source folio order
selftests/ve: check memory.numa_migrate keeps the folio order
mm/memcontrol.c | 11 +-
tools/testing/selftests/ve/Makefile | 1 +
tools/testing/selftests/ve/ve_numa_migrate_test.c | 304 ++++++++++++++++++++++
3 files changed, 311 insertions(+), 5 deletions(-)
---
base-commit: b499efa17aa9fb8f20512d1831d1cb61b22e60d3
change-id: 20261028-b4-prep-khorenko-vstor-151607-numa-migrate-order-4e3a664a89d4
Best regards,
--
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 13+ messages in thread* [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets of the source folio order 2026-10-28 12:22 [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko @ 2026-10-28 12:22 ` Konstantin Khorenko 2026-10-28 12:26 ` no-reply 2026-10-28 13:09 ` Vasileios Almpanis 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko ` (2 subsequent siblings) 3 siblings, 2 replies; 13+ messages in thread From: Konstantin Khorenko @ 2026-10-28 12:22 UTC (permalink / raw) To: Pavel Tikhomirov; +Cc: devel memcg_numa_migrate_new_folio() treats every large folio as a PMD-sized THP and allocates an HPAGE_PMD_ORDER destination for it. The branch is the 4.18 pattern of new_page() in mm/mempolicy.c: a compound page on the LRU could only be a THP then, so PageTransHuge() implied HPAGE_PMD_ORDER. Today the page cache holds large folios of any order (NFS sizes a folio to the write, XFS and shmem have them too) and anonymous memory has mTHP, while folio_test_large() is still just folio_test_head() and is true for any order from 1 up. migrate_pages() never checks that the destination order matches the source. For an order-1 or order-4 file folio the destination is an order-9 folio that inherits the source index and page cache reference count, so every later page cache operation that sizes the folio by its order goes wrong: nrpages underflows, neighbouring xarray slots get wiped, the folio can never be freed. On a two-node host a VM migration to another NUMA node along with a backup to NFS ends up with a negative nrpages, and closing the backup file dies in nfs4_evict_inode(): kernel BUG at fs/inode.c:737! RIP: 0010:clear_inode+0x75/0x80 Call Trace: nfs4_evict_inode+0x21/0x80 [nfsv4] evict+0x115/0x2a0 nfs4_delegreturn_release+0x55/0xa0 [nfsv4] rpc_free_task+0x39/0x60 [sunrpc] __rpc_execute+0x3d1/0x3e0 [sunrpc] A debug build dies earlier, on the first migration of a large folio of any file system, in mem_cgroup_migrate(): VM_BUG_ON_FOLIO(folio_nr_pages(old) != folio_nr_pages(new)) Make memcg_numa_migrate_new_folio() handle large folios properly. Fixes: 9f8a754ad6c2 ("mm: memcontrol: add memory.numa_migrate file") https://virtuozzo.atlassian.net/browse/VSTOR-151607 Feature: mm: interface to migrate memory between NUMA nodes upon userspace request Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- mm/memcontrol.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mm/memcontrol.c b/mm/memcontrol.c index 0734c0fa21d05..d8a87913ace37 100644 --- a/mm/memcontrol.c +++ b/mm/memcontrol.c @@ -3480,6 +3480,7 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio, { struct memcg_numa_migrate_struct *ms = (void *)private; gfp_t gfp_mask = GFP_HIGHUSER_MOVABLE | __GFP_NORETRY | __GFP_NOWARN; + unsigned int order = 0; ms->current_node = next_node(ms->current_node, *ms->target_nodes); if (ms->current_node >= MAX_NUMNODES) { @@ -3487,13 +3488,13 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio, VM_BUG_ON(ms->current_node >= MAX_NUMNODES); } - if (thp_migration_supported() && folio_test_large(old_folio)) { - return __folio_alloc(GFP_TRANSHUGE_LIGHT | __GFP_THISNODE, - HPAGE_PMD_ORDER, ms->current_node, - ms->target_nodes); + if (folio_test_large(old_folio)) { + gfp_mask = GFP_TRANSHUGE_LIGHT | __GFP_THISNODE; + order = folio_order(old_folio); } - return __folio_alloc(gfp_mask, 0, ms->current_node, ms->target_nodes); + return __folio_alloc(gfp_mask, order, ms->current_node, + ms->target_nodes); } /* -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets of the source folio order 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko @ 2026-10-28 12:26 ` no-reply 2026-10-28 13:09 ` Vasileios Almpanis 1 sibling, 0 replies; 13+ messages in thread From: no-reply @ 2026-10-28 12:26 UTC (permalink / raw) To: Pavel Tikhomirov, Konstantin Khorenko; +Cc: devel > memcg_numa_migrate_new_folio() treats every large folio as a PMD-sized > THP and allocates an HPAGE_PMD_ORDER destination for it. The branch is > the 4.18 pattern of new_page() in mm/mempolicy.c: a compound page on > the LRU could only be a THP then, so PageTransHuge() implied > HPAGE_PMD_ORDER. > > Today the page cache holds large folios of any order (NFS sizes a folio > to the write, XFS and shmem have them too) and anonymous memory has > mTHP, while folio_test_large() is still just folio_test_head() and is > true for any order from 1 up. migrate_pages() never checks that the > destination order matches the source. > > For an order-1 or order-4 file folio the destination is an order-9 > folio that inherits the source index and page cache reference count, > so every later page cache operation that sizes the folio by its order > [ ... ] > Fixes: 9f8a754ad6c2 ("mm: memcontrol: add memory.numa_migrate file") > https://virtuozzo.atlassian.net/browse/VSTOR-151607 > > Feature: mm: interface to migrate memory between NUMA nodes upon userspace request > 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/20261028-b4-prep-khorenko-vstor-151607-numa-migrate-order-v1-0-268336526677@virtuozzo.com?part=1 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets of the source folio order 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko 2026-10-28 12:26 ` no-reply @ 2026-10-28 13:09 ` Vasileios Almpanis 2026-10-28 14:14 ` Konstantin Khorenko 1 sibling, 1 reply; 13+ messages in thread From: Vasileios Almpanis @ 2026-10-28 13:09 UTC (permalink / raw) To: Konstantin Khorenko; +Cc: devel > memcg_numa_migrate_new_folio() treats every large folio as a PMD-sized > THP and allocates an HPAGE_PMD_ORDER destination for it. The branch is > the 4.18 pattern of new_page() in mm/mempolicy.c: a compound page on > the LRU could only be a THP then, so PageTransHuge() implied > HPAGE_PMD_ORDER. > > Today the page cache holds large folios of any order (NFS sizes a folio > to the write, XFS and shmem have them too) and anonymous memory has > mTHP, while folio_test_large() is still just folio_test_head() and is > true for any order from 1 up. migrate_pages() never checks that the > destination order matches the source. > > For an order-1 or order-4 file folio the destination is an order-9 > folio that inherits the source index and page cache reference count, > so every later page cache operation that sizes the folio by its order > goes wrong: nrpages underflows, neighbouring xarray slots get wiped, > the folio can never be freed. > > On a two-node host a VM migration to another NUMA node along with a > backup to NFS ends up with a negative nrpages, and closing the backup > file dies in nfs4_evict_inode(): > > kernel BUG at fs/inode.c:737! > RIP: 0010:clear_inode+0x75/0x80 > Call Trace: > nfs4_evict_inode+0x21/0x80 [nfsv4] > evict+0x115/0x2a0 > nfs4_delegreturn_release+0x55/0xa0 [nfsv4] > rpc_free_task+0x39/0x60 [sunrpc] > __rpc_execute+0x3d1/0x3e0 [sunrpc] > > A debug build dies earlier, on the first migration of a large folio of > any file system, in mem_cgroup_migrate(): > > VM_BUG_ON_FOLIO(folio_nr_pages(old) != folio_nr_pages(new)) > > Make memcg_numa_migrate_new_folio() handle large folios properly. > > Fixes: 9f8a754ad6c2 ("mm: memcontrol: add memory.numa_migrate file") > https://virtuozzo.atlassian.net/browse/VSTOR-151607 > > Feature: mm: interface to migrate memory between NUMA nodes upon userspace request > Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> > > diff --git a/mm/memcontrol.c b/mm/memcontrol.c > index 0734c0fa21d0..d8a87913ace3 100644 > --- a/mm/memcontrol.c > +++ b/mm/memcontrol.c > @@ -3480,6 +3480,7 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio, > { > struct memcg_numa_migrate_struct *ms = (void *)private; > gfp_t gfp_mask = GFP_HIGHUSER_MOVABLE | __GFP_NORETRY | __GFP_NOWARN; > + unsigned int order = 0; > > ms->current_node = next_node(ms->current_node, *ms->target_nodes); > if (ms->current_node >= MAX_NUMNODES) { > @@ -3487,13 +3488,13 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio, > VM_BUG_ON(ms->current_node >= MAX_NUMNODES); > } > > - if (thp_migration_supported() && folio_test_large(old_folio)) { > - return __folio_alloc(GFP_TRANSHUGE_LIGHT | __GFP_THISNODE, > - HPAGE_PMD_ORDER, ms->current_node, > - ms->target_nodes); > + if (folio_test_large(old_folio)) { > + gfp_mask = GFP_TRANSHUGE_LIGHT | __GFP_THISNODE; > + order = folio_order(old_folio); > } > > - return __folio_alloc(gfp_mask, 0, ms->current_node, ms->target_nodes); > + return __folio_alloc(gfp_mask, order, ms->current_node, > + ms->target_nodes); The patch looks good to me. There is another issue that is about parsing and has nothing to do with this patch but I though of bringing it up. If we write an empty nodelist to memory.numa_migrate it will get past nodes_subset. memcg_numa_migrate_new_folio will pass MAX_NUMNODES to __folio_alloc and will cause OOB. We need another patch that adds nodes_empty next to nodes_subset. -- Vasileios Almpanis <vasileios.almpanis@virtuozzo.com> _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets of the source folio order 2026-10-28 13:09 ` Vasileios Almpanis @ 2026-10-28 14:14 ` Konstantin Khorenko 0 siblings, 0 replies; 13+ messages in thread From: Konstantin Khorenko @ 2026-10-28 14:14 UTC (permalink / raw) To: Vasileios Almpanis; +Cc: devel On 10/28/26 14:09, Vasileios Almpanis wrote: ... >> +++ b/mm/memcontrol.c >> @@ -3480,6 +3480,7 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio, >> { >> struct memcg_numa_migrate_struct *ms = (void *)private; >> gfp_t gfp_mask = GFP_HIGHUSER_MOVABLE | __GFP_NORETRY | __GFP_NOWARN; >> + unsigned int order = 0; >> >> ms->current_node = next_node(ms->current_node, *ms->target_nodes); >> if (ms->current_node >= MAX_NUMNODES) { >> @@ -3487,13 +3488,13 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio, >> VM_BUG_ON(ms->current_node >= MAX_NUMNODES); >> } >> >> - if (thp_migration_supported() && folio_test_large(old_folio)) { >> - return __folio_alloc(GFP_TRANSHUGE_LIGHT | __GFP_THISNODE, >> - HPAGE_PMD_ORDER, ms->current_node, >> - ms->target_nodes); >> + if (folio_test_large(old_folio)) { >> + gfp_mask = GFP_TRANSHUGE_LIGHT | __GFP_THISNODE; >> + order = folio_order(old_folio); >> } >> >> - return __folio_alloc(gfp_mask, 0, ms->current_node, ms->target_nodes); >> + return __folio_alloc(gfp_mask, order, ms->current_node, >> + ms->target_nodes); > > The patch looks good to me. There is another issue that is about parsing and > has nothing to do with this patch but I though of bringing it up. If we > write an empty nodelist to memory.numa_migrate it will get past > nodes_subset. memcg_numa_migrate_new_folio will pass MAX_NUMNODES to > __folio_alloc and will cause OOB. We need another patch that adds > nodes_empty next to nodes_subset. Thank you, will think about this in a separate patch(set). _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the folio order 2026-10-28 12:22 [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko @ 2026-10-28 12:22 ` Konstantin Khorenko 2026-10-28 12:28 ` no-reply ` (2 more replies) 2026-10-28 12:49 ` [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source " Pavel Tikhomirov 2026-10-28 13:09 ` Vasileios Almpanis 3 siblings, 3 replies; 13+ messages in thread From: Konstantin Khorenko @ 2026-10-28 12:22 UTC (permalink / raw) To: Pavel Tikhomirov; +Cc: devel memcg_numa_migrate_new_folio() used to allocate a PMD-sized destination for every large folio. A page cache folio of a smaller order then ended up with a 512-page destination carrying its index and reference count. Only the source pages were copied into it, so a read of the folios that followed in the same 2 MB range was served from the uninitialised part of the destination, and the inode died in clear_inode() on eviction. The test writes a file on an XFS image, where 64K writes produce large folios, from a cgroup bound to one NUMA node, migrates the cgroup to a second node, checks that something was migrated and reads the file back against the written pattern. On an unfixed kernel the comparison fails; the test then leaves the image mounted, because unlinking or unmounting would evict the inode and crash the node. It needs root, two NUMA nodes with memory (numa=fake=2 is enough), cgroup2 with the memory and cpuset controllers, and mkfs.xfs, and skips otherwise. https://virtuozzo.atlassian.net/browse/VSTOR-151607 Feature: ve selftests Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> --- tools/testing/selftests/ve/Makefile | 1 + tools/testing/selftests/ve/ve_numa_migrate_test.c | 304 ++++++++++++++++++++++ 2 files changed, 305 insertions(+) diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile index 51f9316beb99b..e96320a183844 100644 --- a/tools/testing/selftests/ve/Makefile +++ b/tools/testing/selftests/ve/Makefile @@ -6,6 +6,7 @@ TEST_GEN_PROGS += ve_ns_owner_test TEST_GEN_PROGS += ve_perms_test TEST_GEN_PROGS += ve_mount_accounting_test TEST_GEN_PROGS += ve_vtty_stress +TEST_GEN_PROGS += ve_numa_migrate_test # CPUID faulting is x86-64 only. ARCH says "x86" for 32-bit too and lib.mk # picks the clang target by its emptiness, so decide by the compiler. diff --git a/tools/testing/selftests/ve/ve_numa_migrate_test.c b/tools/testing/selftests/ve/ve_numa_migrate_test.c new file mode 100644 index 0000000000000..5fbf4af49f7fc --- /dev/null +++ b/tools/testing/selftests/ve/ve_numa_migrate_test.c @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * memory.numa_migrate must give a large page cache folio a destination of + * the same order. An oversized destination inherits the index and the + * reference count of the source, only the source pages are copied into it, + * reads of the neighbouring folios land in its uninitialised part, and the + * inode dies in clear_inode() on eviction. + * + * Write a file on an XFS image (XFS allocates large folios for 64K writes) + * from a cgroup bound to one NUMA node, migrate the cgroup to another node + * and check that the data reads back intact. + * + * Needs root, two NUMA nodes with memory (numa=fake=2 will do), cgroup2 + * with the memory and cpuset controllers, and mkfs.xfs. + */ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sched.h> +#include <fcntl.h> +#include <errno.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <sys/mount.h> +#include <linux/sched.h> + +#include "../kselftest_harness.h" +#include "ve_selftest.h" + +#define CG_NAME "ve_numa_migrate_test" +#define DIR_TEMPLATE "/var/tmp/ve_numa_migrate.XXXXXX" +/* mkfs.xfs refuses anything smaller; the image is sparse. */ +#define IMG_SIZE (320UL << 20) +#define FILE_SIZE (4UL << 20) +#define CHUNK_SIZE (64UL << 10) + +FIXTURE(numa_migrate) +{ + int cgv2_fd; + int cg_fd; + int node_from; + int node_to; + char dir[64]; + char img[80]; + char mnt[80]; + int mounted; + int leave_mounted; + int data_fd; +}; + +/* First two node ids of a sysfs node list such as "0-1" or "0,2-3". */ +static int two_nodes_with_memory(int *from, int *to) +{ + char buf[256], *tok, *save; + int found = 0; + + if (read_file_at(AT_FDCWD, "/sys/devices/system/node/has_memory", + buf, sizeof(buf)) <= 0) + return -1; + + for (tok = strtok_r(buf, ",\n", &save); tok && found < 2; + tok = strtok_r(NULL, ",\n", &save)) { + int a, b, n; + + if (sscanf(tok, "%d-%d", &a, &b) != 2) { + if (sscanf(tok, "%d", &a) != 1) + return -1; + b = a; + } + for (n = a; n <= b && found < 2; n++) { + if (found == 0) + *from = n; + else + *to = n; + found++; + } + } + return found == 2 ? 0 : -1; +} + +static int run_cmd(const char *fmt, ...) +{ + char cmd[256]; + va_list ap; + int status; + + va_start(ap, fmt); + vsnprintf(cmd, sizeof(cmd), fmt, ap); + va_end(ap); + + status = system(cmd); + if (status < 0 || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +/* Bytes of file pages the cgroup has on @node, from memory.numa_stat. */ +static int file_bytes_on_node(int cg_fd, int node, unsigned long long *out) +{ + static char buf[32768]; + char key[16], *p; + + if (read_file_at(cg_fd, "memory.numa_stat", buf, sizeof(buf)) <= 0) + return -1; + p = strstr(buf, "\nfile "); + if (!p) + return -1; + snprintf(key, sizeof(key), " N%d=", node); + p = strstr(p, key); + if (!p) + return -1; + *out = strtoull(p + strlen(key), NULL, 10); + return 0; +} + +static void cleanup(FIXTURE_DATA(numa_migrate) *self) +{ + char path[PATH_MAX]; + + if (self->data_fd >= 0) { + close(self->data_fd); + self->data_fd = -1; + } + if (self->dir[0]) { + snprintf(path, sizeof(path), "%s/data", self->mnt); + unlink(path); + if (self->mounted) + umount(self->mnt); + unlink(self->img); + rmdir(self->mnt); + rmdir(self->dir); + self->dir[0] = '\0'; + } + if (self->cg_fd >= 0) { + close(self->cg_fd); + self->cg_fd = -1; + } + if (self->cgv2_fd >= 0) { + /* The test task may still sit in the cgroup after a failure. */ + write_u64_at(self->cgv2_fd, "cgroup.procs", getpid()); + unlinkat(self->cgv2_fd, CG_NAME, AT_REMOVEDIR); + close(self->cgv2_fd); + self->cgv2_fd = -1; + } +} + +/* + * Returns 0, or 1 with everything undone when the host cannot run the test. + * The harness runs no teardown after a failed setup, so the undo is here. + */ +static int prepare(FIXTURE_DATA(numa_migrate) *self, const char **why) +{ + int fd; + + self->cgv2_fd = mount_cg2_fd(); + if (self->cgv2_fd < 0) { + *why = "cannot mount cgroup2"; + goto fail; + } + if (write_file_at(self->cgv2_fd, "cgroup.subtree_control", + "+memory +cpuset")) { + *why = "cannot enable the memory and cpuset controllers"; + goto fail; + } + if (mkdirat(self->cgv2_fd, CG_NAME, 0755) && errno != EEXIST) { + *why = "cannot create the test cgroup"; + goto fail; + } + self->cg_fd = openat(self->cgv2_fd, CG_NAME, O_PATH | O_DIRECTORY); + if (self->cg_fd < 0) { + *why = "cannot open the test cgroup"; + goto fail; + } + if (faccessat(self->cg_fd, "memory.numa_migrate", W_OK, 0)) { + *why = "no memory.numa_migrate"; + goto fail; + } + if (write_u64_at(self->cg_fd, "cpuset.mems", self->node_from)) { + *why = "cannot bind the cgroup to a node"; + goto fail; + } + + snprintf(self->dir, sizeof(self->dir), DIR_TEMPLATE); + if (!mkdtemp(self->dir)) { + self->dir[0] = '\0'; + *why = "cannot create a temporary directory"; + goto fail; + } + snprintf(self->img, sizeof(self->img), "%s/img", self->dir); + snprintf(self->mnt, sizeof(self->mnt), "%s/mnt", self->dir); + if (mkdir(self->mnt, 0755)) { + *why = "cannot create the mount point"; + goto fail; + } + fd = open(self->img, O_CREAT | O_RDWR, 0600); + if (fd < 0) { + *why = "cannot create the image file"; + goto fail; + } + if (ftruncate(fd, IMG_SIZE)) { + close(fd); + *why = "cannot size the image file"; + goto fail; + } + close(fd); + if (run_cmd("mkfs.xfs -q -f %s", self->img)) { + *why = "mkfs.xfs failed"; + goto fail; + } + if (run_cmd("mount -o loop %s %s", self->img, self->mnt)) { + *why = "cannot loop-mount the XFS image"; + goto fail; + } + self->mounted = 1; + return 0; +fail: + cleanup(self); + return 1; +} + +FIXTURE_SETUP(numa_migrate) +{ + const char *why; + + self->cgv2_fd = -1; + self->cg_fd = -1; + self->data_fd = -1; + self->dir[0] = '\0'; + + if (geteuid()) + SKIP(return, "needs root"); + if (two_nodes_with_memory(&self->node_from, &self->node_to)) + SKIP(return, "needs two NUMA nodes with memory"); + if (access("/usr/sbin/mkfs.xfs", X_OK) && access("/sbin/mkfs.xfs", X_OK)) + SKIP(return, "needs mkfs.xfs"); + if (prepare(self, &why)) + SKIP(return, "%s", why); +} + +FIXTURE_TEARDOWN(numa_migrate) +{ + if (self->leave_mounted) { + TH_LOG("leaving %s mounted: evicting the file would hit " + "BUG_ON(nrpages) in clear_inode()", self->mnt); + self->dir[0] = '\0'; + } + cleanup(self); +} + +TEST_F(numa_migrate, large_folio_order) +{ + unsigned long long on_to_before, on_to_after; + unsigned char *buf, *check; + char path[PATH_MAX], list[64]; + unsigned long off; + int fd; + + ASSERT_EQ(write_u64_at(self->cg_fd, "cgroup.procs", getpid()), 0); + + buf = malloc(CHUNK_SIZE); + check = malloc(CHUNK_SIZE); + ASSERT_NE(buf, NULL); + ASSERT_NE(check, NULL); + + snprintf(path, sizeof(path), "%s/data", self->mnt); + fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0600); + ASSERT_GE(fd, 0); + self->data_fd = fd; + for (off = 0; off < FILE_SIZE; off += CHUNK_SIZE) { + memset(buf, (off / CHUNK_SIZE) & 0xff, CHUNK_SIZE); + ASSERT_EQ(pwrite(fd, buf, CHUNK_SIZE, off), (ssize_t)CHUNK_SIZE); + } + ASSERT_EQ(fsync(fd), 0); + + ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to, + &on_to_before), 0); + + /* From here on a failure means eviction would crash the kernel. */ + self->leave_mounted = 1; + snprintf(list, sizeof(list), "%d,%d", self->node_from, self->node_to); + ASSERT_EQ(write_file_at(self->cg_fd, "cpuset.mems", list), 0); + ASSERT_EQ(write_u64_at(self->cg_fd, "memory.numa_migrate", + self->node_to), 0); + + ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to, + &on_to_after), 0); + TH_LOG("file bytes on node %d: %llu -> %llu", self->node_to, + on_to_before, on_to_after); + ASSERT_GE(on_to_after, on_to_before + FILE_SIZE); + + for (off = 0; off < FILE_SIZE; off += CHUNK_SIZE) { + memset(buf, (off / CHUNK_SIZE) & 0xff, CHUNK_SIZE); + ASSERT_EQ(pread(fd, check, CHUNK_SIZE, off), (ssize_t)CHUNK_SIZE); + ASSERT_EQ(memcmp(buf, check, CHUNK_SIZE), 0); + } + self->leave_mounted = 0; + + free(buf); + free(check); +} + +TEST_HARNESS_MAIN -- 2.43.0 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the folio order 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko @ 2026-10-28 12:28 ` no-reply 2026-10-28 12:49 ` Pavel Tikhomirov 2026-10-28 13:09 ` Vasileios Almpanis 2 siblings, 0 replies; 13+ messages in thread From: no-reply @ 2026-10-28 12:28 UTC (permalink / raw) To: Konstantin Khorenko, Pavel Tikhomirov; +Cc: devel Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: - [Medium] selftests/ve: numa_migrate test never checks that large folios were created - [Medium] selftests/ve: strict numa_migrate pass threshold can fail on a correct kernel and leave the XFS mount behind - [Medium] selftests/ve: mkfs.xfs skip probe checks fixed paths, not PATH - [Low] selftests/ve: ve_numa_migrate_test binary missing from .gitignore -- commit 53864b9544be735f4056fc53973030b5631878a2 Author: Konstantin Khorenko <khorenko@virtuozzo.com> selftests/ve: check memory.numa_migrate keeps the folio order This adds a selftest that writes a file on an XFS loop image from a cgroup bound to one NUMA node, migrates the cgroup to a second node with memory.numa_migrate, and reads the file back against the written pattern. It is meant to catch memcg_numa_migrate_new_folio() allocating an oversized destination for a large page cache folio. It skips when root, two NUMA nodes, cgroup2 controllers or mkfs.xfs are missing. Link: https://virtuozzo.atlassian.net/browse/VSTOR-151607 > diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile > index 51f9316beb99..e96320a18384 100644 > --- a/tools/testing/selftests/ve/Makefile > +++ b/tools/testing/selftests/ve/Makefile > @@ -6,6 +6,7 @@ TEST_GEN_PROGS += ve_ns_owner_test > TEST_GEN_PROGS += ve_perms_test > TEST_GEN_PROGS += ve_mount_accounting_test > TEST_GEN_PROGS += ve_vtty_stress > +TEST_GEN_PROGS += ve_numa_migrate_test [Severity: Low] Should tools/testing/selftests/ve/.gitignore get a matching entry? The ignore list currently ends with ve_vtty_stress and has no entry for ve_numa_migrate_test, which this line now builds. After a build the binary will show up as untracked in git status, and the patch does not touch .gitignore. [ ... ] > diff --git a/tools/testing/selftests/ve/ve_numa_migrate_test.c b/tools/testing/selftests/ve/ve_numa_migrate_test.c > new file mode 100644 > index 000000000000..5fbf4af49f7f > --- /dev/null > +++ b/tools/testing/selftests/ve/ve_numa_migrate_test.c [ ... ] > + * Write a file on an XFS image (XFS allocates large folios for 64K writes) > + * from a cgroup bound to one NUMA node, migrate the cgroup to another node > + * and check that the data reads back intact. [Severity: Medium] Is it safe to assume large folios were actually created here? Nothing in the test verifies it. mapping_max_folio_order() returns 0 when CONFIG_TRANSPARENT_HUGEPAGE is not set. Large folio allocation can also fall back to order-0 under fragmentation or memory pressure. In either case every folio is order-0, and the only precondition check (ASSERT_GE on the node_to file bytes) is satisfied by order-0 folios as well. The memcmp readback then succeeds because nothing was mis-migrated, and the test reports PASS without covering the folio_test_large() branch in memcg_numa_migrate_new_folio(). Could the test check for large folios, for example through /proc/kpageflags or the THP counters, and SKIP when none are seen? [ ... ] > + if (access("/usr/sbin/mkfs.xfs", X_OK) && access("/sbin/mkfs.xfs", X_OK)) > + SKIP(return, "needs mkfs.xfs"); [Severity: Medium] Can this probe cause a false skip? It only looks in /usr/sbin and /sbin, while prepare() runs the tool through run_cmd() and system(), which resolves it via PATH: if (run_cmd("mkfs.xfs -q -f %s", self->img)) { *why = "mkfs.xfs failed"; On a host where mkfs.xfs is in /usr/bin or /usr/local/sbin, the test skips with "needs mkfs.xfs" before prepare() would have found the tool. A mkfs failure in prepare() already turns into a SKIP, so would it be simpler to drop this pre-probe? [ ... ] > + /* From here on a failure means eviction would crash the kernel. */ > + self->leave_mounted = 1; > + snprintf(list, sizeof(list), "%d,%d", self->node_from, self->node_to); > + ASSERT_EQ(write_file_at(self->cg_fd, "cpuset.mems", list), 0); > + ASSERT_EQ(write_u64_at(self->cg_fd, "memory.numa_migrate", > + self->node_to), 0); > + > + ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to, > + &on_to_after), 0); > + TH_LOG("file bytes on node %d: %llu -> %llu", self->node_to, > + on_to_before, on_to_after); > + ASSERT_GE(on_to_after, on_to_before + FILE_SIZE); [Severity: Medium] Can this strict threshold fail on a correct kernel and then leave the XFS mount behind? The assertion requires every byte of the 4MB file to have moved to node_to. The memory.numa_migrate write does not guarantee that: mm/memcontrol.c:__memcg_numa_migrate_pages() { ret = migrate_pages(&pages, memcg_numa_migrate_new_folio, NULL, (unsigned long)&ms, MIGRATE_ASYNC, MR_SYSCALL, NULL); putback_movable_pages(&pages); if (ret < 0) return ret; ... } Only a negative return is propagated. A positive count of folios that failed to migrate is dropped, and __memcg_numa_migrate_write() returns "ret ?: nbytes", so the ASSERT_EQ on the write passes on a partial migration. Large destination folios are allocated in memcg_numa_migrate_new_folio() with: if (folio_test_large(old_folio)) { gfp_mask = GFP_TRANSHUGE_LIGHT | __GFP_THISNODE; order = folio_order(old_folio); } That has no direct reclaim or compaction and no node fallback, so it can fail on a fragmented node_to and leave the folio on node_from. memcg_numa_isolate_pages() also skips folios that are not on the LRU: /* Only take pages on LRU: a check now makes later tests safe */ if (!folio_test_lru(folio)) goto isolate_fail; Folios still sitting in per-CPU LRU batches are skipped without a drain. Any of these makes the ASSERT_GE fail without a kernel bug. Because leave_mounted was set just above, FIXTURE_TEARDOWN then skips the unmount and the loop mount and image stay on the host. Would a tolerant check, such as requiring a meaningful fraction of the file to have moved, work better? Could leave_mounted also be set only after the data-integrity check has shown a problem, rather than before the migration? [ ... ] -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261028-b4-prep-khorenko-vstor-151607-numa-migrate-order-v1-0-268336526677@virtuozzo.com?part=2 _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the folio order 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko 2026-10-28 12:28 ` no-reply @ 2026-10-28 12:49 ` Pavel Tikhomirov 2026-10-28 14:08 ` Konstantin Khorenko 2026-10-28 13:09 ` Vasileios Almpanis 2 siblings, 1 reply; 13+ messages in thread From: Pavel Tikhomirov @ 2026-10-28 12:49 UTC (permalink / raw) To: Konstantin Khorenko; +Cc: devel > memcg_numa_migrate_new_folio() used to allocate a PMD-sized destination > for every large folio. A page cache folio of a smaller order then ended > up with a 512-page destination carrying its index and reference count. > Only the source pages were copied into it, so a read of the folios that > followed in the same 2 MB range was served from the uninitialised part > of the destination, and the inode died in clear_inode() on eviction. > > The test writes a file on an XFS image, where 64K writes produce large > folios, from a cgroup bound to one NUMA node, migrates the cgroup to a > second node, checks that something was migrated and reads the file back > against the written pattern. On an unfixed kernel the comparison fails; > the test then leaves the image mounted, because unlinking or unmounting > would evict the inode and crash the node. > > It needs root, two NUMA nodes with memory (numa=fake=2 is enough), > cgroup2 with the memory and cpuset controllers, and mkfs.xfs, and skips > otherwise. This case looks a bit implicit, we rely on the size of huge pages XFS allocates, maybe we can add folio size check, or somehow force allocation of proper order. This is not blocking I guess. -- Pavel Tikhomirov <ptikhomirov@virtuozzo.com> _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the folio order 2026-10-28 12:49 ` Pavel Tikhomirov @ 2026-10-28 14:08 ` Konstantin Khorenko 0 siblings, 0 replies; 13+ messages in thread From: Konstantin Khorenko @ 2026-10-28 14:08 UTC (permalink / raw) To: Pavel Tikhomirov; +Cc: devel On 10/28/26 13:49, Pavel Tikhomirov wrote: >> memcg_numa_migrate_new_folio() used to allocate a PMD-sized destination >> for every large folio. A page cache folio of a smaller order then ended >> up with a 512-page destination carrying its index and reference count. >> Only the source pages were copied into it, so a read of the folios that >> followed in the same 2 MB range was served from the uninitialised part >> of the destination, and the inode died in clear_inode() on eviction. >> >> The test writes a file on an XFS image, where 64K writes produce large >> folios, from a cgroup bound to one NUMA node, migrates the cgroup to a >> second node, checks that something was migrated and reads the file back >> against the written pattern. On an unfixed kernel the comparison fails; >> the test then leaves the image mounted, because unlinking or unmounting >> would evict the inode and crash the node. >> >> It needs root, two NUMA nodes with memory (numa=fake=2 is enough), >> cgroup2 with the memory and cpuset controllers, and mkfs.xfs, and skips >> otherwise. > > This case looks a bit implicit, we rely on the size of huge pages XFS > allocates, maybe we can add folio size check, or somehow force > allocation of proper order. This is not blocking I guess. Agreed, v1 took the folio order on faith. v2 will read the compound flags of the file's pages through /proc/self/pagemap and /proc/kpageflags before the migration. A file without a large folio fails the test, on purpose a skip would hide that the test no longer reaches the bug, while a failure tells us the XFS write path stopped giving large folios on that setup and the test needs another way to get them. _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the folio order 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko 2026-10-28 12:28 ` no-reply 2026-10-28 12:49 ` Pavel Tikhomirov @ 2026-10-28 13:09 ` Vasileios Almpanis 2026-10-28 14:12 ` Konstantin Khorenko 2 siblings, 1 reply; 13+ messages in thread From: Vasileios Almpanis @ 2026-10-28 13:09 UTC (permalink / raw) To: Konstantin Khorenko; +Cc: devel > memcg_numa_migrate_new_folio() used to allocate a PMD-sized destination > for every large folio. A page cache folio of a smaller order then ended > up with a 512-page destination carrying its index and reference count. > Only the source pages were copied into it, so a read of the folios that > followed in the same 2 MB range was served from the uninitialised part > of the destination, and the inode died in clear_inode() on eviction. > > The test writes a file on an XFS image, where 64K writes produce large > folios, from a cgroup bound to one NUMA node, migrates the cgroup to a > second node, checks that something was migrated and reads the file back > against the written pattern. On an unfixed kernel the comparison fails; > the test then leaves the image mounted, because unlinking or unmounting > would evict the inode and crash the node. > > It needs root, two NUMA nodes with memory (numa=fake=2 is enough), > cgroup2 with the memory and cpuset controllers, and mkfs.xfs, and skips > otherwise. > > https://virtuozzo.atlassian.net/browse/VSTOR-151607 > > Feature: ve selftests > Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com> > > diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile > index 51f9316beb99..e96320a18384 100644 > --- a/tools/testing/selftests/ve/Makefile > +++ b/tools/testing/selftests/ve/Makefile > @@ -6,6 +6,7 @@ TEST_GEN_PROGS += ve_ns_owner_test > TEST_GEN_PROGS += ve_perms_test > TEST_GEN_PROGS += ve_mount_accounting_test > TEST_GEN_PROGS += ve_vtty_stress > +TEST_GEN_PROGS += ve_numa_migrate_test > > # CPUID faulting is x86-64 only. ARCH says "x86" for 32-bit too and lib.mk > # picks the clang target by its emptiness, so decide by the compiler. > diff --git a/tools/testing/selftests/ve/ve_numa_migrate_test.c b/tools/testing/selftests/ve/ve_numa_migrate_test.c > new file mode 100644 > index 000000000000..5fbf4af49f7f > --- /dev/null > +++ b/tools/testing/selftests/ve/ve_numa_migrate_test.c > @@ -0,0 +1,304 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * memory.numa_migrate must give a large page cache folio a destination of > + * the same order. An oversized destination inherits the index and the > + * reference count of the source, only the source pages are copied into it, > + * reads of the neighbouring folios land in its uninitialised part, and the > + * inode dies in clear_inode() on eviction. > + * > + * Write a file on an XFS image (XFS allocates large folios for 64K writes) > + * from a cgroup bound to one NUMA node, migrate the cgroup to another node > + * and check that the data reads back intact. > + * > + * Needs root, two NUMA nodes with memory (numa=fake=2 will do), cgroup2 > + * with the memory and cpuset controllers, and mkfs.xfs. > + */ > +#define _GNU_SOURCE > +#include <stdio.h> > +#include <stdlib.h> > +#include <string.h> > +#include <unistd.h> > +#include <sched.h> > +#include <fcntl.h> > +#include <errno.h> > +#include <sys/stat.h> > +#include <sys/wait.h> > +#include <sys/mount.h> > +#include <linux/sched.h> > + > +#include "../kselftest_harness.h" > +#include "ve_selftest.h" > + > +#define CG_NAME "ve_numa_migrate_test" > +#define DIR_TEMPLATE "/var/tmp/ve_numa_migrate.XXXXXX" > +/* mkfs.xfs refuses anything smaller; the image is sparse. */ > +#define IMG_SIZE (320UL << 20) > +#define FILE_SIZE (4UL << 20) > +#define CHUNK_SIZE (64UL << 10) > + > +FIXTURE(numa_migrate) > +{ > + int cgv2_fd; > + int cg_fd; > + int node_from; > + int node_to; > + char dir[64]; > + char img[80]; > + char mnt[80]; > + int mounted; > + int leave_mounted; > + int data_fd; > +}; > + > +/* First two node ids of a sysfs node list such as "0-1" or "0,2-3". */ > +static int two_nodes_with_memory(int *from, int *to) > +{ > + char buf[256], *tok, *save; > + int found = 0; > + > + if (read_file_at(AT_FDCWD, "/sys/devices/system/node/has_memory", > + buf, sizeof(buf)) <= 0) > + return -1; > + > + for (tok = strtok_r(buf, ",\n", &save); tok && found < 2; > + tok = strtok_r(NULL, ",\n", &save)) { > + int a, b, n; > + > + if (sscanf(tok, "%d-%d", &a, &b) != 2) { > + if (sscanf(tok, "%d", &a) != 1) > + return -1; > + b = a; > + } > + for (n = a; n <= b && found < 2; n++) { > + if (found == 0) > + *from = n; > + else > + *to = n; > + found++; > + } > + } > + return found == 2 ? 0 : -1; > +} > + > +static int run_cmd(const char *fmt, ...) > +{ > + char cmd[256]; > + va_list ap; > + int status; > + > + va_start(ap, fmt); > + vsnprintf(cmd, sizeof(cmd), fmt, ap); > + va_end(ap); > + > + status = system(cmd); > + if (status < 0 || !WIFEXITED(status)) > + return -1; > + return WEXITSTATUS(status); > +} > + > +/* Bytes of file pages the cgroup has on @node, from memory.numa_stat. */ > +static int file_bytes_on_node(int cg_fd, int node, unsigned long long *out) > +{ > + static char buf[32768]; > + char key[16], *p; > + > + if (read_file_at(cg_fd, "memory.numa_stat", buf, sizeof(buf)) <= 0) > + return -1; > + p = strstr(buf, "\nfile "); > + if (!p) > + return -1; > + snprintf(key, sizeof(key), " N%d=", node); > + p = strstr(p, key); > + if (!p) > + return -1; > + *out = strtoull(p + strlen(key), NULL, 10); > + return 0; > +} > + > +static void cleanup(FIXTURE_DATA(numa_migrate) *self) > +{ > + char path[PATH_MAX]; > + > + if (self->data_fd >= 0) { > + close(self->data_fd); > + self->data_fd = -1; > + } > + if (self->dir[0]) { > + snprintf(path, sizeof(path), "%s/data", self->mnt); > + unlink(path); > + if (self->mounted) > + umount(self->mnt); > + unlink(self->img); > + rmdir(self->mnt); > + rmdir(self->dir); > + self->dir[0] = '\0'; > + } > + if (self->cg_fd >= 0) { > + close(self->cg_fd); > + self->cg_fd = -1; > + } > + if (self->cgv2_fd >= 0) { > + /* The test task may still sit in the cgroup after a failure. */ > + write_u64_at(self->cgv2_fd, "cgroup.procs", getpid()); > + unlinkat(self->cgv2_fd, CG_NAME, AT_REMOVEDIR); > + close(self->cgv2_fd); > + self->cgv2_fd = -1; > + } > +} > + > +/* > + * Returns 0, or 1 with everything undone when the host cannot run the test. > + * The harness runs no teardown after a failed setup, so the undo is here. > + */ > +static int prepare(FIXTURE_DATA(numa_migrate) *self, const char **why) > +{ > + int fd; > + > + self->cgv2_fd = mount_cg2_fd(); > + if (self->cgv2_fd < 0) { > + *why = "cannot mount cgroup2"; > + goto fail; > + } > + if (write_file_at(self->cgv2_fd, "cgroup.subtree_control", > + "+memory +cpuset")) { > + *why = "cannot enable the memory and cpuset controllers"; > + goto fail; > + } > + if (mkdirat(self->cgv2_fd, CG_NAME, 0755) && errno != EEXIST) { > + *why = "cannot create the test cgroup"; > + goto fail; > + } > + self->cg_fd = openat(self->cgv2_fd, CG_NAME, O_PATH | O_DIRECTORY); > + if (self->cg_fd < 0) { > + *why = "cannot open the test cgroup"; > + goto fail; > + } > + if (faccessat(self->cg_fd, "memory.numa_migrate", W_OK, 0)) { > + *why = "no memory.numa_migrate"; > + goto fail; > + } > + if (write_u64_at(self->cg_fd, "cpuset.mems", self->node_from)) { > + *why = "cannot bind the cgroup to a node"; > + goto fail; > + } > + > + snprintf(self->dir, sizeof(self->dir), DIR_TEMPLATE); > + if (!mkdtemp(self->dir)) { > + self->dir[0] = '\0'; > + *why = "cannot create a temporary directory"; > + goto fail; > + } > + snprintf(self->img, sizeof(self->img), "%s/img", self->dir); > + snprintf(self->mnt, sizeof(self->mnt), "%s/mnt", self->dir); > + if (mkdir(self->mnt, 0755)) { > + *why = "cannot create the mount point"; > + goto fail; > + } > + fd = open(self->img, O_CREAT | O_RDWR, 0600); > + if (fd < 0) { > + *why = "cannot create the image file"; > + goto fail; > + } > + if (ftruncate(fd, IMG_SIZE)) { > + close(fd); > + *why = "cannot size the image file"; > + goto fail; > + } > + close(fd); > + if (run_cmd("mkfs.xfs -q -f %s", self->img)) { > + *why = "mkfs.xfs failed"; > + goto fail; > + } > + if (run_cmd("mount -o loop %s %s", self->img, self->mnt)) { > + *why = "cannot loop-mount the XFS image"; > + goto fail; > + } > + self->mounted = 1; > + return 0; > +fail: > + cleanup(self); > + return 1; > +} > + > +FIXTURE_SETUP(numa_migrate) > +{ > + const char *why; > + > + self->cgv2_fd = -1; > + self->cg_fd = -1; > + self->data_fd = -1; > + self->dir[0] = '\0'; > + > + if (geteuid()) > + SKIP(return, "needs root"); > + if (two_nodes_with_memory(&self->node_from, &self->node_to)) > + SKIP(return, "needs two NUMA nodes with memory"); > + if (access("/usr/sbin/mkfs.xfs", X_OK) && access("/sbin/mkfs.xfs", X_OK)) > + SKIP(return, "needs mkfs.xfs"); > + if (prepare(self, &why)) > + SKIP(return, "%s", why); > +} > + > +FIXTURE_TEARDOWN(numa_migrate) > +{ > + if (self->leave_mounted) { > + TH_LOG("leaving %s mounted: evicting the file would hit " > + "BUG_ON(nrpages) in clear_inode()", self->mnt); > + self->dir[0] = '\0'; > + } > + cleanup(self); > +} > + > +TEST_F(numa_migrate, large_folio_order) > +{ > + unsigned long long on_to_before, on_to_after; > + unsigned char *buf, *check; > + char path[PATH_MAX], list[64]; > + unsigned long off; > + int fd; > + > + ASSERT_EQ(write_u64_at(self->cg_fd, "cgroup.procs", getpid()), 0); > + > + buf = malloc(CHUNK_SIZE); > + check = malloc(CHUNK_SIZE); > + ASSERT_NE(buf, NULL); > + ASSERT_NE(check, NULL); > + > + snprintf(path, sizeof(path), "%s/data", self->mnt); > + fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0600); > + ASSERT_GE(fd, 0); > + self->data_fd = fd; > + for (off = 0; off < FILE_SIZE; off += CHUNK_SIZE) { > + memset(buf, (off / CHUNK_SIZE) & 0xff, CHUNK_SIZE); > + ASSERT_EQ(pwrite(fd, buf, CHUNK_SIZE, off), (ssize_t)CHUNK_SIZE); > + } > + ASSERT_EQ(fsync(fd), 0); > + > + ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to, > + &on_to_before), 0); > + > + /* From here on a failure means eviction would crash the kernel. */ > + self->leave_mounted = 1; > + snprintf(list, sizeof(list), "%d,%d", self->node_from, self->node_to); > + ASSERT_EQ(write_file_at(self->cg_fd, "cpuset.mems", list), 0); > + ASSERT_EQ(write_u64_at(self->cg_fd, "memory.numa_migrate", > + self->node_to), 0); > + > + ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to, > + &on_to_after), 0); > + TH_LOG("file bytes on node %d: %llu -> %llu", self->node_to, > + on_to_before, on_to_after); Non blocker: in hosts with many CPUs we could read unflushed stats. Maybe do the memcmp loop first and retry numa_stat read for a few seconds. -- Vasileios Almpanis <vasileios.almpanis@virtuozzo.com> _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the folio order 2026-10-28 13:09 ` Vasileios Almpanis @ 2026-10-28 14:12 ` Konstantin Khorenko 0 siblings, 0 replies; 13+ messages in thread From: Konstantin Khorenko @ 2026-10-28 14:12 UTC (permalink / raw) To: Vasileios Almpanis; +Cc: devel On 10/28/26 14:09, Vasileios Almpanis wrote: ... >> + /* From here on a failure means eviction would crash the kernel. */ >> + self->leave_mounted = 1; >> + snprintf(list, sizeof(list), "%d,%d", self->node_from, self->node_to); >> + ASSERT_EQ(write_file_at(self->cg_fd, "cpuset.mems", list), 0); >> + ASSERT_EQ(write_u64_at(self->cg_fd, "memory.numa_migrate", >> + self->node_to), 0); >> + >> + ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to, >> + &on_to_after), 0); >> + TH_LOG("file bytes on node %d: %llu -> %llu", self->node_to, >> + on_to_before, on_to_after); > > Non blocker: in hosts with many CPUs we could read unflushed stats. > Maybe do the memcmp loop first and retry numa_stat read for a few > seconds. Yeah, will try to handle this in v2. _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order 2026-10-28 12:22 [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko @ 2026-10-28 12:49 ` Pavel Tikhomirov 2026-10-28 13:09 ` Vasileios Almpanis 3 siblings, 0 replies; 13+ messages in thread From: Pavel Tikhomirov @ 2026-10-28 12:49 UTC (permalink / raw) To: Konstantin Khorenko; +Cc: devel On Wed, 28 Oct 2026 13:22:16 +0100, Konstantin Khorenko <khorenko@virtuozzo.com> wrote: > mm/memcontrol: numa_migrate destinations of the source folio order > > memory.numa_migrate, our memcg file that vcmmd uses to move a cgroup's > memory between NUMA nodes, allocates a 2 MB destination for every large > folio it migrates. Page cache folios of a smaller order get a > destination of the wrong size, and the page cache falls apart around > it. Patch 1 is the fix, patch 2 a selftest that catches it without > crashing the node. > > [...] Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com> -- Pavel Tikhomirov <ptikhomirov@virtuozzo.com> _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order 2026-10-28 12:22 [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko ` (2 preceding siblings ...) 2026-10-28 12:49 ` [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source " Pavel Tikhomirov @ 2026-10-28 13:09 ` Vasileios Almpanis 3 siblings, 0 replies; 13+ messages in thread From: Vasileios Almpanis @ 2026-10-28 13:09 UTC (permalink / raw) To: Konstantin Khorenko; +Cc: devel On Wed, 28 Oct 2026 13:22:16 +0100, Konstantin Khorenko <khorenko@virtuozzo.com> wrote: > mm/memcontrol: numa_migrate destinations of the source folio order > > memory.numa_migrate, our memcg file that vcmmd uses to move a cgroup's > memory between NUMA nodes, allocates a 2 MB destination for every large > folio it migrates. Page cache folios of a smaller order get a > destination of the wrong size, and the page cache falls apart around > it. Patch 1 is the fix, patch 2 a selftest that catches it without > crashing the node. > > [...] Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com> -- Vasileios Almpanis <vasileios.almpanis@virtuozzo.com> _______________________________________________ Devel mailing list Devel@openvz.org https://lists.openvz.org/mailman/listinfo/devel ^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2026-10-28 14:15 UTC | newest] Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-28 12:22 [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko 2026-10-28 12:26 ` no-reply 2026-10-28 13:09 ` Vasileios Almpanis 2026-10-28 14:14 ` Konstantin Khorenko 2026-10-28 12:22 ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko 2026-10-28 12:28 ` no-reply 2026-10-28 12:49 ` Pavel Tikhomirov 2026-10-28 14:08 ` Konstantin Khorenko 2026-10-28 13:09 ` Vasileios Almpanis 2026-10-28 14:12 ` Konstantin Khorenko 2026-10-28 12:49 ` [Devel] [PATCH hci-8.1 0/2] mm/memcontrol: numa_migrate destinations of the source " Pavel Tikhomirov 2026-10-28 13:09 ` Vasileios Almpanis
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.