# DF-0603 — Verdict

## Verdict
**REPRODUCED.** Info-leak confirmed: `DIOCIGETIFACES` returns the entire in-kernel
`struct pfi_kif` to userspace verbatim, including raw kernel pointers (RB tree
linkage, `pfik_ifp`, `pfik_group`, `pfik_dynaddrs` TAILQ_HEAD). On the unpatched
`6.5-DEVELOPMENT #0` kernel the PoC observes **34 kernel-pointer-shaped qwords
across 9 kifs** in a single call. Three independent runs return the same count
(deterministic). The single-fix module brings this to **0** with no functional
regression (kif names, counters, flags, states, rules all still returned).

## Mechanism (trigger → primitive → effect)

1. **Trigger** — open `/dev/pf` (root-only; mode `0600 root:wheel`) and issue
   `ioctl(fd, DIOCIGETIFACES, &io)` with `io.pfiio_esize = sizeof(struct pfi_kif)`
   and a sufficiently large `pfiio_buffer`. Dispatch:
   `sys/net/pf/pf_ioctl.c:3019-3028` validates only `pfiio_esize ==
   sizeof(struct pfi_kif)` and forwards to `pfi_get_ifaces()`.
2. **Primitive (wholesale copyout)** — `sys/net/pf/pf_if.c:763-790`,
   specifically line **778**: `copyout(p, buf++, sizeof(*buf))`. `p` is a live
   `struct pfi_kif *` from the in-kernel RB tree `pfi_ifs`. The struct layout
   (`sys/net/pf/pfvar.h:1171-1183`) carries these kernel-pointer fields:
   - `pfik_tree.rbe_left / rbe_right / rbe_parent` (3 ptrs, RB_ENTRY at offset 16/24/32 — `sys/sys/tree.h:333-339`)
   - `pfik_ifp` (`struct ifnet *`, offset 184)
   - `pfik_group` (`struct ifg_group *`, offset 192)
   - `pfik_dynaddrs.tqh_first / tqh_last` (TAILQ_HEAD, offset 208/216; for empty lists `tqh_last` points back into the kif itself)
3. **Effect** — a comprehensive host-kernel heap/text pointer oracle: 3–5 live
   kernel pointers per kif × N kifs. On this guest (KASLR off) the leak is
   redundant for symbol resolution but is still a real pointer disclosure; on
   a KASLR-hardened kernel it would defeat KASLR and reveal heap layout (the
   classic precondition for turning a separate UAF/overflow primitive into a
   reliable exploit). Across a jail boundary with delegated `/dev/pf`, this
   leaks host-kernel addresses to a sandboxed context.

## Privilege / threat model
- `/dev/pf` is `0600 root:wheel`. Confirmed: as unprivileged `maxx` (uid 1001),
  `cat /dev/pf` ⇒ `Permission denied` and `open()` fails. PoC must run as root
  (or inside a jail with `/dev/pf` delegated via devfs rules — a standard
  jail-with-pf setup, per the finding's threat model).
- This is a **pure info-leak (CWE-200)** — no write primitive, no escalation
  chain exists. Per Phase 6 hard-blocker rule, read-only primitives have no
  escalation chain; impact ceiling is KASLR-defeat / heap-layout disclosure.

## PoC changes
The original PoC folder shipped only a `README.md` (no source). I wrote
`pfleak.c` from scratch:
- Uses `<net/pf/pfvar.h>` for `struct pfi_kif` / `struct pfioc_iface` /
  `DIOCIGETIFACES`.
- Allocates a 64-entry buffer and issues one `DIOCIGETIFACES`; reports the
  returned count.
- For each kif, scans the 8-byte qwords of the struct and prints any word
  matching the canonical x86_64 kernel-address mask
  `(v & 0xffff000000000000) == 0xffff000000000000`.
- Prints name/flags/tzero/states/rules so functional correctness is visible
  pre/post fix.
- Exits non-zero if no pointer was leaked (used by the fix-validation step as
  the "fixed" success marker).

## Exploit chain
N/A — pure read-only info leak. No escalation chain is derivable; the impact
ceiling is host-kernel pointer disclosure (KASLR-defeat + heap-layout oracle
for a privileged `/dev/pf` opener, or a cross-jail host-address leak).

## Fix validation (Phase 8)

**Important architectural note**: PF is **not compiled into the GENERIC kernel**
(`sys/conf/files:1576` marks `net/pf/pf_if.c` as `optional pf`, and
`sys/config/X86_64_GENERIC` does not include `device pf`). PF ships as a loadable
module at `/boot/kernel/pf.ko`. The bug therefore lives in the module, and the
correct validation vehicle is a rebuilt `pf.ko`, not a rebuilt kernel
(`make nativekernel` was started, discovered to be a no-op for this code path,
and abandoned — see `fix_build.log`).

**Single-fix build**: applied `fix.diff` to `/usr/src/sys/net/pf/pf_if.c` on the
`with-src` snapshot, then `cd /usr/src/sys/net/pf && make
KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC` ⇒ `pf.ko` rebuilt, rc=0
(`fix_build.log`).

**Before/after** (same guest, same kernel, swapped module + reloaded):

| kernel/module              | PoC output                                                        |
|----------------------------|-------------------------------------------------------------------|
| unpatched `/boot/kernel/pf.ko` (3.6 MB, buildID `d3c1d9…`) | `SUMMARY: 34 kernel-pointer-shaped qwords leaked across 9 kif(s)` |
| fixed `/boot/kernel/pf.ko` (377 KB, buildID `a6d0a4…`)     | `SUMMARY: 0 kernel-pointer-shaped qwords leaked across 9 kif(s)`  |

3 consecutive runs on the patched module all return **0** leaks. The kif names,
counters, flags, states and rules are still returned correctly (no functional
regression). Fix closes the leak.

## Recommended fix
`fix.diff` zeroes the pointer fields of a stack-local copy of the kif before
`copyout()` — same shape as the finding markdown's `## Recommended fix`
proposal. `matches finding proposal`. A longer-term fix (separate
userspace-visible struct mirroring OpenBSD's `pfi_kif_uv`) is out of scope for
this verification.
