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)
PoC verification
Evidence pack
findings/poc/DF-0428 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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β issuesSIOCSETPFSYNCto configurepfsync0(DragonFly'sifconfigdoes not expose the pfsync ioctls). Built as the unprivileged user but must be run as root (the ioctl iscaps_priv_check'd).inject_pfsync.cβ crafts a raw IP packet (ip_p=240,ip_ttl=255) carrying aPFSYNC_ACT_CLRpayload 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_pfsyncprintssent 64 bytes: ... proto=240 ttl=255 act=CLR ...- No kernel message in
dmesg. - No
pfsyncprotocol innetstat -s(onlytcp 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
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 theIPPROTO_PFSYNC(240)input handler viain_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/BUSpackets 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. IssuesSIOCSETPFSYNCdirectly (DragonFly'sifconfigdoes not exposesyncdev/syncpeerfor pfsync). Used to configurepfsync0with a syncdev + peer + maxupdates.inject_pfsync.cβ NEW. Crafts a raw IP packet (ip_p=240,ip_ttl=255) carrying aPFSYNC_ACT_CLRpayload with an arbitrary spoofed source IP and a chosen creatorid, sent viaIPPROTO_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.
Recommended fix
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_testablenot_applicable -- false positive. No fix authored.
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.
No comments yet.