futex: Cure exit race
PATCH 1/1 futex: Cure exit race
Stefan reported, that the glibc tst-robustpi4 test case fails occasionally. That case creates the following race between sys_exit() and sys_futex(LOCK_PI):
CPU0 CPU1
sys_exit() sys_futex()
do_exit() futex_lock_pi()
exit_signals(tsk) No waiters:
tsk->flags |= PF_EXITING; *uaddr == 0x00000PID
mm_release(tsk) Set waiter bit
exit_robust_list(tsk) { *uaddr = 0x80000PID;
Set owner died attach_to_pi_owner() {
*uaddr = 0xC0000000; tsk = get_task(PID);
} if (!tsk->flags & PF_EXITING) {
... attach();
tsk->flags |= PF_EXITPIDONE; } else {
if (!(tsk->flags & PF_EXITPIDONE))
return -EAGAIN;
return -ESRCH; <--- FAIL
}ESRCH is returned all the way to user space, which triggers the glibc test case assert. Returning ESRCH unconditionally is wrong here because the user space value has been changed by the exiting task to 0xC0000000, i.e. the FUTEX_OWNER_DIED bit is set and the futex PID value has been cleared. This is a valid state and the kernel has to handle it, i.e. taking the futex.
Cure it by rereading the user space value when PF_EXITING and PF_EXITPIDONE is set in the task which owns the futex. If the value has changed, let the kernel retry the operation, which includes all regular sanity checks and correctly handles the FUTEX_OWNER_DIED case.
If it hasn't changed, then return ESRCH as there is no way to distinguish this case from malfunctioning user space. This happens when the exiting task did not have a robust list, the robust list was corrupted or the user space value in the futex was simply bogus.
--- kernel/futex.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-)
kernel/futex.c +53 -4
@@ -1148,11 +1148,60 @@ static int attach_to_pi_state(u32 __userreturn ret; } +static int handle_exit_race (u32 __user *uaddr,u32 uval,struct task_struct *tsk) +{ +u32 uval2; + +/* +* If PF_EXITPIDONE is not yet set try again. +*/ +if (!(tsk->flags &PF_EXITPIDONE )) +return -EAGAIN ; + +/* +* Reread the user space value to handle the following situation: +* +* CPU0 CPU1 +* +* sys_exit() sys_futex() +* do_exit() futex_lock_pi() +* exit_signals(tsk) No waiters: +* tsk->flags |= PF_EXITING; *uaddr == 0x00000PID +* mm_release(tsk) Set waiter bit +* exit_robust_list(tsk) { *uaddr = 0x80000PID; +* Set owner died attach_to_pi_owner() { +* *uaddr = 0xC0000000; tsk = get_task(PID); +* } if (!tsk->flags & PF_EXITING) { +* ... attach(); +* tsk->flags |= PF_EXITPIDONE; } else { +* if (!(tsk->flags & PF_EXITPIDONE)) +* return -EAGAIN; +* return -ESRCH; <--- FAIL +* } +* +* Returning ESRCH unconditionally is wrong here because the +* user space value has been changed by the exiting task. +*/ +if (get_futex_value_locked (&uval2, uaddr)) +return -EFAULT ; + +/* If the user space value has changed, try again. */ +if (uval2 != uval) +return -EAGAIN ; + +/* +* The exiting task did not have a robust list, the robust list was +* corrupted or the user space value in *uaddr is simply bogus. +* Give up and tell user space. +*/ +return -ESRCH ; +} +/* * Lookup the task for the TID provided from user space and attach to * it after doing proper sanity checks. */ -static int attach_to_pi_owner (u32 uval,union futex_key *key, +static int attach_to_pi_owner (u32 __user *uaddr,u32 uval,union futex_key *key,struct futex_pi_state **ps) {pid_t pid = uval &FUTEX_TID_MASK ;
@@ -1187,7 +1236,7 @@ static int attach_to_pi_owner(u32 uval,* set, we know that the task has finished the * cleanup: */ -int ret = (p->flags &PF_EXITPIDONE ) ? -ESRCH : -EAGAIN ; +int ret =handle_exit_race (uaddr, uval, p);raw_spin_unlock_irq (&p->pi_lock);put_task_struct (p);
@@ -1244,7 +1293,7 @@ static int lookup_pi_state(u32 __user *u* We are the first waiter - try to look up the owner based on * @uval and attach to it. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps); }static int lock_pi_update_atomic (u32 __user *uaddr,u32 uval,u32 newval)
@@ -1352,7 +1401,7 @@ static int futex_lock_pi_atomic(u32 __us* attach to the owner. If that fails, no harm done, we only * set the FUTEX_WAITERS bit in the user space variable. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps); }/**
Quoted 20 lines · Thomas Gleixner
kernel/futex.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-)kernel/futex.c +11
-0@@ -1148,11 +1148,60 @@ static int attach_to_pi_state(u32 __userreturn ret; } +static int handle_exit_race (u32 __user *uaddr,u32 uval,struct task_struct *tsk) +{ +u32 uval2; + +/* +* If PF_EXITPIDONE is not yet set try again. +*/ +if (!(tsk->flags &PF_EXITPIDONE )) +return -EAGAIN ; + +/*
kernel/futex.c static int attach_to_pi_state(u32 __user · line 1171
Quoted 10 lines
kernel/futex.c +10
-0+* Reread the user space value to handle the following situation: +* +* CPU0 CPU1 +* +* sys_exit() sys_futex() +* do_exit() futex_lock_pi() +* exit_signals(tsk) No waiters: +* tsk->flags |= PF_EXITING; *uaddr == 0x00000PID +* mm_release(tsk) Set waiter bit +* exit_robust_list(tsk) { *uaddr = 0x80000PID;
Just to clarify; this is: sys_futex() <- futex_lock_pi() <- futex_lock_pi_atomic(), where we do:
lock_pi_update_atomic(); // changes the futex word attach_to_pi_owner(); // possibly returns ESRCH after changing the word
Quoted 9 lines
kernel/futex.c +9
-0+* Set owner died attach_to_pi_owner() { +* *uaddr = 0xC0000000; tsk = get_task(PID); +* } if (!tsk->flags & PF_EXITING) { +* ... attach(); +* tsk->flags |= PF_EXITPIDONE; } else { +* if (!(tsk->flags & PF_EXITPIDONE)) +* return -EAGAIN; +* return -ESRCH; <--- FAIL +* }
kernel/futex.c static int attach_to_pi_state(u32 __user · line 1190
Quoted 10 lines
kernel/futex.c +10
-0+* +* Returning ESRCH unconditionally is wrong here because the +* user space value has been changed by the exiting task. +*/ +if (get_futex_value_locked (&uval2, uaddr)) +return -EFAULT ; + +/* If the user space value has changed, try again. */ +if (uval2 != uval) +return -EAGAIN ;
And this then goes back to futex_lock_pi(), which does a retry loop.
kernel/futex.c static int attach_to_pi_state(u32 __user · line 1197
Quoted 6 lines
kernel/futex.c +6
-0+/* +* The exiting task did not have a robust list, the robust list was +* corrupted or the user space value in *uaddr is simply bogus. +* Give up and tell user space. +*/ +return -ESRCH ;
If it is unchanged; -ESRCH is a valid return value.
kernel/futex.c static int attach_to_pi_state(u32 __user · line 1198
Quoted 1 line
kernel/futex.c +1
-0+}
There is another callers of futex_lock_pi_atomic(), futex_proxy_trylock_atomic(), which is part of futex_requeue(), that too does a retry loop on -EAGAIN.
And there is another caller of attach_to_pi_owner(): lookup_pi_state(), and that too is in futex_requeue() and handles the retry case properly.
Yes, this all looks good.
Quoted 11 lines · Peter Zijlstra
On Mon, Dec 10, 2018 at 04:23:06PM +0100, Thomas Gleixner wrote:
There is another callers offutex_lock_pi_atomic(),futex_proxy_trylock_atomic(), which is part offutex_requeue(), that too does a retry loop on-EAGAIN.And there is another caller of
attach_to_pi_owner():lookup_pi_state(), and that too is infutex_requeue()and handles the retry case properly.Yes, this all looks good.
Bah. The little devil in the unconcious part of my brain insisted on thinking further about that EAGAIN loop even despite my attempt to page that futex horrors out again immediately after sending that patch.
There is another related issue which is even worse than just mildly confusing user space:
task1(SCHED_OTHER)
sys_exit()
do_exit()
exit_mm()
task1->flags |= PF_EXITING;---> preemption
task2(SCHED_FIFO)
sys_futex(LOCK_PI)
....
attach_to_pi_owner() {
...
if (!task1->flags & PF_EXITING) {
attach();
} else {
if (!(tsk->flags & PF_EXITPIDONE))
return -EAGAIN;Now assume UP or both tasks pinned on the same CPU. That results in a livelock because task2 is going to loop forever.
No immediate idea how to cure that one w/o creating a mess.
Thanks,
tglx
Quoted 39 lines · Thomas Gleixner
Quoted 11 lines · Peter Zijlstra
On Mon, Dec 10, 2018 at 04:23:06PM +0100, Thomas Gleixner wrote:
There is another callers offutex_lock_pi_atomic(),futex_proxy_trylock_atomic(), which is part offutex_requeue(), that too does a retry loop on-EAGAIN.And there is another caller of
attach_to_pi_owner():lookup_pi_state(), and that too is infutex_requeue()and handles the retry case properly.Yes, this all looks good.
Bah. The little devil in the unconcious part of my brain insisted on thinking further about that EAGAIN loop even despite my attempt to page that futex horrors out again immediately after sending that patch.
There is another related issue which is even worse than just mildly confusing user space:
task1(SCHED_OTHER) sys_exit() do_exit() exit_mm() task1->flags |= PF_EXITING;---> preemption task2(SCHED_FIFO) sys_futex(LOCK_PI) .... attach_to_pi_owner() { ... if (!task1->flags & PF_EXITING) { attach(); } else { if (!(tsk->flags & PF_EXITPIDONE)) return -EAGAIN;
Quoted 4 lines
Now assume UP or both tasks pinned on the same CPU. That results in a livelock because task2 is going to loop forever.
No immediate idea how to cure that one w/o creating a mess.
One possible; but fairly gruesome hack; would be something like the below.
Now, this obviously introduces a priority inversion, but that's arguablly better than a live-lock, also I'm not sure there's really anything 'sane' you can do in the case where your lock holder is dying instead of doing a proper unlock anyway.
But no, I'm not liking this much either...
kernel/exit.c +2 -0
@@ -806,6 +806,8 @@ void __noreturn do_exit(long code)* task into the wait for ever nirwana as well. */ tsk->flags |=PF_EXITPIDONE ; +smp_mb (); +wake_up_bit (&tsk->flags,3 /* PF_EXITPIDONE */ );set_current_state (TASK_UNINTERRUPTIBLE );schedule (); }
kernel/futex.c +21 -7
@@ -1148,8 +1148,8 @@ static int attach_to_pi_state(u32 __user *uaddr, u32 uval,* Lookup the task for the TID provided from user space and attach to * it after doing proper sanity checks. */ -static int attach_to_pi_owner (u32 uval,union futex_key *key, -struct futex_pi_state **ps) +static int attach_to_pi_owner (u32 __user *uaddr,u32 uval,union futex_key *key, +struct futex_pi_state **ps,struct task_struct **pe) {pid_t pid = uval &FUTEX_TID_MASK ;struct futex_pi_state *pi_state;
@@ -1187,10 +1236,15 @@ static int attach_to_pi_owner(u32 uval, union futex_key *key,* set, we know that the task has finished the * cleanup: */ int ret =handle_exit_race (uaddr, uval, p);raw_spin_unlock_irq (&p->pi_lock); -put_task_struct (p); + +if (ret == -EAGAIN ) + *pe = p; +else +put_task_struct (p); +return ret; }
@@ -1244,7 +1298,7 @@ static int lookup_pi_state(u32 __user *uaddr, u32 uval,* We are the first waiter - try to look up the owner based on * @uval and attach to it. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps); }static int lock_pi_update_atomic (u32 __user *uaddr,u32 uval,u32 newval)
@@ -1282,7 +1336,8 @@ static int lock_pi_update_atomic(u32 __user *uaddr, u32 uval, u32 newval)static int futex_lock_pi_atomic (u32 __user *uaddr,struct futex_hash_bucket *hb,union futex_key *key,struct futex_pi_state **ps, -struct task_struct *task,int set_waiters) +struct task_struct *task,int set_waiters, +struct task_struct **exiting) {u32 uval, newval, vpid =task_pid_vnr (task);struct futex_q *top_waiter;
@@ -1352,7 +1407,7 @@ static int futex_lock_pi_atomic(u32 __user *uaddr, struct futex_hash_bucket *hb,* attach to the owner. If that fails, no harm done, we only * set the FUTEX_WAITERS bit in the user space variable. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps, exiting); }/**
@@ -2716,6 +2771,7 @@ static int futex_lock_pi(u32 __user *uaddr, unsigned int flags,struct rt_mutex_waiter rt_waiter;struct futex_hash_bucket *hb;struct futex_q q = futex_q_init; +struct task_struct *exiting;int res, ret;if (!IS_ENABLED (CONFIG_FUTEX_PI ))
@@ -2733,6 +2789,7 @@ static int futex_lock_pi(u32 __user *uaddr, unsigned int flags, } retry: + exiting =NULL ; ret =get_futex_key (uaddr, flags &FLAGS_SHARED , &q.key,VERIFY_WRITE );if (unlikely (ret !=0 )) goto out;
@@ -2740,7 +2797,7 @@ static int futex_lock_pi(u32 __user *uaddr, unsigned int flags, retry_private: hb =queue_lock (&q); - ret =futex_lock_pi_atomic (uaddr, hb, &q.key, &q.pi_state, current,0 ); + ret =futex_lock_pi_atomic (uaddr, hb, &q.key, &q.pi_state, current,0 , &exiting);if (unlikely (ret)) {/* * Atomic work succeeded and we got the lock,
@@ -2762,6 +2819,12 @@ static int futex_lock_pi(u32 __user *uaddr, unsigned int flags,*/ queue_unlock (hb);put_futex_key (&q.key); + +if (exiting) { +wait_bit (&exiting->flags,3 /* PF_EXITPIDONE */ ,TASK_UNINTERRUPTIBLE ); +put_task_struct (exiting); + } +cond_resched (); goto retry;default :
kernel/exit.c void __noreturn do_exit(long code) · line 810
Quoted 7 lines · Peter Zijlstra
On Mon, Dec 10, 2018 at 06:43:51PM +0100, Thomas Gleixner wrote:
kernel/exit.c +2
-0@@ -806,6 +806,8 @@ void __noreturn do_exit(long code)* task into the wait for ever nirwana as well. */ tsk->flags |=PF_EXITPIDONE ; +smp_mb (); +wake_up_bit (&tsk->flags,3 /* PF_EXITPIDONE */ );
Using ilog2(PF_EXITPIDONE) spares that horrible inline comment and more importantly selects the right bit. 0x04 is bit 2 ....
kernel/futex.c static int attach_to_pi_owner(u32 uval, union futex_key *key, · line 1244
Quoted 11 lines
kernel/futex.c +3
-1@@ -1187,10 +1236,15 @@ static int attach_to_pi_owner(u32 uval, union futex_key *key,* set, we know that the task has finished the * cleanup: */ int ret =handle_exit_race (uaddr, uval, p);raw_spin_unlock_irq (&p->pi_lock); -put_task_struct (p); + +if (ret == -EAGAIN ) + *pe = p;
Hmm, no. We really want to split the return value for that. EAGAIN is also returned for other reasons.
Plus requeue_pi() needs the same treatment. I'm staring into it, but all I came up with so far is horribly ugly.
Thanks,
tglx
Quoted 11 lines · Thomas Gleixner
kernel/exit.c void __noreturn do_exit(long code) · line 810
Quoted 7 lines · Peter Zijlstra
On Mon, Dec 10, 2018 at 06:43:51PM +0100, Thomas Gleixner wrote:
kernel/exit.c +2
-0@@ -806,6 +806,8 @@ void __noreturn do_exit(long code)* task into the wait for ever nirwana as well. */ tsk->flags |=PF_EXITPIDONE ; +smp_mb (); +wake_up_bit (&tsk->flags,3 /* PF_EXITPIDONE */ );Using
ilog2(PF_EXITPIDONE)spares that horrible inline comment and more importantly selects the right bit.0x04is bit 2 ....
Plus wake_up_bit() and wait_on_bit() want an unsigned long, but tsk->flags is unsigned int....
Moar staring....
Quoted 13 lines · Thomas Gleixner
Quoted 11 lines · Thomas Gleixner
kernel/exit.c void __noreturn do_exit(long code) · line 810
Quoted 7 lines · Peter Zijlstra
On Mon, Dec 10, 2018 at 06:43:51PM +0100, Thomas Gleixner wrote:
kernel/exit.c +2
-0@@ -806,6 +806,8 @@ void __noreturn do_exit(long code)* task into the wait for ever nirwana as well. */ tsk->flags |=PF_EXITPIDONE ; +smp_mb (); +wake_up_bit (&tsk->flags,3 /* PF_EXITPIDONE */ );Using
ilog2(PF_EXITPIDONE)spares that horrible inline comment and more importantly selects the right bit.0x04is bit 2 ....
Quoted 4 lines
Plus
wake_up_bit()andwait_on_bit()want an unsigned long, buttsk->flagsis unsigned int....Moar staring....
Aside of that calling wake_on_bit() unconditionally can be slow if the waitqueue in the hash bucket is not empty.
So while cooking up an alternative solution I found yet another exit race:
CPU0 CPU1
sys_futex() sys_exit()
futex_lock_pi() do_exit()
No waiters:
*uaddr == 0x00000PID;
Set waiter bit
*uaddr = 0x80000PID;
attach_to_pi_owner()
tsk = get_task(PID); exit_signals(tsk)
if (!(tsk->flags & PF_EXITING))
... tsk->flags |= PF_EXITING;
mm_release(tsk)
exit_robust_list(tsk)
Set owner died and clear PID
*uaddr = 0xC0000000;
if (unlikely(!list_empty(&tsk->pi_state_list)))
list_add(&pi_state->list,
&tsk->pi_state_list);I put that all on hold until Jan 7.
If somebody is really bored, here is the WIP patch series which addresses the live lock mess: https://tglx.de/~tglx/patches.tar
Thanks,
tglx
A message that is not in the archive (sent off-list or not yet received): [email protected]
Quoted 10 lines · Sasha Levin
This commit has been processed because it contains a -stable tag. The stable tag indicates that it's relevant for the following trees: all
The bot has tested the following trees: v4.19.8, v4.14.87, v4.9.144, v4.4.166, v3.18.128,
v4.19.8: Build OK!
v4.14.87: Build OK!
v4.9.144: Build failed! Errors:
kernel/futex.c:1186:28: error: ???uaddr??? undeclared (first use in this function)
Quoted 7 lines
v4.4.166: Build failed! Errors:
kernel/futex.c:1181:28: error: ???uaddr??? undeclared (first use in this function)v3.18.128: Build failed! Errors:
kernel/futex.c:1103:28: error: ???uaddr??? undeclared (first use in this function)How should we proceed with this patch?
I'll look into that once this is sorted... I so love these rotten kernels.
Thanks,
tglx
Sasha Levin
maintainerQuoted 14 lines · Thomas Gleixner
Quoted 13 lines · Sasha Levin
This commit has been processed because it contains a -stable tag. The stable tag indicates that it's relevant for the following trees: all
The bot has tested the following trees: v4.19.8, v4.14.87, v4.9.144, v4.4.166, v3.18.128,
v4.19.8: Build OK!
v4.14.87: Build OK!
v4.9.144: Build failed! Errors:
kernel/futex.c:1186:28: error: ???uaddr??? undeclared (first use in this function)v4.4.166: Build failed! Errors:
kernel/futex.c:1181:28: error: ???uaddr??? undeclared (first use in this function)
Quoted 6 lines
Quoted 4 lines
v3.18.128: Build failed! Errors:
kernel/futex.c:1103:28: error: ???uaddr??? undeclared (first use in this function)How should we proceed with this patch?
I'll look into that once this is sorted... I so love these rotten kernels.
It seems we need:
734009e96d19 ("futex: Change locking rules")Which isn't trivial to backport.
--
Thanks,
Sasha
Quoted 11 lines · Sasha Levin
Quoted 4 lines · Thomas Gleixner
Quoted 1 line · Sasha Levin
How should we proceed with this patch?
I'll look into that once this is sorted... I so love these rotten kernels.
It seems we need:
734009e96d19 ("futex: Change locking rules")Which isn't trivial to backport.
It's simpler to backport the fix. I'll look at that once we agreed on the final solution.
Thanks,
tglx
Hi Thomas,
does this also handle the ESRCH returned byattach_to_pi_owner(...)
{...
if (!pid) return -ESRCH; p = find_get_task_by_vpid(pid); if (!p) return -ESRCH;
...
I think pid should never be zero when attach_to_pi_owner is called.
But it can happen that p is null? At least I traced the "return -ESRCH" with the 4.17 kernel. Unfortunately both returns were done by the same instruction address.
Bye
Stefan
Quoted 143 lines · Thomas Gleixner
Stefan reported, that the glibc tst-robustpi4 test case fails occasionally. That case creates the following race between
sys_exit()andsys_futex(LOCK_PI):CPU0 CPU1
sys_exit()sys_futex()
do_exit()futex_lock_pi()
exit_signals(tsk)No waiters:
tsk->flags|=PF_EXITING; *uaddr == 0x00000PID
mm_release(tsk)Set waiter bitexit_robust_list(tsk) { *uaddr = 0x80000PID; Set owner died attach_to_pi_owner() { *uaddr = 0xC0000000; tsk = get_task(PID); } if (!tsk->flags & PF_EXITING) { ... attach(); tsk->flags |= PF_EXITPIDONE; } else { if (!(tsk->flags & PF_EXITPIDONE)) return -EAGAIN; return -ESRCH; <--- FAIL }ESRCH is returned all the way to user space, which triggers the glibc test case assert. Returning ESRCH unconditionally is wrong here because the user space value has been changed by the exiting task to
0xC0000000, i.e. theFUTEX_OWNER_DIEDbit is set and the futex PID value has been cleared. This is a valid state and the kernel has to handle it, i.e. taking the futex.Cure it by rereading the user space value when
PF_EXITINGandPF_EXITPIDONEis set in the task which owns the futex. If the value has changed, let the kernel retry the operation, which includes all regular sanity checks and correctly handles theFUTEX_OWNER_DIEDcase.If it hasn't changed, then return ESRCH as there is no way to distinguish this case from malfunctioning user space. This happens when the exiting task did not have a robust list, the robust list was corrupted or the user space value in the futex was simply bogus.
Reported-by Stefan Liebler <[email protected]>Signed-off-by Thomas Gleixner <[email protected]>Cc Heiko Carstens <[email protected]>Cc Peter Zijlstra <[email protected]>Cc Darren Hart <[email protected]>Cc Ingo Molnar <[email protected]>--- kernel/futex.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-)kernel/futex.c +53
-4@@ -1148,11 +1148,60 @@ static int attach_to_pi_state(u32 __userreturn ret; } +static int handle_exit_race (u32 __user *uaddr,u32 uval,struct task_struct *tsk) +{ +u32 uval2; + +/* +* If PF_EXITPIDONE is not yet set try again. +*/ +if (!(tsk->flags &PF_EXITPIDONE )) +return -EAGAIN ; + +/* +* Reread the user space value to handle the following situation: +* +* CPU0 CPU1 +* +* sys_exit() sys_futex() +* do_exit() futex_lock_pi() +* exit_signals(tsk) No waiters: +* tsk->flags |= PF_EXITING; *uaddr == 0x00000PID +* mm_release(tsk) Set waiter bit +* exit_robust_list(tsk) { *uaddr = 0x80000PID; +* Set owner died attach_to_pi_owner() { +* *uaddr = 0xC0000000; tsk = get_task(PID); +* } if (!tsk->flags & PF_EXITING) { +* ... attach(); +* tsk->flags |= PF_EXITPIDONE; } else { +* if (!(tsk->flags & PF_EXITPIDONE)) +* return -EAGAIN; +* return -ESRCH; <--- FAIL +* } +* +* Returning ESRCH unconditionally is wrong here because the +* user space value has been changed by the exiting task. +*/ +if (get_futex_value_locked (&uval2, uaddr)) +return -EFAULT ; + +/* If the user space value has changed, try again. */ +if (uval2 != uval) +return -EAGAIN ; + +/* +* The exiting task did not have a robust list, the robust list was +* corrupted or the user space value in *uaddr is simply bogus. +* Give up and tell user space. +*/ +return -ESRCH ; +} +/* * Lookup the task for the TID provided from user space and attach to * it after doing proper sanity checks. */ -static int attach_to_pi_owner (u32 uval,union futex_key *key, +static int attach_to_pi_owner (u32 __user *uaddr,u32 uval,union futex_key *key,struct futex_pi_state **ps) {pid_t pid = uval &FUTEX_TID_MASK ;@@ -1187,7 +1236,7 @@ static int attach_to_pi_owner(u32 uval,* set, we know that the task has finished the * cleanup: */ -int ret = (p->flags &PF_EXITPIDONE ) ? -ESRCH : -EAGAIN ; +int ret =handle_exit_race (uaddr, uval, p);raw_spin_unlock_irq (&p->pi_lock);put_task_struct (p);@@ -1244,7 +1293,7 @@ static int lookup_pi_state(u32 __user *u* We are the first waiter - try to look up the owner based on * @uval and attach to it. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps); }static int lock_pi_update_atomic (u32 __user *uaddr,u32 uval,u32 newval)@@ -1352,7 +1401,7 @@ static int futex_lock_pi_atomic(u32 __us* attach to the owner. If that fails, no harm done, we only * set the FUTEX_WAITERS bit in the user space variable. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps); }/**
Stefan,
Quoted 11 lines · Stefan Liebler
does this also handle the ESRCH returned by
attach_to_pi_owner(...)
{...if (!pid) return -ESRCH; p = find_get_task_by_vpid(pid); if (!p) return -ESRCH;...
I think pid should never be zero when
attach_to_pi_owneris called.
Yeah, I just checked again. It's a paranoid check.
Quoted 3 lines
But it can happen that p is null? At least I traced the "return
-ESRCH" with the 4.17 kernel. Unfortunately both returns were done by the same instruction address.
Yes, you are right. We need the same sanity check for that part. Updated patch below.
Now I "just" have to come up with a cure for that livelock thing ....
Thanks,
tglx
8<--------------
kernel/futex.c +63 -6
@@ -1148,11 +1148,65 @@ static int attach_to_pi_state(u32 __userreturn ret; } +static int handle_exit_race (u32 __user *uaddr,u32 uval, +struct task_struct *tsk) +{ +u32 uval2; + +/* +* If PF_EXITPIDONE is not yet set, then try again. +*/ +if (tsk && !(tsk->flags &PF_EXITPIDONE )) +return -EAGAIN ; + +/* +* Reread the user space value to handle the following situation: +* +* CPU0 CPU1 +* +* sys_exit() sys_futex() +* do_exit() futex_lock_pi() +* futex_lock_pi_atomic() +* exit_signals(tsk) No waiters: +* tsk->flags |= PF_EXITING; *uaddr == 0x00000PID +* mm_release(tsk) Set waiter bit +* exit_robust_list(tsk) { *uaddr = 0x80000PID; +* Set owner died attach_to_pi_owner() { +* *uaddr = 0xC0000000; tsk = get_task(PID); +* } if (!tsk->flags & PF_EXITING) { +* ... attach(); +* tsk->flags |= PF_EXITPIDONE; } else { +* if (!(tsk->flags & PF_EXITPIDONE)) +* return -EAGAIN; +* return -ESRCH; <--- FAIL +* } +* +* Returning ESRCH unconditionally is wrong here because the +* user space value has been changed by the exiting task. +* +* The same logic applies to the case where the exiting task is +* already gone. +*/ +if (get_futex_value_locked (&uval2, uaddr)) +return -EFAULT ; + +/* If the user space value has changed, try again. */ +if (uval2 != uval) +return -EAGAIN ; + +/* +* The exiting task did not have a robust list, the robust list was +* corrupted or the user space value in *uaddr is simply bogus. +* Give up and tell user space. +*/ +return -ESRCH ; +} +/* * Lookup the task for the TID provided from user space and attach to * it after doing proper sanity checks. */ -static int attach_to_pi_owner (u32 uval,union futex_key *key, +static int attach_to_pi_owner (u32 __user *uaddr,u32 uval,union futex_key *key,struct futex_pi_state **ps) {pid_t pid = uval &FUTEX_TID_MASK ;
@@ -1162,12 +1216,15 @@ static int attach_to_pi_owner(u32 uval,/* * We are the first waiter - try to look up the real owner and attach * the new pi_state to it, but bail out when TID = 0 [1] +* +* The !pid check is paranoid. None of the call sites should end up +* with pid == 0, but better safe than sorry. Let the caller retry */ if (!pid) -return -ESRCH ; +return -EAGAIN ; p =find_get_task_by_vpid (pid);if (!p) -return -ESRCH ; +return handle_exit_race (uaddr, uval,NULL );if (unlikely (p->flags &PF_KTHREAD )) {put_task_struct (p);
@@ -1187,7 +1244,7 @@ static int attach_to_pi_owner(u32 uval,* set, we know that the task has finished the * cleanup: */ -int ret = (p->flags &PF_EXITPIDONE ) ? -ESRCH : -EAGAIN ; +int ret =handle_exit_race (uaddr, uval, p);raw_spin_unlock_irq (&p->pi_lock);put_task_struct (p);
@@ -1244,7 +1301,7 @@ static int lookup_pi_state(u32 __user *u* We are the first waiter - try to look up the owner based on * @uval and attach to it. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps); }static int lock_pi_update_atomic (u32 __user *uaddr,u32 uval,u32 newval)
@@ -1352,7 +1409,7 @@ static int futex_lock_pi_atomic(u32 __us* attach to the owner. If that fails, no harm done, we only * set the FUTEX_WAITERS bit in the user space variable. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, newval, key, ps); }/**
tip-bot for Thomas Gleixner
merge botfutex: Cure exit race
Commit-ID: da791a667536bf8322042e38ca85d55a78d3c273 Gitweb: https://git.kernel.org/tip/da791a667536bf8322042e38ca85d55a78d3c273 Author: Thomas Gleixner <[email protected]> AuthorDate: Mon, 10 Dec 2018 14:35:14 +0100 Committer: Thomas Gleixner <[email protected]> CommitDate: Tue, 18 Dec 2018 23:13:15 +0100
futex: Cure exit race
Stefan reported, that the glibc tst-robustpi4 test case fails occasionally. That case creates the following race between sys_exit() and sys_futex_lock_pi():
CPU0 CPU1
sys_exit() sys_futex()
do_exit() futex_lock_pi()
exit_signals(tsk) No waiters:
tsk->flags |= PF_EXITING; *uaddr == 0x00000PID
mm_release(tsk) Set waiter bit
exit_robust_list(tsk) { *uaddr = 0x80000PID;
Set owner died attach_to_pi_owner() {
*uaddr = 0xC0000000; tsk = get_task(PID);
} if (!tsk->flags & PF_EXITING) {
... attach();
tsk->flags |= PF_EXITPIDONE; } else {
if (!(tsk->flags & PF_EXITPIDONE))
return -EAGAIN;
return -ESRCH; <--- FAIL
}ESRCH is returned all the way to user space, which triggers the glibc test case assert. Returning ESRCH unconditionally is wrong here because the user space value has been changed by the exiting task to 0xC0000000, i.e. the FUTEX_OWNER_DIED bit is set and the futex PID value has been cleared. This is a valid state and the kernel has to handle it, i.e. taking the futex.
Cure it by rereading the user space value when PF_EXITING and PF_EXITPIDONE is set in the task which 'owns' the futex. If the value has changed, let the kernel retry the operation, which includes all regular sanity checks and correctly handles the FUTEX_OWNER_DIED case.
If it hasn't changed, then return ESRCH as there is no way to distinguish this case from malfunctioning user space. This happens when the exiting task did not have a robust list, the robust list was corrupted or the user space value in the futex was simply bogus.
--- kernel/futex.c | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 6 deletions(-)
kernel/futex.c +63 -6
@@ -1148,11 +1148,65 @@ out_error:return ret; } +static int handle_exit_race (u32 __user *uaddr,u32 uval, +struct task_struct *tsk) +{ +u32 uval2; + +/* +* If PF_EXITPIDONE is not yet set, then try again. +*/ +if (tsk && !(tsk->flags &PF_EXITPIDONE )) +return -EAGAIN ; + +/* +* Reread the user space value to handle the following situation: +* +* CPU0 CPU1 +* +* sys_exit() sys_futex() +* do_exit() futex_lock_pi() +* futex_lock_pi_atomic() +* exit_signals(tsk) No waiters: +* tsk->flags |= PF_EXITING; *uaddr == 0x00000PID +* mm_release(tsk) Set waiter bit +* exit_robust_list(tsk) { *uaddr = 0x80000PID; +* Set owner died attach_to_pi_owner() { +* *uaddr = 0xC0000000; tsk = get_task(PID); +* } if (!tsk->flags & PF_EXITING) { +* ... attach(); +* tsk->flags |= PF_EXITPIDONE; } else { +* if (!(tsk->flags & PF_EXITPIDONE)) +* return -EAGAIN; +* return -ESRCH; <--- FAIL +* } +* +* Returning ESRCH unconditionally is wrong here because the +* user space value has been changed by the exiting task. +* +* The same logic applies to the case where the exiting task is +* already gone. +*/ +if (get_futex_value_locked (&uval2, uaddr)) +return -EFAULT ; + +/* If the user space value has changed, try again. */ +if (uval2 != uval) +return -EAGAIN ; + +/* +* The exiting task did not have a robust list, the robust list was +* corrupted or the user space value in *uaddr is simply bogus. +* Give up and tell user space. +*/ +return -ESRCH ; +} +/* * Lookup the task for the TID provided from user space and attach to * it after doing proper sanity checks. */ -static int attach_to_pi_owner (u32 uval,union futex_key *key, +static int attach_to_pi_owner (u32 __user *uaddr,u32 uval,union futex_key *key,struct futex_pi_state **ps) {pid_t pid = uval &FUTEX_TID_MASK ;
@@ -1162,12 +1216,15 @@ static int attach_to_pi_owner(u32 uval, union futex_key *key,/* * We are the first waiter - try to look up the real owner and attach * the new pi_state to it, but bail out when TID = 0 [1] +* +* The !pid check is paranoid. None of the call sites should end up +* with pid == 0, but better safe than sorry. Let the caller retry */ if (!pid) -return -ESRCH ; +return -EAGAIN ; p =find_get_task_by_vpid (pid);if (!p) -return -ESRCH ; +return handle_exit_race (uaddr, uval,NULL );if (unlikely (p->flags &PF_KTHREAD )) {put_task_struct (p);
@@ -1187,7 +1244,7 @@ static int attach_to_pi_owner(u32 uval, union futex_key *key,* set, we know that the task has finished the * cleanup: */ -int ret = (p->flags &PF_EXITPIDONE ) ? -ESRCH : -EAGAIN ; +int ret =handle_exit_race (uaddr, uval, p);raw_spin_unlock_irq (&p->pi_lock);put_task_struct (p);
@@ -1244,7 +1301,7 @@ static int lookup_pi_state(u32 __user *uaddr, u32 uval,* We are the first waiter - try to look up the owner based on * @uval and attach to it. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, uval, key, ps); }static int lock_pi_update_atomic (u32 __user *uaddr,u32 uval,u32 newval)
@@ -1352,7 +1409,7 @@ static int futex_lock_pi_atomic(u32 __user *uaddr, struct futex_hash_bucket *hb,* attach to the owner. If that fails, no harm done, we only * set the FUTEX_WAITERS bit in the user space variable. */ -return attach_to_pi_owner (uval, key, ps); +return attach_to_pi_owner (uaddr, newval, key, ps); }/**