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
- walks devclass
lgedevice_listto find eachstruct lge_softc, - waits for the interface to be
IFF_UP(solge_list_rx_inithas populatedlge_rx_list[i]with live jumbo buffers), - locates the RX descriptor at
sc->lge_ldata->lge_rx_list[sc->lge_cdata.lge_rx_cons], - atomically writes
cur_rx->lge_ctl = 0x0000FFFF(length=65535, all error bits clear:LGE_RXCTL_ERRMASK = 0x7C400000, none set in0x0000FFFF) andcur_rx->lge_sts = 0(no checksum-error bits), - triggers
lge_rxeofon 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.
Recommended fix
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.
Related findings
- 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)
PoC verification
Evidence pack
findings/poc/DF-1519 Β· 8 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 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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.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
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- l
- g
- e
- /
- i
- f
- _
- l
- g
- e
- .
- c
- :
- 8
- 7
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- l
- g
- e
- /
- i
- f
- _
- l
- g
- e
- .
- c
- :
- 8
- 8
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- l
- g
- e
- /
- i
- f
- _
- l
- g
- e
- .
- c
- :
- 9
- 0
- 1
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.
No comments yet.