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

emx_tx_collect walks the TX ring toward an unvalidated TDH register value: out-of-range write-back (0xFFFFFFFF on surprise removal) -> all TX buffers freed (DMA-after-free window) then KKASSERT panic / permanent serializer-held spin (DF-3714 em(4) twin)

Summary

emx_tx_collect() reads the raw TDH MMIO register into an int and uses it as the cleanup walk terminator with no range validation. An out-of-range value (0xFFFFFFFF read-back on surprise removal -> tdh = -1, or any value >= num_tx_desc from a faulty device) makes 'while (first != tdh)' unreachable: the walk frees every TX buffer including hardware-owned descriptors (DMA-after-free window), then KKASSERT panics INVARIANTS builds while production builds spin forever holding the TX serializer - permanent wedge of every ifsq/ifconfig/watchdog path on that ring. VERIFIED: harness section [C] - TDH 0x1000 and 0xFFFFFFFF(-1) vs a 512-entry ring never reach the terminator; in-range TDH=100 safe. Requires a faulty NIC or surprise removal (hot-unplug/flaky PCIe) - a routine operator action, not directly forgeable by an unprivileged user.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3737 Β· 12 files
FileTypeDescriptionSize
emx_bounds_harness.c β€” 5.5 KB view raw
build.sh β€” 115 B view raw
run.sh β€” 121 B view raw
build.log β€” 143 B view raw
run.log β€” 234 B view raw
run.harness.log β€” 2.0 KB view raw
env.txt β€” 819 B view raw
fix.diff β€” 643 B view raw
VERDICT.md β€” 2.5 KB ↓ raw
README.md β€” 1.8 KB ↓ raw
verdict.json β€” 2.8 KB view raw
manifest.json β€” 1.0 KB view raw

DF-3737 β€” emx(4) unvalidated TDH walk in emx_tx_collect (DF-3714 em(4) twin)

What

emx_tx_collect() reads the raw TDH register into an int and walks the TX ring until first == tdh, with no range validation:

  • tdh = E1000_READ_REG(&tdata->sc->hw, E1000_TDH(tdata->idx)); β€” sys/dev/netif/emx/if_emx.c:2817
  • while (first != tdh) { ... } β€” if_emx.c:2832
  • KKASSERT(num_avail < tdata->num_tx_desc); β€” if_emx.c:2835
  • wrap: if (++first == tdata->num_tx_desc) first = 0; β€” :2853-2854

An out-of-range TDH β€” e.g. MMIO read-back of 0xFFFFFFFF on surprise removal, narrowing to tdh = -1, or any value β‰₯ num_tx_desc from a faulty device β€” makes the terminator unreachable: first cycles 0..num_tx_desc-1 forever. First pass frees every tx_buf slot (DMA-after-free window vs the still-armed device), then KKASSERT/num_avail overflow: INVARIANTS builds panic at :2835; production builds spin forever holding the TX serializer (permanent wedge of every ifsq/ifconfig path on that ring).

Exact twin of em(4)'s DF-3714 (Low: device-fault-triggered local DoS).

Build / run

cc -O -o /root/emx_bounds_harness /root/emx_bounds_harness.c
/root/emx_bounds_harness        # section [C]

Expected output

[C] num_tx_desc=512, ... raw TDH register=0xffffffff -> int tdh=-1
  TDH out of ring range: walked 4194304 descriptors (8192 full ring wraps)
  without reaching TDH -> unbounded loop under the TX serializer
  [OVERFLOW] KKASSERT(num_avail<num_tx_desc) fires on INVARIANTS builds ...

(iteration-capped replica of the loop math; the real loop has no cap).

Device-side injection

Guest has no emx hardware (see env.txt). Triggering for real needs surprise removal / faulty-device register read-back β€” documented in README; source-certain / hardware-gated (inconclusive per family convention).

VERDICT.md
↓ download raw

DF-3737 β€” VERDICT

Status: inconclusive (source-certain, runtime hardware-gated) β€” same convention as the em(4) twin DF-3714. reproduced=0, impact=none on this guest.

Reproduced what, and why not more

Guest has no emx hardware (env.txt: kldload if_emx loads the module, no device attaches). The walk math was replicated and run on the guest (run.harness.log section [C]): with num_tx_desc=512, - raw TDH 0x1000 (out-of-range): 4,194,304 iterations / 8192 full ring wraps without termination (capped; the kernel loop is uncapped); - raw TDH 0xFFFFFFFF β†’ int tdh = -1: same unreachable terminator; - in-range TDH=100: normal cleanup.

The kernel-side consequence chain is source-certain: 1. emx_tx_collect() β€” sys/dev/netif/emx/if_emx.c:2808-2868 β€” reads TDH at :2817 into int tdh, no range check. 2. while (first != tdh) (:2832) frees each tx_buf[first] via emx_free_txbuf() (bus_dmamap_unload + m_freem, :391-401) β€” after one full wrap every TX buffer is freed while the device may still own the descriptors (DMA-after-free window; on a truly-removed device harmless, on a flapping/faulty one it is a live primitive). 3. KKASSERT(num_avail < tdata->num_tx_desc) (:2835) panics INVARIANTS builds as soon as num_avail reaches the ring size; production builds keep spinning with the TX serializer held β†’ every ifsq/ifconfig/watchdog path on that ring blocks forever (system degradation, effectively irreversible without reboot).

Callers: emx_start fallback (:1116, :1130), emx_tx_intr β†’ emx_txeof boundary GC via emx_try_txgc (:412-424), emx_txgc_timer (:444), emx_tx_purge from the LSC interrupt (:2888).

Threat model

Requires a faulty NIC or surprise removal (hot-unplug, flaky PCIe link) β€” not attacker-forgeable by an unprivileged local user alone, but hotplug removal is a routine operator action. Severity Low (matches DF-3714).

Fix

fix.diff (git-apply clean): bail out of emx_tx_collect() when (uint32_t)tdh >= tdata->num_tx_desc instead of walking toward an unreachable index. Cannot be runtime-validated without hardware β†’ 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 (git apply --check clean); cannot be runtime-validated on a guest without emx hardware.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-3737/VERDICT.md; run.harness.log (section [C]); build.log; env.txt; fix.diff (bail when (uint32_t)tdh >= num_tx_desc, git-apply clean)

PoC changes

No seed PoC. emx_bounds_harness.c section [C] authored for this finding (shared with DF-3736).

Verified recommended fix

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

Verdict

Source-certain, runtime hardware-gated (same convention as the em(4) twin DF-3714): emx_tx_collect (sys/dev/netif/emx/if_emx.c:2808-2868) uses the raw TDH register read at :2817 (narrowed to int) as the cleanup walk terminator with no range validation. An out-of-range TDH - MMIO read-back 0xFFFFFFFF on surprise removal (tdh = -1) or any value >= num_tx_desc from a faulty device - makes 'while (first != tdh)' (:2832) unreachable: first wraps mod num_tx_desc (:2853-2854) forever. First full pass frees every TX buffer incl. hardware-owned descriptors via emx_free_txbuf (bus_dmamap_unload + m_freem, :391-401) - DMA-after-free window; KKASSERT(num_avail < num_tx_desc) at :2835 then panics INVARIANTS builds, while production builds spin forever holding the TX serializer (permanent wedge of ifsq/ifconfig/watchdog paths on that ring). Walk math replicated on the guest (run.harness.log section [C]: TDH 0x1000 and 0xFFFFFFFF vs 512-entry ring - no termination at the replica's 4194304-iteration cap; in-range TDH safe). Triggering for real requires surprise removal / faulty device - absent on the audit guest -> inconclusive per family convention.