//nefariousplan

CVE-2026-46300: Two Fragnesia PoCs Stop At The Line The Real Exploit Calls verify_write_denied

pattern

cve

proof of concept

Fragnesia is CVE-2026-46300, William Bowling's universal Linux local privilege escalation in the XFRM ESP-in-TCP subsystem, disclosed on the oss-security list on 13 May 2026. The real exploit is 1,302 lines of C. It enters a user namespace, installs a transport-mode ESP-in-TCP security association under a known AES-128-GCM key, then splice-races bytes from the page cache of /usr/bin/su through the espintcp ULP and XORs the GCM keystream into the cached file page. Three repositories on GitHub today carry the string CVE-2026-46300 in their name. One is a faithful mirror of the V12 Security PoC. The other two are 117 lines of Python that call unshare(CLONE_NEWUSER), map uid 0 to the outer user, and open a shell with a renamed prompt. The real exploit calls a function named verify_write_denied immediately after that same setup, to assert the state is not yet root. The two placeholders ship the part before that line.

The repository names contain CVE-2026-46300. The files do not contain Fragnesia.

A GitHub search for repository names containing CVE-2026-46300, as of 16 May 2026, returns three results.

v12-security/pocs/tree/main/fragnesia is not one of them, because the repository name is pocs, not the CVE. It contains the working exploit, fragnesia.c, 1,302 lines, attributed in its header comment to William Bowling on the V12 team. The header points at the netdev patch on lists.openwall.net/netdev/2026/05/13/79. The file builds with gcc -O2 -Wall -Wextra -static, runs as an unprivileged user (modulo AppArmor on Ubuntu), and on success drops to a root shell by overwriting the first 192 bytes of /usr/bin/su in the page cache and execve-ing it.

HORKimhab/CVE-2026-46300 (one star, two commits on 14 May 2026 from [email protected]) is a faithful mirror of the V12 PoC under a disclaimer-wrapped README. The repository's pocs/fragnesia/fragnesia.c is byte-for-byte identical to the V12 file, and the README's final section reads:

## Credit or reference

- pocs: https://github.com/v12-security/pocs

This is a mirror, not a placeholder. It is in the scanner's result set because its name matches; the bytes inside it are the real exploit.

The other two results are the placeholders.

Sentebale/CVE-2026-46300. One star. First commit 2026-05-14 15:31:14 +0800 from [email protected]. Nine commits in twenty-three minutes the same day, all from the same author. The repository contains one Python file, CVE-2026-46300.py, 117 lines, and a README whose body is a Kylin Linux uname -a line and a transcript:

python3 CVE-2026-46300.py

ns> whoami

root

ns>

0xBlackash/CVE-2026-46300. Zero stars. Three commits on 15 May 2026 from [email protected]. The repository contains CVE-2026-46300.py (117 lines), CVE-2026-46300.sh (48 lines), and a README of shields and emoji headers reading Severity: High, Linux Kernel, Attack: Local Privilege Escalation, with the section Proof of Concept linking to v12-security/pocs/tree/main/fragnesia.

The two CVE-2026-46300.py files are byte-for-byte identical except for one line:

-            line = input("ns> ")
+            line = input("PWNED> ")

That is the diff. Sentebale's prompt is ns>. 0xBlackash's prompt is PWNED>. The timestamps support a copy direction: Sentebale's first commit is 25 hours before 0xBlackash's first commit, and the two author identities differ. The bytes do not.

0xBlackash adds a companion bash script, CVE-2026-46300.sh, whose mechanical core is:

unshare_cmd="unshare --user --map-root-user --net"

$unshare_cmd bash -c '
    ip link set lo up 2>/dev/null || true
    ip addr add 127.0.0.1/8 dev lo 2>/dev/null || true
    echo "[+] Network namespace ready"
    PS1="(Fragnesia) \u@\h:\w# " bash
'

The artifact this script produces is a bash shell whose prompt reads (Fragnesia) root@host:/path#. The user-facing tell, the only thing the script does that distinguishes it from unshare --user --map-root-user --net bash, is the PS1.

