pfsync sync engine is dead code: pfsyncif never initialized, RX dispatch never compiled, bulk updater never scheduled β silent total loss of pf HA state synchronization
Summary
The global pfsyncif is assigned exactly once in the entire tree - the NULL initializer. pfsync_clone_create() omits upstream's 'pfsyncif = sc', so every engine entry point takes its sc==NULL early-out forever: pfsync_input, pfsync_pack_state, pfsync_request_update, pfsync_clear_states. Independently, the RX protosw registration is guarded by #ifdef NPFSYNC which no build defines, and the bulk updater is never scheduled - the only sc_bulk_tmo arming passes pfsync_timeout instead of pfsync_bulk_update. A fourth defect: SIOCSETPFSYNC's default-peer reset assigns host-byte-order INADDR_PFSYNC_GROUP without htonl (would target 240.0.0.224). VERIFIED on guest (stock + patched): pfsync0 configured with a unicast peer, pf states generated and updating, tcpdump shows 0 packets for the whole window on both interfaces - a live engine must transmit within its 1s tick; plus exhaustive static proof (rg 'pfsyncif =' returns only the initializer; NPFSYNC appears only as the ifdef pair). Operators of HA firewall pairs (pfsync+carp, the documented deployment) configure pfsync, get success from every config step, and silently carry ZERO shared state: on failover every connection/NAT state is lost - re-auth storms, black-holed sessions, broken security-redundancy assumptions, no error/warning/counter. Also: this reachability analysis shows known DF-0428..0432 (unauthenticated protocol family) are NOT exploitable as shipped, and would all re-arm the instant someone wires the engine without also adding peer authentication. No uid0 route (functional control loss).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3379 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.5 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| trigger_df3379.c | β | 2.5 KB | view raw | |
| build.sh | β | 104 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 109 B | view raw | |
| run.log | β | 2.6 KB | view raw | |
| env.txt | β | 1.1 KB | view raw | |
| verdict.json | β | 4.3 KB | view raw |
DF-3379 β pfsync sync engine is dead code (pfsyncif never initialized; RX dispatch never compiled)
Impact
On any DragonFly system where an operator configures pfsync (the pf state-synchronization
interface used with CARP for HA firewall failover), no state synchronization ever occurs:
no pfsync packet is ever built or transmitted, and no pfsync packet can ever be received.
ifconfig-equivalent configuration (SIOCSETPFSYNC) succeeds, the interface exists, counters
exist β but the engine behind it cannot run. An HA pair silently carries zero shared
state: on failover every connection/NAT state is lost (re-authentication storms, black-holed
sessions, asymmetric-state assumptions broken), and the operator has no error, no warning,
and no counter to tell them.
Root cause (three independent defects, each alone fatal to the engine)
pfsyncifis never assigned βsys/net/pf/if_pfsync.c:90declaresstruct pfsync_softc *pfsyncif = NULL;andpfsync_clone_create()(if_pfsync.c:131-192) never stores the new softc into it (upstream OpenBSD pfsync_clone_create doespfsyncif = sc). Every engine entry point takes itssc == NULLearly-out: -pfsync_input()β drops every packet at if_pfsync.c:489 (if (!sc || ...) goto done) -pfsync_pack_state()β if_pfsync.c:1286-1287 (if (sc == NULL) return (0)) -pfsync_request_update()β if_pfsync.c:1449-1450 -pfsync_clear_states()β if_pfsync.c:1492-1493- The RX dispatch is never compiled β
sys/netinet/in_proto.c:281-296guards the.pr_input = pfsync_inputprotosw entry with#ifdef NPFSYNC, but nothing in the tree definesNPFSYNC(nouse_pfsync.hinclude exists;sys/conf/files:1574buildsif_pfsync.conly as part ofpf; no kernel config names a pfsync device). The stale comment at if_pfsync.c:482 ("This function is not yet called from anywhere") is therefore accurate despite in_proto.c's apparent registration. - The bulk updater is never scheduled and is mis-wired β
pfsync_bulk_update()(if_pfsync.c:1545) is referenced by no callout anywhere; the onlycallout_reset(&sc->sc_bulk_tmo, ...)in the file (if_pfsync.c:1604) passespfsync_timeoutas the handler instead ofpfsync_bulk_update(upstream OpenBSD passes pfsync_bulk_update). AdditionallySIOCSETPFSYNCresets the default peer in host byte order (sc->sc_sync_peer.s_addr = INADDR_PFSYNC_GROUP;at if_pfsync.c:1076-1077, nohtonl, vs. the correcthtonl(...)at if_pfsync.c:150-151), which would send "multicast" pfsync to 240.0.0.224 (class E) instead of 224.0.0.240 if the engine were live.
Security consequences
- Silent loss of the HA state-sync security control (failover drops all state).
- TriReachability correction for this audit: known findings DF-0428, DF-0429, DF-0430,
DF-0431, DF-0432 (the unauthenticated pfsync protocol family) are not reachable on
this tree as shipped β the entire RX parser is behind two dead switches (
NPFSYNCcompile guard and the!sccheck). They remain latent: any future commit that assignspfsyncifand definesNPFSYNCre-arms all five without any further code change.
Reproduce
See build.sh / run.sh. Summary:
1. kldload pf; ifconfig pfsync0 create
2. ./trigger_df3379 (SIOCSETPFSYNC syncdev=lo0 syncpeer=10.0.2.2 maxupd=128 β
a unicast peer defeats the "no listener configured" early-out at if_pfsync.c:1294,
so a live engine must transmit)
3. enable pf with pass out all keep state; generate states (ping/ssh)
4. tcpdump -i vtnet0 -n proto 240 and tcpdump -i lo0 -n proto 240 β zero packets,
while pfctl -s state shows live, updating states.
Expected result (bug present)
pfctl -s statenon-empty; states created and updated during the capture window.- Zero proto-240 (pfsync) packets on any interface, ever.
- Static:
grep -n "pfsyncif" sys/net/pf/if_pfsync.cshows only the NULL initializer and reads β no assignment.grep -rn NPFSYNC sys/shows only the#ifdef/#endifpair in in_proto.c.
Fix direction
Wire the engine up deliberately (assign pfsyncif = sc in pfsync_clone_create / clear on
destroy; compile-time or runtime registration of the RX hook; fix the bulk-update callout
handler and the htonl on the default peer) β but only together with the authentication
fix for the DF-0428..0432 family, since a working engine immediately exposes all five.
Absent that, the honest fix is to refuse SIOCSETPFSYNC configuration (or refuse creating
the clone) so operators get an error instead of silent non-synchronization.
DF-3379 VERDICT
Status: reproduced (defect confirmed live + airtight static proof) Impact ceiling: none at the kernel-compromise level β the security loss is the silent total absence of pf state synchronization on HA deployments (failover drops all state), plus the audit-reachability correction for DF-0428..0432.
What was verified on the guest (DragonFly 6.5-DEVELOPMENT, INVARIANTS kernels #0 stock and #1 patched)
-
Behavioral (run.log): pfsync0 configured via SIOCSETPFSYNC with
syncdev=lo0AND unicastsyncpeer=10.0.2.2(kernel confirmed both via SIOCGETPFSYNC). pf enabled withpass out all keep state; states created and updating (icmp + tcp ESTABLISHED).tcpdump proto 240on both vtnet0 and lo0 for the whole window: 0 packets captured. With a unicast peer and a syncdev set, the only early-out that could legitimately stop transmission (if_bpf==NULL && sc_sync_ifp==NULL && peer==group, if_pfsync.c:1294-1303) does not apply β a live engine would have emitted INS/UPD within its 1-second tick. It emitted nothing becausepfsync_pack_state()returns at once:struct pfsync_softc *sc = pfsyncif; if (sc == NULL) return (0);(if_pfsync.c:1277-1287). -
Static (in-guest and in-tree, exhaustive): -
pfsyncifis assigned exactly once in the whole tree β the NULL initializer at if_pfsync.c:90.pfsync_clone_create()(131-192) never doespfsyncif = sc(upstream OpenBSD does). No other file writes it (verifiedrg "pfsyncif\s*=" sys/). - The RX hookpfsync_input(registered in in_proto.c:288 under#ifdef NPFSYNC, in_proto.c:281) can never be compiled: nothing defines NPFSYNC; nouse_pfsync.hinclude exists anywhere; conf/files:1574 builds if_pfsync.c only as part ofpf. Hence the packet parser that DF-0428..0432 describe is doubly unreachable β the protosw entry does not exist, and even a hand-routed packet would drop at the!sccheck (if_pfsync.c:489). - The bulk updaterpfsync_bulk_update()is never scheduled by anything, and the onlycallout_reset(&sc->sc_bulk_tmo, ...)in the file (if_pfsync.c:1604) passespfsync_timeoutinstead ofpfsync_bulk_update. -SIOCSETPFSYNCpeer-reset assigns host-byte-orderINADDR_PFSYNC_GROUPwithouthtonl(if_pfsync.c:1076-1077 vs. correct 150-151) β would target 240.0.0.224. -
Corroboration: DF base
ifconfigcontains no pfsync support at all (no SIOCSETPFSYNC caller in sbin/ifconfig) β the interface's userspace story is equally vestigial.
Why this matters
- Operators configuring pfsync HA pairs get zero synchronization with zero diagnostics β a silent failure of a security-redundancy control.
- Triage correction: DF-0428, DF-0429, DF-0430, DF-0431, DF-0432 are not reachable on this tree as shipped; they become live the moment someone "fixes" the wiring without also adding peer authentication β the two must land together.
Fix
Do NOT simply assign pfsyncif = sc and define NPFSYNC: that re-arms the entire
unauthenticated protocol family (DF-0428..0432). The safe near-term fix is to make the
dead engine loud: refuse SIOCSETPFSYNC (and/or refuse creating pfsync clones) with
EOPNOTSUPP until the engine is actually wired and authenticated. The complete fix
(wire the engine + peer authentication + bulk callout handler + htonl) is an upstream
engineering task tracked by the DF-0428..0432 + DF-3379 finding set.
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_testableConfirmed kernel references
Detail
Evidence (decisive lines)
["run.log: SIOCGETPFSYNC readback (syncdev=lo0, maxupd=128) + live pf states + '0 packets captured' for proto 240 on both vtnet0 and lo0 + in-guest static grep (pfsyncif only at :90/:466/:1278/:1445/:1488; NPFSYNC only as #ifdef/#endif; use_pfsync.h absent)", "trigger_df3379.c: SIOCSETPFSYNC with syncdev+unicast peer - the unicast peer defeats the legitimate 'nobody listening' early-out at if_pfsync.c:1294, making the zero-TX observation decisive", 'VERDICT.md: full three-defect analysis and the DF-0428..0432 reachability correction', 'sbin/ifconfig contains no SIOCSETPFSYNC caller at all (grep in repo) - userspace story equally vestigial']
PoC changes
No PoC seed existed (new finding). Wrote a purpose-built SIOCSETPFSYNC trigger (syncdev + unicast peer) because DF's base ifconfig has no pfsync subcommands; the unicast peer is essential to make zero-TX a positive proof rather than an early-out artifact.
Verified recommended fix
Near term: make the dead engine loud - fail SIOCSETPFSYNC / clone creation with EOPNOTSUPP until the engine is actually wired. Full fix (assign pfsyncif in clone create/destroy, register RX, fix :1604 handler and :1076 htonl) MUST land together with peer authentication for the DF-0428..0432 family, or those five findings go live unchanged.
Verdict
The pfsync state-synchronization engine is dead code in this tree, three independent ways, verified statically (exhaustive tree grep) and behaviorally on the guest. (1) The global pfsyncif is never assigned anywhere - its only occurrence as an lvalue is the NULL initializer at if_pfsync.c:90; pfsync_clone_create() (131-192) omits the upstream 'pfsyncif = sc', so pfsync_input (:489), pfsync_pack_state (:1286), pfsync_request_update (:1449) and pfsync_clear_states (:1492) all take their sc==NULL early-out forever. (2) The RX dispatch in in_proto.c:281-296 is guarded by #ifdef NPFSYNC, which nothing defines (no use_pfsync.h include exists; conf/files:1574 builds if_pfsync.c only as part of pf), so pfsync_input is never registered in any build - the comment at :482 is accurate. (3) The bulk updater pfsync_bulk_update is never scheduled and the only sc_bulk_tmo arming (:1604) passes pfsync_timeout instead of pfsync_bulk_update; plus the SIOCSETPFSYNC default-peer reset assigns host-byte-order INADDR_PFSYNC_GROUP (:1076-1077 vs htonl at :150-151). Behavioral proof on the guest: pfsync0 configured with syncdev=lo0 AND unicast peer 10.0.2.2 (confirmed readback), pf enabled with keep-state, states creating/updating - tcpdump proto 240 on vtnet0 and lo0 captured ZERO packets over the window, where a live engine must transmit within its 1s tick. Security impact: silent total loss of the HA state-sync control on any deployment that configures it (failover drops all state, no error/warning/counter); triage correction: known findings DF-0428..0432 (unauthenticated protocol family) are unreachable as shipped and would re-arm the moment the engine is wired without authentication.
No comments yet.