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

pcn_rxeof trusts 16-bit NIC-reported pcn_rxlen as m_len without MCLBYTES bound: OOB heap read past RX mbuf cluster

  • File: sys/dev/netif/pcn/if_pcn.c
  • Lines: 798, 799, 746, 742, 745
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U:C:H/I:N/A:H
  • CWE: CWE-125 Out-of-bounds Read
  • Confidence: certain

Summary

pcn_rxeof copies the 16-bit cur_rx->pcn_rxlen field (range 0..65535) out of the NIC RX descriptor, subtracts ETHER_CRC_LEN (4), and assigns the result directly to m->m_len = m->m_pkthdr.len with no upper-bound check against the RX mbuf cluster size.

The cluster (allocated in pcn_newbuf via MGETHDR+MCLGET, then m_adj'd by ETHER_ALIGN=2) only backs MCLBYTES-2 = 2046 bytes from m_data.

A malicious or buggy PCIe device (or emulated PCnet; the driver itself notes VMware's PCnet emulation is broken at lines 432–437) that writes a descriptor with pcn_rxlen > 2050 and PCN_RXSTAT_ERR clear makes the network stack walk up to ~63533 bytes past the live cluster in ifp->if_input / ether_input / bpf_mtap / m_copydata: kernel heap information leak of stale slab contents and/or page-fault panic.

Also covers the pcn_rxlen < 4 underflow into negative m_len.

Direct sibling of DF-1410 (if_xe), DF-1478 (if_my), DF-1481 (if_vr), DF-1490 (if_tx), DF-1514 (if_ste), DF-1519 (if_lge), DF-1526 (if_sf), DF-1551 (if_sn).

Root cause

pcn_rxeof at sys/dev/netif/pcn/if_pcn.c:798-799:

m->m_len = m->m_pkthdr.len =
    cur_rx->pcn_rxlen - ETHER_CRC_LEN;

cur_rx->pcn_rxlen is declared u_int16_t in struct pcn_rx_desc (if_pcnreg.h:350).

The subtraction promotes to int giving range -4..65531. There is NO comparison of this value against MCLBYTES, against PCN_RXLEN (1536, the DMA buffer size programmed to the chip at pcn_newbuf:746), or against the usable cluster space.

Buffer geometry (pcn_newbuf lines 725–748):

  • MGETHDR+MCLGET yields a cluster of exactly MCLBYTES=2048 bytes (sys/param.h:494-497, MCLSHIFT=11).
  • Line 735/738 sets m_len=MCLBYTES.
  • Line 742 m_adj(m_new, ETHER_ALIGN) advances m_data to ext_buf+2 and drops m_len to 2046.
  • Line 745 c->pcn_rbaddr = vtophys(mtod(m_new, caddr_t)) DMA-targets ext_buf+2.
  • So the chip writes into ext_buf[2..2+PCN_RXLEN-1] = ext_buf[2..1537]; the cluster ends at ext_buf[2047].

Legitimate hardware (buffer size PCN_RXLEN=1536) therefore reports pcn_rxlen<=1536, m_len<=1532, all within the cluster.

The driver relies entirely on the chip honoring the programmed buffer-size field; nothing in the driver enforces it.

A descriptor with pcn_rxlen=0xFFFF and pcn_rxstat with PCN_RXSTAT_ERR clear (e.g. only STP|ENP set, which is exactly what pcn_newbuf writes at line 748) is trusted unconditionally and yields m_len=65531 on a 2046-byte buffer.

The error-summary guard at line 779 (cur_rx->pcn_rxstat & PCN_RXSTAT_ERR) only filters packets the chip chose to flag; a descriptor written by a hostile/buggy device with ERR clear bypasses it.

There is no STP/ENP sanity check either (BUFF overflow would normally set ERR|BUFF, but again the driver does not enforce this for itself).

Threat

Attacker position: a malicious or compromised PCIe NIC function β€” VFIO/PCI passthrough of a crafted PCnet device into a DragonFlyBSD guest, a malicious Thunderbolt/USB-attached NIC, or buggy/emulated silicon (the driver's own comments at if_pcn.c:432-437 call out VMware's PCnet emulation as broken).

The attacker writes a DMA RX descriptor with pcn_rxlen>2050 and PCN_RXSTAT_ERR clear; pcn_rxeof reads that DMA memory at line 769 and trusts the length at line 798.

Under default driver config the chip is programmed to accept only <=1536-byte frames (PCN_RXLEN at pcn_newbuf:746), so a remote L2 attacker sending ordinary frames <=1518B cannot reach this path on correctly-functioning AMD Am79C97x silicon β€” hence Medium, not High.

Impact once triggered:

  • kernel heap info-leak (up to ~63533 bytes, though practically bounded by the next unmapped page) via ether_input/BPF/raw-socket delivery, recovering stale packet contents or adjacent slab pointers;
  • or kernel panic (Fatal trap 12: page fault while in kernel mode in bcopy/ether_input) when the OOB read crosses into an unmapped page.

Demonstrable locally without hardware via a kldload module that pokes a crafted descriptor and lets the next RINT interrupt run pcn_rxeof.

Exploit / PoC

Angle A (software proof; requires root to kldload but proves the unbounded-read defect with no special hardware, matching the verified sibling PoC strategy):

Build a kldload kernel module poc_pcnrx.c that

  1. walks devclass pcn (devclass_find("pcn") + devclass_get_softc) to find each struct pcn_softc,
  2. waits for IFF_UP and at least one packet to arrive so the ring is populated,
  3. locates the current RX descriptor via sc->pcn_ldata->pcn_rx_list[sc->pcn_cdata.pcn_rx_prod],
  4. atomically writes .pcn_rxlen = 0xFFFF, .pcn_rxstat = PCN_RXSTAT_STP|PCN_RXSTAT_ENP (no OWN, no ERR β€” see if_pcnreg.h:359-366),
  5. lets the next RINT interrupt (or pcn_rxeof called manually via a poked CSR) run.

pcn_rxeof computes m_len=65531 on the 2048-byte cluster and calls ifp->if_input (line 802); bpf_mtap/ether_input/m_copydata traverse ~63533 bytes past ext_buf.

Build: cc -c -DKLDLOAD -I/sys poc_pcnrx.c; ld -d -r poc_pcnrx.o; kldload ./poc_pcnrx.ko.

Success looks like Fatal trap 12: page fault while in kernel mode inside bcopy/ether_input, or β€” with slab grooming so the trailing bytes are mapped and a raw socket open on the interface β€” leaked kernel heap bytes observable in the received payload.

Angle B (no root; requires hostile PCIe): a QEMU/KVM guest with a passed-through or crafted Am79C972 function writes a DMA descriptor with the crafted pcn_rxlen/stat; the host running this driver hits the same path.

Bound the NIC-reported length to the RX buffer size before using it as m_len. The check also closes the pcn_rxlen < ETHER_CRC_LEN underflow into negative m_len.

Apply at sys/dev/netif/pcn/if_pcn.c immediately before line 798:

--- a/sys/dev/netif/pcn/if_pcn.c
+++ b/sys/dev/netif/pcn/if_pcn.c
@@ -795,6 +795,20 @@ pcn_rxeof(struct pcn_softc *sc)

        PCN_INC(i, PCN_RX_LIST_CNT);

+       /*
+        * Validate the NIC-reported frame length against the RX
+        * buffer size.  The descriptor's buffer size was programmed
+        * to PCN_RXLEN (1536) in pcn_newbuf and the cluster backs
+        * only MCLBYTES - ETHER_ALIGN = 2046 bytes from m_data.
+        * A malicious or buggy PCIe device (or emulated PCnet)
+        * can otherwise report pcn_rxlen up to 65535, making the
+        * stack walk past the 2KB mbuf cluster.  Also rejects
+        * pcn_rxlen < ETHER_CRC_LEN which would underflow m_len.
+        */
+       if (cur_rx->pcn_rxlen < ETHER_HDR_LEN + ETHER_CRC_LEN ||
+           cur_rx->pcn_rxlen > MCLBYTES - ETHER_ALIGN + ETHER_CRC_LEN) {
+           IFNET_STAT_INC(ifp, ierrors, 1);
+           pcn_newbuf(sc, i, m);
+           continue;
+       }
+
        /* No errors; receive the packet. */
        IFNET_STAT_INC(ifp, ipackets, 1);
        m->m_len = m->m_pkthdr.len =

This matches the pattern applied across the sibling NIC drivers (DF-1410/1478/1481/1490/1514/1519/1526/1551).

  • DF-1410 (twin, if_xe): 12-bit RX length OOB.
  • DF-1478 (twin, if_my): 12-bit RX length OOB.
  • DF-1481 (twin, if_vr): 11-bit RX length OOB.
  • DF-1490 (twin, if_tx): 16-bit RX length OOB.
  • DF-1514 (twin, if_ste): 13-bit RX length OOB.
  • DF-1519 (twin, if_lge): 16-bit RX length OOB (jumbo).
  • DF-1526 (twin, if_sf): RX length OOB + DMA overflow.
  • DF-1551 (twin, if_sn): packet_length -= 6 underflow.
  • DF-1452 (twin, if_ae): same RX-length OOB.
  • DF-1131 (twin, bwn): same RX-length OOB.
  • DF-1517 (twin, ath): wifi RX length OOB.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1562 Β· 8 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.7 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.7 KB ↓ raw
fix.diff suggested-fix git-apply-able unified diff fixing the cited bug 949 B view raw
fix_apply.log apply-log patch --dry-run --forward output proving fix.diff applies cleanly on with-src 547 B view raw
env.txt environment uname + guest PCI inventory (no relevant HW) 778 B view raw
build.sh build-script echo pointer to kernel rebuild path 362 B view raw
run.sh run-script echo pointer to VERDICT.md 304 B view raw
fix_build.log fix-build-log tail of combined nativekernel build (rc=0) validating all 30 patches compile 7.2 KB view raw
README.md readme human-readable summary
↓ download raw

PoC DF-1562: if_pcn.c 16-bit RX len OOB

Class: Heap OOB read Cited site: sys/dev/netif/pcn/if_pcn.c:798-799,746

Reproduction status

HW/module gated β€” cannot be live-triggered on the audit QEMU guest.

The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so the cited code path is not reachable at runtime on this guest.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/netif/pcn/if_pcn.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

m->m_len = m->m_pkthdr.len = cur_rx->pcn_rxlen - ETHER_CRC_LEN. pcn_rxlen is u16 (if_pcnreg.h:350) range 0..65535. NO bound check vs MCLBYTES=2048 or PCN_RXLEN=1536 (DMA buffer programmed at pcn_newbuf:746). Cluster MGETHDR+MCLGET=2048 bytes then m_adj(ETHER_ALIGN=2) leaves 2046 usable. For pcn_rxlen > 2050 walks up to ~63533 bytes past cluster.

Realistic impact ceiling (on suitable HW)

kernel heap info leak + panic; threat model: malicious PCIe NIC (VFIO passthrough)

Fix

Reject cur_rx->pcn_rxlen < ETHER_CRC_LEN or > MCLBYTES - ETHER_ALIGN + ETHER_CRC_LEN in pcn_rxeof.

See fix.diff for the git-apply-able patch.

How to validate the fix

scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1562.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1562.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1562: if_pcn.c 16-bit RX len OOB

Verdict

INCONCLUSIVE (HW/module gated) β€” source-level confirmed, fix validated.

The bug is real and present in master DEV source at sys/dev/netif/pcn/if_pcn.c:798-799,746, but the affected driver attaches only to hardware not present in the audit QEMU guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered here. The fix.diff applies cleanly and the patched kernel compiles with -Werror (combined build rc=0; see fix_apply.log).

Mechanism (cited path β†’ primitive β†’ effect)

m->m_len = m->m_pkthdr.len = cur_rx->pcn_rxlen - ETHER_CRC_LEN. pcn_rxlen is u16 (if_pcnreg.h:350) range 0..65535. NO bound check vs MCLBYTES=2048 or PCN_RXLEN=1536 (DMA buffer programmed at pcn_newbuf:746). Cluster MGETHDR+MCLGET=2048 bytes then m_adj(ETHER_ALIGN=2) leaves 2046 usable. For pcn_rxlen > 2050 walks up to ~63533 bytes past cluster.

Reachability on this guest

No β€” sys/dev/netif/pcn/if_pcn.c:798-799 is in a driver/module that only attaches to hardware absent from the audit guest. The trigger requires the relevant PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS loaded by root or via VFIO passthrough).

Phase 6 β€” escalation potential

This is a Heap OOB read 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).

