[PATCH] kernel/futex: handle the case where we got a "late" waiter

classic Classic list List threaded Threaded
5 messages Options
Reply | Threaded
Open this post in threaded view
|

[PATCH] kernel/futex: handle the case where we got a "late" waiter

Sebastian Andrzej Siewior-4
futex_unlock_pi() gets uval before taking the hb lock. Now imagine
someone in futex_lock_pi() took the lock. While futex_unlock_pi() waits
for the hb lock, the LOCK_PI sets FUTEX_WAITERS and drops the lock.
Now, futex_unlock_pi() figures out that there is waiter and invokes
wake_futex_pi() with the old uval which does not yet have FUTEX_WAITERS
set. This flaw lets cmpxchg_futex_value_locked() fail and return -EINVAL.

To address this race we could either use get_futex_value_locked() after
we obtained the lock but then we would need to handle the EFAULT case
(which I think means get_user() without the lock).
To avoid this I let wake_futex_pi() detect this and return -EAGAIN so it
can read the new value and try again.

As bad as this may sound: FUTEX_OWNER_DIED is not set while the owner is
unlocking the lock. FUTEX_WAITERS gets set by another task *but* this
means that the owner invoked the futex() syscall rather than doing a
cmpxchg() in userland (and libc does not do that).

Fixes: ccf9e6a80d9e ("futex: Make unlock_pi more robust")
Signed-off-by: Sebastian Andrzej Siewior <[hidden email]>
---
 kernel/futex.c | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/kernel/futex.c b/kernel/futex.c
