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)
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 | 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 |
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 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_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.