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
- walks devclass
txdevice_listto find eachepic_softc_t, - waits for the interface to be
IFF_UP(soepic_init_ringshas populatedsc->rx_desc[i].bufaddrwith live clusters), - locates the descriptor at
sc->cur_rxviasc->rx_desc + sc->cur_rx, - atomically writes
sc->rx_desc[cur_rx].rxlength = 65535andsc->rx_desc[cur_rx].status = 0x8001(clear the 'owned by NIC' bit0x8000to mark driver-owned, set bit 0 'OK'), - triggers
epic_rx_doneon 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.
Recommended fix
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.
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-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-1490 Β· 10 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.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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part ofmake -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
-Werrorcflags β 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
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- t
- x
- /
- i
- f
- _
- t
- x
- .
- c
- :
- 5
- 5
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- t
- x
- /
- i
- f
- _
- t
- x
- .
- c
- :
- 5
- 8
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- t
- x
- /
- i
- f
- _
- t
- x
- .
- c
- :
- 6
- 0
- 0
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.
No comments yet.