[PATCH] selftests/futex: Add test for robust exit with a foreign futex owner

Zhan Xusheng posted 1 patch 1 month, 3 weeks ago
.../selftests/futex/functional/robust_list.c  | 79 +++++++++++++++++++
1 file changed, 79 insertions(+)
[PATCH] selftests/futex: Add test for robust exit with a foreign futex owner
Posted by Zhan Xusheng 1 month, 3 weeks ago
commit 6d4514ca9cdf ("futex: Prevent robust futex exit race some more")
fixed a lost wakeup in handle_futex_death(): when a robust waiter dies
mid-lock (list_op_pending set) while the futex is owned by a third thread
with FUTEX_WAITERS clear, the kernel must still wake the next waiter,
otherwise a waiter parked behind that futex sleeps forever. Since the
dying thread is not the owner, FUTEX_OWNER_DIED must not be set either.

Add a regression test to robust_list.c reproducing this "state B"
sequence: a waiter parks on a futex whose word is a foreign TID without
FUTEX_WAITERS, and a robust thread then dies with that futex in its
list_op_pending. It reuses the existing robust-list scaffolding.

Without the fix the wakeup is lost and futex_wait() times out at
FUTEX_TIMEOUT; with it handle_futex_death() issues the wakeup. Checked
both ways: the test fails on a kernel without the fix and passes on one
with it.

Signed-off-by: Zhan Xusheng <zhanxusheng@xiaomi.com>
---
 .../selftests/futex/functional/robust_list.c  | 79 +++++++++++++++++++
 1 file changed, 79 insertions(+)

diff --git a/tools/testing/selftests/futex/functional/robust_list.c b/tools/testing/selftests/futex/functional/robust_list.c
index b3fab60181d5..be8149cc67f8 100644
--- a/tools/testing/selftests/futex/functional/robust_list.c
+++ b/tools/testing/selftests/futex/functional/robust_list.c
@@ -434,6 +434,85 @@ TEST(test_set_list_op_pending)
 		ksft_test_result_fail("%s\n", __func__);
 }
 
+/*
+ * State B reproducer for commit 6d4514ca9cdf ("futex: Prevent robust futex
+ * exit race some more").
+ *
+ * Model the sequence where the original owner unlocked and woke us, a third
+ * thread then took the futex uncontended (so the futex word holds a foreign
+ * TID with FUTEX_WAITERS clear), and we die mid-lock before acquiring it or
+ * re-arming FUTEX_WAITERS. handle_futex_death() must still wake the next
+ * waiter, otherwise a waiter parked behind a foreign-owned futex whose
+ * FUTEX_WAITERS bit was lost sleeps forever. As the dying thread is not the
+ * owner, it must not set FUTEX_OWNER_DIED either.
+ */
+#define FOREIGN_TID 0x00c0ffee
+
+static int child_fn_foreign_owner(void *arg)
+{
+	struct lock_struct *lock = arg;
+	struct robust_list_head head;
+
+	if (set_list(&head)) {
+		ksft_test_result_fail("set_robust_list error\n");
+		return -1;
+	}
+	if ((unsigned int)gettid() == FOREIGN_TID) {
+		ksft_test_result_fail("TID collided with FOREIGN_TID\n");
+		return -1;
+	}
+
+	/* We are mid-lock ... */
+	head.list_op_pending = &lock->list;
+	/* ... and the futex is owned by a third thread, FUTEX_WAITERS clear. */
+	atomic_store(&lock->futex, FOREIGN_TID);
+
+	/* Let the parent enter futex_wait() before we die (see child_fn_lock). */
+	pthread_barrier_wait(&barrier);
+	usleep(SLEEP_US);
+
+	return 0;
+}
+
+/*
+ * A waiter is parked on a futex whose word is a foreign TID with no
+ * FUTEX_WAITERS bit (state B). A robust thread dies with that futex in its
+ * list_op_pending. The kernel must wake the parked waiter even though the
+ * dying thread does not own the futex, and it must not set FUTEX_OWNER_DIED.
+ */
+TEST(test_robust_death_foreign_owner_wakeup)
+{
+	struct lock_struct lock = { .futex = 0 };
+	struct timespec to = { .tv_sec = FUTEX_TIMEOUT, .tv_nsec = 0 };
+	int ret, pid, wstatus;
+
+	ret = pthread_barrier_init(&barrier, NULL, 2);
+	ASSERT_EQ(ret, 0);
+
+	pid = create_child(&child_fn_foreign_owner, &lock);
+	ASSERT_NE(pid, -1);
+
+	pthread_barrier_wait(&barrier);
+
+	/*
+	 * *futex == FOREIGN_TID here, so this blocks. With the fix the dying
+	 * child's robust walk wakes us; without it, futex_wait() times out.
+	 */
+	ret = futex_wait((futex_t *)&lock.futex, FOREIGN_TID, &to, 0);
+	ASSERT_EQ(ret, 0);
+
+	/* The dying thread was not the owner: OWNER_DIED must not be set. */
+	ASSERT_FALSE(lock.futex & FUTEX_OWNER_DIED);
+
+	wait(&wstatus);
+	pthread_barrier_destroy(&barrier);
+
+	if (!WEXITSTATUS(wstatus))
+		ksft_test_result_pass("%s\n", __func__);
+	else
+		ksft_test_result_fail("%s\n", __func__);
+}
+
 #define CHILD_NR 10
 
 static int child_lock_holder(void *arg)
-- 
2.43.0