Open-source project
rootsecdev/cve_2026_31431 avatar
rootsecdev/cve_2026_31431

The Copy Fail detector checks whether a kernel corrupts page cache through in-place AEAD

Exploit POC for CVE_2026_31431

575 stars112 forksPythonLicense varies

At a glance

What is it?
This repository holds a non-destructive detector and a proof-of-concept privilege escalation for CVE-2026-31431, a bug where the Linux algif_aead module writes four attacker-controlled bytes into the page cache of a file spliced into an in-place AEAD operation. The detector uses a sentinel file and never touches system binaries, which makes it usable on a machine you care about.
Who is it for?
Adopt the detector script if you run Linux hosts and need to know whether the algif_aead module in your kernel is vulnerable, because it works on a sentinel file in a temp directory, exits with a distinct code for each outcome and removes its own file on exit. Do not run the exploit script on a host you are not authorised to test, since the README states the technique is real privilege escalation and is illegal in most jurisdictions without that authorisation.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 178 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The bug is four bytes of scratch space landing in a file's page cache

CVE-2026-31431, which the repository calls the Copy Fail bug, lives in the Linux kernel's algif_aead module and the authencesn algorithm. The mechanism is worth following closely because it explains why the whole thing works without touching the disk.

algif_aead runs AEAD operations in place, meaning req->src is the same as req->dst. That is normally fine, because in-place encryption reads and writes the same buffer. The problem arrives when the source data comes from a regular file through splice(). When you splice a file page into the socket, the destination scatterlist ends up holding references to the file's page-cache pages, and the kernel will write into them.

Then the authencesn algorithm, which is hmac(sha256) with cbc(aes), performs a 4-byte scratch write. The bytes are the AAD's seqno_lo field, bytes 4 to 7 of the AAD supplied through sendmsg, and they get written into that destination, which is the page-cache page rather than a private buffer.

The consequence is precise and unusual. Because the on-disk file is never modified, there is no on-disk signature. The corruption exists only in the running kernel's view, and it is observed only by readers that share the page cache. /etc/passwd and /usr/bin/su are both world-readable, which means an unprivileged local user can corrupt the running kernel's view of either one.

Affected kernels are those carrying commit 72548b093ee3, the in-place AEAD change from 2017, without the upstream revert. The disclosure confirmed Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3 and SUSE 16, but the README notes the underlying primitive predates that range, which is the important part for anyone auditing an older kernel.

The upstream fix reverts in-place AEAD operations to out-of-place, keeping page-cache pages out of writable scatterlists.

The detector works on a sentinel file and never touches a system binary

The repository contains two files and the README is careful about which one is safe to run. test_cve_2026_31431.py is the non-destructive detector, operating on a sentinel file in a temp directory and never touching system binaries. exploit_cve_2026_31431.py is the privilege escalation. Both are pure Python 3.10 or newer stdlib, with no dependencies to install.

The detector runs with one command:

bash
python3 test_cve_2026_31431.py

What it does has five steps. First it confirms that AF_ALG and the authencesn(hmac(sha256),cbc(aes)) algorithm are reachable from an unprivileged process, since without both there is nothing to test. Second it creates a 4 KiB sentinel file in a temp directory and populates the page cache. Third it sends 8 bytes of AAD inline through sendmsg with a cmsg, with seqno_lo set to the marker PWND, and then uses os.splice() to move 32 bytes of the sentinel's page-cache page into the AF_ALG operation socket. Fourth it calls recv() to drive decryption: the auth check fails with EBADMSG, but the scratch write fires regardless. Fifth it re-reads the file, from the page cache rather than the disk, and looks for the marker.

The output classes are what make this usable in a scan. Precondition not met means AF_ALG or authencesn is unavailable and the exit code is 0. VULNERABLE to CVE-2026-31431 means the marker landed in the spliced page and the exit code is 2. Page cache MODIFIED via in-place AEAD splice path means the page was written to but the marker did not land at the expected position, and the README says to treat that as vulnerable, also exiting 2. Page cache intact means the system is patched and the exit code is 0. A test error exits 1.

So the exit codes carry the verdict, which means the detector can be driven from a script without parsing text. The README is explicit about the containment: the detector never touches /usr/bin/su, /etc/passwd or any other file outside the temp directory it creates, and that file is removed on exit.

Why the exploit needs a four-digit UID and no name service cache

The proof of concept converts the page-cache corruption into root, and the README is unusually precise about the conditions that have to hold.

