diff mbox series

[v2] keys: Fix UAF in key_put()

Message ID 2874581.1742399866@warthog.procyon.org.uk (mailing list archive)
State New
Headers show
Series [v2] keys: Fix UAF in key_put() | expand

Commit Message

David Howells March 19, 2025, 3:57 p.m. UTC
Once a key's reference count has been reduced to 0, the garbage collector
thread may destroy it at any time and so key_put() is not allowed to touch
the key after that point.  The most key_put() is normally allowed to do is
to touch key_gc_work as that's a static global variable.

However, in an effort to speed up the reclamation of quota, this is now
done in key_put() once the key's usage is reduced to 0 - but now the code
is looking at the key after the deadline, which is forbidden.

Fix this by using a flag to indicate that a key can be gc'd now rather than
looking at the key's refcount in the garbage collector.

Fixes: 9578e327b2b4 ("keys: update key quotas in key_put()")
Reported-by: syzbot+6105ffc1ded71d194d6d@syzkaller.appspotmail.com
Signed-off-by: David Howells <dhowells@redhat.com>
Tested-by: syzbot+6105ffc1ded71d194d6d@syzkaller.appspotmail.com
cc: Jarkko Sakkinen <jarkko@kernel.org>
cc: Oleg Nesterov <oleg@redhat.com>
cc: Kees Cook <kees@kernel.org>
cc: Hillf Danton <hdanton@sina.com>,
cc: keyrings@vger.kernel.org
Cc: stable@vger.kernel.org # v6.10+
---
 include/linux/key.h |    1 +
 security/keys/gc.c  |    4 +++-
 security/keys/key.c |    2 ++
 3 files changed, 6 insertions(+), 1 deletion(-)

Comments

