DragonFlyBSD Kernel Audit
← dashboard
DF-0414

Unchecked ph->length in PPPoE discovery packets: remote heap OOB read via get_tag/scan_tags walk bound

Field Value
ID DF-0414
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H
CWE CWE-125 Out-of-bounds Read
File sys/netgraph7/pppoe/ng_pppoe.c
Lines 1314-1619
Area netgraph7 (PPPoE)
Confidence certain
Discovered 2026-07-01
Reported pending

Summary

The PPPoE discovery packet handler (ng_pppoe_rcvdata_ether) reads the length field from the PPPoE header (ph->length) but never validates it against the actual mbuf payload size. Tag-walking functions get_tag() and scan_tags() use this attacker-controlled ph->length as their iteration bound. A remote unauthenticated attacker on the same Ethernet segment can send a minimal PADI/PADO/PADR/PADS frame with ph->length = 0xFFFF, causing the tag walker to read far past the mbuf data into adjacent kernel heap — enabling both remote kernel panic (DoS) and kernel heap information leak via reflected tag data in outgoing responses.

Root cause

ng_pppoe_rcvdata_ether discovery branch (ng_pppoe.c:1314):

length = ntohs(wh->ph.length);
switch(wh->eh.ether_type) {
case ETHERTYPE_PPPOE_DISC:
    /* mbuf contiguity handling (lines 1324-1357) */
    /* ... but NO validation that length <= payload size ... */

The session branch at line 1632 correctly validates:

if (m->m_pkthdr.len < length)
    LEAVE(EMSGSIZE);

But the discovery branch omits this check entirely.

get_tag() at ng_pppoe.c:301-304:

static const struct pppoe_tag*
get_tag(const struct pppoe_hdr* ph, uint16_t idx)
{
    const char *const end = (const char *)next_tag(ph);
    // next_tag returns &ph->tag[0] + ntohs(ph->length)

The comment at line 299 says: "assume we already sanity checked ph->length" — but the discovery path never does.

Threat model & preconditions

  • Attacker position: unauthenticated, on the same L2 segment as a netgraph PPPoE node.
  • Privileges gained or impact: (1) Remote kernel panic via page fault when the OOB read crosses an unmapped page. (2) Kernel heap info leak: scan_tags() copies OOB-derived tag data into outgoing PPPoE responses (PADO/PADR/PADS) via insert_tag(), which are transmitted back on the segment. send_acname() copies up to 31 bytes of OOB data into a NGM_PPPOE_ACNAME control message.
  • Required config: a netgraph PPPoE node attached to an Ethernet interface (client or server side).
  • Reachability: send a single Ethernet frame with ether_type = ETHERTYPE_PPPOE_DISC, ph->length set larger than the actual payload.

Proof of concept

PoC source: findings/poc/DF-0414/poc.py

Build & run

pip install scapy
sudo python3 poc.py --iface eth0

Expected output

Fatal trap 12: page fault while in kernel mode
KDB: stack backtrace:
#1 get_tag at ng_pppoe.c:304
#2 ng_pppoe_rcvdata_ether at ng_pppoe.c:...

Impact

  • Remote unauthenticated single-packet DoS — a crafted PADI with ph->length = 0xFFFF in a 60-byte frame causes an immediate page fault.
  • Remote kernel heap info leak — reflected OOB bytes in PADO/PADR/PADS tag data are received by the attacker, leaking kernel heap contents including potential kernel pointers (KASLR bypass).
  • Any host on the LAN segment running netgraph PPPoE is affected.

Add the length validation to the discovery branch, mirroring the session branch:

--- a/sys/netgraph7/pppoe/ng_pppoe.c
+++ b/sys/netgraph7/pppoe/ng_pppoe.c
@@ -1357,6 +1357,12 @@
        }
    }

+   /* Validate header length against actual payload */
+   if (length > m->m_pkthdr.len - sizeof(*wh)) {
+       LEAVE(EMSGSIZE);
+   }
+
    sp = NG_HOOK_PRIVATE(hook);
 .neghead:

For defense-in-depth, change get_tag()/scan_tags() to accept an explicit pktlen parameter derived from m->m_pkthdr.len - sizeof(*wh) and use min(ph->length, pktlen) as the walk bound.

References

  • PPPoE discovery: RFC 2516 §4.
  • The session branch validation at line 1632 proves the intended contract.
  • FreeBSD's netgraph PPPoE has similar checks in some revisions.

Timeline

  • 2026-07-01 Discovered during automated audit.
  • 2026-07-01 Reported to DragonFlyBSD security contact (pending).