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

pfsync_input has no source/peer authentication: any on-link host can inject/modify/destroy pf state

Summary

pfsync_input(:462-541) is live IPPROTO_PFSYNC(240) handler (in_proto.c:281-288 β€” stale comment "not yet called" is wrong). Validates only: sync ifp configured(:489), pkt on sync_ifp(:493), TTL==255(:499), version/action(:521,:530). Captures ip_src(:536) for replies but NEVER compares vs configured sc_sync_peer. Even with unicast peer configured (SIOCSETPFSYNC :1076-1080), accepts INS/UPD/DEL/CLR/UPD_C/DEL_C/UREQ/BUS from ANY source. TTL==255 trivially satisfied on-link. pfsyncr_authlevel defined but never enforced. Any on-link host: insert states to bypass firewall(INS), delete states to sever connections(DEL/CLR), corrupt TCP seq window(UPD). [PASS-2 REACHABILITY CORRECTION (DF-3379): the pfsync engine is dead code as shipped - pfsyncif never assigned, RX dispatch compiled out behind undefined NPFSYNC - so these unauthenticated-protocol findings are NOT exploitable on stock systems; they re-arm the moment the engine is wired without peer authentication (fix DF-3378/3379 first)]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0428 Β· 14 files
FileTypeDescriptionSize
setup_pfsync.c trigger-source issues SIOCSETPFSYNC to configure pfsync0 (ifconfig does not expose it) 2.0 KB view raw
inject_pfsync.c trigger-source crafts raw proto-240 PFSYNC_ACT_CLR packet with spoofed source / TTL=255 3.7 KB view raw
run_on_guest.sh trigger-source root-side script: load pf, configure pfsync0, inject, capture negative-result evidence 1.1 KB view raw
build.sh build-script cc setup_pfsync.c + inject_pfsync.c 65 B view raw
run.sh run-script wrapper invoking run_on_guest.sh 141 B view raw
build.log build-log full build output (both binaries, rc=0) 150 B view raw
run.log run-log decisive run: dmesg before=132 after=132 (no kernel response), netstat -s has no pfsync protocol, pfsync_input ABSENT from static kernel 838 B view raw
inetsw_dump.txt evidence gdb dump of kernel.debug inetsw[]: 13 entries, NO protocol 240 1.2 KB view raw
env.txt environment uname, cc version, kldstat, kern.version 365 B view raw
VERDICT.md verdict full false-positive narrative with path:line evidence 8.4 KB ↓ raw
README.md readme how to reproduce the negative result 2.6 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 how to reproduce the negative result
↓ download raw

DF-0428 β€” pfsync_input reachability PoC

Verdict: NOT REPRODUCED (FALSE POSITIVE). See VERDICT.md for the full analysis. The claimed IPPROTO_PFSYNC(240) injection surface does not exist: pfsync_input is dead code on the default kernel and on pf.ko-loaded kernels.

What this PoC does

  • setup_pfsync.c β€” issues SIOCSETPFSYNC to configure pfsync0 (DragonFly's ifconfig does not expose the pfsync ioctls). Built as the unprivileged user but must be run as root (the ioctl is caps_priv_check'd).
  • inject_pfsync.c β€” crafts a raw IP packet (ip_p=240, ip_ttl=255) carrying a PFSYNC_ACT_CLR payload from an arbitrary spoofed source to the configured peer / multicast group. This is the "on-link attacker" the finding describes. Must be run as root (raw socket).

Build

./build.sh

Equivalently:

cc -o setup_pfsync setup_pfsync.c
cc -o inject_pfsync inject_pfsync.c

Run

The run demonstrates the negative result: inject the packet and observe the kernel does nothing.

# (root, on the guest)
kldload pf.ko
pfctl -e
ifconfig pfsync0 create          # usually already exists once pf.ko loaded
./setup_pfsync vtnet0 224.0.0.240 128   # configure syncdev (triggers bulkfail panic ~5s later β€” see VERDICT.md)

# inject from a spoofed on-link source
./inject_pfsync 10.0.2.99 224.0.0.240 0xdeadbeef

To avoid the unrelated pfsync_bulkfail panic, skip the ./setup_pfsync step β€” the demonstration of unreachability holds without configuring a syncdev (the handler is never even dispatched).

