[PATCH] selftests/futex: add FUTEX_LOCK_PI owner-exiting coverage

Valery Borovsky posted 1 patch 8 hours ago
.../selftests/futex/functional/.gitignore     |   1 +
.../selftests/futex/functional/Makefile       |   3 +-
.../futex/functional/futex_lock_pi_exiting.c  | 265 ++++++++++++++++++
.../testing/selftests/futex/functional/run.sh |   3 +
4 files changed, 271 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/futex/functional/futex_lock_pi_exiting.c
[PATCH] selftests/futex: add FUTEX_LOCK_PI owner-exiting coverage
Posted by Valery Borovsky 8 hours ago
The futex functional tests cover FUTEX_LOCK_PI timeout semantics
(futex_wait_timeout.c) and robust-list owner death (robust_list.c), but
nothing exercises a non-robust PI owner exiting while holding the lock,
nor the basic ownership / EDEADLK / unlock word semantics of
FUTEX_LOCK_PI.

Add futex_lock_pi_exiting.c with three tests:

 - lock_unlock_basic: an uncontended FUTEX_LOCK_PI puts the owner TID in
   the futex word, a recursive lock by the owner returns EDEADLK, and
   FUTEX_UNLOCK_PI clears the word.

 - owner_dies_with_blocked_waiter: a thread acquires a PI futex and exits
   while holding it; a blocked FUTEX_LOCK_PI waiter must come out cleanly
   (0, EOWNERDEAD or ESRCH) and own the lock when it acquires.

 - stress_owner_exits: repeatedly drive the same exiting-owner path.

