| Message ID | 20260518-fragnesia-variant-v1-1-45b9df38cc78@suse.com |
|---|---|
| State | Accepted |
| Headers | show |
| Series | sockets/xfrm03: Add skb_segment SKBFL_SHARED_FRAG stripping test | expand |
| Context | Check | Description |
|---|---|---|
| ltpci/copilot-review | warning | Needs revision |
| ltpci/github-build-doc | success | success |
| ltpci/github-build-debian_stable_aarch64-linux-gnu-gcc_arm64 | success | success |
| ltpci/github-build-debian_stable_s390x-linux-gnu-gcc_s390x | success | success |
| ltpci/github-build-debian_stable_powerpc64le-linux-gnu-gcc_ppc64el | success | success |
| ltpci/github-build-quay-io-centos-centos_stream9_gcc | success | success |
| ltpci/github-build-ubuntu_jammy_gcc | success | success |
| ltpci/github-build-ubuntu_noble_gcc | success | success |
| ltpci/github-build-alpine_latest_gcc | success | success |
| ltpci/github-build-debian_testing_clang | success | success |
| ltpci/github-build-debian_testing_gcc | success | success |
| ltpci/github-build-fedora_latest_clang | success | success |
| ltpci/github-build-opensuse-archive_42-2_gcc | success | success |
| ltpci/github-build-debian_oldstable_clang | success | success |
| ltpci/github-build-debian_stable_gcc | success | success |
| ltpci/github-build-opensuse-leap_latest_gcc | success | success |
| ltpci/github-build-debian_stable_gcc | success | success |
| ltpci/github-build-debian_oldstable_gcc | success | success |
Hi Andrea, On Mon, 18 May 2026 09:27:53 +0200, Andrea Cervesato wrote: > sockets/xfrm03: Add skb_segment SKBFL_SHARED_FRAG stripping test > + /* Let the espintcp strparser process buffered ESP data */ > + usleep(30000); Sleep-based synchronisation is not allowed (G2). Replace with a polling loop on the accepted socket or TST_CHECKPOINT_WAIT between the child sender and the parent receiver. [...] > + /* > + * Splice pipe into TCP socket. On the forwarding > + * path, skb_segment() may strip SKBFL_SHARED_FRAG, > + * allowing in-place ESP decrypt on page cache pages. > + * May fail on patched kernels. > + */ > + splice(pipefd[0], NULL, cli_fd, NULL, DATA_SIZE, 0); SAFE_SPLICE is available in lapi/splice.h and is already used earlier in this file. Since failure is intentionally ignored on patched kernels, wrap with TEST() so the result is recorded without aborting and the SAFE_* rule is satisfied. --- Note: Our agent completed the review of the patch. 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
Hi, one minor suggestion below, otherwise: Reviewed-by: Martin Doucha <mdoucha@suse.cz> I've tried to simplify the namespace setup but I haven't found a usable kernel build where I could verify that the reproducer still fails even though esp6 variant of Fragnesia is already fixed. I can only find kernels where both CVE variants are fixed or present. We should revisit this after Labs conf. On 5/18/26 09:27, Andrea Cervesato wrote: > From: Andrea Cervesato <andrea.cervesato@suse.com> > > Verify that skb_segment() does not strip the SKBFL_SHARED_FRAG flag > from page-cache fragments when splitting GRO-coalesced packets, > allowing ESP-in-TCP to decrypt in-place and corrupt read-only file > data. > > The test creates a three-namespace topology (sender - middle - > receiver) connected via veth pairs, disables GSO/TSO/GRO on the > middle-to-receiver link to force skb_segment(), installs an > ESP-in-TCP xfrm SA in the receiver, splices known file data through > the forwarding path, and verifies the page cache was not corrupted. > > Reproducer based on: > https://github.com/v12-security/pocs/tree/main/fragnesia-5db89c99566fc > > Signed-off-by: Andrea Cervesato <andrea.cervesato@suse.com> > --- > runtest/cve | 1 + > testcases/network/sockets/.gitignore | 1 + > testcases/network/sockets/xfrm03.c | 325 +++++++++++++++++++++++++++++++++++ > 3 files changed, 327 insertions(+) > > diff --git a/runtest/cve b/runtest/cve > index 74ee8e9ba4287a99dbf0412921acb11a5be53283..a5952b56c48771def1032efdf0cbe3847f7dc23c 100644 > --- a/runtest/cve > +++ b/runtest/cve > @@ -96,3 +96,4 @@ cve-2025-21756 cve-2025-21756 > cve-2026-31431 af_alg08 > cve-2026-43284 xfrm01 > cve-2026-46300 xfrm02 > +cve-2026-46300-skb-segment xfrm03 > diff --git a/testcases/network/sockets/.gitignore b/testcases/network/sockets/.gitignore > index 35bc0462b676b041d9a5b52a37fded973d0157a9..3e8962bbf34c8574460c032cf9eb3b38a55eb697 100644 > --- a/testcases/network/sockets/.gitignore > +++ b/testcases/network/sockets/.gitignore > @@ -1,2 +1,3 @@ > /xfrm01 > /xfrm02 > +/xfrm03 > diff --git a/testcases/network/sockets/xfrm03.c b/testcases/network/sockets/xfrm03.c > new file mode 100644 > index 0000000000000000000000000000000000000000..de6042e8ad8a806dcdaa152785511731a02cbeb0 > --- /dev/null > +++ b/testcases/network/sockets/xfrm03.c > @@ -0,0 +1,325 @@ > +// SPDX-License-Identifier: GPL-2.0-or-later > +/* > + * Copyright (C) 2026 SUSE LLC Andrea Cervesato <andrea.cervesato@suse.com> > + */ > + > +/*\ > + * Verify that skb_segment() does not strip the SKBFL_SHARED_FRAG flag > + * from page-cache fragments when splitting GRO-coalesced packets, > + * causing ESP-in-TCP to decrypt in-place and corrupt read-only file > + * data. > + * > + * When file data is spliced into a TCP socket, the kernel references > + * page-cache pages directly in the skb and marks them with > + * SKBFL_SHARED_FRAG. If the packet traverses a forwarding path where > + * GRO coalesces segments on ingress and skb_segment() splits them on > + * egress (because GSO is disabled), skb_segment() incorrectly strips > + * SKBFL_SHARED_FRAG from the child segments. When the receiver has > + * TCP_ULP "espintcp" enabled, the ESP handler decrypts in-place on > + * page-cache pages, corrupting the cached file contents. > + * > + * The test creates three network namespaces connected via veth pairs > + * (sender - middle - receiver), disables GSO/TSO/GRO on the > + * middle-to-receiver link to force skb_segment(), installs an > + * ESP-in-TCP xfrm SA in the receiver, writes known data to a > + * read-only file, splices it into a TCP socket from the sender, > + * enables espintcp ULP on the receiver side, and verifies the page > + * cache was not corrupted. > + * > + * Reproducer based on: > + * https://github.com/v12-security/pocs/tree/main/fragnesia-5db89c99566fc > + */ > + > +#define _GNU_SOURCE > + > +#include <linux/ethtool.h> > +#include <linux/sockios.h> > +#include <net/if.h> > + > +#include "tst_test.h" > +#include "tst_net.h" > +#include "tst_netdevice.h" > +#include "lapi/tcp.h" > +#include "lapi/splice.h" > +#include "lapi/sched.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) > +#define PREFIX_SIZE (2 + ESP_HDR_SIZE) > +#define NETMASK 24 > + > +#define SENDER_ADDR 0x0a000101 /* 10.0.1.1 */ > +#define MIDDLE_ADDR1 0x0a000102 /* 10.0.1.2 */ > +#define MIDDLE_ADDR2 0x0a000201 /* 10.0.2.1 */ > +#define RECEIVER_ADDR 0x0a000202 /* 10.0.2.2 */ > + > +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 int middlens = -1; > +static int senderns = -1; > +static int receiverns = -1; > + > +static void disable_offloads(const char *ifname) > +{ > + struct ifreq ifr; > + struct ethtool_value val = { .data = 0 }; > + int fd; > + > + fd = SAFE_SOCKET(AF_INET, SOCK_DGRAM, 0); > + > + memset(&ifr, 0, sizeof(ifr)); > + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); > + ifr.ifr_data = (void *)&val; > + > + val.cmd = ETHTOOL_SGSO; > + SAFE_IOCTL(fd, SIOCETHTOOL, &ifr); > + > + val.cmd = ETHTOOL_STSO; > + SAFE_IOCTL(fd, SIOCETHTOOL, &ifr); > + > + val.cmd = ETHTOOL_SGRO; > + SAFE_IOCTL(fd, SIOCETHTOOL, &ifr); > + > + SAFE_CLOSE(fd); > +} > + > +static void setup(void) > +{ > + char keyhex[KEYTOTAL * 2 + 3]; > + char spihex[16]; > + char port_str[8]; > + int i, ret; > + > + tst_setup_netns(); > + NETDEV_SET_STATE("lo", 1); > + > + CREATE_VETH_PAIR("veth_m1", "veth_s"); > + CREATE_VETH_PAIR("veth_m2", "veth_r"); > + > + NETDEV_ADD_ADDRESS_INET("veth_m1", htonl(MIDDLE_ADDR1), NETMASK, 0); > + NETDEV_ADD_ADDRESS_INET("veth_m2", htonl(MIDDLE_ADDR2), NETMASK, 0); > + NETDEV_SET_STATE("veth_m1", 1); > + NETDEV_SET_STATE("veth_m2", 1); > + > + SAFE_FILE_PRINTF("/proc/sys/net/ipv4/ip_forward", "1"); > + disable_offloads("veth_m2"); > + > + middlens = SAFE_OPEN("/proc/self/ns/net", O_RDONLY); > + > + SAFE_UNSHARE(CLONE_NEWNET); > + senderns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY); > + SAFE_SETNS(middlens, CLONE_NEWNET); > + NETDEV_CHANGE_NS_FD("veth_s", senderns); > + > + SAFE_SETNS(senderns, CLONE_NEWNET); > + NETDEV_SET_STATE("lo", 1); > + NETDEV_ADD_ADDRESS_INET("veth_s", htonl(SENDER_ADDR), NETMASK, 0); > + NETDEV_SET_STATE("veth_s", 1); > + NETDEV_ADD_ROUTE_INET("veth_s", 0, 0, 0, 0, htonl(MIDDLE_ADDR1)); > + SAFE_SETNS(middlens, CLONE_NEWNET); > + > + SAFE_UNSHARE(CLONE_NEWNET); > + receiverns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY); > + SAFE_SETNS(middlens, CLONE_NEWNET); > + NETDEV_CHANGE_NS_FD("veth_r", receiverns); > + > + SAFE_SETNS(receiverns, CLONE_NEWNET); > + NETDEV_SET_STATE("lo", 1); > + NETDEV_ADD_ADDRESS_INET("veth_r", htonl(RECEIVER_ADDR), NETMASK, 0); > + NETDEV_SET_STATE("veth_r", 1); > + NETDEV_ADD_ROUTE_INET("veth_r", 0, 0, 0, 0, htonl(MIDDLE_ADDR2)); > + > + keyhex[0] = '0'; > + keyhex[1] = 'x'; > + for (i = 0; i < KEYTOTAL; i++) > + sprintf(keyhex + 2 + i * 2, "%02x", aead_key[i]); > + > + snprintf(spihex, sizeof(spihex), "0x%08x", SPI); > + snprintf(port_str, sizeof(port_str), "%d", TCP_PORT); > + > + const char *const xfrm_cmd[] = { > + "ip", "xfrm", "state", "add", > + "src", "10.0.2.2", "dst", "10.0.2.2", > + "proto", "esp", "spi", spihex, > + "encap", "espintcp", port_str, port_str, "0.0.0.0", > + "aead", "rfc4106(gcm(aes))", keyhex, "128", > + "mode", "transport", > + NULL > + }; > + > + ret = tst_cmd(xfrm_cmd, NULL, NULL, TST_CMD_PASS_RETVAL); > + if (ret) > + tst_brk(TBROK, "Failed to install xfrm ESP-in-TCP state"); Please change the TBROK to TCONF. Older kernels don't support espintcp even though they have the necessary kernel modules. We should revisit this command later and convert it to direct rtnetlink request in order to correctly handle other errors than missing protocol support. > + > + SAFE_SETNS(middlens, CLONE_NEWNET); > + > + for (i = 0; i < DATA_SIZE; i++) > + original[i] = (uint8_t)(i & 0xff); > +} > + > +static void try_corrupt(void) > +{ > + struct sockaddr_in addr = { > + .sin_family = AF_INET, > + .sin_addr.s_addr = htonl(RECEIVER_ADDR), > + .sin_port = htons(TCP_PORT), > + }; > + uint8_t prefix[PREFIX_SIZE]; > + uint16_t frame_len; > + uint32_t spi_net, seq_net; > + char ulp[] = "espintcp"; > + int acc_fd; > + loff_t off; > + > + 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); > + > + SAFE_SETNS(receiverns, CLONE_NEWNET); > + > + srv_fd = SAFE_SOCKET(AF_INET, 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); > + > + if (!SAFE_FORK()) { > + int cli_fd, pipefd[2]; > + > + SAFE_CLOSE(srv_fd); > + SAFE_SETNS(senderns, CLONE_NEWNET); > + > + cli_fd = SAFE_SOCKET(AF_INET, 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); > + > + SAFE_POSIX_FADVISE(file_fd, 0, 0, POSIX_FADV_DONTNEED); > + > + off = 0; > + SAFE_SPLICE(file_fd, &off, pipefd[1], NULL, DATA_SIZE, 0); > + > + /* > + * Splice pipe into TCP socket. On the forwarding > + * path, skb_segment() may strip SKBFL_SHARED_FRAG, > + * allowing in-place ESP decrypt on page cache pages. > + * 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); > + > + exit(0); > + } > + > + acc_fd = SAFE_ACCEPT(srv_fd, NULL, NULL); > + SAFE_CLOSE(srv_fd); > + > + tst_reap_children(); > + > + SAFE_SETSOCKOPT(acc_fd, IPPROTO_TCP, TCP_ULP, ulp, sizeof(ulp)); > + > + /* Let the espintcp strparser process buffered ESP data */ > + usleep(30000); > + > + SAFE_CLOSE(acc_fd); > + > + SAFE_SETNS(middlens, CLONE_NEWNET); > +} > + > +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 skb_segment ESP-in-TCP forwarding"); > + 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); > + > + if (receiverns != -1) > + SAFE_CLOSE(receiverns); > + > + if (senderns != -1) > + SAFE_CLOSE(senderns); > + > + if (middlens != -1) > + SAFE_CLOSE(middlens); > +} > + > +static struct tst_test test = { > + .test_all = run, > + .setup = setup, > + .cleanup = cleanup, > + .needs_tmpdir = 1, > + .forks_child = 1, > + .needs_kconfigs = (const char *[]) { > + "CONFIG_USER_NS=y", > + "CONFIG_NET_NS=y", > + "CONFIG_VETH", > + "CONFIG_XFRM", > + "CONFIG_INET_ESP", > + "CONFIG_INET_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"}, > + {} > + }, > + .tags = (const struct tst_tag[]) { > + {"CVE", "2026-46300"}, > + {} > + }, > +}; > > --- > base-commit: 8cd7644a52e348393d453d0ec5bf9424ea31a4cc > change-id: 20260517-fragnesia-variant-8d026f8c0730 > > Best regards,
Hi Martin, > I've tried to simplify the namespace setup but I haven't found a usable > kernel build where I could verify that the reproducer still fails even > though esp6 variant of Fragnesia is already fixed. I can only find > kernels where both CVE variants are fixed or present. We should revisit > this after Labs conf. yes, I was able to reproduce this bug on tumbleweed only. Let's talk about the Labs conference. Regards, -- Andrea Cervesato SUSE QE Automation Engineer Linux andrea.cervesato@suse.com
Hi! > > + ret = tst_cmd(xfrm_cmd, NULL, NULL, TST_CMD_PASS_RETVAL); > > + if (ret) > > + tst_brk(TBROK, "Failed to install xfrm ESP-in-TCP state"); > > Please change the TBROK to TCONF. Older kernels don't support espintcp > even though they have the necessary kernel modules. We should revisit > this command later and convert it to direct rtnetlink request in order > to correctly handle other errors than missing protocol support. I've fixed this and pushed the patch, thanks.
diff --git a/runtest/cve b/runtest/cve index 74ee8e9ba4287a99dbf0412921acb11a5be53283..a5952b56c48771def1032efdf0cbe3847f7dc23c 100644 --- a/runtest/cve +++ b/runtest/cve @@ -96,3 +96,4 @@ cve-2025-21756 cve-2025-21756 cve-2026-31431 af_alg08 cve-2026-43284 xfrm01 cve-2026-46300 xfrm02 +cve-2026-46300-skb-segment xfrm03 diff --git a/testcases/network/sockets/.gitignore b/testcases/network/sockets/.gitignore index 35bc0462b676b041d9a5b52a37fded973d0157a9..3e8962bbf34c8574460c032cf9eb3b38a55eb697 100644 --- a/testcases/network/sockets/.gitignore +++ b/testcases/network/sockets/.gitignore @@ -1,2 +1,3 @@ /xfrm01 /xfrm02 +/xfrm03 diff --git a/testcases/network/sockets/xfrm03.c b/testcases/network/sockets/xfrm03.c new file mode 100644 index 0000000000000000000000000000000000000000..de6042e8ad8a806dcdaa152785511731a02cbeb0 --- /dev/null +++ b/testcases/network/sockets/xfrm03.c @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2026 SUSE LLC Andrea Cervesato <andrea.cervesato@suse.com> + */ + +/*\ + * Verify that skb_segment() does not strip the SKBFL_SHARED_FRAG flag + * from page-cache fragments when splitting GRO-coalesced packets, + * causing ESP-in-TCP to decrypt in-place and corrupt read-only file + * data. + * + * When file data is spliced into a TCP socket, the kernel references + * page-cache pages directly in the skb and marks them with + * SKBFL_SHARED_FRAG. If the packet traverses a forwarding path where + * GRO coalesces segments on ingress and skb_segment() splits them on + * egress (because GSO is disabled), skb_segment() incorrectly strips + * SKBFL_SHARED_FRAG from the child segments. When the receiver has + * TCP_ULP "espintcp" enabled, the ESP handler decrypts in-place on + * page-cache pages, corrupting the cached file contents. + * + * The test creates three network namespaces connected via veth pairs + * (sender - middle - receiver), disables GSO/TSO/GRO on the + * middle-to-receiver link to force skb_segment(), installs an + * ESP-in-TCP xfrm SA in the receiver, writes known data to a + * read-only file, splices it into a TCP socket from the sender, + * enables espintcp ULP on the receiver side, and verifies the page + * cache was not corrupted. + * + * Reproducer based on: + * https://github.com/v12-security/pocs/tree/main/fragnesia-5db89c99566fc + */ + +#define _GNU_SOURCE + +#include <linux/ethtool.h> +#include <linux/sockios.h> +#include <net/if.h> + +#include "tst_test.h" +#include "tst_net.h" +#include "tst_netdevice.h" +#include "lapi/tcp.h" +#include "lapi/splice.h" +#include "lapi/sched.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) +#define PREFIX_SIZE (2 + ESP_HDR_SIZE) +#define NETMASK 24 + +#define SENDER_ADDR 0x0a000101 /* 10.0.1.1 */ +#define MIDDLE_ADDR1 0x0a000102 /* 10.0.1.2 */ +#define MIDDLE_ADDR2 0x0a000201 /* 10.0.2.1 */ +#define RECEIVER_ADDR 0x0a000202 /* 10.0.2.2 */ + +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 int middlens = -1; +static int senderns = -1; +static int receiverns = -1; + +static void disable_offloads(const char *ifname) +{ + struct ifreq ifr; + struct ethtool_value val = { .data = 0 }; + int fd; + + fd = SAFE_SOCKET(AF_INET, SOCK_DGRAM, 0); + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); + ifr.ifr_data = (void *)&val; + + val.cmd = ETHTOOL_SGSO; + SAFE_IOCTL(fd, SIOCETHTOOL, &ifr); + + val.cmd = ETHTOOL_STSO; + SAFE_IOCTL(fd, SIOCETHTOOL, &ifr); + + val.cmd = ETHTOOL_SGRO; + SAFE_IOCTL(fd, SIOCETHTOOL, &ifr); + + SAFE_CLOSE(fd); +} + +static void setup(void) +{ + char keyhex[KEYTOTAL * 2 + 3]; + char spihex[16]; + char port_str[8]; + int i, ret; + + tst_setup_netns(); + NETDEV_SET_STATE("lo", 1); + + CREATE_VETH_PAIR("veth_m1", "veth_s"); + CREATE_VETH_PAIR("veth_m2", "veth_r"); + + NETDEV_ADD_ADDRESS_INET("veth_m1", htonl(MIDDLE_ADDR1), NETMASK, 0); + NETDEV_ADD_ADDRESS_INET("veth_m2", htonl(MIDDLE_ADDR2), NETMASK, 0); + NETDEV_SET_STATE("veth_m1", 1); + NETDEV_SET_STATE("veth_m2", 1); + + SAFE_FILE_PRINTF("/proc/sys/net/ipv4/ip_forward", "1"); + disable_offloads("veth_m2"); + + middlens = SAFE_OPEN("/proc/self/ns/net", O_RDONLY); + + SAFE_UNSHARE(CLONE_NEWNET); + senderns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY); + SAFE_SETNS(middlens, CLONE_NEWNET); + NETDEV_CHANGE_NS_FD("veth_s", senderns); + + SAFE_SETNS(senderns, CLONE_NEWNET); + NETDEV_SET_STATE("lo", 1); + NETDEV_ADD_ADDRESS_INET("veth_s", htonl(SENDER_ADDR), NETMASK, 0); + NETDEV_SET_STATE("veth_s", 1); + NETDEV_ADD_ROUTE_INET("veth_s", 0, 0, 0, 0, htonl(MIDDLE_ADDR1)); + SAFE_SETNS(middlens, CLONE_NEWNET); + + SAFE_UNSHARE(CLONE_NEWNET); + receiverns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY); + SAFE_SETNS(middlens, CLONE_NEWNET); + NETDEV_CHANGE_NS_FD("veth_r", receiverns); + + SAFE_SETNS(receiverns, CLONE_NEWNET); + NETDEV_SET_STATE("lo", 1); + NETDEV_ADD_ADDRESS_INET("veth_r", htonl(RECEIVER_ADDR), NETMASK, 0); + NETDEV_SET_STATE("veth_r", 1); + NETDEV_ADD_ROUTE_INET("veth_r", 0, 0, 0, 0, htonl(MIDDLE_ADDR2)); + + keyhex[0] = '0'; + keyhex[1] = 'x'; + for (i = 0; i < KEYTOTAL; i++) + sprintf(keyhex + 2 + i * 2, "%02x", aead_key[i]); + + snprintf(spihex, sizeof(spihex), "0x%08x", SPI); + snprintf(port_str, sizeof(port_str), "%d", TCP_PORT); + + const char *const xfrm_cmd[] = { + "ip", "xfrm", "state", "add", + "src", "10.0.2.2", "dst", "10.0.2.2", + "proto", "esp", "spi", spihex, + "encap", "espintcp", port_str, port_str, "0.0.0.0", + "aead", "rfc4106(gcm(aes))", keyhex, "128", + "mode", "transport", + NULL + }; + + ret = tst_cmd(xfrm_cmd, NULL, NULL, TST_CMD_PASS_RETVAL); + if (ret) + tst_brk(TBROK, "Failed to install xfrm ESP-in-TCP state"); + + SAFE_SETNS(middlens, CLONE_NEWNET); + + for (i = 0; i < DATA_SIZE; i++) + original[i] = (uint8_t)(i & 0xff); +} + +static void try_corrupt(void) +{ + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(RECEIVER_ADDR), + .sin_port = htons(TCP_PORT), + }; + uint8_t prefix[PREFIX_SIZE]; + uint16_t frame_len; + uint32_t spi_net, seq_net; + char ulp[] = "espintcp"; + int acc_fd; + loff_t off; + + 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); + + SAFE_SETNS(receiverns, CLONE_NEWNET); + + srv_fd = SAFE_SOCKET(AF_INET, 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); + + if (!SAFE_FORK()) { + int cli_fd, pipefd[2]; + + SAFE_CLOSE(srv_fd); + SAFE_SETNS(senderns, CLONE_NEWNET); + + cli_fd = SAFE_SOCKET(AF_INET, 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); + + SAFE_POSIX_FADVISE(file_fd, 0, 0, POSIX_FADV_DONTNEED); + + off = 0; + SAFE_SPLICE(file_fd, &off, pipefd[1], NULL, DATA_SIZE, 0); + + /* + * Splice pipe into TCP socket. On the forwarding + * path, skb_segment() may strip SKBFL_SHARED_FRAG, + * allowing in-place ESP decrypt on page cache pages. + * 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); + + exit(0); + } + + acc_fd = SAFE_ACCEPT(srv_fd, NULL, NULL); + SAFE_CLOSE(srv_fd); + + tst_reap_children(); + + SAFE_SETSOCKOPT(acc_fd, IPPROTO_TCP, TCP_ULP, ulp, sizeof(ulp)); + + /* Let the espintcp strparser process buffered ESP data */ + usleep(30000); + + SAFE_CLOSE(acc_fd); + + SAFE_SETNS(middlens, CLONE_NEWNET); +} + +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 skb_segment ESP-in-TCP forwarding"); + 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); + + if (receiverns != -1) + SAFE_CLOSE(receiverns); + + if (senderns != -1) + SAFE_CLOSE(senderns); + + if (middlens != -1) + SAFE_CLOSE(middlens); +} + +static struct tst_test test = { + .test_all = run, + .setup = setup, + .cleanup = cleanup, + .needs_tmpdir = 1, + .forks_child = 1, + .needs_kconfigs = (const char *[]) { + "CONFIG_USER_NS=y", + "CONFIG_NET_NS=y", + "CONFIG_VETH", + "CONFIG_XFRM", + "CONFIG_INET_ESP", + "CONFIG_INET_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"}, + {} + }, + .tags = (const struct tst_tag[]) { + {"CVE", "2026-46300"}, + {} + }, +};