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

msk_txeof() consumes the device-supplied TX done index (0..4095) with no MSK_TX_RING_CNT (256) range check: bogus index force-completes the entire in-flight TX window (premature dmamap_unload/m_freem on HW_OWNER descriptors)

Summary

msk_txeof walks 'for (; cons != idx; MSK_INC(cons, MSK_TX_RING_CNT))' where idx is device-supplied: the status LE's 11-bit fields (0..4095) from DMA and the watchdog's raw 16-bit CSR read - while the ring holds 256 entries. No range check exists. For an idx that can never equal cons (>=256, or behind cons), the only terminator is the msk_tx_cnt<=0 break, so one bogus index drains ALL in-flight descriptors: bus_dmamap_unload + m_freem on mbufs whose TX LEs still carry HW_OWNER - the NIC may still be DMA-reading those clusters (stale/freed kernel memory transmitted on the wire) - and leaves msk_tx_cons advanced to msk_tx_prod while the NIC prefetch unit still walks the old LEs. Memory-safe by construction (bounded by msk_tx_cnt, MSK_INC modulo keeps accesses in-bounds) - Low, device-trust hardening. Hardware-gated (no Marvell device on the guest); one-line range-check fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3808 Β· 5 files
FileTypeDescriptionSize
VERDICT.md β€” 3.4 KB ↓ raw
README.md β€” 974 B ↓ raw
env.txt β€” 376 B view raw
fix.diff β€” 640 B view raw
verdict.json β€” 3.6 KB view raw

DF-3808 β€” analysis-only pack (no runnable PoC)

Structural source finding: msk_txeof() consumes the device-supplied TX done index (11-bit status-LE fields / 16-bit STAT_TXAx_RIDX register, 0..4095 possible) with no MSK_TX_RING_CNT (256) range check. A bogus index force-drains the in-flight TX window: unloads dmamaps and frees mbufs the NIC may still own (HW_OWNER), desyncs cons/prod against the NIC's prefetch unit. Walk stays in-bounds (bounded by msk_tx_cnt), so impact is premature completion / stale-data-on-wire potential, not OOB.

