# DF-0276 — Wrong-pointer kfree in DIOCADDADDR error path (PF ioctl)

## Claim
`pf_ioctl.c` `DIOCADDADDR` error path (line 2169) does
`kfree(ap, M_PFPOOLADDRPL)` where `ap` is the framework-owned
`dev_ioctl_args *` parameter of `pfioctl(struct dev_ioctl_args *ap)` (line 981) —
**not** the freshly `kmalloc`'d `pa`. The correct sibling path at line 2178 uses
`pa`. Freeing a non-heap (stack) pointer corrupts slab metadata / panics and also
leaks `pa`. `/dev/pf` is 0600 root:wheel, so this is root→kernel.

## Build
```
cc -o diocaddaddr_wrong_kfree diocaddaddr_wrong_kfree.c
```
(self-contained: no kernel headers; ioctl numbers + struct offsets measured on
this kernel, sizeof(pfioc_pooladdr)=1136.)

## Run (as root — `/dev/pf` is 0600)
```
kldload pf
./diocaddaddr_wrong_kfree
```

## Expected — VERIFICATION OUTCOME: NOT REPRODUCED (dead code)

The PoC was built and run as root on the unpatched guest (pf.ko loaded,
`/dev/pf` 0600). Result:

```
[+] got ticket=2
[+] DIOCADDADDR bogus ifname -> expect kernel panic now
[+] DIOCADDADDR returned rc=0 errno=0 (Undefined error: 0) -- NO panic
```

**No panic, no corruption.** The reason, traced line-by-line in the source:

the buggy branch is guarded by `if (pa->kif == NULL)` at `pf_ioctl.c:2168`.
`pa->kif` is assigned from `pfi_kif_get(pa->ifname)` at `pf_ioctl.c:2167`.
`pfi_kif_get()` (`sys/net/pf/pf_if.c:185`) is a **lookup-or-create** function:
it calls `pfi_kif_find()` (an `RB_FIND`, returns NULL if absent) and, if not
found, **unconditionally `kmalloc`s a new kif with `M_WAITOK`** and returns it
(`pf_if.c:195-201`). `M_WAITOK` never returns NULL (it blocks until memory is
available, or panics the whole system on catastrophic OOM — but never hands back
a NULL pointer). There is **no path** by which `pfi_kif_get()` returns NULL in
normal operation.

Therefore the `if (pa->kif == NULL)` test at `pf_ioctl.c:2168` is **dead code**,
and the buggy `kfree(ap, ...)` at line 2169 is **unreachable at runtime**. The
PoC confirms this: `pfi_kif_get("zznonexist0")` happily *creates* a kif for the
bogus name, `pa->kif` is non-NULL, the success path runs, and `pa` is inserted
into `pf_pabuf` (line 2182) — no error path, no wrong-pointer free, no leak.

## Verdict
**NOT REPRODUCED at runtime — latent (dead-code) defect.** The code at line 2169
is genuinely wrong (it would corrupt/panic *if* reached), but the only caller of
its guard (`pfi_kif_get`) is create-on-demand and never returns NULL, so the
branch cannot be entered on this kernel. It would become a live root→kernel
corruption only if `pfi_kif_get`'s contract were changed to reject unknown names.

The one-line fix (`kfree(ap,...)` → `kfree(pa,...)`) is still correct and worth
applying as defense-in-depth / latent-bug hygiene. It is verified to apply
cleanly and to compile into pf.ko.

## Trigger that was attempted
`DIOCBEGINADDRS` (get ticket) → `DIOCADDADDR` with `addr.type=PF_ADDR_ADDRMASK`,
`af=AF_INET`, `ifname="zznonexist0"` (a name no interface has), carrying the
valid ticket. This reaches `pfi_kif_get("zznonexist0")`, which *creates* a kif
rather than returning NULL, so the `kif==NULL` error path is not taken.
