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

lge_rxeof trusts 16-bit NIC-reported LGE_RXBYTES as m_len / m_devget copy length without jumbo-buffer bound: up to 56519-byte OOB heap read

  • File: sys/dev/netif/lge/if_lge.c
  • Lines: 870, 887, 901, 921
  • 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

lge_rxeof extracts the 16-bit NIC-reported frame length via LGE_RXBYTES(cur_rx) (lge_ctl & 0xFFFF, range 0..65535) and assigns it directly to m->m_pkthdr.len = m->m_len (line 901) or passes total_len+ETHER_ALIGN to m_devget (line 887) with no upper-bound check against the RX jumbo buffer capacity (LGE_JUMBO_FRAMELEN - ETHER_ALIGN = 9016 bytes).

A malicious or buggy PCIe NIC can write lge_ctl with a length up to 65535 while leaving the error bits clear, causing the network stack to walk up to 56519 bytes past the 9016-byte buffer into the contiguous jumbo pool β€” a kernel heap info leak of prior packet data / uninitialized slab contents, or a panic if the read crosses the pool boundary.

Root cause

In lge_rxeof (sys/dev/netif/lge/if_lge.c:847-925):

Line 870: total_len = LGE_RXBYTES(cur_rx); β€” LGE_RXBYTES is defined as (x->lge_ctl & 0xFFFF) (sys/dev/netif/lge/if_lgereg.h:455), a 16-bit field ranging 0..65535.

The lge_ctl field is DMA-coherent memory written back by the NIC after receive; the driver reads it directly and trusts it unconditionally.

Lines 880–884: the only guard is if (rxctl & LGE_RXCTL_ERRMASK) which checks LENERR|OFLOW|CRCERR|RUNT|ALGNERR (if_lgereg.h:431-434). This depends on the NIC reliably setting LGE_RXCTL_LENERR for oversized frames β€” the driver performs NO independent length validation. A malicious NIC can set lge_ctl = 0x0000FFFF (length=65535) with all error bits clear.

Line 901 (direct path): m->m_pkthdr.len = m->m_len = total_len; β€” the mbuf's backing store is a jumbo buffer of LGE_JUMBO_FRAMELEN=9018 bytes (if_lgereg.h:490), allocated without M_ZERO via contigmalloc (if_lge.c:739). After m_adj(ETHER_ALIGN=2) in lge_newbuf (if_lge.c:705), m_data = ext_buf+2 and usable capacity from m_data is 9016 bytes. Setting m_len=65535 makes ether_input/BPF/m_copydata read 56519 bytes past the buffer.

Line 887 (m_devget fallback path, taken when lge_newbuf fails): m_devget(mtod(m, char *) - ETHER_ALIGN, total_len + ETHER_ALIGN, 0, ifp) β€” mtod(m)-ETHER_ALIGN = ext_buf (start of 9018-byte buffer); copy length = total_len+2. For total_len=65535: copies 65537 bytes from a 9018-byte buffer β†’ same 56519-byte OOB.

Line 921: ifp->if_input(ifp, m, NULL, -1); delivers the over-length mbuf up the stack, where the OOB traversal occurs.

The RX descriptor's lge_fraglen was programmed to 9016 in lge_newbuf (if_lge.c:710: c->lge_fraglen = m_new->m_len), which tells the NIC the max DMA write, but LGE_RXBYTES is the NIC-reported received length written independently into lge_ctl β€” a device that reports length > fraglen while keeping error bits clear is never caught.

The jumbo pool is one contiguous allocation of LGE_JMEM β‰ˆ 3.3 MB (384 slots Γ— 9024 bytes, if_lgereg.h:492-499). An OOB read of 56519 bytes spans ~6 adjacent slots within this pool, so it will NOT fault on an unmapped page β€” it reliably leaks stale heap data (prior packet contents, uninitialized contigmalloc memory without M_ZERO).

Threat

Attacker position: a malicious or compromised PCIe NIC function (VFIO/PCI passthrough to a QEMU/KVM guest, a hostile Thunderbolt/ExpressCard NIC, or LXT1001 silicon errata) that writes a DMA descriptor with lge_ctl & 0xFFFF > 9016 and all LGE_RXCTL_ERRMASK bits clear.

The driver reads lge_ctl from DMA-coherent memory (line 866: rxctl = cur_rx->lge_ctl) and trusts it unconditionally.

