sl_compress_init heap overflow via unvalidated max_state from PPP IPCP negotiation
| Field | Value |
|---|---|
| ID | DF-0677 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/net/ppp_layer/slcompress.c |
| Lines | 69, 77-79 |
| Area | net/ppp (VJ compression init) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
sl_compress_init() does not clamp its max_state parameter against
MAX_STATES (16). The sppp caller passes p[4] β an attacker-controlled
byte from the IPCP compression option β directly from a remote PPP peer.
When max_state > 15, the initialization loop writes tstate[i].cs_id
and tstate[i].cs_next out of bounds past the 16-entry tstate[]
array, producing a massive heap overflow (up to ~32KB for max_state=255)
in the M_TEMP kernel heap zone. This is reachable without
authentication when no LCP auth option is negotiated.
Root cause
sl_compress_init() at sys/net/ppp_layer/slcompress.c:63-87:
64: sl_compress_init(struct slcompress *comp, int max_state)
65: {
69: if (max_state == -1) {
70: max_state = MAX_STATES - 1; /* sentinel only */
71: bzero((char *)comp, sizeof(*comp));
72: } else {
73: bzero((char *)comp->tstate, sizeof(comp->tstate));
74: bzero((char *)comp->rstate, sizeof(comp->rstate));
75: }
77: for (i = max_state; i > 0; --i) {
78: tstate[i].cs_id = i;
79: tstate[i].cs_next = &tstate[i - 1];
80: }
Line 69 checks only if (max_state == -1) (sentinel). For any other value,
max_state is used as-is. The loop at line 77 writes tstate[i] for every
i from max_state down to 1. Since tstate[] has only MAX_STATES=16
entries (indices 0-15), any max_state > 15 causes OOB writes.
The caller sppp_ipcp_RCR at sys/net/sppp/if_spppsubr.c:2976 passes
p[4] directly β a raw byte (0-255) from the IPCP Configure-Request
compression option received from the remote PPP peer. The second caller
sppp_ipcp_RCN_nak at if_spppsubr.c:3167 is equally vulnerable.
For max_state=255: i is u_int, iterates 255β1. Entries i=16..255
write 240 cstate-sized chunks (144 bytes each = ~34,560 bytes) past the
slcompress allocation into adjacent M_TEMP heap. Each write deposits a
kernel pointer (cs_next = &tstate[i-1]) and a byte (cs_id = i&0xff).
Threat model & preconditions
- Attacker: a PPP peer (serial line, PPPoE session, or any link using
sppp). In the common no-auth configuration (trusted links, misconfigured PPPoE servers, leased lines), the attacker is unauthenticated βsppptransitions toPHASE_NETWORKwithout authentication when no LCP auth option is negotiated (if_spppsubr.c:2588-2592). VJ compression is enabled by default (CONF_ENABLE_VJatif_spppsubr.c:959). - Trigger: send a single IPCP Configure-Request with option type=2 (compression), protocol=0x002D (VJ), max_state=255, compress_cid=0.
- Impact: kernel memory corruption β reliable panic (DoS) from corrupted heap metadata/objects, and potential kernel code execution (privilege escalation) with heap grooming. The overflow writes are at a known, controllable stride (144 bytes), making heap grooming feasible.
Recommended fix
Clamp max_state to [0, MAX_STATES-1] before the loop:
--- a/sys/net/ppp_layer/slcompress.c
+++ b/sys/net/ppp_layer/slcompress.c
@@ -69,6 +69,13 @@
if (max_state == -1) {
max_state = MAX_STATES - 1;
bzero((char *)comp, sizeof(*comp));
+ } else {
+ /* Clamp to prevent heap overflow from malicious callers
+ * (e.g. unvalidated IPCP max_state from remote PPP peer). */
+ if (max_state < 0 || max_state > MAX_STATES - 1)
+ max_state = MAX_STATES - 1;
bzero((char *)comp->tstate, sizeof(comp->tstate));
bzero((char *)comp->rstate, sizeof(comp->rstate));
}
Additionally, validate p[4] in the sppp callers as defense-in-depth.
References
sys/net/ppp_layer/slcompress.c:69,77-79β the unclampedmax_stateloop.sys/net/sppp/if_spppsubr.c:2976,3167β the unvalidatedp[4]callers.sys/net/slcompress.hβMAX_STATES=16,tstate[MAX_STATES].
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0677 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| slc_oob.c | trigger-source | harness: kmalloc slcompress + guard region, call sl_compress_init(comp,255), detect OOB | 3.0 KB | view raw |
| Makefile | build-config | compiles real slcompress.c from /usr/src into the module | 332 B | β download |
| build.sh | build-script | make -> slc_oob.ko | 198 B | view raw |
| run.sh | run-script | kldload ./slc_oob.ko | 255 B | view raw |
| build.log | build-log | final successful module build | 6.8 KB | view raw |
| run.log | run-log | baseline (unfixed): OVERFLOW_DETECTED evidence + hexdump | 1.3 KB | view raw |
| fix_run.log | run-log | fixed: NO_OVERFLOW | 923 B | view raw |
| fix.diff | suggested-fix | clamp max_state to [0,MAX_STATES-1] before the loop | 687 B | view raw |
| env.txt | environment | uname / kern.version / cc version | 247 B | view raw |
| VERDICT.md | verdict | full narrative + primitive characterization + fix validation | 5.8 KB | β raw |
| README.md | readme | build/run/expected | 1.6 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0677 PoC β sl_compress_init heap overflow
Build (as unprivileged user maxx, or root)
cd findings/poc/DF-0677 ./build.sh # -> slc_oob.ko (compiles the real slcompress.c into the module)
Run (as root β this is a primitive-proof harness for a remote-PPP bug)
./run.sh # kldload ./slc_oob.ko ; watch dmesg dmesg | grep DF0677
Expected (bug present, unfixed kernel/module source)
DF0677: RESULT=OVERFLOW_DETECTED 2015 guard bytes clobbered, farthest write at +32123 bytes past struct DF0677: guard[0..15]: a0 d1 44 18 01 f8 ff ff aa aa 20 aa aa aa aa aa
i.e. sl_compress_init(comp, 255) wrote ~32 KB past the 4656-byte struct slcompress, depositing kernel pointers (cs_next) and a controlled byte (cs_id=0x20) into adjacent kernel heap. (No panic β the corruption is silent; on a real sppp link this corrupts M_TEMP neighbor objects.)
Expected (after applying fix.diff to /usr/src/sys/net/ppp_layer/slcompress.c)
DF0677: RESULT=NO_OVERFLOW guard region untouched (clamp held, max_state=255)
Harness notes
slc_oob.creplicates sppp's allocation exactly (kmalloc(sizeof(struct slcompress), M_TEMP),if_spppsubr.c:964) and callssl_compress_init(comp, 255)(if_spppsubr.c:2976withp[4]=255).- A 64 KB guard region filled with 0xAA sits right after the struct; any byte that is not 0xAA after the call is an OOB write. The first 16 bytes are hexdumped to show the deposited pointer + controlled id.
- The real
slcompress.cis compiled into the module (.PATH: /usr/src/sys/net/ppp_layer), so applyingfix.diffto that file and rebuilding this module tests the exact fix.
DF-0677 β sl_compress_init heap overflow via unvalidated max_state
Verdict: REPRODUCED (heap overflow primitive confirmed + fix validated)
sl_compress_init(comp, max_state) (sys/net/ppp_layer/slcompress.c:64-87) does not
clamp max_state against MAX_STATES (16). The loop
for (i = max_state; i > 0; --i) {
tstate[i].cs_id = i; /* controlled byte */
tstate[i].cs_next = &tstate[i-1]; /* kernel pointer */
}
writes tstate[16..max_state] out of bounds past the 16-entry tstate[] array
embedded in struct slcompress. The sppp caller passes p[4] β a raw byte
(0..255) taken verbatim from a remote PPP peer's IPCP Configure-Request compression
option (sys/net/sppp/if_spppsubr.c:2976, and the NAK path at :3167). sp->pp_comp
itself is a kmalloc(sizeof(struct slcompress), M_TEMP) (if_spppsubr.c:964), so the
overflow corrupts the adjacent M_TEMP kernel heap.
Primitive characterization (measured on this guest)
| property | value |
|---|---|
| allocation overflowed | kmalloc(sizeof(struct slcompress)=4656, M_TEMP) β kmalloc-8192 bucket |
| worst-case overflow | max_state=255 β writes tstate[32..255] past the struct = ~32 KB (32123-byte span) |
| bytes actually written | 2015 bytes, in scattered 9-byte chunks (8-byte pointer + 1-byte id) at a 144-byte stride |
| content control | per chunk: a kernel pointer (cs_next=&tstate[i-1], heap-relative, predictable w/ KASLR off) and a controlled byte (cs_id = i & 0xff, attacker-chosen via max_state) |
| immediate effect | no panic β overflow lands in mapped slab/kmem pages, corruption planted silently |
Decisive harness evidence (dmesg after kldload slc_oob.ko, baseline/unfixed):
DF0677: RESULT=OVERFLOW_DETECTED 2015 guard bytes clobbered, farthest write at +32123 bytes past struct
DF0677: guard[0..15]: a0 d1 44 18 01 f8 ff ff aa aa 20 aa aa aa aa aa
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^
kernel pointer 0xfffff8011844d1a0 cs_id=0x20 (=32, attacker-controlled)
Reachability & threat model
- Trigger path (as filed): remote, unauthenticated PPP peer.
spppreachesPHASE_NETWORK(and thus IPCP) without authentication when no LCP auth option is negotiated (if_spppsubr.c:2588-2592); VJ compression is enabled by default (CONF_ENABLE_VJ,if_spppsubr.c:959). A single crafted IPCP Configure-Request (option type=2 IPCP compression, proto=0x002d VJ, max_state=255) reaches the sink. Common on serial PPP, PPPoE, leased lines. - Local reachability: the same
sl_compress_initis also called from the netgraphng_vjcnode (sys/netgraph7/vjc/ng_vjc.c:316/322) withc->maxChannelfrom aNGM_VJC_SET_CONFIGcontrol message β reachable from userspace viang_socketif that module is loadable by the caller (root-onlykldload, but ng_socket control itself is not privilege-gated). This is a potential local-unprivileged path; not exercised in this run.
Escalation ceiling / why no uid=0 line
This is a remote-peer-driven memory-corruption bug, not a local-unprivileged
syscall bug. The "local unprivileged user β uid=0" primary question does not map
cleanly: locally instantiating a sppp interface (the filed trigger) requires root
(ifconfig create), and the attacker is the remote PPP peer. Given the guest has
no SMAP / no SMEP / no KASLR, the realistic ceiling is remote kernel code
execution: the 32 KB attacker-influenced heap write (controlled byte + predictable
heap pointers, fixed 144-byte stride) is a strong grooming primitive β a remote
attacker who can hold the PPP link open can shape adjacent M_TEMP slabs, corrupt a
victim object holding a function pointer / ucred *, and redirect it to
userspace-resident shellcode (no SMEP) that escalates. Full remote heap-grooming β
code-exec was not developed in this session (it requires a live PPP-link harness);
the primitive is, however, fully demonstrated and characterized above. A local
escalation via the ng_vjc path is the most promising follow-up.
PoC changes
- Wrote
slc_oob.c+Makefile: a loadable harness that replicates sppp's exact allocation (kmalloc(sizeof(struct slcompress), M_TEMP)) and callssl_compress_init(comp, 255), using a 64 KB guard region + 0xAA sentinel to detect the OOB deterministically and hexdump the first written chunk. It compiles the realslcompress.cfrom the source tree (no dependency on the unloaded sppp/sl module), so it exercises the actual vulnerable code. build.sh/run.sh:makethenkldload ./slc_oob.ko.
Fix (fix.diff)
Clamp max_state to [0, MAX_STATES-1] after the max_state == -1 block, before
the loop, covering all callers (sppp p[4], ng_vjc c->maxChannel, and any future
caller) and the negative-int case (which would otherwise wrap a u_int loop index):
if (max_state < 0 || max_state > MAX_STATES - 1)
max_state = MAX_STATES - 1;
This matches the finding proposal's intent (clamp before the loop) but is placed
as a single clamp point rather than inside the else branch, so it also protects the
max_state == -1 re-set path and negatives. Validated: baseline OVERFLOW_DETECTED
(2015 bytes, pointer+0x20) β fixed NO_OVERFLOW (guard untouched).
Kernel references (verified)
sys/net/ppp_layer/slcompress.c:64βsl_compress_init(comp, max_state)sys/net/ppp_layer/slcompress.c:69β only== -1is special-cased, no clampsys/net/ppp_layer/slcompress.c:77-79β the unboundedtstate[i]loopsys/net/slcompress.h:47β#define MAX_STATES 16sys/net/slcompress.h:153βstruct cstate tstate[MAX_STATES]sys/net/sppp/if_spppsubr.c:964βsp->pp_comp = kmalloc(sizeof(struct slcompress), M_TEMP, ...)sys/net/sppp/if_spppsubr.c:2976,3167βsl_compress_init(sp->pp_comp, p[4])(unvalidated byte)
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED. sl_compress_init no max_state clamp -> 32KB heap overflow with kernel ptrs + controlled byte at 144B stride. Remote PPP unauth. Harness-confirmed.
No comments yet.