Oleg Nesterov March 19, 2025, 4:30 p.m. UTC | #1
On 03/19, David Howells wrote:
>
> --- a/security/keys/gc.c
> +++ b/security/keys/gc.c
> @@ -218,8 +218,10 @@ static void key_garbage_collector(struct work_struct *work)
>  		key = rb_entry(cursor, struct key, serial_node);
>  		cursor = rb_next(cursor);
>  
> -		if (refcount_read(&key->usage) == 0)
> +		if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
> +			smp_mb(); /* Clobber key->user after FINAL_PUT seen. */
>  			goto found_unreferenced_key;
> +		}
>  
>  		if (unlikely(gc_state & KEY_GC_REAPING_DEAD_1)) {
>  			if (key->type == key_gc_dead_keytype) {
> diff --git a/security/keys/key.c b/security/keys/key.c
> index 3d7d185019d3..7198cd2ac3a3 100644
> --- a/security/keys/key.c
> +++ b/security/keys/key.c
> @@ -658,6 +658,8 @@ void key_put(struct key *key)
>  				key->user->qnbytes -= key->quotalen;
>  				spin_unlock_irqrestore(&key->user->lock, flags);
>  			}
> +			smp_mb(); /* key->user before FINAL_PUT set. */

Can't resist, smp_mb__before_atomic() should equally work,
but this doesn't really matter, please forget.

> +			set_bit(KEY_FLAG_FINAL_PUT, &key->flags);
>  			schedule_work(&key_gc_work);

I believe this patch is correct,

Reviewed-by: Oleg Nesterov <oleg@redhat.com>
Linus Torvalds March 19, 2025, 6:11 p.m. UTC | #2
On Wed, 19 Mar 2025 at 09:31, Oleg Nesterov <oleg@redhat.com> wrote:
>
> Can't resist, smp_mb__before_atomic() should equally work,
> but this doesn't really matter, please forget.

We really should have "test_bit_acquire()" and "set_bit_release()".

Well, we do have the test_bit_acquire().

We just don't have the set_bit side, because we only have the bit
clearing version (and it's called "clear_bit_unlock()" for historical
reasons).

Annoying.

             Linus
David Howells March 19, 2025, 6:47 p.m. UTC | #3
Linus Torvalds <torvalds@linuxfoundation.org> wrote:

> We really should have "test_bit_acquire()" and "set_bit_release()".

I considered using test_bit_acquire() but, as you say, there's no
set_bit_release() as yet.  I could switch things to initialise the flag to set
on key creation and clear the flag instead.

David
Jarkko Sakkinen March 20, 2025, 4:14 p.m. UTC | #4
On Wed, Mar 19, 2025 at 03:57:46PM +0000, David Howells wrote:
>     
> Once a key's reference count has been reduced to 0, the garbage collector
> thread may destroy it at any time and so key_put() is not allowed to touch
> the key after that point.  The most key_put() is normally allowed to do is
> to touch key_gc_work as that's a static global variable.
> 
> However, in an effort to speed up the reclamation of quota, this is now
> done in key_put() once the key's usage is reduced to 0 - but now the code
> is looking at the key after the deadline, which is forbidden.
> 
> Fix this by using a flag to indicate that a key can be gc'd now rather than
> looking at the key's refcount in the garbage collector.
> 
> Fixes: 9578e327b2b4 ("keys: update key quotas in key_put()")
> Reported-by: syzbot+6105ffc1ded71d194d6d@syzkaller.appspotmail.com
> Signed-off-by: David Howells <dhowells@redhat.com>
> Tested-by: syzbot+6105ffc1ded71d194d6d@syzkaller.appspotmail.com
> cc: Jarkko Sakkinen <jarkko@kernel.org>
> cc: Oleg Nesterov <oleg@redhat.com>
> cc: Kees Cook <kees@kernel.org>
> cc: Hillf Danton <hdanton@sina.com>,
> cc: keyrings@vger.kernel.org
> Cc: stable@vger.kernel.org # v6.10+
> ---
>  include/linux/key.h |    1 +
>  security/keys/gc.c  |    4 +++-
>  security/keys/key.c |    2 ++
>  3 files changed, 6 insertions(+), 1 deletion(-)
> 
> diff --git a/include/linux/key.h b/include/linux/key.h
> index 074dca3222b9..ba05de8579ec 100644
> --- a/include/linux/key.h
> +++ b/include/linux/key.h
> @@ -236,6 +236,7 @@ struct key {
>  #define KEY_FLAG_ROOT_CAN_INVAL	7	/* set if key can be invalidated by root without permission */
>  #define KEY_FLAG_KEEP		8	/* set if key should not be removed */
>  #define KEY_FLAG_UID_KEYRING	9	/* set if key is a user or user session keyring */
> +#define KEY_FLAG_FINAL_PUT	10	/* set if final put has happened on key */
>  
>  	/* the key type and key description string
>  	 * - the desc is used to match a key against search criteria
> diff --git a/security/keys/gc.c b/security/keys/gc.c
> index 7d687b0962b1..f27223ea4578 100644
> --- a/security/keys/gc.c
> +++ b/security/keys/gc.c
> @@ -218,8 +218,10 @@ static void key_garbage_collector(struct work_struct *work)
>  		key = rb_entry(cursor, struct key, serial_node);
>  		cursor = rb_next(cursor);
>  
> -		if (refcount_read(&key->usage) == 0)
> +		if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
> +			smp_mb(); /* Clobber key->user after FINAL_PUT seen. */

test_bit() is already atomic.

https://docs.kernel.org/core-api/wrappers/atomic_bitops.html

>  			goto found_unreferenced_key;
> +		}
>  
>  		if (unlikely(gc_state & KEY_GC_REAPING_DEAD_1)) {
>  			if (key->type == key_gc_dead_keytype) {
> diff --git a/security/keys/key.c b/security/keys/key.c
> index 3d7d185019d3..7198cd2ac3a3 100644
> --- a/security/keys/key.c
> +++ b/security/keys/key.c
> @@ -658,6 +658,8 @@ void key_put(struct key *key)
>  				key->user->qnbytes -= key->quotalen;
>  				spin_unlock_irqrestore(&key->user->lock, flags);
>  			}
> +			smp_mb(); /* key->user before FINAL_PUT set. */
> +			set_bit(KEY_FLAG_FINAL_PUT, &key->flags);

Ditto.

Nit: I'm just thinking should the name imply more like that "now
key_put() is actually done". E.g., even something like KEY_FLAG_PUT_DONE
would be more self-descriptive.

>  			schedule_work(&key_gc_work);
>  		}
>  	}
> 
> 

BR, Jarkko
David Howells March 20, 2025, 4:39 p.m. UTC | #5
Jarkko Sakkinen <jarkko@kernel.org> wrote:

> > +		if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
> > +			smp_mb(); /* Clobber key->user after FINAL_PUT seen. */
> 
> test_bit() is already atomic.

Atomiticity doesn't apply to test_bit() - it only matters when it does two (or
more) accesses that must be perceptually indivisible (e.g. set_bit doing RMW).

But atomiticity isn't the issue here, hence the barrier.  You need to be
looking at memory-barriers.txt, not atomic_bitops.txt.

We have two things to correctly order and set_bit() does not imply sufficient
barriering; test_and_set_bit() does, but not set_bit(), hence Linus's comment
about really wanting a set_bit_release().

> > +			smp_mb(); /* key->user before FINAL_PUT set. */
> > +			set_bit(KEY_FLAG_FINAL_PUT, &key->flags);
> 
> Ditto.

Ditto. ;-)

> Nit: I'm just thinking should the name imply more like that "now
> key_put() is actually done". E.g., even something like KEY_FLAG_PUT_DONE
> would be more self-descriptive.

