# DF-3934 verdict narrative

## Reproduced?
No (hardware-gated).  Confidence: likely; source-certain as to the missing
bound and the unmasked indices, hardware-gated as to the flood trigger —
identical classification to the proven-by-inspection family finding
DF-3761 (alc_rxintr), of which this is the age(4) instance.

## Root cause chain (path:line)

1. The work-limit knobs are dead code:
   if_agevar.h:60-63 defines AGE_PROC_MIN/AGE_PROC_MAX/AGE_PROC_DEFAULT —
   `rg AGE_PROC sys/dev/netif/age/*` shows they are referenced NOWHERE in
   if_age.c.  (Exactly the DF-3761 signature: "the process_limit knob
   exists but is never read".)
2. age_rxintr() walks without any bound:
   if_age.c:2149  `for (prog = 0; rr_cons != rr_prod; prog++) { ... }`
   Every iteration runs age_rxeof → m_getcl/m_freem/if_input under the
   serializer (ASSERT_SERIALIZED via age_intr, if_age.c:1862).  A line-rate
   flood that keeps the CMB producer ahead of the consumer keeps this loop
   alive indefinitely: the serializer is never dropped, age_tick/mii_tick,
   watchdog and ioctls on the interface all starve — NIC + CPU livelock.
3. The producer/consumer values are 16-bit register fields used unmasked:
   if_age.c:1879-1882
   ```c
   sc->age_tpd_cons = (le32toh(cmb->tpd_cons) & TPD_CONS_MASK) >> TPD_CONS_SHIFT;  /* 0..65535 */
   sc->age_rr_prod  = (le32toh(cmb->rprod_cons) & RRD_PROD_MASK) >> RRD_PROD_SHIFT; /* 0..65535 */
   ```
   but the rings are AGE_TX_RING_CNT=256 and AGE_RR_RING_CNT=512
   (if_agevar.h:33-35).  rr_cons cycles mod 512 (AGE_DESC_INC,
   if_age.c:2171), so rr_prod values >= 512 are unreachable targets: with
   sane-but-full rings the loop relies solely on the nsegs==0 break
   (if_age.c:2152-2153) — a hostile ring (all entries with nsegs!=0 and a
   length matching the segment count) removes both brakes and the handler
   never exits.  Same shape as DF-1293's unmasked rx_cons, but on the
   *producer* fields and the *loop bound* rather than an array index.
4. TX side of the same defect: age_txintr (if_age.c:1946)
   `for (prog = 0; cons != tpd_cons; ...)` — tpd_cons unmasked; when the
   target is unreachable the loop is bounded only by `tx_cnt` (if_age.c:1947)
   and therefore reclaims the ENTIRE in-flight ring: it frees tx_m mbufs and
   bus_dmamap_unload()s maps for descriptors the NIC has not acknowledged —
   returning those DMA maps to age_encap's reuse pool while the hardware may
   still own the buffers (malicious-NIC DMA-after-free window).  Indexed
   access itself stays in range (cons is AGE_DESC_INC-wrapped), so this is
   the over-reclaim variant, not an OOB.

## Impact
- Flood path: interrupt-context livelock under the serializer → interface
  DoS + CPU starvation (Medium, needs line-rate RX — remote, unauthenticated
  in the sense that any peer that can send frames to the MAC does it).
- Hostile-NIC path: permanent handler wedge + TX over-reclaim DMA hazard.

## Fix
Bound the walk with AGE_PROC_DEFAULT, mask both producer fields to their
ring sizes (fix.diff).

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