# DF-3537 VERDICT — reproduced (kernel heap info leak)

## Bottom line

**Reproduced.** An unprivileged *frame source* for a configured VJ
decompression path (the remote PPP peer in real deployments; root-configured
netgraph sockets in the local lab) makes the kernel read up to 17 bytes past
the end of a 2..18-byte "compressed TCP" frame and persist those bytes in
kernel-resident decompression state, from which they are re-emitted inside
reconstructed TCP/IP headers. Impact class: **kernel memory info leak**
(stale mbuf contents / cross-packet data disclosure). Not memory corruption;
no write crosses an object boundary (all writes land inside `struct cstate`).

## How it reproduces (guest: DragonFly 6.5-DEVELOPMENT #0, stock INVARIANTS kernel)

Graph: `vinj:d <-> vjc:vjcomp`, `vsyn:d <-> vjc:vjuncomp`, `vrcv:d <-> vjc:ip`,
configured decompression-only (`enableDecomp=1`).

1. **Sanity / mirror check** — a legal 19-byte frame with checksum bytes
   `C7 5D` yields `cstate.th_sum = 0xC75D` via `NGM_VJC_GET_STATE`: the
   engine's read offsets are exactly as modeled (run.log `[sanity]`).
2. **Deterministic oracle** — a **2-byte** frame `[0x40, 0x01]` forces the
   mandatory checksum read (slcompress.c:524) to offsets 2,3 — past
   `m_len=2`. **30/30 rounds** (and 30/30 in a second run) wrote stale
   mbuf bytes into `th_sum` (observed `0x1111` — the *window field* of the
   preceding sync packet, i.e. cross-packet residue; earlier diagnostic runs
   also produced `0x6572`, `0x7995`, `0x0033/0x0034` — see leak_sample.txt).
   The 2-byte frame itself is dropped (`vjlen 4 > buflen 2`) but the OOB
   bytes are already saved in the cstate.
3. **End-to-end exfil** — 4-byte probes (`0x67` change bitmap) walk offsets
   4..15 (all OOB); diagnostic runs corrupted `th_urp/th_win/th_ack/ip_id`
   with earlier-packet residue (diag10: `urp=0x33 win+=0x34` traced to the
   sync frame's seq bytes) and a clean emit frame delivered the corrupted
   header out the `ip` hook (emit output matched the cstate exactly,
   `ip_id+1` implicit increment included). The DECODE-field oracle is
   recycling-dependent (stale bytes often coincide with current state, which
   self-masks); the 2-byte checksum oracle is deterministic.

## Root cause (path:line)

- `sys/netgraph/vjc/ng_vjc.c:456-461` — pullup guarantee is only
  `min(MAX_VJHEADER, m->m_pkthdr.len)` bytes;
- `sys/net/ppp_layer/slcompress.c:524` — mandatory 2-byte checksum read
  before any length validation;
- `sys/net/ppp_layer/slcompress.c:115-140` — DECODE* macros read 1–3 bytes
  each with no `buflen` check;
- `sys/net/ppp_layer/slcompress.c:571-575` — the only length check runs
  after all reads.

## Exploit chain

unprivileged/remote frame source → short vjcomp frame → engine reads stale
mbuf bytes past packet end → values persist in rstate[cid].cs_ip →
subsequent clean compressed frame carries them inside the reconstructed
TCP/IP header to the ip hook (network side) → attacker reads reconstructed
stream. Per probe pair: up to 2 bytes (checksum oracle, node-state visible)
plus up to ~10 bytes (DECODE fields, network-visible). Repeated at will;
leaks whatever previously occupied the mbuf (other sessions' traffic,
kernel structures; adjacent-heap reads possible with cluster-end layouts).

## Fix validation

`fix.diff` adds `buf+buflen` bounds checks to every read in
`sl_uncompress_tcp_core()` (DECODE macros, changes/CID/checksum reads, and
an `ip` header minimum for the UNCOMPRESSED path). Rebuilt `ng_vjc.ko`
(the module compiles `slcompress.c`) and re-ran the identical PoC:

- baseline (unpatched): oracle **30/30** leaks, `run.log`/`run.2.log`
- patched: oracle **0/30** leaks, `[sanity] th_sum = 0xC75D OK`
  (legal 19-byte frames still decompress; no regression), `fix_run.log`

Same validation performed for the ng7 twin (DF-3538): 30/30 → 0/30.

## Threat model / severity notes

Reaching the vjcomp hook requires a configured VJ-compression path — the
same precondition class as DF-3272 (sppp IPCP VJ max-slot OOB write, rated
High): a PPP peer that controls the compressed stream. The *data* is
unauthenticated link-layer input. Locally, building the graph needs root,
so the local lab PoC is a demonstration vehicle, not the threat. Leak-only
(no write primitive) → Medium.

## 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.