The sequence is four steps. The script looks up the running user's UID line in /etc/passwd and finds the byte offset of the 4-character UID field. It issues one write4 against that offset, replacing the UID with 0000. It calls pwd.getpwnam(user) to confirm that libc now reports UID 0. With the --shell flag it then calls execvp("su", ["su", user]), and the user enters their own password: PAM validates against /etc/shadow, which is untouched, and then setuid(getpwnam(user).pw_uid) lands at 0.

That last detail is what makes the bug so cheap. The password check still happens against the real shadow file, so the tool does not crack anything. It only needs the name lookup to report a different UID, and it gets that by writing four bytes into a page the kernel has already cached.

The requirements section lists three conditions. The running user must have a 4-digit UID, between 1000 and 9999, because the write is a fixed 4-byte replacement; 1- to 3-digit UIDs need multi-shot writes, which means extending write4. No NSS caching daemon, meaning nscd, sssd or systemd-userdbd, may be masking /etc/passwd reads, and if getpwnam still returns the real UID after the patch, the workaround is to restart or bypass the cache, or pick a different user. And the /etc/passwd page must stay in cache between the patch and the su exec, which the README says is in practice reliable on any system with normal memory pressure.

The write4 primitive itself is described in four lines of pseudocode. sendmsg carries the 8-byte AAD with cmsg entries for ALG_SET_OP=DECRYPT, ALG_SET_IV and ALG_SET_AEAD_ASSOCLEN=8, plus the MSG_MORE flag. Then splice moves 32 bytes from the target file into a pipe at a given offset, another splice moves 32 bytes from the pipe into the operation socket, and recv() returns EBADMSG after the scratch write has already landed.

The landing offset within the page corresponds to the offset_src passed to splice, which is what turns a four-byte write into an arbitrary-offset write against a chosen file.

The corrupted page persists until you evict it, and the cleanup is documented

The aftermath is the part that will surprise an operator who runs the exploit on a shared machine. The on-disk /etc/passwd is unchanged, so a reboot or a page eviction fixes everything. Between the patch and the cleanup, the kernel's view of the file is wrong.

The dry-run path, which is the exploit script without --shell, auto-evicts the corrupted page on exit using POSIX_FADV_DONTNEED, so UID to name lookups go back to normal immediately.

After --shell, the page is left corrupted until you clear it. While it is corrupted, anything resolving UID 1000 to a name, such as ls, file managers and scp or sftp ownership checks, will fail or show numeric IDs. The unprivileged eviction is one command:

bash
# unprivileged - request page-cache eviction for /etc/passwd:
python3 -c "import os; fd=os.open('/etc/passwd', os.O_RDONLY); \
            os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED); os.close(fd)"

From a root shell the alternative is dropping the page cache wholesale:

bash
# from the root shell:
echo 3 > /proc/sys/vm/drop_caches

A reboot also clears it.

The security reading of this is worth stating plainly. The bug corrupts a shared, world-readable kernel structure and leaves the evidence in a place that no file integrity checker will find, because the file on disk is untouched. A monitoring agent that reads /etc/passwd through the page cache may see corrupted data, while a disk-level integrity tool sees nothing wrong at all. The README's dry-run auto-eviction exists precisely to limit that window, and it is the reason to prefer the detector over the exploit on any machine that matters.

The mitigation disables the module outright until a patched kernel arrives

Until the patched kernel reaches your distribution, the README gives a mitigation that stops the module from loading at all:

bash
sudo tee /etc/modprobe.d/disable-algif-aead.conf <<<'install algif_aead /bin/false'
sudo rmmod algif_aead 2>/dev/null

The first line writes a modprobe configuration file that installs algif_aead as /bin/false, so loading the module succeeds and does nothing. The second line removes it if it is currently loaded, with stderr suppressed so a missing module is not an error. The persistent file means the module stays disabled across reboots.

The verification step is the detector itself. After applying the mitigation, the README says test_cve_2026_31431.py should report Precondition not met and exit 0, because AF_ALG can no longer reach the authencesn algorithm through algif_aead.

What does this cost? The README does not discuss the impact, and that is worth working out before applying it broadly. algif_aead is the kernel interface for AEAD algorithms through the crypto API's socket interface, so anything on the host that uses it for kernel-space authenticated encryption stops working. Software that links against the AF_ALG socket interface for AEAD, rather than for hash or cipher operations covered by other algif modules, will see the algorithm become unreachable.

The upstream fix is the cleaner answer. It reverts in-place AEAD operations to out-of-place, which keeps page-cache pages out of writable scatterlists and removes the primitive rather than the feature. The module-disabling mitigation is the stopgap for the window between disclosure and distribution updates, and the README frames it that way.

