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+MCLGETyields a cluster of exactlyMCLBYTES=2048bytes (sys/param.h:494-497,MCLSHIFT=11).- Line 735/738 sets
m_len=MCLBYTES. - Line 742
m_adj(m_new, ETHER_ALIGN)advancesm_datatoext_buf+2and dropsm_lento 2046. - Line 745
c->pcn_rbaddr = vtophys(mtod(m_new, caddr_t))DMA-targetsext_buf+2. - So the chip writes into
ext_buf[2..2+PCN_RXLEN-1] = ext_buf[2..1537]; the cluster ends atext_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 modeinbcopy/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
- walks devclass
pcn(devclass_find("pcn")+devclass_get_softc) to find eachstruct pcn_softc, - waits for
IFF_UPand at least one packet to arrive so the ring is populated, - locates the current RX descriptor via
sc->pcn_ldata->pcn_rx_list[sc->pcn_cdata.pcn_rx_prod], - atomically writes
.pcn_rxlen = 0xFFFF,.pcn_rxstat = PCN_RXSTAT_STP|PCN_RXSTAT_ENP(noOWN, noERRβ seeif_pcnreg.h:359-366), - lets the next RINT interrupt (or
pcn_rxeofcalled 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.
Recommended fix
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).
Related findings
- 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 -= 6underflow. - 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)
PoC verification
Evidence pack
findings/poc/DF-1562 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.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
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- p
- c
- n
- /
- i
- f
- _
- p
- c
- n
- .
- c
- :
- 7
- 9
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- p
- c
- n
- /
- i
- f
- _
- p
- c
- n
- .
- c
- :
- 7
- 4
- 6
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.
No comments yet.