- Go 96.2%
- Makefile 1.9%
- C 1.1%
- Shell 0.8%
| cmd/cve-check | ||
| internal | ||
| .gitignore | ||
| .golangci.yml | ||
| BUILD | ||
| CONTRIBUTORS.md | ||
| FILEMAP.md | ||
| go.mod | ||
| go.sum | ||
| install.sh | ||
| LICENSE.md | ||
| Makefile | ||
| README.md | ||
| SECURITY.md | ||
CVE Checker for Copy Fail (CVE-2026-31431)
Authors/Contributors: Patrick Doyle (Author/Maintainer), Effie Renard (Co-Author), Chris F. (Support/Code Review/Testing)
AI Disclosure: Claude Code used for writing tests files, writing test data, and a code security review. All edits were manually audited by one of the contributors.
Description
Single statically-linked Go binary that classifies a Linux host's exposure to CVE-2026-31431.
The tool is passive. It does not exploit the bug. The mechanism probe (see safety statement below) issues exactly two syscalls and never touches the vulnerable code path.
What it Checks
| Signal | Source | Flag |
|---|---|---|
| Kernel version vs. upstream fix | uname(2) |
--kernel-version |
| Distro changelog mentions the CVE | apt/rpm/apk or on-disk changelog.Debian.gz |
--changelog |
algif_aead module status |
/proc/modules, modprobe.d, /lib/modules |
--module |
| Vulnerable surface reachable | socket(AF_ALG) + bind(authencesn(...)) |
--mechanism |
| Compiled into vmlinux | /boot/config-$(uname -r) or A (one-/proc/config.gz |
(part of --module) |
If no check flags are given, all four run.
Output Modes
| Mode | Trigger | Use case |
|---|---|---|
| Pretty styled (Default) | TTY stdout, no --format override, no NO_COLOR env |
Interactive shell runs |
| Plain text | non-TTY stdout, OR --format=text, OR NO_COLOR=1, OR --no-color |
Shell Scripts, Pipes |
| JSON | --format=json |
Syslog/SIEM ingest, Shell Scripts |
| Quiet | --quiet |
Exit code only (automation/scripts) |
Force pretty when piped: cvecheck --pretty | tee report.txt.
Force plain in a TTY: cvecheck --no-color or NO_COLOR=1 cvecheck.
Safety Statement (Mechanism Probe)
The probe issues exactly two syscalls socket(AF_ALG, SOCK_SEQPACKET, 0), and bind(fd, &SockaddrALG{Type:"aead", Name:"authencesn(hmac(sha256), cbc(aes))"}). It's then followed by close(fd). It performs no setsockopt for a key, no accept, no sendmsg, no splice, and no pipe creation. The vulnerable code path requires sendmsg of crypto data combined with a splice from a page-cache-backed file descriptor; none of those operations occur, so the bug cannot trigger.
A successful probe means the surface is reachable. Whether the kernel still contains the bug is decided by the kernel-version + changelog signals.
Side effect: a successful bind autoloads algif_aead via the kernel module autoloader. The module check runs before the mechanism probe so the loaded-state report is pre-probe. To suppress autoload entirely, blacklist algif_aead first.
Exit Codes
| Code | Meaning |
|---|---|
0 |
PATCHED / NOT_VULNERABLE / MITIGATED / LIKELY_NOT_EXPLOITABLE |
2 |
VULNERABLE or MECHANISM_REACHABLE (partial scan, kernel-version not checked) |
3 |
INCONCLUSIVE (insufficient signals) |
Distro Coverage
| Distro | Detect | Kernel pkg | Notes |
|---|---|---|---|
| Ubuntu / Debian / Mint / Pop!_OS | apt |
linux-image-$(uname -r) |
Disk-first changelog read avoids apt changelog network fragility on signed kernels |
| RHEL / CentOS / Rocky / Alma / Fedora / Amazon Linux | rpm |
kernel |
Ships algif_aead built-in (CONFIG_..._AEAD=y); blacklist mitigation not effective |
| Oracle Linux | rpm |
kernel or kernel-uek |
UEK detected via uek substring in uname -r |
| openSUSE Leap / Tumbleweed / SLES | rpm |
kernel-default |
Tumbleweed omits ID_LIKE; handled by explicit ID match |
| Alpine | apk |
linux-lts / linux-virt / etc. |
Flavor picked from release suffix |
| Arch / CachyOS / Manjaro / Endeavour / Gentoo | none | n/a | Rolling/source: relies on kernel-version check |
Deploy
Automatic Install
Auto-detects your arch, downloads the right binary into $(pwd), and verifies its SHA-256 against the published SHA256SUMS:
curl -fsSL https://copyfail.pcdoyle.dev/install.sh | sh
The script exits non-zero on checksum mismatch and removes the bad file.
Download Binary (manual)
Pick the binary for your host architecture:
| Architecture | Binary |
|---|---|
Intel/AMD 64-bit (x86_64) |
cvecheck-linux-x86_64 |
ARM 64-bit (aarch64) |
cvecheck-linux-arm64 |
Intel/AMD 32-bit (i?86) |
cvecheck-linux-x86 |
Verify before running:
curl -LO https://github.com/pcdoyle/copy-fail-cve-2026-31431/releases/latest/download/cvecheck-linux-x86_64
curl -LO https://github.com/pcdoyle/copy-fail-cve-2026-31431/releases/latest/download/SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
chmod +x cvecheck-linux-x86_64
Build from Source
make build # bin/cvecheck-linux-{x86_64,arm64,x86}
# SCP(SFTP) to Server:
scp ./bin/cvecheck-linux-x86_64 host:/tmp/ # Less than 4 MB, static, no glibc dep
Run the Program
SSH to Host
# SSH:
ssh <host>
# Run Binary:
/tmp/cvecheck-linux-x86_64 # Human friendly pretty output.
/tmp/cvecheck-linux-x86_64 --format=json # Script, Syslog, and SIEM friendly
/tmp/cvecheck-linux-x86_64 --format=text # Script, and Pipe friendly
Run as SSH Remote Command
ssh <host> /tmp/cvecheck-linux-x86_64 # If run directly defaults to --format=text
ssh <host> /tmp/cvecheck-linux-x86_64 --format=json # Syslog/SIEM-friendly
For Containers/Chroot:
Mount the host root somewhere readable and pass --root:
docker run --rm -v /:/host:ro alpine /tmp/cvecheck --root /<host-path>
--root controls all on-disk lookups (/etc/os-release, modprobe.d, /lib/modules, /boot/config-*, changelog files). The kernel-version and mechanism probes still touch the running kernel via uname(2) and socket(AF_ALG, ...).
Precedence Ladder
Resolved in this order (first match wins):
PATCHED: Distro changelog references the CVE ID.NOT_VULNERABLE: Running kernel >= upstream fixed version.MITIGATED(grubby):initcall_blacklist=algif_aead_initpresent in/proc/cmdline.MITIGATED(kprobe): supervisor live with valid sentinel.MITIGATED(modprobe):algif_aeadblacklisted and not loaded and not built into vmlinux.LIKELY_NOT_EXPLOITABLE:AF_ALGunavailable and module not on disk.MECHANISM_REACHABLE: Surface reachable but kernel version not checked.VULNERABLE: Kernel in vuln range and mechanism reachable.INCONCLUSIVE: None of the above match.
If --mechanism runs alongside --kernel-version and both signal trouble, the result is VULNERABLE with a remediation hint.
Mitigations
cvecheck can install one of three stop-gaps. Pick the right method
for the host's algif_aead delivery shape and your reboot tolerance.
Run cvecheck first; the verdict's remediation block calls out the
correct subcommand. Or run cvecheck mitigation list for a side-by-side
comparison.
Strategy comparison
| Strategy | Type | Reboot | Reversible | Requires | Use on |
|---|---|---|---|---|---|
| modprobe | passive (file-only) | no | yes | linux + root | loadable-module hosts (Debian / Ubuntu / SUSE / Alpine / Arch) |
| kprobe | active (supervisor) | no | yes | linux/amd64 + root + BTF + FUNCTION_ERROR_INJECTION=y + BPF_KPROBE_OVERRIDE=y |
built-in algif_aead hosts (RHEL / Oracle / Amazon / CloudLinux) |
| grubby | passive (boot arg) | yes | yes | linux + root + /usr/sbin/grubby (RHEL-family) |
built-in algif_aead hosts where a reboot is acceptable |
Built-in hosts on RHEL-family distros can use grubby (persistent, reboot required) or kprobe (no-reboot stop-gap, needs error injection). Built-in hosts whose kernel lacks error injection AND that cannot run grubby have no stop-gap: only a kernel upgrade fixes them.
All three methods are stop-gaps. A kernel upgrade is the durable fix.
Command surface
cvecheck mitigation # overview help
cvecheck mitigation list # strategy comparison
cvecheck mitigation status # aggregate status of every strategy
cvecheck mitigation kprobe <verb> # install | uninstall | status
cvecheck mitigation modprobe <verb> # install | uninstall | status
cvecheck mitigation grubby <verb> # install | uninstall | status
Every leaf has a per-command --help describing what files it writes,
preflight requirements, and exit codes (e.g. cvecheck mitigation kprobe install --help).
mitigation modprobe: modprobe.d blacklist (loadable-module hosts)
Drops /etc/modprobe.d/cve-2026-31431.conf containing install algif_aead /bin/false,
then rmmod algif_aead. Kernel autoloader refuses to bring it back until the
conf is removed. Inert on built-in hosts.
sudo cvecheck mitigation modprobe install # write conf + rmmod (idempotent)
sudo cvecheck mitigation modprobe status # conf present? module loaded? built-in?
sudo cvecheck mitigation modprobe uninstall # remove conf only (module stays unloaded)
After install, re-running cvecheck flips the verdict to MITIGATED via the
existing module-blacklist rule (Blacklisted && !Loaded && !BuiltIn).
mitigation kprobe: eBPF kprobe stop-gap (built-in hosts)
Loads an eBPF kprobe at __x64_sys_socket that returns EAFNOSUPPORT to
non-root callers and logs each attempt to journald. No binary copy beyond
/usr/local/sbin/cvecheck, no separate artifact.
sudo /usr/local/sbin/cvecheck mitigation kprobe install # preflight + systemd unit + enable --now
sudo cvecheck mitigation kprobe status # liveness, sentinel, recent journal lines
sudo cvecheck mitigation kprobe uninstall # stop, disable, remove unit (binary kept)
journalctl -u cvecheck-mitigation -f # live blocked-call audit log
| Platform | Linux/amd64 only |
| Privileges | Root (CAP_BPF + CAP_SYS_ADMIN; ambient caps in the unit) |
| Kernel needs | BTF (/sys/kernel/btf/vmlinux), CONFIG_FUNCTION_ERROR_INJECTION=y, CONFIG_BPF_KPROBE_OVERRIDE=y, __x64_sys_socket carries ALLOW_ERROR_INJECTION |
| Allowlist | UID 0 only; hardcoded, no install flag |
| Persistence | systemd enable --now; lives across crashes (Restart=on-failure) and reboots |
| Audit | every blocked call hits journald: pid uid gid comm family=38 -> EAFNOSUPPORT |
| Scope | Stop-gap. Does not patch the bug. Operators must still ship a fixed kernel. |
After install, re-running cvecheck flips the verdict to MITIGATED with reason
"AF_ALG kprobe mitigation active (... pid N)". Killing the supervisor or
mitigation kprobe uninstall flips it back.
If preflight fails (no systemd, no BTF, no error injection support, kprobe attach refused) the install aborts non-zero with the failing gate. No fallback, a half-working install is worse than a failure in this case.
mitigation grubby: initcall_blacklist boot arg (RHEL-family, reboot required)
Appends initcall_blacklist=algif_aead_init to every kernel entry via
grubby --update-kernel=ALL --args=.... This is Red Hat's documented
mitigation for CVE-2026-31431: persistent across reboots, but the
running boot is unaffected until the host reboots.
sudo cvecheck mitigation grubby install # grubby --update-kernel=ALL --args=initcall_blacklist=algif_aead_init
sudo reboot # required: arg only takes effect on next boot
sudo cvecheck mitigation grubby status # 0 = arg in /proc/cmdline; 1 = pending reboot or not installed
sudo cvecheck mitigation grubby uninstall # remove the arg from every kernel entry
| Platform | Linux + root |
| Distro | RHEL-family (RHEL, CentOS, Rocky, AlmaLinux, Fedora, Amazon Linux, Oracle Linux); ships grubby preinstalled |
| Persistence | Boot loader edit; survives reboots until uninstalled |
| Reboot | Required. Mitigation is not active in the current boot until the host reboots. |
| Verify | cat /proc/cmdline | grep initcall_blacklist post-reboot, or cvecheck mitigation grubby status |
| Scope | Stop-gap. Does not patch the bug. Operators must still ship a fixed kernel. |
After reboot, re-running cvecheck flips the verdict to MITIGATED with
reason citing the boot arg. mitigation grubby uninstall removes the
arg from grubby's stored entries; the running boot keeps it until the
next reboot.
Remediation hints
- Loadable-module distros (Debian, Ubuntu, SUSE, Alpine, Arch): block
algif_aeaduntil kernel upgrade lands. Theinstall /bin/falseform prevents kernel autoloading; a plainblacklistdoes not.echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/cve-2026-31431.conf sudo rmmod algif_aead 2>/dev/null; true - Built-in RHEL-family hosts that can reboot (RHEL, CentOS, Rocky, Alma, Fedora, Amazon, Oracle): append the boot arg via grubby and reboot. Red Hat's documented mitigation. Persistent across reboots.
sudo grubby --update-kernel=ALL --args="initcall_blacklist=algif_aead_init" sudo reboot cat /proc/cmdline | grep initcall_blacklist # verify post-reboot - Built-in hosts that cannot reboot: blacklist files are no-ops because
CONFIG_CRYPTO_USER_API_AEAD=yputs the symbol insidevmlinux. Usecvecheck mitigation kprobe installif the kernel ships error injection support; otherwise only a kernel upgrade (orkpatch-style live patch) resolves exposure. The tool detects this.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
changelog: error="apt: exit status 100 ... Changelog unavailable for linux-signed-amd64" |
Debian/Ubuntu signed kernel; apt mirror does not serve the source-pkg changelog |
Error appears only when the on-disk file is missing in addition to the mirror being inaccessable, install apt-doc or wait for the mirror. |
module: ... config_src="" |
No kernel config readable (no /boot/config-*, no /proc/config.gz, /boot is root-only) |
Run as root, or accept that BuiltIn cannot be determined. Verdict may still resolves via other signals. |
mechanism: error="EAFNOSUPPORT" |
Kernel built without CONFIG_CRYPTO_USER_API, so the host is not vulnerable via AF_ALG |
Verdict will be LIKELY_NOT_EXPLOITABLE. |