* [PATCH] selftests/mm: uffd-missing-wp: add GC related functionality test
@ 2026-09-28 3:26 Martin Cracauer
2026-09-28 3:38 ` Andrew Morton
2026-09-28 8:53 ` David Hildenbrand (Arm)
0 siblings, 2 replies; 3+ messages in thread
From: Martin Cracauer @ 2026-09-28 3:26 UTC (permalink / raw)
To: akpm, linux-mm
Cc: david, ljs, liam, vbabka, rppt, surenb, mhocko, shuah, peterx,
linux-kernel, linux-kselftest
Add a test that asserts that functionality needed for generational
garbage collection works. Namely to implement a write barrier using
userfaultfd instead of using segmentation faults. The planned target
is the GC in SBCL.
The real GC will initially fill heap pages by using on-demand loading
of pages from an image file, either via mmap(2) or using userfaultfd
to decompress a page read from an image file made from individually
compressed blocks. This test doesn't do that so that it needs no disk
I/O and no decompression library. Instead, a fault will be filled by
copying page content from prepared pages in the heap.
Assisted-by: Anthropic:Opus-5.5
Signed-off-by: Martin Cracauer <cracauer@cons.org>
---
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/uffd-missing-wp.c | 413 +++++++++++++++++++
2 files changed, 414 insertions(+)
create mode 100644 tools/testing/selftests/mm/uffd-missing-wp.c
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index d3e9bd67904a..76176d62b3b3 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -92,6 +92,7 @@ TEST_GEN_FILES += transhuge-stress
TEST_GEN_FILES += uffd-stress
TEST_GEN_FILES += uffd-unit-tests
TEST_GEN_FILES += uffd-wp-mremap
+TEST_GEN_FILES += uffd-missing-wp
TEST_GEN_FILES += split_huge_page_test
TEST_GEN_FILES += ksm_tests
TEST_GEN_FILES += ksm_functional_tests
diff --git a/tools/testing/selftests/mm/uffd-missing-wp.c b/tools/testing/selftests/mm/uffd-missing-wp.c
new file mode 100644
index 000000000000..481e7056cebd
--- /dev/null
+++ b/tools/testing/selftests/mm/uffd-missing-wp.c
@@ -0,0 +1,413 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * userfaultfd MISSING + WP test, modelled on a garbage collector.
+ *
+ * An anonymous region is registered with userfaultfd in both MISSING and
+ * WP modes. A handler thread plays the part of the GC runtime:
+ *
+ * - On a missing fault it fills the page from a backing store with
+ * UFFDIO_COPY. (A real GC might keep evicted pages compressed on disk;
+ * here the backing store is an in-memory buffer with distinct contents
+ * per page, so a page filled with the wrong data is detected.)
+ *
+ * - On a write-protect fault it records the page as dirty and removes
+ * write protection from that page only. This is the userfaultfd
+ * equivalent of mprotect(PROT_READ) plus bookkeeping in a SIGSEGV
+ * handler, as used for GC write barriers.
+ *
+ * The main thread checks that:
+ * - reads of missing pages are resolved with the right contents,
+ * - a write to a missing page is resolved before the write lands,
+ * - a write to a write-protected page is reported with the WP flag and
+ * then completes,
+ * - write protection is tracked and resolved per page,
+ * - UFFDIO_UNREGISTER leaves populated contents intact and stops
+ * delivering faults for the range.
+ *
+ * Like the GC it models, the handler assumes pages are only write-protected
+ * after they have been populated.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/eventfd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+#include <linux/userfaultfd.h>
+
+#include "../kselftest.h"
+
+#define NR_PAGES 4
+#define NR_TESTS 7
+
+/* GC bookkeeping, updated by the handler thread. */
+struct page_stats {
+ unsigned int missing_faults;
+ unsigned int wp_faults;
+ uint64_t last_flags;
+};
+
+static size_t page_size;
+static size_t area_len;
+static int uffd;
+static int stop_fd;
+static char *area;
+static char *backing;/* what the handler fills missing pages from */
+static char *expected;/* what each page should contain right now */
+static struct page_stats stats[NR_PAGES];
+
+#define LOAD(x)__atomic_load_n(&(x), __ATOMIC_ACQUIRE)
+#define STORE(x, v)__atomic_store_n(&(x), (v), __ATOMIC_RELEASE)
+#define INC(x)__atomic_add_fetch(&(x), 1, __ATOMIC_RELEASE)
+
+static uint64_t pattern(unsigned long page, unsigned long word)
+{
+ return 0xdeadbeef00000000ULL | (page << 16) | word;
+}
+
+static uint64_t *page_ptr(unsigned long page)
+{
+ return (uint64_t *)(area + page * page_size);
+}
+
+/* Write a word in the region and record it in the expected image. */
+static void store(unsigned long page, unsigned long word, uint64_t val)
+{
+ page_ptr(page)[word] = val;
+ ((uint64_t *)(expected + page * page_size))[word] = val;
+}
+
+static void handle_missing(unsigned long page)
+{
+ struct uffdio_copy copy = {
+ .dst = (unsigned long)area + page * page_size,
+ .src = (unsigned long)backing + page * page_size,
+ .len = page_size,
+ .mode = 0,
+ };
+ struct uffdio_range range = {
+ .start = copy.dst,
+ .len = page_size,
+ };
+
+ if (!ioctl(uffd, UFFDIO_COPY, ©))
+ return;
+ if (errno != EEXIST)
+ ksft_exit_fail_msg("UFFDIO_COPY page %lu: %s\n",
+ page, strerror(errno));
+ /* Populated concurrently; the faulting thread still needs a wake. */
+ if (ioctl(uffd, UFFDIO_WAKE, &range))
+ ksft_exit_fail_msg("UFFDIO_WAKE page %lu: %s\n",
+ page, strerror(errno));
+}
+
+static void handle_wp(unsigned long page)
+{
+ struct uffdio_writeprotect wp = {
+ .range = {
+ .start = (unsigned long)area + page * page_size,
+ .len = page_size,
+ },
+ .mode = 0,/* clear WP; also wakes the faulting thread */
+ };
+
+ if (ioctl(uffd, UFFDIO_WRITEPROTECT, &wp))
+ ksft_exit_fail_msg("UFFDIO_WRITEPROTECT clear page %lu: %s\n",
+ page, strerror(errno));
+}
+
+static void *fault_handler(void *arg)
+{
+ struct pollfd pfd[2] = {
+ { .fd = uffd, .events = POLLIN },
+ { .fd = stop_fd, .events = POLLIN },
+ };
+ struct uffd_msg msg;
+ unsigned long addr, page;
+ uint64_t flags;
+ ssize_t ret;
+
+ for (;;) {
+ if (poll(pfd, 2, -1) < 0) {
+ if (errno == EINTR)
+ continue;
+ ksft_exit_fail_msg("poll: %s\n", strerror(errno));
+ }
+ if (pfd[1].revents & POLLIN)
+ return NULL;
+ if (pfd[0].revents & POLLERR)
+ ksft_exit_fail_msg("POLLERR on userfaultfd\n");
+ if (!(pfd[0].revents & POLLIN))
+ continue;
+
+ ret = read(uffd, &msg, sizeof(msg));
+ if (ret < 0) {
+ if (errno == EAGAIN)
+ continue;
+ ksft_exit_fail_msg("read userfaultfd: %s\n",
+ strerror(errno));
+ }
+ if (ret != sizeof(msg))
+ ksft_exit_fail_msg("short read from userfaultfd: %zd\n",
+ ret);
+ if (msg.event != UFFD_EVENT_PAGEFAULT)
+ ksft_exit_fail_msg("unexpected userfaultfd event %u\n",
+ msg.event);
+
+ addr = msg.arg.pagefault.address & ~(page_size - 1);
+ if (addr < (unsigned long)area ||
+ addr >= (unsigned long)area + area_len)
+ ksft_exit_fail_msg("fault at %#lx outside region\n",
+ addr);
+ page = (addr - (unsigned long)area) / page_size;
+ flags = msg.arg.pagefault.flags;
+
+ /* Record before resolving, so the faulter sees it on wake. */
+ STORE(stats[page].last_flags, flags);
+ if (flags & UFFD_PAGEFAULT_FLAG_WP) {
+ INC(stats[page].wp_faults);
+ handle_wp(page);
+ } else {
+ INC(stats[page].missing_faults);
+ handle_missing(page);
+ }
+ }
+}
+
+static bool check_stats(unsigned long page, unsigned int missing,
+ unsigned int wp)
+{
+ unsigned int m = LOAD(stats[page].missing_faults);
+ unsigned int w = LOAD(stats[page].wp_faults);
+
+ if (m == missing && w == wp)
+ return true;
+ ksft_print_msg("page %lu: %u missing / %u wp faults, expected %u / %u\n",
+ page, m, w, missing, wp);
+ return false;
+}
+
+static bool check_flags(unsigned long page, uint64_t want)
+{
+ uint64_t mask = UFFD_PAGEFAULT_FLAG_WRITE | UFFD_PAGEFAULT_FLAG_WP;
+ uint64_t got = LOAD(stats[page].last_flags) & mask;
+
+ if (got == want)
+ return true;
+ ksft_print_msg("page %lu: fault flags %#llx, expected %#llx\n", page,
+ (unsigned long long)got, (unsigned long long)want);
+ return false;
+}
+
+static bool check_page(unsigned long page)
+{
+ const uint64_t *want = (uint64_t *)(expected + page * page_size);
+ unsigned long i;
+ uint64_t got;
+
+ for (i = 0; i < page_size / sizeof(uint64_t); i++) {
+ got = page_ptr(page)[i];
+ if (got != want[i]) {
+ ksft_print_msg("page %lu word %lu: %#llx, expected %#llx\n",
+ page, i, (unsigned long long)got,
+ (unsigned long long)want[i]);
+ return false;
+ }
+ }
+ return true;
+}
+
+static void test_read_missing(unsigned long page)
+{
+ bool ok;
+
+ (void)page_ptr(page)[0];
+ ok = check_stats(page, 1, 0);
+ ok &= check_flags(page, 0);
+ ok &= check_page(page);
+ ksft_test_result(ok, "read fault fills missing page %lu\n", page);
+}
+
+static void test_write_missing(void)
+{
+ bool ok;
+
+ store(1, 0, 0x42);
+ ok = check_stats(1, 1, 0);
+ ok &= check_flags(1, UFFD_PAGEFAULT_FLAG_WRITE);
+ ok &= check_page(1);
+ ksft_test_result(ok, "write fault fills missing page 1 before the write\n");
+}
+
+static void test_write_protected(void)
+{
+ struct uffdio_writeprotect wp = {
+ .range = { .start = (unsigned long)area, .len = area_len },
+ .mode = UFFDIO_WRITEPROTECT_MODE_WP,
+ };
+ bool ok;
+
+ if (ioctl(uffd, UFFDIO_WRITEPROTECT, &wp))
+ ksft_exit_fail_msg("UFFDIO_WRITEPROTECT: %s\n", strerror(errno));
+
+ store(0, 0, 0x43);
+ ok = check_stats(0, 1, 1);
+ ok &= check_flags(0, UFFD_PAGEFAULT_FLAG_WRITE | UFFD_PAGEFAULT_FLAG_WP);
+ ok &= check_page(0);
+ ksft_test_result(ok, "write to write-protected page 0 is reported, then completes\n");
+}
+
+static void test_wp_per_page(void)
+{
+ bool ok;
+
+ store(0, 1, 0x45);/* page 0 already unprotected: no new fault */
+ store(1, 2, 0x44);/* page 1 still protected: its own fault */
+ ok = check_stats(0, 1, 1);
+ ok &= check_stats(1, 1, 1);
+ ok &= check_flags(1, UFFD_PAGEFAULT_FLAG_WRITE | UFFD_PAGEFAULT_FLAG_WP);
+ ok &= check_stats(2, 1, 0);/* never written: not dirty */
+ ok &= check_page(0);
+ ok &= check_page(1);
+ ksft_test_result(ok, "write protection is tracked and resolved per page\n");
+}
+
+static void test_unregister(void)
+{
+ struct uffdio_range range = {
+ .start = (unsigned long)area,
+ .len = area_len,
+ };
+ unsigned long page;
+ bool ok = true;
+
+ if (ioctl(uffd, UFFDIO_UNREGISTER, &range))
+ ksft_exit_fail_msg("UFFDIO_UNREGISTER: %s\n", strerror(errno));
+
+ for (page = 0; page < 3; page++)
+ ok &= check_page(page);
+ ksft_test_result(ok, "populated contents survive UFFDIO_UNREGISTER\n");
+}
+
+static void test_after_unregister(void)
+{
+ bool ok;
+
+ /*
+ * Page 3 was never touched. If the fault were still routed to us,
+ * the handler would fill it from the backing store.
+ */
+ ok = check_page(3);
+ ok &= check_stats(3, 0, 0);
+ ksft_test_result(ok, "untouched page is zero-filled after unregister\n");
+}
+
+static int uffd_open(uint64_t features, uint64_t *supported)
+{
+ struct uffdio_api api = { .api = UFFD_API, .features = features };
+ int fd;
+
+ fd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+#ifdef UFFD_USER_MODE_ONLY
+ if (fd < 0 && errno == EPERM)
+ fd = syscall(__NR_userfaultfd,
+ O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY);
+#endif
+ if (fd < 0) {
+ if (errno == ENOSYS || errno == EPERM)
+ ksft_exit_skip("userfaultfd unavailable: %s\n",
+ strerror(errno));
+ ksft_exit_fail_msg("userfaultfd: %s\n", strerror(errno));
+ }
+ if (ioctl(fd, UFFDIO_API, &api))
+ ksft_exit_fail_msg("UFFDIO_API: %s\n", strerror(errno));
+ if (supported)
+ *supported = api.features;
+ return fd;
+}
+
+int main(void)
+{
+ uint64_t required = (1ULL << _UFFDIO_COPY) | (1ULL << _UFFDIO_WAKE) |
+ (1ULL << _UFFDIO_WRITEPROTECT);
+ struct uffdio_register reg;
+ unsigned long page, i;
+ uint64_t features;
+ pthread_t thread;
+ int ret;
+
+ ksft_print_header();
+
+ page_size = sysconf(_SC_PAGESIZE);
+ area_len = NR_PAGES * page_size;
+
+ /* UFFDIO_API can only be issued once per fd: probe on a throwaway. */
+ close(uffd_open(0, &features));
+ if (!(features & UFFD_FEATURE_PAGEFAULT_FLAG_WP))
+ ksft_exit_skip("userfaultfd write-protect not supported\n");
+ uffd = uffd_open(UFFD_FEATURE_PAGEFAULT_FLAG_WP, NULL);
+
+ area = mmap(NULL, area_len, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (area == MAP_FAILED)
+ ksft_exit_fail_msg("mmap: %s\n", strerror(errno));
+
+ backing = malloc(area_len);
+ expected = malloc(area_len);
+ if (!backing || !expected)
+ ksft_exit_fail_msg("malloc failed\n");
+ for (page = 0; page < NR_PAGES; page++)
+ for (i = 0; i < page_size / sizeof(uint64_t); i++)
+ ((uint64_t *)(backing + page * page_size))[i] =
+ pattern(page, i);
+ memcpy(expected, backing, area_len);
+ /* Page 3 is only read after unregister: expect a fresh zero page. */
+ memset(expected + 3 * page_size, 0, page_size);
+
+ reg.range.start = (unsigned long)area;
+ reg.range.len = area_len;
+ reg.mode = UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP;
+ if (ioctl(uffd, UFFDIO_REGISTER, ®))
+ ksft_exit_fail_msg("UFFDIO_REGISTER: %s\n", strerror(errno));
+ if ((reg.ioctls & required) != required)
+ ksft_exit_fail_msg("missing range ioctls: have %#llx, need %#llx\n",
+ (unsigned long long)reg.ioctls,
+ (unsigned long long)required);
+
+ stop_fd = eventfd(0, EFD_CLOEXEC);
+ if (stop_fd < 0)
+ ksft_exit_fail_msg("eventfd: %s\n", strerror(errno));
+ ret = pthread_create(&thread, NULL, fault_handler, NULL);
+ if (ret)
+ ksft_exit_fail_msg("pthread_create: %s\n", strerror(ret));
+
+ ksft_set_plan(NR_TESTS);
+
+ test_read_missing(0);
+ test_read_missing(2);
+ test_write_missing();
+ test_write_protected();
+ test_wp_per_page();
+ test_unregister();
+ test_after_unregister();
+
+ if (eventfd_write(stop_fd, 1))
+ ksft_exit_fail_msg("eventfd_write: %s\n", strerror(errno));
+ pthread_join(thread, NULL);
+
+ close(stop_fd);
+ close(uffd);
+ munmap(area, area_len);
+ free(backing);
+ free(expected);
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] selftests/mm: uffd-missing-wp: add GC related functionality test
2026-09-28 3:26 [PATCH] selftests/mm: uffd-missing-wp: add GC related functionality test Martin Cracauer
@ 2026-09-28 3:38 ` Andrew Morton
2026-09-28 8:53 ` David Hildenbrand (Arm)
1 sibling, 0 replies; 3+ messages in thread
From: Andrew Morton @ 2026-09-28 3:38 UTC (permalink / raw)
To: Martin Cracauer
Cc: linux-mm, david, ljs, liam, vbabka, rppt, surenb, mhocko, shuah,
peterx, linux-kernel, linux-kselftest
On Sun, 27 Sep 2026 23:26:31 -0400 Martin Cracauer <cracauer@cons.org> wrote:
> Add a test that asserts that functionality needed for generational
> garbage collection works. Namely to implement a write barrier using
> userfaultfd instead of using segmentation faults. The planned target
> is the GC in SBCL.
>
> The real GC will initially fill heap pages by using on-demand loading
> of pages from an image file, either via mmap(2) or using userfaultfd
> to decompress a page read from an image file made from individually
> compressed blocks. This test doesn't do that so that it needs no disk
> I/O and no decompression library. Instead, a fault will be filled by
> copying page content from prepared pages in the heap.
Thanks. Sashiko asked a couple of things:
https://sashiko.dev/#/patchset/arneZ-KkRWUNI0Ho@cons.org
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] selftests/mm: uffd-missing-wp: add GC related functionality test
2026-09-28 3:26 [PATCH] selftests/mm: uffd-missing-wp: add GC related functionality test Martin Cracauer
2026-09-28 3:38 ` Andrew Morton
@ 2026-09-28 8:53 ` David Hildenbrand (Arm)
1 sibling, 0 replies; 3+ messages in thread
From: David Hildenbrand (Arm) @ 2026-09-28 8:53 UTC (permalink / raw)
To: Martin Cracauer, akpm, linux-mm
Cc: ljs, liam, vbabka, rppt, surenb, mhocko, shuah, peterx,
linux-kernel, linux-kselftest
On 9/28/26 05:26, Martin Cracauer wrote:
> Add a test that asserts that functionality needed for generational
> garbage collection works. Namely to implement a write barrier using
> userfaultfd instead of using segmentation faults. The planned target
> is the GC in SBCL.
>
> The real GC will initially fill heap pages by using on-demand loading
> of pages from an image file, either via mmap(2) or using userfaultfd
> to decompress a page read from an image file made from individually
> compressed blocks. This test doesn't do that so that it needs no disk
> I/O and no decompression library. Instead, a fault will be filled by
> copying page content from prepared pages in the heap.
>
> Assisted-by: Anthropic:Opus-5.5
> Signed-off-by: Martin Cracauer <cracauer@cons.org>
> ---
> tools/testing/selftests/mm/Makefile | 1 +
> tools/testing/selftests/mm/uffd-missing-wp.c | 413 +++++++++++++++++++
> 2 files changed, 414 insertions(+)
> create mode 100644 tools/testing/selftests/mm/uffd-missing-wp.c
Isn't something like that a better fit for LTP?
--
Cheers,
David
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-28 8:53 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-28 3:26 [PATCH] selftests/mm: uffd-missing-wp: add GC related functionality test Martin Cracauer
2026-09-28 3:38 ` Andrew Morton
2026-09-28 8:53 ` David Hildenbrand (Arm)
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®