# DF-3443 — altq_priq dangling `pif_ifq` use-after-free (interface destroyed mid-pf-transaction)

**File:** `sys/net/altq/altq_priq.c`
**Lines:** 114 (pointer captured), 177–179 & 207–209 (unvalidated dereference), plus 348 (`priq_class_create` uses stale `pif_ifq` under `ALTQ_RED`).
**Class:** CWE-416 use-after-free (kernel heap, RMW write).
**Severity:** Low (trigger requires root: `/dev/pf` 0600 + `FWRITE`) — but the
primitive itself is deterministic kernel memory corruption.
**Bucket:** memcorrupt.

## Root cause

`priq_add_altq()` (altq_priq.c:114) stores `pif->pif_ifq = &ifp->if_snd` with
**no reference on the ifnet**. A pf altq transaction spans several ioctls
(`DIOCXBEGIN(PF_RULESET_ALTQ)` → `DIOCADDALTQ`(interface) →
`DIOCADDALTQ`(queue) → `DIOCXCOMMIT`). The queue-spec ioctl finds the
discipline by *string match* on `ifname` in the inactive list
(pf_ioctl.c:2038–2044) with **no liveness check**. Nothing hooks ifnet
destruction back into pf/altq (`if_detach` frees `if_snd.altq_subq`
at if.c:1089 and the ifnet itself, without purging pf's altq entries).

If the interface is destroyed between the interface-spec and queue-spec
ioctls, `priq_add_queue()` does:

```c
ifq = pif->pif_ifq;              /* &freed_ifnet->if_snd            */
PRIQ_LOCK(ifq);                  /* read freed ifnet -> freed array
                                     -> RMW lwkt_serializer in freed
                                     M_DEVBUF chunk (offsets 0..16) */
```

i.e. a read through freed M_IFNET memory followed by atomic RMW writes at
offset 0–16 of a freed M_DEVBUF chunk — exactly where the DF slab allocator
keeps the in-band free-list link (`c_Next`) of the freed chunk.

## Reproduce (guest, root)

```sh
sh findings/poc/DF-3443/setup_guest.sh          # build PoC + ALTQ pf.ko + fw3443.ko
cd /root && ./df3443_poc tap0                   # full trigger (destroys tap0)
cd /root && ./df3443_poc tap0 --nodestroy       # control (live interface)
```

Expected observations (across runs, all seen):
1. **panic** `assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
   failed in chunk_mark_free` (kern_slaballoc.c:1675) — the serializer RMW
   destroyed the freed chunk's `c_Next`; the slab-maintenance timer later
   walks the poisoned free list into a bogus chunk.
2. **wedge** — the ioctl thread blocks forever in
   `lwkt_serialize_adaptive_enter`'s sleep (wchan `aslize`, state `D0`)
   on a garbage interlock in freed memory, holding `pf_token`; the system
   progressively locks up (ssh stops answering).
3. **silent** — the freed interlock happens to read 0/unlocked; the lock is
   entered+released on freed memory with no immediate symptom (corruption
   may still be planted).
4. Control (`--nodestroy`) never fails.

The inspector module `fw3443.ko` (`hw.fw3443.addr` / `hw.fw3443.dump`)
photographs the freed chunks before/after the trigger; the PoC prints the
`pif`/`pif_ifq`/`ifnet`/`altq_subq` chain recovered from the (legitimate)
`DIOCADDALTQ` copy-back.

## Why the rebuilt pf.ko

The shipped `pf.ko` is compiled without the ALTQ ioctl glue (module builds
do not define `ALTQ`; `#ifdef ALTQ` at pf_ioctl.c:90 gates the handlers),
so `DIOCADDALTQ` returns ENODEV. The kernel itself already contains the
priq scheduler (`options ALTQ_PRIQ`), so rebuilding only `pf.ko` against
`KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC` enables the ALTQ ioctls
with stock kernel code. No kernel behavior is otherwise modified.

## Fix

`fix.diff`: revalidate the interface (`ifunit(a->ifname)` under
`ifnet_lock`, compare `pif->pif_ifq == &ifp->if_snd`) in
`priq_add_queue()`/`priq_remove_queue()` — the same lock nesting
`priq_getqstats()` already uses. Validated on an in-guest `nativekernel`
build: trigger returns `ENODEV` (no freed-memory access), control path
still works. The same pattern exists in `altq_cbq.c`, `altq_hfsc.c`,
`altq_fairq.c` and should be fixed there too (out of this file's scope).