No PoC was run: the trigger requires a faulty/hostile Yukon II NIC (the index is written by the NIC's DMA, not by packet contents) and the audit guest has no Marvell hardware (vtnet0/lo0 only, no 11ab: PCI devices). fix.diff adds the range check at the top of msk_txeof(), covering both call sites (status LE at if_msk.c:3206-3217, watchdog register read at :2671-2674).

See VERDICT.md for the full path:line trace.

VERDICT.md
↓ download raw

DF-3808 β€” msk_txeof() trusts the device-supplied TX done index without ring bounds

Verdict

Source-certain structural finding; not PoC-verified (status: untested). Severity Low, confidence likely. The missing validation is provable from the source; exploitation requires the NIC (or errant DMA) to misreport the index, so the guest (no Marvell hardware) cannot exercise the path. The same device-trust class as DF-3604 (bce(4)), filed Low here because the walk stays memory-safe and the impact is premature completion rather than out-of-bounds access.

Root cause (path:line)

msk_txeof() (sys/dev/netif/msk/if_msk.c:2884-2927):

2898:   cons = sc_if->msk_cdata.msk_tx_cons;
2900:   for (; cons != idx; MSK_INC(cons, MSK_TX_RING_CNT)) {
2901:       if (sc_if->msk_cdata.msk_tx_cnt <= 0)
2902:           break;
...
2906:       sc_if->msk_cdata.msk_tx_cnt--;
...
2910:       bus_dmamap_unload(...);
2915:       m_freem(txd->tx_m);
2916:       txd->tx_m = NULL;
2917:   }

idx is device-supplied at both call sites:

  • mskc_handle_events() OP_TXINDEXLE: status & STLE_TXA1_MSKL (if_msk.c:3208-3209) β€” STLE_TXA1_MSKL = 0x00000fff (if_mskreg.h:2198) β†’ 0..4095, and for port B ((status & 0xff000000) >> 24) | ((len & 0xf) << 8) β†’ 0..4095 (if_msk.c:3211-3216), while the ring holds MSK_TX_RING_CNT = 256 entries (if_mskreg.h:2309).
  • msk_watchdog(): idx = CSR_READ_2(sc_if->msk_softc, STAT_TXA1_RIDX) (if_msk.c:2671-2674) β€” a 16-bit hardware register.

There is no range check against MSK_TX_RING_CNT. For an idx >= 256 (or an idx behind cons), cons (wrapped mod 256) can never equal idx; the only terminator is the msk_tx_cnt <= 0 break. Consequences:

  1. The walk drains all in-flight descriptors (≀ msk_tx_cnt ≀ 256 iterations β€” bounded, in-bounds array access via MSK_INC's modulo, so no OOB), calling bus_dmamap_unload() and m_freem() on mbufs whose TX LEs still carry HW_OWNER β€” the NIC may still be DMA-reading those clusters for transmission β†’ transmission of stale/freed kernel memory onto the wire and dmamap state desync.
  2. After the forced drain, msk_tx_cons is advanced to msk_tx_prod while the NIC's prefetch unit still walks the old LEs; subsequent msk_encap() calls overwrite hardware-owned descriptors.

The walk itself stays memory-safe (every visited descriptor is a live, in-flight one, and every EOP descriptor in that window has a non-NULL tx_m set by msk_encap at :2575-2579; m_freem(NULL) is also guarded, sys/kern/uipc_mbuf.c). Hence Low, not Medium.

Threat model

Requires a faulty or compromised Yukon II (or PCIe-level DMA corruption) to write a bogus TX-index into the status LE / STAT_TXAx_RIDX register β€” not reachable from packet contents alone. Same trust boundary as DF-3604/DF-3807; defense-in-depth hardening.

Fix

fix.diff: range-check idx at the top of msk_txeof() (covers both call sites) and ignore out-of-range values.

Kernel references

sys/dev/netif/msk/if_msk.c:2884-2927 (:2900-2902, :2906, :2910-2916), :2671-2674, :3206-3217; sys/dev/netif/msk/if_mskreg.h:2198-2210, :2309.

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 adds the one-line range check at the top of msk_txeof(), covering both call sites (status LE and watchdog register). Not validated at runtime: requires Yukon hardware to exercise msk_txeof; the change is trivially local and cannot affect the valid-index path.

fix.diff
↓ fix.diffper-fix-DF-3808

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full path:line trace incl. the 0..4095 vs 256 arithmetic and the bounded-drain proof); fix.diff (range check, git apply --check clean); env.txt (no Marvell hardware on guest; untested rationale)

PoC changes

No PoC authored: hardware-gated device-trust finding (Low severity). Analysis-only pack.

Verified recommended fix

Range-check the device-supplied done index at the top of msk_txeof(): reject idx < 0 || idx >= MSK_TX_RING_CNT (see fix.diff).

Verdict

Source-certain structural finding, not PoC-verified (untested): the trigger input is written by the NIC (status-LE TX-index fields / STAT_TXAx_RIDX register), not by packet contents, so it cannot be exercised on the audit guest (no Marvell hardware; NO_MARVELL_PCI, vtnet0 lo0 only; src md5 == audited tree). msk_txeof (sys/dev/netif/msk/if_msk.c:2884-2927) consumes the device-supplied done index with NO range validation: mskc_handle_events feeds it 'status & STLE_TXA1_MSKL' and the port-B composite (if_msk.c:3206-3217) - 11-bit values, 0..4095 - and msk_watchdog feeds a raw 16-bit CSR read (if_msk.c:2671-2674), while the ring holds MSK_TX_RING_CNT = 256 entries (if_mskreg.h:2309). For an idx that can never equal cons (>= 256, or behind cons), the walk terminates only via the msk_tx_cnt <= 0 break (:2901-2902) and therefore force-completes the ENTIRE in-flight window: bus_dmamap_unload + m_freem on mbufs whose TX LEs still carry HW_OWNER (:2910-2916) - the NIC may still be DMA-reading those clusters (stale/freed kernel memory transmitted on the wire, dmamap desync) - and leaves msk_tx_cons advanced to msk_tx_prod while the NIC prefetch unit still walks the old LEs (subsequent msk_encap overwrites hardware-owned descriptors). Memory-safe by construction: the walk is bounded by msk_tx_cnt (<= 256) and MSK_INC's modulo keeps all array access in-bounds; every visited EOP descriptor has a non-NULL tx_m (set at :2575-2579) and m_freem(NULL) is guarded (sys/kern/uipc_mbuf.c) - hence Low severity, device-trust/defense-in-depth class (same trust boundary as DF-3604/DF-3807). fix.diff adds a one-line range check at the top of msk_txeof(), covering both call sites.