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

RDR reversed translation port range β†’ kernel divide-by-zero panic (unauthenticated packet trigger)

Summary

pf_get_translation() computes the RDR translation port as (dport - rule_port) % (proxy_port[1] - proxy_port[0] + 1) with an unvalidated divisor. A rdr rule with a reversed port range ('-> host port 5001:5000') loads cleanly - pfctl's parseport keeps a>b and pf_addrule has no proxy_port validation - making the divisor exactly 0. The first matching packet executes idiv-by-zero -> Fatal trap 18 -> panic. VERIFIED deterministically: unpriv connect() to the rdr'ed port, 'Stopped at pf_get_translation+0x31a: idivl %ecx,%eax', guest down; fix validated on rebuilt kernel #3 (reversed range rejected at load, valid ranges unaffected). Rule installation needs root (a plausible 'port 5001:5000' typo, or raw DIOCADDRULE); the panic is triggered by any packet matching the rule - remotely/unauthenticated in production, locally unpriv when the rule exists. DoS/panic class only.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3263 Β· 9 files
FileTypeDescriptionSize
rdr_div0.conf β€” 549 B ↓ download
trigger_div0.c β€” 838 B view raw
run.sh β€” 155 B view raw
build.log β€” 307 B view raw
run.log β€” 912 B view raw
panic.txt β€” 13.5 KB view raw
env.txt β€” 419 B view raw
fix.diff β€” 2.1 KB view raw
VERDICT.md β€” 2.6 KB ↓ raw
VERDICT.md
↓ download raw

DF-3263 VERDICT β€” reproduced (panic, unprivileged trigger)

Result

status: reproduced, impact: panic (remote-triggerable DoS once the rule exists; root-loaded rule + unauthenticated/unprivileged trigger packet).

How it was run

  1. Guest booted (vm.sh up), kldload pf, stock kernel DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (6 CPUs).
  2. Root loads the rule set (rdr_div0.conf): pfctl accepts the reversed range β€” pfctl -sn shows rdr pass on lo0 inet proto tcp from any to 127.0.0.1 port = 9998 -> 127.0.0.1 port 5001:5000.
  3. Unprivileged user (uid 1001 maxx) runs trigger_div0 β€” a plain connect("127.0.0.1", 9998); the SYN traverses pf_check_in on lo0, matches the rdr rule, enters pf_get_translation() PF_RDR branch.
  4. Kernel panics before connect() returns (ssh session died; vm.sh status β†’ down; serial log captured):
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer = 0x8:0xffffffff8261389a
kernel: type 18 trap, code=0
Stopped at      pf_get_translation+0x31a:       idivl   %ecx,%eax

Trap 18 is the x86 #DE; the faulting instruction is the idivl of the modulus at sys/net/pf/pf.c:3306-3310 inside pf_get_translation β€” the exact predicted sink. 100% deterministic (one packet, one panic).

Root cause chain

  • usr.sbin/pfctl/parse.y parseport(): 5001:5000 parses to a=5001, b=5000 (a>b not rejected; only a==b collapses).
  • pf_ioctl.c DIOCADDRULE (1262-1273): pool-list checks only; no proxy_port ordering validation.
  • pf.c:3303 if (r->rpool.proxy_port[1]) β†’ true (5000) β†’ pf.c:3308 divisor 5000 - 5001 + 1 = 0 β†’ idiv trap.

Exploit chain

Not applicable (panic/DoS class). In production this is a remote-triggered kernel panic gated on a pfctl-loadable typo'd rule; pfctl will happily load it from any rules file.

Fix

Validated fix.diff (load-time validation in pf_ioctl.c + defensive degenerate-range check in pf.c). Baseline panic captured; patched kernel re-runs the identical trigger with no panic (see run.log / fix validation notes in manifest).

Impact ceiling

Denial of service (panic). No memory-safety implication (integer #DE). Severity Medium: requires a root-loaded misconfigured rule (or root via raw ioctl), triggered by a single unauthenticated packet.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel rejects the reversed-range rule at load (pfctl: DIOCADDRULE: Invalid argument, LOAD-RC=1); identical trigger produces no panic and the guest stays up; a correctly-ordered range (5000:5001) still loads and functions (no regression).

VERDICT.md (fix section); fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #3: Wed Sep 9 21:06:44 UTC 2026

Confirmed kernel references

Detail

Evidence (decisive lines)

['panic.txt: full serial-console capture of the trap (trap 18, idivl in pf_get_translation+0x31a)', "run.log: root loads rule (pfctl -sn echoes 'port 5001:5000'), unpriv user runs /tmp/trig, ssh dies, vm down", 'README.md / VERDICT.md: root-cause chain with path:line']

PoC changes

none - seed concept was correct; rule and trigger built as specified

Verified recommended fix

Validate translation-pool port range ordering in pf_addrule (pf_ioctl.c) and clamp degenerate ranges in the RDR branch of pf_get_translation (pf.c)

Verdict

Deterministic kernel divide-by-zero panic in pf_get_translation()'s RDR port-range math (pf.c:3306-3310): the modulus divisor proxy_port[1]-proxy_port[0]+1 is never validated, and neither pfctl (parseport() keeps a>b ranges) nor the kernel (pf_addrrule) rejects a reversed translation port range. A pfctl-loadable rule 'rdr ... -> host port 5001:5000' makes the divisor exactly 0; the first matching packet (here: a plain unprivileged connect() to the rdr'ed port, rule pre-installed by root) traps the kernel: 'Fatal trap 18: integer divide fault' 'Stopped at pf_get_translation+0x31a: idivl %ecx,%eax'. Verified on DragonFly 6.5-DEVELOPMENT stock kernel; fix validated by rebuilding with load-time validation + defensive range check (reversed rule now rejected: 'pfctl: DIOCADDRULE: Invalid argument', valid ranges unaffected, guest stays up).