diff --git a/sys/netbt/rfcomm_dlc.c b/sys/netbt/rfcomm_dlc.c --- a/sys/netbt/rfcomm_dlc.c +++ b/sys/netbt/rfcomm_dlc.c @@ -145,15 +145,35 @@ /* * rfcomm_dlc_close(dlc, error) * - * detach DLC from session and clean up + * detach DLC from session and clean up. + * + * The DLC timeout handler (rfcomm_dlc_timeout) and any of the + * network-input close paths (rfcomm_session.c recv_disc/recv_dm/ + * recv_ua) can invoke rfcomm_dlc_close concurrently on different + * CPUs. The previous KKASSERT(rd_state != CLOSED) could not catch + * the race because both callers read a non-CLOSED state before + * either writes CLOSED, and callout_stop (sync) blocks the loser + * until the winner completes the full close — leading to a double + * LIST_REMOVE, double disconnected callback, and double session- + * expiry scheduling. Atomically claim the CLOSED transition so + * exactly one caller performs the teardown; the loser returns + * harmlessly. */ void rfcomm_dlc_close(struct rfcomm_dlc *dlc, int err) { struct rfcomm_session *rs; struct rfcomm_credit *credit; + uint16_t old_state; + + do { + old_state = dlc->rd_state; + if (old_state == RFCOMM_DLC_CLOSED) + return; /* another caller is closing */ + } while (!atomic_cmpset_short(&dlc->rd_state, old_state, + RFCOMM_DLC_CLOSED)); - KKASSERT(dlc->rd_state != RFCOMM_DLC_CLOSED); + KKASSERT(old_state != RFCOMM_DLC_CLOSED); /* Clear credit history */ rs = dlc->rd_session; @@ -165,7 +185,6 @@ LIST_REMOVE(dlc, rd_next); dlc->rd_session = NULL; - dlc->rd_state = RFCOMM_DLC_CLOSED; (*dlc->rd_proto->disconnected)(dlc->rd_upper, err);