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)
PoC verification
Evidence pack
findings/poc/DF-3667 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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:
sys/dev/netif/ix/if_ix.c:2974βlen = le16toh(cur->wb.upper.length);(wb.upper.lengthis DMA'd by the NIC into the descriptor ring.)sys/dev/netif/ix/if_ix.c:3031βmp->m_len = len;sys/dev/netif/ix/if_ix.c:3040βmp->m_pkthdr.len = len;sys/dev/netif/ix/if_ix.c:3044βrxbuf->fmp->m_pkthdr.len += len;(chain)
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)whenmax_frame_size <= MCLBYTES, elseMJUMPAGESIZE (4096). Note: SIOCSIFMTU allows MTU up toIX_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
- 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 andsys/sys/param.h:473,497(MCLBYTES=2048, MJUMPAGESIZE=4096). - Bound-math demonstration (userspace, guest):
df3667_sim.creplicated the assignment chain with poisoned redzones; ran on the audit QEMU guest (run.log):desc len=0xffff -> m_len=65535in a 2048 cluster β 63,487-byte OOB span. This is arithmetic evidence only β the kernel path itself did not execute. - 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), soix_attach/ix_rxeofnever runs. - Fix validation (compile-level):
fix.diffaddsif (len > rxr->rx_mbuf_sz) { ierrors++; ix_rx_discard(...); goto next_desc; }aftereopis computed. Applied to the guest's/usr/srccopy (md5-identical to the audit tree) βif_ix.korebuilt with-Werror, zero errors/warnings (build_patched.logvsbuild_baseline.log). The localsys/tree was never modified. An earlier draft of the hunk placed the check beforeeopis computed and the-Werrorbuild caught it (maybe-uninitialized) β final hunk placement is after the--countline.
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.
Recommended fix
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_testablefix.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).
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.
No comments yet.