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

epic_rx_done trusts 16-bit NIC-reported rxlength as m_len with no MCLBYTES bound: large OOB heap read past RX mbuf cluster (16-bit field)

  • File: sys/dev/netif/tx/if_tx.c
  • Lines: 557, 582, 583, 600, 603, 1429
  • 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

epic_rx_done() reads the NIC DMA descriptor's 16-bit rxlength field, subtracts ETHER_CRC_LEN, and assigns the result directly to m->m_pkthdr.len = m->m_len with no upper-bound check against the RX buffer size (MCLBYTES=2048, programmed at line 1429).

For a malicious/buggy PCIe NIC reporting rxlength > 2048 (up to 65535), the mbuf is handed to ifp->if_input (ether_input/BPF/m_copydata) with m_len up to 65531, walking up to ~63483 bytes past the 2KB cluster into kernel heap: info leak of stale heap contents and/or panic on an unmapped page.

The 16-bit field makes this a far larger OOB reach than the 12-bit (if_xe/ if_my) and 11-bit (if_vr) siblings. A separate underflow exists: rxlength < 4 makes len (declared u_int16_t at line 557) wrap to 65532..65534, catastrophic.

Root cause

epic_rx_done() at sys/dev/netif/tx/if_tx.c:554-610.

Line 557: u_int16_t len; β€” len is 16-bit unsigned.

Line 582: len = desc->rxlength - ETHER_CRC_LEN; β€” desc->rxlength is volatile u_int16_t (sys/dev/netif/tx/if_txreg.h:202), a 16-bit NIC-DMA field ranging 0..65535. ETHER_CRC_LEN=4 (sys/net/ethernet.h:33).

The subtraction promotes to int, then truncates back to u_int16_t on assignment, so rxlength<4 wraps len to 0xFFFE..0xFFFC.

Line 583: m = buf->mbuf; β€” the live RX mbuf whose cluster was allocated at epic_init_rings:1422 via m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR), giving exactly MCLBYTES=2048 bytes of backing (MCLSHIFT=11, sys/sys/param.h:494-497). m_getcl does NOT zero the cluster (sys/kern/uipc_mbuf.c:1175), so trailing bytes are stale heap.

Line 600: m->m_pkthdr.len = m->m_len = len; β€” no comparison of len against MCLBYTES (2048) or against desc->buflength (set to MCLBYTES at line 1429: desc->buflength = MCLBYTES;).

Line 603: ifp->if_input(ifp, m, NULL, -1); β€” hands the over-length mbuf to ether_input, which traverses m_len bytes (BPF tap, header parse, m_copydata on forward).

The only status guard is line 575 if ((desc->status & 1) == 0) (NIC 'OK' bit clear β‡’ error). There is NO length validation anywhere between the DMA descriptor read (line 565) and the m_len assignment (line 600).

The descriptor's buflength (2048) tells the NIC the max DMA it may write, but rxlength is reported independently; a device that reports rxlength > buflength while keeping bit 0 set is never caught by the driver.

For rxlength in [2053, 65535]: len in [2049, 65531], m_len exceeds the 2048 cluster by 1..63483 bytes.

For rxlength in [0, 3]: len wraps to [65532, 65534].

Threat

Attacker position: a malicious or compromised PCIe NIC function (VFIO/PCI passthrough to a QEMU/KVM guest, a hostile Thunderbolt/ExpressCard NIC, or SMC83C170 silicon errata) that writes a DMA descriptor with rxlength > 2048 (or rxlength < 4) and status bit 0 set. The driver reads desc->rxlength and desc->status directly from DMA memory (line 565, via sc->rx_desc which is the DMA-coherent ring at vtophys(sc->rx_desc) programmed at line 1083) and trusts them unconditionally.

Under default driver config the SMC83C170 is a 10/100 controller with no jumbo support and RXCON_DEFAULT (if_txreg.h:166) does not set RXCON_SAVE_ERRORED_PACKETS, so a remote L2 attacker sending ordinary frames <=1518B cannot reach this path on correctly-functioning silicon β€” hence Medium (adjacent-vector), not High. This matches the threat model and severity of the sibling findings DF-1410 (if_xe), DF-1478 (if_my), DF-1481 (if_vr), DF-1452 (if_ae), DF-1131 (bwn).

Impact once triggered:

  • (a) kernel heap info-leak of up to ~63483 bytes of uninitialized/stale heap (prior packet contents, adjacent slab metadata) delivered up the network stack to a socket an attacker reads β€” substantially larger than the ~2043-byte leak of the 12-bit siblings due to the 16-bit field;
  • (b) kernel panic (A:H availability) when the OOB read crosses an unmapped page boundary, which the large reach makes far more likely than in the siblings.

Exploit / PoC

PoC angle A (software proof, proves the unbounded-read defect with no special hardware): a kldload kernel module that

  1. walks devclass tx device_list to find each epic_softc_t,
  2. waits for the interface to be IFF_UP (so epic_init_rings has populated sc->rx_desc[i].bufaddr with live clusters),
  3. locates the descriptor at sc->cur_rx via sc->rx_desc + sc->cur_rx,
  4. atomically writes sc->rx_desc[cur_rx].rxlength = 65535 and sc->rx_desc[cur_rx].status = 0x8001 (clear the 'owned by NIC' bit 0x8000 to mark driver-owned, set bit 0 'OK'),
  5. triggers epic_rx_done on the next interrupt or by directly invoking it.

epic_rx_done computes len = 65535-4 = 65531, assigns m_len=65531 to a 2048-byte cluster, calls ifp->if_input.

Success: Fatal trap 12: page fault while in kernel mode inside ether_input/bcopy (typical when the read crosses a page boundary), or β€” with slab grooming so trailing 63483 bytes stay mapped β€” leaked kernel heap observable via an AF_RAW socket reading the oversized frame.

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

