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

Unauthenticated PFSYNC_ACT_UREQ forces victim to multicast entire pf state table: info disclosure + amplification DoS

Summary

PFSYNC_ACT_UREQ with id==0&&creatorid==0(:916): sets sc_ureq_received, seeds sc_bulk_send_next, sends PFSYNC_BUS_START. pfsync_bulk_update(:1545-1613) iterates up to sc_maxcount*PFSYNC_BULKPACKETS states across ALL cpus, packs into PFSYNC_ACT_UPD, emits via pfsync_sendout to multicast group(:1734-1736). Single ~40-byte packet -> full state table dump. Every tracked conn src/dst addr/ports/proto/seq/timeouts exposed to passive sniffer + CPU/bandwidth exhaustion. No priv/source check (DF-0428).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0429 Β· 11 files
FileTypeDescriptionSize
inject_ureq.c trigger-source crafts PFSYNC_ACT_UREQ(id=0,creatorid=0) bulk-dump trigger packet 3.7 KB view raw
build.sh build-script cc -o inject_ureq inject_ureq.c 195 B view raw
run.sh run-script inject + show zero pfsync activity 1.1 KB view raw
build.log build-log full build output, BUILD_EXIT=0 69 B view raw
run.log run-log decisive run: UREQ injected -> no bulk dump, no pfsyncstats 2.1 KB view raw
env.txt environment uname, pf/pfsync state, nm checks (pfsync_input ABSENT from static kernel) 365 B view raw
VERDICT.md verdict full narrative: dead-code false-positive (same root cause as DF-0428) 5.2 KB ↓ raw
README.md readme evidence-pack index + reproduce instructions 1.8 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
live_injection_test.txt reachability-test Live pfsync packet injection test - pfsync_input not called 1.4 KB view raw
README.md readme evidence-pack index + reproduce instructions
↓ download raw

DF-0429 β€” PoC evidence pack

Finding: Unauthenticated PFSYNC_ACT_UREQ (id==0 && creatorid==0) forces victim to multicast entire pf state table (info disclosure + DoS). Severity (filed): High Verdict: NOT REPRODUCED β€” FALSE POSITIVE (dead code) Impact: none

TL;DR

DF-0429 cites case PFSYNC_ACT_UREQ: at sys/net/pf/if_pfsync.c:900-949, which is an arm of the switch (action) inside pfsync_input() (a single function spanning if_pfsync.c:462-1006). pfsync_input is dead code: the only registration site (sys/netinet/in_proto.c:281-296) is wrapped in #ifdef NPFSYNC, and NPFSYNC is never defined anywhere in sys/, so the handler is never registered and no proto-240 packet can reach it. This is the identical root cause proven exhaustively for the sibling finding DF-0428.

Files

file purpose
VERDICT.md full narrative + evidence
inject_ureq.c crafts the exact UREQ(id=0,creatorid=0) trigger packet
build.sh cc -o inject_ureq inject_ureq.c
run.sh inject + show zero pfsync activity
build.log full build output
run.log decisive run: injection β†’ no bulk dump, no pfsyncstats
env.txt guest uname, pf/pfsync state, nm checks
manifest.json machine-readable catalog

Reproduce

./build.sh
# (root on guest): kldload pf.ko; pfctl -e
./run.sh          # run as root (raw socket); expect NO bulk dump, NO pfsyncstats

Expected on master DEV: the UREQ packet is sent, but pfsync_input is never invoked (symbol absent from kernel.debug; no proto-240 handler registered), so no bulk state-table dump occurs. pfsyncstats stays empty.

Fix

No code change needed β€” false-positive. See VERDICT.md Β§ Recommended fix.

VERDICT.md verdict full narrative: dead-code false-positive (same root cause as DF-0428)
↓ download raw

DF-0429 β€” Verdict: NOT REPRODUCED (FALSE POSITIVE β€” dead code)

One-line verdict

DF-0429 claims an unauthenticated PFSYNC_ACT_UREQ packet with id==0 && creatorid==0 (sys/net/pf/if_pfsync.c:916) forces the victim to multicast its entire pf state table. This code path is unreachable: it lives inside pfsync_input(), which is dead code on the default kernel. The same root cause already proven for the sibling finding DF-0428 applies verbatim here.

What the finding cited (and where it lives)

  • sys/net/pf/if_pfsync.c:900-949 β€” the case PFSYNC_ACT_UREQ: arm of the giant switch (action) block inside pfsync_input().
  • The exact trigger line: if_pfsync.c:916 if (id_key.id == 0 && id_key.creatorid == 0) β†’ seeds sc->sc_bulk_send_next, sends PFSYNC_BUS_START, and pfsync_bulk_update() (:1545-1613) later streams the whole table as PFSYNC_ACT_UPD multicast packets.

