β¬’ 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).

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 235 B view raw
run.sh run-script wrapper invoking run_on_guest.sh 1.5 KB 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.0 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 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.