]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
futex: Add support for unlocking robust futexes
authorThomas Gleixner <tglx@kernel.org>
Tue, 2 Jun 2026 09:09:55 +0000 (11:09 +0200)
committerPeter Zijlstra <peterz@infradead.org>
Wed, 3 Jun 2026 09:38:51 +0000 (11:38 +0200)
Unlocking robust non-PI futexes happens in user space with the following
sequence:

  1) robust_list_set_op_pending(mutex);
  2) robust_list_remove(mutex);

   lval = 0;
  3) lval = atomic_xchg(lock, lval);
  4) if (lval & WAITERS)
  5) sys_futex(WAKE,....);
  6) robust_list_clear_op_pending();

That opens a window between #3 and #6 where the mutex could be acquired by
some other task which observes that it is the last user and:

  A) unmaps the mutex memory
  B) maps a different file, which ends up covering the same address

When the original task exits before reaching #6 then the kernel robust list
handling observes the pending op entry and tries to fix up user space.

In case that the newly mapped data contains the TID of the exiting thread
at the address of the mutex/futex the kernel will set the owner died bit in
that memory and therefore corrupting unrelated data.

PI futexes have a similar problem both for the non-contented user space
unlock and the in kernel unlock:

  1) robust_list_set_op_pending(mutex);
  2) robust_list_remove(mutex);

   lval = gettid();
  3) if (!atomic_try_cmpxchg(lock, lval, 0))
  4) sys_futex(UNLOCK_PI,....);
  5) robust_list_clear_op_pending();

Address the first part of the problem where the futexes have waiters and
need to enter the kernel anyway. Add a new FUTEX_ROBUST_UNLOCK flag, which
is valid for the sys_futex() FUTEX_UNLOCK_PI, FUTEX_WAKE, FUTEX_WAKE_BITSET
operations.

This deliberately omits FUTEX_WAKE_OP from this treatment as it's unclear
whether this is needed and there is no usage of it in glibc either to
investigate.

For the futex2 syscall family this needs to be implemented with a new
syscall.

The sys_futex() case [ab]uses the @uaddr2 argument to hand the pointer to
robust_list_head::list_pending_op into the kernel. This argument is only
evaluated when the FUTEX_ROBUST_UNLOCK bit is set and is therefore backward
compatible.

This is an explicit argument to avoid the lookup of the robust list pointer
and retrieving the pending op pointer from there. User space has the
pointer already available so it can just put it into the @uaddr2
argument. Aside of that this allows the usage of multiple robust lists in
the future without any changes to the internal functions as they just operate
on the provided pointer.

This requires a second flag FUTEX_ROBUST_LIST32 which indicates that the
robust list pointer points to an u32 and not to an u64. This is required
for two reasons:

    1) sys_futex() has no compat variant

    2) The gaming emulators use both both 64-bit and compat 32-bit robust
       lists in the same 64-bit application

As a consequence 32-bit applications have to set this flag unconditionally
so they can run on a 64-bit kernel in compat mode unmodified. 32-bit
kernels return an error code when the flag is not set. 64-bit kernels will
happily clear the full 64 bits if user space fails to set it.

In case of FUTEX_UNLOCK_PI this clears the robust list pending op when the
unlock succeeded. In case of errors, the user space value is still locked
by the caller and therefore the above cannot happen.

In case of FUTEX_WAKE* this does the unlock of the futex in the kernel and
clears the robust list pending op when the unlock was successful. If not,
the user space value is still locked and user space has to deal with the
returned error. That means that the unlocking of non-PI robust futexes has
to use the same try_cmpxchg() unlock scheme as PI futexes.

If the clearing of the pending list op fails (fault) then the kernel clears
the registered robust list pointer if it matches to prevent that exit()
will try to handle invalid data. That's a valid paranoid decision because
the robust list head sits usually in the TLS and if the TLS is not longer
accessible then the chance for fixing up the resulting mess is very close
to zero.

