Thread (1 message) 1 message, 1 author, 2021-06-07

Re: KASAN: use-after-free Read in hci_chan_del

From: Greg KH <gregkh@linuxfoundation.org>
Date: 2021-06-07 10:31:44
Also in: linux-bluetooth, lkml

On Mon, Jun 07, 2021 at 06:02:01PM +0800, Hillf Danton wrote:
On Mon, 7 Jun 2021 09:55:31 +0200 Greg KH wrote:
quoted
On Mon, Jun 07, 2021 at 03:48:28PM +0800, Hillf Danton wrote:
quoted
On Sun, 6 Jun 2021 11:54:22 +0200 Greg KH wrote:
quoted
On Sun, Jun 06, 2021 at 04:50:04PM +0800, Hillf Danton wrote:
quoted
To fix the uaf reported, add reference count to hci channel to track users.
Then only channels with zero users will be released.

It is now only for thoughts.
+++ x/include/net/bluetooth/hci_core.h
@@ -704,6 +704,7 @@ struct hci_chan {
 	struct sk_buff_head data_q;
 	unsigned int	sent;
 	__u8		state;
+	atomic_t ref;
Please no, never use "raw" atomic variables.  Especially for something
like this, use a kref.
Fair, thanks for taking a look at it.

Spin with care for the race the added ref fails to cut.
I do not understand what you mean here.
quoted
To ease review the full syzreport is also attached.

To fix uaf, add user track to hci channel and we will only release channel if
its user hits zero. And a dryrun mechanism is also added to take care of the
race user track fails to cut.

	CPU0			CPU1
	----			----
	hci_chan_del		l2cap_conn_del
				chan->user = 0;

	if (chan->user != 0)
		return;
	synchronize_rcu();
	kfree(chan);

				hci_chan_del();

It is now only for thoughts.
+++ x/include/net/bluetooth/hci_core.h
@@ -704,6 +704,10 @@ struct hci_chan {
 	struct sk_buff_head data_q;
 	unsigned int	sent;
 	__u8		state;
+	__u8		user;
No.
quoted
+	__u8		release;
No please no.
quoted
+
+#define HCHAN_RELEASE_DRYRUN 1
 };
 
 struct hci_conn_params {
+++ x/net/bluetooth/l2cap_core.c
@@ -1903,6 +1903,12 @@ static void l2cap_conn_del(struct hci_co
 
 	mutex_unlock(&conn->chan_lock);
 
+	/* see comment in hci_chan_del() */
+	conn->hchan->release = HCHAN_RELEASE_DRYRUN;
+	smp_wmb();
+	conn->hchan->user--;
And the reason you are open-coding a kref is why???

Please again no.
quoted
+	hci_chan_del(conn->hchan);
+	conn->hchan->release = 0;
 	hci_chan_del(conn->hchan);
 
 	if (conn->info_state & L2CAP_INFO_FEAT_MASK_REQ_SENT)
@@ -7716,6 +7722,8 @@ static struct l2cap_conn *l2cap_conn_add
 	kref_init(&conn->ref);
 	hcon->l2cap_data = conn;
 	conn->hcon = hci_conn_get(hcon);
+	/* dec in l2cap_conn_del() */
+	hchan->user++;
{sigh}

No, there is a reason we wrote kref many _decades_ ago.  Please use it,
your original attempt with an atomic was just fine, just use the proper
data structures the kernel provides you as this is obviously a reference
counted object.
I see your concern. I thought this is a simpler user track than kref and
open coded a couple of lines. I see it is incorrect. Sorry for that.

After taking another look at the added user track, I realised that it serves
no more than a one-off state word that prevents channel from being released.
Then the race behind the uaf can be fixed by adding a state on top of the
dryrun introduced even without tracking users.

The state machine works as the following,
1) it is initialised to be backoff that means channel cannot be released
   at the moment.
2) it is changed to be dryrun on releasing to cut the race that survived
   backoff.
3) it is finally set to zero for release after cutting the chance for race.
Adding another state on top of this feels rough, does it really solve
the race here?  Normally a reference count should be enough to properly
tear things down when needed, rolling back from a "can I try this now"
state still seems racy without the needed lock somewhere.

quoted hunk
+++ x/include/net/bluetooth/hci_core.h
@@ -704,6 +704,10 @@ struct hci_chan {
 	struct sk_buff_head data_q;
 	unsigned int	sent;
 	__u8		state;
+	__u8		release;
+
+#define HCHAN_RELEASE_DRYRUN  1
+#define HCHAN_RELEASE_BACKOFF 2
 };
 
 struct hci_conn_params {
+++ x/net/bluetooth/l2cap_core.c
@@ -1903,6 +1903,10 @@ static void l2cap_conn_del(struct hci_co
 
 	mutex_unlock(&conn->chan_lock);
 
+	/* see comment in hci_chan_del() */
+	conn->hchan->release = HCHAN_RELEASE_DRYRUN;
+	hci_chan_del(conn->hchan);
+	conn->hchan->release = 0;
 	hci_chan_del(conn->hchan);
 
 	if (conn->info_state & L2CAP_INFO_FEAT_MASK_REQ_SENT)
@@ -7716,6 +7720,8 @@ static struct l2cap_conn *l2cap_conn_add
 	kref_init(&conn->ref);
 	hcon->l2cap_data = conn;
 	conn->hcon = hci_conn_get(hcon);
+	/* release is changed in l2cap_conn_del() */
+	hchan->release = HCHAN_RELEASE_BACKOFF;
 	conn->hchan = hchan;
 
 	BT_DBG("hcon %p conn %p hchan %p", hcon, conn, hchan);
+++ x/net/bluetooth/hci_conn.c
@@ -1769,12 +1769,26 @@ void hci_chan_del(struct hci_chan *chan)
 	struct hci_conn *conn = chan->conn;
 	struct hci_dev *hdev = conn->hdev;
 
+	if (chan->release == HCHAN_RELEASE_BACKOFF)
+		return;
+
 	BT_DBG("%s hcon %p chan %p", hdev->name, conn, chan);
 
-	list_del_rcu(&chan->list);
+	if (!list_empty(&chan->list))
+		list_del_rcu(&chan->list);
 
 	synchronize_rcu();
 
+	if (chan->release == HCHAN_RELEASE_DRYRUN) {
+		/*
+		 * after list_del chan is only visible to the owner of dryrun,
+		 * which is needed to cut the race that survived backoff,
+		 * and simply return to owner. 
checkpatch will ding you for this not being the proper format for
networking, it hit me last week as well :)

thanks,

greg k-h
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help