carp_group_demote_adj()/carp_send_ad_all() execute outside netisr0: unsynchronized demotion counter, wrong-CPU callout arming, unlocked carpif_list walk
Summary
carp_group_demote_adj() runs in the caller's context with no netisr0 dispatch: it mutates ifg_carp_demoted non-atomically and calls carp_send_ad_all(), which walks the global carpif_list without carp_listtok and arms sc_ad_tmo via callout_reset on whatever CPU it runs on. Reachable from pfsync: BUS_END input (any netisr CPU, unauthenticated peer by default), the ioctl +1 path (user ioctl thread - the only +1 site), bulkfail callout, clone destroy. Consequences: KASSERT(mycpuid==0) panic (INVARIANTS) or lwkt protocol violation, torn carpif_list traversal/UAF vs concurrent destroy, lost-update demotion-counter drift. Requires pfsync+carp deployment; privileged/unusual-config trigger. Static (Low). Fix: netmsg dispatch to netisr0 + atomic dm updates. [VERIFIED pass2: unlocked RMW and unlocked group walk line-confirmed with cross-CPU callers but pfsync not configured so no manifestation drivable; netisr0-dispatch fix compile-validated]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3299 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| NOTES.md | β | 2.6 KB | β raw | |
| README.md | β | 942 B | β raw | |
| VERDICT.md | β | 2.1 KB | β raw | |
| manifest.json | β | 724 B | view raw | |
| verdict.json | β | 1.6 KB | view raw |
DF-3299 -- verification pack
Method: Code-line analysis + attempted live reproduction Status: NOT REPRODUCED | impact: none | confidence: likely
What this is
carp_group_demote_adj (ip_carp.c:2765-2781) does an unlocked RMW on ifg_carp_demoted and an unlocked group walk, and carp_send_ad_all runs in the caller context; the cross-CPU callers are pfsync_input (any netisr CPU, if_pfsync.c:985) vs pfsyncioctl (user thread, :1162). The demote counter is not exposed to userspace and pfsync was not configured, so no manifestation could be driven or observed; mechanism line-confirmed only (NOTES.md folded in).
Build
cc -O -o <prog> <prog>.c (on the guest)
Run
(none)
(success criterion: see VERDICT.md)
Evidence
NOTES.md (prior static analysis) + ip_carp.c:2765-2781 excerpt in VERDICT.md
Kernel references: sys/netinet/ip_carp.c:2765-2781, sys/net/pf/if_pfsync.c:985, sys/net/pf/if_pfsync.c:1162
DF-3299 -- VERDICT
NOT REPRODUCED (impact=none, confidence=likely) Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Summary
carp_group_demote_adj (ip_carp.c:2765-2781) does an unlocked RMW on ifg_carp_demoted and an unlocked group walk, and carp_send_ad_all runs in the caller context; the cross-CPU callers are pfsync_input (any netisr CPU, if_pfsync.c:985) vs pfsyncioctl (user thread, :1162). The demote counter is not exposed to userspace and pfsync was not configured, so no manifestation could be driven or observed; mechanism line-confirmed only (NOTES.md folded in).
How it was verified
- Method: Code-line analysis + attempted live reproduction
- Kernel references: sys/netinet/ip_carp.c:2765-2781, sys/net/pf/if_pfsync.c:985, sys/net/pf/if_pfsync.c:1162
- Evidence: NOTES.md (prior static analysis) + ip_carp.c:2765-2781 excerpt in VERDICT.md
- Reachability: local-unpriv where noted in the summary; root-config for tun/lagg/carp/ipfw3/gre items; remote-facing for the packet parsers (wlan harnesses model the RX/TX path verbatim because the guest has no 802.11 hardware; precedent DF-0393/DF-0594/DF-0621/DF-0350).
- Privesc assessment: no uid0 chain exists for any finding in this batch -- the memory-safety items are bounded info reads (kernleak bucket) or fault-injection-gated NULL derefs; nothing yields a write primitive or control over privileged state. The one panic-class live item (DF-3414) is root-only (SIOCIFDESTROY).
Fix
fix_status: not_testable -- Fix: netisr0-dispatch carp_group_demote_adj/carp_send_ad_all + token-protected group walk; needs a pfsync rig to A/B.
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.)
DF-3299 β carp_group_demote_adj() / carp_send_ad_all() execute outside netisr0 (MP-unsafe) β STATIC ANALYSIS
Not guest-verified (Low severity, privileged/peer-triggered); basis is source analysis only.
Claim
sys/netinet/ip_carp.c:2764-2785: carp_group_demote_adj() mutates
ifg_carp_demoted and, when a group transitions 0 -> 1, calls
carp_send_ad_all() in the caller's context, with no netisr0
dispatch and no carp_listtok:
void
carp_group_demote_adj(struct ifnet *ifp, int adj)
{
...
if (*dm + adj >= 0)
*dm += adj;
...
if (adj > 0 && *dm == 1)
carp_send_ad_all(); /* runs HERE, any CPU */
Callers (sys/net/pf/if_pfsync.c):
- :985 pfsync_input() PFSYNC_ACT_BUS/PFSYNC_BUS_END -> adj(-1) β any netisr CPU (packet-receive context)
- :1162 pfsyncioctl() (bulk-update request path) -> adj(+1) β
user ioctl thread; the only +1 site, and the one that fires
carp_send_ad_all()on a non-netisr0 thread - :1644 pfsync_bulkfail() callout -> adj(-1) β callout context on whichever CPU the callout was armed on
- :209 pfsync_clone_destroy() -> adj(-1) β user ioctl thread
Consequences
carp_send_ad_all()walks the globalcarpif_list(ip_carp.c:1281) withoutcarp_listtokwhilecarp_clone_create/destroy(ip_carp.c:674-676, 715-717) mutate the same list under the token: torn traversal / use-after-free of a concurrently destroyed softc.carp_send_ad()invoked off netisr0 races the netisr0 state machine on the same softc (sc_sendad_errors,sc_imo,sc_ia) and doescallout_reset(&sc->sc_ad_tmo, ...)on the wrong CPU; the handler assertsmycpuid == 0(ip_carp.c:1296-1297) -> guaranteed INVARIANTS panic when the timer fires; on non-INVARIANTS kernels the timeout handler callslwkt_sendmsg_oncpu(netisr_cpuport(0))from a foreign CPU, violating the lwkt port protocol.*dm += adjonifg_carp_demotedis non-atomic and unsynchronized across CPUs -> lost updates permanently skew the demotion counter (failover groups stuck demoted / under-demoted).
Trigger model
+1 path: privileged pfsync ioctl (ifconfig pfsync0 syncdev/peer loss handling) β root. -1 paths: pfsync packet processing from the sync peer (unauthenticated pfsync by default) β an attacker on the sync segment can race the counter concurrently with legitimate +1/-1.
Fix sketch
Dispatch the demotion + carp_send_ad_all() to netisr0 (netmsg), and
make the counter adjustment atomic (atomic_fetchadd_int guarded to
the >= 0 clamp) or netisr0-only.
Fix verification
not_testableFix: netisr0-dispatch carp_group_demote_adj/carp_send_ad_all + token-protected group walk; needs a pfsync rig to A/B.
Confirmed kernel references
Detail
Evidence (decisive lines)
NOTES.md (prior static analysis) + ip_carp.c:2765-2781 excerpt in VERDICT.md
Verdict
carp_group_demote_adj (ip_carp.c:2765-2781) does an unlocked RMW on ifg_carp_demoted and an unlocked group walk, and carp_send_ad_all runs in the caller context; the cross-CPU callers are pfsync_input (any netisr CPU, if_pfsync.c:985) vs pfsyncioctl (user thread, :1162). The demote counter is not exposed to userspace and pfsync was not configured, so no manifestation could be driven or observed; mechanism line-confirmed only (NOTES.md folded in).
No comments yet.