The problem of non-contended unlocks still exists and will be addressed
separately.

Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Reviewed-by: André Almeida <andrealmeid@igalia.com>
Link: https://patch.msgid.link/20260602090535.670514505@kernel.org
include/uapi/linux/futex.h
io_uring/futex.c
kernel/futex/core.c
kernel/futex/futex.h
kernel/futex/pi.c
kernel/futex/syscalls.c
kernel/futex/waitwake.c

index 75df1eaff75d630df83304f8b1db482e8425055d..10a36c551675bda5899bff5d40ce3e3c42136b06 100644 (file)
 
 #define FUTEX_PRIVATE_FLAG     128
 #define FUTEX_CLOCK_REALTIME   256
+#define FUTEX_ROBUST_UNLOCK    512
+#define FUTEX_ROBUST_LIST32    1024
 
-#define FUTEX_CMD_MASK                 ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME)
+#define FUTEX_CMD_MASK                 ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME | \
+                                         FUTEX_ROBUST_UNLOCK | FUTEX_ROBUST_LIST32)
 
 #define FUTEX_WAIT_PRIVATE             (FUTEX_WAIT | FUTEX_PRIVATE_FLAG)
 #define FUTEX_WAKE_PRIVATE             (FUTEX_WAKE | FUTEX_PRIVATE_FLAG)
 #define FUTEX_WAIT_REQUEUE_PI_PRIVATE  (FUTEX_WAIT_REQUEUE_PI | FUTEX_PRIVATE_FLAG)
 #define FUTEX_CMP_REQUEUE_PI_PRIVATE   (FUTEX_CMP_REQUEUE_PI | FUTEX_PRIVATE_FLAG)
 
+/*
+ * Operations to unlock a futex, clear the robust list pending op pointer and
+ * wake waiters.
+ */
+#define FUTEX_UNLOCK_PI_LIST64                 (FUTEX_UNLOCK_PI | FUTEX_ROBUST_UNLOCK)
+#define FUTEX_UNLOCK_PI_LIST64_PRIVATE         (FUTEX_UNLOCK_PI_LIST64 | FUTEX_PRIVATE_FLAG)
+#define FUTEX_UNLOCK_PI_LIST32                 (FUTEX_UNLOCK_PI | FUTEX_ROBUST_UNLOCK | \
+                                                FUTEX_ROBUST_LIST32)
+#define FUTEX_UNLOCK_PI_LIST32_PRIVATE         (FUTEX_UNLOCK_PI_LIST32 | FUTEX_PRIVATE_FLAG)
+
+#define FUTEX_UNLOCK_WAKE_LIST64               (FUTEX_WAKE | FUTEX_ROBUST_UNLOCK)
+#define FUTEX_UNLOCK_WAKE_LIST64_PRIVATE       (FUTEX_UNLOCK_WAKE_LIST64 | FUTEX_PRIVATE_FLAG)
+
+#define FUTEX_UNLOCK_WAKE_LIST32               (FUTEX_WAKE | FUTEX_ROBUST_UNLOCK | \
+                                                FUTEX_ROBUST_LIST32)
+#define FUTEX_UNLOCK_WAKE_LIST32_PRIVATE       (FUTEX_UNLOCK_WAKE_LIST32 | FUTEX_PRIVATE_FLAG)
+
+#define FUTEX_UNLOCK_BITSET_LIST64             (FUTEX_WAKE_BITSET | FUTEX_ROBUST_UNLOCK)
+#define FUTEX_UNLOCK_BITSET_LIST64_PRIVATE     (FUTEX_UNLOCK_BITSET_LIST64 | FUTEX_PRIVATE_FLAG)
+
+#define FUTEX_UNLOCK_BITSET_LIST32             (FUTEX_WAKE_BITSET | FUTEX_ROBUST_UNLOCK | \
+                                                FUTEX_ROBUST_LIST32)
+#define FUTEX_UNLOCK_BITSET_LIST32_PRIVATE     (FUTEX_UNLOCK_BITSET_LIST32 | FUTEX_PRIVATE_FLAG)
+
 /*
  * Flags for futex2 syscalls.
  *
index 9cc1788ef4c62bd3e979dccf5f8e7a2d0388493b..906701b3c5c66d83218c89053893628f876ba479 100644 (file)
@@ -327,7 +327,7 @@ int io_futex_wake(struct io_kiocb *req, unsigned int issue_flags)
         * Strict flags - ensure that waking 0 futexes yields a 0 result.
         * See commit 43adf8449510 ("futex: FLAGS_STRICT") for details.
         */
