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 <[email protected]> --- .../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

