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

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

  1. walks device_list / devclass my to find each my_softc,
  2. waits for the interface to be IFF_UP and a packet to arrive,
  3. locates the current RX descriptor via sc->my_cdata.my_rx_head->my_ptr,
  4. atomically writes my_status = ((4095 << MY_FLNGShift) & MY_FLNGMASK) | MY_RXFSD | MY_RXLSD (FLNG=4095, no MY_ES, no MY_OWNByNIC) and lets my_rxeof run 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.

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

  • DF-1410 (twin, if_xe): missing MCLBYTES check on RX length.
  • DF-1452 (twin, if_ae): same RX-length OOB.
  • DF-1131 (twin, bwn): same RX-length OOB.
  • DF-1479 (sibling): my_start ignores my_encap failure in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

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

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

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.