futex: Sanitize and document task_struct::futex::state transitions

The futex state is used to prevent a waiter from attaching to the lock
owner while the owner runs the futex cleanup in exit() or exec().

Only the state transition from FUTEX_STATE_OK to FUTEX_STATE_EXITING must
be done with the task's pi_lock held, the transition away from
FUTEX_STATE_EXITING has no serialization requirements on the writer side,
but it's completely non obvious why. It's magically protected by
exit_pi_state(), which operates under tsk::pi_lock, as that's the state
which has to be correct when the waiter observes the new state.

OTOH, taking the pi_lock in futex_cleanup_end() is not a performance issue
because at that point the lock should be uncontended in the vast majority
of cases.

Aside of that the handling of FUTEX_STATE_EXITING in attach_to_pi_owner()
and handle_exit_race() is confusing at best.

Protect the store in futex_cleanup_end() with tsk::pi_lock, handle
FUTEX_STATE_EXITING in attach_to_pi_owner() explicitly and document how
this is supposed to work.

Reported-by: Peter Zijlstra <peterz@infradead.org>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Reviewed-by: Kyle Zeng <kylebot@openai.com>
Acked-by: Peter Zijlstra <peterz@infradead.org>
Cc: stable@vger.kernel.org
This commit is contained in:
Thomas Gleixner 2026-08-07 17:07:08 +02:00
parent 59b3732f95
commit f9ece060cc
2 changed files with 73 additions and 40 deletions

View File

@ -1527,11 +1527,9 @@ static void futex_cleanup_begin(struct task_struct *tsk)
static void futex_cleanup_end(struct task_struct *tsk, int state)
__releases(&tsk->futex.exit_mutex)
{
/*
* Lockless store. The only side effect is that an observer might
* take another loop until it becomes visible.
*/
tsk->futex.state = state;
scoped_guard(raw_spinlock_irq, &tsk->pi_lock)
tsk->futex.state = state;
/*
* Drop the exit protection. This unblocks waiters which observed
* FUTEX_STATE_EXITING to reevaluate the state.

View File

@ -193,6 +193,48 @@ void put_pi_state(struct futex_pi_state *pi_state)
* pi_mutex->wait_lock
* p->pi_lock
*
* Futex kernel state:
*
* The kernel tracks the task state in p::futex::state to protect against exit()
* and exec(). The states are:
*
* - FUTEX_STATE_OK when the task is alive and waiters can be attached
*
* - FUTEX_STATE_EXITING when the task cleans up the robust list and pi
* state. Concurrent waiters cannot attach anymore and have to wait until the
* cleanup is finished to re-evaluate the potential changes of robust list and
* pi state cleanups.
*
* - FUTEX_STATE_DEAD when the task has cleaned up the robust list and
* is about to fully exit.
*
* exec() switches back to FUTEX_STATE_OK after the cleanup.
*
* The state has two related locks:
*
* 1) p::pi_lock
*
* p::pi_lock has to be taken by the waiter when evaluating the state to
* protect against a concurrent exit/exec cleanup by the owner. If the state
* is OK then the waiter can be attached to the owner while still holding
* pi_lock.
*
* The cleanup code has to hold it for all state transitions to ensure that
* the stores to the state cannot be reordered against previous stores on
* which the waiter correctness depends on.
*
* 2) p::futex::exit_mutex
*
* The mutex is acquired when the cleanup starts and released at the end. It
* obviously is not serializing the owner's cleanup against itself. It is
* used to avoid a live lock caused by a waiter preempting the owner's
* cleanup. Such a waiter would busy loop forever waiting for the owner to
* finish the cleanup.
*
* To prevent this, waiters have to drop all locks when observing
* FUTEX_STATE_EXITING and block on the mutex. When the owner releases the
* mutex after finishing the cleanup the waiters make progress and
* re-evaluate the situation.
*/
/*
@ -318,18 +360,10 @@ static int attach_to_pi_state(u32 __user *uaddr, u32 uval,
return ret;
}
static int handle_exit_race(u32 __user *uaddr, u32 uval,
struct task_struct *tsk)
static int handle_exit_race(u32 __user *uaddr, u32 uval)
{
u32 uval2;
/*
* If the futex exit state is not yet FUTEX_STATE_DEAD, tell the
* caller that the alleged owner is busy.
*/
if (tsk && tsk->futex.state != FUTEX_STATE_DEAD)
return -EBUSY;
/*
* Reread the user space value to handle the following situation:
*
@ -427,7 +461,7 @@ static int attach_to_pi_owner(u32 __user *uaddr, u32 uval, union futex_key *key,
return -EAGAIN;
p = find_get_task_by_vpid(pid);
if (!p)
return handle_exit_race(uaddr, uval, NULL);
return handle_exit_race(uaddr, uval);
if (unlikely(p->flags & PF_KTHREAD)) {
put_task_struct(p);
@ -435,41 +469,42 @@ static int attach_to_pi_owner(u32 __user *uaddr, u32 uval, union futex_key *key,
}
/*
* We need to look at the task state to figure out, whether the
* task is exiting. To protect against the change of the task state
* in futex_exit_release(), we do this protected by p->pi_lock:
* We need to look at the task state to figure out whether the task is
* exiting. To protect against the change of the task state from
* FUTEX_STATE_OK to FUTEX_STATE_EXISTING in futex_cleanup_begin() it is
* required to do this protected by p->pi_lock, which prevents the owner
* from concurrently starting the exit cleanup.
*
* If the state is FUTEX_STATE_OK pi_lock must be held until the waiter
* is attached to protect against a concurrent exit()/exec().
*/
raw_spin_lock_irq(&p->pi_lock);
/* Validate that the task is ready for futex operations. */
if (unlikely(p->futex.state != FUTEX_STATE_OK)) {
/*
* The task is on the way out. When the futex state is
* FUTEX_STATE_DEAD, we know that the task has finished
* the cleanup:
* The task is on the way out. When state is FUTEX_STATE_EXITING
* the cleanup is in progress. To avoid a live lock when the
* waiter preempted the owner, store the task pointer in
* @exiting and keep the reference on the task. The calling code
* will drop all locks, block on @p::futex::exit_mutex and wait
* for the owner to finish the cleanup. Once the owner released
* the mutex the waiter drops the reference count and
* re-evaluates the situation.
*/
int ret = handle_exit_race(uaddr, uval, p);
if (p->futex.state == FUTEX_STATE_EXITING) {
raw_spin_unlock_irq(&p->pi_lock);
*exiting = p;
return -EBUSY;
}
int ret = handle_exit_race(uaddr, uval);
raw_spin_unlock_irq(&p->pi_lock);
/*
* If the owner task is between FUTEX_STATE_EXITING and
* FUTEX_STATE_DEAD then store the task pointer and keep
* the reference on the task struct. The calling code will
* drop all locks, wait for the task to reach
* FUTEX_STATE_DEAD and then drop the refcount. This is
* required to prevent a live lock when the current task
* preempted the exiting task between the two states.
*/
if (ret == -EBUSY)
*exiting = p;
else
put_task_struct(p);
put_task_struct(p);
return ret;
}
/*
* If the owner is about to exit() or exec() and tries to modify
* p::futex::exit_state it is serialized against this code by
* p::pi_lock.
*/
if (IS_ENABLED(CONFIG_MMU) && futex_key_is_private(key)) {
/*
* A private futex key holds a pointer to the waiter's mm