Realistic impact ceiling on suitable HW: kernel heap info leak + panic; threat model: malicious PCIe NIC (VFIO passthrough).

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 the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.json).
  • For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.

Fix approach: Reject cur_rx->pcn_rxlen < ETHER_CRC_LEN or > MCLBYTES - ETHER_ALIGN + ETHER_CRC_LEN in pcn_rxeof.

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 β€” HW-gated. Primitive is a kernel heap info leak + panic; threat model is malicious PCIe NIC (VFIO passthrough).

Evidence (decisive lines)

Source: sys/dev/netif/pcn/if_pcn.c:798 β€” m->m_len = m->m_pkthdr.len = cur_rx->pcn_rxlen - ETHER_CRC_LEN (no bound); :746 β€” pcn_newbuf programs PCN_RXLEN=1536. Guest has no PCnet NIC. fix.diff rejects pcn_rxlen < ETHER_CRC_LEN or > MCLBYTES - ETHER_ALIGN + ETHER_CRC_LEN.

PoC changes

Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.

Verified recommended fix

Reject cur_rx->pcn_rxlen < ETHER_CRC_LEN or > MCLBYTES - ETHER_ALIGN + ETHER_CRC_LEN in pcn_rxeof. Full diff in findings/poc/DF-1562/fix.diff.

Verdict

INCONCLUSIVE (HW-gated). Bug confirmed at source level: if_pcn.c:798-799 m->m_len = m->m_pkthdr.len = cur_rx->pcn_rxlen - ETHER_CRC_LEN. pcn_rxlen is u16 (if_pcnreg.h:350) range 0..65535. NO bound check vs MCLBYTES=2048 or PCN_RXLEN=1536 (DMA buffer programmed at pcn_newbuf:746). Cluster MGETHDR+MCLGET=2048 bytes then m_adj(ETHER_ALIGN=2) leaves 2046 usable. For pcn_rxlen > 2050 walks up to ~63533 bytes past cluster into kernel heap. Also covers pcn_rxlen < 4 underflow to negative m_len. pcn(4) only attaches to AMD PCnet/PCI NICs not on the audit guest.