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..623c5f3a1836 --- /dev/null +++ b/tools/testing/selftests/futex/functional/futex_lock_pi_exiting.c @@ -0,0 +1,263 @@ +// 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 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