KEY_FLAG_PUT_DONE isn't right.  There can be lots of puts on a single key -
only the one that reduces it to 0 matters for this.  You could call it
KEY_FLAG_CAN_NOW_GC or KEY_FLAG_GC_ABLE.

David
Jarkko Sakkinen March 20, 2025, 5:28 p.m. UTC | #6
On Thu, Mar 20, 2025 at 04:39:11PM +0000, David Howells wrote:
> Jarkko Sakkinen <jarkko@kernel.org> wrote:
> 
> > > +		if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
> > > +			smp_mb(); /* Clobber key->user after FINAL_PUT seen. */
> > 
> > test_bit() is already atomic.
> 
> Atomiticity doesn't apply to test_bit() - it only matters when it does two (or
> more) accesses that must be perceptually indivisible (e.g. set_bit doing RMW).
> 
> But atomiticity isn't the issue here, hence the barrier.  You need to be
> looking at memory-barriers.txt, not atomic_bitops.txt.
> 
> We have two things to correctly order and set_bit() does not imply sufficient
> barriering; test_and_set_bit() does, but not set_bit(), hence Linus's comment
> about really wanting a set_bit_release().

Oops, I was hallucinating here. And yeah, test_and_set_bit() does
imply full mb as you said.

I was somehow remembering what I did in SGX driver incorrectly and
that led me into misconclusions, sorry.

if (test_and_set_bit(SGX_ENCL_IOCTL, &encl->flags))
	return -EBUSY;

> 
> > > +			smp_mb(); /* key->user before FINAL_PUT set. */
> > > +			set_bit(KEY_FLAG_FINAL_PUT, &key->flags);
> > 
> > Ditto.
> 
> Ditto. ;-)

Duh, no need poke with the stick further (or deeper) ;-)

> 
> > Nit: I'm just thinking should the name imply more like that "now
> > key_put() is actually done". E.g., even something like KEY_FLAG_PUT_DONE
> > would be more self-descriptive.
> 
> KEY_FLAG_PUT_DONE isn't right.  There can be lots of puts on a single key -
> only the one that reduces it to 0 matters for this.  You could call it
> KEY_FLAG_CAN_NOW_GC or KEY_FLAG_GC_ABLE.

Well all alternatives are fine but my thinking was that one that finally
zeros the refcount, "finalizes put" (pick whatever you want anyway).

> 
> David

BR, Jarkko
Jarkko Sakkinen March 20, 2025, 6:46 p.m. UTC | #7
On Thu, Mar 20, 2025 at 07:28:36PM +0200, Jarkko Sakkinen wrote:
> On Thu, Mar 20, 2025 at 04:39:11PM +0000, David Howells wrote:
> > Jarkko Sakkinen <jarkko@kernel.org> wrote:
> > 
> > > > +		if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
> > > > +			smp_mb(); /* Clobber key->user after FINAL_PUT seen. */
> > > 
> > > test_bit() is already atomic.
> > 
> > Atomiticity doesn't apply to test_bit() - it only matters when it does two (or
> > more) accesses that must be perceptually indivisible (e.g. set_bit doing RMW).
> > 
> > But atomiticity isn't the issue here, hence the barrier.  You need to be
> > looking at memory-barriers.txt, not atomic_bitops.txt.
> > 
> > We have two things to correctly order and set_bit() does not imply sufficient
> > barriering; test_and_set_bit() does, but not set_bit(), hence Linus's comment
> > about really wanting a set_bit_release().
> 
> Oops, I was hallucinating here. And yeah, test_and_set_bit() does
> imply full mb as you said.
> 
> I was somehow remembering what I did in SGX driver incorrectly and
> that led me into misconclusions, sorry.
> 
> if (test_and_set_bit(SGX_ENCL_IOCTL, &encl->flags))
> 	return -EBUSY;
> 
> > 
> > > > +			smp_mb(); /* key->user before FINAL_PUT set. */
> > > > +			set_bit(KEY_FLAG_FINAL_PUT, &key->flags);
> > > 
> > > Ditto.
> > 
> > Ditto. ;-)
> 
> Duh, no need poke with the stick further (or deeper) ;-)
> 
> > 
> > > Nit: I'm just thinking should the name imply more like that "now
> > > key_put() is actually done". E.g., even something like KEY_FLAG_PUT_DONE
> > > would be more self-descriptive.
> > 
> > KEY_FLAG_PUT_DONE isn't right.  There can be lots of puts on a single key -
> > only the one that reduces it to 0 matters for this.  You could call it
> > KEY_FLAG_CAN_NOW_GC or KEY_FLAG_GC_ABLE.
> 
> Well all alternatives are fine but my thinking was that one that finally
> zeros the refcount, "finalizes put" (pick whatever you want anyway).

I'll pick this one up tomorrow and put a PR out within this week.

