my_rxeof trusts 12-bit NIC-reported FLNG as m_len without MCLBYTES bound: OOB heap read past RX mbuf cluster
- File:
sys/dev/netif/my/if_my.c - Lines: 1105, 1106, 1108, 1131, 1076
- 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
my_rxeof extracts the 12-bit FLNG field (range 0..4095) from the NIC
descriptor status word, subtracts ETHER_CRC_LEN, and assigns the result
directly to m->m_pkthdr.len = m->m_len with no upper-bound check against
MCLBYTES (2048) β the actual size of the RX mbuf cluster programmed into the
descriptor RBS field at my_newbuf line 1076 ((MCLBYTES-1)<<MY_RBSShift).
For FLNG in [2053, 4095] (total_len 2049..4091), m_len exceeds the 2KB
cluster and the subsequent ether_input / ifp->if_input call (line 1134)
walks up to ~2043 bytes past the cluster into kernel heap: information leak of
stale heap contents and/or panic on unmapped page.
Direct sibling of DF-1410 (if_xe), DF-1452 (if_ae), DF-1131 (bwn).
Root cause
Line 1105: total_len = (rxstat & MY_FLNGMASK) >> MY_FLNGShift.
MY_FLNGMASK=0x0fff0000 (if_myreg.h:229) is a 12-bit field, so FLNG ranges
0..4095.
Line 1106: total_len -= ETHER_CRC_LEN (4), giving a signed-int range
-4..4091.
Line 1108: the only guard is total_len < MINCLSIZE which routes small packets
through m_devget (safe). For total_len >= MINCLSIZE the else branch
(line 1116) is taken and line 1131 sets
m->m_pkthdr.len = m->m_len = total_len on m = cur_rx->my_mbuf (line 1117) β
an mbuf whose cluster was allocated in my_newbuf via MGETHDR+MCLGET (lines
1061, 1067) giving exactly MCLBYTES=2048 bytes of backing store, and whose
DMA buffer size was advertised to the NIC as MCLBYTES-1=2047 at line 1076
(c->my_ptr->my_ctl = (MCLBYTES - 1) << MY_RBSShift).
No comparison of total_len against MCLBYTES (or against the RBS value) is
ever performed.
m_devget (uipc_mbuf.c:2235) is NOT used in this branch β the original
cluster is passed straight up.
For total_len in [2049, 4091] the stack reads 1..2043 bytes past the live
cluster. Stale bytes are uninitialized heap (MCLGET does not zero), so the
leak recovers prior packet contents or adjacent slab data.
Threat
Attacker position: a malicious or compromised PCIe NIC (VFIO/PCI passthrough,
Thunderbolt, MTD80x silicon errata) that writes a descriptor status word with
FLNG>2048 and MY_ES clear. The driver reads rxstat from DMA memory (line
1094) and trusts it unconditionally.
Under default driver config the MTD80x does NOT set MY_ALP (accept-long-packet)
in MY_TCRRCR (my_init line 1428 programs only MY_TFTSF|MY_RBLEN|MY_RPBLE512),
so a remote L2 attacker sending ordinary frames <=1518B cannot reach this path
on correctly-functioning silicon β hence Medium, not High.
Impact once triggered: kernel heap info-leak (up to ~2043 bytes) via the network stack, or kernel panic when the OOB read hits an unmapped page (A:H availability).
Demonstrable locally with a kldload'd module that pokes a crafted rxstat,
proving the missing-bound defect independent of hardware behavior. Same threat
model as DF-1452 (if_ae, Medium).
Exploit / PoC
PoC angle A (software proof, requires root to load but proves the
unbounded-read defect with no special hardware): a kldload kernel module
that
- walks
device_list/ devclassmyto find eachmy_softc, - waits for the interface to be
IFF_UPand a packet to arrive, - locates the current RX descriptor via
sc->my_cdata.my_rx_head->my_ptr, - atomically writes
my_status = ((4095 << MY_FLNGShift) & MY_FLNGMASK) | MY_RXFSD | MY_RXLSD(FLNG=4095, noMY_ES, noMY_OWNByNIC) and letsmy_rxeofrun on the next interrupt.
my_rxeof computes total_len = 4095-4 = 4091, takes the else branch, sets
m_len=4091 on a 2048-byte cluster, calls ifp->if_input. The
ether_input / bpf_mtap / m_copydata traversal walks 2043 bytes past the
cluster β panic (typical: vm_fault on unmapped page) or, with slab grooming so
the trailing 2043 bytes are mapped, an info-leak of stale heap into an mbuf the
stack then delivers to a raw socket the attacker reads.
Success looks like: Fatal trap 12: page fault while in kernel mode inside
bcopy/ether_input chain, or leaked kernel pointers observable via an
AF_RAW receive.
Build: cc -c -DKLDLOAD -I/sys poc_myrx.c; ld -d -r poc_myrx.o; kldload ./poc_myrx.ko.
PoC angle B (no root, requires hostile PCIe): a qemu/KVM guest with a
passed-through or crafted MTD80x function (or a malicious Thunderbolt NIC)
writes a DMA descriptor with the crafted rxstat; the host running this driver
hits the same path.
Recommended fix
Bound total_len to the RX buffer size before using it as m_len. The check
also guards the underflow total_len<0 from FLNG<4:
--- a/sys/dev/netif/my/if_my.c
+++ b/sys/dev/netif/my/if_my.c
@@ -1103,6 +1103,17 @@ my_rxeof(struct my_softc * sc)
continue;
}
/* No errors; receive the packet. */
total_len = (rxstat & MY_FLNGMASK) >> MY_FLNGShift;
total_len -= ETHER_CRC_LEN;
+ /*
+ * Validate the NIC-reported frame length against the RX
+ * buffer size. The descriptor's RBS field was programmed
+ * to MCLBYTES-1 in my_newbuf; a malicious/buggy PCIe
+ * device can otherwise report FLNG up to 4095, making the
+ * stack walk past the 2KB mbuf cluster.
+ */
+ if (total_len < ETHER_HDR_LEN || total_len > MCLBYTES) {
+ IFNET_STAT_INC(ifp, ierrors, 1);
+ cur_rx->my_ptr->my_status = MY_OWNByNIC;
+ continue;
+ }
+
if (total_len < MINCLSIZE) {
m = m_devget(mtod(cur_rx->my_mbuf, void *),
total_len, 0, ifp);
This matches the pattern applied in sibling drivers (every other DFly NIC driver
bounds rxd->len to MCLBYTES or the ring buffer size; see DF-1410, DF-1452,
DF-1292, DF-1336).
Related findings
- DF-1410 (twin, if_xe): missing
MCLBYTEScheck on RX length. - DF-1452 (twin, if_ae): same RX-length OOB.
- DF-1131 (twin, bwn): same RX-length OOB.
- DF-1479 (sibling):
my_startignoresmy_encapfailure in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1478 Β· 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.7 KB | β raw |
| fix.diff | suggested-fix | git-apply-able minimal fix; compiles -Werror clean | 480 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 | 287 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 | 387 B | view raw |
| fix_apply.log | apply-log | patch --dry-run output proving fix.diff applies cleanly on with-src | 384 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-1478: my_rxeof FLNG has no upper-bound check vs MCLBYTES
Class: heap OOB read (DMA-derived length)
Cited site: sys/dev/netif/my/if_my.c:1105-1131
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
No β my(4) is in LINT64 only (not in GENERIC). Myson MTd80x/MTD89x PCI NIC needed; not present in audit guest.
The bug is confirmed at the source level by tracing the cited path:line in
sys/dev/netif/my/if_my.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 1105 total_len = (rxstat & MY_FLNGMASK) >> MY_FLNGShift; β FLNG is 12-bit (0..4095). Line 1106 subtracts ETHER_CRC_LEN. RX cluster is MCLBYTES=2048 (programmed as RBS=MCLBYTES-1 at 1076). For FLNG in [2053,4095], total_len 2049..4091 exceeds 2048. Line 1110 m_devget(..., total_len, ...) and line 1131 m->m_len = total_len both operate on a length > the cluster size β OOB heap read of up to ~2KB past the cluster.
Realistic impact ceiling
leak (info leak / DoS)
Fix
Add if (total_len > MCLBYTES) { drop; continue; } between the FLNG extraction and the m_devget/newbuf path.
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-1478.diff ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1478.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/my && make' # 3. The compile must succeed with -Werror (it does β see build.log).
VERDICT β DF-1478: my_rxeof FLNG has no upper-bound check vs MCLBYTES
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/netif/my/if_my.c:1105-1131,
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 1105 total_len = (rxstat & MY_FLNGMASK) >> MY_FLNGShift; β FLNG is 12-bit (0..4095). Line 1106 subtracts ETHER_CRC_LEN. RX cluster is MCLBYTES=2048 (programmed as RBS=MCLBYTES-1 at 1076). For FLNG in [2053,4095], total_len 2049..4091 exceeds 2048. Line 1110 m_devget(..., total_len, ...) and line 1131 m->m_len = total_len both operate on a length > the cluster size β OOB heap read of up to ~2KB past the cluster.
Reachability on this guest
No β my(4) is in LINT64 only (not in GENERIC). Myson MTd80x/MTD89x PCI NIC needed; not present in audit guest.
Phase 6 β escalation potential
This is a heap OOB read (DMA-derived length) 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 (total_len > MCLBYTES) { drop; continue; } between the FLNG extraction and the m_devget/newbuf path.
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
- /
- m
- y
- /
- i
- f
- _
- m
- y
- .
- c
- :
- 1
- 1
- 0
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- y
- /
- i
- f
- _
- m
- y
- .
- c
- :
- 1
- 1
- 0
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- y
- /
- i
- f
- _
- m
- y
- .
- c
- :
- 1
- 1
- 1
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- y
- /
- i
- f
- _
- m
- y
- .
- c
- :
- 1
- 1
- 3
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- y
- /
- i
- f
- _
- m
- y
- .
- c
- :
- 1
- 0
- 7
- 6
Detail
Exploit chain
none β my(4) module-only AND HW-gated (no Myson 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/my/if_my.c:1105, sys/dev/netif/my/if_my.c:1106, sys/dev/netif/my/if_my.c:1110. 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: add upper-bound check), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.
Verified recommended fix
Add if (total_len > MCLBYTES) { drop; continue; } between the FLNG extraction (1106) and the m_devget/newbuf path. Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1478/fix.diff.
Verdict
my_rxeof line 1105 total_len = (rxstat & MY_FLNGMASK) >> MY_FLNGShift β FLNG is 12-bit (0..4095). Line 1106 subtracts ETHER_CRC_LEN. RX cluster is MCLBYTES=2048 (programmed as RBS=MCLBYTES-1 at 1076). For FLNG in [2053,4095], total_len 2049..4091 exceeds 2048. m_devget at 1110 and m_len=total_len at 1131 both operate on length > cluster size β OOB heap read of up to ~2KB past cluster. my(4) is in LINT64 only (not GENERIC); Myson MTD80x/89x PCI NIC needed β not present in audit guest. Source-level confirmed.
No comments yet.