futex: Address the robust futex unlock race for real
Subsystems (11)
Thomas Gleixner
maintainerv2 cover futex: Address the robust futex unlock race for real
This is a follow up to v1 which can be found here:
https://lore.kernel.org/[email protected]
The v1 cover letter contains a detailed analysis or the underlying problem. TLDR:
The robust futex unlock mechanism is racy in respect to the clearing of the robust_list_head::list_op_pending pointer because unlock and clearing the pointer are not atomic. The race window is between the unlock and clearing the pending op pointer. If the task is forced to exit in this window, exit will access a potentially invalid pending op pointer when cleaning up the robust list. That happens if another task manages to unmap the object containing the lock before the cleanup, which results in an UAF. In the worst case this UAF can lead to memory corruption when unrelated content has been mapped to the same address by the time the access happens.
User space can't solve this problem without help from the kernel. This series provides the kernel side infrastructure to help it along:
1) Combined unlock, pointer clearing, wake-up for the contended case
2) VDSO based unlock and pointer clearing helpers with a fix-up function in the kernel when user space was interrupted within the critical section.
Both ensure that the pointer clearing happens _before_ a task exits and the kernel cleans up the robust list during the exit procedure.
Changes since v1:
- Use a dedicated modifier flag FUTEX_ROBUST_LIST32 for the futex syscall ops to indicate the size of the pending op pointer. This replaces the V1 pointer mangling where bit0 indicated 32-bit size. The flag is mandatory for 32-bit applications. - Florian
- Add a new unsafe_atomic_store_release_user() helper and use it for the unlock. If defaults to 'smp_mb(); unsafe_put_user()'. Architectures where store implies release can remove the smp_mb() by selecting a config option (done for x86). The helper can be overridden by architecture implementations. - André, Peter
- Remove the global exposure of the critical section labels. They are now only available through vdsoXX.so.dbg, which is sufficient for vdso2c to find them - Thomas, Mathieu
- Replace the combined mixed size VDSO unlock helper with dedicated functions for 64-bit and 32-bit pointers. This requires to evaluate up to two ranges in the critical section IP check, but that removes the complexity both in the unlock helpers and the architecture specific decision and pointer retrieval functions.
- Use the proper ASM modifiers and reuse the zeroed register, which is used for the cmpxchg() for zeroing the pending op pointer instead of using a $0 immediate. - Uros
- Address coding style, documentation and naming feedback - André
Thanks to everyone for feedback and discussion!
The modified test case and the delta patch against the previous version is below.
The series applies on v7.0-rc3 and is also available via git:
git://git.kernel.org/pub/scm/linux/kernel/git/tglx/devel.git locking-futex-v2
According to my basic testing this still works correctly with both sizes, but that needs obviously more scrutiny especially from the libc people and André.
If the functionality itself is agreed on we only need to agree on the names and signatures of the functions exposed through the VDSO before we set them in stone. That will hopefully not take another 15 years :)
That said, after 20+ years working on futexes I'm still amazed how much code is required to interact with one or two memory locations in user space. I long ago stated that the futex code consists of 5% functionality and 95% corner case handling. After this work episode I'm convinced that futexes are nothing else than a infinite Rube Goldberg machine in disguise.
Thanks,
tglx
---
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/futex.h>
#include <linux/prctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
typedef uint32_t (*frtu64_t)(uint32_t *, uint32_t, uint64_t *); typedef uint32_t (*frtu32_t)(uint32_t *, uint32_t, uint32_t *);
static frtu64_t frtu64;
static frtu32_t frtu32;
static void get_vdso(void)
{
void *vdso = dlopen("linux-vdso.so.1",
RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
if (!vdso)
vdso = dlopen("linux-gate.so.1",
RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
if (!vdso)
vdso = dlopen("linux-vdso32.so.1",
RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
if (!vdso)
vdso = dlopen("linux-vdso64.so.1",
RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
if (!vdso) {
printf("Failed to find vDSO\n");
exit(1);
}
frtu64 = (frtu64_t)dlsym(vdso, "__vdso_futex_robust_list64_try_unlock");
frtu32 = (frtu32_t)dlsym(vdso, "__vdso_futex_robust_list32_try_unlock");
}#define FUTEX_ROBUST_UNLOCK 512
#define FUTEX_ROBUST_LIST32 1024
#define UNLOCK64 (FUTEX_WAKE | FUTEX_ROBUST_UNLOCK)
#define UNLOCK64_PI (FUTEX_UNLOCK_PI | FUTEX_ROBUST_UNLOCK)
#define UNLOCK32 (UNLOCK64 | FUTEX_ROBUST_LIST32)
#define UNLOCK32_PI (UNLOCK64 | FUTEX_ROBUST_LIST32)
static void set_pop(struct robust_list_head *rhead, pthread_mutex_t *mutex)
{
rhead->list_op_pending = (struct robust_list *)&mutex->__data.__list.__next;
}static void *pop_exp(bool sz32, pthread_mutex_t *mutex)
{
uint64_t exp = (uint64_t)(unsigned long)&mutex->__data.__list.__next;
if (!sz32)
return NULL;
exp &= ~0xFFFFFFFFULL;
return (void *)(unsigned long)exp;
}static void unlock_uncontended(bool sz32, struct robust_list_head *rhead,
pthread_mutex_t *mutex, pid_t tid)
{
uint32_t *lock = (uint32_t *)&mutex->__data.__lock;
void *exp = pop_exp(sz32, mutex);
pid_t lock_tid;
set_pop(rhead, mutex);
*lock = tid;
if (sz32)
lock_tid = frtu32(lock, tid, (uint32_t *)&rhead->list_op_pending);
else
lock_tid = frtu64(lock, tid, (uint64_t *)&rhead->list_op_pending);
if (lock_tid != tid)
printf("Non contended unlock failed. Return: %08x\n", lock_tid);
else if (rhead->list_op_pending != exp)
printf("List op not cleared: %16lx\n", (unsigned long) rhead->list_op_pending);
else if (*lock)
printf("Non contended unlock failed: LOCK %08x\n", *lock);
else
printf("Non contended unlock succeeded\n");
}static void unlock_syscall(bool sz32, bool pi, struct robust_list_head *rhead,
pthread_mutex_t *mutex, pid_t tid)
{
uint32_t *lock = (uint32_t *)&mutex->__data.__lock;
void *exp = pop_exp(sz32, mutex);
unsigned int op;
int ret;
if (sz32)
op = pi ? UNLOCK32_PI : UNLOCK32;
else
op = pi ? UNLOCK64_PI : UNLOCK64;
ret = syscall(SYS_futex, lock, op, !!pi, NULL, (uint32_t *)&rhead->list_op_pending, 0, 0);
if (ret < 0)
printf("syscall unlock failed %d\n", errno);
else if (rhead->list_op_pending != exp)
printf("List op not cleared in syscall: %16lx\n", (unsigned long) rhead->list_op_pending);
else if (*lock)
printf("Contended syscall unlock failed: LOCK %08x\n", *lock);
else
printf("Contended unlock syscall succeeded\n");
}static void unlock_contended(bool sz32, bool pi, struct robust_list_head *rhead,
pthread_mutex_t *mutex, pid_t tid)
{
uint32_t *lock = (uint32_t *)&mutex->__data.__lock;
pid_t lock_tid;
set_pop(rhead, mutex);
*lock = tid | FUTEX_WAITERS;
if (sz32)
lock_tid = frtu32(lock, tid, (uint32_t *)&rhead->list_op_pending);
else
lock_tid = frtu64(lock, tid, (uint64_t *)&rhead->list_op_pending);
if (lock_tid == tid)
printf("Contended unlock succeeded %08x\n", lock_tid);
else if (rhead->list_op_pending != (struct robust_list *)&mutex->__data.__list.__next)
printf("List op cleared: %16lx\n", (unsigned long) rhead->list_op_pending);
else if (!*lock)
printf("Contended unlock cleared LOCK %08x\n", *lock);
else
unlock_syscall(sz32, pi, rhead, mutex, tid);
}static void test(bool sz32)
{
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
struct robust_list_head *rhead;
pid_t tid = gettid();
size_t sz;
if (sz32 && !frtu32)
return;
if (!sz32 && !frtu64)
return;
syscall(SYS_get_robust_list, 0, &rhead, &sz);
printf("Testing non contended unlock %s\n", sz32 ? "POP32" : "POP64");
unlock_uncontended(sz32, rhead, &mutex, tid);
printf("Testing contended FUTEX_WAKE unlock %s\n", sz32 ? "POP32" : "POP64");
unlock_contended(sz32, false, rhead, &mutex, tid);
printf("Testing contended FUTEX_UNLOCK_PI %s\n", sz32 ? "POP32" : "POP64");
unlock_contended(sz32, true, rhead, &mutex, tid);
}int main(int argc, char * const argv[])
{
get_vdso();
test(false);
test(true);
return 0;
}---
Delta diff:
---
arch/Kconfig +4 -0
@@ -403,6 +403,10 @@ config ARCH_32BIT_OFF_T config ARCH_32BIT_USTAT_F_TINODE bool +# Selected by architectures when plain stores have release semantics +config ARCH_STORE_IMPLIES_RELEASE + bool + config HAVE_ASM_MODVERSIONS bool help
arch/x86/Kconfig +1 -0
@@ -119,6 +119,7 @@ config X86 select ARCH_MIGHT_HAVE_PC_PARPORT select ARCH_MIGHT_HAVE_PC_SERIO select ARCH_STACKWALK + select ARCH_STORE_IMPLIES_RELEASE select ARCH_SUPPORTS_ACPI select ARCH_SUPPORTS_ATOMIC_RMW select ARCH_SUPPORTS_DEBUG_PAGEALLOC
arch/x86/entry/vdso/common/vfutex.c +63 -59
134 lines not on this page · raw message
arch/x86/entry/vdso/vdso32/vdso32.lds.S +1 -4
@@ -31,10 +31,7 @@ VERSION __vdso_clock_getres_time64; __vdso_getcpu; #ifdef CONFIG_FUTEX_ROBUST_UNLOCK - __vdso_futex_robust_try_unlock; - __vdso_futex_robust_try_unlock_cs_start; - __vdso_futex_robust_try_unlock_cs_success; - __vdso_futex_robust_try_unlock_cs_end; + __vdso_futex_robust_list32_try_unlock; #endif };
arch/x86/entry/vdso/vdso64/vdso64.lds.S +2 -4
@@ -33,10 +33,8 @@ VERSION { getrandom; __vdso_getrandom; #ifdef CONFIG_FUTEX_ROBUST_UNLOCK - __vdso_futex_robust_try_unlock; - __vdso_futex_robust_try_unlock_cs_start; - __vdso_futex_robust_try_unlock_cs_success; - __vdso_futex_robust_try_unlock_cs_end; + __vdso_futex_robust_list64_try_unlock; + __vdso_futex_robust_list32_try_unlock; #endif local: *; };
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +2 -4
@@ -23,10 +23,8 @@ VERSION { __vdso_time; __vdso_clock_getres; #ifdef CONFIG_FUTEX_ROBUST_UNLOCK - __vdso_futex_robust_try_unlock; - __vdso_futex_robust_try_unlock_cs_start; - __vdso_futex_robust_try_unlock_cs_success; - __vdso_futex_robust_try_unlock_cs_end; + __vdso_futex_robust_list64_try_unlock; + __vdso_futex_robust_list32_try_unlock; #endif local: *; };
arch/x86/entry/vdso/vma.c +21 -6
@@ -78,13 +78,28 @@ static void vdso_futex_robust_unlock_update_ips(void) {const struct vdso_image *image = current->mm->context.vdso_image;unsigned long vdso = (unsigned long ) current->mm->context.vdso; +struct futex_mm_data *fd = ¤t->mm->futex; +struct futex_unlock_cs_range *csr = fd->unlock_cs_ranges; - current->mm->futex.unlock_cs_start_ip = - vdso + image->sym___vdso_futex_robust_try_unlock_cs_start; - current->mm->futex.unlock_cs_success_ip = - vdso + image->sym___vdso_futex_robust_try_unlock_cs_success; - current->mm->futex.unlock_cs_end_ip = - vdso + image->sym___vdso_futex_robust_try_unlock_cs_end; + fd->unlock_cs_num_ranges =0 ; +#ifdef CONFIG_X86_64 +if (image->sym_x86_64_futex_try_unlock_cs_start) { + csr->start_ip = vdso + image->sym_x86_64_futex_try_unlock_cs_start; + csr->end_ip = vdso + image->sym_x86_64_futex_try_unlock_cs_end; + csr->pop_size32 =0 ; + csr++; + fd->unlock_cs_num_ranges++; + } +#endif /* CONFIG_X86_64 */ + +#if defined(CONFIG_X86_32 ) || defined(CONFIG_COMPAT ) +if (image->sym_x86_32_futex_try_unlock_cs_start) { + csr->start_ip = vdso + image->sym_x86_32_futex_try_unlock_cs_start; + csr->end_ip = vdso + image->sym_x86_32_futex_try_unlock_cs_end; + csr->pop_size32 =1 ; + fd->unlock_cs_num_ranges++; + } +#endif /* CONFIG_X86_32 || CONFIG_COMPAT */ }#else static inline void vdso_futex_robust_unlock_update_ips (void ) { }
arch/x86/include/asm/futex_robust.h +5 -30
@@ -4,41 +4,16 @@#include <asm/ptrace.h> -static __always_inline bool x86_futex_needs_robust_unlock_fixup(struct pt_regs *regs) +static __always_inline void __user *x86_futex_robust_unlock_get_pop(struct pt_regs *regs) {/* -* This is tricky in the compat case as it has to take the size check -* into account. See the ASM magic in the VDSO vfutex code. If compat is -* disabled or this is a 32-bit kernel then ZF is authoritive no matter -* what. -*/ -if (!IS_ENABLED (CONFIG_X86_64 ) || !IS_ENABLED (CONFIG_IA32_EMULATION )) -return !!(regs->flags &X86_EFLAGS_ZF ); - -/* -* For the compat case, the core code already established that regs->ip -* is >= cs_start and < cs_end. Now check whether it is at the -* conditional jump which checks the cmpxchg() or if it succeeded and -* does the size check, which obviously modifies ZF too. -*/ -if (regs->ip >= current->mm->futex.unlock_cs_success_ip) -return true; -/* -* It's at the jnz right after the cmpxchg(). ZF tells whether this -* succeeded or not. +* If ZF is set then the cmpxchg succeeded and the pending op pointer +* needs to be cleared. */ -return !!(regs->flags &X86_EFLAGS_ZF ); -} - -#define arch_futex_needs_robust_unlock_fixup (regs) \ - x86_futex_needs_robust_unlock_fixup(regs) - -static __always_inline void __user *x86_futex_robust_unlock_get_pop(struct pt_regs *regs) -{ -return (void __user *)regs->dx; +return regs->flags &X86_EFLAGS_ZF ? (void __user *)regs->dx :NULL ; } -#define arch_futex_robust_unlock_get_pop (regs) \ +#define arch_futex_robust_unlock_get_pop (regs) \ x86_futex_robust_unlock_get_pop(regs)#endif /* _ASM_X86_FUTEX_ROBUST_H */
arch/x86/include/asm/vdso.h +6 -3
@@ -25,9 +25,12 @@ struct vdso_image {long sym_int80_landing_pad;long sym_vdso32_sigreturn_landing_pad;long sym_vdso32_rt_sigreturn_landing_pad; -long sym___vdso_futex_robust_try_unlock_cs_start; -long sym___vdso_futex_robust_try_unlock_cs_success; -long sym___vdso_futex_robust_try_unlock_cs_end; +long sym_x86_64_futex_try_unlock_cs_start; +long sym_x86_64_futex_try_unlock_cs_end; +long sym_x86_64_compat_futex_try_unlock_cs_start; +long sym_x86_64_compat_futex_try_unlock_cs_end; +long sym_x86_32_futex_try_unlock_cs_start; +long sym_x86_32_futex_try_unlock_cs_end; };extern const struct vdso_image vdso64_image;
arch/x86/tools/vdso2c.c +6 -3
@@ -82,9 +82,12 @@ struct vdso_sym required_syms[] = { {"int80_landing_pad", true}, {"vdso32_rt_sigreturn_landing_pad", true}, {"vdso32_sigreturn_landing_pad", true}, - {"__vdso_futex_robust_try_unlock_cs_start", true}, - {"__vdso_futex_robust_try_unlock_cs_success", true}, - {"__vdso_futex_robust_try_unlock_cs_end", true}, + {"x86_64_futex_try_unlock_cs_start", true}, + {"x86_64_futex_try_unlock_cs_end", true}, + {"x86_64_compat_futex_try_unlock_cs_start", true}, + {"x86_64_compat_futex_try_unlock_cs_end", true}, + {"x86_32_futex_try_unlock_cs_start", true}, + {"x86_32_futex_try_unlock_cs_end", true}, }; __attribute__((format(printf, 1, 2))) __attribute__((noreturn))
include/linux/futex.h +17 -6
@@ -115,18 +115,20 @@ static inline int futex_mm_init(struct mm_struct *mm) { return 0; }#ifdef CONFIG_FUTEX_ROBUST_UNLOCK #include <asm/futex_robust.h> -void __futex_fixup_robust_unlock (struct pt_regs *regs); +void __futex_fixup_robust_unlock (struct pt_regs *regs,struct futex_unlock_cs_range *csr); -static inline bool futex_within_robust_unlock (struct pt_regs *regs) +static inline bool futex_within_robust_unlock (struct pt_regs *regs, +struct futex_unlock_cs_range *csr) {unsigned long ip =instruction_pointer (regs); -return ip >= current->mm->futex.unlock_cs_start_ip && - ip < current->mm->futex.unlock_cs_end_ip; +return ip >= csr->start_ip && ip < csr->end_ip; }static inline void futex_fixup_robust_unlock (struct pt_regs *regs) { +struct futex_unlock_cs_range *csr; +/* * Avoid dereferencing current->mm if not returning from interrupt. * current->rseq.event is going to be used anyway in the exit to user
@@ -135,8 +137,17 @@ static inline void futex_fixup_robust_unlock(struct pt_regs *regs)if (!current->rseq.event.user_irq)return ; -if (unlikely (futex_within_robust_unlock (regs))) - __futex_fixup_robust_unlock(regs); + csr = current->mm->futex.unlock_cs_ranges; +if (unlikely (futex_within_robust_unlock (regs, csr))) { +__futex_fixup_robust_unlock (regs, csr); +return ; + } + +/* Multi sized robust lists are only supported with CONFIG_COMPAT */ +if (IS_ENABLED (CONFIG_COMPAT ) && current->mm->futex.unlock_cs_num_ranges ==2 ) { +if (unlikely (futex_within_robust_unlock (regs, ++csr))) +__futex_fixup_robust_unlock (regs, csr); + } }#else /* CONFIG_FUTEX_ROBUST_UNLOCK */ static inline void futex_fixup_robust_unlock (struct pt_regs *regs) {}
include/linux/futex_types.h +28 -20
78 lines not on this page · raw message
include/linux/sched.h +1 -1
@@ -1329,7 +1329,7 @@ struct task_struct {u32 rmid;#endif -struct futex_ctrl futex; +struct futex_sched_data futex;#ifdef CONFIG_PERF_EVENTS u8 perf_recursion[PERF_NR_CONTEXTS ];
include/linux/uaccess.h +9 -0
@@ -644,6 +644,15 @@ static inline void user_access_restore(unsigned long flags) { }#define user_read_access_end user_access_end#endif +#ifndef unsafe_atomic_store_release_user +# define unsafe_atomic_store_release_user (val, uptr, elbl) \ + do { \ + if (!IS_ENABLED(CONFIG_ARCH_STORE_IMPLIES_RELEASE)) \ + smp_mb(); \ + unsafe_put_user(val, uptr, elbl); \ + } while (0) +#endif +/* Define RW variant so the below _mode macro expansion works */ #define masked_user_rw_access_begin (u) masked_user_access_begin(u)#define user_rw_access_begin (u, s) user_access_begin(u, s)
include/uapi/linux/futex.h +40 -32
86 lines not on this page · raw message
include/vdso/futex.h +18 -10
@@ -2,15 +2,14 @@#ifndef _VDSO_FUTEX_H#define _VDSO_FUTEX_H -#include <linux/types.h> - -struct robust_list ; +#include <uapi/linux/types.h> /** -* __vdso_futex_robust_try_unlock - Try to unlock an uncontended robust futex +* __vdso_futex_robust_list64_try_unlock - Try to unlock an uncontended robust futex +* with a 64-bit pending op pointer * @lock: Pointer to the futex lock object * @tid: The TID of the calling task -* @op: Pointer to the task's robust_list_head::list_pending_op +* @pop: Pointer to the task's robust_list_head::list_pending_op * * Return: The content of *@lock. On success this is the same as @tid. *
@@ -28,17 +27,26 @@ struct robust_list;* * User space uses it in the following way: * -* if (__vdso_futex_robust_try_unlock(lock, tid, &pending_op) != tid) +* if (__vdso_futex_robust_list64_try_unlock(lock, tid, &pending_op) != tid) * err = sys_futex($OP | FUTEX_ROBUST_UNLOCK,....); * * If the unlock attempt fails due to the FUTEX_WAITERS bit set in the lock, * then the syscall does the unlock, clears the pending op pointer and wakes the * requested number of waiters. +*/ +__u32 __vdso_futex_robust_list64_try_unlock (__u32 *lock,__u32 tid,__u64 *pop); + +/** +* __vdso_futex_robust_list32_try_unlock - Try to unlock an uncontended robust futex +* with a 32-bit pending op pointer +* @lock: Pointer to the futex lock object +* @tid: The TID of the calling task +* @pop: Pointer to the task's robust_list_head::list_pending_op +* +* Return: The content of *@lock. On success this is the same as @tid. * -* The @op pointer is intentionally void. It has the same requirements as the -* @uaddr2 argument for sys_futex(FUTEX_ROBUST_UNLOCK) operations. See the -* modifier and the related documentation in include/uapi/linux/futex.h +* Same as __vdso_futex_robust_list64_try_unlock() just with a 32-bit @pop pointer. */ -uint32_t __vdso_futex_robust_try_unlock (uint32_t *lock,uint32_t tid,void *op); +__u32 __vdso_futex_robust_list32_try_unlock (__u32 *lock,__u32 tid,__u32 *pop);#endif
kernel/futex/core.c +8 -15
@@ -46,6 +46,8 @@#include <linux/slab.h> #include <linux/vmalloc.h> +#include <vdso/futex.h> +#include "futex.h" #include "../locking/rtmutex_common.h"
@@ -1434,17 +1436,9 @@ static void exit_pi_state_list(struct task_struct *curr)static inline void exit_pi_state_list (struct task_struct *curr) { }#endif -static inline bool mask_pop_addr (void __user **pop) -{ -unsigned long addr = (unsigned long )*pop; - - *pop = (void __user *) (addr & ~FUTEX_ROBUST_UNLOCK_MOD_MASK ); -return !!(addr &FUTEX_ROBUST_UNLOCK_MOD_32BIT ); -} - -bool futex_robust_list_clear_pending (void __user *pop) +bool futex_robust_list_clear_pending (void __user *pop,unsigned int flags) { -bool size32bit =mask_pop_addr (&pop); +bool size32bit = !!(flags &FLAGS_ROBUST_LIST32 );if (!IS_ENABLED (CONFIG_64BIT ) && !size32bit)return false;
@@ -1456,15 +1450,14 @@ bool futex_robust_list_clear_pending(void __user *pop) }#ifdef CONFIG_FUTEX_ROBUST_UNLOCK -void __futex_fixup_robust_unlock (struct pt_regs *regs) +void __futex_fixup_robust_unlock (struct pt_regs *regs,struct futex_unlock_cs_range *csr) { -void __user *pop; +void __user *pop =arch_futex_robust_unlock_get_pop (regs); -if (!arch_futex_needs_robust_unlock_fixup (regs)) +if (!pop)return ; - pop =arch_futex_robust_unlock_get_pop (regs); -futex_robust_list_clear_pending (pop); +futex_robust_list_clear_pending (pop, csr->cs_pop_size32 ?FLAGS_ROBUST_LIST32 :0 ); }#endif /* CONFIG_FUTEX_ROBUST_UNLOCK */
kernel/futex/futex.h +5 -1
@@ -41,6 +41,7 @@#define FLAGS_STRICT 0x0100#define FLAGS_MPOL 0x0200#define FLAGS_UNLOCK_ROBUST 0x0400 +#define FLAGS_ROBUST_LIST32 0x0800/* FUTEX_ to FLAGS_ */ static inline unsigned int futex_to_flags (unsigned int op)
@@ -56,6 +57,9 @@ static inline unsigned int futex_to_flags(unsigned int op)if (op &FUTEX_UNLOCK_ROBUST ) flags |=FLAGS_UNLOCK_ROBUST ; +if (op &FUTEX_ROBUST_LIST32 ) + flags |=FLAGS_ROBUST_LIST32 ; +return flags; }
@@ -452,6 +456,6 @@ extern int futex_unlock_pi(u32 __user *uaddr, unsigned int flags, void __user *pextern int futex_lock_pi (u32 __user *uaddr,unsigned int flags,ktime_t *time,int trylock); -bool futex_robust_list_clear_pending (void __user *pop); +bool futex_robust_list_clear_pending (void __user *pop,unsigned int flags);#endif /* _FUTEX_H */
kernel/futex/pi.c +1 -1
@@ -1298,7 +1298,7 @@ int futex_unlock_pi(u32 __user *uaddr, unsigned int flags, void __user *pop)if (ret || !(flags &FLAGS_UNLOCK_ROBUST ))return ret; -if (!futex_robust_list_clear_pending (pop)) +if (!futex_robust_list_clear_pending (pop, flags))return -EFAULT ;return 0 ;
kernel/futex/waitwake.c +9 -6
@@ -157,16 +157,19 @@ static bool futex_robust_unlock(u32 __user *uaddr, unsigned int flags, void __usif (!(flags &FLAGS_UNLOCK_ROBUST ))return true; -/* First unlock the futex. */ -if (put_user (0U , uaddr)) -return false; +/* 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 TLS. The -* chance of survival is close to zero. +* 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); +return futex_robust_list_clear_pending (pop, flags); + +efault: +return false; }/*
Thomas Gleixner
maintainerPATCH v2 1/11 futex: Move futex task related data into a struct
Having all these members in task_struct along with the required #ifdeffery is annoying, does not allow efficient initializing of the data with memset() and makes extending it tedious.
Move it into a data structure and fix up all usage sites.
---
V2: Rename the struct and add the missing kernel doc - Andre
--- Documentation/locking/robust-futexes.rst | 8 ++-- include/linux/futex.h | 12 ++---- include/linux/futex_types.h | 36 ++++++++++++++++++++ include/linux/sched.h | 16 ++------- kernel/exit.c | 4 +- kernel/futex/core.c | 55 +++++++++++++++---------------- kernel/futex/pi.c | 26 +++++++------- kernel/futex/syscalls.c | 23 ++++-------- 8 files changed, 99 insertions(+), 81 deletions(-)
Documentation/locking/robust-futexes.rst +4 -4
@@ -94,7 +94,7 @@ time, the kernel checks this user-space locks to be cleaned up? In the common case, at do_exit() time, there is no list registered, so -the cost of robust futexes is just a simple current->robust_list != NULL +the cost of robust futexes is just a current->futex.robust_list != NULL comparison. If the thread has registered a list, then normally the list is empty. If the thread/process crashed or terminated in some incorrect way then the list might be non-empty: in this case the kernel carefully
@@ -178,9 +178,9 @@ The patch adds two new syscalls: one to size_t __user *len_ptr); List registration is very fast: the pointer is simply stored in -current->robust_list. [Note that in the future, if robust futexes become -widespread, we could extend sys_clone() to register a robust-list head -for new threads, without the need of another syscall.] +current->futex.robust_list. [Note that in the future, if robust futexes +become widespread, we could extend sys_clone() to register a robust-list +head for new threads, without the need of another syscall.] So there is virtually zero overhead for tasks not using robust futexes, and even for robust futex users, there is only one extra syscall per
include/linux/futex.h +4 -8
@@ -64,14 +64,10 @@ enum {static inline void futex_init_task (struct task_struct *tsk) { - tsk->robust_list =NULL ; -#ifdef CONFIG_COMPAT - tsk->compat_robust_list =NULL ; -#endif -INIT_LIST_HEAD (&tsk->pi_state_list); - tsk->pi_state_cache =NULL ; - tsk->futex_state =FUTEX_STATE_OK ; -mutex_init (&tsk->futex_exit_mutex); +memset (&tsk->futex,0 ,sizeof (tsk->futex)); +INIT_LIST_HEAD (&tsk->futex.pi_state_list); + tsk->futex.state =FUTEX_STATE_OK ; +mutex_init (&tsk->futex.exit_mutex); }void futex_exit_recursive (struct task_struct *tsk);
include/linux/futex_types.h new file +36 -0
@@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_FUTEX_TYPES_H +#define _LINUX_FUTEX_TYPES_H + +#ifdef CONFIG_FUTEX +#include <linux/mutex_types.h> +#include <linux/types.h> + +struct compat_robust_list_head ; +struct futex_pi_state ; +struct robust_list_head ; + +/** +* struct futex_sched_data - Futex related per task data +* @robust_list: User space registered robust list pointer +* @compat_robust_list: User space registered robust list pointer for compat tasks +* @pi_state_list: List head for Priority Inheritance (PI) state management +* @pi_state_cache: Pointer to cache one PI state object per task +* @exit_mutex: Mutex for serializing exit +* @state: Futex handling state to handle exit races correctly +*/ +struct futex_sched_data { +struct robust_list_head __user *robust_list; +#ifdef CONFIG_COMPAT +struct compat_robust_list_head __user *compat_robust_list; +#endif +struct list_head pi_state_list; +struct futex_pi_state *pi_state_cache; +struct mutex exit_mutex; +unsigned int state; +}; +#else +struct futex_sched_data { }; +#endif /* !CONFIG_FUTEX */ + +#endif /* _LINUX_FUTEX_TYPES_H */
include/linux/sched.h +4 -12
@@ -16,6 +16,7 @@#include <linux/cpumask_types.h> #include <linux/cache.h> +#include <linux/futex_types.h> #include <linux/irqflags_types.h> #include <linux/smp_types.h> #include <linux/pid_types.h>
@@ -64,7 +65,6 @@ struct bpf_net_context;struct capture_control ;struct cfs_rq ;struct fs_struct ; -struct futex_pi_state ;struct io_context ;struct io_uring_task ;struct mempolicy ;
@@ -76,7 +76,6 @@ struct pid_namespace;struct pipe_inode_info ;struct rcu_node ;struct reclaim_state ; -struct robust_list_head ;struct root_domain ;struct rq ;struct sched_attr ;
@@ -1329,16 +1328,9 @@ struct task_struct {u32 closid;u32 rmid;#endif -#ifdef CONFIG_FUTEX -struct robust_list_head __user *robust_list; -#ifdef CONFIG_COMPAT -struct compat_robust_list_head __user *compat_robust_list; -#endif -struct list_head pi_state_list; -struct futex_pi_state *pi_state_cache; -struct mutex futex_exit_mutex; -unsigned int futex_state; -#endif + +struct futex_sched_data futex; +#ifdef CONFIG_PERF_EVENTS u8 perf_recursion[PERF_NR_CONTEXTS ];struct perf_event_context *perf_event_ctxp;
kernel/exit.c +2 -2
@@ -989,8 +989,8 @@ void __noreturn do_exit(long code)proc_exit_connector (tsk);mpol_put_task_policy (tsk);#ifdef CONFIG_FUTEX -if (unlikely (current->pi_state_cache)) -kfree (current->pi_state_cache); +if (unlikely (current->futex.pi_state_cache)) +kfree (current->futex.pi_state_cache);#endif /* * Make sure we are holding no locks:
kernel/futex/core.c +28 -27
146 lines not on this page · raw message
kernel/futex/pi.c +13 -13
@@ -14,7 +14,7 @@ int refill_pi_state_cache(void) {struct futex_pi_state *pi_state; -if (likely (current->pi_state_cache)) +if (likely (current->futex.pi_state_cache))return 0 ; pi_state =kzalloc_obj (*pi_state);
@@ -28,17 +28,17 @@ int refill_pi_state_cache(void)refcount_set (&pi_state->refcount,1 ); pi_state->key =FUTEX_KEY_INIT ; - current->pi_state_cache = pi_state; + current->futex.pi_state_cache = pi_state;return 0 ; }static struct futex_pi_state *alloc_pi_state (void ) { -struct futex_pi_state *pi_state = current->pi_state_cache; +struct futex_pi_state *pi_state = current->futex.pi_state_cache;WARN_ON (!pi_state); - current->pi_state_cache =NULL ; + current->futex.pi_state_cache =NULL ;return pi_state; }
@@ -60,7 +60,7 @@ static void pi_state_update_owner(structif (new_owner) {raw_spin_lock (&new_owner->pi_lock);WARN_ON (!list_empty (&pi_state->list)); -list_add (&pi_state->list, &new_owner->pi_state_list); +list_add (&pi_state->list, &new_owner->futex.pi_state_list); pi_state->owner = new_owner;raw_spin_unlock (&new_owner->pi_lock); }
@@ -96,7 +96,7 @@ void put_pi_state(struct futex_pi_stateraw_spin_unlock_irqrestore (&pi_state->pi_mutex.wait_lock, flags); } -if (current->pi_state_cache) { +if (current->futex.pi_state_cache) {kfree (pi_state); }else {/*
@@ -106,7 +106,7 @@ void put_pi_state(struct futex_pi_state*/ pi_state->owner =NULL ;refcount_set (&pi_state->refcount,1 ); - current->pi_state_cache = pi_state; + current->futex.pi_state_cache = pi_state; } }
@@ -179,7 +179,7 @@ void put_pi_state(struct futex_pi_state* * p->pi_lock: * -* p->pi_state_list -> pi_state->list, relation +* p->futex.pi_state_list -> pi_state->list, relation * pi_mutex->owner -> pi_state->owner, relation * * pi_state->refcount:
@@ -327,7 +327,7 @@ static int handle_exit_race(u32 __user ** 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 ) +if (tsk && tsk->futex.state !=FUTEX_STATE_DEAD )return -EBUSY ;/*
@@ -346,8 +346,8 @@ static int handle_exit_race(u32 __user ** *uaddr = 0xC0000000; tsk = get_task(PID); * } if (!tsk->flags & PF_EXITING) { * ... attach(); -* tsk->futex_state = } else { -* FUTEX_STATE_DEAD; if (tsk->futex_state != +* tsk->futex.state = } else { +* FUTEX_STATE_DEAD; if (tsk->futex.state != * FUTEX_STATE_DEAD) * return -EAGAIN; * return -ESRCH; <--- FAIL
@@ -395,7 +395,7 @@ static void __attach_to_pi_owner(struct pi_state->key = *key;WARN_ON (!list_empty (&pi_state->list)); -list_add (&pi_state->list, &p->pi_state_list); +list_add (&pi_state->list, &p->futex.pi_state_list);/* * Assignment without holding pi_state->pi_mutex.wait_lock is safe * because there is no concurrency as the object is not published yet.
@@ -439,7 +439,7 @@ static int attach_to_pi_owner(u32 __user* in futex_exit_release(), we do this protected by p->pi_lock: */ raw_spin_lock_irq (&p->pi_lock); -if (unlikely (p->futex_state !=FUTEX_STATE_OK )) { +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
kernel/futex/syscalls.c +8 -15
@@ -25,17 +25,13 @@* @head: pointer to the list-head * @len: length of the list-head, as userspace expects */ -SYSCALL_DEFINE2(set_robust_list, struct robust_list_head __user *, head, - size_t, len) +SYSCALL_DEFINE2(set_robust_list, struct robust_list_head __user *, head, size_t, len) { -/* -* The kernel knows only one size for now: -*/ +/* The kernel knows only one size for now. */ if (unlikely (len !=sizeof (*head)))return -EINVAL ; - current->robust_list = head; - + current->futex.robust_list = head;return 0 ; }
@@ -43,9 +39,9 @@ static inline void __user *futex_task_ro {#ifdef CONFIG_COMPAT if (compat) -return p->compat_robust_list; +return p->futex.compat_robust_list;#endif -return p->robust_list; +return p->futex.robust_list; }static void __user *futex_get_robust_list_common (int pid,bool compat)
@@ -467,15 +463,13 @@ SYSCALL_DEFINE4(futex_requeue, }#ifdef CONFIG_COMPAT -COMPAT_SYSCALL_DEFINE2 (set_robust_list, - struct compat_robust_list_head __user *, head, - compat_size_t, len) +COMPAT_SYSCALL_DEFINE2(set_robust_list, struct compat_robust_list_head __user *, head, + compat_size_t, len) {if (unlikely (len !=sizeof (*head)))return -EINVAL ; - current->compat_robust_list = head; - + current->futex.compat_robust_list = head;return 0 ; }
@@ -515,4 +509,3 @@ SYSCALL_DEFINE6(futex_time32, u32 __user return do_futex(uaddr, op, val, tp, uaddr2, (unsigned long)utime, val3); }#endif /* CONFIG_COMPAT_32BIT_TIME */ -
Re: [patch v2 01/11] futex: Move futex task related data into a struct
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 7 lines
Having all these members in
task_structalong with the required #ifdeffery is annoying, does not allow efficient initializing of the data withmemset()and makes extending it tedious.Move it into a data structure and fix up all usage sites.
Signed-off-by Thomas Gleixner <[email protected]>
Thomas Gleixner
maintainerPATCH v2 2/11 futex: Move futex related mm_struct data into a struct
In reply to Thomas GleixnerHaving all these members in mm_struct along with the required #ifdeffery is annoying, does not allow efficient initializing of the data with memset() and makes extending it tedious.
Move it into a data structure and fix up all usage sites.
--- include/linux/futex_types.h | 24 ++++++++ include/linux/mm_types.h | 11 --- kernel/futex/core.c | 123 ++++++++++++++++++++------------------------ 3 files changed, 82 insertions(+), 76 deletions(-)
include/linux/futex_types.h +24 -0
@@ -29,8 +29,32 @@ struct futex_sched_data {struct mutex exit_mutex;unsigned int state; }; + +/** +* struct futex_mm_data - Futex related per MM data +* @phash_lock: Mutex to protect the private hash operations +* @phash: RCU managed pointer to the private hash +* @phash_new: Pointer to a newly allocated private hash +* @phash_batches: Batch state for RCU synchronization +* @phash_rcu: RCU head for call_rcu() +* @phash_atomic: Aggregate value for @phash_ref +* @phash_ref: Per CPU reference counter for a private hash +*/ +struct futex_mm_data { +#ifdef CONFIG_FUTEX_PRIVATE_HASH +struct mutex phash_lock; +struct futex_private_hash __rcu *phash; +struct futex_private_hash *phash_new; +unsigned long phash_batches; +struct rcu_head phash_rcu; +atomic_long_t phash_atomic; +unsigned int __percpu *phash_ref; +#endif +}; +#else struct futex_sched_data { }; +struct futex_mm_data { };#endif /* !CONFIG_FUTEX */ #endif /* _LINUX_FUTEX_TYPES_H */
include/linux/mm_types.h +1 -10
@@ -1221,16 +1221,7 @@ struct mm_struct {*/ seqcount_t mm_lock_seq;#endif -#ifdef CONFIG_FUTEX_PRIVATE_HASH -struct mutex futex_hash_lock; -struct futex_private_hash __rcu *futex_phash; -struct futex_private_hash *futex_phash_new; -/* futex-ref */ -unsigned long futex_batches; -struct rcu_head futex_rcu; -atomic_long_t futex_atomic; -unsigned int __percpu *futex_ref; -#endif +struct futex_mm_data futex;unsigned long hiwater_rss;/* High-watermark of RSS usage */ unsigned long hiwater_vm;/* High-water virtual memory usage */
kernel/futex/core.c +57 -66
341 lines not on this page · raw message
Re: [patch v2 02/11] futex: Move futex related mm_struct data into a struct
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 7 lines
Having all these members in
mm_structalong with the required #ifdeffery is annoying, does not allow efficient initializing of the data withmemset()and makes extending it tedious.Move it into a data structure and fix up all usage sites.
Signed-off-by Thomas Gleixner <[email protected]>
Thomas Gleixner
maintainerPATCH v2 3/11 futex: Provide UABI defines for robust list entry modifiers
In reply to Thomas GleixnerThe marker for PI futexes in the robust list is a hardcoded 0x1 which lacks any sensible form of documentation.
Provide proper defines for the bit and the mask and fix up the usage sites. Thereby convert the boolean pi argument into a modifier argument, which allows new modifier bits to be trivially added and conveyed.
--- include/uapi/linux/futex.h | 4 +++ kernel/futex/core.c | 53 +++++++++++++++++++++------------------------ 2 files changed, 29 insertions(+), 28 deletions(-)
include/uapi/linux/futex.h +4 -0
@@ -177,6 +177,10 @@ struct robust_list_head {*/ #define ROBUST_LIST_LIMIT 2048 +/* Modifiers for robust_list_head::list_op_pending */ +#define FUTEX_ROBUST_MOD_PI (0x1UL) +#define FUTEX_ROBUST_MOD_MASK (FUTEX_ROBUST_MOD_PI) +/* * bitset with all bits set for the FUTEX_xxx_BITSET OPs to request a * match of any bit.
kernel/futex/core.c +25 -28
177 lines not on this page · raw message
Re: [patch v2 03/11] futex: Provide UABI defines for robust list entry modifiers
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 8 lines
The marker for PI futexes in the robust list is a hardcoded
0x1which lacks any sensible form of documentation.Provide proper defines for the bit and the mask and fix up the usage sites. Thereby convert the boolean pi argument into a modifier argument, which allows new modifier bits to be trivially added and conveyed.
Signed-off-by Thomas Gleixner <[email protected]>
Thomas Gleixner
maintainerPATCH v2 4/11 uaccess: Provide unsafe_atomic_store_release_user()
In reply to Thomas GleixnerThe upcoming support for unlocking robust futexes in the kernel requires store release semantics. Syscalls do not imply memory ordering on all architectures so the unlock operation requires a barrier.
This barrier can be avoided when stores imply release like on x86.
Provide a generic version with a smp_mb() before the unsafe_put_user(), which can be overridden by architectures.
Provide also a ARCH_STORE_IMPLIES_RELEASE Kconfig option, which can be selected by architectures where store implies release, so that the smp_mb() in the generic implementation can be avoided.
---
V2: New patch
--- arch/Kconfig | 4 ++++ include/linux/uaccess.h | 9 +++++++++ 2 files changed, 13 insertions(+)
arch/Kconfig +4 -0
@@ -403,6 +403,10 @@ config ARCH_32BIT_OFF_T config ARCH_32BIT_USTAT_F_TINODE bool +# Selected by architectures when plain stores have release semantics +config ARCH_STORE_IMPLIES_RELEASE + bool + config HAVE_ASM_MODVERSIONS bool help
include/linux/uaccess.h +9 -0
@@ -644,6 +644,15 @@ static inline void user_access_restore(u#define user_read_access_end user_access_end#endif +#ifndef unsafe_atomic_store_release_user +# define unsafe_atomic_store_release_user (val, uptr, elbl) \ + do { \ + if (!IS_ENABLED(CONFIG_ARCH_STORE_IMPLIES_RELEASE)) \ + smp_mb(); \ + unsafe_put_user(val, uptr, elbl); \ + } while (0) +#endif +/* Define RW variant so the below _mode macro expansion works */ #define masked_user_rw_access_begin (u) masked_user_access_begin(u)#define user_rw_access_begin (u, s) user_access_begin(u, s)
Peter Zijlstra
maintainerRe: [patch v2 04/11] uaccess: Provide unsafe_atomic_store_release_user()
Quoted 21 lines · Thomas Gleixner
arch/Kconfig +4
-0@@ -403,6 +403,10 @@ config ARCH_32BIT_OFF_T config ARCH_32BIT_USTAT_F_TINODE bool +# Selected by architectures when plain stores have release semantics +config ARCH_STORE_IMPLIES_RELEASE + bool + config HAVE_ASM_MODVERSIONS bool helpinclude/linux/uaccess.h +2
-0@@ -644,6 +644,15 @@ static inline void user_access_restore(u#define user_read_access_end user_access_end#endif +#ifndef unsafe_atomic_store_release_user +# define unsafe_atomic_store_release_user (val, uptr, elbl) \
include/linux/uaccess.h static inline void user_access_restore(u · line 658
Quoted 10 lines
include/linux/uaccess.h +7
-0+do { \ +if (!IS_ENABLED (CONFIG_ARCH_STORE_IMPLIES_RELEASE )) \ +smp_mb (); \ +unsafe_put_user (val, uptr, elbl); \ + } while (0) +#endif +/* Define RW variant so the below _mode macro expansion works */ #define masked_user_rw_access_begin (u) masked_user_access_begin(u)#define user_rw_access_begin (u, s) user_access_begin(u, s)
Looking at this again after a sleep; does it make sense to rename this config symbol to something like ARCH_MEMORY_ORDER_TSO or somesuch?
I mean, this is only going to be the 3 TSO architectures (x86, s390 and sparc64) setting this anyway, might as well make a little more generic config symbol for this.
OTOH, its easy enough to rename the config thing if it ever is needed elsewhere I suppose.
Thomas Gleixner
maintainerRe: [patch v2 04/11] uaccess: Provide unsafe_atomic_store_release_user()
Quoted 8 lines · Peter Zijlstra
Quoted 3 lines · Thomas Gleixner
/* Define RW variant so the below
_modemacro expansion works */
#definemasked_user_rw_access_begin(u)masked_user_access_begin(u)
#defineuser_rw_access_begin(u, s)user_access_begin(u, s)Looking at this again after a sleep; does it make sense to rename this config symbol to something like
ARCH_MEMORY_ORDER_TSOor somesuch?
Quoted 6 lines
I mean, this is only going to be the 3 TSO architectures (x86, s390 and sparc64) setting this anyway, might as well make a little more generic config symbol for this.
OTOH, its easy enough to rename the config thing if it ever is needed elsewhere I suppose.
Nah. TSO makes sense and is more useful.
Re: [patch v2 04/11] uaccess: Provide unsafe_atomic_store_release_user()
In reply to Thomas GleixnerEm 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 6 lines
The upcoming support for unlocking robust futexes in the kernel requires store release semantics. Syscalls do not imply memory ordering on all architectures so the unlock operation requires a barrier.
This barrier can be avoided when stores imply release like on x86.
Quoted 8 lines
Provide a generic version with a
smp_mb()before theunsafe_put_user(), which can be overridden by architectures.Provide also a
ARCH_STORE_IMPLIES_RELEASEKconfigoption, which can be selected by architectures where store implies release, so that thesmp_mb()in the generic implementation can be avoided.Signed-off-by Thomas Gleixner <[email protected]>
Thomas Gleixner
maintainerPATCH v2 5/11 x86: Select ARCH_STORE_IMPLIES_RELEASE
In reply to Thomas GleixnerThe generic unsafe_atomic_store_release_user() implementation does:
if (!IS_ENABLED(CONFIG_ARCH_STORE_IMPLIES_RELEASE))
smp_mb();
unsafe_put_user();As stores on x86 imply release, select ARCH_STORE_IMPLIES_RELEASE to avoid the unnecessary smp_mb().
---
V2: New patch
--- arch/x86/Kconfig | 1 + 1 file changed, 1 insertion(+)
arch/x86/Kconfig +1 -0
@@ -119,6 +119,7 @@ config X86 select ARCH_MIGHT_HAVE_PC_PARPORT select ARCH_MIGHT_HAVE_PC_SERIO select ARCH_STACKWALK + select ARCH_STORE_IMPLIES_RELEASE select ARCH_SUPPORTS_ACPI select ARCH_SUPPORTS_ATOMIC_RMW select ARCH_SUPPORTS_DEBUG_PAGEALLOC
Re: [patch v2 05/11] x86: Select ARCH_STORE_IMPLIES_RELEASE
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 10 lines
The generic
unsafe_atomic_store_release_user()implementation does:if (!IS_ENABLED(CONFIG_ARCH_STORE_IMPLIES_RELEASE)) smp_mb(); unsafe_put_user();As stores on x86 imply release, select
ARCH_STORE_IMPLIES_RELEASEto avoid the unnecessarysmp_mb().Signed-off-by Thomas Gleixner <[email protected]>
Thomas Gleixner
maintainerPATCH v2 6/11 futex: Cleanup UAPI defines
In reply to Thomas GleixnerMake the operand defines tabular for readability sake.
---
V2: New patch
--- include/uapi/linux/futex.h | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-)
include/uapi/linux/futex.h +13 -14
@@ -25,23 +25,22 @@#define FUTEX_PRIVATE_FLAG 128#define FUTEX_CLOCK_REALTIME 256 -#define FUTEX_CMD_MASK ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME) -#define FUTEX_WAIT_PRIVATE (FUTEX_WAIT | FUTEX_PRIVATE_FLAG) -#define FUTEX_WAKE_PRIVATE (FUTEX_WAKE | FUTEX_PRIVATE_FLAG) -#define FUTEX_REQUEUE_PRIVATE (FUTEX_REQUEUE | FUTEX_PRIVATE_FLAG) -#define FUTEX_CMP_REQUEUE_PRIVATE (FUTEX_CMP_REQUEUE | FUTEX_PRIVATE_FLAG) -#define FUTEX_WAKE_OP_PRIVATE (FUTEX_WAKE_OP | FUTEX_PRIVATE_FLAG) -#define FUTEX_LOCK_PI_PRIVATE (FUTEX_LOCK_PI | FUTEX_PRIVATE_FLAG) -#define FUTEX_LOCK_PI2_PRIVATE (FUTEX_LOCK_PI2 | FUTEX_PRIVATE_FLAG) -#define FUTEX_UNLOCK_PI_PRIVATE (FUTEX_UNLOCK_PI | FUTEX_PRIVATE_FLAG) -#define FUTEX_TRYLOCK_PI_PRIVATE (FUTEX_TRYLOCK_PI | FUTEX_PRIVATE_FLAG) +#define FUTEX_CMD_MASK ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME) + +#define FUTEX_WAIT_PRIVATE (FUTEX_WAIT | FUTEX_PRIVATE_FLAG) +#define FUTEX_WAKE_PRIVATE (FUTEX_WAKE | FUTEX_PRIVATE_FLAG) +#define FUTEX_REQUEUE_PRIVATE (FUTEX_REQUEUE | FUTEX_PRIVATE_FLAG) +#define FUTEX_CMP_REQUEUE_PRIVATE (FUTEX_CMP_REQUEUE | FUTEX_PRIVATE_FLAG) +#define FUTEX_WAKE_OP_PRIVATE (FUTEX_WAKE_OP | FUTEX_PRIVATE_FLAG) +#define FUTEX_LOCK_PI_PRIVATE (FUTEX_LOCK_PI | FUTEX_PRIVATE_FLAG) +#define FUTEX_LOCK_PI2_PRIVATE (FUTEX_LOCK_PI2 | FUTEX_PRIVATE_FLAG) +#define FUTEX_UNLOCK_PI_PRIVATE (FUTEX_UNLOCK_PI | FUTEX_PRIVATE_FLAG) +#define FUTEX_TRYLOCK_PI_PRIVATE (FUTEX_TRYLOCK_PI | FUTEX_PRIVATE_FLAG)#define FUTEX_WAIT_BITSET_PRIVATE (FUTEX_WAIT_BITSET | FUTEX_PRIVATE_FLAG)#define FUTEX_WAKE_BITSET_PRIVATE (FUTEX_WAKE_BITSET | 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) +#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)/* * Flags for futex2 syscalls.
Re: [patch v2 06/11] futex: Cleanup UAPI defines
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 3 lines
Make the operand defines tabular for readability sake.
Signed-off-by Thomas Gleixner <[email protected]>
Thomas Gleixner
maintainerPATCH v2 7/11 futex: Add support for unlocking robust futexes
In reply to Thomas GleixnerUnlocking 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 the kernel. This argument is only evaluated when the FUTEX_ROBUST_UNLOCK bit is set and is therefore backward compatible.
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.
---
V2: Use store release for unlock - Andre, Peter
Use a separate FLAG for 32bit lists - Florian
Add command defines
--- include/uapi/linux/futex.h | 29 +++++++++++++++++++++++- io_uring/futex.c | 2 - kernel/futex/core.c | 53 +++++++++++++++++++++++++++++++++++++++++++-- kernel/futex/futex.h | 15 +++++++++++- kernel/futex/pi.c | 15 +++++++++++- kernel/futex/syscalls.c | 13 ++++++++--- kernel/futex/waitwake.c | 30 +++++++++++++++++++++++-- 7 files changed, 144 insertions(+), 13 deletions(-)
include/uapi/linux/futex.h +28 -1
@@ -25,8 +25,11 @@#define FUTEX_PRIVATE_FLAG 128#define FUTEX_CLOCK_REALTIME 256 +#define FUTEX_UNLOCK_ROBUST 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_UNLOCK_ROBUST | FUTEX_ROBUST_LIST32)#define FUTEX_WAIT_PRIVATE (FUTEX_WAIT | FUTEX_PRIVATE_FLAG)#define FUTEX_WAKE_PRIVATE (FUTEX_WAKE | FUTEX_PRIVATE_FLAG)
@@ -43,6 +46,30 @@#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_UNLOCK_ROBUST) +#define FUTEX_UNLOCK_PI_LIST64_PRIVATE (FUTEX_UNLOCK_PI_LIST64 | FUTEX_PRIVATE_FLAG) +#define FUTEX_UNLOCK_PI_LIST32 (FUTEX_UNLOCK_PI | FUTEX_UNLOCK_ROBUST | \ + FUTEX_ROBUST_LIST32) +#define FUTEX_UNLOCK_PI_LIST32_PRIVATE (FUTEX_UNLOCK_PI_LIST32 | FUTEX_PRIVATE_FLAG) + +#define FUTEX_UNLOCK_WAKE_LIST64 (FUTEX_WAKE | FUTEX_UNLOCK_ROBUST) +#define FUTEX_UNLOCK_WAKE_LIST64_PRIVATE (FUTEX_UNLOCK_LIST64 | FUTEX_PRIVATE_FLAG) + +#define FUTEX_UNLOCK_WAKE_LIST32 (FUTEX_WAKE | FUTEX_UNLOCK_ROBUST | \ + FUTEX_ROBUST_LIST32) +#define FUTEX_UNLOCK_WAKE_LIST32_PRIVATE (FUTEX_UNLOCK_LIST32 | FUTEX_PRIVATE_FLAG) + +#define FUTEX_UNLOCK_BITSET_LIST64 (FUTEX_WAKE_BITSET | FUTEX_UNLOCK_ROBUST) +#define FUTEX_UNLOCK_BITSET_LIST64_PRIVATE (FUTEX_UNLOCK_BITSET_LIST64 | FUTEX_PRIVATE_FLAG) + +#define FUTEX_UNLOCK_BITSET_LIST32 (FUTEX_WAKE_BITSET | FUTEX_UNLOCK_ROBUST | \ + FUTEX_ROBUST_LIST32) +#define FUTEX_UNLOCK_BITSET_LIST32_PRIVATE (FUTEX_UNLOCK_BITSET_LIST32 | FUTEX_PRIVATE_FLAG) + +/* * Flags for futex2 syscalls. * * NOTE: these are not pure flags, they can also be seen as:
io_uring/futex.c +1 -1
@@ -325,7 +325,7 @@ int io_futex_wake(struct io_kiocb *req,* 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);
kernel/futex/core.c +51 -2
83 lines not on this page · raw message
kernel/futex/futex.h +13 -2
@@ -40,6 +40,8 @@#define FLAGS_NUMA 0x0080#define FLAGS_STRICT 0x0100#define FLAGS_MPOL 0x0200 +#define FLAGS_UNLOCK_ROBUST 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_flagif (op &FUTEX_CLOCK_REALTIME ) flags |=FLAGS_CLOCKRT ; +if (op &FUTEX_UNLOCK_ROBUST ) + flags |=FLAGS_UNLOCK_ROBUST ; + +if (op &FUTEX_ROBUST_LIST32 ) + flags |=FLAGS_ROBUST_LIST32 ; +return flags; }
@@ -438,13 +446,16 @@ extern int futex_unqueue_multiple(structextern 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 */
kernel/futex/pi.c +13 -2
@@ -1129,7 +1129,7 @@ int futex_lock_pi(u32 __user *uaddr, uns* 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 ;
@@ -1138,7 +1138,6 @@ int futex_unlock_pi(u32 __user *uaddr, uif (!IS_ENABLED (CONFIG_FUTEX_PI ))return -ENOSYS ; - retry:if (get_user (uval, uaddr))return -EFAULT ;
@@ -1292,3 +1291,15 @@ int futex_unlock_pi(u32 __user *uaddr, ureturn 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_UNLOCK_ROBUST )) +return ret; + +if (!futex_robust_list_clear_pending (pop, flags)) +return -EFAULT ; + +return 0 ; +}
kernel/futex/syscalls.c +10 -3
@@ -118,6 +118,13 @@ long do_futex(u32 __user *uaddr, int op,return -ENOSYS ; } +if (flags &FLAGS_UNLOCK_ROBUST ) { +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, 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,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); }/*
kernel/futex/waitwake.c +28 -2
@@ -150,12 +150,35 @@ void futex_wake_mark(struct wake_q_head }/* +* 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_UNLOCK_ROBUST )) +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, unsignif (unlikely (ret !=0 ))return ret; +if (!futex_robust_unlock (uaddr, flags, pop)) +return -EFAULT ; +if ((flags &FLAGS_STRICT ) && !nr_wake)return 0 ;
Re: [patch v2 07/11] futex: Add support for unlocking robust futexes
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 5 lines
The
sys_futex()case [ab]uses the @uaddr2 argument to hand the pointer to the kernel. This argument is only evaluated when theFUTEX_ROBUST_UNLOCKbit is set and is therefore backward compatible.
I didn't find anywhere in the commit message that says what this pointers points to, so I would add:
"@uaddr2 argument to hand the address of robust list pending op to the kernel"
and also explain why we can't use current->futex.robust_list->list_op_pending (if I understood it correctly why):
"Instead of using the list_op_pending address found at current->futex.robust_list, use the address explicitly set by the user in the syscall arguments to avoid racing with set_robust_list()"
Anyway, this is
Thomas Gleixner
maintainerRe: [patch v2 07/11] futex: Add support for unlocking robust futexes
Quoted 11 lines · André Almeida
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 5 lines
The
sys_futex()case [ab]uses the @uaddr2 argument to hand the pointer to the kernel. This argument is only evaluated when theFUTEX_ROBUST_UNLOCKbit is set and is therefore backward compatible.I didn't find anywhere in the commit message that says what this pointers points to, so I would add:
"@uaddr2 argument to hand the address of robust list pending op to the kernel"
Right.
Quoted 7 lines
and also explain why we can't use
current->futex.robust_list->list_op_pending(if I understood it correctly why):"Instead of using the
list_op_pendingaddress found atcurrent->futex.robust_list, use the address explicitly set by the user in the syscall arguments to avoid racing withset_robust_list()"
No. The task can't be in the futex syscall and update the robust list pointer concurrently. :)
The reason is to avoid the lookup of the robust list pointer and retrieving the pending op pointer. User space has the pointer already there so it can just put it into the @uaddr2 argument. Aside of that this allows the usage of multiple robust lists in the future w/o any changes to the internal functions as they just operate on the supplied pointer. Only in the unresolvable fault case the kernel checks whether there is a matching robust list registered and clears it to avoid further trouble.
I'll amend the change log.
Thanks,
tglx
André Almeida
reviewerRe: [patch v2 07/11] futex: Add support for unlocking robust futexes
Em 26/03/2026 19:23, Thomas Gleixner escreveu:
Quoted 19 lines · André Almeida
Quoted 11 lines
Em 19/03/2026 20:24, Thomas Gleixner escreveu:
Quoted 5 lines
The
sys_futex()case [ab]uses the @uaddr2 argument to hand the pointer to the kernel. This argument is only evaluated when theFUTEX_ROBUST_UNLOCKbit is set and is therefore backward compatible.I didn't find anywhere in the commit message that says what this pointers points to, so I would add:
"@uaddr2 argument to hand the address of robust list pending op to the kernel"
Right.
Quoted 4 lines
and also explain why we can't use
current->futex.robust_list->list_op_pending(if I understood it correctly why):
Quoted 7 lines
Quoted 3 lines
"Instead of using the
list_op_pendingaddress found atcurrent->futex.robust_list, use the address explicitly set by the user in the syscall arguments to avoid racing withset_robust_list()"No. The task can't be in the futex syscall and update the robust list pointer concurrently. :)
Oh, that's right...
Quoted 9 lines
The reason is to avoid the lookup of the robust list pointer and retrieving the pending op pointer. User space has the pointer already there so it can just put it into the @uaddr2 argument. Aside of that this allows the usage of multiple robust lists in the future w/o any changes to the internal functions as they just operate on the supplied pointer. Only in the unresolvable fault case the kernel checks whether there is a matching robust list registered and clears it to avoid further trouble.
Ok, makes sense!
Quoted 2 lines
I'll amend the change log.
Thanks for the clarification.
Quoted 3 lines
Thanks,
tglx
Thomas Gleixner
maintainerPATCH v2 8/11 futex: Add robust futex unlock IP range
In reply to Thomas GleixnerThere will be a VDSO function to unlock robust futexes in user space. The unlock sequence is racy vs. clearing the list_pending_op pointer in the tasks robust list head. To plug this race the kernel needs to know the instruction window. As the VDSO is per MM the addresses are stored in mm_struct::futex.
Architectures which implement support for this have to update these addresses when the VDSO is (re)mapped and indicate the pending op pointer size which is matching the IP.
Arguably this could be resolved by chasing mm->context->vdso->image, but that's architecture specific and requires to touch quite some cache lines. Having it in mm::futex reduces the cache line impact and avoids having yet another set of architecture specific functionality.
To support multi size robust list applications (gaming) this provides two ranges.
---
V2: Store ranges in a struct with size information and allow up to two ranges.
--- include/linux/futex_types.h | 22 ++++++++++++++++++++++ include/linux/mm_types.h | 1 + init/Kconfig | 6 ++++++ 3 files changed, 29 insertions(+)
include/linux/futex_types.h +22 -0
@@ -31,6 +31,20 @@ struct futex_sched_data { };/** +* struct futex_unlock_cs_range - Range for the VDSO unlock critical section +* @start_ip: The start IP of the robust futex unlock critical section (inclusive) +* @end_ip: The end IP of the robust futex unlock critical section (exclusive) +* @pop_size32: Pending OP pointer size indicator. 0 == 64-bit, 1 == 32-bit +*/ +struct futex_unlock_cs_range { +unsigned long start_ip; +unsigned long end_ip; +unsigned int pop_size32; +}; + +#define FUTEX_ROBUST_MAX_CS_RANGES 2 + +/** * struct futex_mm_data - Futex related per MM data * @phash_lock: Mutex to protect the private hash operations * @phash: RCU managed pointer to the private hash
@@ -39,6 +53,10 @@ struct futex_sched_data {* @phash_rcu: RCU head for call_rcu() * @phash_atomic: Aggregate value for @phash_ref * @phash_ref: Per CPU reference counter for a private hash +* +* @unlock_cs_num_ranges: The number of critical section ranges for VDSO assisted unlock +* of robust futexes. +* @unlock_cs_ranges: The critical section ranges for VDSO assisted unlock */ struct futex_mm_data {#ifdef CONFIG_FUTEX_PRIVATE_HASH
@@ -50,6 +68,10 @@ struct futex_mm_data {atomic_long_t phash_atomic;unsigned int __percpu *phash_ref;#endif +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +unsigned int unlock_cs_num_ranges; +struct futex_unlock_cs_range unlock_cs_ranges[FUTEX_ROBUST_MAX_CS_RANGES ]; +#endif };#else
include/linux/mm_types.h +1 -0
@@ -22,6 +22,7 @@#include <linux/types.h> #include <linux/rseq_types.h> #include <linux/bitmap.h> +#include <linux/futex_types.h> #include <asm/mmu.h>
init/Kconfig +6 -0
@@ -1822,6 +1822,12 @@ config FUTEX_MPOL depends on FUTEX && NUMA default y +config HAVE_FUTEX_ROBUST_UNLOCK + bool + +config FUTEX_ROBUST_UNLOCK + def_bool FUTEX && HAVE_GENERIC_VDSO && GENERIC_IRQ_ENTRY && RSEQ && HAVE_FUTEX_ROBUST_UNLOCK + config EPOLL bool "Enable eventpoll support" if EXPERT default y
Peter Zijlstra
maintainerRe: [patch v2 08/11] futex: Add robust futex unlock IP range
include/linux/futex_types.h struct futex_sched_data { · line 45
Quoted 13 lines · Thomas Gleixner
/**
+ * structfutex_unlock_cs_range- Range for the VDSO unlock critical section
+ * @start_ip: The start IP of the robust futex unlock critical section (inclusive)
+ * @end_ip: The end IP of the robust futex unlock critical section (exclusive)
+ * @pop_size32: Pending OP pointer size indicator. 0 == 64-bit, 1 == 32-bit
+ */include/linux/futex_types.h +7
-0+struct futex_unlock_cs_range { +unsigned long start_ip; +unsigned long end_ip; +unsigned int pop_size32; +}; + +#define FUTEX_ROBUST_MAX_CS_RANGES 2
Would it make sense to write that like:
#define FUTEX_ROBUST_MAX_CS_RANGE (1+IS_ENABLED(CONFIG_COMPAT))
Given you only ever use that second entry when COMPAT?
Thomas Gleixner
maintainerRe: [patch v2 08/11] futex: Add robust futex unlock IP range
Quoted 17 lines · Peter Zijlstra
include/linux/futex_types.h struct futex_sched_data { · line 45
Quoted 13 lines · Thomas Gleixner
/**
+ * structfutex_unlock_cs_range- Range for the VDSO unlock critical section
+ * @start_ip: The start IP of the robust futex unlock critical section (inclusive)
+ * @end_ip: The end IP of the robust futex unlock critical section (exclusive)
+ * @pop_size32: Pending OP pointer size indicator. 0 == 64-bit, 1 == 32-bit
+ */include/linux/futex_types.h +7
-0+struct futex_unlock_cs_range { +unsigned long start_ip; +unsigned long end_ip; +unsigned int pop_size32; +}; + +#define FUTEX_ROBUST_MAX_CS_RANGES 2Would it make sense to write that like:
Quoted 3 lines
#define
FUTEX_ROBUST_MAX_CS_RANGE(1+IS_ENABLED(CONFIG_COMPAT))Given you only ever use that second entry when COMPAT?
Indeed!
Re: [patch v2 08/11] futex: Add robust futex unlock IP range
In reply to Thomas GleixnerQuoted 3 lines · Thomas Gleixner
include/linux/futex_types.h +0
-0@@ -31,6 +31,20 @@ struct futex_sched_data {
…
Quoted 8 lines
include/linux/futex_types.h +7
-0+struct futex_unlock_cs_range { +unsigned long start_ip; +unsigned long end_ip; +unsigned int pop_size32; +}; + +#define FUTEX_ROBUST_MAX_CS_RANGES 2
…
include/linux/futex_types.h struct futex_mm_data { · line 75
Quoted 9 lines
include/linux/futex_types.h +4
-0@@ -50,6 +68,10 @@ struct futex_mm_data {atomic_long_t phash_atomic;unsigned int __percpu *phash_ref;#endif +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +unsigned int unlock_cs_num_ranges; +struct futex_unlock_cs_range unlock_cs_ranges[FUTEX_ROBUST_MAX_CS_RANGES ]; +#endif };
While looking at this from an economic point of view, we get:
| unsigned int * phash_ref; /* 80 8 */
| unsigned int unlock_cs_num_ranges; /* 88 4 */
|
| /* XXX 4 bytes hole, try to pack */
|
| struct futex_unlock_cs_range unlock_cs_ranges[2]; /* 96 48 */
|}
|struct futex_unlock_cs_range {
| long unsigned int start_ip; /* 0 8 */
| long unsigned int end_ip; /* 8 8 */
| unsigned int pop_size32; /* 16 4 */
|
| /* size: 24, cachelines: 1, members: 3 */
| /* padding: 4 */
| /* last cacheline: 24 bytes */
|};end_ip could be replaced with a u16 size. There is no need to have pop_size32 as u32, it could be a u16 filling the gap.
On the other hand, pop_size32 could be passed by the caller since it is known if it is the first or the second member / the 64bit or 32bit case.
unlock_cs_num_ranges could probably go because if start_ip == NULL then there is no mapping since it can't be mapped at 0x0. Worst case would be to check two variables vs NULL.
And if we replace end_ip with size then we could remove it because vdso is known at compile so we should know the size at compile time.
Sebastian
Thomas Gleixner
maintainerRe: [patch v2 08/11] futex: Add robust futex unlock IP range
Quoted 6 lines · Sebastian Andrzej Siewior
On 2026-03-20 00:24:46 [+0100], Thomas Gleixner wrote:
end_ipcould be replaced with a u16 size. There is no need to havepop_size32as u32, it could be a u16 filling the gap.
On the other hand,pop_size32could be passed by the caller since it is known if it is the first or the second member / the 64bit or 32bit case.
That's not a win because
{
unsigned long start;
u?? len;
}will always end up with a hole between the array entries.
Quoted 3 lines
unlock_cs_num_rangescould probably go because ifstart_ip== NULL then there is no mapping since it can't be mapped at0x0. Worst case would be to check two variables vs NULL.
That's correct, but it increases the costs for COMPAT as
if (ip >= r[0].start && ip < r[0].start + r[0].end)
// not taken
return .....;
if (ip >= r[1].start && ip < r[1].start + r[1].end)
// not taken
return .....;IP is > 0, so it needs to do both checks with full evaluation. That can be avoided by initializing the r[N].start with ~0UL, which means the first check
ip >= r[0].start
will be false.
Quoted 2 lines
And if we replace
end_ipwith size then we could remove it because vdso is known at compile so we should know the size at compile time.
No, it's not because the VDSO is a user space build and the kernel relies on vdso2c to convert it to a binary blob, which is mapped to user space and the pre?ap-pended container for extable, alternatives and symbols the kernel needs. And no, the build dependency mess is big enough already, no need to make it worse.
Thanks,
tglx
Thomas Gleixner
maintainerPATCH v2 9/11 futex: Provide infrastructure to plug the non contended robust futex unlock race
In reply to Thomas GleixnerWhen the FUTEX_ROBUST_UNLOCK mechanism is used for unlocking (PI-)futexes, then the unlock sequence in user space looks like this:
1) robust_list_set_op_pending(mutex); 2) robust_list_remove(mutex); lval = gettid(); 3) if (atomic_try_cmpxchg(&mutex->lock, lval, 0)) 4) robust_list_clear_op_pending(); else 5) sys_futex(OP | FUTEX_ROBUST_UNLOCK, ....);
That still leaves a minimal race window between #3 and #4 where the mutex could be acquired by some other task, which observes that it is the last user and:
1) unmaps the mutex memory
2) maps a different file, which ends up covering the same address
When then the original task exits before reaching #5 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 corrupt unrelated data.
On X86 this boils down to this simplified assembly sequence:
mov %esi,%eax // Load TID into EAX
xor %ecx,%ecx // Set ECX to 0
#3 lock cmpxchg %ecx,(%rdi) // Try the TID -> 0 transition
.Lstart:
jnz .Lend
#4 movq %rcx,(%rdx) // Clear list_op_pending
.Lend:If the cmpxchg() succeeds and the task is interrupted before it can clear list_op_pending in the robust list head (#4) and the task crashes in a signal handler or gets killed then it ends up in do_exit() and subsequently in the robust list handling, which then might run into the unmap/map issue described above.
This is only relevant when user space was interrupted and a signal is pending. The fix-up has to be done before signal delivery is attempted because:
1) The signal might be fatal so get_signal() ends up in do_exit()
2) The signal handler might crash or the task is killed before returning from the handler. At that point the instruction pointer in pt_regs is not longer the instruction pointer of the initially interrupted unlock sequence.
The right place to handle this is in __exit_to_user_mode_loop() before invoking arch_do_signal_or_restart() as this covers obviously both scenarios.
As this is only relevant when the task was interrupted in user space, this is tied to RSEQ and the generic entry code as RSEQ keeps track of user space interrupts unconditionally even if the task does not have a RSEQ region installed. That makes the decision very lightweight:
if (current->rseq.user_irq && within(regs, csr->unlock_ip_range))
futex_fixup_robust_unlock(regs, csr);futex_fixup_robust_unlock() then invokes a architecture specific function to returen the pending op pointer or NULL. The function evaluates the register content to decide whether the pending ops pointer in the robust list head needs to be cleared.
Assuming the above unlock sequence, then on x86 this decision is the trivial evaluation of the zero flag:
return regs->eflags & X86_EFLAGS_ZF ? regs->dx : NULL;
Other architectures might need to do more complex evaluations due to LLSC, but the approach is valid in general. The size of the pointer is determined from the matching range struct, which covers both 32-bit and 64-bit builds including COMPAT.
The unlock sequence is going to be placed in the VDSO so that the kernel can keep everything synchronized, especially the register usage. The resulting code sequence for user space is:
if (__vdso_futex_robust_list$SZ_try_unlock(lock, tid, &pending_op) != tid)
err = sys_futex($OP | FUTEX_ROBUST_UNLOCK,....);
Both the VDSO unlock and the kernel side unlock ensure that the pending_op pointer is always cleared when the lock becomes unlocked.
---
V2: Convert to the struct range storage and simplify the fixup logic
--- include/linux/futex.h | 42 +++++++++++++++++++++++++++++++++++++++- include/vdso/futex.h | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ kernel/entry/common.c | 9 +++++--- kernel/futex/core.c | 14 +++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-)
include/linux/futex.h +41 -1
@@ -110,7 +110,47 @@ static inline int futex_hash_allocate_de }static inline int futex_hash_free (struct mm_struct *mm) {return 0 ; }static inline int futex_mm_init (struct mm_struct *mm) {return 0 ; } +#endif /* !CONFIG_FUTEX */ -#endif +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +#include <asm/futex_robust.h> + +void __futex_fixup_robust_unlock (struct pt_regs *regs,struct futex_unlock_cs_range *csr); + +static inline bool futex_within_robust_unlock (struct pt_regs *regs, +struct futex_unlock_cs_range *csr) +{ +unsigned long ip =instruction_pointer (regs); + +return ip >= csr->start_ip && ip < csr->end_ip; +} + +static inline void futex_fixup_robust_unlock (struct pt_regs *regs) +{ +struct futex_unlock_cs_range *csr; + +/* +* Avoid dereferencing current->mm if not returning from interrupt. +* current->rseq.event is going to be used anyway in the exit to user +* code, so bringing it in is not a big deal. +*/ +if (!current->rseq.event.user_irq) +return ; + + csr = current->mm->futex.unlock_cs_ranges; +if (unlikely (futex_within_robust_unlock (regs, csr))) { +__futex_fixup_robust_unlock (regs, csr); +return ; + } + +/* Multi sized robust lists are only supported with CONFIG_COMPAT */ +if (IS_ENABLED (CONFIG_COMPAT ) && current->mm->futex.unlock_cs_num_ranges ==2 ) { +if (unlikely (futex_within_robust_unlock (regs, ++csr))) +__futex_fixup_robust_unlock (regs, csr); + } +} +#else /* CONFIG_FUTEX_ROBUST_UNLOCK */ +static inline void futex_fixup_robust_unlock (struct pt_regs *regs) {} +#endif /* !CONFIG_FUTEX_ROBUST_UNLOCK */ #endif
include/vdso/futex.h new file +52 -0
52 lines not on this page · raw message
kernel/entry/common.c +6 -3
@@ -1,11 +1,12 @@// SPDX-License-Identifier: GPL-2.0 -#include <linux/irq-entry-common.h> -#include <linux/resume_user_mode.h> +#include <linux/futex.h> #include <linux/highmem.h> +#include <linux/irq-entry-common.h> #include <linux/jump_label.h> #include <linux/kmsan.h> #include <linux/livepatch.h> +#include <linux/resume_user_mode.h> #include <linux/tick.h> /* Workaround to allow gradual conversion of architecture code */
@@ -60,8 +61,10 @@ static __always_inline unsigned long __eif (ti_work & _TIF_PATCH_PENDING)klp_update_patch_state (current); -if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL)) +if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL)) { +futex_fixup_robust_unlock (regs);arch_do_signal_or_restart (regs); + }if (ti_work & _TIF_NOTIFY_RESUME)resume_user_mode_work (regs);
kernel/futex/core.c +14 -0
@@ -46,6 +46,8 @@#include <linux/slab.h> #include <linux/vmalloc.h> +#include <vdso/futex.h> +#include "futex.h" #include "../locking/rtmutex_common.h"
@@ -1447,6 +1449,18 @@ bool futex_robust_list_clear_pending(voireturn robust_list_clear_pending (pop); } +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +void __futex_fixup_robust_unlock (struct pt_regs *regs,struct futex_unlock_cs_range *csr) +{ +void __user *pop =arch_futex_robust_unlock_get_pop (regs); + +if (!pop) +return ; + +futex_robust_list_clear_pending (pop, csr->cs_pop_size32 ?FLAGS_ROBUST_LIST32 :0 ); +} +#endif /* CONFIG_FUTEX_ROBUST_UNLOCK */ +static void futex_cleanup (struct task_struct *tsk) {if (unlikely (tsk->futex.robust_list)) {
Thomas Gleixner
maintainerRe: [patch v2 09/11] futex: Provide infrastructure to plug the non contended robust futex unlock race
kernel/futex/core.c bool futex_robust_list_clear_pending(voi · line 1460
Quoted 9 lines · Thomas Gleixner
kernel/futex/core.c +9
-0+#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +void __futex_fixup_robust_unlock (struct pt_regs *regs,struct futex_unlock_cs_range *csr) +{ +void __user *pop =arch_futex_robust_unlock_get_pop (regs); + +if (!pop) +return ; + +futex_robust_list_clear_pending (pop, csr->cs_pop_size32 ?FLAGS_ROBUST_LIST32 :0 );
That needs to be
futex_robust_list_clear_pending(pop, csr->pop_size32 ? FLAGS_ROBUST_LIST32 : 0);
Somehow that fixup was lost on the devel machine. Fixed in the git tree.
Thanks,
tglx
Thomas Gleixner
maintainerPATCH v2 10/11 x86/vdso: Prepare for robust futex unlock support
In reply to Thomas GleixnerThere will be a VDSO function to unlock non-contended robust futexes in user space. The unlock sequence is racy vs. clearing the list_pending_op pointer in the task's robust list head. To plug this race the kernel needs to know the critical section window so it can clear the pointer when the task is interrupted within that race window. The window is determined by labels in the inline assembly.
Add these symbols to the vdso2c generator and use them in the VDSO VMA code to update the critical section addresses in mm_struct::futex on (re)map().
The symbols are not exported to user space, but available in the debug version of the vDSO.
---
V2: Rename the symbols
--- arch/x86/entry/vdso/vma.c | 35 +++++++++++++++++++++++++++++++++++ arch/x86/include/asm/vdso.h | 6 ++++++ arch/x86/tools/vdso2c.c | 20 +++++++++++++------- 3 files changed, 54 insertions(+), 7 deletions(-)
arch/x86/entry/vdso/vma.c +35 -0
@@ -73,6 +73,38 @@ static void vdso_fix_landing(const struc regs->ip = new_vma->vm_start + ipoffset; } +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +static void vdso_futex_robust_unlock_update_ips (void ) +{ +const struct vdso_image *image = current->mm->context.vdso_image; +unsigned long vdso = (unsigned long ) current->mm->context.vdso; +struct futex_mm_data *fd = ¤t->mm->futex; +struct futex_unlock_cs_range *csr = fd->unlock_cs_ranges; + + fd->unlock_cs_num_ranges =0 ; +#ifdef CONFIG_X86_64 +if (image->sym_x86_64_futex_try_unlock_cs_start) { + csr->start_ip = vdso + image->sym_x86_64_futex_try_unlock_cs_start; + csr->end_ip = vdso + image->sym_x86_64_futex_try_unlock_cs_end; + csr->pop_size32 =0 ; + csr++; + fd->unlock_cs_num_ranges++; + } +#endif /* CONFIG_X86_64 */ + +#if defined(CONFIG_X86_32 ) || defined(CONFIG_COMPAT ) +if (image->sym_x86_32_futex_try_unlock_cs_start) { + csr->start_ip = vdso + image->sym_x86_32_futex_try_unlock_cs_start; + csr->end_ip = vdso + image->sym_x86_32_futex_try_unlock_cs_end; + csr->pop_size32 =1 ; + fd->unlock_cs_num_ranges++; + } +#endif /* CONFIG_X86_32 || CONFIG_COMPAT */ +} +#else +static inline void vdso_futex_robust_unlock_update_ips (void ) { } +#endif +static int vdso_mremap (const struct vm_special_mapping *sm,struct vm_area_struct *new_vma) {
@@ -80,6 +112,7 @@ static int vdso_mremap(const struct vm_svdso_fix_landing (image, new_vma); current->mm->context.vdso = (void __user *)new_vma->vm_start; +vdso_futex_robust_unlock_update_ips ();return 0 ; }
@@ -189,6 +222,8 @@ static int map_vdso(const struct vdso_im current->mm->context.vdso = (void __user *)text_start; current->mm->context.vdso_image = image; +vdso_futex_robust_unlock_update_ips (); + up_fail:mmap_write_unlock (mm);return ret;
arch/x86/include/asm/vdso.h +6 -0
@@ -25,6 +25,12 @@ struct vdso_image {long sym_int80_landing_pad;long sym_vdso32_sigreturn_landing_pad;long sym_vdso32_rt_sigreturn_landing_pad; +long sym_x86_64_futex_try_unlock_cs_start; +long sym_x86_64_futex_try_unlock_cs_end; +long sym_x86_64_compat_futex_try_unlock_cs_start; +long sym_x86_64_compat_futex_try_unlock_cs_end; +long sym_x86_32_futex_try_unlock_cs_start; +long sym_x86_32_futex_try_unlock_cs_end; };extern const struct vdso_image vdso64_image;
arch/x86/tools/vdso2c.c +13 -7
@@ -75,13 +75,19 @@ struct vdso_sym { };struct vdso_sym required_syms[] = { - {"VDSO32_NOTE_MASK" , true}, - {"__kernel_vsyscall" , true}, - {"__kernel_sigreturn" , true}, - {"__kernel_rt_sigreturn" , true}, - {"int80_landing_pad" , true}, - {"vdso32_rt_sigreturn_landing_pad" , true}, - {"vdso32_sigreturn_landing_pad" , true}, + {"VDSO32_NOTE_MASK" , true}, + {"__kernel_vsyscall" , true}, + {"__kernel_sigreturn" , true}, + {"__kernel_rt_sigreturn" , true}, + {"int80_landing_pad" , true}, + {"vdso32_rt_sigreturn_landing_pad" , true}, + {"vdso32_sigreturn_landing_pad" , true}, + {"x86_64_futex_try_unlock_cs_start" , true}, + {"x86_64_futex_try_unlock_cs_end" , true}, + {"x86_64_compat_futex_try_unlock_cs_start" , true}, + {"x86_64_compat_futex_try_unlock_cs_end" , true}, + {"x86_32_futex_try_unlock_cs_start" , true}, + {"x86_32_futex_try_unlock_cs_end" , true}, }; __attribute__((format(printf, 1, 2))) __attribute__((noreturn))
Thomas Gleixner
maintainerPATCH v2 11/11 x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Thomas GleixnerWhen the FUTEX_ROBUST_UNLOCK mechanism is used for unlocking (PI-)futexes, then the unlock sequence in userspace looks like this:
1) robust_list_set_op_pending(mutex); 2) robust_list_remove(mutex); lval = gettid(); 3) if (atomic_try_cmpxchg(&mutex->lock, lval, 0)) 4) robust_list_clear_op_pending(); else 5) sys_futex(OP,...FUTEX_ROBUST_UNLOCK);
That still leaves a minimal race window between #3 and #4 where the mutex could be acquired by some other task which observes that it is the last user and:
1) unmaps the mutex memory
2) maps a different file, which ends up covering the same address
When then the original task exits before reaching #5 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 corrupt unrelated data.
Provide a VDSO function which exposes the critical section window in the VDSO symbol table. The resulting addresses are updated in the task's mm when the VDSO is (re)map()'ed.
The core code detects when a task was interrupted within the critical section and is about to deliver a signal. It then invokes an architecture specific function which determines whether the pending op pointer has to be cleared or not. The unlock assembly sequence on 64-bit is:
mov %esi,%eax // Load TID into EAX
xor %ecx,%ecx // Set ECX to 0
lock cmpxchg %ecx,(%rdi) // Try the TID -> 0 transition
.Lstart:
jnz .Lend
movq %rcx,(%rdx) // Clear list_op_pending .Lend:
ret
So the decision can be simply based on the ZF state in regs->flags. The pending op pointer is always in DX independent of the build mode (32/64-bit) to make the pending op pointer retrieval uniform. The size of the pointer is stored in the matching criticial section range struct and the core code retrieves it from there. So the pointer retrieval function does not have to care. It is bit-size independent:
return regs->flags & X86_EFLAGS_ZF ? regs->dx : NULL;
There are two entry points to handle the different robust list pending op pointer size:
__vdso_futex_robust_list64_try_unlock() __vdso_futex_robust_list32_try_unlock()
The 32-bit VDSO provides only __vdso_futex_robust_list32_try_unlock().
The 64-bit VDSO provides always __vdso_futex_robust_list64_try_unlock() and when COMPAT is enabled also the list32 variant, which is required to support multi-size robust list pointers used by gaming emulators.
The unlock function is inspired by an idea from Mathieu Desnoyers.
--
V2: Provide different entry points - Florian
Use __u32 and __x86_64__ - Thomas
Use private labels - Thomas
Optimize assembly - Uros
Split the functions up now that ranges are supported in the core and document the actual assembly.
--- arch/x86/Kconfig | 1 arch/x86/entry/vdso/common/vfutex.c | 76 +++++++++++++++++++++++++++++++ arch/x86/entry/vdso/vdso32/Makefile | 5 +- arch/x86/entry/vdso/vdso32/vdso32.lds.S | 3 + arch/x86/entry/vdso/vdso32/vfutex.c | 1 arch/x86/entry/vdso/vdso64/Makefile | 7 +- arch/x86/entry/vdso/vdso64/vdso64.lds.S | 4 + arch/x86/entry/vdso/vdso64/vdsox32.lds.S | 4 + arch/x86/entry/vdso/vdso64/vfutex.c | 1 arch/x86/include/asm/futex_robust.h | 19 +++++++ 10 files changed, 116 insertions(+), 5 deletions(-)
arch/x86/Kconfig +1 -0
@@ -238,6 +238,7 @@ config X86 select HAVE_EFFICIENT_UNALIGNED_ACCESS select HAVE_EISA if X86_32 select HAVE_EXIT_THREAD + select HAVE_FUTEX_ROBUST_UNLOCK select HAVE_GENERIC_TIF_BITS select HAVE_GUP_FAST select HAVE_FENTRY if X86_64 || DYNAMIC_FTRACE
arch/x86/entry/vdso/common/vfutex.c new file +76 -0
76 lines not on this page · raw message
arch/x86/entry/vdso/vdso32/Makefile +3 -2
@@ -7,8 +7,9 @@ vdsos-y := 32 # Files to link into the vDSO: -vobjs-y := note.o vclock_gettime.o vgetcpu.o -vobjs-y += system_call.o sigreturn.o +vobjs-y := note.o vclock_gettime.o vgetcpu.o +vobjs-y += system_call.o sigreturn.o +vobjs-$(CONFIG_FUTEX_ROBUST_UNLOCK) += vfutex.o # Compilation flags flags-y := -DBUILD_VDSO32 -m32 -mregparm=0
arch/x86/entry/vdso/vdso32/vdso32.lds.S +3 -0
@@ -30,6 +30,9 @@ VERSION __vdso_clock_gettime64; __vdso_clock_getres_time64; __vdso_getcpu; +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK + __vdso_futex_robust_list32_try_unlock; +#endif }; LINUX_2.5 {
arch/x86/entry/vdso/vdso32/vfutex.c new file +1 -0
@@ -0,0 +1 @@ +#include "common/vfutex.c"
arch/x86/entry/vdso/vdso64/Makefile +4 -3
@@ -8,9 +8,10 @@ vdsos-y := 64 vdsos-$(CONFIG_X86_X32_ABI) += x32 # Files to link into the vDSO: -vobjs-y := note.o vclock_gettime.o vgetcpu.o -vobjs-y += vgetrandom.o vgetrandom-chacha.o -vobjs-$(CONFIG_X86_SGX) += vsgx.o +vobjs-y := note.o vclock_gettime.o vgetcpu.o +vobjs-y += vgetrandom.o vgetrandom-chacha.o +vobjs-$(CONFIG_X86_SGX) += vsgx.o +vobjs-$(CONFIG_FUTEX_ROBUST_UNLOCK) += vfutex.o # Compilation flags flags-y := -DBUILD_VDSO64 -m64 -mcmodel=small
arch/x86/entry/vdso/vdso64/vdso64.lds.S +4 -0
@@ -32,6 +32,10 @@ VERSION { #endif getrandom; __vdso_getrandom; +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK + __vdso_futex_robust_list64_try_unlock; + __vdso_futex_robust_list32_try_unlock; +#endif local: *; }; }
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +4 -0
@@ -22,6 +22,10 @@ VERSION { __vdso_getcpu; __vdso_time; __vdso_clock_getres; +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK + __vdso_futex_robust_list64_try_unlock; + __vdso_futex_robust_list32_try_unlock; +#endif local: *; }; }
arch/x86/entry/vdso/vdso64/vfutex.c new file +1 -0
@@ -0,0 +1 @@ +#include "common/vfutex.c"
arch/x86/include/asm/futex_robust.h new file +19 -0
@@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _ASM_X86_FUTEX_ROBUST_H +#define _ASM_X86_FUTEX_ROBUST_H + +#include <asm/ptrace.h> + +static __always_inline void __user *x86_futex_robust_unlock_get_pop(struct pt_regs *regs) +{ +/* +* If ZF is set then the cmpxchg succeeded and the pending op pointer +* needs to be cleared. +*/ +return regs->flags &X86_EFLAGS_ZF ? (void __user *)regs->dx :NULL ; +} + +#define arch_futex_robust_unlock_get_pop (regs) \ + x86_futex_robust_unlock_get_pop(regs) + +#endif /* _ASM_X86_FUTEX_ROBUST_H */
Re: [patch v2 11/11] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 8 lines · Thomas Gleixner
mov %esi,%eax // Load TID into EAX xor %ecx,%ecx // Set ECX to 0 lock cmpxchg %ecx,(%rdi) // Try the TID -> 0 transition.Lstart:
jnz .Lend
movq %rcx,(%rdx) // Clearlist_op_pending
.Lend:
ret
[...]
Quoted 39 lines
+ * Assembly template for the try unlock functions. The basic functionality is:
+ *+ * mov esi, %eax Move the TID into EAX + * xor %ecx, %ecx Clear ECX + * lock_cmpxchgl %ecx, (%rdi) Attempt the TID -> 0 transition + * .Lcs_start: Start of the critical section + * jnz .Lcs_end If cmpxchl failed jump to the end + * .Lcs_success: Start of the success section + * movq %rcx, (%rdx) Set the pending op pointer to 0 + * .Lcs_end: End of the critical section+ *
+ * .Lcs_start and .Lcs_end establish the critical section range. .Lcs_success is
+ * technically not required, but there for illustration, debugging and testing.
+ *
+ * WhenCONFIG_COMPATis enabled then the 64-bit VDSO provides two functions.
+ * One for the regular 64-bit sized pending operation pointer and one for a
+ * 32-bit sized pointer to support gaming emulators.
+ *
+ * The 32-bit VDSO provides only the one for 32-bit sized pointers.
+ */arch/x86/entry/vdso/common/vfutex.c new file +19
-0+#define __stringify_1(x...) #x +#define __stringify(x...) __stringify_1(x) + +#define LABEL (name, which) __stringify(name##_futex_try_unlock_cs_##which:) + +#define JNZ_END (name) "jnz " __stringify(name) "_futex_try_unlock_cs_end\n" + +#define CLEAR_POPQ "movq %[zero], %a[pop]\n" +#define CLEAR_POPL "movl %k[zero], %a[pop]\n" + +#define futex_robust_try_unlock (name, clear_pop, __lock, __tid, __pop) \ +({ \ + asm volatile ( \ + " \n" \ + " lock cmpxchgl %k[zero], %a[lock] \n" \ + " \n" \ + LABEL(name, start) \ + " \n" \ + JNZ_END(name) \
arch/x86/entry/vdso/common/vfutex.c line 53
Quoted 10 lines
arch/x86/entry/vdso/common/vfutex.c new file +10
-0+" \n" \ + LABEL(name, success) \ +" \n" \ + clear_pop \ +" \n" \ +LABEL (name, end) \ + : [tid] "+&a" (__tid) \ +: [lock] " D " (__lock), \ +[pop] " d" (__pop), \ +[zero] " S " (0UL) \
[zero] represents an internal register, so the above constraint can be "r" (*). If it remains a hard register constraint (%rsi) for some reason then the above two comments should be updated to reflect the new constraint.
With the constraint changed to "r":
(*) "r" allows the compiler some more freedom. The compiler tracks the values in registers, so it can reuse zero from an unrelated register without moving it to the %rsi and without clobbering the source register. In non-trivial functions, there is a high chance that needed value is already available in some register.
Uros.
Thomas Gleixner
maintainerRe: [patch v2 11/11] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 7 lines · Uros Bizjak
Quoted 1 line · Thomas Gleixner
+ [zero] "S" (0UL) \
[zero] represents an internal register, so the above constraint can be "r" (*). If it remains a hard register constraint (%rsi) for some reason then the above two comments should be updated to reflect the new constraint.
Right. That's a leftover from some earlier experiment where I needed the 'zero' register at a fixed place, but that's all gone.
Quoted 9 lines
With the constraint changed to "r":
(*) "r" allows the compiler some more freedom. The compiler tracks the values in registers, so it can reuse zero from an unrelated register without moving it to the %rsi and without clobbering the source register. In non-trivial functions, there is a high chance that needed value is already available in some register.
I'm aware of that.
Thanks,
tglx
Thomas Gleixner
maintainerQuoted 3 lines · Thomas Gleixner
If the functionality itself is agreed on we only need to agree on the names and signatures of the functions exposed through the VDSO before we set them in stone. That will hopefully not take another 15 years :)
Have the libc folks any further opinion on the syscall and the vDSO part before I prepare v3?
Thanks,
tglx
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines · Thomas Gleixner
If the functionality itself is agreed on we only need to agree on the names and signatures of the functions exposed through the VDSO before we set them in stone. That will hopefully not take another 15 years :)
Have the libc folks any further opinion on the syscall and the vDSO part before I prepare v3?
This whole conversation has been way too much for me to keep up with, so I'm not sure where it's at right now.
From musl's perspective, the way we make robust mutex unlocking safe right now is by inhibiting munmap/mremap/MAP_FIXED and pthread_mutex_destroy while there are any in-flight robust unlocks. It will be nice to be able to conditionally stop doing that if vdso is available, but I can't see using a fallback that requires a syscall, as that would just be a lot more expensive than what we're doing right now and still not work on older kernels. So I think the only part we're interested in is the fully-userspace approach in vdso.
If it sounds like I have a misconception of the current state of this proposal from what I said above, let me know and I'll try to figure out what I'm missing and catch up.
Rich
André Almeida
reviewerEm 26/03/2026 19:08, Rich Felker escreveu:
Quoted 12 lines · Thomas Gleixner
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines
If the functionality itself is agreed on we only need to agree on the names and signatures of the functions exposed through the VDSO before we set them in stone. That will hopefully not take another 15 years :)
Have the libc folks any further opinion on the syscall and the vDSO part before I prepare v3?
This whole conversation has been way too much for me to keep up with, so I'm not sure where it's at right now.
Quoted 9 lines
From musl's perspective, the way we make robust mutex unlocking safe
right now is by inhibiting munmap/mremap/MAP_FIXED andpthread_mutex_destroywhile there are any in-flight robust unlocks. It will be nice to be able to conditionally stop doing that if vdso is available, but I can't see using a fallback that requires a syscall, as that would just be a lot more expensive than what we're doing right now and still not work on older kernels. So I think the only part we're interested in is the fully-userspace approach in vdso.
You just need the syscall for the contented case (where you would need a syscall anyway for a FUTEX_WAKE).
As Thomas wrote in patch 09/11:
The resulting code sequence for user space is:
if (__vdso_futex_robust_list$SZ_try_unlock(lock, tid, &pending_op) !=
tid)
err = sys_futex($OP | FUTEX_ROBUST_UNLOCK,....);
Both the VDSO unlock and the kernel side unlock ensure that thepending_op pointer is always cleared when the lock becomes unlocked.
So you call the vDSO first. If it fails, it means that the lock is contented and you need to call futex(). It will wake a waiter, release the lock and clean list_op_pending.
Quoted 5 lines
If it sounds like I have a misconception of the current state of this proposal from what I said above, let me know and I'll try to figure out what I'm missing and catch up.
Rich
Thomas Gleixner
maintainerQuoted 34 lines · André Almeida
Em 26/03/2026 19:08, Rich Felker escreveu:
Quoted 12 lines · Thomas Gleixner
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines
If the functionality itself is agreed on we only need to agree on the names and signatures of the functions exposed through the VDSO before we set them in stone. That will hopefully not take another 15 years :)
Have the libc folks any further opinion on the syscall and the vDSO part before I prepare v3?
This whole conversation has been way too much for me to keep up with, so I'm not sure where it's at right now.
Quoted 9 lines
From musl's perspective, the way we make robust mutex unlocking safe
right now is by inhibiting munmap/mremap/MAP_FIXED andpthread_mutex_destroywhile there are any in-flight robust unlocks. It will be nice to be able to conditionally stop doing that if vdso is available, but I can't see using a fallback that requires a syscall, as that would just be a lot more expensive than what we're doing right now and still not work on older kernels. So I think the only part we're interested in is the fully-userspace approach in vdso.You just need the syscall for the contented case (where you would need a syscall anyway for a
FUTEX_WAKE).As Thomas wrote in patch 09/11:
The resulting code sequence for user space is:
if (__vdso_futex_robust_list$SZ_try_unlock(lock, tid, &pending_op) !=tid)
err =sys_futex($OP | FUTEX_ROBUST_UNLOCK,....);
Quoted 7 lines
Both the VDSO unlock and the kernel side unlock ensure that the
pending_oppointer is always cleared when the lock becomes unlocked.So you call the vDSO first. If it fails, it means that the lock is contented and you need to call
futex(). It will wake a waiter, release the lock and cleanlist_op_pending.
See also the V1 cover letter which has a full deep dive:
https://lore.kernel.org/[email protected]
TLDR:
The problem can be split into two issues:
1) Contended unlock
2) Uncontended unlock
#1 is solved by moving the unlock into the kernel instead of unlocking
first and then invoking the syscall to wake waiters. The syscall takes the list_pending_op pointer as an argument and after unlocking, i.e. *lock = 0, it clears the list_pending_op pointer
For this to work, it needs to use try_cmpxchg() like PI unlock does.
#2 The race is between the succesful try_cmpxchg() and the clearing of
the list_pending_op pointer
That's where the VDSO comes into play. Instead of having the try_cmpxchg() in the library code the library invokes the VDSO provided variant. That allows the kernel to check in the signal delivery path whether a successful unlock requires a helping hand to clear the list pending op pointer. If the interrupted IP is in the critical section _and_ the try_cmpxchg() succeeded then the kernel clears the pointer.
In x86 ASM:
0000000000001590 <__vdso_futex_robust_list64_try_unlock@@LINUX_2.6>:
1590: mov %esi,%eax
1592: xor %ecx,%ecx
1594: lock cmpxchg %ecx,(%rdi) // Result goes into ZF
1598: jne 159d <- CS start
159a: mov %rcx,(%rdx) // Clear list_pending_op
159d: ret <- CS end
159e: xchg %ax,%axSo if the kernel observes
IP >= CS start && IP < CS end
then it checks the ZF flag in pt_regs and if set it clears the list_pending op.
Obviously #1 depends on #2 to close all holes.
Thanks,
tglx
Quoted 33 lines · André Almeida
Em 26/03/2026 19:08, Rich Felker escreveu:
Quoted 12 lines · Thomas Gleixner
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines
If the functionality itself is agreed on we only need to agree on the names and signatures of the functions exposed through the VDSO before we set them in stone. That will hopefully not take another 15 years :)
Have the libc folks any further opinion on the syscall and the vDSO part before I prepare v3?
This whole conversation has been way too much for me to keep up with, so I'm not sure where it's at right now.
Quoted 9 lines
From musl's perspective, the way we make robust mutex unlocking safe
right now is by inhibiting munmap/mremap/MAP_FIXED andpthread_mutex_destroywhile there are any in-flight robust unlocks. It will be nice to be able to conditionally stop doing that if vdso is available, but I can't see using a fallback that requires a syscall, as that would just be a lot more expensive than what we're doing right now and still not work on older kernels. So I think the only part we're interested in is the fully-userspace approach in vdso.You just need the syscall for the contented case (where you would need a syscall anyway for a
FUTEX_WAKE).As Thomas wrote in patch 09/11:
The resulting code sequence for user space is:
if (__vdso_futex_robust_list$SZ_try_unlock(lock, tid,
&pending_op) != tid)
err =sys_futex($OP | FUTEX_ROBUST_UNLOCK,....);
Quoted 7 lines
Both the VDSO unlock and the kernel side unlock ensure that the
pending_op
pointer is always cleared when the lock becomes unlocked.So you call the vDSO first. If it fails, it means that the lock is contented and you need to call
futex(). It will wake a waiter, release the lock and cleanlist_op_pending.
So would we use the vdso function presence as signal that this functionality is available? In that case, I think what we would do is:
1. Try an uncontended unlock using the vdso.
2. If it fails, attempt FUTEX_ROBUST_UNLOCK.
3. If that fails (note: this could be due to seccomp!), fallback to the old kernel code path, holding off any munmap/etc. while we perform the userspace unlock.
The path where the vdso function is missing would go straight to 3.
Does this sound right?
Rich
Thomas Gleixner
maintainerQuoted 5 lines · Rich Felker
Quoted 3 lines · André Almeida
So you call the vDSO first. If it fails, it means that the lock is contented and you need to call
futex(). It will wake a waiter, release the lock and cleanlist_op_pending.
Quoted 8 lines
So would we use the vdso function presence as signal that this functionality is available? In that case, I think what we would do is:
1. Try an uncontended unlock using the vdso.
2. If it fails, attemptFUTEX_ROBUST_UNLOCK.
3. If that fails (note: this could be due to seccomp!), fallback to the old kernel code path, holding off any munmap/etc. while we perform the userspace unlock.
FUTEX_ROBUST_UNLOCK is a flag similar to FUTEX_PRIVATE which is or'ed on FUTEX_WAKE, FUTEX_WAKE_BITSET and FUTEX_UNLOCK_PI to tell the kernel that it should do the unlock for FUTEX_WAKE* and the pointer clearing for all three variants. UNLOCK_PI already does the contended unlock today. So yeah, seccomp might refuse, but then it might refuse plain FUTEX_WAKE* too which leaves you in a creek without a paddle.
If the kernel supports ROBUST UNLOCK, but does not expose the VDSO function or lacks VDSO at all, you can still use the syscall for the contended case unlock and limit your user space workaround to the successful uncontended unlock case by using try_cmpxchg() in the library code, which is obviously not covered by the fixup as the kernel does not know about it.
I briefly pondered to allow user space to register the critical section (that's how I evaluated the approach in the first place).
But that's a can of worms we should not open at all because the kernel needs to know the registers used (to retrieve the pending op pointer) and the condition for successful uncontended unlock. Keeping that in sync would be a nightmare. With the VDSO that's not an issue as the kernel can keep the changes synchronized and validate with selftests that it actually is correct.
Thanks,
tglx