Remote peer heap out-of-bounds write via IPCP VJ Max-Slot-Id: sl_compress_init(sp->pp_comp, p[4]) unclamped, tstate[16] loop runs to peer-chosen 255
Summary
The IPCP VJ-compression option's Max-Slot-Id byte p[4] (0..255) is passed unclamped into sl_compress_init(), whose tstate loop writes past struct cstate tstate[MAX_STATES=16]: tstate[16..31] smash rstate[] inside the 4656-byte kmalloc'd slcompress object and tstate[32..255] write up to ~32KB past the heap allocation - 9 attacker-positioned bytes per 144-byte stride (valid kernel-heap pointer at +0, index byte at +10), repeating on every IPCP re-negotiation. Reachable by an unauthenticated PPP peer after a 3-frame LCP handshake (no auth by default; CONF_ENABLE_VJ is the default). VERIFIED on guest with a KLD sync-PPP harness + drive frames: kernel-memory dumps show the pointer &tstate[26] + byte 0x1b at comp+3932 and &tstate[31] + 0x20 written 16 bytes PAST the allocation - into the adjacent same-size-class heap chunk (cross-object corruption, exactly as predicted); two deferred panics in an earlier session consistent with later use of corrupted memory. Direct kernel heap corruption from the wire - at minimum remote DoS, plausibly RCE (valid heap pointers placed into adjacent 4097-5120-class objects); uid0 chain documented as needing a matching victim + grooming (per-offset values fixed by the offset). Fix validated (NAK Max-Slot-Id >= MAX_STATES at both sites + defense-in-depth clamp in sl_compress_init).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3272 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| drive3.c | β | 3.8 KB | view raw | |
| poc_3272.c | β | 5.4 KB | view raw | |
| sppp_harness.c | β | 6.2 KB | view raw | |
| build.sh | β | 104 B | view raw | |
| run.sh | β | 126 B | view raw | |
| run.log | β | 3.1 KB | view raw | |
| run.fixed.log | β | 2.2 KB | view raw | |
| env.txt | β | 203 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| manifest.json | β | 957 B | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-3272 β Remote peer heap OOB write via IPCP VJ "Max-Slot-Id" (sl_compress_init unclamped)
What
The peer-supplied IPCP compression option's Max-Slot-Id byte (p[4], 0..255)
is passed unclamped to sl_compress_init(sp->pp_comp, p[4]) at
if_spppsubr.c:2976 (CONF-REQ pass 2) and :3167 (CONF-NAK handler).
sl_compress_init() then executes
for (i = max_state; i > 0; --i) {
tstate[i].cs_id = i; /* struct cstate tstate[MAX_STATES=16] */
tstate[i].cs_next = &tstate[i - 1];
}
sizeof(struct cstate) is 144 (8 ptr + 2+1+1 + 128 hdr, padded); entries
tstate[16..31] land in rstate[] (inside the allocation) and
tstate[32..255] write up to ~32KB past the end of the 4656-byte
kmalloc(sizeof(struct slcompress)) heap object β 9 attacker-positioned
bytes every 144: a valid kernel-heap pointer (&tstate[i-1]) at +0 and the
index byte i at +10.
Reachability
Unauthenticated PPP peer. Default config: CONF_ENABLE_VJ is set at attach
(if_spppsubr.c:958-959), no auth is configured, so a 3-frame LCP handshake
(peer ACKs the kernel's CONF-REQ, sends its own magic-only CONF-REQ) reaches
PHASE_NETWORK; then ONE IPCP CONF-REQ frame with option
02 06 00 2d ff 01 (VJ, Max-Slot-Id=255, Comp-Slot-Id=1) detonates it.
Reproduced (baseline, stock INVARIANTS kernel)
Heap corruption proven by before/after kernel-memory dumps of the pp_comp
neighbourhood (read back via a harness ioctl β introspection only):
- BEFORE: all zeros (fresh
sl_compress_init(comp, -1)). - AFTER: at
comp+3932(=&tstate[27]): pointer0xfffff801185bb4d0β exactlycomp+48+144*26=&tstate[26]β followed by byte0x1b(27). - AFTER, 16 bytes past the end of the allocation, at
comp+4672(=&tstate[32]): pointer0xfffff801185bb7a0=comp+48+144*31, and byte0x20(32) β i.e. the neighbouring 4656-byte heap chunk was overwritten with values predicted exactly by the source analysis.
Delayed panics observed in an earlier session (jump to NULL ip=0x0, and a "vm_fault: fault on stack guard" inside sppp_cp_input's ICF clone) are consistent with later use of the corrupted objects, but were not attributably captured in a clean run (the write is sparse: 9 of every 144 bytes), so the verdict rests on the proven corruption itself.
Exploitability (honest ceiling)
- Primitive: repeatable (every IPCP re-negotiation), attacker-chosen extent
(maxslot 17..255 => reach
comp+2352 .. comp+36768), sparse stride 144, values = valid kernel heap pointers + small integers. Pointer values are valid kernel addresses, so pointer-validation hardening does not help. - Cross-object corruption into the adjacent chunk of the same size class (4097..5120 byte kmallocs) is proven (the tstate[32] write).
- uid0/remote-RCE chain NOT completed this run: needs a victim kernel object
in that size class with an exploitable field at a matching offset
(
+0/+8pointer or+10byte) that the peer can cause to be allocated adjacent β a realistic but non-trivial grooming effort on DFly's size-segregated slab allocator. Documented as the escalation path.
Files
poc_3272.cβ original PoC (handshake + weapon + churn + mem dumps)drive3.cβ the deterministic driver actually used for the recorded run (spray, before-dump, LCP handshake w/ ident re-sync, weapon, after-dump, unspray, churn) β compile & run exactly as in run.shsppp_harness.cβ PoC harness module (see DF-3273 README)run.logβ decisive baseline run (corruption visible in AFTER dumps)run.fixed.logβ same driver on the fix.diff-patched sppp.ko: AFTER == BEFORE (zeros)fix.diffβ clamp Max-Slot-Id to MAX_STATES-1 (NAK out-of-range), same diff also carries the DF-3273 len<4 guardenv.txtβ guest uname etc.
Build / run (guest, root)
cd /root/poc && make && cc -O2 -o drive3 drive3.c
kldload /root/poc/sppp_harness.ko
./drive3
Expected
Baseline: [+] LCP OPENED then AFTER-dumps show stride-144 pointer+index
writes incl. past the end of the allocation (corruption), machine may or may
not panic immediately (sparse write).
Patched: AFTER-dumps identical to BEFORE (all zero), link negotiates on.
DF-3272 β reproduced (remote-peer heap out-of-bounds WRITE, contents verified byte-exact); fix validated (corruption gone).
Root cause (path:line)
- sys/net/sppp/if_spppsubr.c:2976 β
sl_compress_init(sp->pp_comp, p[4])withp[4]= peer's IPCP VJ Max-Slot-Id byte, unclamped (same at :3167). - sys/net/ppp_layer/slcompress.c sl_compress_init(): writes
tstate[i].cs_id=i; tstate[i].cs_next=&tstate[i-1]for i=max_state..1, buttstateisstruct cstate tstate[MAX_STATES=16](sys/net/slcompress.h:153). sizeof(struct cstate)=144. - victim:
sp->pp_comp = kmalloc(sizeof(struct slcompress)=4656)(if_spppsubr.c:964). tstate[32..255] => up to ~32KB past the end.
Reproduction (stock INVARIANTS kernel, guest uname in env.txt)
Peer modeled by frame injection (harness module; see DF-3273/README.md β
sppp code itself stock). driver sequence (drive3.c):
1. read kernel LCP CONF-REQ off the "wire", ACK it (id re-sync loop),
2. send own CONF-REQ with a magic option only -> LCP OPENED, PHASE_NETWORK
(no auth configured; CONF_ENABLE_VJ is the default, if_spppsubr.c:958),
3. inject ONE IPCP CONF-REQ carrying 02 06 00 2d ff 01.
Kernel-memory dumps around pp_comp before/after (harness ioctl, read-only):
BEFORE: all zeros. AFTER (baseline, run.log): comp+3932 (&tstate[27]): 0xfffff801185bb4d0 (= &tstate[26] = comp+48+144*26) followed by byte 0x1b (27) <- cs_id == i comp+4672: 0xfffff801185bb7a0 (= &tstate[31]) + byte 0x20 (32) -> 16 bytes PAST the 4656-byte allocation: the ADJACENT heap chunk (a sprayed 4656-byte object modeling any same-size-class victim) was overwritten with exactly the predicted pointer+index.
So: unauthenticated peer -> kernel heap corruption, cross-object, repeatable (every IPCP CONF-REQ/NAK re-negotiation re-runs the write). No immediate panic in a clean single-shot run (the write is sparse: 9 bytes per 144-byte stride), consistent with the two deferred panics observed in an earlier session (jump to ip=0x0; "vm_fault: fault on stack guard" in the sppp_cp_input ICF clone) after the corrupted regions were later used.
Exploit chain status
- Weaponized frame: done (pure peer frames, 4 total).
- Primitive: attacker-chosen extent (17..255 -> reach comp+2352..comp+36768); at each 144-byte step writes a VALID kernel heap pointer (offset +0) and a small integer (offset +10). Pointer values are live kernel addresses, so the write both corrupts and (if any byte is ever reachable back) leaks kernel-heap layout β no KASLR break needed for the write itself.
- Cross-object corruption into the neighbouring chunk: PROVEN (tstate[32]).
- uid0 / remote code execution: NOT completed. Hard blocker documented: the primitive's per-offset values are fixed by the offset (pointer = &tstate[i-1], id byte = i), so escalation needs a same-size-class (4097..5120-byte) victim whose pointer/length field sits exactly at a 144n+48 / 144n+58 offset from its chunk start and whose corruption yields control β plus heap grooming to place it after pp_comp. On DFly's size-segregated slab allocator that is a research-scale effort; deferred, not impossible.
Fix validation
fix.diff clamps: if (p[4] >= MAX_STATES) { p[4] = sp->ipcp.max_state; break; }
(NAK with our value) in sppp_ipcp_RCR, and skips the re-init in
sppp_ipcp_RCN_nak. Rebuilt in-tree (cc -Werror clean), installed, rebooted,
same driver: AFTER dumps identical to BEFORE (all zeros), link negotiates
normally (run.fixed.log). Baseline corruption gone.
Known-findings residual (not re-reported)
DF-0269 (sppp_print_bytes VLA) and DF-0270 (PAP ACK/NAK debug OOB bound) remain present in the audited source; both are debug-path (IFF_DEBUG) issues.
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
fixedWith fix.diff applied (p[4] >= MAX_STATES -> NAK with our max_state in RCR; skip re-init in RCN_nak), the identical weapon frame leaves the heap identical to baseline (AFTER dumps all zeros, run.fixed.log) and IPCP continues negotiating. Baseline vs patched differ exactly in the corruption.
['findings/poc/DF-3272/run.log (baseline corruption)', 'findings/poc/DF-3272/run.fixed.log (patched: no corruption)', 'findings/poc/DF-3272/fix.diff']
Confirmed kernel references
Detail
Exploit chain
peer frames only: (1) ACK kernel LCP CONF-REQ (id off the wire), (2) send magic-only CONF-REQ -> LCP OPENED -> PHASE_NETWORK (no auth by default, CONF_ENABLE_VJ default on), (3) IPCP CONF-REQ with option 02 06 00 2d ff 01 -> sl_compress_init(comp, 255) -> OOB write comp+2352..comp+36768, 144-byte stride, ptr at +0 / index byte at +10 -> [proven] adjacent same-size-class heap chunk overwritten -> [not completed] victim-object grooming for control flow / uid0
Evidence (decisive lines)
['findings/poc/DF-3272/run.log: AFTER dump at comp+3932 shows 0xfffff801185bb4d0 (= &tstate[26]) + byte 0x1b(27); dump at comp+4672 (16 bytes PAST the 4656-byte allocation) shows 0xfffff801185bb7a0 (= &tstate[31]) + byte 0x20(32) -- cross-object corruption, contents exactly as predicted', 'findings/poc/DF-3272/run.fixed.log: same driver on fix.diff-patched sppp.ko -- AFTER == BEFORE (all zeros)', 'findings/poc/DF-3272/VERDICT.md: full narrative incl. exploitability ceiling']
PoC changes
Original sketch was a standalone C file with wrong assumptions (no driver feeds sppp on this guest; ng_sppp does not compile). Rewrote as a harness KLD modeling the sync-PPP driver (own struct sppp softc + sppp_attach + if_attach + /dev/spppinj0 write->sppp_input, read->TX capture, SIOCSIFFLAGS passthrough) plus a userland driver (drive3.c) that handshakes LCP with ident re-sync (the 3s retransmit timer bumps confid), sprays the slcompress size class, dumps comp memory before/after via a harness ioctl, then churns. Syscall numbers/struct layouts were derived from the guest's own /usr/src.
Verified recommended fix
Clamp the IPCP VJ Max-Slot-Id to MAX_STATES-1 (NAK out-of-range values) before calling sl_compress_init(), at if_spppsubr.c:2976 and :3167; defense-in-depth: clamp inside sl_compress_init() too.
Verdict
Unauthenticated PPP peer heap out-of-bounds WRITE, reproduced with byte-exact contents: an IPCP CONF-REQ VJ-compression option with Max-Slot-Id=255 reaches sl_compress_init(sp->pp_comp, 255) (if_spppsubr.c:2976, also :3167), whose tstate[i] loop (tstate is cstate[16], stride 144) writes up to ~32KB past the 4656-byte pp_comp allocation. Kernel-memory dumps prove a heap pointer + index byte written into the ADJACENT 4656-byte chunk (tstate[32] at comp+4672, past the end), exactly as predicted from source. No immediate panic in a clean single shot (write is sparse: 9 bytes per 144); two deferred panics in an earlier session are consistent with later use of the corrupted objects. uid0/remote-RCE chain not completed: per-offset values are fixed by the offset (pointer=&tstate[i-1], byte=i), so escalation needs a 4097..5120-byte size-class victim with an exploitable field at a matching offset plus grooming on DFly's size-segregated slab allocator -- documented as the escalation path. fix.diff (clamp Max-Slot-Id to MAX_STATES-1, NAK out-of-range) validated in-guest: corruption gone, negotiation proceeds.
No comments yet.