-       ret = futex_wake(iof->uaddr, FLAGS_STRICT | iof->futex_flags,
+       ret = futex_wake(iof->uaddr, FLAGS_STRICT | iof->futex_flags, NULL,
                         iof->futex_val, iof->futex_mask);
        if (ret < 0)
                req_set_fail(req);
index 61f4f559afa10f5d377ddead97fcd06669dfccb7..77ccb7787c349929895cf0980575c74d25c8c0e9 100644 (file)
@@ -1062,7 +1062,7 @@ retry:
        owner = uval & FUTEX_TID_MASK;
 
        if (pending_op && !pi && !owner) {
-               futex_wake(uaddr, FLAGS_SIZE_32 | FLAGS_SHARED, 1,
+               futex_wake(uaddr, FLAGS_SIZE_32 | FLAGS_SHARED, NULL, 1,
                           FUTEX_BITSET_MATCH_ANY);
                return 0;
        }
@@ -1116,7 +1116,7 @@ retry:
         * PI futexes happens in exit_pi_state():
         */
        if (!pi && (uval & FUTEX_WAITERS)) {
-               futex_wake(uaddr, FLAGS_SIZE_32 | FLAGS_SHARED, 1,
+               futex_wake(uaddr, FLAGS_SIZE_32 | FLAGS_SHARED, NULL, 1,
                           FUTEX_BITSET_MATCH_ANY);
        }
 
@@ -1208,6 +1208,27 @@ static void exit_robust_list(struct task_struct *curr)
        }
 }
 
+static bool robust_list_clear_pending(unsigned long __user *pop)
+{
+       struct robust_list_head __user *head = current->futex.robust_list;
+
+       if (!put_user(0UL, pop))
+               return true;
+
+       /*
+        * Just give up. The robust list head is usually part of TLS, so the
+        * chance that this gets resolved is close to zero.
+        *
+        * If @pop_addr is the robust_list_head::list_op_pending pointer then
+        * clear the robust list head pointer to prevent further damage when the
+        * task exits.  Better a few stale futexes than corrupted memory. But
+        * that's mostly an academic exercise.
+        */
+       if (pop == (unsigned long __user *)&head->list_op_pending)
+               current->futex.robust_list = NULL;
+       return false;
+}
+
 #ifdef CONFIG_COMPAT
 static void __user *futex_uaddr(struct robust_list __user *entry,
                                compat_long_t futex_offset)
@@ -1304,6 +1325,21 @@ static void compat_exit_robust_list(struct task_struct *curr)
                handle_futex_death(uaddr, curr, pend_mod, HANDLE_DEATH_PENDING);
        }
 }
+
+static bool compat_robust_list_clear_pending(u32 __user *pop)
+{
+       struct compat_robust_list_head __user *head = current->futex.compat_robust_list;
+
+       if (!put_user(0U, pop))
+               return true;
+
+       /* See comment in robust_list_clear_pending(). */
+       if (pop == &head->list_op_pending)
+               current->futex.compat_robust_list = NULL;
+       return false;
+}
+#else
+static bool compat_robust_list_clear_pending(u32 __user *pop_addr) { return false; }
 #endif
 
 #ifdef CONFIG_FUTEX_PI
@@ -1397,6 +1433,19 @@ static void exit_pi_state_list(struct task_struct *curr)
 static inline void exit_pi_state_list(struct task_struct *curr) { }
 #endif
 
