DF-3638 / verify_df3638.py
#!/usr/bin/env python3 """DF-3638 source-level verification. Asserts the defect in ath_tx_retry_clone() (if_ath_tx.c:4539-4593): on ath_tx_dmasetup() failure the cloned buffer nbf โ which OWNS the moved bf_m (freed inside dmasetup) and bf_node reference (never decref'd) โ is returned to the free list via ath_returnbuf_head(), and _ath_getbuf_locked() (if_ath.c) does not sanitize bf_m/bf_node on reuse. """ import sys, re TX = "sys/dev/netif/ath/ath/if_ath_tx.c" MAIN = "sys/dev/netif/ath/ath/if_ath.c" tx = open(TX).read().splitlines() main = open(MAIN).read().splitlines() def find(lines, pat, start=0): for i in range(start, len(lines)): if re.search(pat, lines[i]): return i return -1 ok = True def check(name, cond, detail=""): global ok print("[%s] %s%s" % ("PASS" if cond else "FAIL", name, (" -- " + detail) if detail else "")) if not cond: ok = False # --- in if_ath_tx.c: ath_tx_retry_clone (skip the forward decl at ~139) --- i_rc = find(tx, r"^ath_tx_retry_clone\(") i_rc = find(tx, r"^ath_tx_retry_clone\(", i_rc + 1) # definition (2nd occurrence) seg = "\n".join(tx[i_rc:i_rc + 55]) # 1. dmasetup on the clone's moved mbuf check("retry_clone calls ath_tx_dmasetup(sc, nbf, nbf->bf_m)", "ath_tx_dmasetup(sc, nbf, nbf->bf_m)" in seg, "if_ath_tx.c:%d" % (i_rc + 1)) # 2. error path returns nbf to the free list, no bf_m/bf_node cleanup i_err = find(tx, r"failed to setup dma for clone", i_rc) sub = "\n".join(tx[i_err:i_err + 15]) check("on dmasetup error nbf is recycled via ath_returnbuf_head", "ath_returnbuf_head(sc, nbf);" in sub, "if_ath_tx.c:%d" % (i_err + 1)) check("no mbuf free / node decref before recycling nbf", "ieee80211_node_decref" not in sub and "free_mbuf" not in sub and "bf_m = NULL" not in sub, "if_ath_tx.c:%d-%d" % (i_err + 1, i_err + 15)) check("error path returns NULL (caller drops the ORIGINAL bf)", "return NULL;" in sub, "") # 3. ath_buf_clone MOVES bf_m/bf_node (ownership transfer, no incref) i_cl = find(main, r"^ath_buf_clone\(") segcl = "\n".join(main[i_cl:i_cl + 50]) check("ath_buf_clone moves bf_m/bf_node into tbf (no incref)", "tbf->bf_node = bf->bf_node;" in segcl and "bf->bf_node = NULL;" in segcl and "incref" not in segcl, "if_ath.c:%d" % (i_cl + 1)) # 4. dmasetup frees the mbuf on error (so nbf->bf_m dangles) i_ds = find(tx, r"^ath_tx_dmasetup\(") segds = "\n".join(tx[i_ds:i_ds + 80]) check("ath_tx_dmasetup frees m0 on error (nbf->bf_m dangles post-free)", "ieee80211_free_mbuf(m0);" in segds, "if_ath_tx.c:%d" % (i_ds + 1)) # 5. _ath_getbuf_locked does NOT clear bf_m/bf_node on reuse i_gb = find(main, r"^_ath_getbuf_locked\(") seggb = "\n".join(main[i_gb:i_gb + 75]) check("_ath_getbuf_locked sanitizes flags/state but NOT bf_m/bf_node", "bzero(&bf->bf_state" in seggb and "bf->bf_m = NULL" not in seggb and "bf->bf_node = NULL" not in seggb, "if_ath.c:%d" % (i_gb + 1)) # 6. ath_freebuf asserts node/m already detached โ proving ref accounting is expected there i_fb = find(main, r"^ath_freebuf\(") segfb = "\n".join(main[i_fb:i_fb + 10]) check("ath_freebuf KASSERTs bf_node==NULL/bf_m==NULL (ref contract)", "bf->bf_node == NULL" in segfb and "bf->bf_m == NULL" in segfb, "if_ath.c:%d" % (i_fb + 1)) print() print("DF-3638 CONFIRMED at source level" if ok else "DF-3638 NOT confirmed") sys.exit(0 if ok else 1) |