# DF-0638 — ng_deflate avail_in underflow → OOB heap read

## Verdict
**REPRODUCED — out-of-bounds kernel heap read confirmed; primitive is read-only
(escallation to uid0 not applicable).**

## Mechanism (cited)
`sys/netgraph7/deflate/ng_deflate.c` `ng_deflate_decompress()`:
- `:539` `inlen = m->m_pkthdr.len;` (`int`).
- `:541` only upper-bounds `inlen > DEFLATE_BUF_SIZE (4096)`; **never lower-bounds**.
- `:552-557` protocol field parse: 1-byte proto (`offset=1`) or 2-byte proto
  (`offset=2`). For `PROT_COMPD (0x00fd)` the 2-byte path is taken, `offset=2`.
- `:567-568` seqnum parse reads `inbuf[offset..offset+1]` and does `offset += 2`
  → `offset = 4`.
- `:583` `priv->cx.avail_in = inlen - offset;` — for a 2-byte frame `inlen-offset`
  = `(int)(2-4) = -2` → widened to `(uInt)0xFFFFFFFE` (`avail_in` is `unsigned int`,
  `sys/net/zlib.h`).
- `:589` `inflate(&priv->cx, Z_PACKET_FLUSH)` is told ~4 GiB of input is available
  starting at `inbuf+4`, so it walks the kernel heap past the 4096-byte `inbuf` in
  the heap-allocated `priv` struct — an OOB read driven by attacker input.
- Identical underflow at the `inflateIncomp` sink `:634-636` (`avail_in = inlen - 1`
  when `inlen==0`).
- The `:569` seqnum gate does not help: on a configured node `priv->seqnum == 0` and
  the `priv` struct is `M_ZERO`-allocated (`:178`), so the zeroed-residue
  `inbuf[2..3]` reads back as seqnum 0 == `priv->seqnum` → passes deterministically.

## Proof (decisive)
A raw netgraph7 socket client builds `mkpeer deflate: out <-> decomp`, configures it
(`inflateInit2`, `seqnum=0`), and injects the 2-byte frame `0x00 0xfd` (PROT_COMPD,
2-byte proto) down the `out` hook. `inflate` consumes bytes far past the 2-byte
input and `dmesg` shows:

```
ng_deflate_decompress: decompression error: -3 (invalid stored block lengths)
ng_deflate_rcvdata: error: 5
```

`Z_DATA_ERROR` "invalid stored block lengths" is proof `inflate` read heap memory
beyond the 2-byte frame (it interpreted out-of-bounds heap bytes as a deflate stored
block and rejected them). The OOB read happened; it stayed within mapped kernel
memory here so it returned a data error rather than page-faulting.

## Exploit chain / escalation assessment
The primitive is a **read-only** OOB heap read feeding `inflate`. There is no write.
- If the OOB bytes happen to be valid deflate, `inflate` would decompress them into
  `outbuf` (`:585`) and the result is returned to the peer via `m_devget`+
  `NG_FWD_NEW_DATA` (`:618/:622`) → **kernel-heap info leak**.
- If the OOB read crosses an unmapped page → **page fault → kernel panic (DoS)**.
- uid0 escalation is **not applicable** to a read-only primitive (Phase 6 valid
  blocker). Realistic ceiling: info leak / DoS.

## Reachability (important caveat)
The vulnerable code is in **netgraph7** (ABI 12). The **default** DragonFly GENERIC
kernel ships the **old** `sys/netgraph/` (ABI 2, `net.graph.abi_version=2`) which has
**no deflate node at all**. So on the stock install ng_deflate is neither built nor
reachable; netgraph7 must be explicitly built+loaded. The threat model (remote PPP
peer via ng_ppp → decomp hook) holds wherever netgraph7 is the active stack.

## PoC changes
- Wrote `df0638.c` (raw netgraph7 control+data socket client) and `build.sh`/`run.sh`
  — none existed. The base `ngctl` is ABI-2-only and cannot drive netgraph7, hence
  the custom client.

## Fix
`fix.diff` lower-bounds `inlen` before the subtractions: in the PROT_COMPD path,
reject `inlen < offset + 2` before the seqnum parse / `avail_in` computation; and in
the `inflateIncomp` path reject `inlen < 1` before `avail_in = inlen - 1`. Validated:
on the fixed module the crafted frame is rejected with `EPIPE` (rcvdata error 32)
and **no** decompression-error/OOB read occurs.