+bool futex_robust_list_clear_pending(void __user *pop, unsigned int flags)
+{
+       bool size32bit = !!(flags & FLAGS_ROBUST_LIST32);
+
+       if (!IS_ENABLED(CONFIG_64BIT) && !size32bit)
+               return false;
+
+       if (IS_ENABLED(CONFIG_64BIT) && size32bit)
+               return compat_robust_list_clear_pending(pop);
+
+       return robust_list_clear_pending(pop);
+}
+
 static void futex_cleanup(struct task_struct *tsk)
 {
        if (unlikely(tsk->futex.robust_list)) {
index 9f6bf6f585fca545bf5d49e527bf1f6fbe7f9f1b..79ef2c709c81e1360545b4a5cb5bc0b2363b0011 100644 (file)
@@ -40,6 +40,8 @@
 #define FLAGS_NUMA             0x0080
 #define FLAGS_STRICT           0x0100
 #define FLAGS_MPOL             0x0200
+#define FLAGS_ROBUST_UNLOCK    0x0400
+#define FLAGS_ROBUST_LIST32    0x0800
 
 /* FUTEX_ to FLAGS_ */
 static inline unsigned int futex_to_flags(unsigned int op)
@@ -52,6 +54,12 @@ static inline unsigned int futex_to_flags(unsigned int op)
        if (op & FUTEX_CLOCK_REALTIME)
                flags |= FLAGS_CLOCKRT;
 
+       if (op & FUTEX_ROBUST_UNLOCK)
+               flags |= FLAGS_ROBUST_UNLOCK;
+
+       if (op & FUTEX_ROBUST_LIST32)
+               flags |= FLAGS_ROBUST_LIST32;
+
        return flags;
 }
 
@@ -449,13 +457,16 @@ extern int futex_unqueue_multiple(struct futex_vector *v, int count);
 extern int futex_wait_multiple(struct futex_vector *vs, unsigned int count,
                               struct hrtimer_sleeper *to);
 
-extern int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset);
+extern int futex_wake(u32 __user *uaddr, unsigned int flags, void __user *pop,
+                     int nr_wake, u32 bitset);
 
 extern int futex_wake_op(u32 __user *uaddr1, unsigned int flags,
                         u32 __user *uaddr2, int nr_wake, int nr_wake2, int op);
 
-extern int futex_unlock_pi(u32 __user *uaddr, unsigned int flags);
+extern int futex_unlock_pi(u32 __user *uaddr, unsigned int flags, void __user *pop);
 
 extern int futex_lock_pi(u32 __user *uaddr, unsigned int flags, ktime_t *time, int trylock);
 
+bool futex_robust_list_clear_pending(void __user *pop, unsigned int flags);
+
 #endif /* _FUTEX_H */
index e037a97fe27704caaebb96cc5b249d8e275ce1a4..9dd5c0b1ac6ab1407b5d287367690c7259a156d3 100644 (file)
@@ -1139,7 +1139,7 @@ out:
  * This is the in-kernel slowpath: we look up the PI state (if any),
  * and do the rt-mutex unlock.
  */
-int futex_unlock_pi(u32 __user *uaddr, unsigned int flags)
+static int __futex_unlock_pi(u32 __user *uaddr, unsigned int flags)
 {
        u32 curval, uval, vpid = task_pid_vnr(current);
        union futex_key key = FUTEX_KEY_INIT;
@@ -1148,7 +1148,6 @@ int futex_unlock_pi(u32 __user *uaddr, unsigned int flags)
 
        if (!IS_ENABLED(CONFIG_FUTEX_PI))
                return -ENOSYS;
-
 retry:
        if (get_user(uval, uaddr))
                return -EFAULT;
@@ -1302,3 +1301,15 @@ pi_faulted:
        return ret;
 }
 
+int futex_unlock_pi(u32 __user *uaddr, unsigned int flags, void __user *pop)
+{
+       int ret = __futex_unlock_pi(uaddr, flags);
+
+       if (ret || !(flags & FLAGS_ROBUST_UNLOCK))
+               return ret;
+
+       if (!futex_robust_list_clear_pending(pop, flags))
+               return -EFAULT;
+
+       return 0;
+}
index 8944ff4930d7c66792184916b9484af64dacab7b..2fa19d9d008de9689798043d17f114b091339f17 100644 (file)
@@ -118,6 +118,13 @@ long do_futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout,
                        return -ENOSYS;
        }
 
