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)
PoC verification
Evidence pack
findings/poc/DF-4107 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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_LENwith 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_testablefix.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
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).
No comments yet.