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

PRIMED branch of ng_pppoe_rcvdata: ph->length never validated against the actual packet length β€” get_tag/scan_tags walk ~900 bytes past a 32-byte mbuf and the OOB heap bytes are disclosed in the emitted PADO (v1 live-stack twin of DF-3487; third sink distinct from DF-0457)

Summary

The session-hook branch for PPPOE_PRIMED (the server-daemon PADI echo path) m_pullups only sizeof(struct pppoe_full_hdr), reads length=ntohs(ph->length) and never compares it to m_pkthdr.len before get_tag(PTT_SRV_NAME), get_tag(PTT_HOST_UNIQ) and scan_tags walk to &ph->tag[0]+ph->length; make_packet bcopy's the attacker-shaped tag into the PADO and sendpacket emits it. DF-0457 covered the ethernet-hook branch; the session branch validates; this PRIMED branch does neither. VERIFIED on guest (pure userland ng_socket harness, root): 32-byte PADI with ph.length=1024 and SRV tag_len=900 -> PADO returns 945 bytes whose SRV tag carries 892 bytes read past the mbuf, 3/3 rounds; on busy boots the PADO's Service-Name tag carried 14 kernel-pointer-like sequences (direct-map + kernel image) plus mbuf metadata. ph->length up to 0xFFFF -> ~64KB walk -> single-packet panic potential. Unauth adjacent peer via standard PADI->PADO relay flow; direct root ng_socket trigger. Fix validated at module level (session-branch check mirrored): 3/3 EMSGSIZE rejections, no PADO, no crash.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3491 Β· 11 files
FileTypeDescriptionSize
ppdprimed.c β€” 10.7 KB view raw
build.sh β€” 85 B view raw
run.sh β€” 133 B view raw
build.log β€” 63 B view raw
run.log β€” 3.8 KB view raw
run.2.log β€” 4.6 KB view raw
fixrun.log β€” 387 B view raw
fix.diff β€” 745 B view raw
VERDICT.md β€” 3.1 KB ↓ raw
README.md β€” 2.8 KB ↓ raw
env.txt β€” 247 B view raw

DF-3491 β€” v1 ng_pppoe PRIMED branch: unvalidated ph->length β†’ kernel

heap OOB read disclosed in the emitted PADO

twin of ng7 DF-3487; third sink distinct from DF-0457 (ethernet-hook path)

File: sys/netgraph/pppoe/ng_pppoe.c β€” the LIVE v1 netgraph stack (stock-loadable: kldload ng_pppoe).

Root cause

ng_pppoe_rcvdata() session-hook branch, PPPOE_PRIMED state (:1297-1359):

  • m_pullup(sizeof(*wh)) only (:1304-1309) β€” 20 bytes contiguous;
  • length = ntohs(wh->ph.length) (:1312) β€” never validated against m->m_pkthdr.len;
  • get_tag(ph, PTT_SRV_NAME) (:1340), get_tag(ph, PTT_HOST_UNIQ) (:1353), scan_tags(sp, ph) (:1356) all bound their walk by &ph->tag[0] + ntohs(ph->length) under the comment "assume we already sanity checked ph->length" (ng_pppoe.c:280-282);
  • insert_tag(sp, tag) β†’ make_packet() (:376) bcopy's the attacker-shaped Service-Name tag (up to ~1470 bytes) into the PADO and sendpacket() emits it on the ethernet hook.

The session branch below (:1221-1229) does validate (m_pkthdr.len < length β†’ drop); the ethernet discovery branch is DF-0457; this PRIMED branch is a third, distinct sink.

Reproduce (guest, root)

kldload ng_socket && kldload ng_pppoe
cc -O -o ppdprimed ppdprimed.c && ./ppdprimed

Pure-userland harness (no helper module): control+data ng_socket; graph srvN:eN<->pppoe:ethernet, srvN:sN<->pppoe:svcN; NGM_PPPOE_OFFER puts svcN in PRIMED; a 32-byte PADI (ph.length=1024, SRV tag claims 900 data bytes, 8 real) is sent down the session hook; the PADO comes back on the ethernet-side hook.

Expected output (stock module)

[+] round N: PADO received: 945 bytes (expected 945 ...)
    [DF-3491] Service-Name carries 892 bytes read OOB past the 32-byte mbuf
      ptr-like sequences found: 14
    [DF-3493] AC-COOKIE = raw kernel sess_con pointer 0xfffff8008d9e65e0

Busy-heap boots leak live pointers in the OOB region β€” run.log (first invocation) contains 0xfffff8008dcf2800 (direct-map heap), 0xffffffff806bd330 / 0xffffffff806bc610 (kernel text/data), 0xfffff8004f0a25d0, mbuf metadata (00 08 00 00, 00 03 c0 00). run.2.log is the 3/3-rounds stability run.

Threat model

The PRIMED branch parses a PADI echoed back by the PPPoE server daemon (the standard PADI→PADO offer flow); a daemon relaying peer-supplied PADI payload from an unauthenticated LAN peer delivers attacker-chosen ph->length. Direct trigger also by the netgraph-controlling root user. Impact: kernel heap disclosure (pointers + adjacent objects) and potential panic when the tag walk crosses an unmapped page (up to 0xFFFF).

Fix

fix.diff mirrors the session-branch check (m->m_pkthdr.len - sizeof(*wh) < length β†’ EMSGSIZE). Validated: after module rebuild the identical input is rejected 3/3 (sendto: Message too long), no PADO, no crash (fixrun.log).

VERDICT.md
↓ download raw

DF-3491 β€” VERDICT

