futex: Address the robust futex unlock race for real
Subsystems (11)
Thomas Gleixner
maintainercover futex: Address the robust futex unlock race for real
The robust futex unlock mechanism is racy in respect to the clearing of the robust_list_head::list_op_pending pointer as described here:
https://sourceware.org/bugzilla/show_bug.cgi?id=14485
This issue has been ignored for almost 15 years and has been brought up again recently by Andre:
https://lore.kernel.org/lkml/[email protected]/
Florian asked for this to be addressed in a VDSO function and a couple of days ago Mathieu proposed a fix:
https://lore.kernel.org/[email protected]
While the idea of using a VDSO unlock helper function along with kernel side fixups is correct, the proposed solution is incomplete and untested. Aside of that it just glues it somehow into the existing code without taking the larger picture into account.
So I sat down and did a complete analysis. The problem is in the unlock sequences of non-PI and PI futexes.
non-PI futexes do:
1) robust_list_set_op_pending(mutex); 2) robust_list_remove(mutex); lval = 0; 3) atomic_xchg(lock, lval); 4) if (lval & FUTEX_WAITERS) 5) sys_futex(WAKE,....); 6) robust_list_clear_op_pending();
PI futexes do:
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();
In both cases the late clearing of the robust list pending op pointer (#6/#5) can cause the following problem:
Task 1 Task 2
robust_list_set_op_pending(mutex);
unlock(mutex);
--> interrupt which
forces the T1 to exit.
lock(mutex); // Non-contended
// Last user of the mapping
unlock(mutex);
destroy(mutex);
unmap();
map(some other file);
do_exit()
handle_robust_list()
handle_pending_op()
val = mutex->lock;
if (val == TID)
mutex->lock = val | FUTEX_OWNER_DIED;This causes memory corruption in the new mapping when:
1) the new mapping covers the address of mutex->lock
2) the memory contains the TID of the exiting task
Unlikely to happen, but possible.
It does not matter whether the unlock happens in the kernel or whether there are waiters or not as in all scenarios T2 can observe being the last user _before_ T1 manages to clear the pending op pointer.
So the first question to ask is why the unlock sequences of non-PI and PI futexes are different.
The unlock sequence of non-PI predates both PI and robust futexes. The robust handling was added around it. PI must use try_cmpxchg() and cannot unlock a contended futex in user space as that would create inconsistent PI state in the kernel.
But technically there is no requirement to keep the non-PI unlock sequence as it is. It can do the same as PI and let the kernel unlock the futex in the contended case. It has to go into the kernel anyway when there are waiters. That functionality does not exist in the kernel, but it is trivial to add.
With that the unlock sequence of non-PI and PI becomes identical and can be easily extended to let the kernel do the clearing of the pending op pointer as well. So that becomes:
robust_list_set_op_pending(mutex); lval = gettid(); if (atomic_try_cmpxchg(lock, lval, 0)) robust_list_clear_op_pending(); else
sys_futex($OP | FUTEX_ROBUST_UNLOCK, .... op_pointer);
which reduces the problem space significantly. The remaining issue is the non-contended unlock case, which can be handled with a VDSO unlock sequence and kernel side fixup.
But that op pointer has an issue when COMPAT is enabled. To support mixed-mode futexes for gaming emulators the kernel allows a 64-bit application to register both a 64-bit native robust list head and a 32-bit compat robust list head. That means on COMPAT kernels the type of the op pointer is undefined.
That can be solved by marking the pointer size in the pointer itself similar to the way how the robust list pointer marks PI futexes in bit
0. If the bit is set the pointer has to be treated as 32-bit pointer. If not it uses the native size (64-bit). This is required for several 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
3) Having the pointer handed in spares the evaluation of the registered robust lists to figure out whether the futex address is matching the registered [compat_]robust_list_head::list_pending_op pointer.
Which would become even worse when the multi robust list changes Andre is working on get merged.
As a consequence 32-bit applications have to set this bit unconditionally so they can run on a 64-bit kernel in compat mode unmodified. 32-bit kernels return an error code when the bit is not set. 64-bit kernels will happily clear the full 64 bits if user space fails to set it.
Arguably this could be done with different functions and more flags, but there is no benefit for that.
With that addressed there is "only" the non-contended case left.
The important question is when this has to be dealt with. It's only relevant when:
1) the unlocking task was interrupted in the unlock sequence between successful unlock and the store which clears the pointer.
2) the unlocking task needs to handle a signal on return from interrupt to user space
The proper place to check for this is therefore right before the invocation of arch_do_signal_or_restart() in __exit_to_user_mode_loop().
That needs to check whether the task was interrupted and the instruction pointer is within the critical section. On x86 (COMPAT=n) this critical section boils down to:
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 $0x0,(%rdx) // Clear list_op_pending
.Lend:I.e. the critical section is: IP >= .Lstart && IP < .Lend. That's a very light weight range check operation which won't affect signal heavy applications in a noticable way.
If the range check observes IP in the critical section it invokes the fixup function which determines whether the pointer has to be cleared. In the above x86 case this just has to evaluate regs->flag & X86_EFLAGS_ZF.
If ZF is set then the pointer must be cleared. The pointer address is retrieved from regs->dx in this case.
The COMPAT case with different sizes is slightly more complex as it has to do the size checking in the success path by testing bit 0. That obviously modifies ZF too, so it has to check whether IP is within the success region.
mov %esi,%eax // Load TID into EAX mov %rdx,%rsi // Get the op pointer xor %ecx,%ecx // Set ECX to 0 and $0xfffffffffffffffe,%rsi // Clear the size bit lock cmpxchg %ecx,(%rdi) // Try the TID -> 0 transition .Lstart: jnz .Lend .Lsuccess: testl $0x1,(%rdx) // Test the size bit jz .Lop64 // Not set: 64-bit movl $0x0,(%rsi) // Clear 32-bit jmp .Lend .Lop64: movq $0x0,(%rsi) // Clear 64-bit
.Lend:
ret
Again it could be argued to do that with two different fixup functions, but there is no point to that:
1) User space needs to handle the two variants instead of just relying on a bit which can be saved in the mutex at initialization time.
2) It becomes pointlessly different from the syscall op pointer handling
3) The fixup decision function has then to evaluate which code path is used. That just adds more symbols and range checking for no real value.
Note, that the fixup is done when a signal is pending even if there is no signal delivered. That's required because fatal signals are not reaching the actual signal delivery code where e.g. RSEQ is handled. They never return from get_signal(). Doing it at that point is harmless and the only downside is a redundant store if the task continues. The pending op pointer is task local so it does not matter whether it's nullified twice or not.
The remaining question is how to do the range checking. Mathiue's proposal [ab]used the exception table to do the range check. That's not really the correct approach because:
1) It is not an exception handler
2) It is a fixup similar to RSEQ critical section fixups
So the right approach is to use a dedicated mechanism and not to glue it into something which has a completely different purpose. That just creates complexity and overhead.
The critical section labels can be made visible in the VDSO and the VDSO image generator provides the offsets in the image struct to the kernel. The VDSO [re]map() evaluates them and puts them into mm_struct::futex::XXX.
Arguably the range check could chase all of that down via mm->context->vdso->image, but that touches way more cache lines and requires architecture specific range checking functions. Letting the architecture specific VDSO [re]map() code store the information in mm_struct::futex reduces the cache line impact and makes the range check generic.
The series implements all of the above and passes the simple user space test case appended below compiled for both 32-bit and 64-bit and tested on a COMPAT enabled 64-bit kernel. 32-bit kernel should just work, but hasn't been tested yet.
The fixup mechanism is easy to validate. Run the program in gdb and set a breakpoint at __vdso_futex_robust_try_unlock_cs_start or at __vdso_futex_robust_try_unlock_cs_success.
Hitting the breakpoint causes the fixup to run because gdb/ptrace uses signals to control the tracee. So the op pointer must have been cleared if the cmpxchg() succeeded:
(gdb) b __vdso_futex_robust_try_unlock_cs_start
(gdb) run
Testing non contended unlock
Breakpoint 1, 0x00007ffff7fc67a3 in __vdso_futex_robust_try_unlock () (gdb) info r
rdx 0x7ffff7dc1a70 140737351785072 rsi 0x7ffff7dc1a70 140737351785072 rdi 0x7fffffffea90 140737488349840 eflags 0x246 [ PF ZF IF ]
(gdb) x/xg 0x7ffff7dc1a70
0x7ffff7dc1a70: 0x0000000000000000
ZF is set, which indicates that the cmpxchg() succeeded. The fixup is "premature" but as explained above that is harmless. The success path writing 0 again is just a redundant store with no side effects as the pointer is task local.
Letting it continue hits the contended case:
(gdb) info 5
rdx 0x7ffff7dc1a70 140737351785072 rsi 0x7ffff7dc1a70 140737351785072 rdi 0x7fffffffea90 140737488349840 eflags 0xa87 [ CF PF SF IF OF ]
(gdb) x/xg 0x7ffff7dc1a70
0x7ffff7dc1a70: 0x00007fffffffeab0
Note the lack of ZF in eflags, which means cmpxchg() failed. RDI points to mutex->lock and the op pointer still to mutex->list (offset 0x20) as it should be.
A 32-bit executable with the same breakpoint shows the bit 0 handling:
eax 0x1c0 448 ecx 0x0 0 edx 0xf7fbc5f5 -134494731 ebx 0x56558ff4 1448447988 esi 0xf7fbc5f4 -134494732 edi 0xffffdbf8 -9224 eflags 0x246 [ PF ZF IF ]
(gdb) x/xw 0xf7fbc5f4
0xf7fbc5f4: 0x00000000
EDX has bit 0 set and ESI has the real pointer address with bit 0 cleared. ESI is used to clear it. EDX is handed to the fixup function and that takes the correct path by testing bit 0.
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
Thanks
tglx
---
It's so much easier to suggest solutions when you don't know too much about the problem.
- Malcolm Forbes
--- Documentation/locking/robust-futexes.rst | 8 arch/x86/Kconfig | 1 arch/x86/entry/vdso/common/vfutex.c | 72 +++++++ arch/x86/entry/vdso/vdso32/Makefile | 5 arch/x86/entry/vdso/vdso32/vdso32.lds.S | 6 arch/x86/entry/vdso/vdso32/vfutex.c | 1 arch/x86/entry/vdso/vdso64/Makefile | 7 arch/x86/entry/vdso/vdso64/vdso64.lds.S | 6 arch/x86/entry/vdso/vdso64/vdsox32.lds.S | 6 arch/x86/entry/vdso/vdso64/vfutex.c | 1 arch/x86/entry/vdso/vma.c | 20 ++ arch/x86/include/asm/futex_robust.h | 44 ++++ arch/x86/include/asm/vdso.h | 3 arch/x86/tools/vdso2c.c | 17 + include/linux/futex.h | 43 +++- include/linux/futex_types.h | 74 +++++++ include/linux/mm_types.h | 12 - include/linux/sched.h | 16 - include/uapi/linux/futex.h | 24 ++ include/vdso/futex.h | 44 ++++ init/Kconfig | 6 io_uring/futex.c | 2 kernel/entry/common.c | 9 kernel/exit.c | 4 kernel/futex/core.c | 303 ++++++++++++++++++------------- kernel/futex/futex.h | 11 - kernel/futex/pi.c | 41 ++-- kernel/futex/syscalls.c | 36 +-- kernel/futex/waitwake.c | 27 ++ 29 files changed, 636 insertions(+), 213 deletions(-)
---
#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 (*frtu_t)(uint32_t *, uint32_t, void *);
static frtu_t frtu;
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);
}
frtu = (frtu_t)dlsym(vdso, "__vdso_futex_robust_try_unlock");
if (!frtu) {
printf("Failed to find frtu in vDSO\n");
exit(1);
}
}static void *mangle_pop(struct robust_list_head *rh)
{
unsigned long addr = (unsigned long) &rh->list_op_pending;
if (__BITS_PER_LONG != 64)
addr |= 0x01;
return (void *)addr;
}#define FUTEX_ROBUST_UNLOCK 512
#define UNLOCK (FUTEX_WAKE | FUTEX_ROBUST_UNLOCK)
#define UNLOCK_PI (FUTEX_UNLOCK_PI | FUTEX_ROBUST_UNLOCK)
int main(int argc, char * const argv[])
{
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
uint32_t *lock = (uint32_t *)&mutex.__data.__lock;
struct robust_list_head *rhead;
pid_t ltid, tid = gettid();
struct robust_list *list;
size_t sz;
void *pop;
get_vdso();
syscall(SYS_get_robust_list, 0, &rhead, &sz);
pop = mangle_pop(rhead);
list = (struct robust_list *)&mutex.__data.__list.__next;
printf("Testing non contended unlock\n");
*lock = tid;
rhead->list_op_pending = list;
ltid = frtu(lock, tid, pop);
if (ltid != tid)
printf("Non contended unlock failed: LTID %08x\n", ltid);
else if (rhead->list_op_pending)
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");
printf("Testing contended FUTEX_WAKE unlock\n");
*lock = tid | FUTEX_WAITERS;
rhead->list_op_pending = list;
ltid = frtu(lock, tid, pop);
if (ltid == tid) {
printf("Contended unlock succeeded %08x\n", ltid);
} else if (!rhead->list_op_pending) {
printf("List op cleared: %16lx\n", (unsigned long) rhead->list_op_pending);
} else if (!*lock) {
printf("Contended unlock cleared LOCK %08x\n", *lock);
} else {
int ret = syscall(SYS_futex, lock, UNLOCK, 1, NULL, pop, 0, 0);
if (ret < 0)
printf("FUTEX_WAKE|FUTEX_ROBUST_UNLOCK failed %d\n", errno);
else if (rhead->list_op_pending)
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");
}
printf("Testing contended FUTEX_UNLOCK_PI unlock\n");
*lock = tid | FUTEX_WAITERS;
rhead->list_op_pending = list;
ltid = frtu(lock, tid, pop);
if (ltid == tid) {
printf("Contended unlock succeeded %08x\n", ltid);
} else if (!rhead->list_op_pending) {
printf("List op cleared: %16lx\n", (unsigned long) rhead->list_op_pending);
} else if (!*lock) {
printf("Contended unlock cleared LOCK %08x\n", *lock);
} else {
int ret = syscall(SYS_futex, lock, UNLOCK_PI, 0, NULL, pop, 0, 0);
if (ret < 0)
printf("FUTEX_UNLOCK_PI|FUTEX_ROBUST_UNLOCK failed %d\n", errno);
else if (rhead->list_op_pending)
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");
}
return 0;
}Thomas Gleixner
maintainerPATCH 1/8 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.
--- Documentation/locking/robust-futexes.rst | 8 ++-- include/linux/futex.h | 12 ++---- include/linux/futex_types.h | 34 +++++++++++++++++++ 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, 97 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 +34 -0
@@ -0,0 +1,34 @@ +/* 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_ctrl - 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 +* @exit_mutex: Mutex for serializing exit +* @state: Futex handling state to handle exit races correctly +*/ +struct futex_ctrl { +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_ctrl { }; +#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_ctrl 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 1/8] futex: Move futex task related data into a struct
Quoted 7 lines · Thomas Gleixner
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]>
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
André Almeida
reviewerRe: [patch 1/8] futex: Move futex task related data into a struct
In reply to Thomas GleixnerEm 16/03/2026 14:12, 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]>
[...]
include/linux/futex_types.h line 14
Quoted 11 lines
include/linux/futex_types.h new file +11
-0+ +#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_ctrl - Futex related per task data
maybe futex_task_data to match futex_mm_data?
Quoted 2 lines
+ * @robust_list: User space registered robust list pointer
+ * @compat_robust_list: User space registered robust list pointer for compat tasks
to avoid kernel-doc warnings:
@pi_state_list: List of PI mutexes holded by this task @pi_state_cache: Copy of the PI state
Quoted 13 lines
+ * @exit_mutex: Mutex for serializing exit
+ * @state: Futex handling state to handle exit races correctly
+ */include/linux/futex_types.h new file +10
-0+struct futex_ctrl { +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; +};
Thomas Gleixner
maintainerRe: [patch 1/8] futex: Move futex task related data into a struct
Quoted 6 lines · André Almeida
Em 16/03/2026 14:12, Thomas Gleixner escreveu:
include/linux/futex_types.h line 14
Quoted 3 lines
include/linux/futex_types.h new file +3
-0+ +/** +* struct futex_ctrl - Futex related per task data maybe
futex_task_datato matchfutex_mm_data?
Sure.
Quoted 8 lines
Quoted 2 lines
+ * @robust_list: User space registered robust list pointer
+ * @compat_robust_list: User space registered robust list pointer for compat tasksto avoid kernel-doc warnings:
@pi_state_list: List of PI mutexes holded by this task @pi_state_cache: Copy of the PI state
Oops.
Thomas Gleixner
maintainerPATCH 2/8 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 | 22 +++++++ include/linux/mm_types.h | 11 --- kernel/futex/core.c | 123 ++++++++++++++++++++------------------------ 3 files changed, 80 insertions(+), 76 deletions(-)
include/linux/futex_types.h +22 -0
@@ -31,4 +31,26 @@ struct futex_ctrl {struct futex_ctrl { };#endif /* !CONFIG_FUTEX */ +/** +* 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 +}; +#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 2/8] futex: Move futex related mm_struct data into a struct
Quoted 30 lines · Thomas Gleixner
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]>--- include/linux/futex_types.h | 22 +++++++ include/linux/mm_types.h | 11 --- kernel/futex/core.c | 123 ++++++++++++++++++++------------------------ 3 files changed, 80 insertions(+), 76 deletions(-)include/linux/futex_types.h +11
-0@@ -31,4 +31,26 @@ struct futex_ctrl {struct futex_ctrl { };#endif /* !CONFIG_FUTEX */ +/** +* 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 {
include/linux/futex_types.h struct futex_ctrl { · line 54
Quoted 10 lines
include/linux/futex_types.h +10
-0+#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 +};
In the previous patch you use the approach of declaring an empty structure within the #else, and here the content of the struct is conditional. Preferably pick one approach ?
Other than this minor nit:
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
Thomas Gleixner
maintainerPATCH 3/8 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.
--- 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 3/8] futex: Provide UABI defines for robust list entry modifiers
Quoted 2 lines · Thomas Gleixner
The marker for PI futexes in the robust list is a hardcoded
0x1which lacks any sensible form of documentation.
lol, yes indeed :)
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
André Almeida
reviewerRe: [patch 3/8] futex: Provide UABI defines for robust list entry modifiers
In reply to Thomas GleixnerEm 16/03/2026 14:13, Thomas Gleixner escreveu:
Quoted 5 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.
Most of the diff is about a change that's not described here.
Quoted 1 line
Signed-off-by Thomas Gleixner <[email protected]>
[...]
kernel/futex/core.c static void __user *futex_uaddr(struct r · line 1246
Quoted 4 lines
static void compat_exit_robust_list(struct task_struct *curr) {kernel/futex/core.c +1
-1-struct compat_robust_list_head __user *head = curr->futex.compat_robust_list; +struct compat_robust_list_head __user *head = current->futex.compat_robust_list;
It seems you accidentally changed from curr-> to current->
Quoted 4 lines
kernel/futex/core.c +1
-2+unsigned int limit =ROBUST_LIST_LIMIT , cur_mod, next_mod, pend_mod;struct robust_list __user *entry, *next_entry, *pending; -unsigned int limit =ROBUST_LIST_LIMIT , pi, pip; -unsigned int next_pi;
Thomas Gleixner
maintainerRe: [patch 3/8] futex: Provide UABI defines for robust list entry modifiers
Quoted 8 lines · André Almeida
Em 16/03/2026 14:13, Thomas Gleixner escreveu:
Quoted 5 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.
Most of the diff is about a change that's not described here.
I'll add some more blurb to it.
Quoted 6 lines
kernel/futex/core.c static void __user *futex_uaddr(struct r · line 1246
Quoted 4 lines
static void compat_exit_robust_list(struct task_struct *curr) {kernel/futex/core.c +1
-1-struct compat_robust_list_head __user *head = curr->futex.compat_robust_list; +struct compat_robust_list_head __user *head = current->futex.compat_robust_list;It seems you accidentally changed from curr-> to current->
Copy & Pasta :)
Thomas Gleixner
maintainerPATCH 4/8 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) 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.
The pointer has a modifier to indicate that it points to an u32 and not to an u64. This is required for several 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
3) Having the pointer handed in spares the evaluation of the registered robust lists to figure out whether the futex address is matching the registered [compat_]robust_list_head::list_pending_op pointer.
As a consequence 32-bit applications have to set this bit unconditionally so they can run on a 64-bit kernel in compat mode unmodified. 32-bit kernels return an error code when the bit 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.
--- include/uapi/linux/futex.h | 20 ++++++++++++++ io_uring/futex.c | 2 - kernel/futex/core.c | 61 +++++++++++++++++++++++++++++++++++++++++++-- kernel/futex/futex.h | 11 ++++++-- kernel/futex/pi.c | 15 +++++++++-- kernel/futex/syscalls.c | 13 +++++++-- kernel/futex/waitwake.c | 27 ++++++++++++++++++- 7 files changed, 136 insertions(+), 13 deletions(-)
include/uapi/linux/futex.h +19 -1
@@ -25,7 +25,8 @@#define FUTEX_PRIVATE_FLAG 128#define FUTEX_CLOCK_REALTIME 256 -#define FUTEX_CMD_MASK ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME) +#define FUTEX_UNLOCK_ROBUST 512 +#define FUTEX_CMD_MASK ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME | FUTEX_UNLOCK_ROBUST)#define FUTEX_WAIT_PRIVATE (FUTEX_WAIT | FUTEX_PRIVATE_FLAG)#define FUTEX_WAKE_PRIVATE (FUTEX_WAKE | FUTEX_PRIVATE_FLAG)
@@ -182,6 +183,23 @@ struct robust_list_head {#define FUTEX_ROBUST_MOD_MASK (FUTEX_ROBUST_MOD_PI)/* +* Modifier for FUTEX_ROBUST_UNLOCK uaddr2. Required to distinguish the storage +* size for the robust_list_head::list_pending_op. This solves two problems: +* +* 1) COMPAT tasks +* +* 2) The mixed mode magic gaming use case which has both 32-bit and 64-bit +* robust lists. Oh well.... +* +* Long story short: 32-bit userspace must set this bit unconditionally to +* ensure that it can run on a 64-bit kernel in compat mode. If user space +* screws that up a 64-bit kernel will happily clear the full 64-bits. 32-bit +* kernels return an error code if the bit is not set. +*/ +#define FUTEX_ROBUST_UNLOCK_MOD_32BIT (0x1UL) +#define FUTEX_ROBUST_UNLOCK_MOD_MASK (FUTEX_ROBUST_UNLOCK_MOD_32BIT) + +/* * bitset with all bits set for the FUTEX_xxx_BITSET OPs to request a * match of any bit. */
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 +59 -2
91 lines not on this page · raw message
kernel/futex/futex.h +9 -2
@@ -40,6 +40,7 @@#define FLAGS_NUMA 0x0080#define FLAGS_STRICT 0x0100#define FLAGS_MPOL 0x0200 +#define FLAGS_UNLOCK_ROBUST 0x0400/* FUTEX_ to FLAGS_ */ static inline unsigned int futex_to_flags (unsigned int op)
@@ -52,6 +53,9 @@ static inline unsigned int futex_to_flagif (op &FUTEX_CLOCK_REALTIME ) flags |=FLAGS_CLOCKRT ; +if (op &FUTEX_UNLOCK_ROBUST ) + flags |=FLAGS_UNLOCK_ROBUST ; +return flags; }
@@ -438,13 +442,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); +#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)) +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 +25 -2
@@ -150,12 +150,32 @@ 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. */ +if (put_user (0U , uaddr)) +return false; + +/* +* 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. +*/ +return futex_robust_list_clear_pending (pop); +} + +/* * 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 +186,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 4/8] futex: Add support for unlocking robust futexes
On 2026-03-16 13:13, Thomas Gleixner wrote:
[...]
Quoted 4 lines
PI futexes have a similar problem both for the non-contented user space unlock and the in kernel unlock:
Quoted 8 lines
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();
We can document that the window in this case is between #3 and #5.
Other than this detail:
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
André Almeida
reviewerRe: [patch 4/8] futex: Add support for unlocking robust futexes
In reply to Thomas GleixnerEm 16/03/2026 14:13, Thomas Gleixner escreveu:
[...]
Quoted 7 lines
kernel/futex/waitwake.c +1
-0@@ -150,12 +150,32 @@ void futex_wake_mark(struct wake_q_head }/* +* If requested, clear the robust list pending op and unlock the futex
kernel/futex/waitwake.c void futex_wake_mark(struct wake_q_head · line 163
Quoted 10 lines
kernel/futex/waitwake.c +10
-0+*/ +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. */ +if (put_user (0U , uaddr)) +return false; +
On glibc code, the futex unlock happens atomically:
atomic_exchange_release (&mutex->__data.__lock, 0)
Is OK to do it unatomically?
I couldn't find a race condition given that the only thread that should be able to write to the futex address must be the lock owner anyways, but I don't know why userspace does it atomically in the first place.
Peter Zijlstra
maintainerRe: [patch 4/8] futex: Add support for unlocking robust futexes
Quoted 23 lines · André Almeida
Em 16/03/2026 14:13, Thomas Gleixner escreveu:
[...]
Quoted 6 lines
kernel/futex/waitwake.c +1
-0@@ -150,12 +150,32 @@ void futex_wake_mark(struct wake_q_head }/* +* If requested, clear the robust list pending op and unlock the futex kernel/futex/waitwake.c void futex_wake_mark(struct wake_q_head · line 162
Quoted 10 lines
kernel/futex/waitwake.c +10
-0+*/ +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. */ +if (put_user (0U , uaddr)) +return false; +On glibc code, the futex unlock happens atomically:
Quoted 7 lines
atomic_exchange_release (&mutex->__data.__lock, 0)Is OK to do it unatomically?
I couldn't find a race condition given that the only thread that should be able to write to the futex address must be the lock owner anyways, but I don't know why userspace does it atomically in the first place.
So userspace could probably get away with doing:
atomic_store_explicit(&mutex->__data.__lock, 0, memory_order_release);
IOW a plain store-release. And yeah, I think the kernel probably should do a store-release too. It doesn't matter on x86, but if we have a weakly ordered architecture where the mode transition is also not serializing, we could be having trouble.
Thomas Gleixner
maintainerRe: [patch 4/8] futex: Add support for unlocking robust futexes
Quoted 32 lines · Peter Zijlstra
Quoted 23 lines · André Almeida
Em 16/03/2026 14:13, Thomas Gleixner escreveu:
[...]
Quoted 6 lines
kernel/futex/waitwake.c +1
-0@@ -150,12 +150,32 @@ void futex_wake_mark(struct wake_q_head }/* +* If requested, clear the robust list pending op and unlock the futex kernel/futex/waitwake.c void futex_wake_mark(struct wake_q_head · line 162
Quoted 10 lines
kernel/futex/waitwake.c +10
-0+*/ +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. */ +if (put_user (0U , uaddr)) +return false; +On glibc code, the futex unlock happens atomically:
Quoted 7 lines
atomic_exchange_release (&mutex->__data.__lock, 0)Is OK to do it unatomically?
I couldn't find a race condition given that the only thread that should be able to write to the futex address must be the lock owner anyways, but I don't know why userspace does it atomically in the first place.
Quoted 8 lines
So userspace could probably get away with doing:
atomic_store_explicit(&mutex->__data.__lock, 0, memory_order_release);IOW a plain store-release. And yeah, I think the kernel probably should do a store-release too. It doesn't matter on x86, but if we have a weakly ordered architecture where the mode transition is also not serializing, we could be having trouble.
No. There is a syscall in between and if that is not sufficient then the architecure has more severe troubles than that store, no?
Thanks,
tglx
Peter Zijlstra
maintainerRe: [patch 4/8] futex: Add support for unlocking robust futexes
Quoted 37 lines · Thomas Gleixner
Quoted 36 lines · Peter Zijlstra
Quoted 23 lines · André Almeida
Em 16/03/2026 14:13, Thomas Gleixner escreveu:
[...]
Quoted 6 lines
kernel/futex/waitwake.c +1
-0@@ -150,12 +150,32 @@ void futex_wake_mark(struct wake_q_head }/* +* If requested, clear the robust list pending op and unlock the futex kernel/futex/waitwake.c void futex_wake_mark(struct wake_q_head · line 162
Quoted 10 lines
kernel/futex/waitwake.c +10
-0+*/ +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. */ +if (put_user (0U , uaddr)) +return false; +On glibc code, the futex unlock happens atomically:
Quoted 7 lines
atomic_exchange_release (&mutex->__data.__lock, 0)Is OK to do it unatomically?
I couldn't find a race condition given that the only thread that should be able to write to the futex address must be the lock owner anyways, but I don't know why userspace does it atomically in the first place.
So userspace could probably get away with doing:
atomic_store_explicit(&mutex->__data.__lock, 0, memory_order_release);
Quoted 7 lines
Quoted 4 lines
IOW a plain store-release. And yeah, I think the kernel probably should do a store-release too. It doesn't matter on x86, but if we have a weakly ordered architecture where the mode transition is also not serializing, we could be having trouble.
No. There is a syscall in between and if that is not sufficient then the architecure has more severe troubles than that store, no?
So I think we once tried to determine if syscall could be considered to imply memory ordering, and I think the take-away at the time was that we could not assume so.
But its been a long time, maybe I misremember.
Re: [patch 4/8] futex: Add support for unlocking robust futexes
* Peter Zijlstra:
Quoted 8 lines
Quoted 2 lines
No. There is a syscall in between and if that is not sufficient then the architecure has more severe troubles than that store, no?
So I think we once tried to determine if syscall could be considered to imply memory ordering, and I think the take-away at the time was that we could not assume so.
But its been a long time, maybe I misremember.
I remember the same thing, and I think I saw test failures where system calls where not a (strong) barrier on POWER.
However, some system calls better be barriers. For example, writing to a pipe should synchronize with reading from the pipe and poll wakeup on the read end. Likewise for sockets, I assume. As far as I know, POSIX is silent on this topic, though.
Thanks,
Florian
Peter Zijlstra
maintainerRe: [patch 4/8] futex: Add support for unlocking robust futexes
In reply to Peter ZijlstraQuoted 43 lines · Peter Zijlstra
Quoted 42 lines · Thomas Gleixner
Quoted 40 lines · Peter Zijlstra
Quoted 23 lines · André Almeida
Em 16/03/2026 14:13, Thomas Gleixner escreveu:
[...]
Quoted 6 lines
kernel/futex/waitwake.c +1
-0@@ -150,12 +150,32 @@ void futex_wake_mark(struct wake_q_head }/* +* If requested, clear the robust list pending op and unlock the futex kernel/futex/waitwake.c void futex_wake_mark(struct wake_q_head · line 162
Quoted 10 lines
kernel/futex/waitwake.c +10
-0+*/ +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. */ +if (put_user (0U , uaddr)) +return false; +On glibc code, the futex unlock happens atomically:
Quoted 7 lines
atomic_exchange_release (&mutex->__data.__lock, 0)Is OK to do it unatomically?
I couldn't find a race condition given that the only thread that should be able to write to the futex address must be the lock owner anyways, but I don't know why userspace does it atomically in the first place.
So userspace could probably get away with doing:
atomic_store_explicit(&mutex->__data.__lock, 0, memory_order_release);IOW a plain store-release. And yeah, I think the kernel probably should do a store-release too. It doesn't matter on x86, but if we have a weakly ordered architecture where the mode transition is also not serializing, we could be having trouble.
Quoted 8 lines
Quoted 2 lines
No. There is a syscall in between and if that is not sufficient then the architecure has more severe troubles than that store, no?
So I think we once tried to determine if syscall could be considered to imply memory ordering, and I think the take-away at the time was that we could not assume so.
But its been a long time, maybe I misremember.
Ah, it was for the sys_membarrier() thing. And yes, a syscall itself does not imply memory ordering for all architectures. And since this very much needs release semantics, it is best to be explicit about that.
Thomas Gleixner
maintainerRe: [patch 4/8] futex: Add support for unlocking robust futexes
Quoted 10 lines · Peter Zijlstra
Quoted 5 lines · Peter Zijlstra
So I think we once tried to determine if syscall could be considered to imply memory ordering, and I think the take-away at the time was that we could not assume so.
But its been a long time, maybe I misremember.
Ah, it was for the
sys_membarrier()thing. And yes, a syscall itself does not imply memory ordering for all architectures. And since this very much needs release semantics, it is best to be explicit about that.
What a mess...
Thomas Gleixner
maintainerPATCH 5/8 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.
Arguably this could be resolved by chasing mm->context->vdso->image, but that 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.
--- include/linux/futex_types.h | 32 +++++++++++++++++++++++++------- include/linux/mm_types.h | 1 + init/Kconfig | 6 ++++++ 3 files changed, 32 insertions(+), 7 deletions(-)
include/linux/futex_types.h +25 -7
@@ -33,13 +33,26 @@ struct futex_ctrl { };/** * 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 +* @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 +* +* @unlock_cs_start_ip: The start IP of the robust futex unlock critical section +* +* @unlock_cs_success_ip: The IP of the robust futex unlock critical section which +* indicates that the unlock (cmpxchg) was successful +* Required to handle the compat size insanity for mixed mode +* game emulators. +* +* Not evaluated by the core code as that only +* evaluates the start/end range. Can therefore be 0 if the +* architecture does not care. +* +* @unlock_cs_end_ip: The end IP of the robust futex unlock critical section */ struct futex_mm_data {#ifdef CONFIG_FUTEX_PRIVATE_HASH
@@ -51,6 +64,11 @@ struct futex_mm_data {atomic_long_t phash_atomic;unsigned int __percpu *phash_ref;#endif +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +unsigned long unlock_cs_start_ip; +unsigned long unlock_cs_success_ip; +unsigned long unlock_cs_end_ip; +#endif };#endif /* _LINUX_FUTEX_TYPES_H */
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
Re: [patch 5/8] futex: Add robust futex unlock IP range
Quoted 9 lines · Thomas Gleixner
There will be a VDSO function to unlock robust futexes in user space. The unlock sequence is racy vs. clearing the
list_pending_oppointer 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 inmm_struct::futex.Architectures which implement support for this have to update these addresses when the VDSO is (re)mapped.
Quoted 6 lines
Arguably this could be resolved by chasing
mm->context->vdso->image, but that that's architecture specific and requires to touch quite some cache lines. Having it inmm::futexreduces the cache line impact and avoids having yet another set of architecture specific functionality.Signed-off-by Thomas Gleixner <[email protected]>
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
André Almeida
reviewerRe: [patch 5/8] futex: Add robust futex unlock IP range
In reply to Thomas GleixnerEm 16/03/2026 14:13, Thomas Gleixner escreveu:
Quoted 2 lines
Arguably this could be resolved by chasing
mm->context->vdso->image, but that that's architecture specific and requires to touch quite some cache
"that that's"
Thomas Gleixner
maintainerPATCH 6/8 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 #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 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 $0x0,(%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, unlock_ip_range))
futex_fixup_robust_unlock(regs);futex_fixup_robust_unlock() then invokes a architecture specific function which 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 results in the trivial evaluation of the zero flag:
return regs->eflags & X86_EFLAGS_ZF;
Other architectures might need to do more complex evaluations due to LLSC, but the approach is valid in general. In case that COMPAT is enabled the decision function is a bit more complex, but that's an implementation detail.
The handling code also requires to retrieve the pending op pointer via an architecture specific function to be able to perform the clearing.
The unlock sequence is going to be placed in the VDSO so that the kernel can keep everything synchronized. The resulting code sequence for user space is:
if (__vdso_futex_robust_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.
The pending op pointer has the same modifier requirements as the @uaddr2 argument of sys_futex(FUTEX_ROBUST_UNLOCK) for the very same reasons. That means VDSO implementations need to support the variable size case for the pending op pointer as well if COMPAT is enabled.
--- include/linux/futex.h | 31 ++++++++++++++++++++++++++++++- include/vdso/futex.h | 44 ++++++++++++++++++++++++++++++++++++++++++++ kernel/entry/common.c | 9 ++++++--- kernel/futex/core.c | 13 +++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-)
include/linux/futex.h +30 -1
@@ -110,7 +110,36 @@ 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); + +static inline bool futex_within_robust_unlock (struct pt_regs *regs) +{ +unsigned long ip =instruction_pointer (regs); + +return ip >= current->mm->futex.unlock_cs_start_ip && + ip < current->mm->futex.unlock_cs_end_ip; +} + +static inline void futex_fixup_robust_unlock (struct pt_regs *regs) +{ +/* +* 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 ; + +if (unlikely (futex_within_robust_unlock (regs))) +__futex_fixup_robust_unlock (regs); +} +#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 +44 -0
44 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 +13 -0
@@ -1455,6 +1455,19 @@ 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) +{ +void __user *pop; + +if (!arch_futex_needs_robust_unlock_fixup (regs)) +return ; + + pop =arch_futex_robust_unlock_get_pop (regs); +futex_robust_list_clear_pending (pop); +} +#endif /* CONFIG_FUTEX_ROBUST_UNLOCK */ +static void futex_cleanup (struct task_struct *tsk) {if (unlikely (tsk->futex.robust_list)) {
Re: [patch 6/8] futex: Provide infrastructure to plug the non contended robust futex unlock race
Quoted 12 lines · Thomas Gleixner
When the
FUTEX_ROBUST_UNLOCKmechanism 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, ....);
Quoted 8 lines
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 addressWhen then the original task exits before reaching #6 then the kernel robust
"... before reaching #4 ...".
[...]
Quoted 8 lines
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, unlock_ip_range)) futex_fixup_robust_unlock(regs);
Nice trick to re-use the rseq infra, but where is the added dependency on CONFIG_RSEQ=y ?
Thanks,
Mathieu
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
Thomas Gleixner
maintainerRe: [patch 6/8] futex: Provide infrastructure to plug the non contended robust futex unlock race
Quoted 32 lines · Mathieu Desnoyers
Quoted 12 lines · Thomas Gleixner
When the
FUTEX_ROBUST_UNLOCKmechanism 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, ....);Quoted 8 lines
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 addressWhen then the original task exits before reaching #6 then the kernel robust
"... before reaching #4 ...".
[...]
Quoted 6 lines
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:
Quoted 5 lines
Quoted 2 lines
if (current->rseq.user_irq && within(regs, unlock_ip_range)) futex_fixup_robust_unlock(regs);Nice trick to re-use the rseq infra, but where is the added dependency on
CONFIG_RSEQ=y?
Here:
+config FUTEX_ROBUST_UNLOCK
+ def_bool FUTEX && HAVE_GENERIC_VDSO && GENERIC_IRQ_ENTRY && RSEQ && HAVE_FUTEX_ROBUST_UNLOCK
Re: [patch 6/8] futex: Provide infrastructure to plug the non contended robust futex unlock race
Quoted 1 line · Thomas Gleixner
On Mon, Mar 16 2026 at 14:35, Mathieu Desnoyers wrote:
[...]
Quoted 8 lines
Quoted 3 lines
Nice trick to re-use the rseq infra, but where is the added dependency on
CONFIG_RSEQ=y?Here:
+config
FUTEX_ROBUST_UNLOCK
+def_boolFUTEX &&HAVE_GENERIC_VDSO&&GENERIC_IRQ_ENTRY&& RSEQ &&HAVE_FUTEX_ROBUST_UNLOCK
*blink*. Yes I missed it. Clear as day.
Thanks,
Mathieu
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
Thomas Gleixner
maintainerPATCH 7/8 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 instruction window so it can clear the pointer when the task is interrupted within that race window.
Add the 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().
--- arch/x86/entry/vdso/vma.c | 20 ++++++++++++++++++++ arch/x86/include/asm/vdso.h | 3 +++ arch/x86/tools/vdso2c.c | 17 ++++++++++------- 3 files changed, 33 insertions(+), 7 deletions(-)
arch/x86/entry/vdso/vma.c +20 -0
@@ -73,6 +73,23 @@ 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; + + 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; +} +#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 +97,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 +207,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 +3 -0
@@ -25,6 +25,9 @@ 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; };extern const struct vdso_image vdso64_image;
arch/x86/tools/vdso2c.c +10 -7
@@ -75,13 +75,16 @@ 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}, + {"__vdso_futex_robust_try_unlock_cs_start" , true}, + {"__vdso_futex_robust_try_unlock_cs_success" , true}, + {"__vdso_futex_robust_try_unlock_cs_end" , true}, }; __attribute__((format(printf, 1, 2))) __attribute__((noreturn))
Thomas Gleixner
maintainerPATCH 8/8 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 #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 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 assembly sequence for the non COMPAT case 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 $0x0,(%rdx) // Clear list_op_pending .Lend:
ret
So the decision can be simply based on the ZF state in regs->flags.
If COMPAT is enabled then the try_unlock() function needs to take the size bit in the OP pointer into account, which makes it slightly more complex:
mov %esi,%eax // Load TID into EAX mov %rdx,%rsi // Get the op pointer xor %ecx,%ecx // Set ECX to 0 and $0xfffffffffffffffe,%rsi // Clear the size bit lock cmpxchg %ecx,(%rdi) // Try the TID -> 0 transition .Lstart: jnz .Lend .Lsuccess: testl $0x1,(%rdx) // Test the size bit jz .Lop64 // Not set: 64-bit movl $0x0,(%rsi) // Clear 32-bit jmp .Lend .Lop64: movq $0x0,(%rsi) // Clear 64-bit
.Lend:
ret
The decision function has to check whether regs->ip is in the success portion as the size bit test obviously modifies ZF too. If it is before .Lsuccess then ZF contains the cmpxchg() result. If it's at of after .Lsuccess then the pointer has to be cleared.
The original pointer with the size bit is preserved in RDX so the fixup can utilize the existing clearing mechanism, which is used by sys_futex().
Arguably this could be avoided by providing separate functions and making the IP range for the quick check in the exit to user path cover the whole text section which contains the two functions. But that's not a win at all because:
1) User space needs to handle the two variants instead of just relying on a bit which can be saved in the mutex at initialization time.
2) The fixup decision function has then to evaluate which code path is used. That just adds more symbols and range checking for no real value.
The unlock function is inspired by an idea from Mathieu Desnoyers.
--- arch/x86/Kconfig | 1 arch/x86/entry/vdso/common/vfutex.c | 72 +++++++++++++++++++++++++++++++ arch/x86/entry/vdso/vdso32/Makefile | 5 +- arch/x86/entry/vdso/vdso32/vdso32.lds.S | 6 ++ arch/x86/entry/vdso/vdso32/vfutex.c | 1 arch/x86/entry/vdso/vdso64/Makefile | 7 +-- arch/x86/entry/vdso/vdso64/vdso64.lds.S | 6 ++ arch/x86/entry/vdso/vdso64/vdsox32.lds.S | 6 ++ arch/x86/entry/vdso/vdso64/vfutex.c | 1 arch/x86/include/asm/futex_robust.h | 44 ++++++++++++++++++ 10 files changed, 144 insertions(+), 5 deletions(-)
arch/x86/Kconfig +1 -0
@@ -237,6 +237,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 +72 -0
72 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 +6 -0
@@ -30,6 +30,12 @@ VERSION __vdso_clock_gettime64; __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; +#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 +6 -0
@@ -32,6 +32,12 @@ VERSION { #endif 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; +#endif local: *; }; }
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +6 -0
@@ -22,6 +22,12 @@ VERSION { __vdso_getcpu; __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; +#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 +44 -0
44 lines not on this page · raw message
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 11 lines · Thomas Gleixner
When the
FUTEX_ROBUST_UNLOCKmechanism 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);
[...]
Quoted 3 lines
When then 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.
There is no #6.
[...]
Review
Quoted 2 lines
(quoted diff) +2
-0+ +uint32_t __vdso_futex_robust_try_unlock(uint32_t *lock, uint32_t tid, void *pop)
I'm not sure I see the link between "list_pending_op" and @pop ?
arch/x86/entry/vdso/common/vfutex.c line 47
Quoted 5 lines
arch/x86/entry/vdso/common/vfutex.c new file +5
-0+{ + asmvolatile ( +".global __vdso_futex_robust_try_unlock_cs_start \n" +".global __vdso_futex_robust_try_unlock_cs_success \n" + ".global __vdso_futex_robust_try_unlock_cs_end \n"
Those .global are fragile: they depend on making sure the compiler does not emit those symbols more than once per compile unit (due to optimizations).
I understand that you want to skip the "iteration on a section" within the kernel fast path, and I agree with that intent, but I think we can achieve that goal with more robustness by:
- emitting those as triplets within a dedicated section,
- validating that the section only contains a single triplet within the vdso2c script.
This would fail immediately in the vsdo2c script if the compiler does not do as expected rather than silently fail to cover part of the emitted code range.
[...]
arch/x86/entry/vdso/common/vfutex.c line 60
Quoted 1 line
arch/x86/entry/vdso/common/vfutex.c new file +1
-0+ : [tid] "+a" (tid)
Could use a few comments:
- "tid" sits in eax.
arch/x86/entry/vdso/common/vfutex.c line 61
Quoted 1 line
arch/x86/entry/vdso/common/vfutex.c new file +1
-0+ : [ptr] "D" (lock),
- "lock" sits in edi/rdi.
arch/x86/entry/vdso/common/vfutex.c line 62
Quoted 1 line
arch/x86/entry/vdso/common/vfutex.c new file +1
-0+ [pop] "d" (pop),
This constraint puts the unmasked "pop" pointer into edx/rdx.
arch/x86/entry/vdso/common/vfutex.c line 64
Quoted 2 lines
arch/x86/entry/vdso/common/vfutex.c new file +2
-0+ [val] "r" (0) +ASM_PAD_CONSTRAINT
The masked "pop" pointer sits in esi/rsi.
[...]
arch/x86/include/asm/futex_robust.h line 12
Quoted 1 line
arch/x86/include/asm/futex_robust.h new file +1
-0+* disabled or this is a 32-bit kernel then ZF is authoritive no matter
authoritative
arch/x86/include/asm/futex_robust.h line 38
Quoted 4 lines
arch/x86/include/asm/futex_robust.h new file +4
-0+ +static __always_inline void __user *x86_futex_robust_unlock_get_pop(struct pt_regs *regs) +{ +return (void __user *)regs->dx;
When userspace is compat 32-bit, with a 64-bit kernel, are we sure that the 32 upper bits are cleared ? If not can we rely on compat_robust_list_clear_pending to ignore those top bits in put_user(0U, pop) ?
Thanks,
Mathieu
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 10 lines · Mathieu Desnoyers
arch/x86/include/asm/futex_robust.h line 38
Quoted 4 lines · Thomas Gleixner
arch/x86/include/asm/futex_robust.h new file +4
-0+ +static __always_inline void __user *x86_futex_robust_unlock_get_pop(struct pt_regs *regs) +{ +return (void __user *)regs->dx;When userspace is compat 32-bit, with a 64-bit kernel, are we sure that the 32 upper bits are cleared ? If not can we rely on
compat_robust_list_clear_pendingto ignore those top bits input_user(0U, pop)?
Which compat version are you talking about?
1) A 32-bit application which truly runs as compat
2) A 64-bit application which uses both variants and invokes the 64-bit VDSO from a 32-bit program segment
#1 is inherently safe. The 32-bit application uses the compat 32-bit VDSO
which only accesses the lower half of the registers. So the mov $ptr, %edx results in zero extending the 32-bit value. From the SDM:
"32-bit operands generate a 32-bit result, zero-extended to a
64-bit result in the destination general-purpose register."
The exception/interrupt entry switches into 64-bit mode, but due to the above the upper 32 bit are 0. So it's safe to just blindly use regs->dx.
Otherwise it would be pretty impossible to run 32-bit user space on a 64-bit kernel.
#2 can really be assumed to be safe as there must be some magic
translation in the emulation code which handles the different calling conventions.
That's not any different when 32-bit code which runs in the context of a 64-bit application invokes a syscall or a library function.
If that goes wrong, then it's not a kernel problem because the application explicitely tells the kernel to corrupt it's own memory.
The golden rule of UNIX applies here as always:
Do what user space asked for unless it results in a boundary violation which can't be achieved by user space itself.
IOW, let user space shoot itself into the foot when it desires to do so.
Thanks,
tglx
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 19 lines · Thomas Gleixner
Quoted 10 lines · Mathieu Desnoyers
arch/x86/include/asm/futex_robust.h line 38
Quoted 4 lines · Thomas Gleixner
arch/x86/include/asm/futex_robust.h new file +4
-0+ +static __always_inline void __user *x86_futex_robust_unlock_get_pop(struct pt_regs *regs) +{ +return (void __user *)regs->dx;When userspace is compat 32-bit, with a 64-bit kernel, are we sure that the 32 upper bits are cleared ? If not can we rely on
compat_robust_list_clear_pendingto ignore those top bits input_user(0U, pop)?Which compat version are you talking about?
1) A 32-bit application which truly runs as compat
2) A 64-bit application which uses both variants and invokes the 64-bit VDSO from a 32-bit program segment
Quoted 6 lines
#1 is inherently safe. The 32-bit application uses the compat 32-bit VDSO
which only accesses the lower half of the registers. So the mov $ptr, %edx results in zero extending the 32-bit value. From the SDM:"32-bit operands generate a 32-bit result, zero-extended to a
64-bit result in the destination general-purpose register."
Ah, very well, this is the important piece I was missing.
Quoted 4 lines
The exception/interrupt entry switches into 64-bit mode, but due to the above the upper 32 bit are 0. So it's safe to just blindly use
regs->dx.
OK.
[...]
Quoted 3 lines
#2 can really be assumed to be safe as there must be some magic
translation in the emulation code which handles the different calling conventions.
[...]
Sounds good,
Thanks,
Mathieu
Signature · 3 lines
Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Mathieu DesnoyersQuoted 3 lines · Mathieu Desnoyers
Quoted 1 line
+uint32_t
__vdso_futex_robust_try_unlock(uint32_t *lock, uint32_t tid, void *pop)I'm not sure I see the link between "
list_pending_op" and @pop ?
What so hard to understand about that? The function prototype in include/vdso/futex.h is extensively documented.
Quoted 6 lines
[...]
arch/x86/entry/vdso/common/vfutex.c line 60
Quoted 1 line
arch/x86/entry/vdso/common/vfutex.c new file +1
-0+ : [tid] "+a" (tid)Could use a few comments:
- "tid" sits in eax.
If someone needs a comment to understand the constraint, then that persone definitely should not touch the code. I'm all for documentation and comments, but documenting the obvious is not useful at all. See:
https://www.kernel.org/doc/html/latest/process/maintainer-tip.html#comment-style
I'm amazed that you complain about these obvious details and not about the actual lack of a general comment which explains the actual inner workings of that ASM maze. That would be actually useful for a unexperienced reader. Interesting preference.
Thanks,
tglx
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Mathieu DesnoyersQuoted 10 lines · Mathieu Desnoyers
arch/x86/entry/vdso/common/vfutex.c line 47
Quoted 5 lines · Thomas Gleixner
arch/x86/entry/vdso/common/vfutex.c new file +5
-0+{ + asmvolatile ( +".global __vdso_futex_robust_try_unlock_cs_start \n" +".global __vdso_futex_robust_try_unlock_cs_success \n" + ".global __vdso_futex_robust_try_unlock_cs_end \n"Those .global are fragile: they depend on making sure the compiler does not emit those symbols more than once per compile unit (due to optimizations).
This is a single global function which contains the unique ASM code with those unique symbols. The unique compilation unit containing it ends up in the VDSO "library".
Q: Which optimizations would cause the compiler to emit them more than
once?
A: None.
If that happens then the compiler is seriously broken and the resulting VDSO trainwreck is the least of your worries.
That would be equivalent to a single global C function in a unique compilation unit being emitted more than once.
So what is fragile about that?
Thanks,
tglx
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Thomas GleixnerOn Mon, Mar 16, 2026 at 06:13:34PM +0100, Thomas Gleixner wrote:
(...)
Quoted 13 lines
arch/x86/Kconfig +1
-0@@ -237,6 +237,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 line 10
Quoted 10 lines
arch/x86/entry/vdso/common/vfutex.c new file +10
-0@@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include <vdso/futex.h> + +/* +* Compat enabled kernels have to take the size bit into account to support the +* mixed size use case of gaming emulators. Contrary to the kernel robust unlock +* mechanism all of this does not test for the 32-bit modifier in 32-bit VDSOs +* and in compat disabled kernels. User space can keep the pieces. +*/ +#if defined(CONFIG_X86_64 ) && !defined(BUILD_VDSO32_64 )
#ifndef __x86_64__ ?
Quoted 22 lines
arch/x86/entry/vdso/common/vfutex.c new file +22
-0+ +#ifdef CONFIG_COMPAT + +# define ASM_CLEAR_PTR \ + " testl $1, (%[pop]) \n" \ + " jz .Lop64 \n" \ + " movl $0, (%[pad]) \n" \ + " jmp __vdso_futex_robust_try_unlock_cs_end \n" \ + ".Lop64: \n" \ + " movq $0, (%[pad]) \n" + +# define ASM_PAD_CONSTRAINT ,[pad] "S" (((unsigned long)pop) & ~0x1UL) + +#else /* CONFIG_COMPAT */ + +# define ASM_CLEAR_PTR \ + " movq $0, (%[pop]) \n" + +# define ASM_PAD_CONSTRAINT + +#endif /* !CONFIG_COMPAT */ +
arch/x86/entry/vdso/common/vfutex.c line 42
Quoted 10 lines
arch/x86/entry/vdso/common/vfutex.c new file +10
-0+#else /* CONFIG_X86_64 && !BUILD_VDSO32_64 */ + +# define ASM_CLEAR_PTR \ + " movl $0, (%[pad]) \n" + +# define ASM_PAD_CONSTRAINT ,[pad] "S" (((unsigned long)pop) & ~0x1UL) + +#endif /* !CONFIG_X86_64 || BUILD_VDSO32_64 */ + +uint32_t __vdso_futex_robust_try_unlock (uint32_t *lock,uint32_t tid,void *pop)
While uint32_t is originally a userspace type, in the kernel it also pulls in other internal types which are problematic in the vDSO. __u32 from uapi/linux/types.h avoid this issue.
(...)
arch/x86/entry/vdso/vdso64/vdsox32.lds.S VERSION { · line 30
Quoted 12 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +5
-0@@ -22,6 +22,12 @@ VERSION { __vdso_getcpu; __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;
These three symbols are not meant to be used from outside the vDSO implementation, so they don't need to be exported by the linkerscripts.
Quoted 4 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +1
-0+#endif local: *; };}
(...)
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 10 lines · Thomas Weißschuh
arch/x86/entry/vdso/common/vfutex.c line 10
Quoted 7 lines · Thomas Gleixner
arch/x86/entry/vdso/common/vfutex.c new file +7
-0+/* +* Compat enabled kernels have to take the size bit into account to support the +* mixed size use case of gaming emulators. Contrary to the kernel robust unlock +* mechanism all of this does not test for the 32-bit modifier in 32-bit VDSOs +* and in compat disabled kernels. User space can keep the pieces. +*/ +#if defined(CONFIG_X86_64 ) && !defined(BUILD_VDSO32_64 )#ifndef
__x86_64__?
#ifdef :)
Just had to double check and convince myself that __x86_64__ is set when building for the X86_X32 ABI. Seems to work.
Quoted 5 lines
Quoted 1 line
+uint32_t
__vdso_futex_robust_try_unlock(uint32_t *lock, uint32_t tid, void *pop)While
uint32_tis originally a userspace type, in the kernel it also pulls in other internal types which are problematic in the vDSO.__u32fromuapi/linux/types.havoid this issue.
Sure.
Quoted 9 lines
Quoted 8 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +2
-0@@ -22,6 +22,12 @@ VERSION { __vdso_getcpu; __vdso_time; __vdso_clock_getres; +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK + __vdso_futex_robust_try_unlock;
Quoted 6 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S VERSION { · line 29
Quoted 3 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +3
-0+ __vdso_futex_robust_try_unlock_cs_start; + __vdso_futex_robust_try_unlock_cs_success; + __vdso_futex_robust_try_unlock_cs_end;These three symbols are not meant to be used from outside the vDSO implementation, so they don't need to be exported by the linkerscripts.
You are right in principle and I had it differently in the first place until I realized that exposing them is useful for debugging and validation purposes.
As I pointed out in the cover letter you can use GDB to actually verify the fixup magic. That makes it obviously possible to write a user space selftest without requiring to decode the internals of the VDSO.
Due to my pretty limited userspace DSO knowledge that was the best I came up with. If you have a better idea, please let me know.
Thanks,
tglx
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 12 lines · Thomas Gleixner
Quoted 10 lines · Thomas Weißschuh
arch/x86/entry/vdso/common/vfutex.c line 10
Quoted 7 lines · Thomas Gleixner
arch/x86/entry/vdso/common/vfutex.c new file +7
-0+/* +* Compat enabled kernels have to take the size bit into account to support the +* mixed size use case of gaming emulators. Contrary to the kernel robust unlock +* mechanism all of this does not test for the 32-bit modifier in 32-bit VDSOs +* and in compat disabled kernels. User space can keep the pieces. +*/ +#if defined(CONFIG_X86_64 ) && !defined(BUILD_VDSO32_64 )#ifndef
__x86_64__?
Quoted 1 line
#ifdef :)
Indeed :-)
Quoted 2 lines
Just had to double check and convince myself that
__x86_64__is set when building for theX86_X32ABI. Seems to work.
Afaik this is mandated by the x32 ABI. Together with __ILP32__. In any case it doesn't matter as the x32 vDSO is not actually built but instead is a copy of the x86_64 one with its elf type patched around.
(...)
Quoted 20 lines
Quoted 9 lines
Quoted 8 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +2
-0@@ -22,6 +22,12 @@ VERSION { __vdso_getcpu; __vdso_time; __vdso_clock_getres; +#ifdef CONFIG_FUTEX_ROBUST_UNLOCK + __vdso_futex_robust_try_unlock;Quoted 6 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S VERSION { · line 29
Quoted 3 lines
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +3
-0+ __vdso_futex_robust_try_unlock_cs_start; + __vdso_futex_robust_try_unlock_cs_success; + __vdso_futex_robust_try_unlock_cs_end;These three symbols are not meant to be used from outside the vDSO implementation, so they don't need to be exported by the linkerscripts.
You are right in principle and I had it differently in the first place until I realized that exposing them is useful for debugging and validation purposes.
Quoted 6 lines
As I pointed out in the cover letter you can use GDB to actually verify the fixup magic. That makes it obviously possible to write a user space selftest without requiring to decode the internals of the VDSO.
Due to my pretty limited userspace DSO knowledge that was the best I came up with. If you have a better idea, please let me know.
I would have expected GDB to be able to use the separate vDSO debugging symbols to find these symbols. So far I was not able to make it work, but I blame my limited GDB knowledge.
Or move the symbols into a dedicated version to make clear that this is not a stable interface.
Thomas
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 6 lines · Thomas Weißschuh
Quoted 2 lines
Due to my pretty limited userspace DSO knowledge that was the best I came up with. If you have a better idea, please let me know.
I would have expected GDB to be able to use the separate vDSO debugging symbols to find these symbols. So far I was not able to make it work, but I blame my limited GDB knowledge.
I got it "working" by manually loading vdso64.so.dbg at the right offset, which only took about 10 attempts to get it right. Then you can use actual local symbols.
vdso2c picks them up correctly too.
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 8 lines · Thomas Gleixner
Quoted 6 lines · Thomas Weißschuh
Quoted 2 lines
Due to my pretty limited userspace DSO knowledge that was the best I came up with. If you have a better idea, please let me know.
I would have expected GDB to be able to use the separate vDSO debugging symbols to find these symbols. So far I was not able to make it work, but I blame my limited GDB knowledge.
Quoted 5 lines
I got it "working" by manually loading vdso64.so.dbg at the right offset, which only took about 10 attempts to get it right. Then you can use actual local symbols.
vdso2c picks them up correctly too.
What also works is to have GDB look up the debug symbols through their debug ids. At this point the load address of the vDSO is already known.
$ make vdso_install INSTALL_MOD_PATH=$SOME_DIRECTORY $ gdb -ex "set debug-file-directory $SOME_DIRECTORY/lib/modules/$(uname -r)/vdso" $BINARY
Depending on the distribution the vDSO from the kernel package might already be set up to be found automatically.
Maybe we could add a helper to scripts/gdb/ which uses $(vdso-install-y) to either populate a debug-file-directory automatically or hook into the GDB lookup process to avoid these manual steps.
Thomas
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
* Thomas Weißschuh:
Quoted 21 lines · Thomas Gleixner
Quoted 8 lines · Thomas Weißschuh
Quoted 6 lines
Quoted 2 lines
Due to my pretty limited userspace DSO knowledge that was the best I came up with. If you have a better idea, please let me know.
I would have expected GDB to be able to use the separate vDSO debugging symbols to find these symbols. So far I was not able to make it work, but I blame my limited GDB knowledge.
Quoted 5 lines
I got it "working" by manually loading vdso64.so.dbg at the right offset, which only took about 10 attempts to get it right. Then you can use actual local symbols.
vdso2c picks them up correctly too.
What also works is to have GDB look up the debug symbols through their debug ids. At this point the load address of the vDSO is already known.
$ make vdso_install INSTALL_MOD_PATH=$SOME_DIRECTORY $ gdb -ex "set debug-file-directory $SOME_DIRECTORY/lib/modules/$(uname -r)/vdso" $BINARY
Quoted 6 lines
Depending on the distribution the vDSO from the kernel package might already be set up to be found automatically.
Maybe we could add a helper to
scripts/gdb/which uses $(vdso-install-y) to either populate a debug-file-directory automatically or hook into the GDB lookup process to avoid these manual steps.
If the the /lib/modules/$(uname -r)/vdso/vdsoNN.so path is standard, we can use it in the link map, at the expense of an additional system call during process startup. It isn't just a performance cost. We've seen seccomp filters that kill the process on uname calls.
Thanks,
Florian
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 26 lines · Florian Weimer
* Thomas Weißschuh:
Quoted 24 lines · Thomas Gleixner
Quoted 8 lines · Thomas Weißschuh
Quoted 6 lines
Quoted 2 lines
Due to my pretty limited userspace DSO knowledge that was the best I came up with. If you have a better idea, please let me know.
I would have expected GDB to be able to use the separate vDSO debugging symbols to find these symbols. So far I was not able to make it work, but I blame my limited GDB knowledge.
Quoted 5 lines
I got it "working" by manually loading vdso64.so.dbg at the right offset, which only took about 10 attempts to get it right. Then you can use actual local symbols.
vdso2c picks them up correctly too.
What also works is to have GDB look up the debug symbols through their debug ids. At this point the load address of the vDSO is already known.
$ make vdso_install INSTALL_MOD_PATH=$SOME_DIRECTORY $ gdb -ex "set debug-file-directory $SOME_DIRECTORY/lib/modules/$(uname -r)/vdso" $BINARYDepending on the distribution the vDSO from the kernel package might already be set up to be found automatically.
Quoted 8 lines
Quoted 3 lines
Maybe we could add a helper to
scripts/gdb/which uses $(vdso-install-y) to either populate a debug-file-directory automatically or hook into the GDB lookup process to avoid these manual steps.If the the /lib/modules/$(uname -r)/vdso/vdsoNN.so path is standard, we can use it in the link map, at the expense of an additional system call during process startup. It isn't just a performance cost. We've seen seccomp filters that kill the process on uname calls.
It's not a standard, only what 'make vdso_install' ends up doing and some package managers are copying. Also the file is not guaranteed to be there, as it is only part of the kernel debug packages, if packaged at all.
So I don't think it makes sense to integrate it into low-level system components. For debugging, the distros can already make the file available through /usr/lib/debug/ so it works out-of-the box. (As done by Debian)
Thomas
Peter Zijlstra
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Florian WeimerQuoted 4 lines · Florian Weimer
If the the /lib/modules/$(uname -r)/vdso/vdsoNN.so path is standard, we can use it in the link map, at the expense of an additional system call during process startup. It isn't just a performance cost. We've seen seccomp filters that kill the process on uname calls.
I've always though it would be a good idea to expose vdsoNN.so somewhere in our virtual filesystem, either /proc or /sys or whatever.
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 8 lines · Peter Zijlstra
Quoted 4 lines · Florian Weimer
If the the /lib/modules/$(uname -r)/vdso/vdsoNN.so path is standard, we can use it in the link map, at the expense of an additional system call during process startup. It isn't just a performance cost. We've seen seccomp filters that kill the process on uname calls.
I've always though it would be a good idea to expose vdsoNN.so somewhere in our virtual filesystem, either /proc or /sys or whatever.
Yes, exposing this through a virtual fs would be really sensible. It's not a lot of data.
VDSO debug info is ~40K extra per vdsoNN.so and vdsoNN.so.dbg compresses to ~25k with zstd. Converting that whole thing to a binary blob and bake it into the kernel is nor rocket science.
Thanks,
tglx
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Thomas WeißschuhQuoted 8 lines · Thomas Weißschuh
Quoted 1 line
vdso2c picks them up correctly too.
What also works is to have GDB look up the debug symbols through their debug ids. At this point the load address of the vDSO is already known.
$ make vdso_install INSTALL_MOD_PATH=$SOME_DIRECTORY $ gdb -ex "set debug-file-directory $SOME_DIRECTORY/lib/modules/$(uname -r)/vdso" $BINARY
Quoted 6 lines
Depending on the distribution the vDSO from the kernel package might already be set up to be found automatically.
Maybe we could add a helper to
scripts/gdb/which uses $(vdso-install-y) to either populate a debug-file-directory automatically or hook into the GDB lookup process to avoid these manual steps.
Is this a complete vdso.so as mapped in process or just the debug symbols or both?
Looking at my Debian thingy this seems to be there as of
https://packages.debian.org/sid/amd64/linux-image-6.19.8+deb14-amd64-dbg/filelist
| /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vdso/vdso32.so | /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vdso/vdso64.so | /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vdso/vdsox32.so | /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vmlinux
or do we talk about other things? Usually there is -dbgsym with the stripped out debug symbols under /usr/lib/debug/.build-id/ but the kernel seems different.
Quoted 1 line
Thomas
Sebastian
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 12 lines · Sebastian Andrzej Siewior
Quoted 11 lines · Thomas Weißschuh
Quoted 1 line
vdso2c picks them up correctly too.
What also works is to have GDB look up the debug symbols through their debug ids. At this point the load address of the vDSO is already known.
$ make vdso_install INSTALL_MOD_PATH=$SOME_DIRECTORY $ gdb -ex "set debug-file-directory $SOME_DIRECTORY/lib/modules/$(uname -r)/vdso" $BINARYDepending on the distribution the vDSO from the kernel package might already be set up to be found automatically.
Quoted 6 lines
Quoted 3 lines
Maybe we could add a helper to
scripts/gdb/which uses $(vdso-install-y) to either populate a debug-file-directory automatically or hook into the GDB lookup process to avoid these manual steps.Is this a complete vdso.so as mapped in process or just the debug symbols or both?
$(vdso-install-y) references full vDSO images including executable code and debug symbols. The one mapped into userspace is stripped.
Quoted 11 lines
Looking at my Debian thingy this seems to be there as of
https://packages.debian.org/sid/amd64/linux-image-6.19.8+deb14-amd64-dbg/filelist| /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vdso/vdso32.so | /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vdso/vdso64.so | /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vdso/vdsox32.so | /usr/lib/debug/lib/modules/6.19.8+deb14-amd64/vmlinuxor do we talk about other things? Usually there is -dbgsym with the stripped out debug symbols under
/usr/lib/debug/.build-id/but the kernel seems different.
$ dpkg -L linux-image-6.12.74+deb13+1-amd64-dbg | grep -e vdso/vdso -e /usr/lib/debug/.build-id/ /usr/lib/debug/lib/modules/6.12.74+deb13+1-amd64/vdso/vdso32.so /usr/lib/debug/lib/modules/6.12.74+deb13+1-amd64/vdso/vdso64.so /usr/lib/debug/lib/modules/6.12.74+deb13+1-amd64/vdso/vdsox32.so /usr/lib/debug/.build-id/4a/bb1230e4abe0e2d856e1a304b392831ab7a8e1.debug /usr/lib/debug/.build-id/5f/a3a3ed11e017bcc765ade0997821383a7d4df8.debug /usr/lib/debug/.build-id/6e/d4f6c60913c24e158bbdfd680b8a1c1b07d8a4.debug
$ readlink /usr/lib/debug/.build-id/4a/bb1230e4abe0e2d856e1a304b392831ab7a8e1.debug ../../lib/modules/6.12.74+deb13+1-amd64/vdso/vdso32.so
This looks as expected to me. It doesn't help for development kernels, though. Also kbuild 'make deb-pkg' does *not* package these, see also [0].
One thing to note is that Debian does not follow the 'make vdso_install' layout. Not that this would be a requirement or anything.
[0] https://lore.kernel.org/lkml/20260318-kbuild-pacman-vdso-install-v1-1-48ceb31c0e80@weissschuh.net/
Thomas
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 4 lines · Thomas Weißschuh
$ readlink /usr/lib/debug/.build-id/4a/bb1230e4abe0e2d856e1a304b392831ab7a8e1.debug ../../lib/modules/6.12.74+deb13+1-amd64/vdso/vdso32.soThis looks as expected to me. It doesn't help for development kernels, though.
Good to know.
Quoted 1 line
Also kbuild 'make deb-pkg' does *not* package these, see also [0].
This is not used by Debian kernel building package but by people building a Debian package. If it is useful maybe add it.
Quoted 2 lines
One thing to note is that Debian does not follow the 'make
vdso_install' layout. Not that this would be a requirement or anything.
Okay. Well, if it needs to change or anything I know who to poke.
Quoted 1 line
Thomas
Sebastian
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Thomas Gleixner* Thomas Gleixner:
Quoted 8 lines
Arguably this could be avoided by providing separate functions and making the IP range for the quick check in the exit to user path cover the whole text section which contains the two functions. But that's not a win at all because:
1) User space needs to handle the two variants instead of just relying on a bit which can be saved in the mutex at initialization time.
I'm pretty sure that on the user-space side, we wouldn't have cross-word-size operations (e.g., 64-bit code working on both 64-bit and 32-bit robust mutexes). Certainly not within libcs. The other point about complexity is of course still valid.
Thanks,
Florian
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 7 lines · Florian Weimer
* Thomas Gleixner:
Quoted 5 lines
Arguably this could be avoided by providing separate functions and making the IP range for the quick check in the exit to user path cover the whole text section which contains the two functions. But that's not a win at all because:
Quoted 8 lines
Quoted 3 lines
1) User space needs to handle the two variants instead of just relying on a bit which can be saved in the mutex at initialization time.
I'm pretty sure that on the user-space side, we wouldn't have cross-word-size operations (e.g., 64-bit code working on both 64-bit and 32-bit robust mutexes). Certainly not within libcs. The other point about complexity is of course still valid.
Right, I know that no libc implementation supports such an insanity, but the kernel unfortunately allows to do so and it's used in the wild :(
So we have to deal with it somehow and the size modifier was the most straight forward solution I could come up with. I'm all ears if someone has a better idea.
That said, do you see any issue from libc size versus extending the WAKE/UNLOCK_PI functionality with that UNLOCK_ROBUST functionality?
I did some basic performance tests in the meanwhile with an open coded mutex implementation. I can't observe any significant difference between doing the unlock in user space or letting the kernel do it, but that needs of course more scrunity.
Thanks,
tglx
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
* Thomas Gleixner:
Quoted 17 lines · Florian Weimer
Quoted 7 lines
* Thomas Gleixner:
Quoted 5 lines
Arguably this could be avoided by providing separate functions and making the IP range for the quick check in the exit to user path cover the whole text section which contains the two functions. But that's not a win at all because:
Quoted 8 lines
Quoted 3 lines
1) User space needs to handle the two variants instead of just relying on a bit which can be saved in the mutex at initialization time.
I'm pretty sure that on the user-space side, we wouldn't have cross-word-size operations (e.g., 64-bit code working on both 64-bit and 32-bit robust mutexes). Certainly not within libcs. The other point about complexity is of course still valid.
Quoted 6 lines
Right, I know that no libc implementation supports such an insanity, but the kernel unfortunately allows to do so and it's used in the wild :(
So we have to deal with it somehow and the size modifier was the most straight forward solution I could come up with. I'm all ears if someone has a better idea.
Maybe a separate futex op? And the vDSO would have the futex call, mangle uaddr2 as required for the shared code section that handles both ops?
As far as I can tell at this point, the current proposal should work. We'd probably start with using the syscall-based unlock.
Thanks,
Florian
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 8 lines · Florian Weimer
* Thomas Gleixner:
Quoted 6 lines
Right, I know that no libc implementation supports such an insanity, but the kernel unfortunately allows to do so and it's used in the wild :(
So we have to deal with it somehow and the size modifier was the most straight forward solution I could come up with. I'm all ears if someone has a better idea.
Quoted 6 lines
Maybe a separate futex op? And the vDSO would have the futex call, mangle uaddr2 as required for the shared code section that handles both ops?
As far as I can tell at this point, the current proposal should work. We'd probably start with using the syscall-based unlock.
Something like the below compiled but untested delta diff which includes also the other unrelated feedback fixups?
Thanks,
tglx
---
arch/x86/entry/vdso/common/vfutex.c +181 -35
@@ -1,72 +1,218 @@// SPDX-License-Identifier: GPL-2.0-only #include <vdso/futex.h> +/* +* Assembly template for the try unlock functions. The basic functionality for +* 64-bit is: +* +* At the call site: +* mov &lock, %rdi Store the lock pointer in RDI +* mov &pop, %rdx Store the pending op pointer in RDX +* mov TID, %esi Store the thread's TID in ESI +* +* 64-bit unlock function: +* 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 $0, (%rdx) Set the pending op pointer to 0 +* .Lcs_end: End of the critical section +* +* For COMPAT enabled 64-bit kernels this is a bit more complex because the size +* of the @pop pointer has to be determined in the success section: +* +* At the 64-bit call site: +* mov &lock, %rdi Store the lock pointer in RDI +* mov &pop, %rdx Store the pending op pointer in RDX +* mov TID, %esi Store the thread's TID in ESI +* +* At the 32-bit call site: +* mov &lock, %edi Store the lock pointer in EDI +* mov &pop, %edx Store the pending op pointer in EDX +* mov TID, %esi Store the thread's TID in ESI +* +* The 32-bit entry point: +* or $0x1, %edx Mark the op pointer 32-bit +* +* Common unlock function: +* mov esi, %eax Move the TID into EAX +* xor %ecx, %ecx Clear ECX +* mov %rdx, %rsi Store the op pointer in RSI +* and ~0x1, %rsi Clear the size bit in RSI +* 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 +* test $0x1, %rdx Test the 32-bit size bit in the original pointer +* jz .Lop64 If not set, clear 64-bit +* movl $0, (%rsi) Set the 32-bit pending op pointer to 0 +* jmp .Lcs_end Leave the critical section +* .Lop64: movq $0, (%rsi) Set the 64-bit pending op pointer to 0 +* .Lcs_end: End of the critical section +* +* The 32-bit VDSO needs to set the 32-bit size bit as well to keep the code +* compatible for the kernel side fixup function, but it does not require the +* size evaluation in the success path. +* +* At the 32-bit call site: +* mov &lock, %edi Store the lock pointer in EDI +* mov &pop, %edx Store the pending op pointer in EDX +* mov TID, %esi Store the thread's TID in ESI +* +* The 32-bit entry point does: +* or $0x1, %edx Mark the op pointer 32-bit +* +* 32-bit unlock function: +* mov esi, %eax Move the TID into EAX +* xor %ecx, %ecx Clear ECX +* mov %edx, %esi Store the op pointer in ESI +* and ~0x1, %esi Clear the size bit in ESI +* lock_cmpxchgl %ecx, (%edi) 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 +* movl $0, (%esi) Set the 32-bit pending op pointer to 0 +* .Lcs_end: End of the critical section +* +* The pointer modification makes sure that the unlock function can determine +* the pending op pointer size correctly and clear either 32 or 64 bit. +* +* The intermediate storage of the unmangled pointer (bit 0 cleared) in [ER]SI +* makes sure that the store hits the right address. +* +* The mangled pointer (bit 0 set for 32-bit) stays in [ER]DX so that the kernel +* side fixup function can determine the storage size correctly and always +* retrieve regs->rdx without any extra knowledge of the actual code path taken +* or checking the compat mode of the task. +* +* The .Lcs_success label is technically not required for a pure 64-bit and the +* 32-bit VDSO but is kept there for simplicity. In those cases the ZF flag in +* regs->eflags is authoritative for the whole critical section and no further +* evaluation is required. +* +* In the 64-bit compat case the .Lcs_success label is required because the +* pointer size check modifies the ZF flag, which means it is only valid for the +* case where .Lcs_start <= regs->ip < L.cs_success, which is obviously the +* same as l.cs_start == regs->ip for x86. +* +* That's still a valuable distinction for clarity to keep the ASM template the +* same for all case. This is also a template for other architectures which +* might have different requirements even for the non COMPAT case. +* +* That means in the 64-bit compat case the decision to do the fixup is: +* +* if (regs->ip >= .Lcs_start && regs->ip < L.cs_success) +* return (regs->eflags & ZF); +* return regs->ip < .Lcs_end; +* +* As the initial critical section check in the return to user space code +* already established that: +* +* .Lcs_start <= regs->ip < L.cs_end +* +* that decision can be simplified to: +* +* return regs->ip >= L.cs_success || regs->eflags & ZF; +* +*/ +#define robust_try_unlock_asm (__tid, __lock, __pop) \ + asm volatile ( \ + ".global __kernel_futex_robust_try_unlock_cs_start \n" \ + ".global __kernel_futex_robust_try_unlock_cs_success \n" \ + ".global __kernel_futex_robust_try_unlock_cs_end \n" \ + " \n" \ + " lock cmpxchgl %[val], (%[ptr]) \n" \ + " \n" \ + "__kernel_futex_robust_try_unlock_cs_start: \n" \ + " \n" \ + " jnz __kernel_futex_robust_try_unlock_cs_end \n" \ + " \n" \ + "__kernel_futex_robust_try_unlock_cs_success: \n" \ + " \n" \ + ASM_CLEAR_PTR \ + " \n" \ + "__kernel_futex_robust_try_unlock_cs_end: \n" \ + : [tid] "+a" (__tid) \ + : [ptr] "D" (__lock), \ + [pop] "d" (__pop), \ + [val] "r" (0) \ + ASM_PAD_CONSTRAINT(__pop) \ + : "memory" \ + ) +/* * Compat enabled kernels have to take the size bit into account to support the * mixed size use case of gaming emulators. Contrary to the kernel robust unlock * mechanism all of this does not test for the 32-bit modifier in 32-bit VDSOs * and in compat disabled kernels. User space can keep the pieces. */ -#if defined(CONFIG_X86_64 ) && !defined(BUILD_VDSO32_64 ) - +#ifdef __x86_64__#ifdef CONFIG_COMPAT # define ASM_CLEAR_PTR \ " testl $1, (%[pop]) \n" \ " jz .Lop64 \n" \ " movl $0, (%[pad]) \n" \ - " jmp __vdso_futex_robust_try_unlock_cs_end \n" \ + " jmp __kernel_futex_robust_try_unlock_cs_end \n" \ ".Lop64: \n" \ " movq $0, (%[pad]) \n" -# define ASM_PAD_CONSTRAINT ,[pad] "S" (((unsigned long)pop) & ~0x1UL) +# define ASM_PAD_CONSTRAINT (__pop) ,[pad] "S" (((unsigned long)__pop) & ~0x1UL) + +__u32 noinline __vdso_futex_robust_try_unlock_64(__u32 *lock, __u32 tid, __u64 *pop) +{ +robust_try_unlock_asm (lock, tid, pop); +return tid; +} + +__u32 noinline __vdso_futex_robust_try_unlock_32(__u32 *lock, __u32 tid, __u32 *pop) +{ +__u64 pop_addr = ((u64 ) pop) |FUTEX_ROBUST_UNLOCK_MOD_32BIT ; + +return __vdso_futex_robust_try_unlock_64 (lock, tid, (__u64 *)pop_addr); +} + +__u32 futex_robust_try_unlock_64 (__u32 *,__u32 ,__u64 *) + __attribute__((weak,alias ("__vdso_futex_robust_try_unlock_64" ))); + +__u32 futex_robust_try_unlock_32 (__u32 *,__u32 ,__u32 *) + __attribute__((weak,alias ("__vdso_futex_robust_try_unlock_32" )));#else /* CONFIG_COMPAT */ # define ASM_CLEAR_PTR \ " movq $0, (%[pop]) \n" -# define ASM_PAD_CONSTRAINT +# define ASM_PAD_CONSTRAINT (__pop) + +__u32 noinline __vdso_futex_robust_try_unlock_64(__u32 *lock, __u32 tid, __u64 *pop) +{ +robust_try_unlock_asm (lock, tid, pop); +return tid; +} + +__u32 futex_robust_try_unlock_64 (__u32 *,__u32 ,__u64 *) + __attribute__((weak,alias ("__vdso_futex_robust_try_unlock_64" )));#endif /* !CONFIG_COMPAT */ -#else /* CONFIG_X86_64 && !BUILD_VDSO32_64 */ +#else /* __x86_64__ */ # define ASM_CLEAR_PTR \ " movl $0, (%[pad]) \n" -# define ASM_PAD_CONSTRAINT ,[pad] "S" (((unsigned long)pop) & ~0x1UL) - -#endif /* !CONFIG_X86_64 || BUILD_VDSO32_64 */ +# define ASM_PAD_CONSTRAINT (__pop) ,[pad] "S" (((unsigned long)__pop) & ~0x1UL) -uint32_t __vdso_futex_robust_try_unlock (uint32_t *lock,uint32_t tid,void *pop) +__u32 noinline __vdso_futex_robust_try_unlock_32(__u32 *lock, __u32 tid, __u32 *pop) { - asmvolatile ( -".global __vdso_futex_robust_try_unlock_cs_start \n" -".global __vdso_futex_robust_try_unlock_cs_success \n" -".global __vdso_futex_robust_try_unlock_cs_end \n" -" \n" -" lock cmpxchgl %[val], (%[ptr]) \n" -" \n" -"__vdso_futex_robust_try_unlock_cs_start: \n" -" \n" -" jnz __vdso_futex_robust_try_unlock_cs_end \n" -" \n" -"__vdso_futex_robust_try_unlock_cs_success: \n" -" \n" -ASM_CLEAR_PTR -" \n" -"__vdso_futex_robust_try_unlock_cs_end: \n" - : [tid]"+a" (tid) - : [ptr]"D" (lock), - [pop]"d" (pop), - [val]"r" (0 ) - ASM_PAD_CONSTRAINT - :"memory" - ); +__u32 pop_addr = ((u32 ) pop) |FUTEX_ROBUST_UNLOCK_MOD_32BIT ; +robust_try_unlock_asm (lock, tid, (__u32 *)pop_addr);return tid; } -uint32_t futex_robust_try_unlock (uint32_t *,uint32_t ,void **) - __attribute__((weak,alias ("__vdso_futex_robust_try_unlock" ))); +__u32 futex_robust_try_unlock_32 (__u32 *,__u32 ,__u32 *) + __attribute__((weak,alias ("__vdso_futex_robust_try_unlock_32" ))); +#endif /* !__x86_64__ */
arch/x86/entry/vdso/vdso32/vdso32.lds.S +4 -4
@@ -31,10 +31,10 @@ 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_try_unlock_32; + __kernel_futex_robust_try_unlock_cs_start; + __kernel_futex_robust_try_unlock_cs_success; + __kernel_futex_robust_try_unlock_cs_end; #endif };
arch/x86/entry/vdso/vdso64/vdso64.lds.S +5 -4
@@ -33,10 +33,11 @@ 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_try_unlock_64; + __vdso_futex_robust_try_unlock_32; + __kernel_futex_robust_try_unlock_cs_start; + __kernel_futex_robust_try_unlock_cs_success; + __kernel_futex_robust_try_unlock_cs_end; #endif local: *; };
arch/x86/entry/vdso/vdso64/vdsox32.lds.S +2 -1
@@ -23,7 +23,8 @@ VERSION { __vdso_time; __vdso_clock_getres; #ifdef CONFIG_FUTEX_ROBUST_UNLOCK - __vdso_futex_robust_try_unlock; + __vdso_futex_robust_try_unlock_64; + __vdso_futex_robust_try_unlock_32; __vdso_futex_robust_try_unlock_cs_start; __vdso_futex_robust_try_unlock_cs_success; __vdso_futex_robust_try_unlock_cs_end;
include/linux/futex_types.h +9 -5
@@ -11,13 +11,15 @@ struct futex_pi_state;struct robust_list_head ;/** -* struct futex_ctrl - Futex related per task data +* 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_ctrl { +struct futex_sched_data {struct robust_list_head __user *robust_list;#ifdef CONFIG_COMPAT struct compat_robust_list_head __user *compat_robust_list;
@@ -27,9 +29,6 @@ struct futex_ctrl {struct mutex exit_mutex;unsigned int state; }; -#else -struct futex_ctrl { }; -#endif /* !CONFIG_FUTEX */ /** * struct futex_mm_data - Futex related per MM data
@@ -71,4 +70,9 @@ struct futex_mm_data {#endif }; +#else +struct futex_sched_data { }; +struct futex_mm_data { }; +#endif /* !CONFIG_FUTEX */ +#endif /* _LINUX_FUTEX_TYPES_H */
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/uapi/linux/futex.h +1 -17
@@ -26,6 +26,7 @@#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 | FUTEX_UNLOCK_ROBUST)#define FUTEX_WAIT_PRIVATE (FUTEX_WAIT | FUTEX_PRIVATE_FLAG)
@@ -182,23 +183,6 @@ struct robust_list_head {#define FUTEX_ROBUST_MOD_PI (0x1UL)#define FUTEX_ROBUST_MOD_MASK (FUTEX_ROBUST_MOD_PI) -/* -* Modifier for FUTEX_ROBUST_UNLOCK uaddr2. Required to distinguish the storage -* size for the robust_list_head::list_pending_op. This solves two problems: -* -* 1) COMPAT tasks -* -* 2) The mixed mode magic gaming use case which has both 32-bit and 64-bit -* robust lists. Oh well.... -* -* Long story short: 32-bit userspace must set this bit unconditionally to -* ensure that it can run on a 64-bit kernel in compat mode. If user space -* screws that up a 64-bit kernel will happily clear the full 64-bits. 32-bit -* kernels return an error code if the bit is not set. -*/ -#define FUTEX_ROBUST_UNLOCK_MOD_32BIT (0x1UL) -#define FUTEX_ROBUST_UNLOCK_MOD_MASK (FUTEX_ROBUST_UNLOCK_MOD_32BIT) -/* * bitset with all bits set for the FUTEX_xxx_BITSET OPs to request a * match of any bit.
include/vdso/futex.h +21 -5
@@ -2,12 +2,11 @@#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_try_unlock_64 - Try to unlock an uncontended robust futex +* with a 64-bit 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
@@ -39,6 +38,23 @@ struct robust_list;* @uaddr2 argument for sys_futex(FUTEX_ROBUST_UNLOCK) operations. See the * modifier and the related documentation in include/uapi/linux/futex.h */ -uint32_t __vdso_futex_robust_try_unlock (uint32_t *lock,uint32_t tid,void *op); +__u32 __vdso_futex_robust_try_unlock_64 (__u32 *lock,__u32 tid,__u64 *op); + +/** +* __vdso_futex_robust_try_unlock_32 - Try to unlock an uncontended robust futex +* with a 32-bit 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 +* +* Return: The content of *@lock. On success this is the same as @tid. +* +* Same as __vdso_futex_robust_try_unlock_64() just with a 32-bit @op pointer. +*/ +__u32 __vdso_futex_robust_try_unlock_32 (__u32 *lock,__u32 tid,__u32 *op); + +/* Modifier to convey the size of the op pointer */ +#define FUTEX_ROBUST_UNLOCK_MOD_32BIT (0x1UL) +#define FUTEX_ROBUST_UNLOCK_MOD_MASK (FUTEX_ROBUST_UNLOCK_MOD_32BIT)#endif
kernel/futex/core.c +18 -11
@@ -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,28 @@ bool futex_robust_list_clear_pending(void __user *pop) }#ifdef CONFIG_FUTEX_ROBUST_UNLOCK +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 ); +} +void __futex_fixup_robust_unlock (struct pt_regs *regs) { +unsigned int flags =0 ;void __user *pop;if (!arch_futex_needs_robust_unlock_fixup (regs))return ; pop =arch_futex_robust_unlock_get_pop (regs); -futex_robust_list_clear_pending (pop); + +if (mask_pop_addr (&pop)) + flags =FUTEX_ROBUST_UNLOCK_MOD_32BIT ; + +futex_robust_list_clear_pending (pop, flags); }#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 +1 -1
@@ -166,7 +166,7 @@ static bool futex_robust_unlock(u32 __user *uaddr, unsigned int flags, void __us* deeper trouble as the robust list head is usually part of TLS. The * chance of survival is close to zero. */ -return futex_robust_list_clear_pending (pop); +return futex_robust_list_clear_pending (pop, flags); }/*
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 3 lines · Thomas Gleixner
On Tue, Mar 17 2026 at 11:37, Florian Weimer wrote:
Something like the below compiled but untested delta diff which includes also the other unrelated feedback fixups?
The more I look into it, the more I regret that we allowed mixed mode in the first place. Which way I turn it around the code becomes more horrible than it really should be.
If I understand it correctly then the only real world use case is the x86 emulator for ARM64. That made me think about possible options to keep the insanity restricted.
1) Delegate the problem to the ARM64 people :)
2) Make that mixed size mode depend on a config option
3) Require that such a use case issues a prctl to switch into that special case mode.
or a combination of those.
Andre?
Peter Zijlstra
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 15 lines · Thomas Gleixner
Quoted 3 lines · Thomas Gleixner
On Tue, Mar 17 2026 at 11:37, Florian Weimer wrote:
Something like the below compiled but untested delta diff which includes also the other unrelated feedback fixups?The more I look into it, the more I regret that we allowed mixed mode in the first place. Which way I turn it around the code becomes more horrible than it really should be.
If I understand it correctly then the only real world use case is the x86 emulator for ARM64. That made me think about possible options to keep the insanity restricted.
1) Delegate the problem to the ARM64 people :)
Quoted 8 lines
2) Make that mixed size mode depend on a config option
3) Require that such a use case issues a prctl to switch into that special case mode.
or a combination of those.
Andre?
Well, he has this patch set that creates multiple lists, which would nicely solve things for FEX I recon.
André Almeida
reviewerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Thomas GleixnerEm 18/03/2026 19:08, Thomas Gleixner escreveu:
Quoted 10 lines
2) Make that mixed size mode depend on a config option
3) Require that such a use case issues a prctl to switch into that special case mode.
or a combination of those.
Andre?
Those two last options works for me, if it helps to make the code more readable. However, I think that QEMU might be interested in those features as well :) I'm going to ping them
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 7 lines · André Almeida
Em 18/03/2026 19:08, Thomas Gleixner escreveu:
Quoted 6 lines
2) Make that mixed size mode depend on a config option
3) Require that such a use case issues a prctl to switch into that special case mode.
Quoted 8 lines
Quoted 4 lines
or a combination of those.
Andre?
Those two last options works for me, if it helps to make the code more readable. However, I think that QEMU might be interested in those features as well :) I'm going to ping them
I already came up with something. It makes the fixup range larger as it has to cover two functions and then pick the right one.
So the range check becomes:
if (likely(!ip_within(regs, mm->futex.cs_start, mm->futex.cs_end)))
return;
if (likely(!mm->futex.cs_multi)) {
fixup(regs, NULL);
return;
}
csr = mm->futex.cs_ranges;
for (range = 0; range < mm->futex.cs_multi; range++, csr++) {
if (ip_within(regs, csr->cs_start, csr->cs_end)) {
fixup(regs, csr);
return;
}
}Or something daft like that.
That makes the multi CS range check generic and still optimizes for the single entry case. The ASM functions become minimal w/o extra pointer size conditionals.
Thanks,
tglx
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
In reply to Thomas GleixnerQuoted 119 lines · Thomas Gleixner
When the
FUTEX_ROBUST_UNLOCKmechanism 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 addressWhen then 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 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 assembly sequence for the non COMPAT case 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 $0x0,(%rdx) // Clear list_op_pending.Lend:
retSo the decision can be simply based on the ZF state in
regs->flags.If COMPAT is enabled then the
try_unlock()function needs to take the size bit in the OP pointer into account, which makes it slightly more complex:mov %esi,%eax // Load TID into EAX mov %rdx,%rsi // Get the op pointer xor %ecx,%ecx // Set ECX to 0 and $0xfffffffffffffffe,%rsi // Clear the size bit lock cmpxchg %ecx,(%rdi) // Try the TID -> 0 transition .Lstart: jnz .Lend .Lsuccess: testl $0x1,(%rdx) // Test the size bit jz .Lop64 // Not set: 64-bit movl $0x0,(%rsi) // Clear 32-bit jmp .Lend .Lop64: movq $0x0,(%rsi) // Clear 64-bit.Lend:
retThe decision function has to check whether
regs->ipis in the success portion as the size bit test obviously modifies ZF too. If it is before .Lsuccess then ZF contains thecmpxchg()result. If it's at of after .Lsuccess then the pointer has to be cleared.The original pointer with the size bit is preserved in RDX so the fixup can utilize the existing clearing mechanism, which is used by
sys_futex().Arguably this could be avoided by providing separate functions and making the IP range for the quick check in the exit to user path cover the whole text section which contains the two functions. But that's not a win at all because:
1) User space needs to handle the two variants instead of just relying on a bit which can be saved in the mutex at initialization time.
2) The fixup decision function has then to evaluate which code path is used. That just adds more symbols and range checking for no real value.
The unlock function is inspired by an idea from Mathieu Desnoyers.
Signed-off-by Thomas Gleixner <[email protected]>--- arch/x86/Kconfig | 1 arch/x86/entry/vdso/common/vfutex.c | 72 +++++++++++++++++++++++++++++++ arch/x86/entry/vdso/vdso32/Makefile | 5 +- arch/x86/entry/vdso/vdso32/vdso32.lds.S | 6 ++ arch/x86/entry/vdso/vdso32/vfutex.c | 1 arch/x86/entry/vdso/vdso64/Makefile | 7 +-- arch/x86/entry/vdso/vdso64/vdso64.lds.S | 6 ++ arch/x86/entry/vdso/vdso64/vdsox32.lds.S | 6 ++ arch/x86/entry/vdso/vdso64/vfutex.c | 1 arch/x86/include/asm/futex_robust.h | 44 ++++++++++++++++++ 10 files changed, 144 insertions(+), 5 deletions(-)arch/x86/Kconfig +1
-0@@ -237,6 +237,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_FTRACEarch/x86/entry/vdso/common/vfutex.c new file +2
-0@@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include <vdso/futex.h>
arch/x86/entry/vdso/common/vfutex.c line 12
Quoted 10 lines
arch/x86/entry/vdso/common/vfutex.c new file +10
-0+ +/* +* Compat enabled kernels have to take the size bit into account to support the +* mixed size use case of gaming emulators. Contrary to the kernel robust unlock +* mechanism all of this does not test for the 32-bit modifier in 32-bit VDSOs +* and in compat disabled kernels. User space can keep the pieces. +*/ +#if defined(CONFIG_X86_64 ) && !defined(BUILD_VDSO32_64 ) + +#ifdef CONFIG_COMPAT
The following asm template can be substantially improved.
arch/x86/entry/vdso/common/vfutex.c line 15
Quoted 2 lines
arch/x86/entry/vdso/common/vfutex.c new file +2
-0+# define ASM_CLEAR_PTR \ + " testl $1, (%[pop]) \n" \
Please use byte-wide instruction, TESTB with address operand modifier, "%a[pop]" instead of "(%[pop])":
testb $1, %a[pop]
arch/x86/entry/vdso/common/vfutex.c line 17
Quoted 2 lines
arch/x86/entry/vdso/common/vfutex.c new file +2
-0+" jz .Lop64 \n" \ +" movl $0, (%[pad]) \n" \
Here you can reuse zero-valued operand "val" and use address operand modifier. Please note %k modifier.
movl %k[val], %a[pad]
arch/x86/entry/vdso/common/vfutex.c line 20
Quoted 3 lines
arch/x86/entry/vdso/common/vfutex.c new file +3
-0+" jmp __vdso_futex_robust_try_unlock_cs_end \n" \ +".Lop64: \n" \ +" movq $0, (%[pad]) \n"
Again, zero-valued operand "val" and address op modifier can be used here:
movq %[val], %a[pad]
arch/x86/entry/vdso/common/vfutex.c line 27
Quoted 7 lines
arch/x86/entry/vdso/common/vfutex.c new file +7
-0+ +# define ASM_PAD_CONSTRAINT ,[pad] "S" (((unsigned long)pop) & ~0x1UL) + +#else /* CONFIG_COMPAT */ + +# define ASM_CLEAR_PTR \ + " movq $0, (%[pop]) \n"
movq %[val], %a[pop]
arch/x86/entry/vdso/common/vfutex.c line 36
Quoted 9 lines
arch/x86/entry/vdso/common/vfutex.c new file +9
-0+ +# define ASM_PAD_CONSTRAINT + +#endif /* !CONFIG_COMPAT */ + +#else /* CONFIG_X86_64 && !BUILD_VDSO32_64 */ + +# define ASM_CLEAR_PTR \ + " movl $0, (%[pad]) \n"
movl %[val], %a[pad]
Quoted 14 lines
arch/x86/entry/vdso/common/vfutex.c new file +14
-0+ +# define ASM_PAD_CONSTRAINT ,[pad] "S" (((unsigned long)pop) & ~0x1UL) + +#endif /* !CONFIG_X86_64 || BUILD_VDSO32_64 */ + +uint32_t __vdso_futex_robust_try_unlock (uint32_t *lock,uint32_t tid,void *pop) +{ + asmvolatile ( +".global __vdso_futex_robust_try_unlock_cs_start \n" +".global __vdso_futex_robust_try_unlock_cs_success \n" +".global __vdso_futex_robust_try_unlock_cs_end \n" +" \n" +" lock cmpxchgl %[val], (%[ptr]) \n" + " \n"
arch/x86/entry/vdso/common/vfutex.c line 60
Quoted 10 lines
arch/x86/entry/vdso/common/vfutex.c new file +10
-0+"__vdso_futex_robust_try_unlock_cs_start: \n" +" \n" +" jnz __vdso_futex_robust_try_unlock_cs_end \n" +" \n" +"__vdso_futex_robust_try_unlock_cs_success: \n" +" \n" +ASM_CLEAR_PTR +" \n" +"__vdso_futex_robust_try_unlock_cs_end: \n" + : [tid] "+a" (tid)
You need earlyclobber here "+&a", because not all input arguemnts are read before this argument is written.
arch/x86/entry/vdso/common/vfutex.c line 63
Quoted 3 lines
arch/x86/entry/vdso/common/vfutex.c new file +3
-0+ : [ptr] "D" (lock), + [pop] "d" (pop), + [val] "r" (0)
[val] "r" (0UL), so the correct register width will be used. I'd name this operand [zero], because 0 lives here, and it will be reused in several places.
Uros.
Quoted 140 lines
arch/x86/entry/vdso/common/vfutex.c new file +9
-0+ ASM_PAD_CONSTRAINT + :"memory" + ); + +return tid; +} + +uint32_t futex_robust_try_unlock (uint32_t *,uint32_t ,void **) + __attribute__((weak,alias ("__vdso_futex_robust_try_unlock" )));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=0arch/x86/entry/vdso/vdso32/vdso32.lds.S +6
-0@@ -30,6 +30,12 @@ VERSION __vdso_clock_gettime64; __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; +#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=smallarch/x86/entry/vdso/vdso64/vdso64.lds.S +6
-0@@ -32,6 +32,12 @@ VERSION { #endif 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; +#endif local: *; }; }arch/x86/entry/vdso/vdso64/vdsox32.lds.S +6
-0@@ -22,6 +22,12 @@ VERSION { __vdso_getcpu; __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; +#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 +44
-0@@ -0,0 +1,44 @@ +/* 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 bool x86_futex_needs_robust_unlock_fixup(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. +*/ +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; +} + +#define arch_futex_robust_unlock_get_pop (regs) \ + x86_futex_robust_unlock_get_pop(regs) + +#endif /* _ASM_X86_FUTEX_ROBUST_H */
Thomas Gleixner
maintainerRe: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 4 lines · Uros Bizjak
Quoted 1 line · Thomas Gleixner
+#ifdef
CONFIG_COMPATThe following asm template can be substantially improved.
In theory.
Quoted 7 lines
arch/x86/entry/vdso/common/vfutex.c line 15
Quoted 2 lines
arch/x86/entry/vdso/common/vfutex.c new file +2
-0+# define ASM_CLEAR_PTR \ + " testl $1, (%[pop]) \n" \Please use byte-wide instruction, TESTB with address operand modifier, "%a[pop]" instead of "(%[pop])":
testb $1, %a[pop]
New fangled %a syntax seems to work. Didn't know about that. Though I'm not convinced that this is an improvement. At least not for someone who is used to read/write plain old school ASM for several decades.
Quoted 7 lines
arch/x86/entry/vdso/common/vfutex.c line 17
Quoted 2 lines
arch/x86/entry/vdso/common/vfutex.c new file +2
-0+" jz .Lop64 \n" \ +" movl $0, (%[pad]) \n" \Here you can reuse zero-valued operand "val" and use address operand modifier. Please note %k modifier.
movl %k[val], %a[pad]
...
Quoted 1 line
movq %[val], %a[pad]
But this one does not and _cannot_ work.
Error: incorrect register %rcx' used with l' suffix
Which is obvious because of the initalization:
[val] "r" (0UL)
which makes it explicitely R$$. If you change the initialization back to
[val] "r" (0) // or (0U)
the failure unsurprisingly becomes
Error: incorrect register %ecx' used with q' suffix
So much for the theory....
<SNIP>
Quoted 1 line
arch/x86/entry/vdso/common/vfutex.c line 64
Quoted 1 line
arch/x86/entry/vdso/common/vfutex.c new file +1
-0+ASM_PAD_CONSTRAINT
tons of useless quoted text
Quoted 1 line
Quoted 1 line
+#endif /*
_ASM_X86_FUTEX_ROBUST_H*/
</SNIP>
Can you please trim your replies properly?
Thanks,
tglx
Re: [patch 8/8] x86/vdso: Implement __vdso_futex_robust_try_unlock()
Quoted 27 lines · Thomas Gleixner
Quoted 4 lines · Uros Bizjak
Quoted 1 line · Thomas Gleixner
+#ifdef
CONFIG_COMPATThe following asm template can be substantially improved.
In theory.
Quoted 7 lines
arch/x86/entry/vdso/common/vfutex.c line 15
Quoted 2 lines
arch/x86/entry/vdso/common/vfutex.c new file +2
-0+# define ASM_CLEAR_PTR \ + " testl $1, (%[pop]) \n" \Please use byte-wide instruction, TESTB with address operand modifier, "%a[pop]" instead of "(%[pop])":
testb $1, %a[pop]
New fangled %a syntax seems to work. Didn't know about that. Though I'm not convinced that this is an improvement. At least not for someone who is used to read/write plain old school ASM for several decades.
Quoted 6 lines
arch/x86/entry/vdso/common/vfutex.c line 17
Quoted 2 lines
arch/x86/entry/vdso/common/vfutex.c new file +2
-0+" jz .Lop64 \n" \ +" movl $0, (%[pad]) \n" \Here you can reuse zero-valued operand "val" and use address operand modifier. Please note %k modifier.
Quoted 7 lines
Quoted 1 line
movl %k[val], %a[pad]
...
Quoted 1 line
movq %[val], %a[pad]
But this one does not and _cannot_ work.
Error: incorrect register
%rcx' used withl' suffix
Ah, I missed:
" lock cmpxchgl %[val], (%[ptr]) \n"
Please use %k[val] here and it will work. %k forces %ecx.
Quoted 3 lines
Which is obvious because of the initalization:
[val] "r" (0UL)
You have to force 0 to %r$$ for x86_64 when movq is used. The resulting code (xorl %ecx, %ecx) is the same, but the compiler knows what type of value is created here.
Uros.