selftests/futex: Add FUTEX_LOCK_PI owner-exiting coverage

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 210d36d892 ("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>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Link: https://patch.msgid.link/20260607183249.246037-1-vebohr@gmail.com
This commit is contained in:
Valery Borovsky
2026-07-05 21:49:18 +02:00
committed by Thomas Gleixner
parent 553bd67a90
commit 90a0286c7d
4 changed files with 269 additions and 1 deletions
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0-only
futex_lock_pi_exiting
futex_numa_mpol
futex_priv_hash
futex_requeue
@@ -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
@@ -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 <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 = 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
@@ -51,3 +51,6 @@ echo
echo
./futex_numa_mpol
echo
./futex_lock_pi_exiting