reproduced / leak / certain β€” kernel heap OOB read (892 bytes past a 32-byte mbuf per packet) actively disclosed in an emitted PADO frame, plus panic potential at ph->length up to 0xFFFF. Same defect class and sink shape as ng7's verified DF-3487; this is the v1 (live-stack) twin.

How it was reproduced

./ppdprimed (root, stock ng_pppoe.ko + ng_socket.ko):

  1. cs = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL), node named srvN (bind), ds = socket(..., NG_DATA) connect'ed to srvN:.
  2. NGM_MKPEER(".:", pppoe, eN, ethernet) + NGM_CONNECT(".:", sN ↔ path "eN" : svcN) + NGM_PPPOE_OFFER("svcN", ac_name="pocAC") β†’ session hook svcN enters PPPOE_PRIMED.
  3. sendto(ds, padi32, "sN") with the crafted 32-byte PADI (ph.length=1024, Service-Name tag header claiming 900 data bytes, 8 real bytes "SERVPADI").
  4. Kernel path: ng_pppoe_rcvdata else-branch (:1253) β†’ PRIMED (:1297) β†’ no m_pullup beyond 20 bytes, length=1024 never checked against m_pkthdr.len=32 β†’ get_tag(PTT_SRV_NAME) accepts the tag whose data spans [24,924) of a 32-byte mbuf β†’ make_packet bcopy's 4+900 bytes β†’ PADO (945 bytes total) emitted to the ethernet hook β†’ captured on the ng_socket data hook.

Decisive evidence

  • run.2.log: 3/3 rounds, each PADO exactly 945 bytes with the Service-Name tag carrying 892 bytes beyond the 32-byte input; AC-COOKIE = 0xfffff8008d9e46f0 every round.
  • run.log (busy-heap boot): OOB region contains live kernel pointers (0xfffff8008dcf2800, 0xfffff8004f0a25d0, kernel-image pointers 0xffffffff806bd330, 0xffffffff806bc610) and mbuf metadata β€” 14 ptr-like sequences in one capture. Kernel-memory pattern consistent across runs.
  • fixrun.log (patched module, fix.diff): identical input rejected 3/3 with EMSGSIZE (the new check), zero PADO output, no crash.

Exploit chain / ceiling

Information disclosure: unauthenticated LAN peer (when the server daemon relays PADI payload, the standard offer flow) reads kernel heap around the mbuf zone: pointers defeat KASLR-style layout randomization and feed follow-on corruption bugs. ph->length up to 0xFFFF can walk the tag header parse ~64 KB past the packet β†’ single-packet panic potential (not needed for the leak; DF-3487's ng7 analysis showed the same). No write primitive on this path.

Fix validation

fix.diff adds the session-branch check to the PRIMED branch (:1312β†’ after code = wh->ph.code;): if (m->m_pkthdr.len - sizeof(*wh) < length) LEAVE(EMSGSIZE);. Module rebuilt in-guest (/usr/src/sys/netgraph/pppoe), reloaded, PoC re-run: baseline 945-byte PADO w/ OOB β†’ patched: sendto: Message too long 3/3, no PADO, no crash. fix_status=fixed.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src copy, module rebuilt and reloaded: identical 32-byte PADI/ph.length=1024 input now rejected 3/3 with EMSGSIZE ('Message too long') at sendto, no PADO emitted, no panic, guest stable.

fixrun.log (3/3 EMSGSIZE rejections on patched module); contrast run.log/run.2.log (945-byte PADO with 892 OOB bytes on stock module)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 + rebuilt ng_pppoe.ko (module-level fix validation)

Confirmed kernel references

Detail

Exploit chain

unauthenticated LAN peer -> PADI relayed by server daemon down its PRIMED session hook (standard PADI->PADO offer flow; direct trigger also by root ng_socket user) -> get_tag walk end = &ph->tag[0]+ph->length far past packet -> OOB heap bytes embedded in PADO SRV tag -> heap layout disclosure (KASLR defeat, slab reconnaissance). ph->length up to 0xFFFF can walk ~64KB into unmapped VA -> single-packet panic potential.

Evidence (decisive lines)

run.2.log: 3/3 rounds, 945-byte PADO, 'Service-Name carries 892 bytes read OOB'; run.log: ptr-like sequences found: 14 (0xfffff8008dcf2800 etc.); fixrun.log: patched module rejects 3/3 with EMSGSIZE, no PADO

PoC changes

Seed was the ng7 DF-3487 kernel-module harness; rewrote as a pure-userland v1 ng_socket program (control+data sockets, NG_MKPEER/NGM_CONNECT/NGM_PPPOE_OFFER control messages, 32-byte crafted PADI sent on the session data hook); fixed the NG_DATA connect path to use an absolute node ref ("srvN:") and added per-round fresh graphs because PRIMED->SOFFER transitions after the first PADI.

Verified recommended fix

Mirror the session-branch check in the PRIMED branch: reject when m->m_pkthdr.len - sizeof(*wh) < ntohs(ph->length) (see fix.diff)

Verdict

v1 ng_pppoe (live stack) ng_pppoe_rcvdata() PPPOE_PRIMED branch never validates ph->length against the actual mbuf length before get_tag()/scan_tags() walk; make_packet() copies the attacker-shaped Service-Name tag into the PADO and emits it. Reproduced 3/3 with a pure-userland ng_socket harness: 32-byte PADI with ph.length=1024 and SRV tag_len=900 yields a 945-byte PADO whose SRV tag carries 892 bytes read past the 32-byte mbuf; busy-heap captures contain kernel pointers (0xfffff8008dcf2800, 0xffffffff806bd330, 0xffffffff806bc610, 0xfffff8004f0a25d0) and mbuf metadata. Third distinct sink: DF-0457 covered the ethernet discovery branch, the session branch checks, this one does not. Fix validated at module level.