Skip to content

[BUG] <wireless/bluetooth: ACL RX buffer double-release and dangling conn->rx for fragmented L2CAP PDUs> #19835

Description

@kanodayo1111

Description / Steps to reproduce the issue

Bug report

Description

The HCI RX worker releases every dequeued buffer unconditionally after dispatch:

/* bt_hcicore.c, hci_rx_work() */
switch (buf->type)
  {
    case BT_ACL_IN:
      hci_acl(buf);
      break;
    ...
  }
bt_buf_release(buf);   /* unconditional */

but the same buffer is also released downstream for ACL data:

  • Complete L2CAP PDU: bt_conn_receive()bt_l2cap_receive()bt_att_receive() (bt_att.c) or bt_smp_receive() (bt_smp.c), which both end with bt_buf_release(buf). Result: double release of the same buffer (refcount underflow; DEBUGASSERT(buf->ref > 0) fires in debug builds, silent corruption / buffer-pool exhaustion in release builds).
  • Fragmented L2CAP PDU: bt_conn_receive() stores the first fragment in conn->rx (bt_conn.c: conn->rx = buf;) without taking a reference, then the worker's unconditional release frees it. When the continuation fragment arrives, memcpy(bt_buf_extend(conn->rx, ...)) writes into freed memory (use-after-free).

Impact

  • Present with any standard HCI controller; triggered by any inbound ACL traffic.
  • Debug builds assert/crash on the first complete inbound PDU; release builds behave "mostly fine" on single-core serialized RX (the two releases happen back-to-back) and fail randomly once the freed buffer gets reallocated in between — exactly the "first connection works, later ones corrupt" pattern.
  • Fragmented PDUs deterministically corrupt memory via the dangling conn->rx.

Regression origin

The "worker releases at end of loop" model was introduced by PR #2571 (c6947199b21), while downstream releases in bt_conn_receive/bt_att_receive/bt_smp_receive were kept. PR #9082 (dd5abe86914c) removed the error-path releases in hci_acl/hci_event but left the successful ACL path double-release in place.

Proposed fix

Before handing the buffer to the connection layer, take an extra reference for the downstream owner:

  conn = bt_conn_lookup_handle(buf->u.acl.handle);
  if (!conn)
    {
      return;
    }

  bt_buf_addref(buf);
  bt_conn_receive(conn, buf, flags);
  bt_conn_release(conn);

so that the worker's loop-end release and the downstream release each own one reference. (Alternative: remove the worker's loop-end release and require each handler to own/release, matching Zephyr's rx_work_handler which does not release after dispatch.)

Reference

Zephyr main rx_work_handler() does not release the buffer after dispatch; ownership transfers unidirectionally (error paths release, conn->rx takes ownership of the first fragment, L2CAP/ATT release the completed PDU).

On which OS does this issue occur?

[OS: Linux]

What is the version of your OS?

Ubuntu 22.04.5 LTS

NuttX Version

master

Issue Architecture

[Arch: all]

Issue Area

[Area: Other]

Host information

No response

Verification

  • I have verified before submitting the report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Arch: allIssues that apply to all architecturesOS: LinuxIssues related to Linux (building system, etc)Type: BugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions