// 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 * *****************************************************************************/ #define _GNU_SOURCE #include #include #include #include #include #include #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 = 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(); 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) { for (int 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