* [LTP] [PATCH] sockets/xfrm02: Add ESP-in-TCP page cache corruption test
@ 2026-05-13 14:36 Andrea Cervesato
2026-05-13 15:00 ` Cyril Hrubis
2026-05-13 16:27 ` [LTP] " linuxtestproject.agent
0 siblings, 2 replies; 3+ messages in thread
From: Andrea Cervesato @ 2026-05-13 14:36 UTC (permalink / raw)
To: Linux Test Project
From: Andrea Cervesato <andrea.cervesato@suse.com>
Verify that ESP-in-TCP (espintcp) does not corrupt the page cache when
file data is spliced into a TCP socket. When MSG_SPLICE_PAGES references
page cache pages directly in the skb and the receiving socket has
espintcp ULP enabled, the kernel's ESP handler may decrypt the payload
in-place on those pages, corrupting the cached file contents.
The test sets up an ESP-in-TCP xfrm state on IPv6 loopback, writes
known data to a file, creates a TCP connection where the receiver
enables espintcp ULP, splices the file data into the TCP socket as
part of a crafted ESP-in-TCP frame, and then verifies whether the
page cache was corrupted.
Reproducer based on:
https://github.com/v12-security/pocs/tree/main/fragnesia
Signed-off-by: Andrea Cervesato <andrea.cervesato@suse.com>
---
runtest/cve | 1 +
testcases/network/sockets/.gitignore | 1 +
testcases/network/sockets/xfrm02.c | 229 +++++++++++++++++++++++++++++++++++
3 files changed, 231 insertions(+)
diff --git a/runtest/cve b/runtest/cve
index 530f8751ed3a8e8aa7e9110d89d577df3e8cc6ce..5fe83ec2d1803a5c0f6b4eba05fcf00cc80c8809 100644
--- a/runtest/cve
+++ b/runtest/cve
@@ -95,3 +95,4 @@ cve-2025-38236 cve-2025-38236
cve-2025-21756 cve-2025-21756
cve-2026-31431 af_alg08
cve-2026-43284 xfrm01
+cve-2026-fragnesia xfrm02
diff --git a/testcases/network/sockets/.gitignore b/testcases/network/sockets/.gitignore
index 6f3c0ad84c000f0214f371c6a601afb592b15faa..35bc0462b676b041d9a5b52a37fded973d0157a9 100644
--- a/testcases/network/sockets/.gitignore
+++ b/testcases/network/sockets/.gitignore
@@ -1 +1,2 @@
/xfrm01
+/xfrm02
diff --git a/testcases/network/sockets/xfrm02.c b/testcases/network/sockets/xfrm02.c
new file mode 100644
index 0000000000000000000000000000000000000000..7497998015b6bd51db36e80f077173963fdef394
--- /dev/null
+++ b/testcases/network/sockets/xfrm02.c
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2026 SUSE LLC Andrea Cervesato <andrea.cervesato@suse.com>
+ */
+
+/*\
+ * Verify that ESP-in-TCP (espintcp) does not corrupt the page cache
+ * when file data is spliced into a TCP socket.
+ *
+ * When file data is spliced into a TCP socket, the kernel uses
+ * MSG_SPLICE_PAGES to reference page cache pages directly in the skb.
+ * If the receiving socket has TCP_ULP "espintcp" enabled and a matching
+ * xfrm SA exists, the kernel's ESP handler decrypts the payload
+ * in-place on those page cache pages, corrupting the cached file
+ * contents.
+ *
+ * The test sets up an ESP-in-TCP xfrm state on IPv6 loopback, writes
+ * known data to a file, creates a TCP connection where the receiver
+ * enables espintcp ULP, splices the file data into the TCP socket as
+ * part of a crafted ESP-in-TCP frame, and then verifies whether the
+ * page cache was corrupted.
+ *
+ * Reproducer based on:
+ * https://github.com/v12-security/pocs/tree/main/fragnesia
+ */
+
+#define _GNU_SOURCE
+
+#include "tst_test.h"
+#include "tst_net.h"
+#include "tst_netdevice.h"
+#include "lapi/tcp.h"
+#include "lapi/splice.h"
+
+#define TESTFILE "pagecache_test"
+#define DATA_SIZE 4096
+
+#define SPI 0x100
+#define TCP_PORT 5556
+#define IV_LEN 8
+#define ESP_HDR_SIZE 16
+#define AES_KEYLEN 16
+#define SALT_LEN 4
+#define KEYTOTAL (AES_KEYLEN + SALT_LEN)
+
+/* ESP-in-TCP frame prefix: 2-byte length + ESP header */
+#define PREFIX_SIZE (2 + ESP_HDR_SIZE)
+
+#define XFRM_CMD \
+ "ip xfrm state add" \
+ " src ::1 dst ::1" \
+ " proto esp spi 0x%08x" \
+ " encap espintcp %d %d ::" \
+ " aead 'rfc4106(gcm(aes))' %s 128" \
+ " mode transport"
+
+static const uint8_t aead_key[KEYTOTAL] = {
+ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
+ 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
+ 0x01, 0x02, 0x03, 0x04
+};
+
+static uint8_t original[DATA_SIZE];
+static int file_fd = -1;
+static int srv_fd = -1;
+
+static void setup(void)
+{
+ char keyhex[KEYTOTAL * 2 + 3];
+ char cmd[512];
+ int i, ret;
+
+ tst_setup_netns();
+ NETDEV_SET_STATE("lo", 1);
+
+ keyhex[0] = '0';
+ keyhex[1] = 'x';
+ for (i = 0; i < KEYTOTAL; i++)
+ sprintf(keyhex + 2 + i * 2, "%02x", aead_key[i]);
+
+ snprintf(cmd, sizeof(cmd), XFRM_CMD, SPI, TCP_PORT, TCP_PORT, keyhex);
+
+ ret = tst_system(cmd);
+ if (ret)
+ tst_brk(TBROK, "Failed to install xfrm ESP-in-TCP state");
+
+ for (i = 0; i < DATA_SIZE; i++)
+ original[i] = (uint8_t)(i & 0xff);
+}
+
+static void try_corrupt(void)
+{
+ struct sockaddr_in6 addr = {
+ .sin6_family = AF_INET6,
+ .sin6_addr = IN6ADDR_LOOPBACK_INIT,
+ .sin6_port = htons(TCP_PORT),
+ };
+ uint8_t prefix[PREFIX_SIZE];
+ uint16_t frame_len;
+ uint32_t spi_net, seq_net;
+ char ulp[] = "espintcp";
+ char drain;
+ int acc_fd;
+ loff_t off;
+ pid_t pid;
+ int status;
+
+ frame_len = htons(PREFIX_SIZE + DATA_SIZE);
+ memcpy(prefix, &frame_len, 2);
+
+ spi_net = htonl(SPI);
+ memcpy(prefix + 2, &spi_net, 4);
+
+ seq_net = htonl(1);
+ memcpy(prefix + 6, &seq_net, 4);
+
+ memset(prefix + 10, 0xcc, IV_LEN);
+
+ srv_fd = SAFE_SOCKET(AF_INET6, SOCK_STREAM, 0);
+ SAFE_SETSOCKOPT_INT(srv_fd, SOL_SOCKET, SO_REUSEADDR, 1);
+ SAFE_BIND(srv_fd, (struct sockaddr *)&addr, sizeof(addr));
+ SAFE_LISTEN(srv_fd, 1);
+
+ pid = SAFE_FORK();
+
+ if (!pid) {
+ int cli_fd, pipefd[2];
+
+ SAFE_CLOSE(srv_fd);
+
+ cli_fd = SAFE_SOCKET(AF_INET6, SOCK_STREAM, 0);
+ SAFE_SETSOCKOPT_INT(cli_fd, IPPROTO_TCP, TCP_NODELAY, 1);
+ SAFE_CONNECT(cli_fd, (struct sockaddr *)&addr, sizeof(addr));
+
+ SAFE_SEND(1, cli_fd, prefix, sizeof(prefix), 0);
+ SAFE_PIPE(pipefd);
+
+ off = 0;
+ SAFE_SPLICE(file_fd, &off, pipefd[1], NULL, DATA_SIZE, 0);
+
+ /*
+ * Splice pipe into TCP socket. The kernel uses
+ * MSG_SPLICE_PAGES to keep page cache references in
+ * the skb. On loopback the receiver's ESP handler may
+ * decrypt in-place, corrupting the page cache. May
+ * fail on patched kernels.
+ */
+ splice(pipefd[0], NULL, cli_fd, NULL, DATA_SIZE, 0);
+
+ SAFE_CLOSE(pipefd[0]);
+ SAFE_CLOSE(pipefd[1]);
+ SAFE_CLOSE(cli_fd);
+
+ TST_CHECKPOINT_WAKE(0);
+ exit(0);
+ }
+
+ acc_fd = SAFE_ACCEPT(srv_fd, NULL, NULL);
+ SAFE_CLOSE(srv_fd);
+
+ TST_CHECKPOINT_WAIT(0);
+
+ SAFE_SETSOCKOPT(acc_fd, IPPROTO_TCP, TCP_ULP, ulp, sizeof(ulp));
+
+ /* Kick the espintcp strparser to process buffered ESP data */
+ SAFE_RECV(0, acc_fd, &drain, 1, 0);
+
+ SAFE_CLOSE(acc_fd);
+ SAFE_WAITPID(pid, &status, 0);
+}
+
+static void run(void)
+{
+ uint8_t readback[DATA_SIZE];
+
+ file_fd = SAFE_OPEN(TESTFILE, O_WRONLY | O_CREAT, 0444);
+ SAFE_WRITE(SAFE_WRITE_ALL, file_fd, original, DATA_SIZE);
+ SAFE_CLOSE(file_fd);
+
+ file_fd = SAFE_OPEN(TESTFILE, O_RDONLY);
+ try_corrupt();
+ 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, DATA_SIZE) != 0)
+ tst_res(TFAIL, "Page cache corrupted via xfrm ESP-in-TCP splice");
+ else
+ tst_res(TPASS, "Page cache was not corrupted");
+
+ SAFE_UNLINK(TESTFILE);
+}
+
+static void cleanup(void)
+{
+ if (srv_fd != -1)
+ SAFE_CLOSE(srv_fd);
+
+ if (file_fd != -1)
+ SAFE_CLOSE(file_fd);
+}
+
+static struct tst_test test = {
+ .test_all = run,
+ .setup = setup,
+ .cleanup = cleanup,
+ .needs_tmpdir = 1,
+ .forks_child = 1,
+ .needs_checkpoints = 1,
+ .needs_kconfigs = (const char *[]) {
+ "CONFIG_USER_NS=y",
+ "CONFIG_NET_NS=y",
+ "CONFIG_XFRM",
+ "CONFIG_INET6_ESP",
+ "CONFIG_INET6_ESPINTCP",
+ "CONFIG_CRYPTO_GCM",
+ NULL
+ },
+ .save_restore = (const struct tst_path_val[]) {
+ {"/proc/sys/user/max_user_namespaces", "1024", TST_SR_SKIP},
+ {}
+ },
+ .needs_cmds = (struct tst_cmd[]) {
+ {.cmd = "ip"},
+ {}
+ },
+};
---
base-commit: e1fc50957c98ae4c27064756e063de0e7136cde3
change-id: 20260513-fragnesia-9588d855becf
Best regards,
--
Andrea Cervesato <andrea.cervesato@suse.com>
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [LTP] [PATCH] sockets/xfrm02: Add ESP-in-TCP page cache corruption test
2026-05-13 14:36 [LTP] [PATCH] sockets/xfrm02: Add ESP-in-TCP page cache corruption test Andrea Cervesato
@ 2026-05-13 15:00 ` Cyril Hrubis
2026-05-13 16:27 ` [LTP] " linuxtestproject.agent
1 sibling, 0 replies; 3+ messages in thread
From: Cyril Hrubis @ 2026-05-13 15:00 UTC (permalink / raw)
To: Andrea Cervesato; +Cc: Linux Test Project
Hi!
> +#define _GNU_SOURCE
> +
> +#include "tst_test.h"
> +#include "tst_net.h"
> +#include "tst_netdevice.h"
> +#include "lapi/tcp.h"
> +#include "lapi/splice.h"
> +
> +#define TESTFILE "pagecache_test"
> +#define DATA_SIZE 4096
> +
> +#define SPI 0x100
> +#define TCP_PORT 5556
> +#define IV_LEN 8
> +#define ESP_HDR_SIZE 16
> +#define AES_KEYLEN 16
> +#define SALT_LEN 4
> +#define KEYTOTAL (AES_KEYLEN + SALT_LEN)
> +
> +/* ESP-in-TCP frame prefix: 2-byte length + ESP header */
> +#define PREFIX_SIZE (2 + ESP_HDR_SIZE)
> +
> +#define XFRM_CMD \
> + "ip xfrm state add" \
> + " src ::1 dst ::1" \
> + " proto esp spi 0x%08x" \
> + " encap espintcp %d %d ::" \
> + " aead 'rfc4106(gcm(aes))' %s 128" \
> + " mode transport"
> +
> +static const uint8_t aead_key[KEYTOTAL] = {
> + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
> + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
> + 0x01, 0x02, 0x03, 0x04
> +};
> +
> +static uint8_t original[DATA_SIZE];
> +static int file_fd = -1;
> +static int srv_fd = -1;
> +
> +static void setup(void)
> +{
> + char keyhex[KEYTOTAL * 2 + 3];
> + char cmd[512];
> + int i, ret;
> +
> + tst_setup_netns();
> + NETDEV_SET_STATE("lo", 1);
> +
> + keyhex[0] = '0';
> + keyhex[1] = 'x';
> + for (i = 0; i < KEYTOTAL; i++)
> + sprintf(keyhex + 2 + i * 2, "%02x", aead_key[i]);
> +
> + snprintf(cmd, sizeof(cmd), XFRM_CMD, SPI, TCP_PORT, TCP_PORT, keyhex);
> +
> + ret = tst_system(cmd);
> + if (ret)
> + tst_brk(TBROK, "Failed to install xfrm ESP-in-TCP state");
I do not like this part that much. We have tst_cmd() that is better than
tst_system() and also netlink library which could probably do the same.
Given that this is important we can go with this for now, but I would
like to see it fixed later.
> + for (i = 0; i < DATA_SIZE; i++)
> + original[i] = (uint8_t)(i & 0xff);
> +}
> +
> +static void try_corrupt(void)
> +{
> + struct sockaddr_in6 addr = {
> + .sin6_family = AF_INET6,
> + .sin6_addr = IN6ADDR_LOOPBACK_INIT,
> + .sin6_port = htons(TCP_PORT),
> + };
> + uint8_t prefix[PREFIX_SIZE];
> + uint16_t frame_len;
> + uint32_t spi_net, seq_net;
> + char ulp[] = "espintcp";
> + char drain;
> + int acc_fd;
> + loff_t off;
> + pid_t pid;
> + int status;
> +
> + frame_len = htons(PREFIX_SIZE + DATA_SIZE);
> + memcpy(prefix, &frame_len, 2);
> +
> + spi_net = htonl(SPI);
> + memcpy(prefix + 2, &spi_net, 4);
> +
> + seq_net = htonl(1);
> + memcpy(prefix + 6, &seq_net, 4);
> +
> + memset(prefix + 10, 0xcc, IV_LEN);
> +
> + srv_fd = SAFE_SOCKET(AF_INET6, SOCK_STREAM, 0);
> + SAFE_SETSOCKOPT_INT(srv_fd, SOL_SOCKET, SO_REUSEADDR, 1);
> + SAFE_BIND(srv_fd, (struct sockaddr *)&addr, sizeof(addr));
> + SAFE_LISTEN(srv_fd, 1);
> +
> + pid = SAFE_FORK();
> +
> + if (!pid) {
> + int cli_fd, pipefd[2];
> +
> + SAFE_CLOSE(srv_fd);
> +
> + cli_fd = SAFE_SOCKET(AF_INET6, SOCK_STREAM, 0);
> + SAFE_SETSOCKOPT_INT(cli_fd, IPPROTO_TCP, TCP_NODELAY, 1);
> + SAFE_CONNECT(cli_fd, (struct sockaddr *)&addr, sizeof(addr));
> +
> + SAFE_SEND(1, cli_fd, prefix, sizeof(prefix), 0);
> + SAFE_PIPE(pipefd);
> +
> + off = 0;
> + SAFE_SPLICE(file_fd, &off, pipefd[1], NULL, DATA_SIZE, 0);
> +
> + /*
> + * Splice pipe into TCP socket. The kernel uses
> + * MSG_SPLICE_PAGES to keep page cache references in
> + * the skb. On loopback the receiver's ESP handler may
> + * decrypt in-place, corrupting the page cache. May
> + * fail on patched kernels.
> + */
> + splice(pipefd[0], NULL, cli_fd, NULL, DATA_SIZE, 0);
> +
> + SAFE_CLOSE(pipefd[0]);
> + SAFE_CLOSE(pipefd[1]);
> + SAFE_CLOSE(cli_fd);
> +
> + TST_CHECKPOINT_WAKE(0);
> + exit(0);
> + }
> +
> + acc_fd = SAFE_ACCEPT(srv_fd, NULL, NULL);
> + SAFE_CLOSE(srv_fd);
> +
> + TST_CHECKPOINT_WAIT(0);
> +
> + SAFE_SETSOCKOPT(acc_fd, IPPROTO_TCP, TCP_ULP, ulp, sizeof(ulp));
> +
> + /* Kick the espintcp strparser to process buffered ESP data */
> + SAFE_RECV(0, acc_fd, &drain, 1, 0);
> +
> + SAFE_CLOSE(acc_fd);
> + SAFE_WAITPID(pid, &status, 0);
This WAITPID shouldn't be here, it will mask any errors from the SAFE_
macros in the child.
> +}
> +
> +static void run(void)
> +{
> + uint8_t readback[DATA_SIZE];
> +
> + file_fd = SAFE_OPEN(TESTFILE, O_WRONLY | O_CREAT, 0444);
> + SAFE_WRITE(SAFE_WRITE_ALL, file_fd, original, DATA_SIZE);
> + SAFE_CLOSE(file_fd);
> +
> + file_fd = SAFE_OPEN(TESTFILE, O_RDONLY);
> + try_corrupt();
> + 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, DATA_SIZE) != 0)
> + tst_res(TFAIL, "Page cache corrupted via xfrm ESP-in-TCP splice");
> + else
> + tst_res(TPASS, "Page cache was not corrupted");
> +
> + SAFE_UNLINK(TESTFILE);
Since we corrupted the page cache when the test fails maybe we should
drop it on failure here just in case?
I.e. write 1 to /proc/sys/vm/drop_caches?
> +}
> +
> +static void cleanup(void)
> +{
> + if (srv_fd != -1)
> + SAFE_CLOSE(srv_fd);
> +
> + if (file_fd != -1)
> + SAFE_CLOSE(file_fd);
> +}
> +
> +static struct tst_test test = {
> + .test_all = run,
> + .setup = setup,
> + .cleanup = cleanup,
> + .needs_tmpdir = 1,
> + .forks_child = 1,
> + .needs_checkpoints = 1,
> + .needs_kconfigs = (const char *[]) {
> + "CONFIG_USER_NS=y",
> + "CONFIG_NET_NS=y",
> + "CONFIG_XFRM",
> + "CONFIG_INET6_ESP",
> + "CONFIG_INET6_ESPINTCP",
> + "CONFIG_CRYPTO_GCM",
> + NULL
> + },
> + .save_restore = (const struct tst_path_val[]) {
> + {"/proc/sys/user/max_user_namespaces", "1024", TST_SR_SKIP},
> + {}
> + },
> + .needs_cmds = (struct tst_cmd[]) {
> + {.cmd = "ip"},
> + {}
> + },
> +};
>
> ---
> base-commit: e1fc50957c98ae4c27064756e063de0e7136cde3
> change-id: 20260513-fragnesia-9588d855becf
>
> Best regards,
> --
> Andrea Cervesato <andrea.cervesato@suse.com>
>
>
> --
> Mailing list info: https://lists.linux.it/listinfo/ltp
--
Cyril Hrubis
chrubis@suse.cz
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [LTP] sockets/xfrm02: Add ESP-in-TCP page cache corruption test
2026-05-13 14:36 [LTP] [PATCH] sockets/xfrm02: Add ESP-in-TCP page cache corruption test Andrea Cervesato
2026-05-13 15:00 ` Cyril Hrubis
@ 2026-05-13 16:27 ` linuxtestproject.agent
1 sibling, 0 replies; 3+ messages in thread
From: linuxtestproject.agent @ 2026-05-13 16:27 UTC (permalink / raw)
To: Andrea Cervesato; +Cc: ltp
Hi Andrea,
On 2026-05-13, Andrea Cervesato wrote:
> sockets/xfrm02: Add ESP-in-TCP page cache corruption test
> + acc_fd = SAFE_ACCEPT(srv_fd, NULL, NULL);
> + SAFE_CLOSE(srv_fd);
srv_fd is not reset to -1 after this close. cleanup() checks
`srv_fd != -1` and will double-close the stale descriptor. Add
`srv_fd = -1;` here.
> + file_fd = SAFE_OPEN(TESTFILE, O_WRONLY | O_CREAT, 0444);
> + SAFE_WRITE(SAFE_WRITE_ALL, file_fd, original, DATA_SIZE);
> + SAFE_CLOSE(file_fd);
[...]
> + SAFE_CLOSE(file_fd);
[...]
> + SAFE_CLOSE(file_fd);
Same problem for file_fd: none of the three SAFE_CLOSE calls in run()
reset file_fd to -1, so cleanup() will double-close it. Add
`file_fd = -1;` after each one.
> + splice(pipefd[0], NULL, cli_fd, NULL, DATA_SIZE, 0);
SAFE_SPLICE exists. If failure on patched kernels must be tolerated,
capture the return in a local variable and add a comment explaining
why the error is intentionally ignored.
---
Note:
Our agent completed the review of the patch. The full review can be
found at: https://patchwork.ozlabs.org/project/ltp/list/?series=504173
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-05-13 16:28 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-13 14:36 [LTP] [PATCH] sockets/xfrm02: Add ESP-in-TCP page cache corruption test Andrea Cervesato
2026-05-13 15:00 ` Cyril Hrubis
2026-05-13 16:27 ` [LTP] " linuxtestproject.agent
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox