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

vge_rxeof trusts the NIC-DMA'd 14-bit RX descriptor length with no MCLBYTES clamp: single-fragment path sets m_len/pkthdr.len up to 16379 on a 2048-byte cluster (~14KB heap OOB read into if_input/BPF) and signed-underflows to -4 for BUFSIZ<4 (DF-3604/4055/4083 family)

Summary

total_len comes straight from the 14-bit BUFSIZ field of the NIC-DMA'd descriptor; the single-fragment completion assigns it unclamped into m_len/pkthdr.len against a single MCLBYTES cluster; multi-frag pkthdr.len likewise unbounded vs the chain sum. BUFSIZ=0..3 yields -4 lengths. Read-side only. Clamp + drop fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4107 Β· 4 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
verdict.json β€” 2.1 KB view raw
fix.diff β€” 887 B view raw

DF-4107 β€” vge(4): vge_rxeof trusts the NIC-DMA'd 14-bit descriptor length with no MCLBYTES clamp β€” single-fragment path sets m_len up to 16379 on a 2048-byte cluster (and signed-underflows to -4 for len<4)

File: sys/dev/netif/vge/if_vge.c (pass 2) Class: heap OOB read (CWE-125) via trusted DMA-written length β€” DF-3604 (bce) / DF-4055 (et) / DF-4083 (sis) family instance. Hardware gate: VT612x NIC (absent on the audit guest). Source-level certainty.

The defect

if_vge.c:1268: total_len = VGE_RXBYTES(cur_rx); where VGE_RXBYTES(x) = ((le32toh((x)->vge_sts) & VGE_RDSTS_BUFSIZ) >> 16) (if_vgevar.h:55-56) and VGE_RDSTS_BUFSIZ = 0x3FFF0000 (if_vgereg.h:676) β€” a 14-bit NIC-DMA-written value: 0..16383.

Single-fragment completion path (no SOF head chain), if_vge.c:1361-1363:

} else {
        m->m_pkthdr.len = m->m_len = (total_len - ETHER_CRC_LEN);
}

No comparison against MCLBYTES (2048; the RX buffer is a single mbuf cluster loaded at vge_newbuf:1131-1134). The NIC normally writes BUFSIZ ≀ cluster size, but the value is device/DMA-written: a malfunctioning or compromised NIC (PCIe DMA attack, bad FW, errata) sets 0x3FFF β†’ m_len = m_pkthdr.len = 16379 on a 2048-byte cluster β†’ up to ~14 KB heap OOB read walked by ether_input/IP and duplicated to every BPF listener.

