ste_rxeof trusts 13-bit NIC-reported FRAMELEN as m_len without MCLBYTES bound: large OOB heap read past RX mbuf cluster
- File:
sys/dev/netif/ste/if_ste.c - Lines: 678, 695
- 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
ste_rxeof extracts the NIC-reported frame length from the RX descriptor status
word with the 13-bit mask STE_RXSTAT_FRAMELEN (0x1FFF, range 0..8191) and
assigns it directly to the RX mbuf's m_len/m_pkthdr.len with no validation
against the RX cluster geometry.
The cluster, after m_adj(ETHER_ALIGN=2) in ste_newbuf, exposes only
MCLBYTES-ETHER_ALIGN = 2046 valid bytes; any total_len in [2047..8191]
makes ether_input and the upper network stack read up to 6145 bytes past the
live cluster into adjacent kernel heap.
This is the wider-field sibling of DF-1410 (if_xe, 12-bit), DF-1478 (if_my,
12-bit), DF-1481 (if_vr, 11-bit / max-3-byte OOB), and DF-1490 (if_tx,
16-bit).
Root cause
At sys/dev/netif/ste/if_ste.c:678:
total_len = cur_rx->ste_ptr->ste_status & STE_RXSTAT_FRAMELEN;
STE_RXSTAT_FRAMELEN is 0x00001FFF (sys/dev/netif/ste/if_stereg.h:418), a
13-bit field yielding 0..8191.
There is NO comparison of total_len against MCLBYTES, against
(MCLBYTES - ETHER_ALIGN), against the descriptor DMA length (1540), or against
ETHER_MAX_LEN+EVL_ENCAPLEN anywhere in ste_rxeof.
The only error gate is if (rxstat & STE_RXSTAT_FRAME_ERR) at line 658 β
FRAME_ERR is bit 14 (0x00004000), independent of the length field; the driver
performs zero length validation.
At line 695 m->m_pkthdr.len = m->m_len = total_len; commits the untrusted
length into the mbuf.
The RX cluster is allocated by ste_newbuf:1027-1047 via MGETHDR+MCLGET
(MCLBYTES=2048 per sys/sys/param.h:497) followed by m_adj(m_new, ETHER_ALIGN)
(ETHER_ALIGN=2 per sys/net/ethernet.h:41), so m_data sits at ext_buf+2
and the valid data window is ext_buf[2..2047] (2046 bytes).
The DMA descriptor length is hardcoded to
(1536 + EVL_ENCAPLEN) | STE_FRAG_LAST = 1540 (line 1047), so the chip DMA
target never overflows the cluster β but the FRAMELEN field reported in the
status word is independent and is what becomes m_len.
For FRAMELEN in [2047..8191] the network stack reads m_data[0..total_len-1]
which extends past ext_buf+2047 into adjacent slab/heap.
For comparison, the standard DragonFlyBSD NIC-driver idiom (e.g.
sys/dev/netif/fxp/if_fxp.c:1408
if (total_len < sizeof(struct ether_header) || total_len > MCLBYTES - RFA_ALIGNMENT_FUDGE - sizeof(struct fxp_rfa)))
explicitly bounds the length before assignment.
Threat
Primary attacker position: a malicious or compromised PCIe NIC function presented to the host (VFIO/pci-passthrough of an emulated Sundance ST201 to an untrusted QEMU/KVM guest, a Thunderbolt/ExpressCard add-in NIC, or D-Link DFE-550TX / Sundance ST201 silicon hitting an erratum).
The descriptor status word lives in DMA memory the device writes; the host reads
rxstat at line 643/678 unconditionally and trusts every bit. The attacker
writes status = (FRAMELEN=8191 & 0x1FFF) | STE_RXSTAT_DMADONE (with FRAME_ERR
clear) β ste_rxeof then sets m_len=8191 and hands the mbuf to
ifp->if_input.
Secondary position: on correctly-functioning ST201 silicon with the
MAX_FRAMELEN register programmed to ETHER_MAX_LEN+EVL_ENCAPLEN=1522
(ste_init:1213), accepted frames have FRAMELEN<=1522 and the path is
unreachable remotely; the bug is a hardening/defense-in-depth hole that bites the
moment the chip's reported length deviates from the DMA byte count (which is
exactly what hostile PCIe and many real-world errata do).
Impact once triggered:
- up to 6145 bytes of adjacent kernel heap info-leak delivered to the network stack and reachable via raw/BPF sockets as stale prior-packet bytes or adjacent slab metadata;
- or kernel panic (page fault in
ether_input/ip_input) when the inflated read crosses into an unmapped page (8191 bytes is virtually guaranteed to cross a page boundary past a 2 KiB slab).
Exploit / PoC
Angle A β kldload software PoC proving the missing-bound defect independent of
hardware (root on host, demonstrates exploitability): a kernel module
ste_oob_leak.ko that
- walks the
stedevclass viadevclass_find("ste")+devclass_get_device, - for each
ste_softcwaits untilIFF_UPand at least one RX descriptor has been DMA'd, - locates the head descriptor via
sc->ste_cdata.ste_rx_head->ste_ptr, - atomically writes
cur_rx->ste_ptr->ste_status = (8191 & STE_RXSTAT_FRAMELEN) | STE_RXSTAT_DMADONE(FRAME_ERRclear), - triggers
ste_rxeofvia the next interrupt or by scheduling a one-shot callout that pokesSTE_ISR_RX_DMADONEinSTE_ISR_ACK.
ste_rxeof computes total_len=8191, allocates a fresh mbuf (ste_newbuf
succeeds), then sets m->m_len = m->m_pkthdr.len = 8191 and calls
ifp->if_input. ether_input_oncpu β ether_demux_oncpu β ip_input walks the
inflated mbuf.
With a slab-groomed heap (spray 2 KiB clusters so the trailing allocations hold
prior AF_PACKET RX bytes / mbuf metadata), open an AF_RAW / BPF socket on
ste0: the 8191-byte mbuf is copied to user space, exposing ~6 KiB of stale
kernel heap (proof: write the same byte pattern from a second NIC into the
groomed slab first, then read it back from the AF_RAW socket on the leaked
tail).
Or simpler: run under option KASAN and observe a clean OOB-read report in
dmesg at if_ste.c:695.
Angle B β no-root hostile-PCIe: a QEMU/KVM guest configured with
-device sundance... (or vfio-pci bound to a real DFE-550) DMAs the crafted
status word; the host ste(4) hits the same path.
Build: standard DragonFly kldload(8) for angle A
(cc -DKLD_MODULE -c ste_oob_leak.c && ld -d -r -o ste_oob_leak.kld *.o && kldload ./ste_oob_leak.ko).
Success: KASAN OOB-read report at if_ste.c:695, or
Fatal trap 12: page fault in ether_input for the page-boundary variant, or
N>=2047 bytes of recognizable groomed-pattern delivered to userspace via
AF_RAW.
Recommended fix
Bound total_len to the RX cluster geometry before committing it to m_len,
mirroring the fxp/dc/sk/em idiom. Drop oversized/undersized frames on the
same error path already used for FRAME_ERR (status cleared, mbuf left in place
for reuse):
--- a/sys/dev/netif/ste/if_ste.c
+++ b/sys/dev/netif/ste/if_ste.c
@@ -675,6 +675,21 @@ ste_rxeof(struct ste_softc *sc)
/* No errors; receive the packet. */
m = cur_rx->ste_mbuf;
total_len = cur_rx->ste_ptr->ste_status & STE_RXSTAT_FRAMELEN;
+
+ /*
+ * Validate the NIC-reported frame length against the RX
+ * buffer geometry. The RX cluster is MCLBYTES with m_data
+ * advanced by ETHER_ALIGN, so the live window is
+ * MCLBYTES - ETHER_ALIGN bytes. STE_RXSTAT_FRAMELEN is a
+ * 13-bit field (0..8191); a malicious/buggy PCIe device or
+ * chip erratum can report a length larger than what was
+ * actually DMA'd, which would make if_input read past the
+ * cluster into adjacent kernel heap.
+ */
+ if (total_len < ETHER_HDR_LEN ||
+ total_len > MCLBYTES - ETHER_ALIGN) {
+ IFNET_STAT_INC(ifp, ierrors, 1);
+ cur_rx->ste_ptr->ste_status = 0;
+ continue;
+ }
/*
* Try to conjure up a new mbuf cluster. If that
The check is placed before ste_newbuf() so a bad length does not waste an mbuf
allocation; the existing cur_rx->ste_mbuf stays bound to the descriptor and is
reused next pass.
ETHER_HDR_LEN(=14) < valid < MCLBYTES-ETHER_ALIGN(=2046) admits every
legitimate 60..1522-byte Ethernet/VLAN frame while rejecting the entire inflated
tail.
This is the same shape of fix applied in DF-1410/DF-1478/DF-1481/DF-1490 and
matches the long-standing fxp idiom at sys/dev/netif/fxp/if_fxp.c:1408.
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-1452 (twin, if_ae): same RX-length OOB.
- DF-1131 (twin, bwn): same RX-length OOB.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1514 Β· 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 | 700 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 | 300 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-1514: if_ste.c RX total_len OOB read
Class: Heap OOB read (DMA-controlled length)
Cited site: sys/dev/netif/ste/if_ste.c:678,695
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/ste/if_ste.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
ste_rxeof reads total_len = cur_rx->ste_ptr->ste_status & STE_RXSTAT_FRAMELEN (a 13-bit DMA-coherent field, 0..8191) with NO bound check. The RX cluster is MGETHDR+MCLGET (2048 bytes) + m_adj(ETHER_ALIGN=2) leaving 2046 usable bytes. m->m_pkthdr.len = m->m_len = total_len at line 695; for FRAMELEN in [2047..8191], ether_input/ip_input walks up to ~6145 bytes past the cluster into kernel heap.
Realistic impact ceiling (on suitable HW)
kernel heap info leak + panic; sibling of DF-1410
Fix
Clamp total_len to MCLBYTES - ETHER_ALIGN before assigning to m_len/m_pkthdr.len.
See fix.diff for the git-apply-able patch.
How to validate the fix
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1514.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1514.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-1514: if_ste.c RX total_len OOB read
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/netif/ste/if_ste.c:678,695, 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)
ste_rxeof reads total_len = cur_rx->ste_ptr->ste_status & STE_RXSTAT_FRAMELEN (a 13-bit DMA-coherent field, 0..8191) with NO bound check. The RX cluster is MGETHDR+MCLGET (2048 bytes) + m_adj(ETHER_ALIGN=2) leaving 2046 usable bytes. m->m_pkthdr.len = m->m_len = total_len at line 695; for FRAMELEN in [2047..8191], ether_input/ip_input walks up to ~6145 bytes past the cluster into kernel heap.
Reachability on this guest
No β sys/dev/netif/ste/if_ste.c:678 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; sibling of DF-1410.
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: Clamp total_len to MCLBYTES - ETHER_ALIGN before assigning to m_len/m_pkthdr.len.
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
- /
- s
- t
- e
- /
- i
- f
- _
- s
- t
- e
- .
- c
- :
- 6
- 7
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- s
- t
- e
- /
- i
- f
- _
- s
- t
- e
- .
- c
- :
- 6
- 9
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- s
- t
- e
- /
- i
- f
- _
- s
- t
- e
- .
- c
- :
- 1
- 0
- 4
- 2
Detail
Exploit chain
none β HW-gated driver bug, no live trigger on this guest. Primitive (heap OOB read) is documented at source level; realistic ceiling is kernel heap info leak + panic on suitable HW.
Evidence (decisive lines)
Source: sys/dev/netif/ste/if_ste.c:678 β total_len = cur_rx->ste_ptr->ste_status & STE_RXSTAT_FRAMELEN (no bound check); :695 β m->m_pkthdr.len = m->m_len = total_len. Guest pciconf -lv shows no Sundance NIC. fix.diff clamps total_len to MCLBYTES - ETHER_ALIGN before the m_len assignment.
PoC changes
Created evidence pack from scratch (no prior PoC): README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.
Verified recommended fix
Clamp total_len to MCLBYTES - ETHER_ALIGN (2046) right before m->m_pkthdr.len = m->m_len = total_len in ste_rxeof. Matches the pattern used by other NIC drivers. Full diff in findings/poc/DF-1514/fix.diff.
Verdict
INCONCLUSIVE (HW-gated). Bug confirmed at source level: if_ste.c:678 reads total_len = ste_status & STE_RXSTAT_FRAMELEN (13-bit DMA-coherent field, 0..8191) with NO bound check vs MCLBYTES (2048) cluster / 2046-byte usable buffer; line 695 assigns total_len to m_len/m_pkthdr.len. A malicious NIC (VFIO passthrough) supplying FRAMELEN in [2047..8191] makes ether_input/ip_input walk up to ~6145 bytes past the cluster into kernel heap (info leak + panic). The ste(4) driver attaches only to Sundance ST201 PCI NICs not present on the audit QEMU guest (virtio+PIIX3 only).
No comments yet.