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)
PoC verification
Evidence pack
findings/poc/DF-3263 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- Guest booted (vm.sh up),
kldload pf, stock kernelDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026(6 CPUs). - Root loads the rule set (
rdr_div0.conf): pfctl accepts the reversed range βpfctl -snshowsrdr pass on lo0 inet proto tcp from any to 127.0.0.1 port = 9998 -> 127.0.0.1 port 5001:5000. - Unprivileged user (uid 1001 maxx) runs
trigger_div0β a plainconnect("127.0.0.1", 9998); the SYN traverses pf_check_in on lo0, matches the rdr rule, enters pf_get_translation() PF_RDR branch. - 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:5000parses 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 divisor5000 - 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
fixedPatched 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
Confirmed kernel references
- sys/net/pf/pf.c:3303
- sys/net/pf/pf.c:3306
- sys/net/pf/pf.c:3308
- sys/net/pf/pf_ioctl.c:1262
- usr.sbin/pfctl/parse.y:3092
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).
No comments yet.