[PATCH 15/14] selftests: futex: Add tests for robust unlock within the critical section.

Sebastian Andrzej Siewior
2026-04-04T09:39:43Z

From: Sebastian Andrzej Siewior <sebastian@breakpoint.cc>

I took Thomas’ initial test case from the cover letter and reworked it
so that it uses ptrace() to single‑step through the VDSO unlock
operation. The test expects the lock to remain locked, with
`list_op_pending' pointing somewhere, when entering the VDSO unlock
path. Once execution steps into the critical section, it expects the
kernel to perform the fixup that is, to unlock the lock and clear
`list_op_pending'.

The test requires VDSO debug symbols, typically provided by
vdso64.so.dbg or vdso32.so.dbg. It attempts to locate the appropriate
file automatically, but the user may override this by setting the
VDSO_DBG environment variable. If neither method succeeds, libelf falls
back to its usual lookup mechanism under /usr/lib/debug/.build-id/

Signed-off-by: Sebastian Andrzej Siewior <sebastian@breakpoint.cc>
---
.../selftests/futex/functional/Makefile | 3 +-
.../futex/functional/robust_list_critical.c | 402 ++++++++++++++++++
2 files changed, 404 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/futex/functional/robust_list_critical.c

diff --git a/tools/testing/selftests/futex/functional/Makefile b/tools/testing/selftests/futex/functional/Makefile
index af7ec309ea78d..40d7256477a40 100644
--- a/tools/testing/selftests/futex/functional/Makefile
+++ b/tools/testing/selftests/futex/functional/Makefile
@@ -4,7 +4,7 @@ LIBNUMA_TEST = $(shell sh -c "$(PKG_CONFIG) numa --atleast-version 2.0.16 > /dev

INCLUDES := -I../include -I../../ $(KHDR_INCLUDES)
CFLAGS := $(CFLAGS) -g -O2 -Wall -pthread -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 $(INCLUDES) $(KHDR_INCLUDES) -DLIBNUMA_VER_$(LIBNUMA_TEST)=1
-LDLIBS := -lpthread -lrt -lnuma
+LDLIBS := -lpthread -lrt -lnuma -ldw

LOCAL_HDRS := \
../include/futextest.h \
@@ -23,6 +23,7 @@ TEST_GEN_PROGS := \
futex_numa_mpol \
futex_waitv \
futex_numa \
+ robust_list_critical \
robust_list

TEST_PROGS := run.sh
diff --git a/tools/testing/selftests/futex/functional/robust_list_critical.c b/tools/testing/selftests/futex/functional/robust_list_critical.c
new file mode 100644
index 0000000000000..b9490d24eb10a
--- /dev/null
+++ b/tools/testing/selftests/futex/functional/robust_list_critical.c
@@ -0,0 +1,402 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2026, Sebastian Andrzej Siewior <sebastian@breakpoint.cc>
+ *
+ * Testing the VDSO robust‑list unlock critical section. Specifically, when the
+ * kernel detects that user space attempts to unlock a robust lock and is
+ * preempted inside the VDSO critical section, the kernel must complete the
+ * unlock operation and reset the `list_op_pending' field.
+ */
+
+#define _GNU_SOURCE
+#include <err.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+
+#include <linux/futex.h>
+#include <linux/prctl.h>
+
+#include <link.h>
+#include <elfutils/libdwfl.h>
+
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/user.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+
+static char *vdso_dbg_file =
+#ifdef __LP64__
+ "vdso64.so.dbg";
+#else
+ "vdso32.so.dbg";
+#endif
+
+typedef uint32_t (*frtu64_t)(uint32_t *, uint32_t, uint64_t *);
+typedef uint32_t (*frtu32_t)(uint32_t *, uint32_t, uint32_t *);
+
+static frtu64_t frtu64;
+static frtu32_t frtu32;
+static uint64_t frtu64_end;
+static uint64_t frtu32_end;
+
+static uint64_t __futex_list64_try_unlock_cs_start;
+static uint64_t __futex_list64_try_unlock_cs_end;
+static uint64_t __futex_list64_try_unlock_cs_success;
+static uint64_t __futex_list32_try_unlock_cs_start;
+static uint64_t __futex_list32_try_unlock_cs_end;
+static uint64_t __futex_list32_try_unlock_cs_success;
+
+static bool pc_is_within(struct user_regs_struct *regs, uint64_t start, uint64_t end)
+{
+ unsigned long pc;
+
+#if defined(__x86_64__)
+ pc = regs->rip;
+#elif defined(__riscv)
+ pc = reg->pc;
+# error Missing ptrace support
+#endif
+ if (pc >= (long) start && pc < end)
+ return true;
+ return false;
+}
+
+static int is_vdso_mod(const char *mod)
+{
+ return !strncmp(mod, "[vdso: ", 7);
+}
+
+static int module_callback(Dwfl_Module *mod, void **userdata, const char *name,
+ Dwarf_Addr start, void *arg)
+{
+ const char *sname;
+ GElf_Addr addr;
+ int vdso_syms;
+ GElf_Sym sym;
+ int i;
+
+ /* We can only recognize the vdso by inspecting the "magic name". */
+ if (!is_vdso_mod(name))
+ return DWARF_CB_OK;
+
+ vdso_syms = dwfl_module_getsymtab(mod);
+ if (vdso_syms < 0)
+ err(1, "dwfl_module_getsymtab: %s", dwfl_errmsg (-1));
+
+ for (i = 0; i < vdso_syms; i++) {
+
+ sname = dwfl_module_getsym_info(mod, i, &sym, &addr, NULL, NULL, NULL);
+ if (!strcmp("__vdso_futex_robust_list64_try_unlock", sname)) {
+ frtu64 = (frtu64_t) addr;
+ frtu64_end = addr + sym.st_size;
+ } else if (!strcmp("__futex_list64_try_unlock_cs_start", sname)) {
+ __futex_list64_try_unlock_cs_start = addr;
+ } else if (!strcmp("__futex_list64_try_unlock_cs_end", sname)) {
+ __futex_list64_try_unlock_cs_end = addr;
+ } else if (!strcmp("__futex_list64_try_unlock_cs_success", sname)) {
+ __futex_list64_try_unlock_cs_success = addr;
+
+ } else if (!strcmp("__vdso_futex_robust_list32_try_unlock", sname)) {
+ frtu32 = (frtu32_t) addr;
+ frtu32_end = addr + sym.st_size;
+ } else if (!strcmp("__futex_list32_try_unlock_cs_start", sname)) {
+ __futex_list32_try_unlock_cs_start = addr;
+ } else if (!strcmp("__futex_list32_try_unlock_cs_end", sname)) {
+ __futex_list32_try_unlock_cs_end = addr;
+ } else if (!strcmp("__futex_list32_try_unlock_cs_success", sname)) {
+ __futex_list32_try_unlock_cs_success = addr;
+ }
+
+ }
+ return DWARF_CB_OK;
+}
+
+static int find_dbg_local_def(Dwfl_Module *mod, void **userdata, const char *modname,
+ GElf_Addr base, const char *file_name,
+ const char *debuglink_file, GElf_Word debuglink_crc,
+ char **debuginfo_file_name)
+{
+ int fd;
+
+ if (is_vdso_mod(modname)) {
+ fd = open(vdso_dbg_file, O_RDONLY);
+ if (fd >= 0)
+ return fd;
+ }
+
+ return dwfl_standard_find_debuginfo(mod, userdata, modname, base, file_name,
+ debuglink_file, debuglink_crc, debuginfo_file_name);
+}
+
+static void get_symbols(void)
+{
+ char *debuginfo_path;
+ const Dwfl_Callbacks proc_callbacks = {
+ .find_debuginfo = find_dbg_local_def,
+ .debuginfo_path = &debuginfo_path,
+ .find_elf = dwfl_linux_proc_find_elf,
+ };
+ int result;
+ pid_t pid;
+ Dwfl *dwfl;
+
+ dwfl = dwfl_begin(&proc_callbacks);
+ if (!dwfl)
+ errx(1, "dwfl_begin: %s", dwfl_errmsg(-1));
+
+ pid = getpid();
+ result = dwfl_linux_proc_report(dwfl, pid);
+ if (result < 0)
+ errx(1, "dwfl_linux_proc_report: %s", dwfl_errmsg(-1));
+ else if (result > 0)
+ errx(1, "dwfl_linux_proc_report");
+
+ result = dwfl_linux_proc_attach(dwfl, pid, false);
+ if (result < 0)
+ err (1, "dwfl_linux_proc_attach: %s", dwfl_errmsg(-1));
+ else if (result > 0)
+ err (1, "dwfl_linux_proc_attach");
+
+ if (dwfl_report_end(dwfl, NULL, NULL) != 0)
+ err (1, "dwfl_report_end: %s", dwfl_errmsg(-1));
+
+ if (dwfl_getmodules(dwfl, module_callback, NULL, 0) != 0)
+ err (1, "dwfl_getmodules: %s", dwfl_errmsg(-1));
+ dwfl_end(dwfl);
+}
+
+static pthread_mutex_t test_mutex;
+static int unlock_uncontended(struct __test_metadata *_metadata, bool is_32bit)
+{
+ struct robust_list_head *rhead;
+ uint32_t *lock;
+ pid_t lock_tid;
+ int error = 1;
+ pid_t tid;
+ size_t sz;
+
+ syscall(SYS_get_robust_list, 0, &rhead, &sz);
+ pthread_mutex_init(&test_mutex, NULL);
+
+ tid = gettid();
+ lock = (uint32_t *)&test_mutex.__data.__lock;
+ *lock = tid;
+ /* Set the pending prior unlocking */
+ rhead->list_op_pending = (struct robust_list *)&test_mutex.__data.__list.__next;
+
+ raise(SIGTRAP);
+ if (is_32bit)
+ lock_tid = frtu32(lock, tid, (uint32_t *)&rhead->list_op_pending);
+ else
+ lock_tid = frtu64(lock, tid, (uint64_t *)&rhead->list_op_pending);
+
+ if (lock_tid != tid)
+ TH_LOG("Non contended unlock failed. Return: %08x expected %08x\n",
+ lock_tid, tid);
+ else
+ error = 0;
+ return error;
+}
+
+enum trace_state {
+ STATE_WAIT = 0,
+ STATE_ENTER_VDSO,
+ STATE_IN_CS,
+ STATE_OUT_CS,
+ STATE_LEAVE_VDSO,
+};
+
+static void trace_child(struct __test_metadata *_metadata, pid_t child, bool is_32bit)
+{
+ int state = STATE_WAIT;
+ struct robust_list_head *rhead;
+ size_t sz;
+ bool do_end = false;
+
+ syscall(SYS_get_robust_list, 0, &rhead, &sz);
+ do {
+ struct user_regs_struct regs;
+ uint64_t lock_val;
+ bool in_vdso;
+ int wstatus;
+ pid_t rpid;
+
+ rpid = waitpid(child, &wstatus, 0);
+ if (rpid != child)
+ errx(1, "waitpid");
+ if (!do_end) {
+ if (WSTOPSIG(wstatus) != SIGTRAP)
+ errx(1, "NOT SIGTRAP");
+ } else {
+ if (!WIFEXITED(wstatus))
+ errx(1, "Did not exit, but we are done");
+ ASSERT_EQ(WEXITSTATUS(wstatus), 0);
+ return;
+ }
+
+ if (ptrace(PTRACE_GETREGS, child, 0, &regs) != 0)
+ errx(1, "PTRACE_GETREGS");
+
+ if (is_32bit) {
+ in_vdso = pc_is_within(&regs, (long)frtu32, frtu32_end);
+ } else {
+ in_vdso = pc_is_within(&regs, (long)frtu64, frtu64_end);
+ }
+ if (in_vdso) {
+ unsigned long rhead_val;
+
+ if (state == STATE_WAIT) {
+ state = STATE_ENTER_VDSO;
+
+ } else {
+ if (is_32bit) {
+ if (pc_is_within(&regs, __futex_list32_try_unlock_cs_start,
+ __futex_list32_try_unlock_cs_end))
+ state = STATE_IN_CS;
+ else
+ state = STATE_OUT_CS;
+ } else {
+ if (pc_is_within(&regs, __futex_list64_try_unlock_cs_start,
+ __futex_list64_try_unlock_cs_end))
+ state = STATE_IN_CS;
+ else
+ state = STATE_OUT_CS;
+ }
+ }
+
+ errno = 0;
+ rhead_val = ptrace(PTRACE_PEEKDATA, child, &rhead->list_op_pending, 0);
+ if (rhead_val == -1 && errno != 0)
+ err(1, "PTRACE_PEEKDATA");
+
+ lock_val = ptrace(PTRACE_PEEKDATA, child, &test_mutex.__data.__lock, 0);
+ if (lock_val == -1 && errno != 0)
+ err(1, "PTRACE_PEEKDATA");
+
+ if (state == STATE_ENTER_VDSO) {
+ /* Entering VDSO, it supposed to be locked */
+ ASSERT_NE(rhead_val, 0);
+ ASSERT_EQ(lock_val, child);
+
+ } else if (state == STATE_IN_CS) {
+ /*
+ * If the critical section has been entered then
+ * the kernel has to unlock and clean list_op_pending.
+ * On 32bit the pointer is just 32bit wide, the
+ * upper 32bit are cleaned on 64bit.
+ */
+ if (is_32bit)
+ rhead_val &= 0xffffffff;
+
+ ASSERT_EQ(rhead_val, 0);
+ ASSERT_EQ(lock_val, 0);
+ }
+
+ if (ptrace(PTRACE_SINGLESTEP, child, 0, 0))
+ err(1, "PTRACE_SINGLESTEP");
+ } else {
+ int ret;
+
+ if (state == STATE_WAIT) {
+ ret = ptrace(PTRACE_SINGLESTEP, child, 0, 0);
+ } else if (state == STATE_OUT_CS) {
+ state = STATE_LEAVE_VDSO;
+ ret = ptrace(PTRACE_SINGLESTEP, child, 0, 0);
+ } else if (state == STATE_LEAVE_VDSO) {
+ state = STATE_WAIT;
+ do_end = true;
+ ret = ptrace(PTRACE_CONT, child, 0, 0);
+ }
+ if (ret != 0)
+ err(1, "ptrace continue failed");
+ }
+ } while (1);
+}
+
+TEST(robust_unlock_64)
+{
+ pid_t child;
+
+ if (!frtu64)
+ SKIP(return, "Missing __vdso_futex_robust_list64_try_unlock() in VDSO\n");
+
+ if ((long)frtu64 == frtu64_end)
+ SKIP(return, "size for __vdso_futex_robust_list64_try_unlock\n");
+
+ if (!__futex_list64_try_unlock_cs_start ||
+ !__futex_list64_try_unlock_cs_end ||
+ !__futex_list64_try_unlock_cs_success) {
+ SKIP(return, "Missing debug symbols, use VDSO_DBG=PATH/TO/vdso64.so.dbg.\n");
+ }
+
+ child = fork();
+ if (child < 0)
+ err(1, "fork()");
+
+ if (!child) {
+ if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0)
+ err(1, "PTRACE_TRACEME");
+
+ exit(unlock_uncontended(_metadata, false));
+ } else {
+ trace_child(_metadata, child, false);
+ }
+}
+
+TEST(robust_unlock_32)
+{
+ pid_t child;
+
+ if (!frtu32)
+ SKIP(return, "Missing __vdso_futex_robust_list32_try_unlock() in VDSO\n");
+
+ if ((long)frtu32 == frtu32_end)
+ SKIP(return, "size for __vdso_futex_robust_list32_try_unlock\n");
+
+ if (!__futex_list32_try_unlock_cs_start ||
+ !__futex_list32_try_unlock_cs_end ||
+ !__futex_list32_try_unlock_cs_success) {
+ SKIP(return, "Missing debug symbols, use VDSO_DBG=PATH/TO/vdso32.so.dbg.\n");
+ }
+
+ child = fork();
+ if (child < 0)
+ err(1, "fork()");
+
+ if (!child) {
+ if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0)
+ err(1, "PTRACE_TRACEME");
+
+ exit(unlock_uncontended(_metadata, true));
+ } else {
+ trace_child(_metadata, child, true);
+ }
+}
+
+int main(int argc, char **argv)
+{
+ char *vdso_dbg;
+
+ vdso_dbg = getenv("VDSO_DBG");
+ if (vdso_dbg) {
+ vdso_dbg_file = vdso_dbg;
+ ksft_print_msg("Using %s as VDSO debug symbols\n", vdso_dbg_file);
+ }
+
+ get_symbols();
+
+ return test_harness_run(argc, argv);
+}
--
2.53.0