Re: [PATCH] Bluetooth: btusb: Gate ACL delivery on HCI handle presence

Luiz Augusto von Dentz <[email protected]> Mon, 27 Jul 2026 16:34:06 -0400
Newsgroups org.kernel.vger.linux-bluetooth
Message-ID <CABBYNZJk8sgQKjLKet1pp7fXD0uCo1O33WQ8F4LxN23A2S_r3g@mail.gmail.com>
Hi Amir,

On Tue, Jul 21, 2026 at 9:23 PM Amir Abudubai <[email protected]> wrote:
>
> On some USB devices, the interrupt-IN endpoint can lag the bulk-IN
> endpoint by multiple polling intervals. Early ACL data can then reach
> HCI before the Connection Complete event establishes its handle, causing
> the data to be dropped for an unknown handle.
>
> This issue was observed and the fix verified on the following adapters:
>   * 8087:0025 Intel Corp. Wireless-AC 9260 Bluetooth Adapter
>   * 7392:c611 Edimax Technology Co., Ltd Edimax Bluetooth Adapter
>
> Extend the existing poll_sync queue to support independent release gates
> while preserving its underlying behavior. Add an optional gate which
> holds ACL delivery until the HCI handle appears or a bounded timeout
> expires. Anchor both time gates to packet arrival to prevent timeout
> drift from accumulating as queued packets reach the FIFO head.
>
> BT_HCIBTUSB_EARLY_ACL_HOLD controls whether the gate is enabled by
> default. The force_early_acl_hold debugfs entry controls it per adapter
> while the adapter is down.
>
> Assisted-by: OpenCode:openai/gpt-5.6-sol
> Signed-off-by: Amir Abudubai <[email protected]>

This really sounds like an over-engineered design; I'm not sure why it
couldn't be done by just increasing the number of polls we wait before
delivering the ACL packet, it has been quite a long while we don't
receive any reports of such races still exists. Also, some vendors use
special handles for debugging, these never have any completing event
or anything.

