⬢ DragonFlyBSD Kernel Audit
DF-3272 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-3272",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "memcorrupt",
  "confidence": "certain",
  "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 DFlys 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.",
  "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": [
    "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"
  ],
  "kernel_refs": [
    "sys/net/sppp/if_spppsubr.c:2976",
    "sys/net/sppp/if_spppsubr.c:3167",
    "sys/net/sppp/if_spppsubr.c:964",
    "sys/net/ppp_layer/slcompress.c:20",
    "sys/net/slcompress.h:153"
  ],
  "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 guests own /usr/src.",
  "attempts": 6,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "runtime_sec": 2100,
  "guest_dirty": 0,
  "build_cmd": "cd /root/poc && make && cc -O2 -o drive3 drive3.c   (harness KLD + driver; fix build: cd /usr/src/sys/net/sppp && make)",
  "run_cmd": "kldload /root/poc/sppp_harness.ko && /root/poc/drive3",
  "code_hash": "4ebc33c04c73cfeca304586c3f5f0e5c9d0af96d87d650972b6a7ec74fdf401a",
  "notes": "Legacy (PPP) bucket. Delayed panics observed in an earlier session (ip=0x0 jump; vm_fault: fault on stack guard in the sppp_cp_input ICF clone) are consistent with this corruption but not attributably captured; the verdict rests on the proven write. The harness memory-dump ioctl is PoC introspection only -- the trigger is pure peer frames.",
  "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.",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 (sppp.ko rebuilt from patched /usr/src/sys/net/sppp, cc 8.3 -Werror clean)",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "With 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.",
  "fix_evidence": [
    "findings/poc/DF-3272/run.log (baseline corruption)",
    "findings/poc/DF-3272/run.fixed.log (patched: no corruption)",
    "findings/poc/DF-3272/fix.diff"
  ]
}