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

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

  1. walks the ste devclass via devclass_find("ste") + devclass_get_device,
  2. for each ste_softc waits until IFF_UP and at least one RX descriptor has been DMA'd,
  3. locates the head descriptor via sc->ste_cdata.ste_rx_head->ste_ptr,
  4. atomically writes cur_rx->ste_ptr->ste_status = (8191 & STE_RXSTAT_FRAMELEN) | STE_RXSTAT_DMADONE (FRAME_ERR clear),
  5. triggers ste_rxeof via the next interrupt or by scheduling a one-shot callout that pokes STE_ISR_RX_DMADONE in STE_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.

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.

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

No comments yet.

PoC verification

Evidence pack

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

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 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: 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

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