BR, Jarkko
Jarkko Sakkinen March 20, 2025, 6:48 p.m. UTC | #8
On Thu, Mar 20, 2025 at 08:46:41PM +0200, Jarkko Sakkinen wrote:
> On Thu, Mar 20, 2025 at 07:28:36PM +0200, Jarkko Sakkinen wrote:
> > On Thu, Mar 20, 2025 at 04:39:11PM +0000, David Howells wrote:
> > > Jarkko Sakkinen <jarkko@kernel.org> wrote:
> > > 
> > > > > +		if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
> > > > > +			smp_mb(); /* Clobber key->user after FINAL_PUT seen. */
> > > > 
> > > > test_bit() is already atomic.
> > > 
> > > Atomiticity doesn't apply to test_bit() - it only matters when it does two (or
> > > more) accesses that must be perceptually indivisible (e.g. set_bit doing RMW).
> > > 
> > > But atomiticity isn't the issue here, hence the barrier.  You need to be
> > > looking at memory-barriers.txt, not atomic_bitops.txt.
> > > 
> > > We have two things to correctly order and set_bit() does not imply sufficient
> > > barriering; test_and_set_bit() does, but not set_bit(), hence Linus's comment
> > > about really wanting a set_bit_release().
> > 
> > Oops, I was hallucinating here. And yeah, test_and_set_bit() does
> > imply full mb as you said.
> > 
> > I was somehow remembering what I did in SGX driver incorrectly and
> > that led me into misconclusions, sorry.
> > 
> > if (test_and_set_bit(SGX_ENCL_IOCTL, &encl->flags))
> > 	return -EBUSY;
> > 
> > > 
> > > > > +			smp_mb(); /* key->user before FINAL_PUT set. */
> > > > > +			set_bit(KEY_FLAG_FINAL_PUT, &key->flags);
> > > > 
> > > > Ditto.
> > > 
> > > Ditto. ;-)
> > 
> > Duh, no need poke with the stick further (or deeper) ;-)
> > 
> > > 
> > > > Nit: I'm just thinking should the name imply more like that "now
> > > > key_put() is actually done". E.g., even something like KEY_FLAG_PUT_DONE
> > > > would be more self-descriptive.
> > > 
> > > KEY_FLAG_PUT_DONE isn't right.  There can be lots of puts on a single key -
> > > only the one that reduces it to 0 matters for this.  You could call it
> > > KEY_FLAG_CAN_NOW_GC or KEY_FLAG_GC_ABLE.
> > 
> > Well all alternatives are fine but my thinking was that one that finally
> > zeros the refcount, "finalizes put" (pick whatever you want anyway).
> 
> I'll pick this one up tomorrow and put a PR out within this week.

I can try get this done tomorrow fully (with only one patch in the PR)
so that we would get it still to the ongoing release...

BR, Jarkko
diff mbox series

Patch

diff --git a/include/linux/key.h b/include/linux/key.h
index 074dca3222b9..ba05de8579ec 100644
--- a/include/linux/key.h
+++ b/include/linux/key.h
@@ -236,6 +236,7 @@  struct key {
 #define KEY_FLAG_ROOT_CAN_INVAL	7	/* set if key can be invalidated by root without permission */
 #define KEY_FLAG_KEEP		8	/* set if key should not be removed */
 #define KEY_FLAG_UID_KEYRING	9	/* set if key is a user or user session keyring */
+#define KEY_FLAG_FINAL_PUT	10	/* set if final put has happened on key */
 
 	/* the key type and key description string
 	 * - the desc is used to match a key against search criteria
diff --git a/security/keys/gc.c b/security/keys/gc.c
index 7d687b0962b1..f27223ea4578 100644
--- a/security/keys/gc.c
+++ b/security/keys/gc.c
@@ -218,8 +218,10 @@  static void key_garbage_collector(struct work_struct *work)
 		key = rb_entry(cursor, struct key, serial_node);
 		cursor = rb_next(cursor);
 
-		if (refcount_read(&key->usage) == 0)
+		if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
+			smp_mb(); /* Clobber key->user after FINAL_PUT seen. */
 			goto found_unreferenced_key;
+		}
 
 		if (unlikely(gc_state & KEY_GC_REAPING_DEAD_1)) {
 			if (key->type == key_gc_dead_keytype) {
diff --git a/security/keys/key.c b/security/keys/key.c
index 3d7d185019d3..7198cd2ac3a3 100644
--- a/security/keys/key.c
+++ b/security/keys/key.c
@@ -658,6 +658,8 @@  void key_put(struct key *key)
 				key->user->qnbytes -= key->quotalen;
 				spin_unlock_irqrestore(&key->user->lock, flags);
 			}
+			smp_mb(); /* key->user before FINAL_PUT set. */
+			set_bit(KEY_FLAG_FINAL_PUT, &key->flags);
 			schedule_work(&key_gc_work);
 		}
 	}