#!/usr/bin/env bash # # copyfail-test.sh — Safe local check for CVE-2026-31431 ("Copy Fail") # # Hosted by WebWorld / checkdomain.ie # https://www.checkdomain.ie/copyfail # # This script is READ-ONLY. It does NOT exploit the vulnerability. # It checks: # 1. Your kernel version vs. known patched versions # 2. Whether the algif_aead module is loaded or blocked # 3. Whether an unprivileged AF_ALG bind succeeds (proxy for exposure) # # Usage: # curl -sSL https://www.checkdomain.ie/copyfail-test.sh | bash # # Or download and review first (recommended): # curl -sSL https://www.checkdomain.ie/copyfail-test.sh -o copyfail-test.sh # less copyfail-test.sh # bash copyfail-test.sh # # Exit codes: # 0 = PATCHED or MITIGATED # 1 = VULNERABLE (action required) # 2 = UNKNOWN (manual review needed) # set -u # ----- colours ----- if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then RED=$(tput setaf 1); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3) BLUE=$(tput setaf 4); BOLD=$(tput bold); RESET=$(tput sgr0) else RED=""; GREEN=""; YELLOW=""; BLUE=""; BOLD=""; RESET="" fi say() { printf "%s\n" "$*"; } ok() { printf " ${GREEN}✓${RESET} %s\n" "$*"; } warn() { printf " ${YELLOW}!${RESET} %s\n" "$*"; } bad() { printf " ${RED}✗${RESET} %s\n" "$*"; } info() { printf " ${BLUE}·${RESET} %s\n" "$*"; } header() { printf "\n${BOLD}%s${RESET}\n" "$1" printf "%s\n" "$(printf '%.0s-' {1..60})" } # ----- banner ----- cat <<'BANNER' ============================================================ Copy Fail (CVE-2026-31431) — Local Check WebWorld / checkdomain.ie ============================================================ BANNER # ----- 1. system info ----- header "System" KERNEL=$(uname -r) ARCH=$(uname -m) info "Kernel: $KERNEL" info "Architecture: $ARCH" DISTRO="unknown" DISTRO_VER="unknown" if [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release DISTRO="${ID:-unknown}" DISTRO_VER="${VERSION_ID:-unknown}" info "Distribution: ${PRETTY_NAME:-$DISTRO $DISTRO_VER}" fi # ----- 2. kernel version verdict ----- header "Kernel version check" # Strip distro suffix to get bare kernel version (e.g. 5.15.0-25-generic -> 5.15.0) BARE_KERNEL=$(echo "$KERNEL" | sed -E 's/-.*$//') KMAJ=$(echo "$BARE_KERNEL" | cut -d. -f1) KMIN=$(echo "$BARE_KERNEL" | cut -d. -f2) KPAT=$(echo "$BARE_KERNEL" | cut -d. -f3) KMAJ=${KMAJ:-0}; KMIN=${KMIN:-0}; KPAT=${KPAT:-0} # The 2017 vulnerable optimization landed in 4.9. Anything older lacks the # combination that creates Copy Fail (per Theori advisory). PRE_2017=0 if (( KMAJ < 4 )) || (( KMAJ == 4 && KMIN < 9 )); then PRE_2017=1 fi # Check kernel build date — patches landed in mainline April 1, 2026. # A kernel built after ~April 15, 2026 is very likely patched, even if the # version string looks old (distros backport silently). BUILD_DATE_EPOCH=0 if [[ -f /proc/version ]]; then # /proc/version sometimes contains a build date BUILD_LINE=$(cat /proc/version 2>/dev/null || true) info "Build info: $(echo "$BUILD_LINE" | head -c 100)..." fi # Try to read kernel package build date as a stronger signal PKG_DATE="" if command -v dpkg-query >/dev/null 2>&1; then PKG=$(dpkg-query -W -f='${Package}\n' 'linux-image-*' 2>/dev/null | grep -v 'unsigned\|generic-hwe\|meta' | head -1 || true) if [[ -n "$PKG" ]]; then PKG_DATE=$(dpkg-query -W -f='${db-fsys:Last-Modified}\n' "$PKG" 2>/dev/null || true) fi elif command -v rpm >/dev/null 2>&1; then PKG_DATE=$(rpm -q --qf '%{INSTALLTIME}\n' kernel 2>/dev/null | tail -1 || true) fi if [[ -n "$PKG_DATE" && "$PKG_DATE" =~ ^[0-9]+$ ]]; then BUILD_DATE_EPOCH=$PKG_DATE HUMAN_DATE=$(date -d "@$PKG_DATE" 2>/dev/null || echo "$PKG_DATE") info "Kernel pkg installed: $HUMAN_DATE" fi # Cutoff: April 15, 2026 = 1776499200 (rough; gives distros 2 weeks to backport) PATCH_CUTOFF=1776499200 KERNEL_VERDICT="unknown" if (( PRE_2017 == 1 )); then KERNEL_VERDICT="patched-by-age" ok "Kernel predates 2017 vulnerable change (< 4.9). Not affected by Copy Fail." elif (( BUILD_DATE_EPOCH > PATCH_CUTOFF )); then KERNEL_VERDICT="likely-patched" ok "Kernel package installed after April 15, 2026 — likely patched." info "(Distros backport security fixes silently; trust the build date over the version string.)" else KERNEL_VERDICT="needs-review" warn "Cannot confirm patch status from kernel version alone." info "Distros backport CVE fixes without changing the version string." info "Run your distro's update command and reboot to be sure." fi # ----- 3. algif_aead module status ----- header "AF_ALG / algif_aead module status" MODULE_BLOCKED=0 MODULE_LOADED=0 if grep -rqsE '^\s*(install|blacklist)\s+algif_aead' /etc/modprobe.d/ 2>/dev/null; then MODULE_BLOCKED=1 ok "algif_aead is blocked via modprobe.d (good mitigation if not patched)" fi if lsmod 2>/dev/null | grep -q '^algif_aead'; then MODULE_LOADED=1 warn "algif_aead module is currently loaded" elif (( MODULE_BLOCKED == 0 )); then info "algif_aead is not currently loaded (but can be auto-loaded on demand)" fi # ----- 4. AF_ALG socket bind probe (harmless) ----- header "AF_ALG exposure probe" PROBE_RESULT="unknown" if command -v python3 >/dev/null 2>&1; then # This ONLY attempts to bind an AF_ALG socket to the authencesn algorithm. # No exploit, no writes, no splice, no shellcode. If the bind succeeds, the # vulnerable interface is reachable from unprivileged userspace. PROBE=$(python3 - <<'PY' 2>&1 import socket, sys try: s = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET s.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) s.close() print("REACHABLE") except PermissionError: print("BLOCKED_PERM") except OSError as e: print(f"BLOCKED_OS:{e.errno}") except Exception as e: print(f"ERROR:{type(e).__name__}") PY ) case "$PROBE" in REACHABLE) PROBE_RESULT="reachable" warn "Vulnerable interface is reachable from unprivileged userspace." info "This alone doesn't mean you're exploitable — your kernel may be patched." ;; BLOCKED_PERM|BLOCKED_OS:*) PROBE_RESULT="blocked" ok "AF_ALG bind blocked ($PROBE) — exposure is reduced." ;; *) PROBE_RESULT="unknown" info "Probe inconclusive: $PROBE" ;; esac else info "python3 not available; skipping live probe." fi # ----- 5. container hint ----- header "Container check" IN_CONTAINER=0 if [[ -f /.dockerenv ]] || grep -qaE '(docker|kubepods|containerd|lxc)' /proc/1/cgroup 2>/dev/null; then IN_CONTAINER=1 warn "This appears to be running INSIDE a container." info "Copy Fail breaks container boundaries. Patch the HOST kernel, not just the container." info "Also apply seccomp policy on the host to block AF_ALG socket creation." else ok "Running on a host (not a container)." fi # ----- 6. final verdict ----- header "Verdict" EXIT_CODE=2 if [[ "$KERNEL_VERDICT" == "patched-by-age" ]]; then printf "${GREEN}${BOLD}NOT AFFECTED${RESET} — kernel predates the 2017 change.\n" EXIT_CODE=0 elif [[ "$KERNEL_VERDICT" == "likely-patched" ]]; then printf "${GREEN}${BOLD}LIKELY PATCHED${RESET} — kernel package installed after the patch window.\n" info "Confirm with: your distro's security advisory listing CVE-2026-31431" EXIT_CODE=0 elif (( MODULE_BLOCKED == 1 )) && [[ "$PROBE_RESULT" == "blocked" ]]; then printf "${YELLOW}${BOLD}MITIGATED${RESET} — algif_aead is blocked. Patch ASAP anyway.\n" EXIT_CODE=0 elif [[ "$PROBE_RESULT" == "reachable" ]]; then printf "${RED}${BOLD}LIKELY VULNERABLE${RESET} — AF_ALG is reachable and patch status unconfirmed.\n" bad "Action required: patch your kernel and reboot, or block algif_aead now." EXIT_CODE=1 else printf "${YELLOW}${BOLD}NEEDS MANUAL REVIEW${RESET}\n" info "Run your distro's update command and reboot, then re-run this script." EXIT_CODE=2 fi # ----- 7. recommended actions ----- header "Recommended actions" case "$DISTRO" in ubuntu|debian) say " sudo apt update && sudo apt upgrade && sudo reboot" ;; rhel|centos|rocky|almalinux|fedora) say " sudo dnf update kernel && sudo reboot" ;; amzn) say " sudo yum update kernel && sudo reboot" ;; sles|suse|opensuse*) say " sudo zypper update kernel-default && sudo reboot" ;; *) say " Use your distribution's package manager to update the kernel, then reboot." ;; esac if (( MODULE_BLOCKED == 0 )); then say "" say " If you can't patch right now, block the vulnerable module:" say " echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/disable-algif.conf" say " sudo rmmod algif_aead 2>/dev/null || true" fi say "" say " Full guidance: https://www.checkdomain.ie/copyfail" say " Need help? Open a ticket with WebWorld support." say "" exit "$EXIT_CODE"