Under default driver config the interface MTU is ETHERMTU=1500 (if_lge.c:519), so a remote L2 attacker sending ordinary frames ≀1518 bytes cannot reach this path on correctly-functioning silicon β€” the NIC reports total_len ≀ 1518, well within the 9016-byte buffer. Even with jumbo MTU (up to LGE_JUMBO_MTU β‰ˆ 9000), frames ≀ 9018 fit in the buffer.

Hence Medium (adjacent-vector), matching sibling findings.

Impact once triggered:

  • (a) kernel heap info leak of up to ~56519 bytes of uninitialized/stale heap (prior packet contents, adjacent slab/jumbo-slot data) delivered up the network stack to a socket an attacker reads;
  • (b) kernel panic (A:H) if the OOB read extends past the 3.3 MB contiguous jumbo pool onto an unmapped page.

The 16-bit field gives a larger OOB reach than the 11/12-bit siblings (DF-1478, DF-1481, DF-1410), though the 9016-byte buffer reduces it relative to the 2048-byte-buffer siblings (DF-1490).

Direct sibling of DF-1490 (if_tx, 16-bit RX length), DF-1478 (if_my, 12-bit), DF-1481 (if_vr, 11-bit), DF-1410 (if_xe, 12-bit).

Exploit / PoC

PoC angle A (software proof, proves the unbounded-read defect with no special hardware β€” requires root to kldload but proves the bug independent of NIC behavior): a kldload kernel module that

  1. walks devclass lge device_list to find each struct lge_softc,
  2. waits for the interface to be IFF_UP (so lge_list_rx_init has populated lge_rx_list[i] with live jumbo buffers),
  3. locates the RX descriptor at sc->lge_ldata->lge_rx_list[sc->lge_cdata.lge_rx_cons],
  4. atomically writes cur_rx->lge_ctl = 0x0000FFFF (length=65535, all error bits clear: LGE_RXCTL_ERRMASK = 0x7C400000, none set in 0x0000FFFF) and cur_rx->lge_sts = 0 (no checksum-error bits),
  5. triggers lge_rxeof on the next interrupt (or by directly invoking it via a software-interrupt trigger).

lge_rxeof computes total_len = 65535, passes the error check (rxctl & ERRMASK == 0), sets m_len=65535 on a 9016-byte buffer, calls ifp->if_input.

Success: with slab grooming so trailing 56519 bytes stay mapped within the 3.3 MB contiguous jumbo pool β€” leaked kernel heap observable via an AF_RAW/bpf socket receiving the oversized frame (info leak of stale packet data from adjacent jumbo slots); or without grooming, Fatal trap 12: page fault while in kernel mode inside ether_input/bcopy when the read crosses past the pool onto an unmapped page.

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

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

For a smaller-scale proof (total_len = 9017, just 1 byte past buffer): same approach with cur_rx->lge_ctl = (cur_rx->lge_ctl & 0xFFFF0000) | 9017 β€” proves the missing bound with minimal collateral damage and lower crash risk.

Bound the NIC-reported length to the RX buffer geometry before using it as m_len or m_devget copy length. The check runs after the error-bit guard and before any use of total_len, matching the fix applied in every sibling DFly NIC driver.

--- a/sys/dev/netif/lge/if_lge.c
+++ b/sys/dev/netif/lge/if_lge.c
@@ -877,6 +877,19 @@ lge_rxeof(struct lge_softc *sc, int cnt)
            continue;
        }

