All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jann Horn <jannh@google.com>
To: "Mickaël Salaün" <mic@digikod.net>
Cc: "James Morris" <jmorris@namei.org>,
	"Serge E . Hallyn" <serge@hallyn.com>,
	"Al Viro" <viro@zeniv.linux.org.uk>,
	"Andrew Morton" <akpm@linux-foundation.org>,
	"Andy Lutomirski" <luto@amacapital.net>,
	"Anton Ivanov" <anton.ivanov@cambridgegreys.com>,
	"Arnd Bergmann" <arnd@arndb.de>,
	"Casey Schaufler" <casey@schaufler-ca.com>,
	"David Howells" <dhowells@redhat.com>,
	"Jeff Dike" <jdike@addtoit.com>,
	"Jonathan Corbet" <corbet@lwn.net>,
	"Kees Cook" <keescook@chromium.org>,
	"Michael Kerrisk" <mtk.manpages@gmail.com>,
	"Richard Weinberger" <richard@nod.at>,
	"Shuah Khan" <shuah@kernel.org>,
	"Vincent Dagonneau" <vincent.dagonneau@ssi.gouv.fr>,
	"Kernel Hardening" <kernel-hardening@lists.openwall.com>,
	"Linux API" <linux-api@vger.kernel.org>,
	linux-arch <linux-arch@vger.kernel.org>,
	"open list:DOCUMENTATION" <linux-doc@vger.kernel.org>,
	linux-fsdevel <linux-fsdevel@vger.kernel.org>,
	"kernel list" <linux-kernel@vger.kernel.org>,
	"open list:KERNEL SELFTEST FRAMEWORK"
	<linux-kselftest@vger.kernel.org>,
	linux-security-module <linux-security-module@vger.kernel.org>,
	"the arch/x86 maintainers" <x86@kernel.org>,
	"Mickaël Salaün" <mic@linux.microsoft.com>
Subject: Re: [PATCH v30 07/12] landlock: Support filesystem access-control
Date: Wed, 24 Mar 2021 04:10:27 +0100	[thread overview]
Message-ID: <CAG48ez0ex48pCgunrg+BpJ-LppZUVXhsmiEW_2d2mhbkDB793Q@mail.gmail.com> (raw)
In-Reply-To: <7e494b74-8d5d-a109-6327-992d7d8fca87@digikod.net>

