HCI ACL Buffer Accounting: Recover Credits Without Reusing Live Packets
Count HCI ACL packets against the controller pool that actually owns them, then return capacity from per-handle completion deltas or disconnect reclamation exactly once. Do not count application messages, radio packets or L2CAP credits as though they were the same resource.
Applicability: Bluetooth Core 6.1 packet-based host-to-controller ACL flow control, with Linux v6.12 implementation examples. ISO and synchronous-data accounting are excluded from the worked ledger. Capacities and fragmentation below are hypothetical, and no controller or throughput test was run.

1. Discover the correct pool
The Core buffer-size procedure supplies the maximum HCI ACL data length and available packet count. Preserve both: one bounds each packet’s data portion, while the other bounds how many packets can occupy the pool.
A dual-mode controller can report no dedicated LE ACL pool. In that case, LE uses the pool discovered through HCI Read Buffer Size for shared BR/EDR and LE traffic. Do not interpret zero dedicated LE capacity as permission for unlimited LE sends, or allocate an independent software pool on top of the shared one.
Assign every connection to an explicit pool record. If the product uses both transports, shared capacity must include both outstanding totals. The simple example here has only LE connections, so its sum is easy to inspect.
Record controller firmware, buffer-size responses and host release with the ledger. Re-read and rebuild the model through the supported initialization path after controller recovery; stale capacity from an earlier instance is not a valid starting point.
2. Count the packet boundary being controlled
Suppose a controlled fixture advertises a maximum HCI ACL data portion of 27 octets. A complete 64-octet L2CAP PDU, including its L2CAP header, requires three HCI ACL packets if fragmented as 27, 27 and 10 octets.
Those three sends consume three packet slots. They do not consume one slot because the application supplied one message, and they do not consume 64 slots because the message contains 64 octets. The HCI packet header is outside the stated data-length limit.
Keep the HCI fragmentation record separate from eventual Link Layer packetization. Different controller data-length settings can change on-air behavior without changing what the host already submitted as HCI packets.
For each committed send, atomically decrement the selected pool’s free count and increment that connection’s outstanding count. Use the actual transport submission boundary. Local queue ownership and uncertain transport failure need explicit handling rather than speculative credit return.
3. Apply completion counts as deltas
HCI Number Of Completed Packets reports how many packets have completed for each listed handle since the previous report. Add that delta to the owning pool’s free capacity and subtract it from that handle’s outstanding count.
This differs from Num_HCI_Command_Packets in command flow control, which reports the current command allowance. Give the two event paths distinct types or names so a shared “credit update” helper cannot apply the wrong arithmetic.
Completion means the controller buffer is free for reuse. It does not prove that the remote application consumed or durably stored the message. Preserve application acknowledgement separately when the product needs that assurance.
For a proposed strict diagnostic ledger, reject a completion count larger than the handle’s outstanding total and flag a protocol/accounting fault. Do not silently repair the record by granting more free slots than the pool contains.
4. Walk the disconnect boundary
Take a hypothetical six-packet pool. Connection A has three outstanding HCI packets and B has two, leaving one free. A completion report returns two for A: free becomes three, A outstanding becomes one and B remains two.
When A’s Disconnection Complete arrives, Core requires the host to treat its remaining unacknowledged packets as flushed and their buffers freed. Reclaim A’s remaining one slot, not its original three. The pool now has four free slots and B still owns two.
When B later completes two packets, free returns to six. At every step, free plus outstanding packets equals capacity. Record the exact ordering so an earlier completion cannot also be included in disconnect reclamation.
The controller must not report a handle in Number Of Completed Packets after that connection’s disconnection event. A deliberately injected late report is therefore a fault case, not a normal race to compensate by adding credit again.
Host-side deferred work can still outlive a connection. Associate it with a generation and retire it on disconnect. Numeric handle reuse must not allow a queued old callback to reduce a new connection’s outstanding count.
5. Compare the model with the shipping implementation
The Linux v6.12 LE scheduler selects le_cnt when a dedicated LE pool exists, otherwise acl_cnt. It decrements the selected count and increments connection sent accounting as packets are dispatched.
The matching completion handler looks up each handle, subtracts the reported count from conn->sent and returns it to the corresponding pool. It ignores handles with no connection and clamps pool counters to their configured maxima.
Those observations do not mean the kernel implements the proposed strict validation above. In particular, the inspected handler subtracts the count before its pool clamp; the clamp alone is not evidence that every per-connection accounting inconsistency was detected. Use the exact source when interpreting diagnostics.
Do not modify host counters from an application to “unstick” a stalled stream. Diagnose missing completion events, incorrect pool selection, transport loss and lifecycle transitions through the stack’s supported facilities.
6. Qualify invariants and recovery
| Proposed fixture | Required observation |
|---|---|
| Zero free slots | No additional ACL packet dispatched |
| One completion for a multi-fragment PDU | One slot returned, regardless of message completion |
| Batched multi-handle event | Each delta applied to the right connection and pool |
| Completion then disconnect | Only the remaining outstanding count reclaimed |
| Duplicate or excessive completion | Diagnostic fault, no invented capacity |
| Shared BR/EDR and LE workload | One shared free-capacity budget respected |
Include malformed event lengths and handle reuse in a replay harness. Verify that incomplete event data cannot partly mutate the ledger. Treat controller reset as a new pool generation and resolve the transport boundary before accepting new accounting events.
Log capacity, free slots, per-handle outstanding counts, completion deltas and reclamation reason. Correlate these with host queue age, but do not call a recovered buffer an end-to-end delivery acknowledgement.
Acceptance is a conserved resource ledger under normal progress and teardown. That makes a stall explainable: the host is either waiting for real controller capacity, holding local work, or facing an identified accounting fault.