Expected (master DEV, current code)

  • inject_pfsync prints sent 64 bytes: ... proto=240 ttl=255 act=CLR ...
  • No kernel message in dmesg.
  • No pfsync protocol in netstat -s (only tcp udp ip icmp igmp carp ip6 icmp6).
  • No change in pfctl -s states.

β†’ pfsync_input is unreachable; the injected packet hits the RAW wildcard handler (rip_input) and is dropped. The "any on-link host can inject pfsync packets" primitive does not exist.

Confirming the root cause yourself

# 1. The gate (compiled out because NPFSYNC is never defined)
grep -n NPFSYNC sys/netinet/in_proto.c
grep -rn NPFSYNC sys/    # only the two gate lines; no #define, no option

# 2. Static kernel has no pfsync_input
nm /boot/kernel/kernel.debug | grep pfsync_input    # empty

# 3. inetsw[] has no protocol-240 entry
gdb -q -batch \
  -ex 'printf "entries=%lu\n", sizeof(inetsw)/sizeof(struct protosw)' \
  -ex 'set $i=0' \
  -ex 'while $i<sizeof(inetsw)/sizeof(struct protosw)' \
  -ex 'printf "inetsw[%d] proto=%d\n",$i,inetsw[$i].pr_protocol' \
  -ex 'set $i=$i+1' -ex 'end' \
  /boot/kernel/kernel.debug
VERDICT.md verdict full false-positive narrative with path:line evidence
↓ download raw

DF-0428 β€” Verdict: NOT REPRODUCED (FALSE POSITIVE)

One-line verdict

The claimed attack surface β€” pfsync_input reachable as a live IPPROTO_PFSYNC(240) handler from any on-link host β€” does not exist on the default kernel, on the default kernel + pf.ko loaded, or on the default kernel + pf.ko + pfsync0 configured. pfsync_input is dead code. The finding mis-read the #ifdef NPFSYNC gate in in_proto.c.

The claimed mechanism (what the finding asserted)

  • pfsync_input (sys/net/pf/if_pfsync.c:462-541) is registered as the IPPROTO_PFSYNC(240) input handler via in_proto.c:281-288.
  • It validates only sync-ifp, packet-ifp, TTL==255, version, action β€” never the source address against the configured peer.
  • Any on-link host can inject INS/UPD/DEL/CLR/UPD_C/DEL_C/UREQ/BUS packets to manipulate the pf state table.

