nge_rxeof trusts device-written 16-bit descriptor BUFLEN: unbounded mbuf length on the jumbo minicluster (OOB read propagated up the network stack)
Summary
NGE_RXBYTES is the chip-DMA-written nge_ctl & 0xFFFF consumed with no clamp. On the x86-64 path m_len = total_len on a 9024-byte NGE_JLEN slot with only 9016 usable; the m_devget fallback likewise copies total_len+2 bytes out of that slot. A PKT_OK descriptor with BUFLEN > 9016 yields up to ~56KB OOB read past the cluster delivered to sockets/BPF or a page-fault panic. Giants accepted by config, MTU to 9000 - only the compliant chip's own bounds stand in the way. Distinct from DF-1384 (lifetime race). Clamp-and-recycle fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4113 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 2.8 KB | β raw | |
| presence_probe.sh | β | 425 B | view raw | |
| env.txt | β | 347 B | view raw | |
| fix.diff | β | 772 B | view raw | |
| manifest.json | β | 699 B | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4113 β nge(4): nge_rxeof trusts device-written 16-bit descriptor BUFLEN β unbounded mbuf length on the jumbo cluster
What this is
nge_rxeof() (sys/dev/netif/nge/if_nge.c:1198-1304) reads the received
frame length straight out of the DMA-visible RX descriptor:
total_len = NGE_RXBYTES(cur_rx); /* if_nge.c:1225 = cur_rx->nge_ctl & 0xFFFF (if_ngereg.h:517,527) */
and then, on the live x86-64 path, installs it as the mbuf length with no clamp:
} else { /* nge_newbuf() succeeded */
m->m_pkthdr.rcvif = ifp;
m->m_pkthdr.len = m->m_len = total_len; /* if_nge.c:1270 */
}
...
ifp->if_input(ifp, m, NULL, -1); /* if_nge.c:1300 */
The RX buffer is one jumbo minicluster: NGE_JLEN = 9024 bytes with 8
consumed by the alignment m_adj (if_nge.c:1063-1074), i.e. 9016 usable
β and the descriptor told the chip only 9010 (c->nge_ctl = m->m_len).
Nothing checks total_len against any of that. A descriptor BUFLEN
above 9016 with PKT_OK set produces an m_len up to 65535 on a 9 KB
allocation β ether_input and everything above walks past the cluster
(up to ~56 KB OOB read propagated to sockets/BPF = kernel heap info
leak, or page-fault panic).
The fallback copy path is equally unbounded: m_devget(mtod(m, char *)
- ETHER_ALIGN, total_len + ETHER_ALIGN, 0, ifp) (if_nge.c:1255-1256)
copies total_len + 2 bytes out of the 9024-byte cluster β the copy
destination is fine (m_devget chunks correctly), the source read is
OOB for total_len > 9016.
Note the chip is run with NGE_RXCFG_RX_GIANTS (if_ngereg.h:308) and
MTU up to NGE_JUMBO_MTU=9000 (if_nge.c:1994), so oversized frames are
accepted, not dropped by config β the only thing standing between a
giant frame and an OOB is the chip's own descriptor-bounds behavior on
a compliant device. A faulty or hostile DP83820 (or any PCIe peer
writing descriptor memory) bypasses it.
Threat model
Requires a malicious/malfunctioning NIC or corrupted descriptor DMA β not triggerable by an unprivileged user on healthy hardware. Hence Medium, memcorrupt-bucket (OOB read, no write).
PoC status β untested (hardware absent)
Guest has vtnet0 lo0 only β no NS DP83820/83821 (presence_probe.sh).
Source-level certainty.
Fix
Clamp total_len to the usable cluster size and recycle the descriptor
(fix.diff).
DF-4113 β VERDICT
Finding: nge_rxeof() uses the device-DMA-written 16-bit BUFLEN
field (NGE_RXBYTES(cur_rx) = nge_ctl & 0x0000FFFF,
sys/dev/netif/nge/if_nge.c:1225, if_ngereg.h:517/527) as the mbuf
length with no clamp against the jumbo minicluster backing the buffer.
Status: untested. The audit guest has no DP83820/83821 device
(ifconfig -l β vtnet0 lo0; no NS PCI IDs β presence_probe.sh), so
the corrupted-descriptor state cannot be produced on the guest. Source
certainty: the missing bound is unconditional on every received frame
that has PKT_OK set.
Source-level trace (proof):
1. RX buffers are jumbo miniclusters: nge_newbuf attaches an
NGE_JLEN-slot (9024 B) buffer (if_nge.c:1059-1067, NGE_JLEN math at
if_ngereg.h:604-609), burns 8 bytes for alignment
(m_adj(m_new, sizeof(uint64_t)), if_nge.c:1074), and gives the chip
c->nge_ctl = m_new->m_len (if_nge.c:1078) = 9010 usable descriptor
size.
2. On a completed descriptor the driver checks only
NGE_CMDSTS_PKT_OK (if_nge.c:1233) β no length sanity β takes
total_len = nge_ctl & 0xFFFF (:1225) and, on the live x86-64 path
(the audit guest's platform), installs it:
m->m_pkthdr.len = m->m_len = total_len (if_nge.c:1270) then
if_input (:1300). For any device-fed BUFLEN > 9016, everything
above ether_input reads past the 9024-byte slot (up to ~56 KB OOB) β
kernel heap disclosure to sockets/BPF, or fault on unmapped page.
3. The m_devget fallback (non-x86-64 or newbuf-ENOBUFS) copies
total_len + ETHER_ALIGN bytes from mtod(m) - ETHER_ALIGN
(if_nge.c:1255-1256): destination chunking is correct (m_devget,
sys/kern/uipc_mbuf.c:2234-2269), the source cluster read is OOB
for the same oversized BUFLEN β the leak persists on that path.
4. The chip runs with giants accepted (NGE_RXCFG_RX_GIANTS in
NGE_RXCFG, if_ngereg.h:306-308; programmed at if_nge.c:1788) and the
driver accepts MTU up to NGE_JUMBO_MTU=9000 (if_nge.c:1994) β a
compliant chip self-limits DMA to the descriptor size, but nothing
in the kernel enforces it.
Impact ceiling: kernel heap info leak (read primitive) + panic; no write primitive. Hardware-gated (faulty/hostile NIC or peer DMA).
Fix: clamp total_len > NGE_JLEN - sizeof(uint64_t) β ierror +
recycle via nge_newbuf(sc, cur_rx, m) (same shape as the existing
PKT_OK-error path at if_nge.c:1233-1237). fix.diff; not validatable
on this guest β fix_status not_tested.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff authored against the read-only sys/ tree (patch --dry-run clean); cannot be validated on a guest without DP83820 hardware.
findings/poc/DF-4113/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4113/VERDICT.md (trace if_nge.c:1225/1270/1255-1256/1074/1078/1300/1233-1237, if_ngereg.h:517/527/604-609); presence_probe.sh + env.txt; fix.diff (NGE_JLEN clamp + recycle)
PoC changes
No seed PoC. Hardware-gated (no DP83820 on guest); presence probe only.
Verified recommended fix
Clamp NGE_RXBYTES() to NGE_JLEN - sizeof(uint64_t) in nge_rxeof and recycle the descriptor (PKT_OK-error-path shape) when exceeded.
Verdict
Source-certain, hardware-gated. nge_rxeof takes the received frame length from the device-DMA-written 16-bit BUFLEN field (NGE_RXBYTES(cur_rx), sys/dev/netif/nge/if_nge.c:1225; if_ngereg.h:517/527) with no kernel-side clamp, then installs it as the mbuf length on the live x86-64 path (m->m_pkthdr.len = m->m_len = total_len, if_nge.c:1270) on an NGE_JLEN=9024-byte jumbo minicluster with only 9016 usable after the alignment m_adj (if_nge.c:1074; descriptor advertises 9010, if_nge.c:1078). A PKT_OK descriptor with BUFLEN > 9016 (faulty/hostile DP83820 or peer descriptor-DMA corruption) yields up to ~56KB OOB read past the cluster propagated through if_input (if_nge.c:1300) to sockets/BPF (kernel heap info leak) or a page-fault panic; the m_devget fallback path (if_nge.c:1255-1256) reads total_len+2 bytes from the same 9024-byte cluster - equally OOB. Giants are enabled (NGE_RXCFG_RX_GIANTS, if_ngereg.h:306-308) and MTU up to 9000 accepted (if_nge.c:1994): only the compliant chip's own descriptor bounds stand between an oversized frame and the OOB. Guest has no NS DP83820 (presence_probe.sh: NGE_ABSENT) - honestly untested; not a false positive: the missing bound is unconditional on every received frame.
No comments yet.