+       /*
+        * Validate the NIC-reported frame length against the RX
+        * buffer size.  The descriptor fraglen was programmed to
+        * LGE_JUMBO_FRAMELEN - ETHER_ALIGN in lge_newbuf; a
+        * malicious/buggy PCIe device can otherwise report
+        * LGE_RXBYTES up to 65535 (16-bit field) while keeping
+        * the error bits clear, making the stack walk far past
+        * the jumbo buffer into kernel heap.
+        */
+       if (total_len > (LGE_JUMBO_FRAMELEN - ETHER_ALIGN)) {
+           IFNET_STAT_INC(ifp, ierrors, 1);
+           lge_newbuf(sc, &LGE_RXTAIL(sc), m);
+           continue;
+       }
+
        if (lge_newbuf(sc, &LGE_RXTAIL(sc), NULL) == ENOBUFS) {
            m0 = m_devget(mtod(m, char *) - ETHER_ALIGN,
                      total_len + ETHER_ALIGN, 0, ifp);

The check runs after the LGE_RXCTL_ERRMASK guard (line 880) and before total_len is used in either the m_devget path (line 887) or the direct m_len assignment (line 901).

On rejection, the existing jumbo mbuf is re-armed via lge_newbuf(sc, &LGE_RXTAIL(sc), m) (the m != NULL path in lge_newbuf reuses the existing buffer without reallocation), so no buffer is lost.

The upper bound LGE_JUMBO_FRAMELEN - ETHER_ALIGN (9016) matches the buffer capacity programmed at line 710.

  • DF-1490 (twin, if_tx): 16-bit RX length 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-1514 (twin, if_ste): 13-bit RX length OOB.
  • DF-1452 (twin, if_ae): same RX-length OOB.
  • DF-1131 (twin, bwn): same RX-length OOB.
  • DF-1517 (twin, ath): wifi RX length OOB.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1519 Β· 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 639 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 304 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-1519: if_lge.c 16-bit RX length OOB on jumbo cluster

Class: Heap OOB read (16-bit DMA len on 9018-byte jumbo) Cited site: sys/dev/netif/lge/if_lge.c:870,880-901

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/lge/if_lge.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

LGE_RXBYTES(cur_rx) = lge_ctl & 0xFFFF (16-bit, 0..65535) is DMA-coherent and trusted unconditionally. The error filter (880-884) only checks rxctl error bits, depends on NIC reliably setting LENERR. A malicious NIC: lge_ctl=0x0000FFFF, error bits clear. Line 901 sets m->m_pkthdr.len = m->m_len = total_len on a LGE_JUMBO_FRAMELEN=9018 byte jumbo buffer.

Realistic impact ceiling (on suitable HW)

kernel heap OOB read up to ~56K past jumbo cluster

Fix

Clamp total_len to LGE_JUMBO_FRAMELEN - ETHER_ALIGN before the mbuf length assignment.

See fix.diff for the git-apply-able patch.

How to validate the fix

scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1519.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1519.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-1519: if_lge.c 16-bit RX length OOB on jumbo cluster

Verdict

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

The bug is real and present in master DEV source at sys/dev/netif/lge/if_lge.c:870,880-901, 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)

LGE_RXBYTES(cur_rx) = lge_ctl & 0xFFFF (16-bit, 0..65535) is DMA-coherent and trusted unconditionally. The error filter (880-884) only checks rxctl error bits, depends on NIC reliably setting LENERR. A malicious NIC: lge_ctl=0x0000FFFF, error bits clear. Line 901 sets m->m_pkthdr.len = m->m_len = total_len on a LGE_JUMBO_FRAMELEN=9018 byte jumbo buffer.

Reachability on this guest

No β€” sys/dev/netif/lge/if_lge.c:870 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 OOB read up to ~56K past jumbo cluster.

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 LGE_JUMBO_FRAMELEN - ETHER_ALIGN before the mbuf length 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 β€” HW-gated. Primitive is a kernel heap OOB read up to ~56K past a jumbo cluster.

Evidence (decisive lines)

Source: sys/dev/netif/lge/if_lge.c:870 β€” total_len = LGE_RXBYTES(cur_rx) (no bound); :901 β€” m->m_pkthdr.len = m->m_len = total_len. lgereg.h:455 LGE_RXBYTES = lge_ctl & 0xFFFF; :490 LGE_JUMBO_FRAMELEN=9018. Guest has no Level-1 NIC. fix.diff clamps total_len to LGE_JUMBO_FRAMELEN - ETHER_ALIGN.

PoC changes

Created evidence pack from scratch: 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 LGE_JUMBO_FRAMELEN - ETHER_ALIGN before the m_len assignment in lge_rxeof. Full diff in findings/poc/DF-1519/fix.diff.

Verdict

INCONCLUSIVE (HW-gated). Bug confirmed at source level: if_lge.c:870 total_len = LGE_RXBYTES(cur_rx) = lge_ctl & 0xFFFF (16-bit DMA-coherent, 0..65535). Error filter :880-884 only checks rxctl error bits, depends on NIC reliably setting LENERR. A malicious NIC: lge_ctl=0x0000FFFF, error bits clear, line :901 m->m_pkthdr.len = m->m_len = total_len on LGE_JUMBO_FRAMELEN=9018 byte buffer -> ~56K heap OOB read. lge(4) only attaches to Level 1 LX8038 PCI NICs not present on the audit guest.