[LTP] [Valgrind-developers] [PATCH] Make userfaultfd0{1, 3, 4} LTP tests valgrind compatible

Martin Cermak mcermak@redhat.com
Thu Apr 23 16:44:31 CEST 2026


On  Wed  2026-04-22  15:08 , Martin Cermak wrote:
> On  Wed  2026-04-22  14:17 , Petr Vorel wrote:
> > 
> > 
> > > On 4/22/26 7:37 AM, Petr Vorel wrote:
> > > > > However, some of the testcases can be easily changed to use forked
> > > > > processes instead of threads.  That's what this patch does.  When
> > > > > client program forks, Valgrind forks too, and that allows for the needed
> > > > > parallelism to handle the page fault.
> > > > You understand process vs. threads more than me. But shouldn't mmap() use
> > > > MAP_SHARED instead of MAP_PRIVATE for those which aren't using /dev/userfaultfd?
> > 
> > > The documentation for userfaultfd mentions threads.  I'm afraid we'll
> > > lose vital coverage if we move to forked processes.
> > 
> > +1, at least some tests would keep threads (not all userfaultfd tests would be
> > converted, but yes, that's why I suggested to use MAP_SHARED, which could be
> > similar to threads (yes, the difference between processes and threads in Linux
> > kernel is not that huge as both are created in clone(), it's mostly about what
> > is shared).
> 
> My goal is to make at least part of these userfaultfd tests
> compatible with Valgrind.  I've experimented with clone() and it
> seems like Valgrind has limited support for it.  That said,
> adding another set of userfaultfd tests using fork() or clone()
> instead of threads might help.  Some of such new tests will be
> "incompatible" with valgrind, but it might be a test coverage
> improvement.

Here is an ai-assisted patch that works for me:


>From 693f3015d39b39e2c263aac57c183e3e4cf7df4d Mon Sep 17 00:00:00 2001
From: Martin Cermak <mcermak@redhat.com>
Date: Thu, 23 Apr 2026 15:51:10 +0200
Subject: [PATCH] Provide Valgrind-compatible userfaultfd tests

The userfaultfd tests use threads.  Threads are executed serially
in Valgrind, but parallelism is important for userfaultfd tests.
When a page fault happens in parent thread, kernel stops it, and
waits for the handler thread to handle the page fault. That said,
userfaultfd tests 01 - 06 stall in Valgrind.

This update comes with second set of userfaultfd tests 07 - 12.
These additional tests use fork() or clone(), thus a separate
process instead of threads.  Support for clone() is also limited
in Valgrind. For that reason only tests 07, 09 and 10 are "Valgrind
compatible".  But it's a test coverage improvement.

Valgrind can filter LTP tests on the per test binary basis.  That's
why this update adds separate tests 07 - 12.

Assisted-by: Anthropic Claude
Reviewed-by: Martin Cermak <mcermak@redhat.com>
---
 include/lapi/sched.h                          |   8 +
 .../kernel/syscalls/userfaultfd/.gitignore    |   6 +
 .../kernel/syscalls/userfaultfd/Makefile      |   3 +
 .../syscalls/userfaultfd/userfaultfd07.c      | 136 +++++++++++++++
 .../syscalls/userfaultfd/userfaultfd08.c      | 113 +++++++++++++
 .../syscalls/userfaultfd/userfaultfd09.c      | 141 ++++++++++++++++
 .../syscalls/userfaultfd/userfaultfd10.c      | 106 ++++++++++++
 .../syscalls/userfaultfd/userfaultfd11.c      | 140 ++++++++++++++++
 .../syscalls/userfaultfd/userfaultfd12.c      | 157 ++++++++++++++++++
 9 files changed, 810 insertions(+)
 create mode 100644 testcases/kernel/syscalls/userfaultfd/userfaultfd07.c
 create mode 100644 testcases/kernel/syscalls/userfaultfd/userfaultfd08.c
 create mode 100644 testcases/kernel/syscalls/userfaultfd/userfaultfd09.c
 create mode 100644 testcases/kernel/syscalls/userfaultfd/userfaultfd10.c
 create mode 100644 testcases/kernel/syscalls/userfaultfd/userfaultfd11.c
 create mode 100644 testcases/kernel/syscalls/userfaultfd/userfaultfd12.c

diff --git a/include/lapi/sched.h b/include/lapi/sched.h
index 05b322c1c..b33de6657 100644
--- a/include/lapi/sched.h
+++ b/include/lapi/sched.h
@@ -121,6 +121,14 @@ static inline int getcpu(unsigned *cpu, unsigned *node)
 # define CLONE_FS	0x00000200
 #endif
 
+#ifndef CLONE_FILES
+# define CLONE_FILES	0x00000400
+#endif
+
+#ifndef CLONE_SIGHAND
+# define CLONE_SIGHAND	0x00000800
+#endif
+
 #ifndef CLONE_PIDFD
 # define CLONE_PIDFD	0x00001000
 #endif
diff --git a/testcases/kernel/syscalls/userfaultfd/.gitignore b/testcases/kernel/syscalls/userfaultfd/.gitignore
index bc32fdf3b..59d28c6f3 100644
--- a/testcases/kernel/syscalls/userfaultfd/.gitignore
+++ b/testcases/kernel/syscalls/userfaultfd/.gitignore
@@ -4,3 +4,9 @@
 /userfaultfd04
 /userfaultfd05
 /userfaultfd06
+/userfaultfd07
+/userfaultfd08
+/userfaultfd09
+/userfaultfd10
+/userfaultfd11
+/userfaultfd12
diff --git a/testcases/kernel/syscalls/userfaultfd/Makefile b/testcases/kernel/syscalls/userfaultfd/Makefile
index 3252e47df..e4ed56b4b 100644
--- a/testcases/kernel/syscalls/userfaultfd/Makefile
+++ b/testcases/kernel/syscalls/userfaultfd/Makefile
@@ -17,3 +17,6 @@ userfaultfd03: CFLAGS += -pthread
 userfaultfd04: CFLAGS += -pthread
 userfaultfd05: CFLAGS += -pthread
 userfaultfd06: CFLAGS += -pthread
+# Tests 07-12: Valgrind-compatible variants using fork/clone
+# Tests 07, 09, 10: fork-based (no pthread needed)
+# Tests 08, 11, 12: clone-based (no pthread needed)
diff --git a/testcases/kernel/syscalls/userfaultfd/userfaultfd07.c b/testcases/kernel/syscalls/userfaultfd/userfaultfd07.c
new file mode 100644
index 000000000..bebf0e921
--- /dev/null
+++ b/testcases/kernel/syscalls/userfaultfd/userfaultfd07.c
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2019 SUSE LLC
+ * Author: Christian Amann <camann@suse.com>
+ */
+
+/*\
+ * Force a pagefault event and handle it using :manpage:`userfaultfd(2)`
+ * from a different process (using fork instead of threads for Valgrind compatibility).
+ */
+
+#include <poll.h>
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "lapi/userfaultfd.h"
+
+#define BEFORE_5_11 1
+#define AFTER_5_11 2
+#define DESC(x) .flags = x, .desc = #x
+
+static struct tcase {
+	int flags;
+	const char *desc;
+	int kver;
+} tcases[] = {
+	{ DESC(O_CLOEXEC | O_NONBLOCK) },
+	{ DESC(O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY),  .kver = AFTER_5_11, },
+};
+
+static int page_size;
+static char *page;
+static void *copy_page;
+static int uffd;
+static int kver;
+
+static void setup(void)
+{
+	if (tst_kvercmp(5, 11, 0) >= 0)
+		kver = AFTER_5_11;
+	else
+		kver = BEFORE_5_11;
+}
+
+static void set_pages(void)
+{
+	page_size = sysconf(_SC_PAGE_SIZE);
+	page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	copy_page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+}
+
+static void reset_pages(void)
+{
+	SAFE_MUNMAP(page, page_size);
+	SAFE_MUNMAP(copy_page, page_size);
+}
+
+static void *pagefault_handler(void)
+{
+	static struct uffd_msg msg;
+	struct uffdio_copy uffdio_copy = {};
+
+	struct pollfd pollfd;
+	int nready;
+
+	pollfd.fd = uffd;
+	pollfd.events = POLLIN;
+	nready = poll(&pollfd, 1, -1);
+	if (nready == -1)
+		tst_brk(TBROK | TERRNO, "Error on poll");
+
+	SAFE_READ(1, uffd, &msg, sizeof(msg));
+
+	if (msg.event != UFFD_EVENT_PAGEFAULT)
+		tst_brk(TBROK | TERRNO, "Received unexpected UFFD_EVENT %d", msg.event);
+
+	memset(copy_page, 'X', page_size);
+
+	uffdio_copy.src = (unsigned long) copy_page;
+
+	uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address
+			& ~(page_size - 1);
+	uffdio_copy.len = page_size;
+	SAFE_IOCTL(uffd, UFFDIO_COPY, &uffdio_copy);
+
+	close(uffd);
+	return NULL;
+}
+
+static void run(unsigned int i)
+{
+	pid_t pid;
+	struct uffdio_api uffdio_api = {};
+	struct uffdio_register uffdio_register;
+	struct tcase *tc = &tcases[i];
+
+	if (tc->kver == AFTER_5_11 && kver == BEFORE_5_11)
+		tst_brk(TCONF, "%s requires kernel >= 5.11", tc->desc);
+
+	set_pages();
+
+	uffd = SAFE_USERFAULTFD(tc->flags, false);
+
+	uffdio_api.api = UFFD_API;
+	SAFE_IOCTL(uffd, UFFDIO_API, &uffdio_api);
+
+	uffdio_register.range.start = (unsigned long) page;
+	uffdio_register.range.len = page_size;
+	uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
+
+	SAFE_IOCTL(uffd, UFFDIO_REGISTER, &uffdio_register);
+
+	pid = SAFE_FORK();
+	if (pid == 0) {
+		pagefault_handler();
+		_exit(0);
+	}
+
+	char c = page[0xf];
+
+	if (c == 'X')
+		tst_res(TPASS, "Pagefault handled!");
+	else
+		tst_res(TFAIL, "Pagefault not handled!");
+
+	SAFE_WAITPID(pid, NULL, 0);
+	reset_pages();
+}
+
+static struct tst_test test = {
+	.setup = setup,
+	.test = run,
+	.tcnt = ARRAY_SIZE(tcases),
+	.forks_child = 1,
+};
diff --git a/testcases/kernel/syscalls/userfaultfd/userfaultfd08.c b/testcases/kernel/syscalls/userfaultfd/userfaultfd08.c
new file mode 100644
index 000000000..f6bde86cc
--- /dev/null
+++ b/testcases/kernel/syscalls/userfaultfd/userfaultfd08.c
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 SUSE LLC
+ * Author: Christian Amann <camann@suse.com>
+ * Author: Ricardo Branco <rbranco@suse.com>
+ */
+
+/*\
+ * Force a pagefault event and handle it using :manpage:`userfaultfd(2)`
+ * from a different process (using clone with CLONE_VM instead of threads
+ * for Valgrind compatibility) using UFFDIO_MOVE.
+ */
+
+#include "config.h"
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "tst_clone.h"
+#include "lapi/sched.h"
+#include "lapi/userfaultfd.h"
+
+#define CHILD_STACK_SIZE (1024 * 1024)
+
+static int page_size;
+static char *page;
+static void *move_page;
+static int uffd;
+static void *child_stack;
+
+static void set_pages(void)
+{
+	page_size = sysconf(_SC_PAGE_SIZE);
+	page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	move_page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+}
+
+static void reset_pages(void)
+{
+	SAFE_MUNMAP(page, page_size);
+	/*
+	 * After UFFDIO_MOVE, move_page is invalid and should not be unmapped.
+	 * The kernel has already removed the mapping during the move operation.
+	 */
+}
+
+static int pagefault_handler(void *arg LTP_ATTRIBUTE_UNUSED)
+{
+	struct uffd_msg msg;
+	struct uffdio_move uffdio_move = {};
+
+	SAFE_READ(1, uffd, &msg, sizeof(msg));
+
+	if (msg.event != UFFD_EVENT_PAGEFAULT)
+		tst_brk(TBROK | TERRNO, "Received unexpected UFFD_EVENT %d", msg.event);
+
+	memset(move_page, 'X', page_size);
+
+	uffdio_move.src = (unsigned long) move_page;
+
+	uffdio_move.dst = (unsigned long) msg.arg.pagefault.address
+			& ~(page_size - 1);
+	uffdio_move.len = page_size;
+	SAFE_IOCTL(uffd, UFFDIO_MOVE, &uffdio_move);
+
+	close(uffd);
+	return 0;
+}
+
+static void run(void)
+{
+	pid_t pid;
+	struct uffdio_api uffdio_api = {};
+	struct uffdio_register uffdio_register;
+
+	set_pages();
+
+	uffd = SAFE_USERFAULTFD(O_CLOEXEC, false);
+
+	uffdio_api.api = UFFD_API;
+	SAFE_IOCTL(uffd, UFFDIO_API, &uffdio_api);
+
+	uffdio_register.range.start = (unsigned long) page;
+	uffdio_register.range.len = page_size;
+	uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
+
+	SAFE_IOCTL(uffd, UFFDIO_REGISTER, &uffdio_register);
+
+	pid = ltp_clone(CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | SIGCHLD,
+			pagefault_handler, NULL, CHILD_STACK_SIZE, child_stack);
+	if (pid == -1)
+		tst_brk(TBROK | TERRNO, "ltp_clone failed");
+
+	char c = page[0xf];
+
+	if (c == 'X')
+		tst_res(TPASS, "Pagefault handled via UFFDIO_MOVE");
+	else
+		tst_res(TFAIL, "Pagefault not handled via UFFDIO_MOVE");
+
+	SAFE_WAITPID(pid, NULL, 0);
+	reset_pages();
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.min_kver = "6.8",
+	.forks_child = 1,
+	.bufs = (struct tst_buffers []) {
+		{&child_stack, .size = CHILD_STACK_SIZE},
+		{},
+	},
+};
diff --git a/testcases/kernel/syscalls/userfaultfd/userfaultfd09.c b/testcases/kernel/syscalls/userfaultfd/userfaultfd09.c
new file mode 100644
index 000000000..e1190a4ce
--- /dev/null
+++ b/testcases/kernel/syscalls/userfaultfd/userfaultfd09.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 SUSE LLC
+ * Author: Christian Amann <camann@suse.com>
+ * Author: Ricardo Branco <rbranco@suse.com>
+ */
+
+/*\
+ * Force a pagefault event and handle it using :manpage:`userfaultfd(2)`
+ * from a different process (using fork instead of threads for Valgrind compatibility)
+ * using /dev/userfaultfd instead of syscall, using USERFAULTFD_IOC_NEW ioctl to create
+ * the uffd & UFFDIO_COPY.
+ */
+
+#include "config.h"
+#include <poll.h>
+#include <unistd.h>
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "lapi/userfaultfd.h"
+
+static int page_size;
+static char *page;
+static void *copy_page;
+static int uffd;
+
+static void setup(void)
+{
+	if (access("/dev/userfaultfd", F_OK) != 0) {
+		int res = (tst_kvercmp(6, 1, 0) < 0) ? TCONF : TBROK;
+
+		tst_brk(res, "/dev/userfaultfd not found");
+	}
+}
+
+static int open_userfaultfd(int flags)
+{
+	int fd, fd2;
+
+	fd = SAFE_OPEN("/dev/userfaultfd", O_RDWR);
+
+	fd2 = SAFE_IOCTL(fd, USERFAULTFD_IOC_NEW, flags);
+
+	SAFE_CLOSE(fd);
+
+	return fd2;
+}
+
+static void set_pages(void)
+{
+	page_size = sysconf(_SC_PAGE_SIZE);
+	page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	copy_page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+}
+
+static void reset_pages(void)
+{
+	SAFE_MUNMAP(page, page_size);
+	SAFE_MUNMAP(copy_page, page_size);
+}
+
+static void *pagefault_handler(void)
+{
+	static struct uffd_msg msg;
+	struct uffdio_copy uffdio_copy = {};
+
+	struct pollfd pollfd;
+	int nready;
+
+	pollfd.fd = uffd;
+	pollfd.events = POLLIN;
+	nready = poll(&pollfd, 1, -1);
+	if (nready == -1)
+		tst_brk(TBROK | TERRNO, "Error on poll");
+
+	SAFE_READ(1, uffd, &msg, sizeof(msg));
+
+	if (msg.event != UFFD_EVENT_PAGEFAULT)
+		tst_brk(TBROK | TERRNO, "Received unexpected UFFD_EVENT %d", msg.event);
+
+	memset(copy_page, 'X', page_size);
+
+	uffdio_copy.src = (unsigned long) copy_page;
+
+	uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address
+			& ~(page_size - 1);
+	uffdio_copy.len = page_size;
+	SAFE_IOCTL(uffd, UFFDIO_COPY, &uffdio_copy);
+
+	close(uffd);
+	return NULL;
+}
+
+static void run(void)
+{
+	pid_t pid;
+	struct uffdio_api uffdio_api = {};
+	struct uffdio_register uffdio_register;
+
+	set_pages();
+
+	uffd = open_userfaultfd(O_CLOEXEC | O_NONBLOCK);
+
+	uffdio_api.api = UFFD_API;
+	SAFE_IOCTL(uffd, UFFDIO_API, &uffdio_api);
+
+	uffdio_register.range.start = (unsigned long) page;
+	uffdio_register.range.len = page_size;
+	uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
+
+	SAFE_IOCTL(uffd, UFFDIO_REGISTER, &uffdio_register);
+
+	pid = SAFE_FORK();
+	if (pid == 0) {
+		pagefault_handler();
+		_exit(0);
+	}
+
+	char c = page[0xf];
+
+	if (c == 'X')
+		tst_res(TPASS, "Pagefault handled via /dev/userfaultfd");
+	else
+		tst_res(TFAIL, "Pagefault not handled via /dev/userfaultfd");
+
+	SAFE_WAITPID(pid, NULL, 0);
+	reset_pages();
+}
+
+static struct tst_test test = {
+	.needs_root = 1,
+	.setup = setup,
+	.test_all = run,
+	.needs_kconfigs = (const char *[]) {
+		"CONFIG_USERFAULTFD=y",
+		NULL
+	},
+	.forks_child = 1,
+};
diff --git a/testcases/kernel/syscalls/userfaultfd/userfaultfd10.c b/testcases/kernel/syscalls/userfaultfd/userfaultfd10.c
new file mode 100644
index 000000000..8e9bd0b0c
--- /dev/null
+++ b/testcases/kernel/syscalls/userfaultfd/userfaultfd10.c
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 SUSE LLC
+ * Author: Christian Amann <camann@suse.com>
+ * Author: Ricardo Branco <rbranco@suse.com>
+ */
+
+/*\
+ * Force a pagefault event and handle it using :manpage:`userfaultfd(2)`
+ * from a different process (using fork instead of threads for Valgrind compatibility)
+ * using UFFDIO_ZEROPAGE.
+ */
+
+#include "config.h"
+#include <poll.h>
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "lapi/userfaultfd.h"
+
+static int page_size;
+static char *page;
+static int uffd;
+
+static void set_pages(void)
+{
+	page_size = sysconf(_SC_PAGE_SIZE);
+	page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+}
+
+static void reset_pages(void)
+{
+	SAFE_MUNMAP(page, page_size);
+}
+
+static void *pagefault_handler(void)
+{
+	static struct uffd_msg msg;
+	struct uffdio_zeropage uffdio_zeropage = {};
+
+	struct pollfd pollfd;
+	int nready;
+
+	pollfd.fd = uffd;
+	pollfd.events = POLLIN;
+	nready = poll(&pollfd, 1, -1);
+	if (nready == -1)
+		tst_brk(TBROK | TERRNO, "Error on poll");
+
+	SAFE_READ(1, uffd, &msg, sizeof(msg));
+
+	if (msg.event != UFFD_EVENT_PAGEFAULT)
+		tst_brk(TBROK | TERRNO, "Received unexpected UFFD_EVENT %d", msg.event);
+
+	uffdio_zeropage.range.start	= msg.arg.pagefault.address
+					& ~(page_size - 1);
+	uffdio_zeropage.range.len	= page_size;
+
+	SAFE_IOCTL(uffd, UFFDIO_ZEROPAGE, &uffdio_zeropage);
+
+	close(uffd);
+	return NULL;
+}
+
+static void run(void)
+{
+	pid_t pid;
+	struct uffdio_api uffdio_api = {};
+	struct uffdio_register uffdio_register;
+
+	set_pages();
+
+	uffd = SAFE_USERFAULTFD(O_CLOEXEC | O_NONBLOCK, false);
+
+	uffdio_api.api = UFFD_API;
+	SAFE_IOCTL(uffd, UFFDIO_API, &uffdio_api);
+
+	uffdio_register.range.start = (unsigned long) page;
+	uffdio_register.range.len = page_size;
+	uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
+
+	SAFE_IOCTL(uffd, UFFDIO_REGISTER, &uffdio_register);
+
+	pid = SAFE_FORK();
+	if (pid == 0) {
+		pagefault_handler();
+		_exit(0);
+	}
+
+	for (int i = 0; i < page_size; i++) {
+		if (page[i] != 0) {
+			tst_res(TFAIL, "page[%d]=0x%x not zero", i, page[i]);
+			return;
+		}
+	}
+
+	tst_res(TPASS, "Pagefault handled with UFFDIO_ZEROPAGE");
+
+	SAFE_WAITPID(pid, NULL, 0);
+	reset_pages();
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.forks_child = 1,
+};
diff --git a/testcases/kernel/syscalls/userfaultfd/userfaultfd11.c b/testcases/kernel/syscalls/userfaultfd/userfaultfd11.c
new file mode 100644
index 000000000..52590d117
--- /dev/null
+++ b/testcases/kernel/syscalls/userfaultfd/userfaultfd11.c
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 SUSE LLC
+ * Author: Christian Amann <camann@suse.com>
+ * Author: Ricardo Branco <rbranco@suse.com>
+ */
+
+/*\
+ * Force a pagefault event and handle it using :manpage:`userfaultfd(2)`
+ * from a different process (using clone with CLONE_VM instead of threads
+ * for Valgrind compatibility) testing UFFDIO_WRITEPROTECT_MODE_WP.
+ */
+
+#include "config.h"
+#include <poll.h>
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "lapi/sched.h"
+#include "lapi/userfaultfd.h"
+
+static int page_size;
+static char *page;
+static int uffd;
+static volatile int wp_fault_seen;
+
+static void set_pages(void)
+{
+	page_size = sysconf(_SC_PAGE_SIZE);
+	page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+	memset(page, 0, page_size);
+}
+
+static void reset_pages(void)
+{
+	SAFE_MUNMAP(page, page_size);
+}
+
+static void pagefault_handler(void)
+{
+	struct uffd_msg msg;
+	struct uffdio_writeprotect uffdio_writeprotect = {};
+
+	struct pollfd pollfd;
+	int nready;
+
+	pollfd.fd = uffd;
+	pollfd.events = POLLIN;
+	nready = poll(&pollfd, 1, -1);
+	if (nready == -1)
+		tst_brk(TBROK | TERRNO, "Error on poll");
+
+	SAFE_READ(1, uffd, &msg, sizeof(msg));
+
+	if (msg.event != UFFD_EVENT_PAGEFAULT)
+		tst_brk(TFAIL, "Received unexpected UFFD_EVENT %d", msg.event);
+
+	if (!(msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WP) ||
+	    !(msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WRITE)) {
+		tst_brk(TFAIL,
+			"Expected WP+WRITE fault but flags=%lx",
+			(unsigned long)msg.arg.pagefault.flags);
+	}
+
+	/* While the WP fault is pending, the write must NOT be visible. */
+	if (page[0xf] != 0)
+		tst_brk(TFAIL,
+			"Write became visible while page was write-protected!");
+
+	wp_fault_seen = 1;
+
+	/* Resolve the fault by clearing WP so the writer can resume. */
+	uffdio_writeprotect.range.start	= msg.arg.pagefault.address & ~(page_size - 1);
+	uffdio_writeprotect.range.len	= page_size;
+
+	SAFE_IOCTL(uffd, UFFDIO_WRITEPROTECT, &uffdio_writeprotect);
+
+	close(uffd);
+}
+
+static void run(void)
+{
+	const struct tst_clone_args clone_args = {
+		.flags = CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND,
+		.exit_signal = SIGCHLD,
+	};
+	pid_t pid;
+	struct uffdio_api uffdio_api;
+	struct uffdio_register uffdio_register;
+	struct uffdio_writeprotect uffdio_writeprotect;
+
+	set_pages();
+
+	uffd = SAFE_USERFAULTFD(O_CLOEXEC | O_NONBLOCK, false);
+
+	uffdio_api.api = UFFD_API;
+	uffdio_api.features = UFFD_FEATURE_PAGEFAULT_FLAG_WP;
+
+	SAFE_IOCTL(uffd, UFFDIO_API, &uffdio_api);
+
+	uffdio_register.range.start = (unsigned long) page;
+	uffdio_register.range.len = page_size;
+	uffdio_register.mode = UFFDIO_REGISTER_MODE_WP;
+
+	SAFE_IOCTL(uffd, UFFDIO_REGISTER, &uffdio_register);
+
+	uffdio_writeprotect.range.start	= (unsigned long)page;
+	uffdio_writeprotect.range.len	= page_size;
+	uffdio_writeprotect.mode	= UFFDIO_WRITEPROTECT_MODE_WP;
+
+	SAFE_IOCTL(uffd, UFFDIO_WRITEPROTECT, &uffdio_writeprotect);
+
+	pid = SAFE_CLONE(&clone_args);
+	if (!pid) {
+		pagefault_handler();
+		_exit(0);
+	}
+
+	/* Try to write */
+	page[0xf] = 'W';
+
+	SAFE_WAITPID(pid, NULL, 0);
+	reset_pages();
+
+	if (wp_fault_seen)
+		tst_res(TPASS, "WRITEPROTECT pagefault handled!");
+	else
+		tst_res(TFAIL, "No WRITEPROTECT pagefault observed");
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.min_kver = "5.7",
+	.needs_kconfigs = (const char *[]) {
+		"CONFIG_HAVE_ARCH_USERFAULTFD_WP=y",
+		NULL
+	},
+	.forks_child = 1,
+};
diff --git a/testcases/kernel/syscalls/userfaultfd/userfaultfd12.c b/testcases/kernel/syscalls/userfaultfd/userfaultfd12.c
new file mode 100644
index 000000000..3fd4e3652
--- /dev/null
+++ b/testcases/kernel/syscalls/userfaultfd/userfaultfd12.c
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 SUSE LLC
+ * Author: Ricardo Branco <rbranco@suse.com>
+ */
+
+/*\
+ * Force a pagefault event and handle it using :manpage:`userfaultfd(2)`
+ * from a different process (using clone with CLONE_VM instead of threads
+ * for Valgrind compatibility) testing UFFDIO_POISON.
+ */
+
+#include "config.h"
+#include <poll.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <unistd.h>
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "tst_clone.h"
+#include "lapi/sched.h"
+#include "lapi/userfaultfd.h"
+
+#define CHILD_STACK_SIZE (1024 * 1024)
+
+static int page_size;
+static char *page;
+static int uffd;
+static int poison_fault_seen;
+static volatile int sigbus_seen;
+static sigjmp_buf jmpbuf;
+static void *child_stack;
+
+static void sigbus_handler(int sig)
+{
+	if (sig == SIGBUS) {
+		sigbus_seen = 1;
+		siglongjmp(jmpbuf, 1);
+	}
+}
+
+static void setup(void)
+{
+	struct sigaction sa = {};
+
+	sa.sa_handler = sigbus_handler;
+	sigemptyset(&sa.sa_mask);
+	SAFE_SIGACTION(SIGBUS, &sa, NULL);
+}
+
+static void set_pages(void)
+{
+	page_size = sysconf(_SC_PAGE_SIZE);
+	page = SAFE_MMAP(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+}
+
+static void reset_pages(void)
+{
+	if (page) {
+		SAFE_MUNMAP(page, page_size);
+		page = NULL;
+	}
+}
+
+static int pagefault_handler(void *arg LTP_ATTRIBUTE_UNUSED)
+{
+	struct uffd_msg msg;
+	struct uffdio_poison uffdio_poison = {};
+	struct pollfd pollfd;
+	int nready;
+
+	pollfd.fd = uffd;
+	pollfd.events = POLLIN;
+	nready = poll(&pollfd, 1, -1);
+	if (nready == -1)
+		tst_brk(TBROK | TERRNO, "Error on poll");
+
+	SAFE_READ(1, uffd, &msg, sizeof(msg));
+
+	if (msg.event != UFFD_EVENT_PAGEFAULT)
+		tst_brk(TFAIL, "Received unexpected UFFD_EVENT %d", msg.event);
+
+	tst_atomic_store(1, &poison_fault_seen);
+
+	/* Poison the page that triggered the fault */
+	uffdio_poison.range.start = msg.arg.pagefault.address & ~(page_size - 1);
+	uffdio_poison.range.len = page_size;
+
+	SAFE_IOCTL(uffd, UFFDIO_POISON, &uffdio_poison);
+
+	close(uffd);
+	return 0;
+}
+
+static void run(void)
+{
+	pid_t pid;
+	struct uffdio_api uffdio_api = {};
+	struct uffdio_register uffdio_register;
+	char dummy;
+
+	poison_fault_seen = 0;
+	sigbus_seen = 0;
+	set_pages();
+
+	uffd = SAFE_USERFAULTFD(O_CLOEXEC | O_NONBLOCK, false);
+
+	uffdio_api.api = UFFD_API;
+	uffdio_api.features = UFFD_FEATURE_POISON;
+
+	SAFE_IOCTL(uffd, UFFDIO_API, &uffdio_api);
+
+	if (!(uffdio_api.features & UFFD_FEATURE_POISON))
+		tst_brk(TCONF, "UFFD_FEATURE_POISON not supported");
+
+	uffdio_register.range.start = (unsigned long) page;
+	uffdio_register.range.len = page_size;
+	uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
+
+	SAFE_IOCTL(uffd, UFFDIO_REGISTER, &uffdio_register);
+
+	pid = ltp_clone(CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | SIGCHLD,
+			pagefault_handler, NULL, CHILD_STACK_SIZE, child_stack);
+	if (pid == -1)
+		tst_brk(TBROK | TERRNO, "ltp_clone failed");
+
+	/* Try to read from the page: should trigger fault, get poisoned, then SIGBUS */
+	if (sigsetjmp(jmpbuf, 1) == 0) {
+		LTP_VAR_USED(dummy) = page[0];
+	}
+
+	SAFE_WAITPID(pid, NULL, 0);
+	reset_pages();
+
+	int poisoned = tst_atomic_load(&poison_fault_seen);
+
+	if (poisoned && sigbus_seen)
+		tst_res(TPASS, "POISON successfully triggered SIGBUS");
+	else if (poisoned && !sigbus_seen)
+		tst_res(TFAIL, "POISON fault seen but no SIGBUS received");
+	else if (!poisoned && sigbus_seen)
+		tst_res(TFAIL, "SIGBUS received but no poison fault seen");
+	else
+		tst_res(TFAIL, "No poison fault or SIGBUS observed");
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.setup = setup,
+	.cleanup = reset_pages,
+	.forks_child = 1,
+	.bufs = (struct tst_buffers []) {
+		{&child_stack, .size = CHILD_STACK_SIZE},
+		{},
+	},
+};
-- 
2.53.0



More information about the ltp mailing list