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

em_tx_collect walks the TX ring toward an unvalidated TDH register value: out-of-range write-back -> all TX buffers freed (DMA-after-free) then KKASSERT panic / permanent serializer-held spin; same class in the 82547 TDT walk

Summary

em_tx_collect() uses the raw TX-head register read directly as the cleanup loop terminator with no range validation. An out-of-range TDH makes 'while (first != tdh)' unreachable: the first ring wrap frees EVERY tx buffer - including descriptors the NIC may still own (DMA-after-free) - then num_avail counts past num_tx_desc tripping KKASSERT on INVARIANTS builds (panic), while on production builds the loop spins forever holding the interface serializer (permanent interrupt-context soft-lockup). Same trust class in em_82547_move_tail_serialized: unvalidated hw_tdt indexes tx_desc_base[hw_tdt] and is walked with modular ++ that an out-of-range start value turns into up to 65536 OOB descriptor reads, ending in an out-of-ring TDT register program. Mirrors DF-3668 (ix)/DF-3680 (igb). VERIFIED: harness - 512-entry ring, TDH=4096 -> non-terminating walk, KKASSERT trip computed at the 513th descriptor; in-range TDH=100 safe.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3714 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
build.sh β€” 113 B view raw
run.sh β€” 153 B view raw
build.log β€” 11 B view raw
run.log β€” 2.5 KB view raw
env.txt β€” 634 B view raw
fix.diff β€” 723 B view raw
verdict.json β€” 2.5 KB view raw
em_bounds_harness.c β€” 7.5 KB view raw

DF-3714 β€” em_tx_collect walks toward an unvalidated NIC TX-head register value

File: sys/dev/netif/em/if_em.c Lines: 3284 (tdh = E1000_READ_REG(&adapter->hw, E1000_TDH(0))), 3299-3322 (cleanup walk), 3302 (KKASSERT) Family: DF-3668 (ix), DF-3680 (igb) β€” TX-head write-back validation.

em_tx_collect() trusts the raw TDH register as the ring stop-index with no range validation. An out-of-range TDH (device fault) makes while (first != tdh) unreachable: first wraps the ring forever in interrupt/serialized context, freeing every TX buffer on the first wrap (including buffers the hardware may still own β€” DMA-after-free), then spinning permanently; KKASSERT(num_avail < num_tx_desc) at :3302 panics INVARIANTS builds after exactly num_tx_desc increments. The same trust class exists in em_82547_move_tail_serialized() (:2003-2011): hw_tdt read from TDT is used to index tx_desc_base[hw_tdt] and walked with ++hw_tdt == num_tx_desc reset logic that an out-of-range start value turns into a ≀65536-iteration OOB descriptor read walk, ending in E1000_WRITE_REG(TDT, hw_tdt) programming the device with an out-of-ring tail index.

Reproduce on hardware

Requires a device fault/descriptor corruption making TDH β‰₯ num_tx_desc (or the 82547 TDT variant). DoS-only class, no memory-corruption sink on the host side beyond the DMA-after-free of TX buffers during the first wrap.

On this audit guest

No em hardware; run.sh presence probe + guest-executed bound-math harness (run.harness.log section [C]): num_tx_desc=512, TDH=4096 β†’ walk never terminates (demonstrated 1,048,576 iterations / 2048 ring wraps at harness limit), KKASSERT trip point computed at iteration 513.

sh build.sh && ./run.sh
VERDICT.md
↓ download raw

DF-3714 β€” VERDICT

Status: source-certain, runtime hardware-gated (inconclusive on this guest β€” no em NIC).

Root cause

em_tx_collect() (sys/dev/netif/em/if_em.c:3275-3335) is the TDH-driven TX cleanup path (invoked from em_start:1169/1184, em_try_txgc:480, em_tx_purge:3349):

  • :3284 tdh = E1000_READ_REG(&adapter->hw, E1000_TDH(0)); β€” used directly as the loop terminator; no tdh < num_tx_desc validation and no masking.
  • :3299-3322 while (first != tdh) { ... num_avail++; tx_buffer = &tx_buffer_area[first]; if (m_head) em_free_txbuffer(...); if (++first == num_tx_desc) first = 0; }.
  • If tdh is outside [0, num_tx_desc) (as a 32-bit register read it can be any device-supplied value), first never equals tdh: the first wrap frees every TX buffer (including descriptors the NIC may still DMA from β€” DMA-after-free), num_avail counts past num_tx_desc tripping KKASSERT(num_avail < num_tx_desc) at :3302 on INVARIANTS builds (panic), and on production builds (KKASSERT compiled out, sys/sys/systm.h:117-119) the loop spins forever while holding the interface serializer β€” permanent interrupt-context soft-lockup.
  • Note em_txeof() (:3213-3272) is safe: it walks driver-internal tx_dd[] indices, not the register.

Same class in the 82547 workaround: em_82547_move_tail_serialized() (:2003-2024) reads hw_tdt = E1000_READ_REG(TDT) into a uint16_t, uses it to index tx_desc_base[hw_tdt] and walks ++hw_tdt == num_tx_desc β€” an out-of-range start value yields up to 65536 OOB descriptor reads (length += tx_desc->lower.flags.length over adjacent kernel memory) and a final E1000_WRITE_REG(E1000_TDT(0), hw_tdt) programming the device with an out-of-ring tail (device-side descriptor fetch beyond the ring).

Threat

Device-trust DoS, same premise/gravity as accepted DF-3668 (ix) and DF-3680 (igb): needs a faulty device (TDH/TDT corruption or erratum). Low severity.

Verification

  • Guest has no em hardware (run.sh).
  • Guest-executed bound-math harness (run.harness.log [C]): TDH=4096 with a 512-entry ring β†’ non-terminating walk (1,048,576 iterations at harness cap), KKASSERT trip at the 513th descriptor, production build β†’ serializer-held permanent spin. In-range TDH (100) verified safe.

Fix

fix.diff: bail out of em_tx_collect() when (uint32_t)tdh >= (uint32_t)num_tx_desc (device fault; do not walk). Not hardware-testable on this guest (fix_status: not_testable).

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.diff authored against the read-only sys/ tree (patch --dry-run clean); cannot be validated on a guest without em hardware.

findings/poc/DF-3714/fix.diff
↓ fix.diffper-fix-DF-3714

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-3714/VERDICT.md; run.log (probe + harness section [C]: TDH=4096 vs 512-entry ring -> 1,048,576 iterations / 2048 wraps without termination; in-range TDH safe); build.log; fix.diff (bail when (uint32_t)tdh >= num_tx_desc)

PoC changes

No seed PoC. em_bounds_harness.c section [C] written for this finding.

Verified recommended fix

Bail out of em_tx_collect() when (uint32_t)TDH >= num_tx_desc instead of walking the ring toward an unreachable index.

Verdict

Source-certain, runtime hardware-gated: em_tx_collect (sys/dev/netif/em/if_em.c:3275-3335) uses the raw TDH register read (:3284) as the cleanup walk terminator with no range validation. An out-of-range TDH makes 'while (first != tdh)' unreachable: first wrap frees every TX buffer (including hardware-owned descriptors - DMA-after-free), KKASSERT(num_avail < num_tx_desc) at :3302 panics INVARIANTS builds, and on production builds (KKASSERT compiled out, sys/sys/systm.h:117-119) the loop spins forever holding the interface serializer - permanent soft-lockup. Same class in em_82547_move_tail_serialized (:2003-2024): unvalidated TDT-based hw_tdt walk over tx_desc_base[] and an out-of-ring TDT register write. Mirrors DF-3668 (ix) / DF-3680 (igb). Guest-executed bound-math harness demonstrates the non-terminating walk; no em hardware on the audit guest.