Why it is a false positive (root cause of the reviewer's error)

The reviewer quoted in_proto.c:281-288 but missed the #ifdef NPFSYNC ... #endif block that wraps the entire registration:

sys/netinet/in_proto.c:281:#ifdef NPFSYNC
sys/netinet/in_proto.c:282:    {
sys/netinet/in_proto.c:283: .pr_type = SOCK_RAW,
...
sys/netinet/in_proto.c:288: .pr_input = pfsync_input,
...
sys/netinet/in_proto.c:296:#endif   /* NPFSYNC */

NPFSYNC is never defined anywhere in the source tree. Verified by exhaustive grep:

$ grep -rn NPFSYNC sys/ sys/conf/ sys/config/ sys/modules/
sys/netinet/in_proto.c:281:#ifdef NPFSYNC          ← the gate
sys/netinet/in_proto.c:296:#endif   /* NPFSYNC */  ← the gate

No #define NPFSYNC, no options NPFSYNC in sys/conf/options, no option/device NPFSYNC in any kernel config (sys/config/X86_64_GENERIC, GENERIC, LINT, NOTES, sys/i386/conf/, sys/x86_64/conf/). β†’ The .pr_input = pfsync_input entry in inetsw[] is compiled out of the static kernel.

Nor does pf.ko dynamically register a handler: nm /boot/kernel/pf.ko shows pfsync_input exported (symbol T) but no reference to inetsw, protosw, or any registration call. pfsyncattach (the documented attach hook) is also unreferenced β€” nothing calls it.

Definitive evidence (five independent confirmations)

1. Source-level: gate is compiled out

sys/netinet/in_proto.c:281 β€” #ifdef NPFSYNC; NPFSYNC undefined everywhere.

2. Static kernel has no pfsync_input

$ nm /boot/kernel/kernel.debug | grep -w pfsync_input
(empty)                  ← ABSENT from static kernel

3. inetsw[] table has NO protocol-240 entry (gdb on kernel.debug)

sizeof protosw = 104
inetsw entries = 13
inetsw[ 0]: pr_protocol=0    pr_input=(nil)               pr_type=0   (IP base)
inetsw[ 1]: pr_protocol=17   pr_input=0xffffffff807ccc20  pr_type=2   (UDP)
inetsw[ 2]: pr_protocol=6    pr_input=0xffffffff807b8e40  pr_type=1   (TCP)
inetsw[ 3]: pr_protocol=255  pr_input=0xffffffff807b5e40  pr_type=3   (RAW)
inetsw[ 4]: pr_protocol=1    pr_input=0xffffffff807ae0a0  pr_type=3   (ICMP)
inetsw[ 5]: pr_protocol=2    pr_input=0xffffffff8079b740  pr_type=3   (IGMP)
inetsw[ 6]: pr_protocol=46   pr_input=0xffffffff807b1de0  pr_type=3   (RSVP)
inetsw[ 7]: pr_protocol=4    pr_input=0xffffffff807a9e70  pr_type=3   (IPinIP)
inetsw[ 8]: pr_protocol=55   pr_input=0xffffffff807a9e70  pr_type=3   (MH)
inetsw[ 9]: pr_protocol=47   pr_input=0xffffffff807a9e70  pr_type=3   (GRE)
inetsw[10]: pr_protocol=41   pr_input=0xffffffff807a9e70  pr_type=3   (IPv6encap)
inetsw[11]: pr_protocol=0    pr_input=0xffffffff807b5e40  pr_type=3   (RAW wildcard)
inetsw[12]: pr_protocol=112  pr_input=0xffffffff807a35e0  pr_type=3   (VRRP/CARP)

240 (IPPROTO_PFSYNC) is absent.

4. Dispatch path: ip_protox[240] resolves to the RAW wildcard

ip_init (sys/netinet/ip_input.c:343-351) first sets every ip_protox[i] to the IPPROTO_RAW wildcard index, then overwrites only the protocols that actually appear in inetsw[]. With no protosw carrying pr_protocol=240, ip_protox[240] remains the wildcard β†’ transport_processing_oncpu (ip_input.c:409-415) dispatches any ip_p=240 packet to rip_input, never to pfsync_input:

sys/netinet/ip_input.c:409: const struct protosw *pr = &inetsw[ip_protox[ip->ip_p]];
sys/netinet/ip_input.c:415: pr->pr_input(&m, &hlen, ip->ip_p);

5. Live guest: handler is not registered, injection does nothing

On 6.5-DEVELOPMENT #0 with pf.ko loaded and pfctl -e'd: - netstat -s enumerates registered IP protocols: tcp, udp, ip, icmp, igmp, carp, ip6, icmp6. No pfsync β€” no handler registered. - Injecting a crafted proto-240 CLR packet (inject_pfsync 10.0.2.99 224.0.0.240 0xdeadbeef, TTL=255) from a raw socket produces no kernel message, no pf-state change, no observable pfsync activity.

The comment the finding claimed was stale β€” "This function is not yet called from anywhere" (sys/net/pf/if_pfsync.c:486) β€” is in fact accurate.

Impact of this false positive

None. The "any on-link host can inject pfsync packets" primitive the finding describes is not reachable. pfsync_input is dormant code; pf.ko only ever sends pfsync packets (via pfsync_sendout), it does not receive them on this platform.

This is consistent with DragonFlyBSD history: the in-kernel pfsync receiver was never finished/wired up (hence the #ifdef NPFSYNC guard and the honest "not yet called" comment). The sender side is functional (pfsync_sendout_mbuf, multicast to 224.0.0.240), but no peer host running this same kernel will process the messages.

Side observation (NOT DF-0428 β€” separate latent bug)

While configuring pfsync0 with a syncdev via SIOCSETPFSYNC to test the input path, the guest hit a different panic in pfsync_bulkfail:

panic: assertion "LWKT_TOKEN_HELD_ANY(&pf_token)" failed in pfsync_bulkfail at if_pfsync.c:1621
pfsync_bulkfail() at pfsync_bulkfail+0x71
softclock_handler() at softclock_handler+0x1b8

pfsync_bulkfail (sys/net/pf/if_pfsync.c:1617) asserts the pf_token is held, but it is invoked from a callout (softclock_handler) without the token. This is a root-only local DoS (requires SIOCSETPFSYNC privilege) triggered ~5 s after configuring a syncdev. It is unrelated to DF-0428's network-injection claim and is not the subject of this verification. Filed here as a side note for the team's awareness; it deserves its own finding if not already tracked.

Build / run (reproduce the negative result)

./build.sh                  # builds setup_pfsync + inject_pfsync
# (root on guest): kldload pf.ko; pfctl -e
./run.sh                    # injects a proto-240 CLR packet
# expected on master DEV: packet sent, NO kernel response, NO pfsync
# protocol in `netstat -s`, NO pf-state change  ->  handler unreachable

PoC changes

  • setup_pfsync.c β€” NEW. Issues SIOCSETPFSYNC directly (DragonFly's ifconfig does not expose syncdev/syncpeer for pfsync). Used to configure pfsync0 with a syncdev + peer + maxupdates.
  • inject_pfsync.c β€” NEW. Crafts a raw IP packet (ip_p=240, ip_ttl=255) carrying a PFSYNC_ACT_CLR payload with an arbitrary spoofed source IP and a chosen creatorid, sent via IPPROTO_RAW. This is the "on-link attacker" the finding describes.
  • No exploit.c β€” there is no memory-corruption primitive (the finding is an auth-bypass claim), and the auth-bypass surface itself is dead.

No code change needed: false-positive. The cited handler (pfsync_input) 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 dynamically register a handler.

If the project ever intends to wire up the pfsync receiver (define NPFSYNC, finish the integration), then the missing-source-authentication concern the finding raises becomes a real hardening item: at minimum, compare ip->ip_src against sc->sc_sync_peer when a unicast peer is configured, and require a non-multicast peer. But that work is gated on first making the code reachable, which is not the case today.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_applicable -- false positive. No fix authored.

n/a
n/a

Confirmed kernel references

Detail

Exploit chain

none β€” dead code. pfsync_input is never registered as a protocol handler.

Evidence (decisive lines)

[OK] raw socket for IPPROTO_PFSYNC created
[OK] sent 32 bytes to loopback (IPPROTO_PFSYNC CLR)
[OK] sent 32 bytes to 224.0.0.240 (IPPROTO_PFSYNC CLR)
pfsyncstats: EMPTY before and after
dmesg: NO pfsync messages
NPFSYNC defined: 0 matches in entire sys/ tree

PoC changes

Created pfsync_live.c: live packet injection test that definitively proves pfsync_input is never called. Added live_injection_test.txt with full evidence.

Verified recommended fix

No code change needed: false-positive. pfsync_input is dead code (#ifdef NPFSYNC gate, NPFSYNC never defined). The source-auth concern is moot since the handler is never registered.

Verdict

NOT REPRODUCED (dead code). LIVE PACKET INJECTION TEST performed: loaded pf.ko (pfsync0 interface appeared), crafted raw IPPROTO_PFSYNC(240) packets with PFSYNC_ACT_CLR payload, sent to 127.0.0.1 and 224.0.0.240. Result: packets sent successfully but ZERO pfsync activity β€” no pfsyncstats in netstat -s, no dmesg messages, no state changes, guest stayed UP. Root cause confirmed: in_proto.c:281-296 wraps the IPPROTO_PFSYNC protocol table entry in #ifdef NPFSYNC, and NPFSYNC is NEVER defined anywhere in sys/ (grep returned 0 matches). Therefore ip_protox[IPPROTO_PFSYNC] is not initialized, incoming proto-240 packets go to rip_input (raw handler), never reaching pfsync_input. pf.ko module init does NOT dynamically register the protocol (0 dynamic registrations found). The finding's claim that pfsync_input is a 'live IPPROTO_PFSYNC handler' is INCORRECT β€” the stale comment 'not yet called' is accurate.