Underflow sub-case: BUFSIZ=0 with RXOK set (hostile descriptor) gives m->m_pkthdr.len = m->m_len = -4 β€” negative length reaching if_input/BPF (DF-3604's "unsigned underflow" analog; here m_len is int, so it propagates as -4).

The multi-fragment path is partially protected by accident (m->m_len = total_len % (MCLBYTES - VGE_ETHER_ALIGN) at 1342), but m_pkthdr.len = total_len - ETHER_CRC_LEN (1360) is likewise unbounded against the sum of the chain's mbuf lengths β€” a lying BUFSIZ desyncs pkthdr.len from the chain, tripping later pullup/copy code.

Build / Run

Hardware-gated; guest has virtio only. See VERDICT.md.

Fix

Clamp before use:

if (total_len > MCLBYTES - VGE_ETHER_ALIGN)
        total_len = MCLBYTES - VGE_ETHER_ALIGN;   /* or drop the frame */

(plus drop on total_len <= ETHER_CRC_LEN). See fix.diff.

VERDICT.md
↓ download raw

DF-4107 VERDICT

Status: untested (hardware-gated β€” no VT612x NIC on the audit guest; source-level certainty certain) Impact: heap OOB read of up to ~14 KB per hostile descriptor handed to the network stack and BPF; negative-length (-4) mbuf on BUFSIZ=0. Read-side primitive only β€” no write.

Trace

  • Descriptor status is DMA-written by the NIC; the driver never re-derives the length from its own bookkeeping:
  • if_vge.c:1268 total_len = VGE_RXBYTES(cur_rx);
  • if_vgevar.h:55-56 VGE_RXBYTES(x) = ((le32toh((x)->vge_sts) & VGE_RDSTS_BUFSIZ) >> 16)
  • if_vgereg.h:676 VGE_RDSTS_BUFSIZ 0x3FFF0000 β†’ range 0..16383.
  • Single-frag EOF path: if_vge.c:1361-1363 assigns m->m_pkthdr.len = m->m_len = total_len - ETHER_CRC_LEN with no MCLBYTES clamp. The mbuf is a single 2048-byte cluster (vge_newbuf:1111-1134, m_getcl + load with maxsize MCLBYTES at if_vge.c:765-768).
  • BUFSIZ=0x3FFF β†’ m_len = 16379 β†’ 14331 bytes past the cluster end are parsed as packet content by ether_inputβ†’IP/TCP and copied to BPF taps β€” kernel heap disclosure / parsing of adjacent slab contents; classic DF-3604/DF-4055/DF-4083 family.
  • BUFSIZ=0..3 with RXOK (or VIDM/CSUMERR pass-through at 1308-1309) β†’ m_len = -4 (int) β†’ negative-length mbuf into if_input.
  • Multi-frag tail (1342) is modular-bounded by luck, but pkthdr.len at 1360 is not bounded against the chain sum.

Preconditions / threat model

A DMA-written descriptor length is trusted. Attack vectors: (a) malicious or mis-programmed NIC (Thunderbolt/PCIe peripheral with DMA), (b) NIC firmware errata, (c) any kernel bug that scribbles descriptor memory. This is exactly the posture the sibling findings (DF-3604 Medium, DF-4055 Medium, DF-4083 Medium) were filed under β€” consistent Medium.

Not DF-1381/DF-1382: those cover the newbuf chunk-decrement OOB write and the SIOCSIFMTU clamp respectively; this is the rxeof length consumer.

Why untested

Guest exposes no VIA NIC (pciconf -l: virtio 1af4:1000 only; ifconfig: vtnet0 lo0). No in-guest path executes vge_rxeof. Source trace is line-exact; the clamp-fix is mechanical.

Fix

Bound total_len to the actual buffer size before it reaches m_len / pkthdr.len (and drop frames with total_len <= ETHER_CRC_LEN). fix.diff.

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 MCLBYTES clamp + degenerate-length drop at the top of the descriptor loop; not exercisable on the guest (driver cannot attach without the NIC).

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

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md trace with the three macro cites; single-cluster allocation proof at vge_newbuf:1111-1134 and tag maxsize MCLBYTES at if_vge.c:765-768.

PoC changes

No runnable PoC (no VT612x hardware on guest); pack is the trace + clamp fix.

Verified recommended fix

Clamp total_len to MCLBYTES - VGE_ETHER_ALIGN and drop frames with total_len <= ETHER_CRC_LEN before touching m_len/pkthdr.len (fix.diff).

Verdict

Source-level certain: vge_rxeof takes total_len from the DMA-written 14-bit VGE_RDSTS_BUFSIZ field (if_vge.c:1268, if_vgevar.h:55-56, if_vgereg.h:676) and the single-fragment completion path assigns it straight into m->m_pkthdr.len/m->m_len (if_vge.c:1361-1363) with no MCLBYTES bound - up to 16379 on a 2048-byte cluster (~14KB heap OOB read into if_input/BPF; DF-3604/DF-4055/DF-4083 family) and a -4 signed length when BUFSIZ<4. Multi-frag pkthdr.len (1360) is likewise unbounded vs the chain. Untestable on the audit guest (virtio NIC only, no VIA 1106:3053).