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)
PoC verification
Evidence pack
findings/poc/DF-3934 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_consadvancing while age_rxintr drains: with noprogbound the interrupt handler loops underifp->if_serializerindefinitely (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.
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)
- 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".) - 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. - 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. - 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 bytx_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_testableFix 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
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.
No comments yet.