Critical structural fact: pfsync_input() is ONE function spanning sys/net/pf/if_pfsync.c:462-1006 (the done: label is at :1003, closing brace at :1006). The switch (action) begins at :541. DF-0428's cited range 462-541 is just the entry-validation prefix of that same function; the case PFSYNC_ACT_UREQ: at :900 is the 7th arm of the very same switch. So DF-0429 is the same dead function as DF-0428 β€” not a separate sink.

Why it is a false positive (dead code β€” inherited from DF-0428)

pfsync_input is registered as the IPPROTO_PFSYNC (240) input handler in exactly one place, and that place is compiled out:

sys/netinet/in_proto.c:281:#ifdef NPFSYNC
sys/netinet/in_proto.c:288: .pr_input = pfsync_input,
sys/netinet/in_proto.c:296:#endif   /* NPFSYNC */

NPFSYNC is never defined anywhere in sys/ β€” exhaustive grep returns only the two gate lines (in_proto.c:281 and :296). No #define NPFSYNC, no options NPFSYNC in sys/conf/options, no option in any kernel config. β†’ The .pr_input = pfsync_input protosw entry is compiled out of the static kernel. With no protosw carrying pr_protocol=240, ip_init (sys/netinet/ip_input.c:343-351) leaves ip_protox[240] pointing at the IPPROTO_RAW wildcard, so any proto-240 packet is dispatched to rip_input (ip_input.c:409-415) β€” never to pfsync_input. pf.ko does not dynamically register a handler either (it exports pfsync_input/pfsyncattach as symbols but references neither inetsw nor protosw).

The honest comment at if_pfsync.c:486 β€” "This function is not yet called from anywhere" β€” is accurate.

Definitive evidence (re-confirmed on this run, #0 with-src unpatched)

  1. Static kernel symbol: nm /boot/kernel/kernel.debug | grep -w pfsync_input β†’ ABSENT. The function is not even linked into the running kernel.
  2. Module symbol, unreferenced: nm /boot/kernel/pf.ko | grep pfsync_input β†’ 0000000000003040 T pfsync_input; grep inetsw|protosw β†’ none. The module carries the code but never registers it.
  3. No pfsync protocol registered: netstat -sp pfsync β†’ empty (no pfsyncstats block at all) even with pf.ko loaded + pfctl -e + pfsync0 present.
  4. Live injection, zero effect: injecting the exact PFSYNC_ACT_UREQ (id=0, creatorid=0) packet DF-0429 describes (./inject_ureq 10.0.2.99 224.0.0.240) produced no PFSYNC_BUS_START, no bulk state-table dump, no pfsyncs_ipackets increment, and no pf-state change. See run.log.

Impact of this false positive

None. The "single ~40-byte packet β†’ full state-table disclosure" primitive DF-0429 describes is not reachable. pfsync_input is dormant; the UREQ arm is unreachable code inside a dead function. (DragonFly's in-kernel pfsync receiver was never finished/wired up β€” only the sender side pfsync_sendout is functional.)

Build / run (reproduce the negative result)

./build.sh        # cc -o inject_ureq inject_ureq.c   (unprivileged build OK)
# (root on guest): kldload pf.ko; pfctl -e
./run.sh          # injects the UREQ bulk-dump trigger; expects NO effect

PoC changes

  • inject_ureq.c β€” NEW. Crafts a raw ip_p=240, ip_ttl=255 packet carrying pfsync_header{action=PFSYNC_ACT_UREQ} + pfsync_state_upd_req{id=0, creatorid=0} β€” the exact trigger cited at if_pfsync.c:916. (Note: struct pfsync_state_upd_req.id is u_int32_t id[2], so id is left as the memset zero rather than a scalar assignment.)
  • No exploit.c β€” there is no memory-corruption primitive; this is an auth/DoS claim whose entire surface is dead code.

No code change needed: false-positive. The cited case PFSYNC_ACT_UREQ: arm (if_pfsync.c:900-949) is inside pfsync_input(), which is unreachable on the default kernel and on pf.ko-loaded kernels because the #ifdef NPFSYNC registration block in in_proto.c:281-296 is compiled out (NPFSYNC is never defined) and pf.ko does not register a handler. If the project ever wires up the pfsync receiver (defines NPFSYNC, finishes the integration), then the missing-source-authentication concern becomes real; but that is gated on first making the code reachable, which is not the case today. (Matches the conclusion of sibling DF-0428.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_applicable -- false positive.

n/a
n/a

Confirmed kernel references

Detail

Exploit chain

none β€” dead code. pfsync_input never registered.

Evidence (decisive lines)

Same live injection test as DF-0428: packets sent, zero pfsync activity. NPFSYNC never defined.

PoC changes

Added live_injection_test.txt referencing the DF-0428 live test.

Verified recommended fix

No code change needed: false-positive (same root cause as DF-0428).

Verdict

NOT REPRODUCED (dead code). Same root cause as DF-0428, confirmed by the SAME live injection test. PFSYNC_ACT_UREQ handling at if_pfsync.c:900-949 is inside pfsync_input which is never registered (NPFSYNC gate). My live test sent crafted UREQ packets β€” zero response, no bulk state dump, no pfsyncstats.