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

ix_txeof/ix_txgc loop on unvalidated NIC TX-head index: out-of-range write-back β†’ infinite interrupt-context spin (KKASSERT panic on INVARIANTS, soft-lockup otherwise)

Summary

TX completion is driven by the NIC-DMA'd head write-back *(txr->tx_hdr) (TDWBA-mapped) fed verbatim as 'int hdr' from four call sites plus the TDH register in ix_txgc. 'while (first != hdr)' assumes 0 <= hdr < tx_ndesc; first wraps inside the ring, so any out-of-range value makes the exit unreachable: INVARIANTS kernels panic at KKASSERT(avail < tx_ndesc); production kernels spin forever in ISR context (soft-lockup, box DoS) with unbounded avail increments. Malicious/faulty NIC (reset-during-DMA artifact, stuck-bus 0xFFFFFFFF, or DMA adversary) converts one bogus write-back into a permanent kernel hang; DoS-only (first stays ring-bounded, no memory corruption). Fix: reject out-of-range head indices.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3668 Β· 8 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
fix.diff β€” 801 B view raw
build_baseline.log β€” 15.8 KB view raw
build_patched.log β€” 15.8 KB view raw
env.txt β€” 1.0 KB view raw
manifest.json β€” 626 B view raw
verdict.json β€” 2.3 KB view raw

DF-3668 β€” ix(4) TX completion trusts NIC-DMA'd head index with no ring bound

File: sys/dev/netif/ix/if_ix.c Β· Severity: Low Β· Class: hardware-trust DoS

PoC

