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

em_rxeof trusts the 16-bit NIC descriptor length with no posted-buffer bound: unbounded m_len/m_pkthdr.len -> heap OOB read up to ~63.5KB past the 2048-byte RX cluster (positive direction of DF-1188)

Summary

em_rxeof() reads the 16-bit length field from the device-DMA'd legacy RX descriptor and publishes it without any bound against the posted buffer (rx_buffer_len = MCLBYTES = 2048): non-EOP fragments get len = desc_len, EOP fragments len = desc_len - ETHER_CRC_LEN, assigned straight to m_len and accumulated into m_pkthdr.len. A device-side write-back of 0xFFFF yields an mbuf claiming 65535 bytes inside a 2048-byte cluster - every downstream consumer (ether parsing, m_pullup, BPF taps, forwarding, m_copydata) reads up to 63487 bytes of adjacent kernel heap per descriptor, compounded by the pkthdr accumulator. The error-path probe last_byte = *(mtod+len-1) reads up to cluster+65534 (positive direction; the ==0 direction is the known DF-1188). Additionally prev_len_adj can subtract up to 4 from a previous fragment whose m_len < 4, injecting a NEGATIVE m_len into the chain. Same premise as the accepted family DF-3604/3639/3663/3667/3679. VERIFIED: guest-executed bound-math harness demonstrates all three variants.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3713 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
build.sh β€” 113 B view raw
run.sh β€” 153 B view raw
build.log β€” 11 B view raw
run.log β€” 2.5 KB view raw
env.txt β€” 634 B view raw
fix.diff β€” 847 B view raw
verdict.json β€” 2.8 KB view raw
em_bounds_harness.c β€” 7.5 KB view raw

DF-3713 β€” em_rxeof trusts the 16-bit NIC descriptor length with no posted-buffer bound

File: sys/dev/netif/em/if_em.c Lines: 3692 (desc_len = le16toh(current_desc->length)), 3694-3706 (len math), 3715 (error-path probe), 3738 (mp->m_len = len), 3761 (fmp->m_pkthdr.len += len) Family: DF-3604 (bce), DF-3639 (bnx), DF-3663 (bge), DF-3667 (ix), DF-3679 (igb) β€” this is the em(4) member of the unbounded-RX-length family. The known em finding DF-1188 covers only desc_len == 0 on an error descriptor (negative-direction probe); this finding is the positive direction (desc_len > posted buffer).

Reproduce on a box with real em(4) hardware

Device-side trigger (requires a faulty/malicious NIC DMA source β€” descriptor corruption, erratum, SR-IOV/emulation fault):

# Post RX buffers (interface up), then DMA-write a legacy RX descriptor
# write-back with length = 0xFFFF and DD|!EOP set:
#   desc->length = 0xFFFF; desc->status = E1000_RXD_STAT_DD;
# -> em_rxeof computes len = 65535, assigns mp->m_len = 65535 on a
#    2048-byte cluster; ether_input/BPF/forwarding walk ~63.5KB of
#    adjacent kernel heap.

On this audit guest

No em hardware. run.sh performs the presence probe; run.harness.log (guest-executed userspace replica of the exact arithmetic, build.sh) demonstrates: desc_len=0xFFFF (non-EOP) β†’ m_len claims 63487 bytes past the cluster; desc_len=0xFFFF (EOP) β†’ 63483 bytes past; error-path probe at :3715 reads at mp->m_data + 65534; prev_len_adj underflow (previous fragment m_len 0, EOP desc_len 2) β†’ lmp->m_len = -2 (negative m_len injected into the RX chain).

sh build.sh && ./run.sh
VERDICT.md
↓ download raw

DF-3713 β€” VERDICT

Status: source-certain, runtime hardware-gated (inconclusive on this guest β€” no em NIC).

Root cause

