DragonFlyBSD Kernel Audit
DF-0724 / fix.diff
← back to finding ↓ download raw
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);