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)
PoC verification
Evidence pack
findings/poc/DF-3737 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:2817while (first != tdh) { ... }β if_emx.c:2832KKASSERT(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).
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_testablefix.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
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.
No comments yet.