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

ix_rxeof trusts NIC-DMA'd descriptor length with no rx_mbuf_sz bound: unbounded m_len/m_pkthdr.len β†’ heap OOB read up to 63,487 bytes past the RX cluster (DF-3604/3639/3663 family)

Summary

ix_rxeof() reads the NIC-written wb.upper.length (16-bit) and publishes it directly as m_len and m_pkthdr.len with no bound against the cluster actually posted (rx_mbuf_sz = MCLBYTES 2048 or MJUMPAGESIZE 4096). A malicious/faulty NIC DMA-writing 0xFFFF yields an mbuf claiming 65535 bytes inside a 2048-byte cluster; every downstream consumer (ether/ip parsing, BPF, forwarding) reads adjacent kernel heap - up to 63,487 bytes per descriptor, compounded by the m_pkthdr.len accumulator for multi-descriptor frames. The driver permits MTU 9710 yet only ever posts 4096-byte clusters, so no size configuration makes the trust safe. Same trust boundary and severity rationale as DF-3604 (bce), DF-3639 (bnx), DF-3663 (bge). VERIFIED: userspace bound-math harness with poisoned redzones on the guest - desc len=0xffff -> m_len=65535 in a 2048 cluster -> OOB span 63487 bytes. Hardware-gated (no Intel 10GbE device): honest inconclusive, source-certain.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3667 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
df3667_sim.c β€” 3.6 KB view raw
run.log β€” 438 B view raw
build.log β€” 15.8 KB 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
fix.diff β€” 776 B view raw
manifest.json β€” 872 B view raw
verdict.json β€” 2.9 KB view raw

DF-3667 β€” ix(4) ix_rxeof trusts NIC-DMA'd descriptor length: unbounded m_len β†’ heap OOB read

File: sys/dev/netif/ix/if_ix.c Β· Severity: Medium Β· Class: RX-descriptor length trust (DF-3604/DF-3639/DF-3663 family)

Build / Run (bound-math demonstration harness)

cc -O2 -Wall -o df3667_sim df3667_sim.c
./df3667_sim

The harness is a userspace simulation (clearly labeled in source) replicating the exact assignment chain of ix_rxeof() with the real cluster sizes. It is evidence for the bound arithmetic, NOT a kernel reproduction β€” the guest has no ixgbe NIC, so the kernel path cannot run there.

Expected output

DF-3667 ix_rxeof descriptor-length trust β€” bound math
cluster(MCLBYTES)=2048      desc len=0xffff  claimed m_len=65535  OOB read span=63487 bytes
cluster(MJUMPAGESIZE)=4096  desc len=0xffff  claimed m_len=65535  OOB read span=61439 bytes
redzone bytes a consumer would hand to the stack (2K cluster): 63487
m_pkthdr.len accumulator (single desc): 65535  (chain adds per desc, if_ix.c:3044)
modest lie 3000 -> 2048 cluster: OOB span=952

Fix validation (compile-level)

fix.diff was applied to the guest's /usr/src (byte-identical to the audit tree, md5-verified) and the driver built as a module with -Werror:

cd /usr/src/sys/dev/netif/ix && make obj && make     # baseline: build_baseline.log
patch -p1 < fix.diff (from /usr/src)                 # patched: build_patched.log
cd /usr/src/sys/dev/netif/ix && make clean && make   # -> if_ix.ko, 0 errors/0 warnings

Runtime behavioral validation is impossible on this guest (no 10GbE hardware).

Guest state

env.txt β€” ix(4) compiled into the running X86_64_GENERIC kernel (config line 217), no Intel 82598/X540/X550 PCI device present, dev.ix.* sysctl count 0. Verdict: inconclusive (hardware absent), source-level certain.

VERDICT.md
↓ download raw

DF-3667 β€” VERDICT

Status: inconclusive (runtime) / source-level certain. Impact class: kernel info leak (heap OOB read) via malicious-NIC DMA. No guest reproduction possible β€” no ixgbe hardware.

Root cause (path:line into the audited tree)

ix_rxeof() reads the NIC-written RX completion descriptor and publishes its 16-bit length into the mbuf with no bound against the cluster actually posted for that descriptor:

The backing cluster is allocated at:

  • sys/dev/netif/ix/if_ix.c:4142 β€” m_getjcl(flags, MT_DATA, M_PKTHDR, rxr->rx_mbuf_sz);
  • sys/dev/netif/ix/if_ix.c:2635-2638 β€” rx_mbuf_sz = MCLBYTES (2048) when max_frame_size <= MCLBYTES, else MJUMPAGESIZE (4096). Note: SIOCSIFMTU allows MTU up to IX_MAX_MTU = 9710 (if_ix.h:101), so 4096 is the largest cluster ever posted β€” there is no MJUM9BYTES/MJUM16BYTES branch in this driver.

There is no len > rx_mbuf_sz (or len - ETHER_CRC_LEN) guard anywhere in ix_rxeof β€” contrast the bge/bnx/bce family fixes from this same audit (DF-3663:3138, DF-3639:2848, DF-3604:4396).

