All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] fs/super.c: Fix lock/unlock imbalance in sget_fc
@ 2018-06-20 13:10 Gustavo A. R. Silva
  2018-06-21  0:23 ` Al Viro
  0 siblings, 1 reply; 3+ messages in thread
From: Gustavo A. R. Silva @ 2018-06-20 13:10 UTC (permalink / raw)
  To: David Howells, Alexander Viro
  Cc: linux-fsdevel, linux-kernel, Gustavo A. R. Silva

It seems a spin_unlock is missing before return at line 532: return old.

Addresses-Coverity-ID: 1470111 ("Missing unlock")
Fixes: 4f3911e76e19 ("vfs: Implement a filesystem superblock creation/configuration context")
Signed-off-by: Gustavo A. R. Silva <gustavo@embeddedor.com>
---
 fs/super.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fs/super.c b/fs/super.c
index 43400f5..ff24532 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -529,6 +529,7 @@ struct super_block *sget_fc(struct fs_context *fc,
 				destroy_unused_super(s);
 				s = NULL;
 			}
+			spin_unlock(&sb_lock);
 			return old;
 		}
 	}
-- 
2.7.4


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH] fs/super.c: Fix lock/unlock imbalance in sget_fc
  2018-06-20 13:10 [PATCH] fs/super.c: Fix lock/unlock imbalance in sget_fc Gustavo A. R. Silva
@ 2018-06-21  0:23 ` Al Viro
  2018-06-21 20:12   ` Gustavo A. R. Silva
  0 siblings, 1 reply; 3+ messages in thread
From: Al Viro @ 2018-06-21  0:23 UTC (permalink / raw)
  To: Gustavo A. R. Silva; +Cc: David Howells, linux-fsdevel, linux-kernel

On Wed, Jun 20, 2018 at 08:10:59AM -0500, Gustavo A. R. Silva wrote:
> It seems a spin_unlock is missing before return at line 532: return old.
> 
> Addresses-Coverity-ID: 1470111 ("Missing unlock")
> Fixes: 4f3911e76e19 ("vfs: Implement a filesystem superblock creation/configuration context")
> Signed-off-by: Gustavo A. R. Silva <gustavo@embeddedor.com>

... and that had been tested how, exactly?  Because I would really like
a reproducer or, short of that, a proof that one exists.  Hell, I would
settle for something that hits the codepath in question with that patch
applied.

"It seems" is not enough - it's a good starting point for investigating,
but no more than that.

[tl;dr - "it seems" is, indeed, no good.  *IF* there's more to it (e.g.
a reproducer), we have some other crap going on and need to investigate
that, but even in that case, the patch is wrong]

As for how to investigate that kind of thing...  Look:

The code in question is
                        if (fc->user_ns != old->s_user_ns) {
                                spin_unlock(&sb_lock);
                                if (s) {
                                        up_write(&s->s_umount);
                                        destroy_unused_super(s);
                                }
                                return ERR_PTR(-EBUSY);
                        }
                        if (!grab_super(old))
                                goto retry;
                        if (s) {
                                up_write(&s->s_umount);
                                destroy_unused_super(s);
                                s = NULL;
                        }
                        return old;

Your hypothesis is that we can get to that return old; with sb_lock
held.  That would almost certainly be a bad thing, since elsewhere
in the same function we have
        spin_unlock(&sb_lock);
        get_filesystem(s->s_type);
        register_shrinker(&s->s_shrink);
        return s;
which appears to return an object with sb_lock dropped, with no obvious
way for a caller to tell one from another.  Even if such a way existed
(it does, actually), that kind of calling conventions would be highly
bug-prone.

The next question is, when would we get to that return old; with
sb_lock held?  We do, apparently, hold it before the if (fc->...)
above (again, strictly speaking not proven yet, but that's the
most likely assumption).  So if grab_super(old) returns true and
we are holding sb_lock, either we do have a problem, or something
subtle is going on.

The obvious next target of examination is grab_super().  Which comes
with
/**
 *      grab_super - acquire an active reference
 *      @s: reference we are trying to make active
 *
 *      Tries to acquire an active reference.  grab_super() is used when we
 *      had just found a superblock in super_blocks or fs_type->fs_supers
 *      and want to turn it into a full-blown active reference.  grab_super()
                                                                 ^^^^^^^^^^^^
 *      is called with sb_lock held and drops it.  Returns 1 in case of
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 *      success, 0 if we had failed (superblock contents was already dead or
 *      dying when grab_super() had been called).  Note that this is only
 *      called for superblocks not in rundown mode (== ones still on ->fs_supers
 *      of their type), so increment of ->s_count is OK here.
 */
and looking for references to sb_lock yields the underscored sentence.  Now,
if that is true (which is not guaranteed - comments can become stale), we do
not need to drop sb_lock after the call of grab_super() - it's already been
dropped by grab_super() itself.

And looking at the actual code we have
static int grab_super(struct super_block *s) __releases(sb_lock)
{
        s->s_count++;
        spin_unlock(&sb_lock);
	^^^^^^^^^^^^^^^^^^^^^---------- dropped, indeed
        down_write(&s->s_umount);
        if ((s->s_flags & SB_BORN) && atomic_inc_not_zero(&s->s_active)) {
                put_super(s);
                return 1;
        }
        up_write(&s->s_umount);
        put_super(s);
        return 0;
}
... and not regained, unless put_super() does something fishy.
static void put_super(struct super_block *sb)
{
        spin_lock(&sb_lock);
        __put_super(sb);
        spin_unlock(&sb_lock);
}
OK, put_super() definitely returns with sb_lock not held, and therefore so does
grab_super().  In other words, the comment does match the reality and trying
to drop sb_lock right after the call of grab_super() would be 100% wrong.

That disproves your hypothesis.  For the sake of completeness, let's finish the
analysis of sget_fc() wrt sb_lock.
struct super_block *sget_fc(struct fs_context *fc,
                            int (*test)(struct super_block *, struct fs_context *),
                            int (*set)(struct super_block *, struct fs_context *))
{
        if (!(fc->sb_flags & SB_KERNMOUNT) &&
            fc->purpose != FS_CONTEXT_FOR_SUBMOUNT) {
                /* Don't allow mounting unless the caller has CAP_SYS_ADMIN
                 * over the namespace.
                 */
                if (!(fc->fs_type->fs_flags & FS_USERNS_MOUNT) &&
                    !capable(CAP_SYS_ADMIN))
                        return ERR_PTR(-EPERM);
                else if (!ns_capable(fc->user_ns, CAP_SYS_ADMIN))
                        return ERR_PTR(-EPERM);
        }
retry:
        spin_lock(&sb_lock);

OK, we definitely do not want to call that with sb_lock held - doing so would
either return ERR_PTR(-EPERM) or deadlock.  So the calling conventions include
"caller is not holding sb_lock".  If so, everything up to retry: should be
executed without sb_lock held, and subsequent code is with sb_lock held.
        if (test) {
                hlist_for_each_entry(old, &fc->fs_type->fs_supers, s_instances) {
                        if (!test(old, fc))
                                continue;
'test' callback should be callable with sb_lock held.  Note that
at least in case when it returns false it must not have dropped
sb_lock - the list we are walking is protected by sb_lock.
                        if (fc->user_ns != old->s_user_ns) {
                                spin_unlock(&sb_lock);
... in which case we also want sb_lock not dropped by test() since we
drop it ourselves.
                                if (s) {
                                        up_write(&s->s_umount);
                                        destroy_unused_super(s);
destroy_unused_super() is called without sb_lock here and examination
shows that it doesn't touch sb_lock itself.
                                }
                                return ERR_PTR(-EBUSY);
                        }
... and in this case we also want sb_lock not dropped by test() either,
since grab_super() will drop it.
                        if (!grab_super(old))
                                goto retry;
we either go back to 'retry:' with sb_lock not held (same as in the
case of reaching retry: without goto) or coninue to
                        if (s) {
                                up_write(&s->s_umount);
                                destroy_unused_super(s);
same as above, called without sb_lock, doesn't touch it.
                                s = NULL;
                        }
                        return old;
... and we return without sb_lock held.
                }
        }

Here (after the if (test) body) we do hold sb_lock
        if (!s) {
                spin_unlock(&sb_lock);
drop and call alloc_super(), which doesn't touch sb_lock
                s = alloc_super(fc);
                if (!s)
                        return ERR_PTR(-ENOMEM);
                goto retry;
... and either return without sb_lock or go back to retry:, with
the same conditions as on other paths leading there.  Incidentally,
since alloc_super() very clearly blocks (GFP_USER kzalloc the very
first thing in there), the calling conventions for sget_fc() include
not just "must not be holding sb_lock" but "must not be holding any
spinlock".
        }
        s->s_fs_info = fc->s_fs_info;
        err = set(s, fc);
OK, so 'set()' is also called under sb_lock.
        if (err) {
                s->s_fs_info = NULL;
                spin_unlock(&sb_lock);
... and at least in case of error must not drop it, since we'd do that
ourselves.
                up_write(&s->s_umount);
                destroy_unused_super(s);
                return ERR_PTR(err);
same as in earlier cases
        }
        fc->s_fs_info = NULL;
        s->s_type = fc->fs_type;
        strlcpy(s->s_id, s->s_type->name, sizeof(s->s_id));
        list_add_tail(&s->s_list, &super_blocks);
        hlist_add_head(&s->s_instances, &s->s_type->fs_supers);
        spin_unlock(&sb_lock);

OK, so 'set()' must not drop sb_lock in any cases.  And from that point
on sb_lock is not held (neither get_filesystem() nor register_shrinker()
touch it)

        get_filesystem(s->s_type);
        register_shrinker(&s->s_shrink);
        return s;
}

So we arrive to the following:

* sget_fc() must not be called with any spinlocks (sb_lock included) held.
* in all cases it returns with sb_lock not held.
* test() and set() callbacks are always called under sb_lock and should not
drop it.

Looking at the shape of that code strengthens the last one to "even
drop-and-retake is not allowed".  With that kind of loop over hlist, dropping
and retaking sb_lock in test() might blow up.  And as for set() callback,
we clearly don't want to create a new instance when an existing one would
satisfy the test() predicate.  And dropping/retaking sb_lock would've
allowed another caller to come and insert a new instance while our
set() has not been holding sb_lock, ending up with just that once we
return and get to hlist_add_head() there.

In other words,
	* called without any spinlocks held
	* returns with no spinlocks held
	* callbacks are always called under sb_lock and must not touch it.

Verifying that callers (and all possible callbacks) satisfy those rules
is left as an exercise for reader...

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] fs/super.c: Fix lock/unlock imbalance in sget_fc
  2018-06-21  0:23 ` Al Viro