+       if (flags & FLAGS_ROBUST_UNLOCK) {
+               if (cmd != FUTEX_WAKE &&
+                   cmd != FUTEX_WAKE_BITSET &&
+                   cmd != FUTEX_UNLOCK_PI)
+                       return -ENOSYS;
+       }
+
        switch (cmd) {
        case FUTEX_WAIT:
                val3 = FUTEX_BITSET_MATCH_ANY;
@@ -128,7 +135,7 @@ long do_futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout,
                val3 = FUTEX_BITSET_MATCH_ANY;
                fallthrough;
        case FUTEX_WAKE_BITSET:
-               return futex_wake(uaddr, flags, val, val3);
+               return futex_wake(uaddr, flags, uaddr2, val, val3);
        case FUTEX_REQUEUE:
                return futex_requeue(uaddr, flags, uaddr2, flags, val, val2, NULL, 0);
        case FUTEX_CMP_REQUEUE:
@@ -141,7 +148,7 @@ long do_futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout,
        case FUTEX_LOCK_PI2:
                return futex_lock_pi(uaddr, flags, timeout, 0);
        case FUTEX_UNLOCK_PI:
-               return futex_unlock_pi(uaddr, flags);
+               return futex_unlock_pi(uaddr, flags, uaddr2);
        case FUTEX_TRYLOCK_PI:
                return futex_lock_pi(uaddr, flags, NULL, 1);
        case FUTEX_WAIT_REQUEUE_PI:
@@ -375,7 +382,7 @@ SYSCALL_DEFINE4(futex_wake,
        if (!futex_validate_input(flags, mask))
                return -EINVAL;
 
-       return futex_wake(uaddr, FLAGS_STRICT | flags, nr, mask);
+       return futex_wake(uaddr, FLAGS_STRICT | flags, NULL, nr, mask);
 }
 
 /*
index ceed9d879059215c968402dde1040a101ec0ce25..8f5e5d3023568f2435b8f40fdeb04d8e2177d196 100644 (file)
@@ -149,13 +149,36 @@ void futex_wake_mark(struct wake_q_head *wake_q, struct futex_q *q)
        wake_q_add_safe(wake_q, p);
 }
 
+/*
+ * If requested, clear the robust list pending op and unlock the futex
+ */
+static bool futex_robust_unlock(u32 __user *uaddr, unsigned int flags, void __user *pop)
+{
+       if (!(flags & FLAGS_ROBUST_UNLOCK))
+               return true;
+
+       /* First unlock the futex, which requires release semantics. */
+       scoped_user_write_access(uaddr, efault)
+               unsafe_atomic_store_release_user(0, uaddr, efault);
+
+       /*
+        * Clear the pending list op now. If that fails, then the task is in
+        * deeper trouble as the robust list head is usually part of the TLS.
+        * The chance of survival is close to zero.
+        */
+       return futex_robust_list_clear_pending(pop, flags);
+
+efault:
+       return false;
+}
+
 /*
  * Wake up waiters matching bitset queued on this futex (uaddr).
  */
-int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset)
+int futex_wake(u32 __user *uaddr, unsigned int flags, void __user *pop, int nr_wake, u32 bitset)
 {
-       struct futex_q *this, *next;
        union futex_key key = FUTEX_KEY_INIT;
+       struct futex_q *this, *next;
        DEFINE_WAKE_Q(wake_q);
        int ret;
 
@@ -166,6 +189,9 @@ int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset)
        if (unlikely(ret != 0))
                return ret;
 
+       if (!futex_robust_unlock(uaddr, flags, pop))
+               return -EFAULT;
+
        if ((flags & FLAGS_STRICT) && !nr_wake)
                return 0;