No runnable PoC exists: the trigger requires an ixgbe NIC whose TX head write-back (*(txr->tx_hdr), DMA'd by the NIC) or TDH register reports a value outside [0, tx_ndesc). The audit guest has no such device (env.txt).

Source-level argument (see VERDICT.md):

  • if_ix.c:2368 β€” while (first != hdr) { ... } in ix_txeof(): first wraps within the ring, hdr is an unmasked int from *(txr->tx_hdr) (declared volatile uint32_t * at if_ix.h:223) or from IXGBE_READ_REG(hw, IXGBE_TDH(...)). If hdr >= tx_ndesc or hdr < 0, the equality is unreachable: the loop spins forever in interrupt context (INVARIANTS kernels trip KKASSERT(avail < txr->tx_ndesc) at 2371 and panic; production kernels soft-lockup with avail incrementing past tx_ndesc).
  • Callers feeding the untrusted value: if_ix.c:1203 (legacy ix_intr), if_ix.c:4788 (ix_msix_tx), if_ix.c:4810 (ix_msix_rxtx), if_ix.c:4957 (ix_npoll_tx), and ix_txgc reads TDH directly at 2400.

Fix validation (compile-level)

fix.diff rejects out-of-range hdr in both ix_txeof() and ix_txgc(). Applied on the guest to /usr/src (md5-identical to the audit tree) β€” if_ix.ko rebuilt with -Werror, clean (build_patched.log).

Run (build validation only)

# on the audit guest:
cd /usr/src && patch -p1 < fix.diff
cd /usr/src/sys/dev/netif/ix && make obj && make
VERDICT.md
↓ download raw

DF-3668 β€” VERDICT

Status: inconclusive (runtime) / source-level certain. Impact class: interrupt-context soft-lockup DoS (INVARIANTS: panic). Hardware-gated β€” no ixgbe NIC on the guest.

Root cause

ix_txeof() (sys/dev/netif/ix/if_ix.c:2355-2387) is the TX completion path fed directly by hardware-provided head indices:

first = txr->tx_next_clean;          /* if_ix.c:2363 β€” software, ring-bounded */
if (first == hdr)
        return;
avail = txr->tx_avail;
while (first != hdr) {               /* if_ix.c:2368 */
        ...
        KKASSERT(avail < txr->tx_ndesc);   /* 2371: INVARIANTS only */
        ++avail;                          /* unbounded on bogus hdr */
        ...
        if (++first == txr->tx_ndesc)
                first = 0;                /* first wraps inside [0, ndesc) */
}

hdr arrives from:

  • *(txr->tx_hdr) β€” the TX head write-back buffer, a cacheline of DMA memory the NIC writes (if_ix.h:223, mapped at if_ix.c:2192-2195):
  • legacy interrupt: ix_intr β†’ if_ix.c:1203
  • MSI-X TX vector: ix_msix_tx β†’ if_ix.c:4788
  • MSI-X combined RX/TX: ix_msix_rxtx β†’ if_ix.c:4810
  • polling: ix_npoll_tx β†’ if_ix.c:4957
  • IXGBE_READ_REG(hw, IXGBE_TDH(idx)) in ix_txgc() β†’ if_ix.c:2400, same unbounded while (first != hdr) loop at 2409.

hdr is an int cast of a full 32-bit DMA'd word. The loop's exit condition assumes 0 <= hdr < tx_ndesc. A hostile or hung NIC that reports e.g. 0xFFFFFFFF (a plausible stuck-bus / reset-during-DMA artifact, or an attacker-controlled DMA write) makes the condition unreachable:

  • INVARIANTS kernels: KKASSERT(avail < txr->tx_ndesc) fires on the first wrap β†’ kernel panic.
  • production kernels: the loop never exits β€” the interrupt/polling thread spins in ISR context at 100% β†’ soft lockup / permanent DoS of the box (RX on that vector stops, watchdog cannot run); avail (int) eventually overflows (UB) after 2^31 iterations.

There is no masking/clamping of the head anywhere in this driver β€” compare the RX path which at least wraps i against rx_ndesc (if_ix.c:3085).

Threat model

Requires control of (or a fault in) the NIC's descriptor write-back β€” the same malicious-peripheral / broken-FWIW trust boundary as DF-3667. Not triggerable by network packets alone. DoS-only: no memory is corrupted (tx_buf[first] stays inside the ring because first wraps).

Verification performed

  1. Full source read of ix_txeof/ix_txgc/ix_msix_* / ix_intr (cited lines).
  2. env.txt: no Intel 10GbE PCI device on the audit guest; ix(4) compiled into the kernel but never attached β†’ runtime reproduction impossible.
  3. fix.diff (reject hdr outside [0, tx_ndesc)) applied to the guest's md5-identical /usr/src; if_ix.ko rebuilt with -Werror, zero errors (build_patched.log). Local sys/ untouched.

fix.diff: early-return on hdr < 0 || hdr >= txr->tx_ndesc in both ix_txeof() and ix_txgc() (treat as spurious interrupt; the watchdog/txgc path will re-run).

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

Compile-validated only (build_patched.log, -Werror clean); runtime comparison needs hardware.

build_baseline.log / build_patched.log; fix.diff applies with patch -p1 from /usr/src.
↓ fix.diffper-fix-DF-3668

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (path:line); build_baseline.log vs build_patched.log (fix compiles clean); env.txt (no device)

PoC changes

New pass-2 finding, no seed. Fix authored directly from the audited source.

Verified recommended fix

Reject hdr outside [0, tx_ndesc) in ix_txeof/ix_txgc (fix.diff).

Verdict

Source-level certain, runtime-inconclusive: ix_txeof (if_ix.c:2368) and ix_txgc (if_ix.c:2409) loop 'while (first != hdr)' on a hardware-provided TX head index (DMA write-back *(txr->tx_hdr) at if_ix.c:1203/4788/4810/4957, or TDH register at 2400) with no range validation against tx_ndesc. A value outside [0, tx_ndesc) makes the exit condition unreachable: INVARIANTS kernels panic on KKASSERT at 2371; production kernels spin forever in interrupt context (soft-lockup DoS) with unbounded avail increments. Hardware-gated (malicious/faulty NIC); guest has no ixgbe device (env.txt). fix.diff (reject out-of-range hdr) compile-validated with -Werror on md5-identical guest source.