#!/bin/sh # dpaa2-linkfix -- detect and recover the dpaa2 "ni" internal link-state desync, # and capture everything needed to find the actual defect. # Workaround for FreeBSD bug 279352 ("dpaa2 (dpni0 device): interface goes # catatonic periodically"). No upstream fix exists; PR open since 2024-05-27. # # dpaa2-linkfix event called by devd on a link-state change # dpaa2-linkfix poll called by cron as a safety net # dpaa2-linkfix dump capture state only, change nothing # # THE BUG. The ni's internal link state goes DOWN without the driver re-issuing # DPAA2_CMD_MAC_SET_LINK_STATE (it is guarded by # `if (link_state != sc->link_state)` in dpaa2_ni_miibus_statchg()). ifconfig # still reports LOWER_UP and the switch still reports the port up, but no packets # pass. Only a real media transition resyncs it. # # WHY devd IS THE RIGHT HOOK. devd cannot see the fault itself -- no interrupt # means no if_link_state_change() means no devd event. But the fault is *created # during a real link transition*, which devd does see (bz@ captured "2 link states # coalesced" immediately before a wedge, PR 279352 comment 16). So we reconcile # the driver's internal link state against reality every time the link moves. # The cron poll is the safety net for a desync with no event at all (PR c17). # # THE LOOP HAZARD. Our own media flap emits LINK_DOWN + LINK_UP, which re-invokes # this script. Guarded three ways: an atomic mkdir lock, a self-inflicted quiet # period, and an hourly rate limit. # # DEBUG.log is the point of this script as much as the recovery is. We dump full # state BEFORE and AFTER the media flap, so the recovery itself is an experiment: # the before/after diff shows exactly which state the flap resynced. MODE=${1:-poll} IF=${2:-dpni0} UNIT=$(echo "$IF" | sed 's/^dpni//') SW=172.16.1.2 # switch, used only to verify recovery OOB=dpni1 # second, unaddressed link to the same switch SETTLE=4 # let the MC settle after a real transition QUIET=45 # ignore events for this long after our own flap MAX_PER_HOUR=4 LOCK=/var/run/dpaa2-linkfix.$IF.lock QUIETF=/var/run/dpaa2-linkfix.$IF.quiet HIST=/var/run/dpaa2-linkfix.$IF.hist WORK=/var/tmp/ai/switch DEBUG=$WORK/DEBUG.log CURL=/usr/local/bin/curl NTFY=https://ntfy.sh/skunkwerks PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin export PATH log() { logger -t dpaa2-linkfix "$IF/$MODE: $*"; } # Push a notification. Backgrounded and time-limited so a slow or dead uplink # can never delay the recovery. During a wedge the uplinks (dpni8/dpni9) stay # up, so this does get out. notify() { [ -x "$CURL" ] || return 0 ( "$CURL" -s -m 10 \ -H "Title: r02 $IF dpaa2 bug 279352" \ -d "r02 $IF: $*" "$NTFY" >/dev/null 2>&1 & ) >/dev/null 2>&1 return 0 } mc_state() { sysctl -n "dev.dpaa2_ni.$UNIT.link" 2>/dev/null | awk -F': ' '/Link State/{print $2}' | awk '{print $1}' } # DO NOT reintroduce an ifconfig call here. 2026-08-20: `ifconfig` is not a # read-only command on this board. Any invocation - even `ifconfig dpni0` - # sends a netlink RTM_GETLINK with NLM_F_DUMP (ifconfig_netlink.c:149), and the # kernel then runs get_operstate_ether() -> SIOCGIFMEDIA -> mii_pollstat() -> # MDIO on EVERY interface (netlink/route/iface.c:126). On r02 that is an # 8-PHY sweep of a shared memac MDIO bus, which is what generates the phantom # link transitions that wedge the DPMAC in the first place. This poll used to # do 288 such sweeps a day, and in event mode it was a positive feedback loop: # flap -> devd -> ifconfig -> more flaps. # # LOWER_UP cannot be read without causing the sweep: it is not a native FreeBSD # flag, it is synthesised at iface.c:315 from the operstate that the MDIO poll # just produced. So the test is dropped entirely and we gate on mc_state() # alone - which is the authoritative signal anyway, being the MC firmware's own # view, i.e. the thing that actually gates the datapath. False positives on a # genuine cable-out are bounded by the MAX_PER_HOUR rate limit below. # # See LOG.md §3c and CURRENT.md §8. # --------------------------------------------------------------------------- # Full state capture. $1 = label (e.g. "BEFORE-HEAL"). # Everything here is read-only except the out-of-band ARP probe, which injects # raw frames on an unaddressed interface and changes no configuration. # --------------------------------------------------------------------------- dump() { LABEL=$1 mkdir -p "$WORK" { echo echo "================================================================" echo "== $LABEL $IF $(date '+%Y-%m-%d %H:%M:%S') mode=$MODE" echo "== uptime:$(uptime)" echo "================================================================" echo "---- 1. THE SIGNATURE: MC internal link state vs ifconfig ----" echo "# Read ONLY the UP/DOWN word. '(IGNORE)'/'(VALID)' is a driver" echo "# display bug (an always-zero reserved bit) and is always IGNORE." sysctl "dev.dpaa2_ni.$UNIT.link" 2>&1 echo "---- sampled 3x, 1s apart, to see if it is flapping ----" for i in 1 2 3; do echo " [$i] $(mc_state)"; sleep 1; done echo "---- 2. ifconfig -m $IF ----" ifconfig -m "$IF" 2>&1 echo "---- 3. INTERRUPTS: did the DPNI link-change IRQ actually fire? ----" echo "# This is the crux of the upstream claim 'we do not see an" echo "# interrupt for a link-state change'. Compare across dumps." vmstat -i 2>&1 | grep -i "dpaa2\|dpni\|total" || echo "(none)" echo "---- 4. all dpaa2_ni.$UNIT sysctls (incl. full stats) ----" sysctl "dev.dpaa2_ni.$UNIT" 2>&1 echo "---- 5. dpaa2_mac objects ----" sysctl dev.dpaa2_mac 2>&1 | grep -v "^dev.dpaa2_mac.[0-9]*.%" || true echo "---- 6. link state of EVERY dpni (is only $IF affected?) ----" for u in 0 1 2 3 4 5 6 7 8 9; do S=$(sysctl -n "dev.dpaa2_ni.$u.link" 2>/dev/null | awk -F': ' '/Link State/{print $2}') R=$(sysctl -n "dev.dpaa2_ni.$u.link" 2>/dev/null | awk -F': ' '/Link Rate/{print $2}') # NOTE: this ifconfig triggers a full 8-PHY MDIO sweep (see the # comment on mc_state()). Tolerated only because dump() runs when # we are already wedged and the evidence is worth more than the # extra dice roll. Do not call dump() speculatively. F=$(ifconfig "dpni$u" 2>/dev/null | sed -n '1s/.*flags=[0-9a-fx]*<\([^>]*\)>.*/\1/p') echo " dpni$u internal='$S' rate=$R flags=$F" done echo "---- 7. PHY / miibus ----" sysctl dev.vscphy 2>&1 | grep -vE "%(pnpinfo|location|parent|driver)" | head -40 echo "---- 8. reachability over $IF ----" ping -c 3 -t 3 "$SW" 2>&1 echo "---- 9. reachability over $OOB (OUT OF BAND, raw ARP, no config) ----" echo "# $OOB is link-active to the same switch but unaddressed, so it" echo "# bypasses the IP stack entirely. If the switch answers here but" echo "# not on $IF, the fault is confined to $IF's DPMAC/datapath." if [ -x /usr/sbin/tcpdump ] || [ -x /usr/bin/tcpdump ]; then tcpdump -i "$OOB" -n -c 4 -s 128 arp > "$WORK/.oob.$$" 2>&1 & T=$! sleep 1 python3 "$WORK/arpsend.py" "$OOB" 172.16.254.253 "$SW" 6 2>&1 sleep 3 kill $T 2>/dev/null; wait $T 2>/dev/null cat "$WORK/.oob.$$" 2>/dev/null rm -f "$WORK/.oob.$$" fi echo "---- 10. the servers ----" for a in 2a02:1b8:10:205::6 2a02:1b8:10:205::7; do echo " ping6 $a:"; ping6 -c 2 -i 1 "$a" 2>&1 | tail -3 done echo "---- 11. neighbour / arp / route ----" ndp -an 2>&1 | head -20 arp -an 2>&1 | head -20 netstat -rn -f inet6 2>&1 | grep -i "205::" || true echo "---- 12. interface error counters ----" netstat -idW 2>&1 | head -20 echo "---- mbufs ----" netstat -m 2>&1 | head -8 echo "---- 13. recent kernel messages (link state changed / coalesced) ----" dmesg 2>&1 | grep -iE "dpni|dpaa2|dpmac|link" | tail -30 echo "---- 14. is r02 still up to the world? (expected: yes) ----" ping6 -c 2 -i 1 2606:4700:4700::1111 2>&1 | tail -3 birdc show protocols 2>/dev/null | head -15 echo "== END $LABEL ==" } >> "$DEBUG" 2>&1 } # --- mode: dump only ------------------------------------------------------- if [ "$MODE" = dump ]; then dump "MANUAL-DUMP" echo "appended to $DEBUG" exit 0 fi # --- guard 1: only one instance at a time (mkdir is atomic) ---------------- if ! mkdir "$LOCK" 2>/dev/null; then exit 0 fi trap 'rmdir "$LOCK" 2>/dev/null' EXIT INT TERM # --- guard 2: ignore the events our own flap generates --------------------- if [ -f "$QUIETF" ]; then AGE=$(( $(date +%s) - $(cat "$QUIETF" 2>/dev/null || echo 0) )) if [ "$AGE" -lt "$QUIET" ]; then exit 0 fi fi [ "$MODE" = event ] && sleep "$SETTLE" S=$(mc_state) # Breadcrumb, event mode only (~5/day given dpni0's flap rate). Without this a # healthy flap leaves no trace at all and we could not tell a working hook from a # silently broken one. It also gives us a local, timestamped flap log. if [ "$MODE" = event ]; then log "link transition seen: internal=$S rate=$(sysctl -n dev.dpaa2_ni.$UNIT.link 2>/dev/null | awk -F': ' '/Link Rate/{print $2}')" fi # The signature: the MC firmware's internal view says DOWN. Keying on the # sysctl rather than on reachability means an unrelated outage can never trick # us into flapping a healthy interface. The former second term (ifconfig # LOWER_UP = yes) was removed on 2026-08-20 - see the comment on mc_state() # above; reading it was itself triggering the fault. if [ "$S" != DOWN ]; then exit 0 fi # --- guard 3: hourly rate limit ------------------------------------------- NOW=$(date +%s) if [ -f "$HIST" ]; then N=$(awk -v now="$NOW" 'now - $1 < 3600' "$HIST" | wc -l | tr -d ' ') if [ "$N" -ge "$MAX_PER_HOUR" ]; then log "SUPPRESSED, already healed $N times this hour; link may be genuinely dead" notify "SUPPRESSED - $N heals this hour, link may be genuinely dead" exit 0 fi awk -v now="$NOW" 'now - $1 < 7200' "$HIST" > "$HIST.n" && mv "$HIST.n" "$HIST" fi echo "$NOW" >> "$HIST" log "bug 279352 signature: MC internal link state DOWN on $IF - healing" notify "WEDGED (MC internal link DOWN). Capturing + healing." # --- evidence BEFORE we change anything ----------------------------------- dump "BEFORE-HEAL" # --- the fix: the sequence the maintainers validated ---------------------- # 'none' or '10baseT/UTP', then back to auto. NOT 100baseTX (duplex mismatch # risk for no benefit). date +%s > "$QUIETF" ifconfig "$IF" media none sleep 3 ifconfig "$IF" media autoselect sleep 8 date +%s > "$QUIETF" S=$(mc_state) USED="media none" if [ "$S" != UP ]; then log "still $S after 'media none', trying 10baseT/UTP (PR 279352 c16)" ifconfig "$IF" media 10baseT/UTP sleep 3 ifconfig "$IF" media autoselect sleep 8 date +%s > "$QUIETF" S=$(mc_state) USED="media none, then 10baseT/UTP" fi # --- evidence AFTER: the diff against BEFORE-HEAL is the experiment ------- dump "AFTER-HEAL (applied: $USED)" if ping -c 2 -t 3 "$SW" >/dev/null 2>&1; then log "HEALED via '$USED': internal link=$S, switch reachable again" notify "HEALED via '$USED'. internal link=$S. See DEBUG.log" else log "HEAL FAILED via '$USED': internal link=$S, switch still unreachable." log "mode 2/3 per LOG.md 3b - a reboot may be needed; media flap not enough" notify "HEAL FAILED via '$USED'. internal link=$S. Still down - may need reboot." fi