# DF-3669 — VERDICT

**Status: inconclusive (runtime) / source-level certain. Impact class: use-after-free on driver detach. Root-only trigger, narrow race window.**

## Root cause and race walk

- `ix_txgc_timer()` (if_ix.c:352-374) is a 1-tick self-re-arming callout
  (re-arm at if_ix.c:373). While the interface is RUNNING, an armed callout is
  pending essentially always; early `return`s at 357-359/364-368 are the only
  non-re-arming exits and require IFF_RUNNING to already be clear.
- `ix_stop()` (called from `ix_powerdown()` under `ifnet_serialize_all()` in
  `ix_detach()`, if_ix.c:686-689) issues only `callout_stop(&txr->tx_gc_timer)`
  (if_ix.c:1767). DragonFly `callout_stop()` disarms a *pending* callout but
  does not wait out a *currently executing* callback.
- A concurrently executing callback on the ring's pinned CPU
  (`tx_gc_timer` is `callout_init_mp`, reset via
  `callout_reset_bycpu(..., txr->tx_intr_cpuid)` at if_ix.c:1157) observes:
  flags check passes (pre-stop), `lwkt_serialize_try(&txr->tx_serialize)`
  FAILS (detach holds all serializers via `ifnet_serialize_all`), so it takes
  `goto done` → `callout_reset(1 tick)` — **re-armed after the stop**.
- `ix_detach()` then: `callout_terminate(&sc->timer)` (693),
  `callout_terminate(&sc->fw_timer)` (696) — no terminate for
  `tx_gc_timer` — releases resources (`if_ix.c:698-735`), and
  `ix_free_rings()` (721) frees `tx_base`/`tx_buf`/DMA maps
  (ix_destroy_tx_ring, if_ix.c:2090-2123) and the ring array itself
  (`kfree(sc->tx_rings)`, if_ix.c:4002).
- ~10 ms later (1 tick at hz=100) the orphaned callout fires:
  - reads `txr->tx_sc->arpcom.ac_if.if_flags` from freed memory (UAF read),
  - `lwkt_serialize_try(&txr->tx_serialize)` — atomic RMW **write** into
    freed slab memory,
  - `callout_reset()` on the freed embedded `struct callout` — corrupts the
    per-CPU callout queue.
  If the slab is reused (M_DEVBUF) between free and fire, the callback
  dispatch itself reads attacker-influenced function/argument pointers.

## Trigger conditions & severity

- Requires an attached ix(4) device and driver detach (kldunload if_ix is
  impossible when built-in; `device_delete`/hot-remove or module unload on a
  modular kernel) — **root-only**, so Low per the audit rubric.
- The race window is one callback execution per tick; under TX GC load with a
  module build, repeated kldunload-under-traffic eventually hits it.

## Verification performed

1. Source walk (all lines cited above) — no `callout_terminate` for
   `tx_gc_timer` anywhere in the driver (rg confirms only `callout_stop` at
   1767).
2. Guest has no ixgbe NIC (`env.txt`): `ix_attach()` never runs, no callout is
   ever armed → runtime reproduction impossible on this guest; also the
   trigger needs root, so it would be out of scope for an unpriv escalation
   anyway.
3. `fix.diff` adds the missing `callout_terminate()` loop in `ix_detach()`
   (guarded by `sc->tx_rings != NULL` for the attach-failure path).
   Compile-validated: patched `if_ix.ko` builds with `-Werror`, clean
   (`build_patched.log`); guest `/usr/src` restored to pristine afterwards.

## Recommended fix

`fix.diff` in this pack.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