On Tue, Mar 23, 2021 at 8:22 PM Mickaël Salaün <mic@digikod.net> wrote:
> On 23/03/2021 18:49, Jann Horn wrote:
> > On Tue, Mar 23, 2021 at 4:54 PM Mickaël Salaün <mic@digikod.net> wrote:
> >> On 23/03/2021 01:13, Jann Horn wrote:
> >>>  On Tue, Mar 16, 2021 at 9:43 PM Mickaël Salaün <mic@digikod.net> wrote:
> >>>> Using Landlock objects and ruleset, it is possible to tag inodes
> >>>> according to a process's domain.
> >>> [...]
> >>>> +static void release_inode(struct landlock_object *const object)
> >>>> +       __releases(object->lock)
> >>>> +{
> >>>> +       struct inode *const inode = object->underobj;
> >>>> +       struct super_block *sb;
> >>>> +
> >>>> +       if (!inode) {
> >>>> +               spin_unlock(&object->lock);
> >>>> +               return;
> >>>> +       }
> >>>> +
> >>>> +       /*
> >>>> +        * Protects against concurrent use by hook_sb_delete() of the reference
> >>>> +        * to the underlying inode.
> >>>> +        */
> >>>> +       object->underobj = NULL;
> >>>> +       /*
> >>>> +        * Makes sure that if the filesystem is concurrently unmounted,
> >>>> +        * hook_sb_delete() will wait for us to finish iput().
> >>>> +        */
> >>>> +       sb = inode->i_sb;
> >>>> +       atomic_long_inc(&landlock_superblock(sb)->inode_refs);
> >>>> +       spin_unlock(&object->lock);
> >>>> +       /*
> >>>> +        * Because object->underobj was not NULL, hook_sb_delete() and
> >>>> +        * get_inode_object() guarantee that it is safe to reset
> >>>> +        * landlock_inode(inode)->object while it is not NULL.  It is therefore
> >>>> +        * not necessary to lock inode->i_lock.
> >>>> +        */
> >>>> +       rcu_assign_pointer(landlock_inode(inode)->object, NULL);
> >>>> +       /*
> >>>> +        * Now, new rules can safely be tied to @inode with get_inode_object().
> >>>> +        */
> >>>> +
> >>>> +       iput(inode);
> >>>> +       if (atomic_long_dec_and_test(&landlock_superblock(sb)->inode_refs))
> >>>> +               wake_up_var(&landlock_superblock(sb)->inode_refs);
> >>>> +}
> >>> [...]
> >>>> +static struct landlock_object *get_inode_object(struct inode *const inode)
> >>>> +{
> >>>> +       struct landlock_object *object, *new_object;
> >>>> +       struct landlock_inode_security *inode_sec = landlock_inode(inode);
> >>>> +
> >>>> +       rcu_read_lock();
> >>>> +retry:
> >>>> +       object = rcu_dereference(inode_sec->object);
> >>>> +       if (object) {
> >>>> +               if (likely(refcount_inc_not_zero(&object->usage))) {
> >>>> +                       rcu_read_unlock();
> >>>> +                       return object;
> >>>> +               }
> >>>> +               /*
> >>>> +                * We are racing with release_inode(), the object is going
> >>>> +                * away.  Wait for release_inode(), then retry.
> >>>> +                */
> >>>> +               spin_lock(&object->lock);
> >>>> +               spin_unlock(&object->lock);
> >>>> +               goto retry;
> >>>> +       }
> >>>> +       rcu_read_unlock();
> >>>> +
> >>>> +       /*
> >>>> +        * If there is no object tied to @inode, then create a new one (without
> >>>> +        * holding any locks).
> >>>> +        */
> >>>> +       new_object = landlock_create_object(&landlock_fs_underops, inode);
> >>>> +       if (IS_ERR(new_object))
> >>>> +               return new_object;
> >>>> +
> >>>> +       /* Protects against concurrent get_inode_object() calls. */
> >>>> +       spin_lock(&inode->i_lock);
> >>>> +       object = rcu_dereference_protected(inode_sec->object,
> >>>> +                       lockdep_is_held(&inode->i_lock));
> >>>
> >>> rcu_dereference_protected() requires that inode_sec->object is not
> >>> concurrently changed, but I think another thread could call
> >>> get_inode_object() while we're in landlock_create_object(), and then
> >>> we could race with the NULL write in release_inode() here? (It
> >>> wouldn't actually be a UAF though because we're not actually accessing
> >>> `object` here.) Or am I missing a lock that prevents this?
> >>>
> >>> In v28 this wasn't an issue because release_inode() was holding
> >>> inode->i_lock (and object->lock) during the NULL store; but in v29 and
> >>> this version the NULL store in release_inode() moved out of the locked
> >>> region. I think you could just move the NULL store in release_inode()
> >>> back up (and maybe add a comment explaining the locking rules for
> >>> landlock_inode(...)->object)?
> >>>
> >>> (Or alternatively you could use rcu_dereference_raw() with a comment
> >>> explaining that the read pointer is only used to check for NULL-ness,
> >>> and that it is guaranteed that the pointer can't change if it is NULL
> >>> and we're holding the lock. But that'd be needlessly complicated, I
> >>> think.)
> >>
> >> To reach rcu_assign_pointer(landlock_inode(inode)->object, NULL) in
> >> release_inode() or in hook_sb_delete(), the
> >> landlock_inode(inode)->object need to be non-NULL,
> >
> > Yes.
> >
> >> which implies that a
> >> call to get_inode_object(inode) either "retry" (because release_inode is
> >> only called by landlock_put_object, which set object->usage to 0) until
> >> it creates a new object, or reuses the existing referenced object (and
> >> increments object->usage).
> >
> > But it can be that landlock_inode(inode)->object only becomes non-NULL
> > after get_inode_object() has checked
> > rcu_dereference(inode_sec->object).
> >
> >> The worse case would be if
> >> get_inode_object(inode) is called just before the
> >> rcu_assign_pointer(landlock_inode(inode)->object, NULL) from
> >> hook_sb_delete(), which would result in an object with a NULL underobj,
> >> which is the expected behavior (and checked by release_inode).
> >
> > The scenario I'm talking about doesn't involve hook_sb_delete().
> >
> >> The line rcu_assign_pointer(inode_sec->object, new_object) from
> >> get_inode_object() can only be reached if the underlying inode doesn't
> >> reference an object,
> >
> > Yes.
> >
> >> in which case hook_sb_delete() will not reach the
> >> rcu_assign_pointer(landlock_inode(inode)->object, NULL) line for this
> >> same inode.
> >>
> >> This works because get_inode_object(inode) is mutually exclusive to
> >> itself with the same inode (i.e. an inode can only point to an object
> >> that references this same inode).
> >
> > To clarify: You can concurrently call get_inode_object() multiple
> > times on the same inode, right? There are no locks held on entry to
> > that function.
> >
> >> I tried to explain this with the comment "Protects against concurrent
> >> get_inode_object() calls" in get_inode_object(), and the comments just
> >> before both rcu_assign_pointer(landlock_inode(inode)->object, NULL).
> >
> > The scenario I'm talking about is:
> >
> > Initially the inode does not have an associated landlock_object. There
> > are two threads A and B. Thread A is going to execute
> > get_inode_object(). Thread B is going to execute get_inode_object()
> > followed immediately by landlock_put_object().
> >
> > thread A: enters get_inode_object()
> > thread A: rcu_dereference(inode_sec->object) returns NULL
> > thread A: enters landlock_create_object()
> > thread B: enters get_inode_object()
> > thread B: rcu_dereference(inode_sec->object) returns NULL
> > thread B: calls landlock_create_object()
> > thread B: sets inode_sec->object while holding inode->i_lock
> > thread B: leaves get_inode_object()
> > thread B: enters landlock_put_object()
> > thread B: object->usage drops to 0, object->lock is taken
> > thread B: calls release_inode()
> > thread B: drops object->lock
> > thread A: returns from landlock_create_object()
> > thread A: takes inode->i_lock
> >
> > At this point, thread B will run:
> >
> >     rcu_assign_pointer(landlock_inode(inode)->object, NULL);
> >
> > while thread A runs:
> >
> >     rcu_dereference_protected(inode_sec->object,
> >         lockdep_is_held(&inode->i_lock));
> >
> > meaning there is a (theoretical) data race, since
> > rcu_dereference_protected() doesn't use READ_ONCE().
>
> Hum, I see, that is what I was missing. And that explain why there is
> (in practice) no impact on winning the race.
>
> I would prefer to use rcu_access_pointer() instead of
> rcu_dereference_protected() to avoid pitfall, and it reflects what I was
> expecting:
>
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -117,9 +117,7 @@ static struct landlock_object
> *get_inode_object(struct inode *const inode)
>
>         /* Protects against concurrent get_inode_object() calls. */
>         spin_lock(&inode->i_lock);
> -       object = rcu_dereference_protected(inode_sec->object,
> -                       lockdep_is_held(&inode->i_lock));
> -       if (unlikely(object)) {
> +       if (unlikely(rcu_access_pointer(inode_sec->object))) {
>                 /* Someone else just created the object, bail out and
> retry. */
>                 spin_unlock(&inode->i_lock);
>                 kfree(new_object);

Ah, yeah, that should work. I had forgotten about rcu_access_pointer().

> But I'm not sure about your proposition to move the NULL store in
> release_inode() back up. Do you mean to add back the inode lock in
> release_inode() like this?
>
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -59,16 +59,12 @@ static void release_inode(struct landlock_object
> *const object)
>          * Makes sure that if the filesystem is concurrently unmounted,
>          * hook_sb_delete() will wait for us to finish iput().
>          */
> +       spin_lock(&inode->i_lock);
>         sb = inode->i_sb;
>         atomic_long_inc(&landlock_superblock(sb)->inode_refs);
>         spin_unlock(&object->lock);
> -       /*
> -        * Because object->underobj was not NULL, hook_sb_delete() and
> -        * get_inode_object() guarantee that it is safe to reset
> -        * landlock_inode(inode)->object while it is not NULL.  It is therefore
> -        * not necessary to lock inode->i_lock.
> -        */
>         rcu_assign_pointer(landlock_inode(inode)->object, NULL);
> +       spin_unlock(&inode->i_lock);
>         /*
>          * Now, new rules can safely be tied to @inode with get_inode_object().
>          */
>
>
> I would prefer to avoid nested locks if it is not necessary though.

Hm, yeah, you have a point there.

Doing it locklessly does make the locking rules a little complicated
though, and you'll have to update the comment inside struct
landlock_inode_security. At the moment, it says:

* @object: Weak pointer to an allocated object.  All writes (i.e.
* creating a new object or removing one) are protected by the
* underlying inode->i_lock.  Disassociating @object from the inode is
* additionally protected by @object->lock, from the time @object's
* usage refcount drops to zero to the time this pointer is nulled out.

which isn't true anymore.

  reply	other threads:[~2021-03-24  3:12 UTC|newest]

Thread overview: 50+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-03-16 20:42 [PATCH v30 00/12] Landlock LSM Mickaël Salaün
2021-03-16 20:42 ` [PATCH v30 01/12] landlock: Add object management Mickaël Salaün
2021-03-19 18:13   ` Kees Cook
2021-03-19 18:57     ` Mickaël Salaün
2021-03-16 20:42 ` [PATCH v30 02/12] landlock: Add ruleset and domain management Mickaël Salaün
2021-03-19 18:40   ` Kees Cook
2021-03-19 19:03     ` Mickaël Salaün
2021-03-19 19:15       ` Kees Cook
2021-03-24 20:31       ` James Morris
2021-03-25  9:29         ` Mickaël Salaün
2021-03-23  0:13   ` Jann Horn
2021-03-16 20:42 ` [PATCH v30 03/12] landlock: Set up the security framework and manage credentials Mickaël Salaün
2021-03-19 18:45   ` Kees Cook
2021-03-19 19:07     ` Mickaël Salaün
2021-03-16 20:42 ` [PATCH v30 04/12] landlock: Add ptrace restrictions Mickaël Salaün
2021-03-19 18:45   ` Kees Cook
2021-03-16 20:42 ` [PATCH v30 05/12] LSM: Infrastructure management of the superblock Mickaël Salaün
2021-03-19 17:24   ` Kees Cook
2021-03-16 20:42 ` [PATCH v30 06/12] fs,security: Add sb_delete hook Mickaël Salaün
2021-03-19 17:24   ` Kees Cook
2021-03-16 20:42 ` [PATCH v30 07/12] landlock: Support filesystem access-control Mickaël Salaün
2021-03-18 23:10   ` James Morris
2021-03-19 18:57   ` Kees Cook
2021-03-19 19:19     ` Mickaël Salaün
2021-03-23 19:30       ` Mickaël Salaün
2021-03-23  0:13   ` Jann Horn
2021-03-23 15:55     ` Mickaël Salaün
2021-03-23 17:49       ` Jann Horn
2021-03-23 19:22         ` Mickaël Salaün
2021-03-24  3:10           ` Jann Horn [this message]
2021-03-16 20:42 ` [PATCH v30 08/12] landlock: Add syscall implementations Mickaël Salaün
2021-03-19 19:06   ` Kees Cook
2021-03-19 21:53     ` Mickaël Salaün
2021-03-24 15:03       ` Mickaël Salaün
2021-03-16 20:42 ` [PATCH v30 09/12] arch: Wire up Landlock syscalls Mickaël Salaün
2021-03-16 20:42 ` [PATCH v30 10/12] selftests/landlock: Add user space tests Mickaël Salaün
2021-03-19 17:56   ` Kees Cook
2021-03-19 18:41     ` Mickaël Salaün
2021-03-19 19:11       ` Kees Cook
2021-03-19 21:57         ` Mickaël Salaün
2021-03-16 20:42 ` [PATCH v30 11/12] samples/landlock: Add a sandbox manager example Mickaël Salaün
2021-03-19 17:26   ` Kees Cook
2021-03-16 20:42 ` [PATCH v30 12/12] landlock: Add user and kernel documentation Mickaël Salaün
2021-03-19 18:03   ` Kees Cook
2021-03-19 18:54     ` Mickaël Salaün
2021-03-23 19:25       ` Mickaël Salaün
2021-03-24 16:21       ` Mickaël Salaün
2021-03-18 23:26 ` [PATCH v30 00/12] Landlock LSM James Morris
2021-03-18 23:26   ` James Morris
2021-03-19 15:52   ` Mickaël Salaün

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=CAG48ez0ex48pCgunrg+BpJ-LppZUVXhsmiEW_2d2mhbkDB793Q@mail.gmail.com \
    --to=jannh@google.com \
    --cc=akpm@linux-foundation.org \
    --cc=anton.ivanov@cambridgegreys.com \
    --cc=arnd@arndb.de \
    --cc=casey@schaufler-ca.com \
    --cc=corbet@lwn.net \
    --cc=dhowells@redhat.com \
    --cc=jdike@addtoit.com \
    --cc=jmorris@namei.org \
    --cc=keescook@chromium.org \
    --cc=kernel-hardening@lists.openwall.com \
    --cc=linux-api@vger.kernel.org \
    --cc=linux-arch@vger.kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=luto@amacapital.net \
    --cc=mic@digikod.net \
    --cc=mic@linux.microsoft.com \
    --cc=mtk.manpages@gmail.com \
    --cc=richard@nod.at \
    --cc=serge@hallyn.com \
    --cc=shuah@kernel.org \
    --cc=vincent.dagonneau@ssi.gouv.fr \
    --cc=viro@zeniv.linux.org.uk \
    --cc=x86@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.