@ 2018-06-21 20:12   ` Gustavo A. R. Silva
  0 siblings, 0 replies; 3+ messages in thread
From: Gustavo A. R. Silva @ 2018-06-21 20:12 UTC (permalink / raw)
  To: Al Viro; +Cc: David Howells, linux-fsdevel, linux-kernel

Hi Al,

Certainly, I never checked grab_super. Lesson learned.

Thanks a lot for taking the time to write this master class. I really 
appreciate it. :)

--
Gustavo

> a reproducer), we have some other crap going on and need to investigate
> that, but even in that case, the patch is wrong]
> 
> As for how to investigate that kind of thing...  Look:
> 
> The code in question is
>                          if (fc->user_ns != old->s_user_ns) {
>                                  spin_unlock(&sb_lock);
>                                  if (s) {
>                                          up_write(&s->s_umount);
>                                          destroy_unused_super(s);
>                                  }
>                                  return ERR_PTR(-EBUSY);
>                          }
>                          if (!grab_super(old))
>                                  goto retry;
>                          if (s) {
>                                  up_write(&s->s_umount);
>                                  destroy_unused_super(s);
>                                  s = NULL;
>                          }
>                          return old;
> 
> Your hypothesis is that we can get to that return old; with sb_lock
> held.  That would almost certainly be a bad thing, since elsewhere
> in the same function we have
>          spin_unlock(&sb_lock);
>          get_filesystem(s->s_type);
>          register_shrinker(&s->s_shrink);
>          return s;
> which appears to return an object with sb_lock dropped, with no obvious
> way for a caller to tell one from another.  Even if such a way existed
> (it does, actually), that kind of calling conventions would be highly
> bug-prone.
> 
> The next question is, when would we get to that return old; with
> sb_lock held?  We do, apparently, hold it before the if (fc->...)
> above (again, strictly speaking not proven yet, but that's the
> most likely assumption).  So if grab_super(old) returns true and
> we are holding sb_lock, either we do have a problem, or something
> subtle is going on.
> 
> The obvious next target of examination is grab_super().  Which comes
> with
> /**
>   *      grab_super - acquire an active reference
>   *      @s: reference we are trying to make active
>   *
>   *      Tries to acquire an active reference.  grab_super() is used when we
>   *      had just found a superblock in super_blocks or fs_type->fs_supers
>   *      and want to turn it into a full-blown active reference.  grab_super()
>                                                                   ^^^^^^^^^^^^
>   *      is called with sb_lock held and drops it.  Returns 1 in case of
>          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   *      success, 0 if we had failed (superblock contents was already dead or
>   *      dying when grab_super() had been called).  Note that this is only
>   *      called for superblocks not in rundown mode (== ones still on ->fs_supers
>   *      of their type), so increment of ->s_count is OK here.
>   */
> and looking for references to sb_lock yields the underscored sentence.  Now,
> if that is true (which is not guaranteed - comments can become stale), we do
> not need to drop sb_lock after the call of grab_super() - it's already been
> dropped by grab_super() itself.
> 
> And looking at the actual code we have
> static int grab_super(struct super_block *s) __releases(sb_lock)
> {
>          s->s_count++;
>          spin_unlock(&sb_lock);
> 	^^^^^^^^^^^^^^^^^^^^^---------- dropped, indeed
>          down_write(&s->s_umount);
>          if ((s->s_flags & SB_BORN) && atomic_inc_not_zero(&s->s_active)) {
>                  put_super(s);
>                  return 1;
>          }
>          up_write(&s->s_umount);
>          put_super(s);
>          return 0;
> }
> ... and not regained, unless put_super() does something fishy.
> static void put_super(struct super_block *sb)
> {
>          spin_lock(&sb_lock);
>          __put_super(sb);
>          spin_unlock(&sb_lock);
> }
> OK, put_super() definitely returns with sb_lock not held, and therefore so does
> grab_super().  In other words, the comment does match the reality and trying
> to drop sb_lock right after the call of grab_super() would be 100% wrong.
> 
> That disproves your hypothesis.  For the sake of completeness, let's finish the
> analysis of sget_fc() wrt sb_lock.
> struct super_block *sget_fc(struct fs_context *fc,
>                              int (*test)(struct super_block *, struct fs_context *),
>                              int (*set)(struct super_block *, struct fs_context *))
> {
>          if (!(fc->sb_flags & SB_KERNMOUNT) &&
>              fc->purpose != FS_CONTEXT_FOR_SUBMOUNT) {
>                  /* Don't allow mounting unless the caller has CAP_SYS_ADMIN
>                   * over the namespace.
>                   */
>                  if (!(fc->fs_type->fs_flags & FS_USERNS_MOUNT) &&
>                      !capable(CAP_SYS_ADMIN))
>                          return ERR_PTR(-EPERM);
>                  else if (!ns_capable(fc->user_ns, CAP_SYS_ADMIN))
>                          return ERR_PTR(-EPERM);
>          }
> retry:
>          spin_lock(&sb_lock);
> 
> OK, we definitely do not want to call that with sb_lock held - doing so would
> either return ERR_PTR(-EPERM) or deadlock.  So the calling conventions include
> "caller is not holding sb_lock".  If so, everything up to retry: should be
> executed without sb_lock held, and subsequent code is with sb_lock held.
>          if (test) {
>                  hlist_for_each_entry(old, &fc->fs_type->fs_supers, s_instances) {
>                          if (!test(old, fc))
>                                  continue;
> 'test' callback should be callable with sb_lock held.  Note that
> at least in case when it returns false it must not have dropped
> sb_lock - the list we are walking is protected by sb_lock.
>                          if (fc->user_ns != old->s_user_ns) {
>                                  spin_unlock(&sb_lock);
> ... in which case we also want sb_lock not dropped by test() since we
> drop it ourselves.
>                                  if (s) {
>                                          up_write(&s->s_umount);
>                                          destroy_unused_super(s);
> destroy_unused_super() is called without sb_lock here and examination
> shows that it doesn't touch sb_lock itself.
>                                  }
>                                  return ERR_PTR(-EBUSY);
>                          }
> ... and in this case we also want sb_lock not dropped by test() either,
> since grab_super() will drop it.
>                          if (!grab_super(old))
>                                  goto retry;
> we either go back to 'retry:' with sb_lock not held (same as in the
> case of reaching retry: without goto) or coninue to
>                          if (s) {
>                                  up_write(&s->s_umount);
>                                  destroy_unused_super(s);
> same as above, called without sb_lock, doesn't touch it.
>                                  s = NULL;
>                          }
>                          return old;
> ... and we return without sb_lock held.
>                  }
>          }
> 
> Here (after the if (test) body) we do hold sb_lock
>          if (!s) {
>                  spin_unlock(&sb_lock);
> drop and call alloc_super(), which doesn't touch sb_lock
>                  s = alloc_super(fc);
>                  if (!s)
>                          return ERR_PTR(-ENOMEM);
>                  goto retry;
> ... and either return without sb_lock or go back to retry:, with
> the same conditions as on other paths leading there.  Incidentally,
> since alloc_super() very clearly blocks (GFP_USER kzalloc the very
> first thing in there), the calling conventions for sget_fc() include
> not just "must not be holding sb_lock" but "must not be holding any
> spinlock".
>          }
>          s->s_fs_info = fc->s_fs_info;
>          err = set(s, fc);
> OK, so 'set()' is also called under sb_lock.
>          if (err) {
>                  s->s_fs_info = NULL;
>                  spin_unlock(&sb_lock);
> ... and at least in case of error must not drop it, since we'd do that
> ourselves.
>                  up_write(&s->s_umount);
>                  destroy_unused_super(s);
>                  return ERR_PTR(err);
> same as in earlier cases
>          }
>          fc->s_fs_info = NULL;
>          s->s_type = fc->fs_type;
>          strlcpy(s->s_id, s->s_type->name, sizeof(s->s_id));
>          list_add_tail(&s->s_list, &super_blocks);
>          hlist_add_head(&s->s_instances, &s->s_type->fs_supers);
>          spin_unlock(&sb_lock);
> 
> OK, so 'set()' must not drop sb_lock in any cases.  And from that point
> on sb_lock is not held (neither get_filesystem() nor register_shrinker()
> touch it)
> 
>          get_filesystem(s->s_type);
>          register_shrinker(&s->s_shrink);
>          return s;
> }
> 
> So we arrive to the following:
> 
> * sget_fc() must not be called with any spinlocks (sb_lock included) held.
> * in all cases it returns with sb_lock not held.
> * test() and set() callbacks are always called under sb_lock and should not
> drop it.
> 
> Looking at the shape of that code strengthens the last one to "even
> drop-and-retake is not allowed".  With that kind of loop over hlist, dropping
> and retaking sb_lock in test() might blow up.  And as for set() callback,
> we clearly don't want to create a new instance when an existing one would
> satisfy the test() predicate.  And dropping/retaking sb_lock would've
> allowed another caller to come and insert a new instance while our
> set() has not been holding sb_lock, ending up with just that once we
> return and get to hlist_add_head() there.
> 
> In other words,
> 	* called without any spinlocks held
> 	* returns with no spinlocks held
> 	* callbacks are always called under sb_lock and must not touch it.
> 
> Verifying that callers (and all possible callbacks) satisfy those rules
> is left as an exercise for reader...
> 

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2018-06-21 20:37 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2018-06-20 13:10 [PATCH] fs/super.c: Fix lock/unlock imbalance in sget_fc Gustavo A. R. Silva
2018-06-21  0:23 ` Al Viro
2018-06-21 20:12   ` Gustavo A. R. Silva

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.