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)
PoC verification
Evidence pack
findings/poc/DF-3491 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 againstm->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 andsendpacket()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).
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):
cs = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL), node namedsrvN(bind),ds = socket(..., NG_DATA)connect'ed tosrvN:.NGM_MKPEER(".:", pppoe, eN, ethernet)+NGM_CONNECT(".:", sN β path "eN" : svcN)+NGM_PPPOE_OFFER("svcN", ac_name="pocAC")β session hooksvcNentersPPPOE_PRIMED.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").- Kernel path:
ng_pppoe_rcvdataelse-branch (:1253) β PRIMED (:1297) β nom_pullupbeyond 20 bytes,length=1024never checked againstm_pkthdr.len=32βget_tag(PTT_SRV_NAME)accepts the tag whose data spans [24,924) of a 32-byte mbuf βmake_packetbcopy'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 =0xfffff8008d9e46f0every round.run.log(busy-heap boot): OOB region contains live kernel pointers (0xfffff8008dcf2800,0xfffff8004f0a25d0, kernel-image pointers0xffffffff806bd330,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 withEMSGSIZE(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
fixedfix.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)
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.
No comments yet.