One-frame remote kernel panic: CONF-NAK/CONF-REJ with h->len < 4 reaches RCN handlers that kmalloc() a negative (size_t-wrapped) length
Summary
sppp_cp_input() clamps len = min(m_pkthdr.len, ntohs(h->len)) and re-checks len<4 only for CONF_REQ. CONF_NAK/CONF_REJ dispatch with len in {0..3} into all six RCN handlers, each beginning 'len -= 4; buf = kmalloc(len, M_TEMP, M_INTWAIT);' - a negative int that becomes ~4GB size_t in kmalloc; the oversized path round_page() WRAPS TO 0, the degenerate kmem_slab_alloc corrupts the kernel map, and the kernel panics deterministically. Gate is only h->ident == confid[] (0 until first CONF-REQ, else readable off the wire). VERIFIED: a single 8-byte injected frame (ff 03 c0 21 03 00 00 00) panicked the stock INVARIANTS guest: 'panic: vm_map_entry_link: dup addr map ... kmem_slab_alloc+0x145 <- _kmalloc+0x5be'. Fix validated (len<4 guard on the NAK/REJ arm): same frame counted as ierror, kernel up, LCP negotiation unaffected. Unauthenticated remote peer; single frame; persistent remote DoS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3273 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.1 KB | β raw | |
| VERDICT.md | β | 2.4 KB | β raw | |
| poc_3273.c | β | 1.2 KB | view raw | |
| sppp_harness.c | β | 6.2 KB | view raw | |
| build.sh | β | 108 B | view raw | |
| run.sh | β | 128 B | view raw | |
| panic.txt | β | 748 B | view raw | |
| env.txt | β | 203 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| manifest.json | β | 985 B | view raw | |
| verdict.json | β | 3.4 KB | view raw |
DF-3273 β Remote peer one-frame kernel panic via CONF-NAK/REJ with h->len < 4
What
sppp_cp_input() clamps len = min(m->m_pkthdr.len, ntohs(h->len)) and only
re-validates len < 4 for CONF_REQ (if_spppsubr.c:1417). CONF_NAK/CONF_REJ
are dispatched to the RCN_nak/RCN_rej handlers with len β {0,1,2,3}
(if_spppsubr.c:1541-1544). Every handler starts with
len -= 4;
buf = kmalloc(len, M_TEMP, M_INTWAIT); /* len is now -4..-1 */
kmalloc((size_t)-4) reaches the oversized path in kern_slaballoc.c,
round_page(0xFFFFFFFFFFFFFFFC) wraps to 0, and the degenerate zero-length
kmem_slab_alloc() corrupts the kernel map β instant panic:
panic: vm_map_entry_link: dup addr map 0xffffffff8159eea0 ent 0xffffffff81604448 kmem_slab_alloc() at kmem_slab_alloc+0x145 _kmalloc() at _kmalloc+0x5be
The only gate is h->ident == sp->confid[cp->protoidx]; confid[] is 0 from
attach until the first CONF-REQ, and is otherwise readable off the wire, so an
unauthenticated PPP peer needs exactly ONE frame.
Files
poc_3273.cβ the trigger (injects one 8-byte LCP CONF-NAK, h->len=0)sppp_harness.cβ PoC harness module (models the sync-PPP driver: allocates a realstruct sppp, attaches viasppp_attach(), feeds frames tosppp_input()via /dev/spppinj0, exactly like ng_sppp.c does)panic.txtβ serial-console panic from the baseline runenv.txtβ guest uname etc.fix.diffβ the len<4 guard in sppp_cp_input (also fixes DF-3272)
Build / run (inside the guest, as root)
# harness module (needs /usr/src kernel source)
cd /root/poc
cat > Makefile <<'EOF'
KMOD= sppp_harness
SRCS= sppp_harness.c
KMODDIR= /root/poc
.include <bsd.kmod.mk>
EOF
make
cc -O2 -o poc_3273 poc_3273.c
kldload /root/poc/sppp_harness.ko # pulls in stock sppp.ko
/root/poc/poc_3273 # -> kernel panics (ssh dies)
Expected
Baseline (stock sppp.ko): instant panic, vm.sh status => down (DDB).
With fix.diff applied to if_spppsubr.c and sppp.ko rebuilt+installed:
"write returned 8 / kernel still alive" β frame rejected, no panic.
DF-3273 β reproduced (panic) on the stock INVARIANTS guest; fixed by fix.diff.
How it was reproduced
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), source tree
md5-identical to the audited tree. Because sppp is driver-fed (the only
in-tree consumer is netgraph7/ng_sppp.c, which currently does not compile β
DF-0721/0720), the peer was modeled the way the harness note prescribes: a
PoC module (sppp_harness.c) allocates a real struct sppp in its own softc
(the classic sync-driver pattern), attaches it with sppp_attach()/if_attach(),
and forwards /dev/spppinj0 writes to sppp_input() one-mbuf-per-write β the
same contract ng_sppp.c:ng_sppp_rcvdata() implements. The sppp code under
test is the unmodified stock /boot/kernel/sppp.ko.
poc_3273 injects a single 8-byte frame: ff 03 c0 21 | 03 00 00 00 (LCP CONF-NAK, id=0, h->len=0) -> sppp_cp_input len = min(pkthdr.len=4, h->len=0) = 0 -> CONF_NAK dispatch -> sppp_lcp_RCN_nak(sp, h, 0) -> len = -4 -> kmalloc(0xFFFFFFFFFFFFFFFC, M_TEMP, M_INTWAIT) -> kern_slaballoc.c oversized path: round_page() wraps to 0 -> kmem_slab_alloc(0) degenerate insert -> panic (panic.txt):
panic: vm_map_entry_link: dup addr map 0xffffffff8159eea0 ent 0xffffffff81604448 vm_map_set_wired_quick() at vm_map_set_wired_quick+0x20 kmem_slab_alloc() at kmem_slab_alloc+0x145 _kmalloc() at _kmalloc+0x5be
One frame, unauthenticated peer, deterministic across resets. The identical code path exists in all six RCN handlers (lcp/ipcp/ipv6cp x rej/nak: if_spppsubr.c:2419, 2492, 3090, 3142, 3530, 3581).
Fix validation
fix.diff adds the same len < 4 guard to the CONF_NAK/CONF_REJ arm of
sppp_cp_input that CONF_REQ already has (mirrors if_spppsubr.c:1417).
Applied to /usr/src/sys/net/sppp/if_spppsubr.c in-guest, module rebuilt with
-Werror (clean), installed to /boot/kernel/sppp.ko, guest rebooted:
poc_3273 -> "write returned 8", kernel stays up, frame counted as ierror.
No behavior regression: LCP negotiation proceeds normally (verified by the
DF-3272 driver on the same patched module).
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
fixedPatched sppp.ko: poc_3273 injects the same frame, write() returns 8, kernel stays up (frame dropped as ierror); DF-3272 driver on the same patched module completes a normal LCP negotiation, so no regression.
['findings/poc/DF-3273/panic.txt (baseline)', 'findings/poc/DF-3273/fix.diff', 'DF-3272 pack run.fixed.log (same patched module, negotiation healthy)']
Confirmed kernel references
Detail
Exploit chain
single 8-byte frame ff03c021 03 00 0000 -> negative-length kmalloc -> allocator panic (remote DoS; no memory-control primitive)
Evidence (decisive lines)
['findings/poc/DF-3273/panic.txt: panic: vm_map_entry_link: dup addr map ... kmem_slab_alloc+0x145 <- _kmalloc+0x5be, captured from the serial console of the baseline run', 'findings/poc/DF-3273/VERDICT.md: full path trace incl. round_page wrap']
PoC changes
Standalone C trigger only (8-byte frame write to the harness device); no fixes needed beyond the harness itself (see DF-3272 pack).
Verified recommended fix
Add the missing len<4 guard to the CONF_NAK/CONF_REJ arm of sppp_cp_input (mirroring the CONF_REQ guard at if_spppsubr.c:1417).
Verdict
Unauthenticated PPP peer panics the kernel with ONE frame: LCP CONF-NAK with h->len=0 (ident 0 matches the initial confid, or is read off the wire). sppp_cp_input only re-validates len<4 for CONF_REQ; CONF_NAK/CONF_REJ are dispatched with len 0..3 to the RCN handlers, whose 'len -= 4; kmalloc(len, M_TEMP, M_INTWAIT)' becomes kmalloc(0xFFFFFFFFFFFFFFFC). round_page() wraps the size to 0, the degenerate oversized allocation path corrupts the kernel map, and the kernel panics ('vm_map_entry_link: dup addr map' via kmem_slab_alloc <- _kmalloc). Deterministic across resets on the stock INVARIANTS guest. Same bug in all six RCN handlers (lcp/ipcp/ipv6cp x rej/nak). fix.diff adds the missing len<4 guard to the CONF_NAK/CONF_REJ arm; validated: frame rejected, no panic, LCP still negotiates.
No comments yet.