em_rxeof() (sys/dev/netif/em/if_em.c:3661-3821) reads the 16-bit length field out of the legacy RX descriptor β€” a value DMA-written by the device (struct e1000_rx_desc, e1000_hw.h:410-417, __le16 length):

  • :3692 desc_len = le16toh(current_desc->length); β€” no bound against the posted buffer (adapter->rx_buffer_len, = MCLBYTES = 2048, set at :684, never changed).
  • :3705 non-EOP fragment: len = desc_len; β€” a legitimate fragment is at most 2048; nothing rejects larger values.
  • :3701 EOP fragment: len = desc_len - ETHER_CRC_LEN.
  • :3738 mp->m_len = len; and :3761 adapter->fmp->m_pkthdr.len += len; β€” the unbounded value becomes the mbuf's advertised data length and the packet's total length, handed to ifp->if_input at :3808.
  • :3715 error path: last_byte = *(mtod(mp, caddr_t) + desc_len - 1); β€” OOB probe at up to cluster + 65534 (the desc_len == 0 negative direction of this line is the already-filed DF-1188).
  • :3697-3699 + :3754-3758: if a previous non-EOP fragment had m_len < 4 (e.g. desc_len 0..3), prev_len_adj = ETHER_CRC_LEN - desc_len subtracts up to 4 from it β†’ negative m_len in the chain (lmp->m_len -= prev_len_adj with no floor).

A device-side length of 0xFFFF yields an mbuf claiming 65535/65531 bytes inside a 2048-byte cluster: every downstream consumer (ether parsing, m_pullup, BPF taps, forwarding, m_copydata) reads up to ~63.5KB past the cluster β€” per descriptor, and compounded by the m_pkthdr.len accumulator for multi-descriptor frames.

Threat

Device-trust boundary: descriptor corruption, device fault/erratum, or a malicious DMA source (PCIe device spoofing an e1000, flawed emulated e1000 β€” the default QEMU NIC model, or SR-IOV style injected writes). Not reachable by a network peer against well-behaved hardware. Same premise and severity rationale as the accepted family findings DF-3604/DF-3639/DF-3663/DF-3667 (bce/bnx/bge/ix) and DF-3679 (igb).

Verification

  • Guest has no em hardware (run.sh probe: vtnet0 lo0 only).
  • Guest-executed bound-math harness (run.harness.log): desc_len=0xFFFF non-EOP β†’ 63487-byte OOB claim; EOP β†’ 63483-byte; error-path probe OOB; prev_len_adj β†’ lmp->m_len = -2 demonstrated. Honest frames (1500) safe.

Fix

fix.diff: after reading desc_len, discard (whole frame chain) when desc_len == 0 (also closes DF-1188) or desc_len > adapter->rx_buffer_len. Cannot be hardware-validated on this guest (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_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against the read-only sys/ tree (patch --dry-run clean); cannot be validated on a guest without em hardware.

findings/poc/DF-3713/fix.diff
↓ fix.diffper-fix-DF-3713

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-3713/VERDICT.md; run.log (probe + harness section [B]/[B2]: 63487/63483-byte OOB claims, probe OOB, negative m_len -2); build.log; fix.diff (discard desc_len==0 or > rx_buffer_len, also closing DF-1188)

PoC changes

No seed PoC. em_bounds_harness.c section [B]/[B2] written for this finding; device-side descriptor-injection procedure documented in README.md.

Verified recommended fix

Discard the frame (whole fmp chain) when desc_len == 0 or desc_len > adapter->rx_buffer_len before assigning m_len.

Verdict

Source-certain, runtime hardware-gated: em_rxeof (sys/dev/netif/em/if_em.c:3661-3821) publishes the 16-bit NIC-DMA'd descriptor length with no bound against the posted buffer: desc_len at :3692, len=desc_len (:3705) or desc_len-4 (:3701) -> mp->m_len=len at :3738 and fmp->m_pkthdr.len += len at :3761. A malicious/faulty device write-back of 0xFFFF yields an mbuf claiming 65535/65531 bytes in a 2048-byte cluster -> heap OOB read of up to 63487/63483 bytes via ether_input/BPF/forwarding, compounded per descriptor by the pkthdr accumulator; the error-path probe at :3715 reads cluster+desc_len-1 (positive direction of DF-1188); prev_len_adj (:3697-3699, :3754-3758) can drive a previous fragment's m_len negative. Same family/rationale as DF-3604/DF-3639/DF-3663/DF-3667/DF-3679; DF-1188 covers only the desc_len==0 direction. Guest-executed bound-math harness demonstrates all three spans; no em hardware on the audit guest.