What whoami actually returns

Both placeholder Python scripts perform the same operation. The parent forks. The child calls unshare(CLONE_NEWUSER), signals the parent, and waits. The parent writes the child's uid mapping:

def parent_maps(pid, uid, gid):
    p = f"/proc/{pid}"
    write_proc(f"{p}/uid_map", f"0 {uid} 1\n")
    write_proc(f"{p}/setgroups", "deny\n")
    write_proc(f"{p}/gid_map", f"0 {gid} 1\n")

uid is the outer user's real uid. The mapping 0 <outer_uid> 1 declares: inside this namespace, the single mapped identity is uid 0, which corresponds to the outer user's uid in the host. The child then calls unshare(CLONE_NEWNET), runs ip link set lo up and ip addr add 127.0.0.1/8 dev lo via subprocess.run, and drops into a REPL that pipes each input line to subprocess.run(cmd, shell=True, executable="/bin/sh").

The user runs python3 CVE-2026-46300.py. The user sees ns> (or PWNED>). The user types whoami. /usr/bin/whoami calls getuid(), finds 0 in the calling process's user namespace, looks up uid 0 in /etc/passwd, and prints the matching name. The name is root. The Sentebale README presents that transcript as the exploit working.

The user is not root. The user is uid 0 inside a user namespace whose only mapped identity is their own outer uid. The kernel's filesystem checks against the host filesystem still see the outer uid. Any operation that requires real privilege, opening /etc/shadow, writing to /usr/bin/su, modifying another user's files, returns EACCES. The string root is a name lookup. It is not a capability state.

The real exploit's source code disproves the placeholders' premise in writing

The real fragnesia.c performs the same namespace dance the placeholders perform, and then performs three more steps the placeholders do not. The author of fragnesia.c knew which of those steps was the bug, and the source code says so.

Inside main:

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

printf("[*] target=%s size=%llu\n", target_file, 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);

verify_write_denied is defined earlier in the file:

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

The function tries to open /usr/bin/su for writing. If the open succeeds, the exploit prints namespace_gate_failed and exits with code 4. If the open fails, the exploit prints <label>_write_open_denied=1 and continues.

The first call, verify_write_denied("outer"), runs before any namespacing. It asserts that the outer user, with no privilege escalation, cannot write to /usr/bin/su. The second call, verify_write_denied("userns_root_mapped_to_outer_user"), runs after setup_user_netns_xfrm() has done the userns enter, the uid_map write, the CLONE_NEWNET unshare, the loopback bring-up, and the XFRM SA install. That call has a label. The label is userns_root_mapped_to_outer_user. It exists in the source code as the file's own description of the state the placeholders deliver.

If verify_write_denied("userns_root_mapped_to_outer_user") ever succeeded, the exploit would treat that as a gate failure, not a success. The real exploit requires the namespace state to be non-privileged. The page-cache primitive that follows, replace_existing_bytes_after, works whether or not the namespace gives you uid 0; the namespace is the cheapest way to obtain CAP_NET_ADMIN for installing the XFRM SA, and nothing more. The two verify_write_denied calls bracket the namespace setup and document, in code that runs every time the exploit runs, that namespacing has not yet conferred the ability to modify /usr/bin/su. The next line, replace_existing_bytes_after, is where Fragnesia lives.

The placeholders ship through the setup_user_netns_xfrm step, in part, and exit before the second verify_write_denied would be called. They deliver the state the exploit's own source code stamps _write_open_denied=1.

The placeholders do not ship the whole prologue either

What the placeholders perform is also not the entire namespace setup. The real setup_user_netns_xfrm executes, in order:

  1. prctl(PR_SET_DUMPABLE, 1, 0, 0, 0).
  2. enter_mapped_userns(): fork, unshare(CLONE_NEWUSER) in the child, parent writes uid_map / setgroups / gid_map, child setresuid(0, 0, 0) / setresgid(0, 0, 0).
  3. unshare(CLONE_NEWNET).
  4. bring_loopback_up(): SIOCSIFFLAGS with IFF_UP on lo.
  5. add_xfrm_espintcp_state(): open AF_NETLINK / NETLINK_XFRM, build an XFRM_MSG_NEWSA message for a transport-mode IPPROTO_ESP SA between ::1 and ::1 with an rfc4106(gcm(aes)) AEAD attribute carrying a 20-byte key and an XFRMA_ENCAP attribute setting encap_type = TCP_ENCAP_ESPINTCP, send.

