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

pf_get_sport() infinite loop (netisr hang) when the NAT pool port range includes port 0 and all candidate ports are occupied

Summary

The downward port-search loop 'for (tmp = cut - 1; tmp >= low; --(tmp))' uses an unsigned 16-bit counter with no wrap guard. pfctl accepts a NAT pool range containing port 0 ('port 0:65535'; NAT defaults only substitute when BOTH ends are 0), so low==0 reaches this branch; when tmp counts down past 0 it wraps to 0xffff and the condition is always true - once every port for the tuple is occupied the netisr thread loops forever, wedging that CPU's network stack. Requires root-configured port range containing 0 AND ~65536 states on one tuple (needs set limit states raised; DF-3268's missing enforcement makes it achievable) - admin-config-dependent remote hang/DoS. Not guest-verified. Fix: clamp low to 1 after the range swap (the ICMP path already does). [VERIFIED pass2: u_int16_t underflow source-certain but full pool occupancy not achievable on rig lo0 ceiling 10.7K states; fix compile-validated]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3266 Β· 9 files
FileTypeDescriptionSize
fillsyn.c β€” 4.3 KB view raw
README.md β€” 625 B ↓ raw
VERDICT.md β€” 2.2 KB ↓ raw
build.sh β€” 175 B view raw
run.sh β€” 166 B view raw
manifest.json β€” 679 B view raw
verdict.json β€” 2.1 KB view raw
fix.diff β€” 549 B view raw
env.txt β€” 272 B view raw

DF-3266 -- verification pack

Finding: pf_get_sport downward loop underflows when the pool range includes port 0

Status: not_reproduced / impact denial_of_service / confidence likely

What was run

guest: cc -O -o fillsyn fillsyn.c (raw SYN injector with valid checksums)

How to reproduce

guest: pfctl -f conf (port 0:65535 nat on lo0), fillsyn 127.0.0.1 9 fill, then trigger

Expected

rule accepted; full saturation (and thus the unterminating loop) not reached on this rig

See VERDICT.md for the full narrative and verdict.json for the persisted machine verdict. Artifacts: fillsyn.c, run.log.

VERDICT.md
↓ download raw

DF-3266 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

Half verified, half refuted-in-practice: pfctl ACCEPTS a NAT pool whose port range includes 0 (nat ... port 0:65535 loads verbatim), and the downward loop for (tmp = cut - 1; tmp >= low; --tmp) with u_int16_t tmp provably underflows to 0xffff at low==0 (condition always true) - pf.c:3033. But saturating all candidate ports was not achievable on this rig: an external-destination fill wedges the QEMU slirp backend (65K pending host connects) before saturation, and a loopback fill collapses to ~10.7K concurrent states (local RSTs tear the half-open states down; a listener variant did not change the ceiling) while 194 million state-table searches were billed at 3.4M/s during the fill. The trigger connect therefore always found a free port (or was EPERM-blocked by pf) and never entered the unterminating loop. Admin-config-dependent DoS; fix (clamp low to 1, as the ICMP path already does) compile-validated.

Kernel references

Exploit chain

(none: availability/correctness finding; no privilege chain)

Fix

clamp low to 1 after the range swap

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 fillsyn fillsyn.c (raw SYN injector with valid checksums)

run: guest: pfctl -f conf (port 0:65535 nat on lo0), fillsyn 127.0.0.1 9 fill, then trigger

expected: rule accepted; full saturation (and thus the unterminating loop) not reached on this rig

Notes on the PoC (what changed vs the finding sketch)

v1 used deliberately broken TCP checksums, which pf drops with EPERM from the raw sender; v2 computes correct checksums; destination moved inside the guest after the slirp wedge

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-3266

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log x2: rule accepted with port 0:65535; fill runs, state ceilings, searches 194030438 at 3464829.2/s, trigger outcomes

PoC changes

v1 used deliberately broken TCP checksums, which pf drops with EPERM from the raw sender; v2 computes correct checksums; destination moved inside the guest after the slirp wedge

Verified recommended fix

clamp low to 1 after the range swap

Verdict

Half verified, half refuted-in-practice: pfctl ACCEPTS a NAT pool whose port range includes 0 (nat ... port 0:65535 loads verbatim), and the downward loop for (tmp = cut - 1; tmp >= low; --tmp) with u_int16_t tmp provably underflows to 0xffff at low==0 (condition always true) - pf.c:3033. But saturating all candidate ports was not achievable on this rig: an external-destination fill wedges the QEMU slirp backend (65K pending host connects) before saturation, and a loopback fill collapses to ~10.7K concurrent states (local RSTs tear the half-open states down; a listener variant did not change the ceiling) while 194 million state-table searches were billed at 3.4M/s during the fill. The trigger connect therefore always found a free port (or was EPERM-blocked by pf) and never entered the unterminating loop. Admin-config-dependent DoS; fix (clamp low to 1, as the ICMP path already does) compile-validated.