Sorry for the confusion on our last email. We did a little more analysis
after then and hope to help developers fix this bug.
The bug was reported by syzbot first in Aug 2020. Since it remains
unpatched to this date, we have conducted some analysis to determine its
security impact and root causes, which hopefully can help with the
patching decisions.
Specifically, we find that even though it is labeled as "UAF read" by
syzbot, it can in fact lead to double free and control flow hijacking as
well. Here is our analysis below (on this kernel version:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/log/?id=af5043c89a8ef6b6949a245fff355a552eaed240)
----------------------------- Root cause analysis:
--------------------------
The use-after-free bug happened because the object has two different
references. But when it was freed, only one reference was removed,
allowing the other reference to be used incorrectly.
Specifically, the object of type "struct hci_chan" can be referenced in
two places from an object called hcon(or conn in hci_chan_create)of type
struct hci_conn : "hcon->chan_list" and "hcon->l2cap_data->hchan". But
only one of them (conn->chan_list) was deleted when freeing "struct
hci_chan" from "hci_disconn_loglink_complete_evt()".
The function "hci_chan_create" shows how the first reference is created.
struct hci_chan *hci_chan_create(struct hci_conn *conn)
{
struct hci_dev *hdev = conn->hdev;
struct hci_chan *chan;
...
chan = kzalloc(sizeof(*chan), GFP_KERNEL);
...
list_add_rcu(&chan->list, &conn->chan_list); // Assign chan to
hcon->chan_list. This is the first reference created.
return chan;
}
"l2cap_conn_add" is the caller of the previous function which shows how
the second reference is created.
static struct l2cap_conn *l2cap_conn_add(struct hci_conn *hcon)
{
struct l2cap_conn *conn = hcon->l2cap_data;
struct hci_chan *hchan;
...
hchan = hci_chan_create(hcon); //"hchan" was created in hci_chan_create
if (!hchan)
return NULL;
conn = kzalloc(sizeof(*conn), GFP_KERNEL);
...
kref_init(&conn->ref);
hcon->l2cap_data = conn;
conn->hcon = hci_conn_get(hcon);
conn->hchan = hchan; // "chan" was assigned to
"hcon->l2cap_data->hchan". This is the second reference.
...
}
When the chan was freed in "hci_disconn_loglink_complete_evt"
(hci_disconn_loglink_complete_evt()->amp_destroy_logical_link()->hci_chan_del()),
we only deleted the reference of "((struct hci_conn *)hcon)->chan_list"
(effectively removing the entry from the list), but the reference of
"((struct hci_conn *)hcon)->l2cap_data->hchan" is still valid.
The function below shows exactly how the free of the object occurs and
how its first reference is removed.
void hci_chan_del(struct hci_chan *chan)
{
struct hci_conn *conn = chan->conn;
struct hci_dev *hdev = conn->hdev;
BT_DBG("%s hcon %p chan %p", hdev->name, conn, chan);
list_del_rcu(&chan->list); // removed "chan" from the list (the
first reference). The second reference((struct hci_conn
*)hcon->l2cap_data->hchan) remains however.
synchronize_rcu();
set_bit(HCI_CONN_DROP, &conn->flags);
hci_conn_put(conn);
skb_queue_purge(&chan->data_q);
kfree(chan); // free "chan"
}
----------------------------- Potential fix: --------------------------
Based on the analysis, it appears that in hci_chan_del(), we should
remove the second reference of (struct hci_conn
*)hcon->l2cap_data->hchan,e.g., setting it to NULL
-------------------------- Control flow hijacking Primitve:
-----------------------------
This function is where the bug impact was originally reported on syzbot
void hci_chan_del(struct hci_chan *chan) //"chan" was freed
{
struct hci_conn *conn = chan->conn; // Syzbot reported the UAF read
struct hci_dev *hdev = conn->hdev;
...
skb_queue_purge(&chan->data_q); // "data_q" comes from the freed
object "chan" therefore it can point to an arbitrary memory address
kfree(chan);
}
The skb was dequeued from the list, however the list is controllable by
an attacker and it can point to an arbitrary memory address.
void skb_queue_purge(struct sk_buff_head *list)
{
struct sk_buff *skb;
while ((skb = skb_dequeue(list)) != NULL) // skb is also controllable
kfree_skb(skb); // dangerous use of skb further down
}
After going through a long call chain:
skb_queue_purge->kfree_skb->__kfree_skb->skb_release_all->skb_release_data,
skb enters "skb_zcopy_clear".
static void skb_release_data(struct sk_buff *skb)
{
...
skb_zcopy_clear(skb, true); // skb entered skb_zcopy_clear() and
will dereference a function pointer inside.
skb_free_head(skb);
}
static inline void skb_zcopy_clear(struct sk_buff *skb, bool zerocopy)
{
struct ubuf_info *uarg = skb_zcopy(skb); // uarg comes from skb,
therefore it also controllable by attacker
if (uarg) {
if (skb_zcopy_is_nouarg(skb)) {
/* no notification callback */
} else if (uarg->callback == sock_zerocopy_callback) {
uarg->zerocopy = uarg->zerocopy && zerocopy;
sock_zerocopy_put(uarg); // uarg enters sock_zerocopy_put()
}
...
}
}
Inside the function below, uarg's function pointer will be dereferenced.
This makes a control flow hijacking possible because uarg is totally
controllable by attackers.
void sock_zerocopy_put(struct ubuf_info *uarg)
{
if (uarg && refcount_dec_and_test(&uarg->refcnt)) {
if (uarg->callback)
uarg->callback(uarg, uarg->zerocopy); // uarg dereferences
a function pointer, and thus we grant a control flow hijacking primitive
...
}
}
SyzScope Team.
On 8/2/2020 1:45 PM, syzbot wrote: