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)
PoC verification
Evidence pack
findings/poc/DF-3714 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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):
:3284tdh = E1000_READ_REG(&adapter->hw, E1000_TDH(0));β used directly as the loop terminator; notdh < num_tx_descvalidation and no masking.:3299-3322while (first != tdh) { ... num_avail++; tx_buffer = &tx_buffer_area[first]; if (m_head) em_free_txbuffer(...); if (++first == num_tx_desc) first = 0; }.- If
tdhis outside[0, num_tx_desc)(as a 32-bit register read it can be any device-supplied value),firstnever equalstdh: the first wrap frees every TX buffer (including descriptors the NIC may still DMA from β DMA-after-free),num_availcounts pastnum_tx_desctrippingKKASSERT(num_avail < num_tx_desc)at:3302on INVARIANTS builds (panic), and on production builds (KKASSERTcompiled 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-internaltx_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_testablefix.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
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.
No comments yet.