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
Description / Steps to reproduce the issue
Bug report
Description
The HCI RX worker releases every dequeued buffer unconditionally after dispatch:
but the same buffer is also released downstream for ACL data:
bt_conn_receive()→bt_l2cap_receive()→bt_att_receive()(bt_att.c) orbt_smp_receive()(bt_smp.c), which both end withbt_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).bt_conn_receive()stores the first fragment inconn->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
conn->rx.Regression origin
The "worker releases at end of loop" model was introduced by PR #2571 (
c6947199b21), while downstream releases inbt_conn_receive/bt_att_receive/bt_smp_receivewere kept. PR #9082 (dd5abe86914c) removed the error-path releases inhci_acl/hci_eventbut 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:
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_handlerwhich 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->rxtakes 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