Threat model

The descriptor write-back is produced by the NIC via DMA. A compromised or malicious peripheral (Thunderclap-style DMA attack, malicious FW/NVM on the card, or a buggy NIC erratum writing an inconsistent wb.upper.length) fully controls len. Nothing on the host validates it. mp->m_len = len then hands the network stack an mbuf claiming up to 65535 valid bytes inside a 2048/4096 byte cluster; every downstream consumer (ether_demux, ip_input, BPF taps, forwarding, m_copydata-style parsers) reads kernel heap beyond the cluster: up to 63,487 bytes of adjacent kernel heap per descriptor are exposed to parsing/leak paths, and m_pkthdr.len accumulation (3044) compounds it for multi-descriptor frames.

This is not remotely triggerable by a packet alone β€” the NIC must misreport the length; the NIC is the trust boundary being violated. Same treatment as the bge/bnx/bce family findings (Medium, certain, kernleak).

What was verified, and how

  1. Source-level certainty: full read of ix_rxeof() (if_ix.c:2948-3100) β€” no bound exists between the length read (2974) and the mbuf publication (3031/3040/3044). Cluster sizing confirmed at 2635-2638/4142 and sys/sys/param.h:473,497 (MCLBYTES=2048, MJUMPAGESIZE=4096).
  2. Bound-math demonstration (userspace, guest): df3667_sim.c replicated the assignment chain with poisoned redzones; ran on the audit QEMU guest (run.log): desc len=0xffff -> m_len=65535 in a 2048 cluster β‡’ 63,487-byte OOB span. This is arithmetic evidence only β€” the kernel path itself did not execute.
  3. Module presence: ix(4) is compiled into the running kernel (X86_64_GENERIC line 217, env.txt), so the vulnerable code ships in the default kernel of this system; no Intel 10GbE PCI device exists on the guest (pciconf -l, dev.ix.* count 0), so ix_attach/ix_rxeof never runs.
  4. Fix validation (compile-level): fix.diff adds if (len > rxr->rx_mbuf_sz) { ierrors++; ix_rx_discard(...); goto next_desc; } after eop is computed. Applied to the guest's /usr/src copy (md5-identical to the audit tree) β†’ if_ix.ko rebuilt with -Werror, zero errors/warnings (build_patched.log vs build_baseline.log). The local sys/ tree was never modified. An earlier draft of the hunk placed the check before eop is computed and the -Werror build caught it (maybe-uninitialized) β€” final hunk placement is after the --count line.

Why not reproduced / why no exploit chain

Escalation to uid=0 requires the memory-corruption primitive to be exercisable; exercising it requires a DMA-capable adversary on the PCIe bus (or a NIC whose firmware misreports lengths). The guest has no such device and QEMU (TCG/KVM q35) has no 82599/X540 model to instantiate, so neither a leak nor a panic can be triggered at runtime. Verdict is therefore inconclusive per the honest status taxonomy (device absent), with confidence: certain on the source-level defect and its impact class.

fix.diff in this pack (bound the descriptor length to rx_mbuf_sz, discard the frame on violation). Mirrors the fixes already carried for bge/bnx/bce in this audit.

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 compile-validated on the guest against md5-identical source (if_ix.ko built with -Werror, zero errors); runtime before/after comparison impossible without ixgbe hardware.

build_baseline.log and build_patched.log (both clean); fix.diff applies with patch -p1 from /usr/src (verified PATCHED + build).
↓ fix.diffper-fix-DF-3667

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (path:line walkthrough); run.log (harness: desc len=0xffff -> OOB span 63487 for 2K cluster); build_baseline.log vs build_patched.log (module builds with the fix under -Werror); env.txt (device absent, driver built into kernel)

PoC changes

No seed existed (pass-2 new finding). Harness written from scratch; first fix.diff hunk placed the guard before eop is computed and the -Werror module build flagged maybe-uninitialized -> moved after the '--count' line.

Verified recommended fix

In ix_rxeof, discard the frame when le16toh(cur->wb.upper.length) > rxr->rx_mbuf_sz (see fix.diff).

Verdict

Source-level certain, runtime-inconclusive: ix_rxeof (sys/dev/netif/ix/if_ix.c:2974,3031,3040,3044) publishes the NIC-DMA'd 16-bit descriptor length as m_len/m_pkthdr.len with no bound against the posted cluster size (rx_mbuf_sz, MCLBYTES=2048 or MJUMPAGESIZE=4096, if_ix.c:2635-2638,4142). A malicious/faulty NIC writing wb.upper.length=0xFFFF yields an mbuf claiming 65535 bytes in a 2048-byte cluster -> up to 63,487 bytes of adjacent kernel heap read by the network stack (info leak; same family as DF-3604/3639/3663). The audit QEMU guest has no ixgbe PCI device (env.txt), so the kernel path cannot execute there; a userspace harness (df3667_sim.c, run.log) proves the bound math. fix.diff compiles clean (-Werror, build_patched.log) but runtime fix validation is impossible without hardware.