--- a/sys/netbt/hci.h 2026-10-11 00:20:51.387137328 +0000 +++ b/sys/netbt/hci.h 2026-10-11 00:21:02.026999604 +0000 @@ -2403,6 +2403,9 @@ uint16_t hl_clock; /* remote clock offset */ TAILQ_HEAD(,l2cap_pdu) hl_txq; /* queue of outgoing PDUs */ + unsigned int hl_teardown; /* exactly-once free guard + * (atomic test-and-set; + * see hci_link_free) */ int hl_txqlen; /* number of fragments */ struct mbuf *hl_rxp; /* incoming PDU (accumulating)*/ struct callout hl_expire; /* connection expiry timer */ --- a/sys/netbt/hci_link.c 2026-10-11 00:20:51.387137328 +0000 +++ b/sys/netbt/hci_link.c 2026-10-11 00:22:08.890134116 +0000 @@ -203,6 +203,13 @@ hci_discon_cp cp; int err; + /* + * Exactly-once teardown: if any other context is already + * freeing this link, its hci_link_free() will finish the job. + */ + if (link->hl_teardown) + return; + crit_enter(); if (link->hl_refcnt > 0) @@ -936,12 +943,42 @@ KKASSERT(link != NULL); + /* + * Exactly-once teardown guard (DF-5739): hci_acl_timeout() in + * the softclock thread and event handlers in the netisr thread + * can reach this function on the same link concurrently. The + * old `callout_stop(); if (callout_active()) return;` deferral + * never triggers on DragonFly callouts because callout_stop() + * clears CALLOUT_ACTIVE unconditionally. The loser of this + * test-and-set returns without touching anything else. + */ + if (atomic_testandset_int(&link->hl_teardown, 0)) + return; + DPRINTF("(%s) #%d, type = %d, state = %d, refcnt = %d\n", device_get_nameunit(link->hl_unit->hci_dev), link->hl_handle, link->hl_type, link->hl_state, link->hl_refcnt); /* ACL reference count */ if (link->hl_refcnt > 0) { + /* + * DF-5738: SCO links hold an anchor reference via + * hl_link (hci_acl_open() from hci_sco_newconn() or + * sco_connect()). Release them first so the refcount + * reaches zero -- hci_cmd_reset() does the same dance. + */ + if (link->hl_type == HCI_LINK_ACL) { + struct hci_link *sco, *snext; + + sco = TAILQ_FIRST(&link->hl_unit->hci_links); + while (sco != NULL) { + snext = TAILQ_NEXT(sco, hl_next); + if (sco->hl_type == HCI_LINK_SCO && + sco->hl_link == link) + hci_link_free(sco, err); + sco = snext; + } + } next = LIST_FIRST(&l2cap_active_list); while ((chan = next) != NULL) { next = LIST_NEXT(chan, lc_ncid); @@ -1002,9 +1039,15 @@ * any case. */ link->hl_state = HCI_LINK_CLOSED; - callout_stop(&link->hl_expire); - if (callout_active(&link->hl_expire)) - return; + /* + * callout_drain() blocks until a running callback has finished + * (recursion from within the callback itself is handled by the + * callout code); callout_terminate() detaches the internal + * _callout so that kfree()ing the embedded callout leaves no + * dangling state behind. + */ + callout_drain(&link->hl_expire); + callout_terminate(&link->hl_expire); /* * If we made a note of clock offset, keep it in a memo