# DF-3443 VERDICT — altq_priq.c dangling pif_ifq UAF

**Status: reproduced** (impact: dos — wedge and/or panic; underlying primitive:
deterministic UAF write into the kernel slab free list). Confidence: certain.

## Narrative

`priq_add_altq()` captures `pif->pif_ifq = &ifp->if_snd` (altq_priq.c:114)
without any ifnet lifetime protection. pf's altq transaction spans multiple
ioctls and locates the discipline by ifname string-match (pf_ioctl.c:2038–2044);
`if_detach()` frees `ifp->if_snd.altq_subq` (if.c:1089) and the ifnet itself
without notifying pf. Destroying the interface between the interface-spec and
queue-spec `DIOCADDALTQ` ioctls therefore leaves `priq_add_queue()` (altq_priq.c:
177–179) executing `PRIQ_LOCK(pif->pif_ifq)` — a read through the freed ifnet
(`ifq->altq_subq` at freed_ifnet+464+64) followed by `lwkt_serialize_adaptive_
enter/exit` atomic RMW writes at offsets 0..16 of the freed `altq_subq` chunk
(192-byte object, 256-class M_DEVBUF slab) — precisely where `kfree()` parked
the in-band free-list link (`c_Next`).

Runtime proof chain (guest `DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC`,
INVARIANTS):

1. **Panic manifestation** (run 1, PoC v2, stock kernel + ALTQ-enabled pf.ko):
   minutes after the trigger the slab-maintenance timer walked the poisoned
   free list:
   `panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0" failed
   in chunk_mark_free at kern_slaballoc.c:1675`
   trace: chunk_mark_free ← slab_cleanup ← slotimer_callback ← softclock_handler
   (panic.txt).

2. **Wedge manifestation** (run 5): the PoC process was photographed in
   `ps ax -o pid,stat,wchan,command` as `1211 D0 aslize ./df3443_poc tap0` —
   state D0 (uninterruptible), wait channel `aslize` = the blocked path of
   `lwkt_serialize_adaptive_enter()` (kern/lwkt_serialize.c:240+): PRIQ_LOCK is
   sleeping on the serializer embedded in freed/reused memory while holding
   `pf_token`; the guest then degraded to a full lockup (ssh banner timeout,
   vm.sh status down). (serial_wedge_run.log; note: the final `Fatal trap 9`
   in that log is my inspector module poking an address after the system was
   already wedged — a measurement artifact, not the finding.)

3. **Memory photographs** (runs 4/6, fw3443.ko inspector): kernel pointers
   recovered from the `DIOCADDALTQ` copy-back (pif=…48c0, pif_ifq=…9750,
   ifnet=…9580, altq_subq=…fb00); after `SIOCIFDESTROY` the freed 256-class
   chunk shows the slab freelist pointer in word0
   (`fffff8008f8afc00`) and gets partially **reused mid-ioctl** (words 2–7
   become a live "tap…"/pif-containing object between the A and B dumps),
   while the kernel-visible `*(pif_ifq->altq_subq)` still resolves into the
   freed region. Two of five full-trigger runs produced catastrophic failure,
   three returned silently (freed interlock read 0).

4. **Control** (`--nodestroy`): identical sequence against a live tap0 —
   clean, pf responsive, no panic (proves the destroy is the differentiator).

## Exploit chain

Not applicable as privilege escalation: every step (pf ioctls, interface
destroy) requires root. The corruption primitive itself (freelist-link
overwrite at a freed 256-class chunk + `last_td` pointer write) would be a
classic slab-poisoning bed for an unprivileged path, but this path is
root-gated; impact is therefore kernel-panic / system-wedge DoS plus
latent heap corruption.

## Fix validation

`fix.diff` adds ifnet revalidation under `ifnet_lock()` (the exact nesting
`priq_getqstats()` already uses) to `priq_add_queue()`/`priq_remove_queue()`.
Built with `make -j6 nativekernel` in-guest (fix_build.log), rebooted, re-ran:

- baseline (unpatched): wedge/panic as above;
- patched: the queue-spec ioctl after destroy returns a clean error
  (ENODEV — `ifunit` finds no tap0), no freed-memory access, guest stays
  healthy; control run (`--nodestroy`) still configures the queue normally
  (no regression). fix_run.log.

Siblings with the same pattern (should receive the same fix):
altq_cbq.c (`cbq_add_queue`), altq_hfsc.c, altq_fairq.c.

## kernel_refs

- sys/net/altq/altq_priq.c:114 (capture)
- sys/net/altq/altq_priq.c:161-184 (priq_add_queue deref)
- sys/net/altq/altq_priq.c:197-214 (priq_remove_queue deref)
- sys/net/pf/pf_ioctl.c:2031-2045 (string-match altq_disc handoff, no liveness)
- sys/net/if.c:1089 (kfree of if_snd.altq_subq at detach)
- sys/kern/kern_slaballoc.c:1675 (freelist assertion the corruption trips)
- sys/kern/lwkt_serialize.c:240 (adaptive enter sleep "aslize")

## 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.