The placeholders implement steps 2 and 3, plus a subprocess.run(["ip", "link", "set", "lo", "up"]) and subprocess.run(["ip", "addr", "add", "127.0.0.1/8", "dev", "lo"]) approximation of step 4. Step 5, the XFRM SA install that is necessary to even attempt the bug, is absent. Neither placeholder file contains the string XFRM. Neither contains ESP. Neither contains espintcp, splice, AEAD, gcm, or netlink. The 20-byte AES key the real exploit hard-codes at xfrm_aead_key[] is not in the placeholders, because the placeholders do no cryptography. The 256-entry keystream lookup table the real exploit builds via AF_ALG is not in the placeholders, because the placeholders do not flip bytes.

If the bug class CVE-2026-46300 names were "an unprivileged user can unshare(CLONE_NEWUSER)," the kernel would not need a patch. The bug class is "splice a file's pages into a TCP socket, install the espintcp ULP after the data is in the receive queue, and have the kernel XOR an attacker-chosen AES-GCM keystream byte into the page-cache page backing the spliced data." The placeholders ship the part the real exploit's source labels harmless, and they ship less of it than the real exploit ships before the label.

Two days, three CVE-shaped wrappers

This is the third post in two days about repositories that bear a CVE number on the badge without bearing the primitive. The shapes vary.

  • CVE-2026-20182: the artifact is a README that links to a SatoshiDisk listing selling 46.77 KB of Python for $660 in Bitcoin. No exploit code in the repository at all. The seller monetises the CISA KEV deadline directly.
  • CVE-2026-22553: the artifact is a Python script that POSTs to /MMadmServ with the parameter command, above which the author's own comment reads # placeholder field. The badge bears the publisher's name; the actual discoverer's name appears nowhere in the repository.
  • CVE-2026-46300 (this post): two artifacts that ship through part of the namespace prologue, omit the XFRM SA install and everything after it, and present a renamed shell prompt as the deliverable. Neither contains the string XFRM.

All three were published in the week the underlying CVE landed. All three appear as PoC available to a scanner that polls GitHub for CVE-named repositories. None of the three is a working exploit for the CVE the repository name carries.

This is the Placeholder PoC shape. The Banyamer exhibit on that pattern's page makes the gap visible by writing the word "placeholder" in a Python comment. The CVE-2026-46300 placeholders extend the pattern by stripping more aggressively. Where Banyamer at least makes a guess at the vulnerable parameter and writes a syntactically plausible HTTP POST, Sentebale and 0xBlackash do not reach the layer of the protocol where the bug lives. The protocol where the bug lives is ESP encapsulated in TCP, parsed by the espintcp ULP, with a keystream byte derived from an AES-128-GCM counter block. The placeholders parse none of that. They call unshare(CLONE_NEWUSER) and input("PWNED> ").

The signal a defender's pipeline derives from "GitHub returns three repositories whose name contains CVE-2026-46300" is, today, three. The number of those repositories that contain code capable of triggering CVE-2026-46300 is one, and that one is a mirror that credits its source. The number authored independently of William Bowling is zero. A patch-prioritisation queue that ranks by "public PoC exists" reads three. The right number is one, and that one is downstream of the V12 publication on lists.openwall.net/netdev/2026/05/13/79, which is where any defender who needed Fragnesia code should have started.

The fix is the same fix the prior two posts named. Search for the CVE in the repository's tracked files, not in the repository's name. If git ls-tree HEAD does not show a file that contains the protocol where the bug lives, the repository does not contain the exploit, regardless of what the badge claims.

PoC: v12-security/pocs/fragnesia

The placeholders renamed the shell prompt. The real exploit's source code names the line where the prompt is not yet root.