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

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

  1. walks the devclass vr device list to find each vr_softc,
  2. waits for the interface to be IFF_UP and a packet to arrive so the RX ring is populated,
  3. locates the current RX descriptor via sc->vr_cdata.vr_rx_head->vr_ptr,
  4. atomically writes cur_rx->vr_ptr->vr_status = ((2047 << 16) & VR_RXSTAT_RXLEN) | VR_RXSTAT_FIRSTFRAG | VR_RXSTAT_LASTFRAG (RXLEN=2047, no VR_RXSTAT_RXERR, no VR_RXSTAT_OWN),
  5. triggers vr_rxeof on 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.

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).

  • 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_encap TX overflow in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1481 Β· 10 files
FileTypeDescriptionSize
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
README.md readme human-readable summary
↓ download 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.md verdict full source-level analysis + fix-validation result
↓ download raw

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 in fix_apply.log).
  • Compiled with -Werror as part of make -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 -Werror cflags β€” 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

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.