pf_map_addr()/pf_set_rt_ifp() dereference rpool->cur without NULL guard β reachable as a remote packet-path panic once a translation pool is emptied
Summary
pf_map_addr() loads acur = rpool->cur and immediately dereferences cur->addr.v.a.addr (:2693-2712), and pf_set_rt_ifp does r->rpool.cur->kif (:3642/:3649), with no NULL check - inconsistent with the BINAT-IN path that explicitly guards 'if (cur)' with the comment 'SMP race possible'. DIOCCHANGEADDR PF_CHANGE_REMOVE of the LAST pool address of an active rule drives pool->cur to NULL - and the ioctl itself NULL-derefs right there (pf_ioctl.c:2331), so today the first crash is in the ioctl; any future guard of that ioctl line exposes pf.c's unguarded packet-path derefs to a remote, packet-triggered NULL-deref panic on the now pool-less rule. Defense-in-depth now, remote DoS then. Fix: guard both sides (refuse to empty a live pool + return 1 when cur==NULL). [VERIFIED pass2: reproduced - DIOCCHANGEADDR remove-last-pool-address panics NULL deref fault 0 in the ioctl; fix compile-validated incl. packet-path guards]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3267 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rmpool.c | β | 2.0 KB | view raw | |
| panic.txt | β | 822 B | view raw | |
| README.md | β | 582 B | β raw | |
| VERDICT.md | β | 2.1 KB | β raw | |
| build.sh | β | 157 B | view raw | |
| run.sh | β | 154 B | view raw | |
| manifest.json | β | 685 B | view raw | |
| verdict.json | β | 1.8 KB | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| env.txt | β | 272 B | view raw |
DF-3267 -- verification pack
Finding: pf rpool->cur dereferenced without NULL guard after pool emptying
Status: reproduced / impact panic / confidence certain
What was run
guest: cc -O -o rmpool rmpool.c (uses
How to reproduce
guest: kldload pf; pfctl -f nat.conf (nat on lo0 ... -> 1.2.3.4); ./rmpool
Expected
Fatal trap 12 page fault, fault address 0x0, in the DIOCCHANGEADDR ioctl (see panic.txt)
See VERDICT.md for the full narrative and verdict.json for the persisted machine verdict. Artifacts: rmpool.c, panic.txt, run.log.
DF-3267 verification
Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Bottom line
DIOCCHANGEADDR with PF_CHANGE_REMOVE of the only pool address of an active nat rule panics immediately: pool->cur = TAILQ_FIRST(&pool->list) is NULL after the removal and the very next PF_ACPY(&pool->counter, &pool->cur->addr.v.a.addr, ...) dereferences it - live: Fatal user address access from kernel mode from rmpool, Fatal trap 12 page fault, fault virtual address 0x0, in the ioctl path. The same NULL rpool->cur would later be dereferenced unguarded on the packet path by pf_map_addr (pf.c:2693-2712) and pf_set_rt_ifp (pf.c:3642/3649) once the ioctl line is guarded, so the fix covers both sides (refuse to empty a live pool + NULL guard in pf_map_addr). Root-triggered panic today, defense-in-depth for the remote path.
Kernel references
- sys/net/pf/pf_ioctl.c:2330-2333
- sys/net/pf/pf.c:2693-2712
- sys/net/pf/pf.c:3642-3649
Exploit chain
(none: availability/correctness finding; no privilege chain)
Fix
refuse removal of the last pool address + NULL guard in pf_map_addr; the ioctl-side guard makes the same call return EINVAL
See fix.diff in this directory (git-apply-able against the read-only sys/ tree; applied only inside the disposable verification guest).
Reproduction
build: guest: cc -O -o rmpool rmpool.c (uses
run: guest: kldload pf; pfctl -f nat.conf (nat on lo0 ... -> 1.2.3.4); ./rmpool
expected: Fatal trap 12 page fault, fault address 0x0, in the DIOCCHANGEADDR ioctl (see panic.txt)
Notes on the PoC (what changed vs the finding sketch)
include path is
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.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (trap 12, fault address 0x0, from rmpool), rmpool.c (DIOCGETRULES ticket + DIOCCHANGEADDR dance), run.log
PoC changes
include path is
Verified recommended fix
refuse removal of the last pool address + NULL guard in pf_map_addr; the ioctl-side guard makes the same call return EINVAL
Verdict
DIOCCHANGEADDR with PF_CHANGE_REMOVE of the only pool address of an active nat rule panics immediately: pool->cur = TAILQ_FIRST(&pool->list) is NULL after the removal and the very next PF_ACPY(&pool->counter, &pool->cur->addr.v.a.addr, ...) dereferences it - live: Fatal user address access from kernel mode from rmpool, Fatal trap 12 page fault, fault virtual address 0x0, in the ioctl path. The same NULL rpool->cur would later be dereferenced unguarded on the packet path by pf_map_addr (pf.c:2693-2712) and pf_set_rt_ifp (pf.c:3642/3649) once the ioctl line is guarded, so the fix covers both sides (refuse to empty a live pool + NULL guard in pf_map_addr). Root-triggered panic today, defense-in-depth for the remote path.
No comments yet.