# DF-3669 — ix(4) detach leaves self-re-arming tx_gc_timer callout armed → use-after-free of tx_rings

**File:** `sys/dev/netif/ix/if_ix.c` · **Severity:** Low (root-only trigger: kldunload/hot-remove) · **Class:** lifetime/UAF

## PoC

No runnable PoC: requires (a) an attached ixgbe NIC (guest has none, `env.txt`)
and (b) an unprivileged-code-unreachable event — driver detach (kldunload or
PCI hot-removal), which is root-only. Documented source-level race:

1. `ix_txgc_timer()` (`if_ix.c:352-374`) re-arms itself unconditionally at the
   `done:` label: `callout_reset(&txr->tx_gc_timer, 1, ix_txgc_timer, txr)`
   (`if_ix.c:373`) — while the interface is RUNNING there is *always* a
   pending armed callout.
2. `ix_stop()` only does `callout_stop(&txr->tx_gc_timer)` (`if_ix.c:1767`) —
   `callout_stop()` does **not** wait for an in-flight callback; a callback
   executing concurrently takes the `goto done` path (its
   `lwkt_serialize_try` fails while detach holds all serializers) and re-arms
   the callout *after* the stop.
3. `ix_detach()` terminates `sc->timer` and `sc->fw_timer`
   (`if_ix.c:693,696`) but **never** the per-ring `tx_gc_timer`, then frees
   the rings: `ix_free_rings()` (`if_ix.c:721`) → `ix_destroy_tx_ring()`
   (`if_ix.c:2102-2122`, frees `tx_base`/`tx_buf`/DMA maps) and
   `kfree(sc->tx_rings)` (`if_ix.c:4002`).
4. The re-armed 1-tick callout fires on freed memory: reads
   `txr->tx_sc->arpcom.ac_if.if_flags`, then
   `lwkt_serialize_try(&txr->tx_serialize)` — a write into freed slab memory
   — and `callout_reset` on the freed embedded `callout`.

## Fix validation (compile-level)

`fix.diff` adds `callout_terminate()` for every `tx_gc_timer` in `ix_detach()`
before rings are freed (terminate disarms AND waits for running callbacks).
Compile-validated on the guest's md5-identical `/usr/src` (`build_patched.log`,
`-Werror` clean).
