β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3267

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3267 Β· 10 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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

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 on this tree and needs first for IFNAMSIZ; PF_CHANGE_REMOVE = 5, PF_NAT = 4 per the pfvar enums

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_testable
↓ fix.diffper-fix-DF-3267

Confirmed 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 on this tree and needs first for IFNAMSIZ; PF_CHANGE_REMOVE = 5, PF_NAT = 4 per the pfvar enums

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.