Add a standalone test for GUEST_MEMFD_FLAG_BIND_NODE. It creates a guest_memfd bound to a given node, mmaps and faults it from the host, and checks with move_pages(2) that every folio landed on that node - with no userspace mbind(). It also covers the rejection paths with -EINVAL - a nonzero pad - an out-of-range node - a node supplied without the flag An unbindable (memoryless/offline) node is rejected at create time. It uses a default (non-protected) VM so the host can fault the fd to observe placement, and needs CONFIG_NUMA. Suggested-by: Dave Jiang Co-developed-by: Dave Jiang Signed-off-by: Dave Jiang Signed-off-by: Gregory Price Assisted-by: Claude:claude-opus-4-8 --- tools/testing/selftests/kvm/Makefile.kvm | 2 + .../kvm/guest_memfd_bind_node_test.c | 213 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 tools/testing/selftests/kvm/guest_memfd_bind_node_test.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index d28a057fa6c2d..f0ddb5cc39db9 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -155,6 +155,7 @@ TEST_GEN_PROGS_x86 += access_tracking_perf_test TEST_GEN_PROGS_x86 += coalesced_io_test TEST_GEN_PROGS_x86 += dirty_log_perf_test TEST_GEN_PROGS_x86 += guest_memfd_test +TEST_GEN_PROGS_x86 += guest_memfd_bind_node_test TEST_GEN_PROGS_x86 += hardware_disable_test TEST_GEN_PROGS_x86 += mmu_stress_test TEST_GEN_PROGS_x86 += rseq_test @@ -194,6 +195,7 @@ TEST_GEN_PROGS_arm64 += coalesced_io_test TEST_GEN_PROGS_arm64 += dirty_log_perf_test TEST_GEN_PROGS_arm64 += get-reg-list TEST_GEN_PROGS_arm64 += guest_memfd_test +TEST_GEN_PROGS_arm64 += guest_memfd_bind_node_test TEST_GEN_PROGS_arm64 += mmu_stress_test TEST_GEN_PROGS_arm64 += rseq_test TEST_GEN_PROGS_arm64 += steal_time diff --git a/tools/testing/selftests/kvm/guest_memfd_bind_node_test.c b/tools/testing/selftests/kvm/guest_memfd_bind_node_test.c new file mode 100644 index 0000000000000..6f33b1ae6c42a --- /dev/null +++ b/tools/testing/selftests/kvm/guest_memfd_bind_node_test.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Standalone test for GUEST_MEMFD_FLAG_BIND_NODE. + * + * Verifies that a guest_memfd created with GUEST_MEMFD_FLAG_BIND_NODE allocates + * its folios on the requested NUMA node, without any userspace mbind(). + * + * The folio placement is checked with move_pages(2) (status query mode). + * + * Self-contained: + * - issues the KVM ioctls directly + * + * Build inside the guest: + * gcc -O2 -o guest_memfd_bind_node_test guest_memfd_bind_node_test.c + * + * Usage: guest_memfd_bind_node_test + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "kselftest.h" + +/* Mirror the (proposed) uapi additions in case the host headers lag. */ +#ifndef GUEST_MEMFD_FLAG_MMAP +#define GUEST_MEMFD_FLAG_MMAP (1ULL << 0) +#endif +#ifndef GUEST_MEMFD_FLAG_INIT_SHARED +#define GUEST_MEMFD_FLAG_INIT_SHARED (1ULL << 1) +#endif +#ifndef GUEST_MEMFD_FLAG_BIND_NODE +#define GUEST_MEMFD_FLAG_BIND_NODE (1ULL << 2) +#endif +#ifndef KVM_CAP_GUEST_MEMFD_FLAGS +#define KVM_CAP_GUEST_MEMFD_FLAGS 244 +#endif + +struct gmemfd_args { + __u64 size; + __u64 flags; + __u32 node; + __u32 pad; + __u64 reserved[5]; +}; + +/* True if the node has any memory (MemTotal > 0). */ +static int node_has_memory(int node) +{ + char path[128]; + char line[256]; + FILE *f; + long total = -1; + + snprintf(path, sizeof(path), + "/sys/devices/system/node/node%d/meminfo", node); + f = fopen(path, "r"); + if (!f) + return 0; + while (fgets(line, sizeof(line), f)) { + if (strstr(line, "MemTotal:")) { + if (sscanf(line, "Node %*d MemTotal: %ld kB", &total) == 1) + break; + } + } + fclose(f); + return total > 0; +} + +static long move_pages_status(void *addr) +{ + void *pages[1] = { addr }; + int status[1] = { -1 }; + long ret; + + ret = syscall(SYS_move_pages, 0, 1, pages, NULL, status, 0); + if (ret) + err(1, "move_pages"); + return status[0]; +} + +int main(int argc, char **argv) +{ + int kvm, vm, gmem, ret; + long page_size = sysconf(_SC_PAGESIZE); + size_t size = page_size * 4; + int target_node; + struct gmemfd_args ga = {}; + char *mem; + size_t i; + + if (argc != 2) + ksft_exit_skip("usage: %s \n", argv[0]); + target_node = atoi(argv[1]); + + kvm = open("/dev/kvm", O_RDWR); + if (kvm < 0) + ksft_exit_skip("open /dev/kvm: %s\n", strerror(errno)); + + /* + * Use a default (non-protected) VM: it has no private memory, so it + * advertises GUEST_MEMFD_FLAG_INIT_SHARED, which lets the host mmap and + * fault the gmem (needed to observe placement with move_pages()). A + * SW-protected VM would not advertise INIT_SHARED and the host fault + * would SIGBUS. + */ + vm = ioctl(kvm, KVM_CREATE_VM, 0UL); + if (vm < 0) + ksft_exit_skip("KVM_CREATE_VM: %s\n", strerror(errno)); + + { + long caps = ioctl(vm, KVM_CHECK_EXTENSION, + KVM_CAP_GUEST_MEMFD_FLAGS); + + printf("KVM_CAP_GUEST_MEMFD_FLAGS = 0x%lx\n", caps); + if (!(caps & GUEST_MEMFD_FLAG_BIND_NODE)) + ksft_exit_skip("BIND_NODE flag not advertised (need CONFIG_NUMA)\n"); + if (!(caps & GUEST_MEMFD_FLAG_INIT_SHARED)) + ksft_exit_skip("INIT_SHARED not advertised; cannot fault from host\n"); + } + + /* Negative: pad must be zero. */ + ga = (struct gmemfd_args){ .size = size, + .flags = GUEST_MEMFD_FLAG_BIND_NODE, + .node = target_node, .pad = 1 }; + ret = ioctl(vm, KVM_CREATE_GUEST_MEMFD, &ga); + if (!(ret < 0 && errno == EINVAL)) + errx(1, "nonzero pad should be rejected (ret=%d errno=%d)", ret, errno); + printf("OK: nonzero pad rejected\n"); + + /* Negative: out-of-range node. */ + ga = (struct gmemfd_args){ .size = size, + .flags = GUEST_MEMFD_FLAG_BIND_NODE, + .node = 1 << 20 }; + ret = ioctl(vm, KVM_CREATE_GUEST_MEMFD, &ga); + if (!(ret < 0 && errno == EINVAL)) + errx(1, "bogus node should be rejected (ret=%d errno=%d)", ret, errno); + printf("OK: out-of-range node rejected\n"); + + /* + * Negative: node set without the flag. Only meaningful for a nonzero + * node: node 0 is indistinguishable from "node field unset". + */ + if (target_node != 0) { + ga = (struct gmemfd_args){ .size = size, .flags = 0, + .node = target_node }; + ret = ioctl(vm, KVM_CREATE_GUEST_MEMFD, &ga); + if (!(ret < 0 && errno == EINVAL)) + errx(1, "node without flag should be rejected (ret=%d errno=%d)", + ret, errno); + printf("OK: node-without-flag rejected\n"); + } + + /* Positive: bind to target_node, mmap, fault, verify placement. */ + ga = (struct gmemfd_args){ + .size = size, + .flags = GUEST_MEMFD_FLAG_BIND_NODE | GUEST_MEMFD_FLAG_MMAP | + GUEST_MEMFD_FLAG_INIT_SHARED, + .node = target_node, + }; + gmem = ioctl(vm, KVM_CREATE_GUEST_MEMFD, &ga); + if (gmem < 0) { + /* + * A node that cannot be bound - memoryless/offline, or a private + * node that did not opt into userspace placement - must be + * rejected at create with EINVAL (mpol_bind_node() fails), not + * produce an opaque failure later. + */ + if (!node_has_memory(target_node) && errno == EINVAL) { + printf("OK: unbindable node %d rejected with EINVAL\n", + target_node); + return 0; + } + err(1, "KVM_CREATE_GUEST_MEMFD(bind node %d)", target_node); + } + if (!node_has_memory(target_node)) + errx(1, "FAIL: unbindable node %d should have been rejected", + target_node); + printf("OK: created gmem fd=%d bound to node %d\n", gmem, target_node); + + mem = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, gmem, 0); + if (mem == MAP_FAILED) + err(1, "mmap gmem"); + + /* Fault every page in (no mbind on this mapping). */ + memset(mem, 0xaa, size); + + for (i = 0; i < size / page_size; i++) { + long node = move_pages_status(mem + i * page_size); + + printf("page %zu -> node %ld (want %d)\n", i, node, target_node); + if (node != target_node) + errx(1, "FAIL: page %zu landed on node %ld, wanted %d", + i, node, target_node); + } + + printf("PASS: all %zu pages on node %d via FLAG_BIND_NODE (no mbind)\n", + size / page_size, target_node); + + munmap(mem, size); + close(gmem); + close(vm); + close(kvm); + return 0; +} -- 2.53.0-Meta