#!/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)
