β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3934

RX walk has no work bound (AGE_PROC_* knobs are dead code) and 16-bit CMB producer indices are not masked to ring sizes β€” serializer-held livelock DoS and TX over-reclaim

Summary

The RX walk has no prog limit under ifp->if_serializer; the AGE_PROC_MIN/MAX/DEFAULT knobs are defined but referenced nowhere; rr_prod/tpd_cons (16-bit, 0..65535) are compared against mod-512/mod-256 walkers unmasked. Remote line-rate RX flood keeps the loop alive indefinitely - ticks, watchdog, ioctls starve; the TX analog drains the whole in-flight ring bounded only by tx_cnt, freeing mbufs/maps the NIC may still own. Budget+mask fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3934 Β· 5 files
FileTypeDescriptionSize
README.md β€” 1.0 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
fix.diff β€” 1.3 KB view raw
env.txt β€” 543 B view raw
verdict.json β€” 2.9 KB view raw

DF-3934 β€” age: RX walk has no work bound (AGE_PROC_* dead) and 16-bit producer indices are not masked to ring sizes

Build / Run (on hardware)

  • Line-rate RX flood to an age(4) interface keeps cmb->rprod_cons advancing while age_rxintr drains: with no prog bound the interrupt handler loops under ifp->if_serializer indefinitely (CPU + NIC livelock; ticks/watchdog starve).
  • Malicious/buggy NIC variant: reporting rr_prod >= 512 (or tpd_cons >= 256, values that mod-512/mod-256 consumers can never equal) makes the loop condition unachievable; only the nsegs==0 / length-mismatch breaks end it, and a hostile ring defeats those too.

Expected result

Interrupt-context livelock holding the serializer: interface and CPU wedge (no panic). The TX side analog (tpd_cons unreachable) drains ALL in-flight descriptors bounded only by tx_cnt β€” reclaiming mbufs/DMA maps for descriptors the NIC may still own.

Hardware-gated (no L1 NIC in the audit guest); family precedent DF-3761 (alc) used the identical classification. See VERDICT.md.

VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix not validated on the guest: the trigger path requires mly/age hardware absent from the audit guest. fix.diff is mechanical (bounds/termination-only) and authored post-verification against the cited lines.

findings/poc/DF-3934/fix.diff + VERDICT.md
↓ fix.diffper-fix-DF-3934

Confirmed kernel references

Detail

Exploit chain

remote line-rate RX flood (or hostile NIC ring) -> unbounded serializer-held RX walk -> interface/CPU livelock DoS

Evidence (decisive lines)

findings/poc/DF-3934/VERDICT.md (incl. dead-macro grep); fix.diff (AGE_PROC_DEFAULT bound + modulo masks)

PoC changes

No seed PoC; hardware-gated. Flood trigger needs only an L1 NIC receiving line-rate traffic.

Verified recommended fix

Bound the age_rxintr walk with AGE_PROC_DEFAULT and mask tpd_cons/rr_prod to their ring sizes.

Verdict

The age(4) instance of the DF-3761 family (alc): the work-limit knobs are dead code (AGE_PROC_MIN/MAX/DEFAULT defined at if_agevar.h:60-63, referenced nowhere in if_age.c) and age_rxintr's walk 'for (prog = 0; rr_cons != rr_prod; prog++)' (if_age.c:2149) has no prog bound while running under ifp->if_serializer - a line-rate RX flood that keeps the CMB producer ahead of the consumer livelocks the interrupt handler (CPU + NIC starvation; ticks/ioctls starve). Additionally the 16-bit producer/consumer register fields are used unmasked: age_tpd_cons/age_rr_prod (if_age.c:1879-1882, 0..65535) vs rings of 256/512 (if_agevar.h:33-35) - rr_prod >= 512 is an unreachable target for the mod-512 walker, leaving only the nsegs==0 break (:2152-2153) to end the loop, which a hostile ring defeats; and on the TX side an unreachable tpd_cons drains the ENTIRE in-flight ring bounded only by tx_cnt (:1946-1951), freeing mbufs/DMA maps for descriptors the NIC may still own (over-reclaim DMA hazard; index access itself stays wrapped, so no OOB there - the OOB-index variant on the rx side is known DF-1293). Hardware-gated (no L1 NIC in guest); classified inconclusive exactly like family DF-3761.