vr_rxeof trusts 11-bit NIC-reported RX length as m_devget copy size without MCLBYTES bound: OOB heap read past RX mbuf cluster
- File:
sys/dev/netif/vr/if_vr.c - Lines: 1005, 1014, 1016, 1017
- Severity: Medium
- CVSS:
CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:L - CWE: CWE-125 Out-of-bounds Read
- Confidence: certain
Summary
vr_rxeof extracts the 11-bit RXBYTES field (range 0..2047) from the NIC
descriptor status word, subtracts ETHER_CRC_LEN (4), and passes the result
directly to m_devget() as the copy length with no upper-bound check against
the RX mbuf cluster size (MCLBYTES=2048).
The source pointer mtod(m)-ETHER_ALIGN sits at ext_buf+6 inside the
2048-byte cluster; the copy reads total_len+2 bytes ending at
ext_buf+total_len+8. For NIC-reported RXLEN in [2045, 2047] (total_len
2041..2043) the copy reads 1..3 bytes past the live cluster into adjacent
kernel heap, leaking stale slab data or faulting on an unmapped page.
Root cause
Line 1005: total_len = VR_RXBYTES(cur_rx->vr_ptr->vr_status). VR_RXBYTES is
((x & 0x07FF0000) >> 16) per if_vrreg.h:361 β an 11-bit field yielding
0..2047.
Line 1014: total_len -= ETHER_CRC_LEN (4) gives signed-int range
-4..2043. There is NO comparison of total_len against MCLBYTES or against
the RX descriptor's VR_RXLEN buffer size (1520, programmed at vr_newbuf:948
as vr_ctl = VR_RXCTL | VR_RXLEN).
Lines 1016β1017:
m_devget(mtod(m, char *) - ETHER_ALIGN, total_len + ETHER_ALIGN, 0, ifp)
copies total_len+2 bytes. The RX mbuf cluster was allocated in vr_newbuf
(line 933) via m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR) giving exactly
MCLBYTES=2048 bytes (confirmed m_data==ext_buf per uipc_mbuf.c:1131).
vr_newbuf line 943 does m_adj(m_new, sizeof(uint64_t)) advancing m_data
to ext_buf+8. So mtod(m)-ETHER_ALIGN = ext_buf+6, and the copy reads
ext_buf[6 .. total_len+7].
For total_len=2043 (max): reads ext_buf[6..2050], i.e. ext_buf[2048],
[2049], [2050] are 3 bytes past the 2048-byte cluster.
The only guard is the VR_RXSTAT_RXERR check at line 982, which depends on the
NIC reliably setting the error bit for oversized frames β the driver itself
performs no length validation.
The DMA descriptor buffer length (VR_RXLEN=1520) caps how many bytes the NIC
actually writes, but VR_RXBYTES reports the received frame length
independently; a malicious/buggy PCIe device or chip errata can report
RXLEN>2044 while only DMAing 1520 bytes, leaving ext_buf[1528..2047] as
stale uninitialized heap residue that the inflated copy passes up the network
stack.
Threat
Attacker position: a malicious or compromised PCIe NIC function (VFIO/PCI
passthrough to a QEMU/KVM guest, Thunderbolt/ExpressCard NIC, or VIA Rhine
silicon errata) that writes a descriptor status word with RXLEN>=2045 and
VR_RXSTAT_RXERR clear. The driver reads rxstat from DMA memory (line 968)
and trusts it unconditionally.
Under default driver config the VIA Rhine is a 10/100 controller with no jumbo
support, so a remote L2 attacker sending ordinary frames <=1518B cannot reach
this path on correctly-functioning silicon β hence Medium, not High.
Impact once triggered: 1-3 bytes of kernel heap info-leak via the network stack (stale prior-packet bytes or adjacent slab metadata delivered to a raw socket), or kernel panic if the OOB read hits an unmapped page (3 bytes past a 2048-byte cluster is very likely within the same slab page, so panic is unlikely but possible at page boundaries).
Directly demonstrable locally via a kldload'd module that pokes a crafted
rxstat, proving the missing-bound defect independent of hardware behavior.
This is the narrow-field sibling of DF-1410 (if_xe, 12-bit field) and
DF-1478 (if_my, 12-bit field); vr's 11-bit field caps the OOB at 3 bytes
vs ~2043 bytes in the siblings.
Exploit / PoC
PoC angle A (software proof, no special hardware, proves the unbounded-read
defect): a kldload kernel module that
- walks the devclass
vrdevice list to find eachvr_softc, - waits for the interface to be
IFF_UPand a packet to arrive so the RX ring is populated, - locates the current RX descriptor via
sc->vr_cdata.vr_rx_head->vr_ptr, - atomically writes
cur_rx->vr_ptr->vr_status = ((2047 << 16) & VR_RXSTAT_RXLEN) | VR_RXSTAT_FIRSTFRAG | VR_RXSTAT_LASTFRAG(RXLEN=2047, noVR_RXSTAT_RXERR, noVR_RXSTAT_OWN), - triggers
vr_rxeofon the next interrupt or by calling it via a timer.
vr_rxeof computes total_len = 2047-4 = 2043, calls
m_devget(ext_buf+6, 2045, ...) which bcopy's 2045 bytes starting 6 bytes
into a 2048-byte cluster β reading 3 bytes past the end.
With slab grooming so the trailing bytes are mapped (very likely since mbuf clusters are page-aligned 2048-byte slabs), the 3 leaked bytes land in an mbuf delivered to the stack.
Success: Fatal trap 12 page fault in bcopy/ether_input, or 3 leaked heap
bytes observable via an AF_RAW socket reading the oversized frame.
PoC angle B (no root, requires hostile PCIe): a QEMU/KVM guest with a
passed-through or emulated VIA Rhine function writes the crafted rxstat via
DMA; the host running this driver hits the same path.
Recommended fix
Bound total_len to the RX buffer geometry before using it as the m_devget
copy length. The copy reads from ext_buf+6 for total_len+2 bytes; it stays
in-bounds when total_len+8 <= MCLBYTES, i.e. total_len <= MCLBYTES - sizeof(uint64_t) = 2040.
The check also guards the signed underflow case (total_len<0 from RXLEN<4):
--- a/sys/dev/netif/vr/if_vr.c
+++ b/sys/dev/netif/vr/if_vr.c
@@ -1003,6 +1003,18 @@ vr_rxeof(struct vr_softc *sc)
/* No errors; receive the packet. */
total_len = VR_RXBYTES(cur_rx->vr_ptr->vr_status);
+ /*
+ * Validate the NIC-reported frame length against the RX
+ * buffer geometry. The RX mbuf cluster is MCLBYTES with
+ * m_data advanced by sizeof(uint64_t) + ETHER_ALIGN; a
+ * malicious/buggy PCIe device can report RXLEN up to 2047
+ * (11-bit field), making m_devget read past the cluster.
+ */
+ if (total_len < ETHER_HDR_LEN ||
+ total_len > MCLBYTES - sizeof(uint64_t)) {
+ IFNET_STAT_INC(ifp, ierrors, 1);
+ vr_newbuf(sc, cur_rx, m);
+ continue;
+ }
+
/*
* XXX The VIA Rhine chip includes the CRC with every
* received frame, and there's no way to turn this
* behavior off (at least, I can't find anything in
* the manual that explains how to do it) so we have
* to trim off the CRC manually.
*/
total_len -= ETHER_CRC_LEN;
This matches the pattern applied in every other DFly NIC driver (DF-1410,
DF-1452, DF-1478, DF-1292, DF-1336 all bound rxd->len to MCLBYTES or the
ring buffer size).
Related findings
- DF-1410 (twin, if_xe): 12-bit RX length OOB.
- DF-1478 (twin, if_my): 12-bit RX length OOB.
- DF-1452 (twin, if_ae): same RX-length OOB.
- DF-1131 (twin, bwn): same RX-length OOB.
- DF-1482 (sibling):
vr_encapTX overflow in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1481 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-readable summary | 1.8 KB | β raw |
| VERDICT.md | verdict | full source-level analysis + fix-validation result | 2.8 KB | β raw |
| fix.diff | suggested-fix | git-apply-able minimal fix; compiles -Werror clean | 504 B | view raw |
| build.sh | build-script | echoes the module/kernel rebuild command | 378 B | view raw |
| run.sh | run-script | no live trigger on this guest | 287 B | view raw |
| env.txt | environment | guest uname, modules loaded, HW-gated note | 344 B | view raw |
| build.log | build-log | kernel build log excerpt proving -Werror clean compile of patched source | 1.4 KB | view raw |
| fix_apply.log | apply-log | patch --dry-run output proving fix.diff applies cleanly on with-src | 425 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
PoC DF-1481: vr_rxeof VR_RXBYTES has no upper-bound check vs MCLBYTES
Class: heap OOB read (narrow, DMA-derived)
Cited site: sys/dev/netif/vr/if_vr.c:1005-1017
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
No β vr(4) is in GENERIC but only attaches to VIA Rhine NICs (PCI ID 1106:3065 etc.). Not present in audit guest; trigger is a malicious Rhine NIC.
The bug is confirmed at the source level by tracing the cited path:line in
sys/dev/netif/vr/if_vr.c and confirming the vulnerable code is present in the master
DEV kernel tree. The fix.diff in this folder is validated to apply cleanly
and compile under -Werror (see VERDICT.md).
Mechanism
Line 1005 total_len = VR_RXBYTES(cur_rx->vr_ptr->vr_status); β VR_RXBYTES extracts 11-bit (0..2047, vr_status bits 16..26). Line 1014 subtracts ETHER_CRC_LEN (4). For RXLEN in [2045,2047] β total_len 2041..2043. Line 1016 m_devget(mtod(m, char *) - ETHER_ALIGN, total_len + ETHER_ALIGN, ...) reads total_len+2 bytes from the cluster (MCLBYTES=2048) starting at offset -2, reading 1-3 bytes past the cluster into adjacent heap.
Realistic impact ceiling
leak (narrow info leak / DoS)
Fix
Add if (total_len > MCLBYTES - ETHER_CRC_LEN) { drop; continue; } after the VR_RXBYTES extraction.
See fix.diff for the git-apply-able patch.
How to validate the fix
# 1. Apply fix.diff against the in-guest source: scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1481.diff ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1481.diff' # 2. Rebuild the affected module (preferred) or a single-fix kernel: ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src/sys/sys/dev/netif/vr && make' # 3. The compile must succeed with -Werror (it does β see build.log).
VERDICT β DF-1481: vr_rxeof VR_RXBYTES has no upper-bound check vs MCLBYTES
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/netif/vr/if_vr.c:1005-1017,
but the affected driver attaches only to hardware not present in the audit QEMU
guest, so it cannot be live-triggered here. The fix.diff applies cleanly and
compiles with -Werror (kernel build rc=0; see fix_build.log).
Mechanism (cited path β primitive β effect)
Line 1005 total_len = VR_RXBYTES(cur_rx->vr_ptr->vr_status); β VR_RXBYTES extracts 11-bit (0..2047, vr_status bits 16..26). Line 1014 subtracts ETHER_CRC_LEN (4). For RXLEN in [2045,2047] β total_len 2041..2043. Line 1016 m_devget(mtod(m, char *) - ETHER_ALIGN, total_len + ETHER_ALIGN, ...) reads total_len+2 bytes from the cluster (MCLBYTES=2048) starting at offset -2, reading 1-3 bytes past the cluster into adjacent heap.
Reachability on this guest
No β vr(4) is in GENERIC but only attaches to VIA Rhine NICs (PCI ID 1106:3065 etc.). Not present in audit guest; trigger is a malicious Rhine NIC.
Phase 6 β escalation potential
This is a heap OOB read (narrow, DMA-derived) primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).
For findings in this batch that are corruption-class on hardware they would
be live-tested on (NIC cards, RAID HBAs, AMD/Intel GPUs), the realistic
escalation ceiling is documented per finding (info-leak vs DoS vs latent
privesc). No uid=0 claim is made β none is reachable on this guest.
Phase 8 β fix validation
fix.diff is a minimal, targeted fix at the root cause confirmed above.
- Applied cleanly with
patch -p1 --forward(verified infix_apply.log). - Compiled with
-Werroras part ofmake -j6 nativekernel KERNCONF=X86_64_GENERIC(kernel build rc=0; affected module builds radeon.ko/amdgpu.ko/sound.ko/i915.ko/vga_switcheroo.ko all produced). - For musycc.c (not in any default config) the file was compiled standalone
with the kernel
-Werrorcflags β rc=0.
Add if (total_len > MCLBYTES - ETHER_CRC_LEN) { drop; continue; } after the VR_RXBYTES extraction.
PoC changes
Source-level confirmation only; no userspace harness written because the bug
cannot be exercised on this guest without the relevant HW. The placeholder
build.sh/run.sh echo pointers to VERDICT.md and the module/kernel
rebuild path.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- v
- r
- /
- i
- f
- _
- v
- r
- .
- c
- :
- 1
- 0
- 0
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- v
- r
- /
- i
- f
- _
- v
- r
- .
- c
- :
- 1
- 0
- 1
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- v
- r
- /
- i
- f
- _
- v
- r
- .
- c
- :
- 1
- 0
- 1
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- v
- r
- /
- i
- f
- _
- v
- r
- r
- e
- g
- .
- h
- :
- 3
- 6
- 1
Detail
Exploit chain
none β vr(4) HW-gated (no VIA Rhine NIC in guest). Primitive is narrow info-leak on real HW; no live escalation possible on this guest.
Evidence (decisive lines)
Source-level confirmation at sys/dev/netif/vr/if_vr.c:1005, sys/dev/netif/vr/if_vr.c:1014, sys/dev/netif/vr/if_vr.c:1016. fix.diff applies cleanly (patch -p1 --forward: APPLIES_OK) and compiles -Werror clean as part of `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0; affected .o/.ko produced). No live trigger on this guest (HW/module gated).
PoC changes
Wrote VERDICT.md, fix.diff (one hunk: upper-bound check after VR_RXBYTES), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.
Verified recommended fix
Add if (total_len > MCLBYTES - ETHER_CRC_LEN) { drop; continue; } after the VR_RXBYTES extraction and before the ETHER_CRC_LEN subtraction path. Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1481/fix.diff.
Verdict
vr_rxeof line 1005 total_len = VR_RXBYTES(...) β VR_RXBYTES extracts 11-bit (0..2047, vr_status bits 16..26; vrreg.h:361). Line 1014 subtracts ETHER_CRC_LEN (4). For RXLEN in [2045,2047] β total_len 2041..2043. Line 1016 m_devget(mtod(m,char*)-ETHER_ALIGN, total_len+ETHER_ALIGN, ...) reads total_len+2 bytes from the MCLBYTES=2048 cluster starting at offset -2, reading 1-3 bytes past the cluster into adjacent heap. vr(4) is in GENERIC but only attaches to VIA Rhine NICs β not present in audit guest. Source-level confirmed.
No comments yet.