# DF-3296 — pf DIOCCHANGEADDR: removing a pool's LAST address NULL-derefs the kernel

## What
`pfioctl()` DIOCCHANGEADDR with `action = PF_CHANGE_REMOVE` frees the
selected `struct pf_pooladdr` and then unconditionally does
(pf_ioctl.c:2330-2332):

```c
pool->cur = TAILQ_FIRST(&pool->list);          /* NULL when pool now empty */
PF_ACPY(&pool->counter, &pool->cur->addr.v.a.addr, pca->af);
```

`PF_ACPY` -> `pf_addrcpy()` (pf.c:411) reads `src->addr32[0]` at virtual
address 0 -> fatal page fault.  With `pca->af = AF_UNSPEC` the copy is a
no-op, the ioctl returns success, and the owning rule is left with
`rpool.cur == NULL` — the next packet matching that nat/rdr/binat/route-to
rule NULL-derefs in `pf_map_addr()` (pf.c:2693/2712).

## Reachability
- /dev/pf is created `0600 root:wheel` (pf_ioctl.c:3360) and
  DIOCCHANGEADDR is a write ioctl (not in the FREAD allow-list,
  pf_ioctl.c:1047-1098) -> requires root (or a devfs rule granting write).
- Deterministic: one ioctl sequence, no race, 100% reproducible.

## Reproduce
```
kldload pf.ko                       # if not loaded
cc -O2 -o trigger_changeaddr trigger_changeaddr.c
./trigger_changeaddr                # ioctl variant: panic in pf_addrcpy
# ./trigger_changeaddr datapath enable   # packet variant: panic in pf_map_addr
```

## Expected
ioctl variant (baseline, stock pf.ko):
```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x0
fault code = supervisor read data, page not present
Stopped at      pf_addrcpy+0x11:        movl    (%rsi),%eax
```
packet variant (baseline): `Stopped at pf_map_addr+0x76: movzbl 0x28(%rbx),%ecx`

## Fix validation
`fix.diff`: refuse removing the last pool address (EBUSY) + defensive
NULL guard around the PF_ACPY.  Validated by rebuilding only the pf
kernel module in the guest:

```
cd /usr/src && patch -p0 < fix.diff
cd /usr/src/sys/net/pf && make obj && make
kldload /usr/obj/usr/src/sys/net/pf/pf.ko
./fixcheck        # remove non-last: OK; remove last: "Device busy", no panic
./trigger_changeaddr   # "ioctl returned Device busy (no panic?)"
```

Note for the datapath variant re-run: leave DIOCSTART *out* or bind the
test NAT rule to lo0 — an unconstrained `nat -> 10.9.9.9` rule with pf
enabled NATs the ssh session itself and black-holes guest networking
(kernel stays up; this is a harness artifact, not the bug).