The exiting-owner retry path is where commit 210d36d892de ("futex: Clear
stale exiting pointer in futex_lock_pi() retry path") fixed a stale task
pointer that tripped WARN_ON_ONCE() in wait_for_owner_exiting(). That
warning does not change the syscall return value, so this test does not
assert on it directly; the stress loop exists to give a kernel booted
with panic_on_warn=1 (as fuzzers and CI commonly run) a chance to trip
on it.

Signed-off-by: Valery Borovsky <vebohr@gmail.com>
---
 .../selftests/futex/functional/.gitignore     |   1 +
 .../selftests/futex/functional/Makefile       |   3 +-
 .../futex/functional/futex_lock_pi_exiting.c  | 265 ++++++++++++++++++
 .../testing/selftests/futex/functional/run.sh |   3 +
 4 files changed, 271 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/futex/functional/futex_lock_pi_exiting.c

diff --git a/tools/testing/selftests/futex/functional/.gitignore b/tools/testing/selftests/futex/functional/.gitignore
index 23b9fea8d190..7c39d10b38e4 100644
--- a/tools/testing/selftests/futex/functional/.gitignore
+++ b/tools/testing/selftests/futex/functional/.gitignore
@@ -1,4 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0-only
+futex_lock_pi_exiting
 futex_numa_mpol
 futex_priv_hash
 futex_requeue
diff --git a/tools/testing/selftests/futex/functional/Makefile b/tools/testing/selftests/futex/functional/Makefile
index 5c1c824f9740..d59faf76a2aa 100644
--- a/tools/testing/selftests/futex/functional/Makefile
+++ b/tools/testing/selftests/futex/functional/Makefile
@@ -26,7 +26,8 @@ TEST_GEN_PROGS := \
 	futex_numa_mpol \
 	futex_waitv \
 	futex_numa \
-	robust_list
+	robust_list \
+	futex_lock_pi_exiting
 
 TEST_PROGS := run.sh
 
diff --git a/tools/testing/selftests/futex/functional/futex_lock_pi_exiting.c b/tools/testing/selftests/futex/functional/futex_lock_pi_exiting.c
new file mode 100644
index 000000000000..eb17c049aedd
--- /dev/null
+++ b/tools/testing/selftests/futex/functional/futex_lock_pi_exiting.c
@@ -0,0 +1,265 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/******************************************************************************
+ *
+ * futex_lock_pi_exiting.c
+ *
+ * Coverage for the FUTEX_LOCK_PI owner-exiting path.  futex_wait_timeout.c
+ * already covers FUTEX_LOCK_PI timeout semantics and robust_list.c covers
+ * owner death via the robust list, but nothing exercises FUTEX_LOCK_PI when a
+ * non-robust PI owner exits while holding the lock, nor the basic ownership /
+ * EDEADLK / unlock word semantics.
+ *
+ * DESCRIPTION
+ *      Three tests:
+ *
+ *      1. lock_unlock_basic - uncontended FUTEX_LOCK_PI semantics: the futex
+ *         word carries the owner TID, a recursive lock by the owner returns
+ *         EDEADLK, and FUTEX_UNLOCK_PI clears the word.
+ *
+ *      2. owner_dies_with_blocked_waiter - a thread acquires a PI futex and
+ *         exits while holding it.  do_exit() runs futex_cleanup_begin() (which
+ *         flips the task's futex state to FUTEX_STATE_EXITING) and
+ *         exit_pi_state_list() (which hands off / tears down the pi_state).  A
+ *         contending FUTEX_LOCK_PI waiter must end up in one of:
+ *
+ *           0          - ownership was transferred to / acquired by the waiter
+ *           EOWNERDEAD - previous owner died holding the lock; the caller is
+ *                        now the owner and must acknowledge by unlocking
+ *           ESRCH      - the owner encoded in the futex word is already gone
+ *
+ *         and on the first two it must actually own the lock afterwards.
+ *
+ *      3. stress_owner_exits - hammer that same exiting-owner path.  This is
+ *         where the following bug lived: the 'exiting' task pointer was not
+ *         reset at the retry label, so after wait_for_owner_exiting() dropped
+ *         its reference a subsequent retry that returned a non-EBUSY error fed
+ *         the stale pointer back in and tripped WARN_ON_ONCE(exiting).  That
+ *         warning is invisible to user space, so this test cannot observe it
+ *         through a syscall return value; it only becomes a visible failure
+ *         (crash) on a kernel booted with panic_on_warn=1 (or built with
+ *         CONFIG_BUG_ON_DATA_CORRUPTION).  The loop drives the path so that
+ *         such a kernel trips on it - the canonical way fuzz/CI catch these.
+ *
+ *        Fix:    210d36d892de ("futex: Clear stale exiting pointer in
+ *                              futex_lock_pi() retry path")
+ *        Fixes:  3ef240eaff36 ("futex: Prevent exit livelock")
+ *
+ * AUTHOR
+ *      Based on futex test boilerplate by Darren Hart <dvhart@linux.intel.com>
+ *
+ *****************************************************************************/
+
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/syscall.h>
+
+#include "futextest.h"
+#include "kselftest_harness.h"
+
+/*
+ * Iterations for the stress variant.  Enough to repeatedly land in the narrow
+ * EXITING window while keeping the test fast.
+ */
+#define STRESS_ITERS 1000
+
+static futex_t pi_futex;
+static pthread_barrier_t locked_barrier;
+static pthread_barrier_t release_barrier;
+
+static pid_t sys_gettid(void)
+{
+	return syscall(SYS_gettid);
+}
+
+/*
+ * Owner thread: acquire the PI futex and exit while still holding it.  Two
+ * modes:
+ *   park == 0: signal that we hold the lock, then exit immediately (racy; the
+ *              waiter races against our exit path).
+ *   park == 1: signal that we hold the lock and keep holding until released
+ *              via release_barrier, so a waiter has time to contend as a real
+ *              PI waiter before we die.
+ */
+static void *owner_thread(void *arg)
+{
+	long park = (long)arg;
+
+	if (futex_lock_pi(&pi_futex, NULL, 0, FUTEX_PRIVATE_FLAG) != 0)
+		return (void *)(intptr_t)-errno;
+
+	pthread_barrier_wait(&locked_barrier);
+
+	if (park)
+		pthread_barrier_wait(&release_barrier);
+
+	/* Die while still holding the lock. */
+	pthread_exit((void *)0);
+}
+
+/*
+ * Block on the PI futex as a waiter.  Returns 0 on acquisition, otherwise the
+ * positive errno.
+ */
+static int waiter_lock_pi(void)
+{
+	int ret;
+
+	errno = 0;
+	ret = futex_lock_pi(&pi_futex, NULL, 0, FUTEX_PRIVATE_FLAG);
+	return ret == 0 ? 0 : errno;
+}
+
+static int outcome_ok(int outcome)
+{
+	return outcome == 0 || outcome == EOWNERDEAD || outcome == ESRCH;
+}
+
+/* Results published by waiter_thread() for the owning thread to assert on. */
+static int waiter_outcome;
+static int waiter_owns;
+
+/*
+ * Waiter thread for the blocked-waiter test.  Contends for the lock and, when
+ * it acquires, records whether the futex word actually carries its TID and
+ * releases the lock itself (FUTEX_UNLOCK_PI must run in the owning thread).
+ */
+static void *waiter_thread(void *arg)
+{
+	pid_t tid = sys_gettid();
+
+	(void)arg;
+	waiter_outcome = waiter_lock_pi();
+	if (waiter_outcome == 0 || waiter_outcome == EOWNERDEAD) {
+		waiter_owns = (pi_futex & FUTEX_TID_MASK) == (futex_t)tid;
+		futex_unlock_pi(&pi_futex, FUTEX_PRIVATE_FLAG);
+	}
+	return NULL;
+}
+
+FIXTURE(lock_pi_exiting) {
+};
+
+FIXTURE_SETUP(lock_pi_exiting) {
+}
+
+FIXTURE_TEARDOWN(lock_pi_exiting) {
+}
+
+/*
+ * Uncontended FUTEX_LOCK_PI semantics, fully deterministic.
+ */
+TEST_F(lock_pi_exiting, lock_unlock_basic)
+{
+	pid_t tid = sys_gettid();
+	int ret;
+
+	pi_futex = FUTEX_INITIALIZER;
+
+	/* Acquire: we become the owner, our TID lands in the futex word. */
+	ret = futex_lock_pi(&pi_futex, NULL, 0, FUTEX_PRIVATE_FLAG);
+	ASSERT_EQ(ret, 0) TH_LOG("lock failed: errno=%d (%s)",
+				 errno, strerror(errno));
+	ASSERT_EQ(pi_futex & FUTEX_TID_MASK, (futex_t)tid)
+		TH_LOG("owner TID not in futex word: 0x%08x", pi_futex);
+
+	/* A recursive lock by the owner must be refused, not deadlock. */
+	errno = 0;
+	ret = futex_lock_pi(&pi_futex, NULL, 0, FUTEX_PRIVATE_FLAG);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EDEADLK)
+		TH_LOG("recursive lock: expected EDEADLK, got errno=%d", errno);
+
+	/* Release: the futex word is handed back clean. */
+	ret = futex_unlock_pi(&pi_futex, FUTEX_PRIVATE_FLAG);
+	ASSERT_EQ(ret, 0) TH_LOG("unlock failed: errno=%d", errno);
+	ASSERT_EQ(pi_futex, (futex_t)0)
+		TH_LOG("futex word not cleared after unlock: 0x%08x", pi_futex);
+}
+
+/*
+ * A PI waiter inherits the lock when the owner dies holding it.
+ *
+ * The owner parks while holding the lock, this thread contends for it, then
+ * the owner exits.  The waiter must come out cleanly (no hang, no unexpected
+ * error) and, when it acquires, must actually own the lock.
+ */
+TEST_F(lock_pi_exiting, owner_dies_with_blocked_waiter)
+{
+	pthread_t owner, waiter;
+
+	pthread_barrier_init(&locked_barrier, NULL, 2);
+	pthread_barrier_init(&release_barrier, NULL, 2);
+	pi_futex = FUTEX_INITIALIZER;
+	waiter_outcome = -1;
+	waiter_owns = 0;
+
+	ASSERT_EQ(pthread_create(&owner, NULL, owner_thread, (void *)1), 0);
+
+	/* Wait until the owner actually holds the lock. */
+	pthread_barrier_wait(&locked_barrier);
+
+	/* Start the waiter and give it time to block as a real PI waiter. */
+	ASSERT_EQ(pthread_create(&waiter, NULL, waiter_thread, NULL), 0);
+	usleep(1000);
+
+	/* Release the owner so it dies while the waiter is queued on it. */
+	pthread_barrier_wait(&release_barrier);
+
+	pthread_join(waiter, NULL);
+	pthread_join(owner, NULL);
+
+	ASSERT_TRUE(outcome_ok(waiter_outcome))
+		TH_LOG("unexpected FUTEX_LOCK_PI outcome: %d (%s)",
+		       waiter_outcome, strerror(waiter_outcome));
+	if (waiter_outcome == 0 || waiter_outcome == EOWNERDEAD)
+		ASSERT_TRUE(waiter_owns)
+			TH_LOG("waiter acquired but futex word lacks its TID");
+
+	pthread_barrier_destroy(&locked_barrier);
+	pthread_barrier_destroy(&release_barrier);
+}
+
+/*
+ * Stress: repeatedly let an owner exit while a waiter contends for the lock.
+ *
+ * Each iteration drives the FUTEX_STATE_EXITING -> -EBUSY -> retry path that
+ * the stale-'exiting'-pointer bug lived on (210d36d892de).  The warning it
+ * fixed is invisible to user space, so on a normally-configured kernel both
+ * the buggy and fixed kernels pass here; the point is to make a kernel booted
+ * with panic_on_warn=1 trip during one of these iterations.
+ */
+TEST_F(lock_pi_exiting, stress_owner_exits)
+{
+	int i;
+
+	for (i = 0; i < STRESS_ITERS; i++) {
+		pthread_t owner;
+		int outcome;
+
+		pthread_barrier_init(&locked_barrier, NULL, 2);
+		pi_futex = FUTEX_INITIALIZER;
+
+		ASSERT_EQ(pthread_create(&owner, NULL, owner_thread,
+					 (void *)0), 0);
+
+		/* Owner holds the lock; race FUTEX_LOCK_PI against its exit. */
+		pthread_barrier_wait(&locked_barrier);
+
+		outcome = waiter_lock_pi();
+		ASSERT_TRUE(outcome_ok(outcome))
+			TH_LOG("iter %d: unexpected outcome %d (%s)",
+			       i, outcome, strerror(outcome));
+		if (outcome == 0 || outcome == EOWNERDEAD)
+			futex_unlock_pi(&pi_futex, FUTEX_PRIVATE_FLAG);
+
+		pthread_join(owner, NULL);
+		pthread_barrier_destroy(&locked_barrier);
+	}
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/futex/functional/run.sh b/tools/testing/selftests/futex/functional/run.sh
index e88545c06d57..d1a681b798bd 100755
--- a/tools/testing/selftests/futex/functional/run.sh
+++ b/tools/testing/selftests/futex/functional/run.sh
@@ -51,3 +51,6 @@ echo
 
 echo
 ./futex_numa_mpol
+
+echo
+./futex_lock_pi_exiting
-- 
2.53.0