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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4113 Β· 7 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4113

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.