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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3379 Β· 9 files
FileTypeDescriptionSize
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)

  1. pfsyncif is never assigned β€” sys/net/pf/if_pfsync.c:90 declares struct pfsync_softc *pfsyncif = NULL; and pfsync_clone_create() (if_pfsync.c:131-192) never stores the new softc into it (upstream OpenBSD pfsync_clone_create does pfsyncif = sc). Every engine entry point takes its sc == NULL early-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
  2. The RX dispatch is never compiled β€” sys/netinet/in_proto.c:281-296 guards the .pr_input = pfsync_input protosw entry with #ifdef NPFSYNC, but nothing in the tree defines NPFSYNC (no use_pfsync.h include exists; sys/conf/files:1574 builds if_pfsync.c only as part of pf; 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.
  3. The bulk updater is never scheduled and is mis-wired β€” pfsync_bulk_update() (if_pfsync.c:1545) is referenced by no callout anywhere; the only callout_reset(&sc->sc_bulk_tmo, ...) in the file (if_pfsync.c:1604) passes pfsync_timeout as the handler instead of pfsync_bulk_update (upstream OpenBSD passes pfsync_bulk_update). Additionally SIOCSETPFSYNC resets the default peer in host byte order (sc->sc_sync_peer.s_addr = INADDR_PFSYNC_GROUP; at if_pfsync.c:1076-1077, no htonl, vs. the correct htonl(...) 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 (NPFSYNC compile guard and the !sc check). They remain latent: any future commit that assigns pfsyncif and defines NPFSYNC re-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 state non-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.c shows only the NULL initializer and reads β€” no assignment. grep -rn NPFSYNC sys/ shows only the #ifdef/#endif pair 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.

VERDICT.md
↓ download raw

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)

  1. Behavioral (run.log): pfsync0 configured via SIOCSETPFSYNC with syncdev=lo0 AND unicast syncpeer=10.0.2.2 (kernel confirmed both via SIOCGETPFSYNC). pf enabled with pass out all keep state; states created and updating (icmp + tcp ESTABLISHED). tcpdump proto 240 on 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 because pfsync_pack_state() returns at once: struct pfsync_softc *sc = pfsyncif; if (sc == NULL) return (0); (if_pfsync.c:1277-1287).

  2. Static (in-guest and in-tree, exhaustive): - pfsyncif is assigned exactly once in the whole tree β€” the NULL initializer at if_pfsync.c:90. pfsync_clone_create() (131-192) never does pfsyncif = sc (upstream OpenBSD does). No other file writes it (verified rg "pfsyncif\s*=" sys/). - The RX hook pfsync_input (registered in in_proto.c:288 under #ifdef NPFSYNC, in_proto.c:281) can never be compiled: nothing defines NPFSYNC; no use_pfsync.h include exists anywhere; conf/files:1574 builds if_pfsync.c only as part of pf. 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 !sc check (if_pfsync.c:489). - The bulk updater pfsync_bulk_update() is never scheduled by anything, and the only callout_reset(&sc->sc_bulk_tmo, ...) in the file (if_pfsync.c:1604) passes pfsync_timeout instead of pfsync_bulk_update. - SIOCSETPFSYNC peer-reset assigns host-byte-order INADDR_PFSYNC_GROUP without htonl (if_pfsync.c:1076-1077 vs. correct 150-151) β†’ would target 240.0.0.224.

  3. Corroboration: DF base ifconfig contains 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_testable
per-fix-DF-3379

Confirmed 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.