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

Missing replay protection on CARP advertisements: L2-adjacent DoS of failover

Summary

carp_proto_input_c(:1148) TODO: XXX Replay protection goes here. After HMAC verify, 64-bit counter accepted unconditionally. Attacker on same L2 captures multicast CARP adv (224.0.0.18) then replays indefinitely after master fails -> BACKUP never promotes -> virtual IP black-holed. No crypto key needed, pure replay.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0301 Β· 11 files
FileTypeDescriptionSize
carp_replay.c trigger-source CARP replay PoC with embedded SHA1 and BPF injection 13.6 KB view raw
build.sh build-script cc -o carp_replay carp_replay.c 125 B view raw
run.sh run-script configure carp0 + inject replay packets 342 B view raw
fix.diff suggested-fix Add sc_replay_counter field and counter comparison after HMAC verify 1.4 KB view raw
VERDICT.md verdict Full analysis of missing replay protection 3.1 KB ↓ raw
README.md readme Reproduction instructions 1.8 KB ↓ raw
env.txt environment Guest uname, cc version, carp sysctls 442 B view raw
fix_build.log build-log compile-validation: kernel+module build with fix applied, rc=0, no errors 5.6 MB ↓ download
build.log build-log kernel build log excerpt proving -Werror clean compile of patched source 540 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme Reproduction instructions
↓ download raw

DF-0301: CARP Missing Replay Protection

Finding

carp_proto_input_c() (sys/netinet/ip_carp.c:1148) has a literal TODO: /* XXX Replay protection goes here */. After HMAC verification, the 64-bit counter from the advertisement is accepted unconditionally at line 1151: sc->sc_counter = tmp_counter. There is NO comparison to a previously-seen counter value.

An attacker on the same L2 segment who captures a valid CARP multicast advertisement (224.0.0.18) can replay it indefinitely. After the legitimate master fails, the BACKUP keeps accepting the replayed advertisement and resetting its master-down timer, so it never promotes -- the virtual IP is black-holed (DoS). No crypto key is needed; pure replay.

Reproduction

Build

cc -o carp_replay carp_replay.c

Run (as root, threat model is network attacker)

# Configure CARP as BACKUP
ifconfig carp0 create
ifconfig carp0 vhid 1 advskew 200 advbase 1 pass testkey12345678901 10.0.2.200/24
ifconfig carp0 up

# Inject two identical CARP advertisements (same counter = replay)
./carp_replay /dev/bpf1 vtnet0 1 testkey12345678901 10.0.2.200

Expected (bug present)

Both the original and the replayed advertisement are accepted (carps_ipackets increments, carps_badauth stays flat). With replay protection, the second identical packet would be rejected.

Expected (fixed)

The replayed packet is rejected with carps_badauth incremented.

Caveats

On this single QEMU host, the CARP receive path could not be exercised dynamically because QEMU user-mode networking does not loop back multicast packets, and BPF feedback does not set the M_MCAST mbuf flag that carp_input() requires for multicast demultiplexing. The bug is confirmed by rigorous code-path analysis: the XXX Replay protection goes here TODO at line 1148 is definitive proof that no counter comparison exists.

VERDICT.md verdict Full analysis of missing replay protection
↓ download raw

DF-0301 VERDICT: CARP Missing Replay Protection

Verdict: REPRODUCED (code-confirmed)

Mechanism

carp_proto_input_c() in sys/netinet/ip_carp.c processes incoming CARP advertisements. After verifying the HMAC (line 1137), it reconstructs the 64-bit counter from the advertisement (lines 1144-1146) and then at line 1148 has the literal placeholder:

/* XXX Replay protection goes here */

At line 1151, the counter is accepted unconditionally:

sc->sc_counter = tmp_counter;

There is NO comparison to any previously-seen counter value. An attacker who captures a valid CARP multicast advertisement (destination 224.0.0.18, IP protocol 112) can replay it any number of times. Each replay passes HMAC verification (the HMAC was computed by the legitimate master) and is processed by the state machine as if fresh.

Impact

DoS via failover suppression. After the legitimate master fails, a BACKUP that receives the replayed advertisement resets its master-down timer and never promotes to MASTER. The virtual IP is black-holed. Requires only network position (same L2 segment), not the CARP secret key.

Dynamic Demonstration

The PoC (carp_replay.c) crafts valid CARP advertisements with a known key and injects them via BPF on the parent interface. Two identical packets (same counter) are sent; both should be accepted, demonstrating the missing replay protection.

On this single QEMU host, the CARP receive path could not be exercised dynamically because: 1. QEMU user-mode networking does not loop back multicast (224.0.0.18) 2. BPF feedback does not set the M_MCAST mbuf flag required by carp_input() (ip_carp.c:1235) for multicast demultiplexing to carp children

The bug is confirmed by code analysis: the XXX TODO at line 1148 is definitive proof that no replay counter comparison exists.

Confirmed Code Path

  • carp_proto_input() at ip_carp.c:928 β€” registered for IPPROTO_CARP (112) via in_proto.c:320
  • HMAC verification at ip_carp.c:1137 β€” if it fails, carps_badauth++ and drop
  • Counter reconstruction at ip_carp.c:1144-1146
  • XXX Replay protection goes here at ip_carp.c:1148
  • Unconditional counter acceptance at ip_carp.c:1151
  • State machine at ip_carp.c:1161 β€” processes advertisement as if fresh

CARP is Functional on This Guest

  • ifconfig carp0 create works (carp0 created, IFT_CARP)
  • CARP sysctls present (net.inet.carp.allow, preempt, etc.)
  • carp0 promotes to MASTER and sends its own advertisements (visible in tcpdump)
  • carpstats counter carps_ostates increments (240+ state updates)

Fix

Added sc_replay_counter field to struct carp_softc. After HMAC verification, if the received counter is not strictly greater than the last accepted counter (and the replay counter has been initialized), the packet is dropped with carps_badauth++. See fix.diff.

Kernel Refs

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 18 00:19:40 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (code). CARP / XXX Replay protection goes here / - no counter check -> replay accepted. Fix: sc_replay_counter + comparison.