PoC angle B (no root, requires hostile PCIe): a QEMU/KVM guest with a passed-through or emulated SMC83C170 function writes the crafted rxlength/status via DMA; the host running this driver hits the same path.

Bound the NIC-reported length to the RX buffer geometry (MCLBYTES) before using it as m_len, and reject the unsigned-underflow case (rxlength < ETHER_CRC_LEN). This matches the fix applied in every sibling DFly NIC driver.

--- a/sys/dev/netif/tx/if_tx.c
+++ b/sys/dev/netif/tx/if_tx.c
@@ -579,6 +579,22 @@ epic_rx_done(epic_softc_t *sc)
            continue;
        }

-       /* Save packet length and mbuf contained packet */
-       len = desc->rxlength - ETHER_CRC_LEN;
-       m = buf->mbuf;
+       /* Save packet length and mbuf contained packet.
+        *
+        * Validate the NIC-reported frame length against the RX
+        * buffer size.  The descriptor buflength was programmed to
+        * MCLBYTES in epic_init_rings; a malicious/buggy PCIe
+        * device can otherwise report rxlength up to 65535 (16-bit
+        * field) while clearing the error bit, making the stack
+        * walk far past the 2KB mbuf cluster.  Also reject the
+        * underflow case rxlength < ETHER_CRC_LEN.
+        */
+       if (desc->rxlength < ETHER_HDR_LEN + ETHER_CRC_LEN ||
+           desc->rxlength > MCLBYTES) {
+           IFNET_STAT_INC(&sc->sc_if, ierrors, 1);
+           desc->status = 0x8000;
+           continue;
+       }
+
+       len = desc->rxlength - ETHER_CRC_LEN;
+       m = buf->mbuf;

        /* Try to get mbuf cluster */

The check runs after the error-bit guard (line 575) and before the m_getcl replacement (line 586), so on rejection the existing cluster is retained (buf->mbuf untouched) and the descriptor is simply re-armed for the NIC without losing the buffer. The lower bound ETHER_HDR_LEN + ETHER_CRC_LEN (18+4=22) rejects runts/underflow; the upper bound MCLBYTES caps the OOB.

  • 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-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-1490 Β· 10 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.7 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 406 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 286 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 358 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-1490: epic_rx_done 16-bit rxlength has no upper-bound check

Class: heap OOB read (DMA-derived) Cited site: sys/dev/netif/tx/if_tx.c:582, 600

Reproduction status

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

No β€” tx(4) (SMC EPIC/83c170) is in GENERIC but only attaches to SMC EtherPower II PCI NICs. Not present in audit guest.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/netif/tx/if_tx.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 582 len = desc->rxlength - ETHER_CRC_LEN; where desc->rxlength is volatile u_int16_t (0..65535). For rxlength<4, the int subtraction wraps to 65532..65534 (then truncates to u_int16_t back to 65532..65534). Line 600 m->m_pkthdr.len = m->m_len = len; assigns to the cluster mbuf with NO bound check vs MCLBYTES=2048. if_input then reads m->m_len bytes from a 2048-byte cluster β†’ OOB heap read.

Realistic impact ceiling

leak (info leak / DoS)

Fix

Add if (len > MCLBYTES) { IFNET_STAT_INC(ierrors); desc->status=0x8000; continue; } between the rxlength computation and the mbuf assignment.

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-1490.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1490.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/tx && 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-1490: epic_rx_done 16-bit rxlength has no upper-bound check

Verdict

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

The bug is real and present in master DEV source at sys/dev/netif/tx/if_tx.c:582, 600, 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 582 len = desc->rxlength - ETHER_CRC_LEN; where desc->rxlength is volatile u_int16_t (0..65535). For rxlength<4, the int subtraction wraps to 65532..65534 (then truncates to u_int16_t back to 65532..65534). Line 600 m->m_pkthdr.len = m->m_len = len; assigns to the cluster mbuf with NO bound check vs MCLBYTES=2048. if_input then reads m->m_len bytes from a 2048-byte cluster β†’ OOB heap read.

Reachability on this guest

No β€” tx(4) (SMC EPIC/83c170) is in GENERIC but only attaches to SMC EtherPower II PCI NICs. Not present in audit guest.

Phase 6 β€” escalation potential

This is a heap OOB read (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 (len > MCLBYTES) { IFNET_STAT_INC(ierrors); desc->status=0x8000; continue; } between the rxlength computation and the mbuf assignment.

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 β€” tx(4) HW-gated (no SMC EPIC NIC in guest). Primitive is info-leak on real HW; no live escalation possible on this guest.

Evidence (decisive lines)

Source-level confirmation at sys/dev/netif/tx/if_tx.c:557, sys/dev/netif/tx/if_tx.c:582, sys/dev/netif/tx/if_tx.c:600. 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 returning early on len > MCLBYTES), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.

Verified recommended fix

Add if (len > MCLBYTES) { IFNET_STAT_INC(ierrors,1); desc->status=0x8000; continue; } between the rxlength computation (582) and the mbuf assignment (600). Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1490/fix.diff.

Verdict

epic_rx_done line 582 len = desc->rxlength - ETHER_CRC_LEN where desc->rxlength is volatile u_int16_t (0..65535). For rxlength<4 the int subtraction wraps to 65532..65534. Line 600 m->m_pkthdr.len = m->m_len = len assigns to the cluster mbuf with NO bound check vs MCLBYTES=2048. if_input then reads m->m_len bytes from a 2048-byte cluster β†’ OOB heap read. tx(4) is in GENERIC but only attaches to SMC EtherPower II (EPIC/83c170) NICs β€” not present in audit guest. Source-level confirmed.