Where this toolkit is the wrong tool

Three limits, and the first is about authorisation rather than technique.

The first is the legal and operational line the README draws itself. It says to use the toolkit only on hosts you own or are explicitly engaged to assess, that the LPE modifies in-memory state in the page cache, that the technique is real privilege escalation, and that running it on systems without authorisation is illegal in most jurisdictions. The exploit script is the one with a root shell at the end of it, and the detector is the one you can point at a fleet.

The second is that the detector only tests one primitive, on one algorithm, for one shape of bug. It confirms AF_ALG and authencesn are reachable, then relies on the splice-into-scatterlist path. A kernel where authencesn is unavailable but another AEAD algorithm is reachable would report Precondition not met and exit 0, which reads as patched but is only untested. The README is honest that this is a precondition failure, and a scan that treats exit 0 as a clean bill of health for the whole kernel is drawing a conclusion the script does not support.

The third is the state of the disclosure. The bug was disclosed on 2026-04-29, the last push to the repository was on 2026-04-29 as well, and there are no GitHub releases, so there is no version to pin. The repository's licence is not stated in the metadata either. The detection logic is written against a specific kernel behaviour and a specific mitigation, so it will need updating as distributions ship the fix, and nothing in the repository signals when that happens.

The alternative approach for most organisations is a patch-level check rather than a behavioural probe. The README names the introducing commit, 72548b093ee3, and the upstream revert, so scanning your distribution's kernel package changelog for the fix is a deterministic check that does not touch the kernel at all. The difference in approach is that the detector tells you what the running kernel actually does, including whether your distribution backported the revert, while the package check tells you what your distribution believes it shipped. Running the detector across a fleet is the way to find the machines whose package changelog and actual kernel disagree, and that is the case where the behavioural probe earns its place.

Editorial conclusion

Adopt the detector script if you run Linux hosts and need to know whether the algif_aead module in your kernel is vulnerable, because it works on a sentinel file in a temp directory, exits with a distinct code for each outcome and removes its own file on exit. Do not run the exploit script on a host you are not authorised to test, since the README states the technique is real privilege escalation and is illegal in most jurisdictions without that authorisation. Verify first that your kernel carries commit 72548b093ee3 without the upstream revert, that the affected distributions confirmed by the disclosure, Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3 and SUSE 16, cover your fleet, that no NSS caching daemon such as nscd, sssd or systemd-userdbd is masking /etc/passwd reads on the target, and that the mitigation is in place before anyone reaches for the exploit path.

Frequently asked questions

What is CVE-2026-31431 and how does the Copy Fail bug work?

It is a bug in the Linux algif_aead module where in-place AEAD operations, req->src equal to req->dst, let the authencesn algorithm write four attacker-controlled bytes from the AAD seqno_lo field into a file's page cache when the source is spliced in. The on-disk file is unchanged, so the corruption is visible only to readers sharing the page cache.

How do I check whether my Linux host is vulnerable to CVE-2026-31431?

Run python3 test_cve_2026_31431.py, which operates on a sentinel file in a temp directory and removes it on exit. Exit code 2 means vulnerable, exit 0 means not vulnerable or the precondition was not met, and exit 1 means a test error.

What does the CVE-2026-31431 exploit script actually modify?

It looks up the running user's UID line in /etc/passwd, finds the byte offset of the UID field and writes 0000 over it in the page cache, then confirms through pwd.getpwnam that the UID is now 0. The on-disk file is never modified.

How do I clear the corrupted /etc/passwd page after running the exploit?

Without --shell the script auto-evicts the page on exit using POSIX_FADV_DONTNEED. After --shell you can request eviction unprivileged with a posix_fadvise call on /etc/passwd, or drop the caches with echo 3 > /proc/sys/vm/drop_caches from a root shell, and a reboot also clears it.

What mitigation is available for CVE-2026-31431 before a patched kernel ships?

Disable the module by writing a modprobe configuration that installs algif_aead as /bin/false and removing the loaded module with rmmod. After that the detector should report Precondition not met and exit 0. The upstream fix instead reverts in-place AEAD operations to out-of-place.

Which kernels are affected by CVE-2026-31431?

Kernels carrying commit 72548b093ee3, the in-place AEAD change from 2017, without the upstream revert. The disclosure confirmed Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3 and SUSE 16, and the README notes the underlying primitive predates that range.

Official sources

  1. Issues
  2. README
  3. rootsecdev/cve_2026_31431 on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rootsecdev-cve-2026-31431.svg)](https://hysenlabs.com/projects/rootsecdev-cve-2026-31431)

Was this useful?