drivers/bluetooth/hci_ldisc.c | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-)
From: Mingyu Wang <25181214217@stu.xidian.edu.cn>
Vulnerabilities leading to Use-After-Free (UAF) and Null Pointer
Dereference (NPD) conditions were observed in the lifecycle management
of hci_uart.
The primary issue arises because the workqueues (init_ready and
write_work) are only flushed/cancelled if the HCI_UART_PROTO_READY
flag is set during TTY close. If a hangup occurs before setup completes,
hci_uart_tty_close() skips the teardown of these workqueues and
proceeds to free the `hu` struct. When the scheduled work executes
later, it blindly dereferences the freed `hu` struct.
Furthermore, several data races and UAFs were identified in the teardown
sequence:
1. Calling hci_uart_flush() from hci_uart_close() without canceling
write_work causes a race condition where both can concurrently
double-free hu->tx_skb. This occurs both in TTY hangup and when the
HCI device is closed via the HCI core.
2. Calling hci_free_dev(hdev) before hu->proto->close(hu) causes a UAF
when vendor specific protocol close callbacks dereference hu->hdev.
3. In the initialization error paths, failing to take the proto_lock
write lock before clearing PROTO_READY leads to races with active
readers. Additionally, hci_uart_tty_receive() accesses hu->hdev
outside the read lock, leading to UAFs if the initialization error
path frees hdev concurrently.
Fix these synchronization and lifecycle issues by:
1. Re-ordering hci_uart_tty_close() to unconditionally cancel init_ready
and write_work first. This prevents the double-free race in
hci_uart_flush(), while preserving the HCI_UART_PROTO_READY flag so
underlying hu->proto->flush() callbacks can still execute safely.
2. Relocating hu->proto->close(hu) strictly prior to hci_free_dev(hdev)
across all close and error paths to prevent vendor-level UAFs.
3. Moving the hdev->stat.byte_rx increment in hci_uart_tty_receive()
inside the proto_lock read-side critical section to safely synchronize
with device unregistration.
4. Adding cancel_work_sync(&hu->write_work) to hci_uart_close() to safely
flush the workqueue before hci_uart_flush() is invoked.
5. Utilizing cancel_work_sync() instead of disable_work_sync() after
flags are cleared to safely flush workqueues without permanently
breaking user-space retry capabilities.
Fixes: 3b799254cf6f ("Bluetooth: hci_uart: Cancel init work before unregistering")
Cc: stable@vger.kernel.org
Signed-off-by: Mingyu Wang <25181214217@stu.xidian.edu.cn>
---
Changes in v8:
- Corrected the teardown sequence in hci_uart_tty_close() by unconditionally canceling write_work BEFORE hci_uart_close(). This completely prevents the tx_skb double-free without prematurely clearing PROTO_READY, ensuring underlying hu->proto->flush(hu) runs correctly.
- Moved hu->hdev->stat.byte_rx increment inside the proto_lock read-side critical section in hci_uart_tty_receive() to prevent read-side UAF against concurrent registration failures.
- Added cancel_work_sync(&hu->write_work) inside hci_uart_close() to eliminate the race condition between write_work and hci_uart_flush() when the interface is brought down via the HCI core.
Changes in v7:
- Reverted disable_work_sync() back to cancel_work_sync() across all error and close paths to preserve user-space retry capabilities.
- Synchronized workqueue teardown safely by atomically clearing PROTO_READY / PROTO_INIT under proto_lock prior to calling cancel_work_sync().
- Fixed a Use-After-Free (UAF) vulnerability in the teardown sequence by relocating hu->proto->close(hu) strictly prior to hci_free_dev(hdev).
- Added cancel_work_sync(&hu->init_ready) at the very beginning of hci_uart_tty_close() to serialize teardown against active asynchronous registration.
Changes in v6:
- Fixed missing `hu->proto_lock` write lock in hci_uart_init_work() error path to prevent race with readers (reported by Sashiko).
- Added disable_work_sync() instead of cancel_work_sync() for `hu->write_work` in hci_uart_init_work() and hci_uart_register_dev() error paths.
Changes in v5:
- Relocated disable_work_sync() to the very top of hci_uart_tty_close(),
before hci_uart_close(), to ensure no new work is submitted during device teardown.
Changes in v4:
- Adopted Luiz's suggestion to use disable_work_sync() instead of
cancel_work_sync() in close path to prevent new work submissions.
Changes in v3:
- Added 'Cc: stable' tag as requested by the stable bot.
Changes in v2:
- Added KASAN/ODEBUG crash trace.
drivers/bluetooth/hci_ldisc.c | 33 ++++++++++++++++++++++++++++-----
1 file changed, 28 insertions(+), 5 deletions(-)
diff --git a/drivers/bluetooth/hci_ldisc.c b/drivers/bluetooth/hci_ldisc.c
index 275ea865bc29..cb56194daad1 100644
--- a/drivers/bluetooth/hci_ldisc.c
+++ b/drivers/bluetooth/hci_ldisc.c
@@ -194,7 +194,15 @@ void hci_uart_init_work(struct work_struct *work)
err = hci_register_dev(hu->hdev);
if (err < 0) {
BT_ERR("Can't register HCI device");
+
+ percpu_down_write(&hu->proto_lock);
clear_bit(HCI_UART_PROTO_READY, &hu->flags);
+ percpu_up_write(&hu->proto_lock);
+
+ /* Safely cancel work after clearing flags */
+ cancel_work_sync(&hu->write_work);
+
+ /* Close protocol before freeing hdev */
hu->proto->close(hu);
hdev = hu->hdev;
hu->hdev = NULL;
@@ -263,8 +271,12 @@ static int hci_uart_open(struct hci_dev *hdev)
/* Close device */
static int hci_uart_close(struct hci_dev *hdev)
{
+ struct hci_uart *hu = hci_get_drvdata(hdev);
+
BT_DBG("hdev %p", hdev);
+ cancel_work_sync(&hu->write_work);
+
hci_uart_flush(hdev);
hdev->flush = NULL;
return 0;
@@ -540,6 +552,12 @@ static void hci_uart_tty_close(struct tty_struct *tty)
if (!hu)
return;
+ /* Wait for init_ready to finish to prevent registration races */
+ cancel_work_sync(&hu->init_ready);
+
+ /* Unconditionally cancel write_work BEFORE hci_uart_close() to prevent double-free */
+ cancel_work_sync(&hu->write_work);
+
hdev = hu->hdev;
if (hdev)
hci_uart_close(hdev);
@@ -549,15 +567,15 @@ static void hci_uart_tty_close(struct tty_struct *tty)
clear_bit(HCI_UART_PROTO_READY, &hu->flags);
percpu_up_write(&hu->proto_lock);
- cancel_work_sync(&hu->init_ready);
- cancel_work_sync(&hu->write_work);
-
if (hdev) {
if (test_bit(HCI_UART_REGISTERED, &hu->flags))
hci_unregister_dev(hdev);
- hci_free_dev(hdev);
}
+ /* Close protocol before freeing hdev */
hu->proto->close(hu);
+
+ if (hdev)
+ hci_free_dev(hdev);
}
clear_bit(HCI_UART_PROTO_SET, &hu->flags);
@@ -625,11 +643,12 @@ static void hci_uart_tty_receive(struct tty_struct *tty, const u8 *data,
* tty caller
*/
hu->proto->recv(hu, data, count);
- percpu_up_read(&hu->proto_lock);
if (hu->hdev)
hu->hdev->stat.byte_rx += count;
+ percpu_up_read(&hu->proto_lock);
+
tty_unthrottle(tty);
}
@@ -695,6 +714,10 @@ static int hci_uart_register_dev(struct hci_uart *hu)
percpu_down_write(&hu->proto_lock);
clear_bit(HCI_UART_PROTO_INIT, &hu->flags);
percpu_up_write(&hu->proto_lock);
+ /* Cancel work after clearing flags */
+ cancel_work_sync(&hu->write_work);
+
+ /* Close protocol before freeing hdev */
hu->proto->close(hu);
hu->hdev = NULL;
hci_free_dev(hdev);
--
2.34.1
© 2016 - 2026 Red Hat, Inc.