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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3299 Β· 5 files
FileTypeDescriptionSize
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

VERDICT.md
↓ download raw

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

  1. carp_send_ad_all() walks the global carpif_list (ip_carp.c:1281) without carp_listtok while carp_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.
  2. carp_send_ad() invoked off netisr0 races the netisr0 state machine on the same softc (sc_sendad_errors, sc_imo, sc_ia) and does callout_reset(&sc->sc_ad_tmo, ...) on the wrong CPU; the handler asserts mycpuid == 0 (ip_carp.c:1296-1297) -> guaranteed INVARIANTS panic when the timer fires; on non-INVARIANTS kernels the timeout handler calls lwkt_sendmsg_oncpu(netisr_cpuport(0)) from a foreign CPU, violating the lwkt port protocol.
  3. *dm += adj on ifg_carp_demoted is 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_testable

Fix: netisr0-dispatch carp_group_demote_adj/carp_send_ad_all + token-protected group walk; needs a pfsync rig to A/B.

per-fix-DF-3299

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