index 0d557e13823a..2911f54a6dde 100644
--- a/kernel/futex.c
+++ b/kernel/futex.c
@@ -1496,8 +1496,12 @@ static int wake_futex_pi(u32 __user *uaddr, u32 uval, struct futex_q *this,
 
  if (cmpxchg_futex_value_locked(&curval, uaddr, uval, newval))
  ret = -EFAULT;
- else if (curval != uval)
- ret = -EINVAL;
+ else if (curval != uval) {
+ if ((FUTEX_TID_MASK & curval) == uval)
+ ret = -EAGAIN;
+ else
+ ret = -EINVAL;
+ }
  if (ret) {
  raw_spin_unlock_irq(&pi_state->pi_mutex.wait_lock);
  return ret;
@@ -2852,6 +2856,12 @@ static int futex_unlock_pi(u32 __user *uaddr, unsigned int flags)
  if (ret == -EFAULT)
  goto pi_faulted;
  /*
+ * Between get_user() and obtaining the hb->lock the uval
+ * gained a flag. Retry with the new value.
+ */
+ if (ret == -EAGAIN)
+ goto pi_faulted;
+ /*
  * wake_futex_pi has detected invalid state. Tell user
  * space.
  */
@@ -2883,6 +2893,8 @@ static int futex_unlock_pi(u32 __user *uaddr, unsigned int flags)
  spin_unlock(&hb->lock);
  put_futex_key(&key);
 
+ if (ret == -EAGAIN)
+ goto retry;
  ret = fault_in_user_writeable(uaddr);
  if (!ret)
  goto retry;
--
2.8.0.rc3

Reply | Threaded
Open this post in threaded view
|

Re: [PATCH] kernel/futex: handle the case where we got a "late" waiter

Davidlohr Bueso-5
On Fri, 15 Apr 2016, Sebastian Andrzej Siewior wrote:

>futex_unlock_pi() gets uval before taking the hb lock. Now imagine
>someone in futex_lock_pi() took the lock. While futex_unlock_pi() waits
>for the hb lock, the LOCK_PI sets FUTEX_WAITERS and drops the lock.
>Now, futex_unlock_pi() figures out that there is waiter and invokes
>wake_futex_pi() with the old uval which does not yet have FUTEX_WAITERS
>set. This flaw lets cmpxchg_futex_value_locked() fail and return -EINVAL.

Hmm but if we're calling futex_unlock_pi() in the first place, doesn't that
indicate that the uval already has FUTEX_WAITERS and therefore failed the
TID->0 transition in userland? That or the thread is bogusly unlocking a
lock that it doesn't own.

This is of course different than the requeue_pi case which can specify
set_waiters but also gets the value via get_futex_value_locked().

Is this a real issue or did you find it by code inspection?

Thanks,
Davidlohr
Reply | Threaded
Open this post in threaded view
|

Re: [PATCH] kernel/futex: handle the case where we got a "late" waiter

Sebastian Andrzej Siewior-4
On 04/20/2016 12:27 AM, Davidlohr Bueso wrote:

> On Fri, 15 Apr 2016, Sebastian Andrzej Siewior wrote:
>
>> futex_unlock_pi() gets uval before taking the hb lock. Now imagine
>> someone in futex_lock_pi() took the lock. While futex_unlock_pi() waits
>> for the hb lock, the LOCK_PI sets FUTEX_WAITERS and drops the lock.
>> Now, futex_unlock_pi() figures out that there is waiter and invokes
>> wake_futex_pi() with the old uval which does not yet have FUTEX_WAITERS
>> set. This flaw lets cmpxchg_futex_value_locked() fail and return -EINVAL.
>
> Hmm but if we're calling futex_unlock_pi() in the first place, doesn't that
> indicate that the uval already has FUTEX_WAITERS and therefore failed the
> TID->0 transition in userland? That or the thread is bogusly unlocking a
> lock that it doesn't own.

I hacked a testing tool which always did the syscall for LOCK_PI /
UNLOCK_PI and this popped up.

>
> This is of course different than the requeue_pi case which can specify
> set_waiters but also gets the value via get_futex_value_locked().
>
> Is this a real issue or did you find it by code inspection?

real issue.
        https://breakpoint.cc/mass-futex2-rl.c

> Thanks,
> Davidlohr

Sebastian
Reply | Threaded
Open this post in threaded view
|

Re: [PATCH] kernel/futex: handle the case where we got a "late" waiter

Thomas Gleixner
In reply to this post by Davidlohr Bueso-5
On Tue, 19 Apr 2016, Davidlohr Bueso wrote:

> On Fri, 15 Apr 2016, Sebastian Andrzej Siewior wrote:
>
> > futex_unlock_pi() gets uval before taking the hb lock. Now imagine
> > someone in futex_lock_pi() took the lock. While futex_unlock_pi() waits
> > for the hb lock, the LOCK_PI sets FUTEX_WAITERS and drops the lock.
> > Now, futex_unlock_pi() figures out that there is waiter and invokes
> > wake_futex_pi() with the old uval which does not yet have FUTEX_WAITERS
> > set. This flaw lets cmpxchg_futex_value_locked() fail and return -EINVAL.
>
> Hmm but if we're calling futex_unlock_pi() in the first place, doesn't that
> indicate that the uval already has FUTEX_WAITERS and therefore failed the
> TID->0 transition in userland? That or the thread is bogusly unlocking a
> lock that it doesn't own.

It can be called unconditionally w/o trying the TID->0 transition in user
space first and we should handle that case.
 
Thanks,

        tglx
Reply | Threaded
Open this post in threaded view
|

[tip:locking/urgent] futex: Handle unlock_pi race gracefully

tip-bot for Peter Zijlstra
In reply to this post by Sebastian Andrzej Siewior-4
Commit-ID:  89e9e66ba1b3bde9d8ea90566c2aee20697ad681
Gitweb:     http://git.kernel.org/tip/89e9e66ba1b3bde9d8ea90566c2aee20697ad681
Author:     Sebastian Andrzej Siewior <[hidden email]>
AuthorDate: Fri, 15 Apr 2016 14:35:39 +0200
Committer:  Thomas Gleixner <[hidden email]>
CommitDate: Wed, 20 Apr 2016 12:33:13 +0200

futex: Handle unlock_pi race gracefully

If userspace calls UNLOCK_PI unconditionally without trying the TID -> 0
transition in user space first then the user space value might not have the
waiters bit set. This opens the following race:

CPU0              CPU1
uval = get_user(futex)
                            lock(hb)
lock(hb)
                            futex |= FUTEX_WAITERS
                            ....
                            unlock(hb)

cmpxchg(futex, uval, newval)

So the cmpxchg fails and returns -EINVAL to user space, which is wrong because
the futex value is valid.

To handle this (yes, yet another) corner case gracefully, check for a flag
change and retry.

[ tglx: Massaged changelog and slightly reworked implementation ]

Fixes: ccf9e6a80d9e ("futex: Make unlock_pi more robust")
Signed-off-by: Sebastian Andrzej Siewior <[hidden email]>
Cc: [hidden email]
Cc: Davidlohr Bueso <[hidden email]>
Cc: Darren Hart <[hidden email]>
Cc: Peter Zijlstra <[hidden email]>
Link: http://lkml.kernel.org/r/1460723739-5195-1-git-send-email-bigeasy@...
Signed-off-by: Thomas Gleixner <[hidden email]>
---
 kernel/futex.c | 25 ++++++++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

diff --git a/kernel/futex.c b/kernel/futex.c
index a5d2e74..fd204e1 100644
--- a/kernel/futex.c
+++ b/kernel/futex.c
@@ -1295,10 +1295,20 @@ static int wake_futex_pi(u32 __user *uaddr, u32 uval, struct futex_q *this,
  if (unlikely(should_fail_futex(true)))
  ret = -EFAULT;
 
- if (cmpxchg_futex_value_locked(&curval, uaddr, uval, newval))
+ if (cmpxchg_futex_value_locked(&curval, uaddr, uval, newval)) {
  ret = -EFAULT;
- else if (curval != uval)
- ret = -EINVAL;
+ } else if (curval != uval) {
+ /*
+ * If a unconditional UNLOCK_PI operation (user space did not
+ * try the TID->0 transition) raced with a waiter setting the
+ * FUTEX_WAITERS flag between get_user() and locking the hash
+ * bucket lock, retry the operation.
+ */
+ if ((FUTEX_TID_MASK & curval) == uval)
+ ret = -EAGAIN;
+ else
+ ret = -EINVAL;
+ }
  if (ret) {
  raw_spin_unlock_irq(&pi_state->pi_mutex.wait_lock);
  return ret;
@@ -2623,6 +2633,15 @@ retry:
  if (ret == -EFAULT)
  goto pi_faulted;
  /*
+ * A unconditional UNLOCK_PI op raced against a waiter
+ * setting the FUTEX_WAITERS bit. Try again.
+ */
+ if (ret == -EAGAIN) {
+ spin_unlock(&hb->lock);
+ put_futex_key(&key);
+ goto retry;
+ }
+ /*
  * wake_futex_pi has detected invalid state. Tell user
  * space.
  */