> ---
>  drivers/bluetooth/Kconfig |  10 ++
>  drivers/bluetooth/btusb.c | 292 +++++++++++++++++++++++++++++++++-----
>  2 files changed, 266 insertions(+), 36 deletions(-)
>
> diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig
> index 4e8c24d757e9..648c0d8b1496 100644
> --- a/drivers/bluetooth/Kconfig
> +++ b/drivers/bluetooth/Kconfig
> @@ -56,6 +56,16 @@ config BT_HCIBTUSB_POLL_SYNC
>           Say Y here to enable USB poll_sync for Bluetooth USB devices by
>           default.
>
> +config BT_HCIBTUSB_EARLY_ACL_HOLD
> +       bool "Enable early ACL hold for Bluetooth USB devices by default"
> +       depends on BT_HCIBTUSB
> +       help
> +         Hold ACL packets whose connection handle is not yet known until the
> +         connection appears or a bounded timeout expires.
> +
> +         Say Y here to enable early ACL hold for Bluetooth USB devices by
> +         default.
> +
>  config BT_HCIBTUSB_BCM
>         bool "Broadcom protocol support"
>         depends on BT_HCIBTUSB
> diff --git a/drivers/bluetooth/btusb.c b/drivers/bluetooth/btusb.c
> index 4d59b28ab30f..a59b0ed91d18 100644
> --- a/drivers/bluetooth/btusb.c
> +++ b/drivers/bluetooth/btusb.c
> @@ -34,6 +34,8 @@ static bool disable_scofix;
>  static bool force_scofix;
>  static bool enable_autosuspend = IS_ENABLED(CONFIG_BT_HCIBTUSB_AUTOSUSPEND);
>  static bool enable_poll_sync = IS_ENABLED(CONFIG_BT_HCIBTUSB_POLL_SYNC);
> +static bool enable_early_acl_hold =
> +       IS_ENABLED(CONFIG_BT_HCIBTUSB_EARLY_ACL_HOLD);
>  static bool reset = true;
>
>  static struct usb_driver btusb_driver;
> @@ -956,6 +958,8 @@ struct btqca_data {
>  };
>
>  #define BTUSB_MAX_ISOC_FRAMES  10
> +#define BTUSB_RX_RECHECK_DELAY msecs_to_jiffies(1)
> +#define BTUSB_EARLY_ACL_HOLD_MIN_US    5000U
>
>  #define BTUSB_INTR_RUNNING     0
>  #define BTUSB_BULK_RUNNING     1
> @@ -987,7 +991,10 @@ struct btusb_data {
>         unsigned long flags;
>
>         bool poll_sync;
> +       bool early_acl_hold;
>         int intr_interval;
> +       unsigned long early_acl_timeout;
> +       ktime_t last_event;
>         struct work_struct  work;
>         struct work_struct  waker;
>         struct delayed_work rx_work;
> @@ -1252,14 +1259,100 @@ static inline void btusb_free_frags(struct btusb_data *data)
>         spin_unlock_irqrestore(&data->rxlock, flags);
>  }
>
> -static int btusb_recv_event(struct btusb_data *data, struct sk_buff *skb)
> +static void btusb_rx_queue_purge(struct btusb_data *data)
> +{
> +       unsigned long flags;
> +
> +       spin_lock_irqsave(&data->rxlock, flags);
> +       skb_queue_purge(&data->acl_q);
> +       spin_unlock_irqrestore(&data->rxlock, flags);
> +}
> +
> +static ktime_t btusb_rx_deadline(struct sk_buff *skb, unsigned long timeout)
>  {
> -       if (data->intr_interval) {
> -               /* Trigger dequeue immediately if an event is received */
> -               schedule_delayed_work(&data->rx_work, 0);
> +       return ktime_add_ns(skb->tstamp, jiffies_to_nsecs(timeout));
> +}
> +
> +static bool btusb_rx_hold_expired(struct sk_buff *skb, unsigned long timeout,
> +                                 ktime_t now)
> +{
> +       return !ktime_before(now, btusb_rx_deadline(skb, timeout));
> +}
> +
> +static unsigned long btusb_rx_hold_delay(struct sk_buff *skb,
> +                                        unsigned long timeout, ktime_t now)
> +{
> +       s64 remaining = ktime_to_ns(ktime_sub(btusb_rx_deadline(skb, timeout),
> +                                             now));
> +
> +       if (remaining <= 0)
> +               return 0;
> +
> +       return max_t(unsigned long, 1, nsecs_to_jiffies(remaining));
> +}
> +
> +static bool btusb_poll_sync_should_hold(struct btusb_data *data,
> +                                       struct sk_buff *skb, ktime_t now)
> +{
> +       unsigned long interval = READ_ONCE(data->intr_interval);
> +
> +       if (!interval || !ktime_before(data->last_event, skb->tstamp))
> +               return false;
> +
> +       return !btusb_rx_hold_expired(skb, interval, now);
> +}
> +
> +static bool btusb_acl_should_hold(struct btusb_data *data,
> +                                 struct sk_buff *skb, ktime_t now)
> +{
> +       unsigned long timeout = READ_ONCE(data->early_acl_timeout);
> +       u16 handle;
> +
> +       if (!timeout || hci_dev_test_flag(data->hdev, HCI_USER_CHANNEL) ||
> +           btusb_rx_hold_expired(skb, timeout, now) ||
> +           skb->len < HCI_ACL_HDR_SIZE)
> +               return false;
> +
> +       handle = le16_to_cpu(hci_acl_hdr(skb)->handle);
> +
> +       /* Only use the returned connection as an existence snapshot. */
> +       return !hci_conn_hash_lookup_handle(data->hdev, hci_handle(handle));
> +}
> +
> +static unsigned long btusb_rx_delay(struct btusb_data *data,
> +                                   struct sk_buff *skb, ktime_t now,
> +                                   bool poll_hold, bool acl_hold)
> +{
> +       unsigned long timeout;
> +
> +       if (poll_hold) {
> +               timeout = READ_ONCE(data->intr_interval);
> +               return btusb_rx_hold_delay(skb, timeout, now);
> +       }
> +
> +       if (acl_hold) {
> +               timeout = READ_ONCE(data->early_acl_timeout);
> +               return min(BTUSB_RX_RECHECK_DELAY,
> +                          btusb_rx_hold_delay(skb, timeout, now));
>         }
>
> -       return data->recv_event(data->hdev, skb);
> +       return 0;
> +}
> +
> +static int btusb_recv_event(struct btusb_data *data, struct sk_buff *skb)
> +{
> +       int err;
> +
> +       err = data->recv_event(data->hdev, skb);
> +
> +       if (READ_ONCE(data->intr_interval))
> +               data->last_event = ktime_get();
> +
> +       if (READ_ONCE(data->intr_interval) ||
> +           READ_ONCE(data->early_acl_timeout))
> +               mod_delayed_work(system_wq, &data->rx_work, 0);
> +
> +       return err;
>  }
>
>  static int btusb_recv_intr(struct btusb_data *data, void *buffer, int count)
> @@ -1332,14 +1425,31 @@ static int btusb_recv_intr(struct btusb_data *data, void *buffer, int count)
>
>  static int btusb_recv_acl(struct btusb_data *data, struct sk_buff *skb)
>  {
> -       /* Only queue ACL packet if intr_interval is set as it means
> -        * force_poll_sync has been enabled.
> -        */
> -       if (!data->intr_interval)
> +       bool poll_hold, acl_hold;
> +       bool queue_empty;
> +       ktime_t now;
> +
> +       if (!READ_ONCE(data->intr_interval) &&
> +           !READ_ONCE(data->early_acl_timeout))
> +               return data->recv_acl(data->hdev, skb);
> +
> +       now = ktime_get();
> +       /* hci_recv_frame() replaces this temporary monotonic arrival time. */
> +       skb_set_delivery_time(skb, now, SKB_CLOCK_MONOTONIC);
> +
> +       queue_empty = skb_queue_empty(&data->acl_q);
> +       poll_hold = btusb_poll_sync_should_hold(data, skb, now);
> +       acl_hold = btusb_acl_should_hold(data, skb, now);
> +
> +       if (queue_empty && !poll_hold && !acl_hold)
>                 return data->recv_acl(data->hdev, skb);
>
>         skb_queue_tail(&data->acl_q, skb);
> -       schedule_delayed_work(&data->rx_work, data->intr_interval);
> +
> +       if (queue_empty)
> +               schedule_delayed_work(&data->rx_work,
> +                                     btusb_rx_delay(data, skb, now,
> +                                                    poll_hold, acl_hold));
>
>         return 0;
>  }
> @@ -1539,6 +1649,10 @@ static int btusb_submit_intr_urb(struct hci_dev *hdev, gfp_t mem_flags)
>         struct btusb_data *data = hci_get_drvdata(hdev);
>         struct urb *urb;
>         unsigned char *buf;
> +       unsigned long early_acl_timeout = 0;
> +       int intr_interval = 0;
> +       unsigned int hold_us;
> +       unsigned int interval_us;
>         unsigned int pipe;
>         int err, size;
>
> @@ -1587,27 +1701,52 @@ static int btusb_submit_intr_urb(struct hci_dev *hdev, gfp_t mem_flags)
>         }
>
>         /* Only initialize intr_interval if URB poll sync is enabled */
> -       if (!data->poll_sync)
> -               goto done;
> +       if (data->poll_sync) {
> +               /* The units are frames (milliseconds) for full and low speed
> +                * devices, and microframes (1/8 millisecond) for highspeed and
> +                * SuperSpeed devices.
> +                *
> +                * This is done once on open/resume so it shouldn't change even
> +                * if force_poll_sync changes.
> +                */
> +               switch (urb->dev->speed) {
> +               case USB_SPEED_SUPER_PLUS:
> +               case USB_SPEED_SUPER:
> +                       intr_interval = usecs_to_jiffies(urb->interval * 125);
> +                       break;
> +               default:
> +                       intr_interval = msecs_to_jiffies(urb->interval);
> +                       break;
> +               }
> +       }
>
> -       /* The units are frames (milliseconds) for full and low speed devices,
> -        * and microframes (1/8 millisecond) for highspeed and SuperSpeed
> -        * devices.
> -        *
> -        * This is done once on open/resume so it shouldn't change even if
> -        * force_poll_sync changes.
> -        */
> -       switch (urb->dev->speed) {
> -       case USB_SPEED_SUPER_PLUS:
> -       case USB_SPEED_SUPER:   /* units are 125us */
> -               data->intr_interval = usecs_to_jiffies(urb->interval * 125);
> -               break;
> -       default:
> -               data->intr_interval = msecs_to_jiffies(urb->interval);
> -               break;
> +       interval_us = 0;
> +       if (data->early_acl_hold && urb->interval > 0) {
> +               switch (urb->dev->speed) {
> +               case USB_SPEED_SUPER_PLUS:
> +               case USB_SPEED_SUPER:
> +               case USB_SPEED_HIGH:
> +                       interval_us = urb->interval * 125;
> +                       break;
> +               case USB_SPEED_FULL:
> +                       interval_us = urb->interval * USEC_PER_MSEC;
> +                       break;
> +               default:
> +                       break;
> +               }
>         }
>
> -done:
> +       if (interval_us) {
> +               /* Allow an event which misses one polling window to arrive in
> +                * the next.
> +                */
> +               hold_us = max(BTUSB_EARLY_ACL_HOLD_MIN_US, interval_us * 2);
> +               early_acl_timeout = usecs_to_jiffies(hold_us);
> +       }
> +
> +       WRITE_ONCE(data->intr_interval, intr_interval);
> +       WRITE_ONCE(data->early_acl_timeout, early_acl_timeout);
> +
>         usb_free_urb(urb);
>
>         return err;
> @@ -2077,18 +2216,22 @@ static int btusb_close(struct hci_dev *hdev)
>
>         BT_DBG("%s", hdev->name);
>
> -       cancel_delayed_work(&data->rx_work);
> +       /* Stop queued RX delivery before shutting down the endpoints. */
> +       cancel_delayed_work_sync(&data->rx_work);
>         cancel_work_sync(&data->work);
>         cancel_work_sync(&data->waker);
>
> -       skb_queue_purge(&data->acl_q);
> -
>         clear_bit(BTUSB_ISOC_RUNNING, &data->flags);
>         clear_bit(BTUSB_BULK_RUNNING, &data->flags);
>         clear_bit(BTUSB_INTR_RUNNING, &data->flags);
>         clear_bit(BTUSB_DIAG_RUNNING, &data->flags);
>
>         btusb_stop_traffic(data);
> +
> +       /* An in-flight URB completion may have requeued RX work. */
> +       cancel_delayed_work_sync(&data->rx_work);
> +
> +       btusb_rx_queue_purge(data);
>         btusb_free_frags(data);
>
>         err = usb_autopm_get_interface(data->intf);
> @@ -2114,9 +2257,9 @@ static int btusb_flush(struct hci_dev *hdev)
>
>         BT_DBG("%s", hdev->name);
>
> -       cancel_delayed_work(&data->rx_work);
> +       cancel_delayed_work_sync(&data->rx_work);
>
> -       skb_queue_purge(&data->acl_q);
> +       btusb_rx_queue_purge(data);
>
>         usb_kill_anchored_urbs(&data->tx_anchor);
>         btusb_free_frags(data);
> @@ -2497,11 +2640,40 @@ static void btusb_rx_work(struct work_struct *work)
>  {
>         struct btusb_data *data = container_of(work, struct btusb_data,
>                                                rx_work.work);
> +       unsigned long delay;
> +       unsigned long flags;
>         struct sk_buff *skb;
> +       bool poll_hold, acl_hold;
> +       ktime_t now;
> +
> +       for (;;) {
> +               spin_lock_irqsave(&data->rxlock, flags);
> +               now = ktime_get();
> +               skb = skb_peek(&data->acl_q);
> +
> +               if (!skb) {
> +                       spin_unlock_irqrestore(&data->rxlock, flags);
> +                       return;
> +               }
> +
> +               poll_hold = btusb_poll_sync_should_hold(data, skb, now);
> +               acl_hold = btusb_acl_should_hold(data, skb, now);
> +               if (!poll_hold && !acl_hold) {
> +                       skb = skb_dequeue(&data->acl_q);
> +                       data->recv_acl(data->hdev, skb);
> +                       spin_unlock_irqrestore(&data->rxlock, flags);
> +                       continue;
> +               }
> +
> +               delay = btusb_rx_delay(data, skb, now, poll_hold, acl_hold);
> +               spin_unlock_irqrestore(&data->rxlock, flags);
>
> -       /* Dequeue ACL data received during the interval */
> -       while ((skb = skb_dequeue(&data->acl_q)))
> -               data->recv_acl(data->hdev, skb);
> +               /* A zero-delay event kick that raced with this worker remains
> +                * pending; schedule_delayed_work() cannot move it later.
> +                */
> +               schedule_delayed_work(&data->rx_work, delay);
> +               return;
> +       }
>  }
>
>  static int btusb_setup_bcm92035(struct hci_dev *hdev)
> @@ -3967,6 +4139,51 @@ static const struct file_operations force_poll_sync_fops = {
>         .llseek         = default_llseek,
>  };
>
> +static ssize_t force_early_acl_hold_read(struct file *file,
> +                                        char __user *user_buf, size_t count,
> +                                        loff_t *ppos)
> +{
> +       struct btusb_data *data = file->private_data;
> +       char buf[3];
> +
> +       buf[0] = data->early_acl_hold ? 'Y' : 'N';
> +       buf[1] = '\n';
> +       buf[2] = '\0';
> +       return simple_read_from_buffer(user_buf, count, ppos, buf, 2);
> +}
> +
> +static ssize_t force_early_acl_hold_write(struct file *file,
> +                                         const char __user *user_buf,
> +                                         size_t count, loff_t *ppos)
> +{
> +       struct btusb_data *data = file->private_data;
> +       bool enable;
> +       int err;
> +
> +       err = kstrtobool_from_user(user_buf, count, &enable);
> +       if (err)
> +               return err;
> +
> +       /* Only allow changes while the adapter is down */
> +       if (test_bit(HCI_UP, &data->hdev->flags))
> +               return -EPERM;
> +
> +       if (data->early_acl_hold == enable)
> +               return -EALREADY;
> +
> +       data->early_acl_hold = enable;
> +
> +       return count;
> +}
> +
> +static const struct file_operations force_early_acl_hold_fops = {
> +       .owner          = THIS_MODULE,
> +       .open           = simple_open,
> +       .read           = force_early_acl_hold_read,
> +       .write          = force_early_acl_hold_write,
> +       .llseek         = default_llseek,
> +};
> +
>  #define BTUSB_HCI_DRV_OP_SUPPORTED_ALTSETTINGS \
>                 hci_opcode_pack(HCI_DRV_OGF_DRIVER_SPECIFIC, 0x0000)
>  #define BTUSB_HCI_DRV_SUPPORTED_ALTSETTINGS_SIZE       0
> @@ -4464,6 +4681,7 @@ static int btusb_probe(struct usb_interface *intf,
>                 usb_enable_autosuspend(data->udev);
>
>         data->poll_sync = enable_poll_sync;
> +       data->early_acl_hold = enable_early_acl_hold;
>
>         err = hci_register_dev(hdev);
>         if (err < 0)
> @@ -4473,6 +4691,8 @@ static int btusb_probe(struct usb_interface *intf,
>
>         debugfs_create_file("force_poll_sync", 0644, hdev->debugfs, data,
>                             &force_poll_sync_fops);
> +       debugfs_create_file("force_early_acl_hold", 0644, hdev->debugfs, data,
> +                           &force_early_acl_hold_fops);
>
>         return 0;
>
>
> base-commit: 6f55ad8fb0acd861e5c4e526b2272fc71952e1e3
> --
> 2.43.0
>


-- 
Luiz Augusto von Dentz