nimble/transport: Fix double free in hci_h4_sm_completed()#2266
Open
94xhn wants to merge 1 commit into
Open
Conversation
hci_h4_sm_completed() (nimble/transport/common/hci_h4/src/hci_h4.c)
is the shared HCI-H4 framing state machine used by several
transports (apollo3, dialog_cmac host role, uart_ll, cdc). Once an
ACL/ISO frame is fully received it does:
rc = h4sm->frame_cb(h4sm->pkt_type, h4sm->om);
if (rc != 0) {
os_mbuf_free_chain(h4sm->om);
}
h4sm->om = NULL;
For the LL->HS direction (apollo3_ble_hci_frame_cb,
hci_cmac_hs_frame_cb, uart_ll's hci_uart_frame_cb) frame_cb()
forwards ACL data to ble_transport_to_hs_acl(), which reaches
ble_hs_rx_data() in nimble/host/src/ble_hs.c. That function is
documented to "consume the supplied mbuf, regardless of the
outcome": on ble_mqueue_put() failure it already frees om via
os_mbuf_free_chain() before returning a non-zero status.
hci_h4_sm_completed() then frees the same h4sm->om a second time,
corrupting the mbuf pool. This is the same double-free pattern
already fixed for ble_hci_emspi_rx_acl() in PR apache#2265 / issue apache#2260,
but here it lives in the shared state machine and is reachable from
apollo3, dialog_cmac (host role) and uart_ll instead of a single
transport.
The other frame_cb() implementations reached from this same switch
case (ble_ll_hci_acl_rx()/ble_ll_hci_iso_rx() in the controller, for
the HS->LL direction used by uart/cdc/dialog_cmac controller role)
always return 0 and always take ownership of om themselves, so
removing the conditional free does not change behavior for them.
The HCI_H4_CMD/HCI_H4_EVT branch right above is intentionally left
untouched: ble_ll_hci_cmd_rx() can legitimately return non-zero
*without* freeing the buffer (the "command busy" path), so that
branch's cleanup-on-error is correct as-is and follows a different
ownership contract than the ACL/ISO one.
Verified with a standalone host-side program that reproduces the
real struct layouts and control flow of hci_h4_sm_completed(),
ble_hs_rx_data() and ble_mqueue_put() against a real malloc()/
free()-backed mbuf: the current code frees the same pointer twice
and the process aborts with STATUS_HEAP_CORRUPTION (0xC0000374);
with this change applied only a single free() occurs and the
process exits cleanly (code 0).
Disclosure: this fix, its analysis and the accompanying
reproduction program were produced with the assistance of an AI
coding agent (Claude), reviewed and submitted by the human author.
Signed-off-by: 94xhn <87560781+94xhn@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
hci_h4_sm_completed()innimble/transport/common/hci_h4/src/hci_h4.cdouble-frees the received
os_mbufon the ACL/ISO path whenever thetransport's
frame_cb()forwards the packet to the host stack and thehost fails to accept it.
Where the bug is
hci_h4_sm_completed()is the shared HCI-H4 framing state machine usedby several transports:
apollo3,dialog_cmac(host role),uart_ll,and
cdc. For the LL->HS direction,frame_cb()is e.g.apollo3_ble_hci_frame_cb()/hci_cmac_hs_frame_cb()/uart_ll'shci_uart_frame_cb(), all of which do forHCI_H4_ACL:which reaches
ble_transport_to_hs_acl_impl()->ble_hs_rx_data()innimble/host/src/ble_hs.c. That function's own doc comment says:and its body is:
ble_mqueue_put()(nimble/host/src/ble_hs_mqueue.c) fails whenever thembuf is not a proper packet-header mbuf:
So on that failure branch
omhas already been freed by the timeble_transport_to_hs_acl()returns non-zero tohci_h4_sm_completed(),which then frees the same pointer again via
os_mbuf_free_chain(h4sm->om).This is the same double-free pattern already reported/fixed in
#2260 / #2265 for
ble_hci_emspi_rx_acl(), but living in the sharedhci_h4.cstate machine instead, and reachable from three differenttransports (
apollo3,dialog_cmachost role,uart_ll) rather than asingle one. It is a distinct bug location/fix from #2265 (different
file, different function, no overlap in the diff), so this is filed as
a separate PR rather than folded into #2265.
Why the HS->LL branch is unaffected, and why CMD/EVT is untouched
For the reverse (HS->LL) direction used by
uart,cdc, anddialog_cmac's controller role,frame_cb()for ACL forwards toble_transport_to_ll_acl()->ble_ll_hci_acl_rx()(
nimble/controller/src/ble_ll_hci.c), which always returns0andalways takes ownership of
omitself (either handing it toble_ll_acl_data_in()or freeing it directly). So removing theconditional free is safe for every currently-existing
frame_cb()implementation reached through this switch case, not just the buggy
ones.
The
HCI_H4_CMD/HCI_H4_EVTbranch directly above is intentionallyleft untouched, because it follows a genuinely different, correct
ownership contract:
ble_ll_hci_cmd_rx()has a legitimate path where itreturns non-zero without taking ownership of the buffer (the "command
busy" case,
if (ble_npl_event_is_queued(ev)) return BLE_ERR_MEM_CAPACITY;),so that branch's "free on non-zero return" cleanup is correct as-is.
The fix
Drop the conditional free for the
HCI_H4_ACL/HCI_H4_ISOcase andjust let
frame_cb()'s return value be ignored (ownership was alreadytransferred, regardless of outcome), mirroring the same "ownership
transferred, do not free again" fix already applied to
ble_hci_emspi_rx_acl()in #2265.Verification
Wrote a standalone host-side C program
(not included in the PR diff) that re-creates the real struct layouts
(
struct os_mbuf/struct os_mbuf_pkthdr,OS_MBUF_IS_PKTHDR) and theexact control flow of
hci_h4_sm_completed()(ACL branch),ble_hs_rx_data(), andble_mqueue_put(), using the host's realmalloc()/free():omwithom_pkthdr_len = 0(not a pkthdr mbuf), the exactcondition
ble_mqueue_put()checks and rejects.ble_mqueue_put()fails ->ble_hs_rx_data()frees
omand returns non-zero ->hci_h4_sm_completed()frees thesame
omagain. Twofree()calls on the same pointer; processaborts with
STATUS_HEAP_CORRUPTION (0xC0000374).free()call, process exits cleanly(exit code 0).
Disclosure
This fix, its root-cause analysis, and the accompanying reproduction
program were produced with the assistance of an AI coding agent
(Claude). The analysis was independently verified against the sources
in this repository and the change was reviewed and submitted by the
human author.