During the last few weeks I ended up with the kind of folder every security person recognizes: half-clean PoCs, copied snippets, one-off build commands, notes about which kernel had which config, and a growing feeling that the real story was bigger than any single CVE.

So I cleaned it up into a single repository:

linux-lpe-pocs

The repo is a collection of Linux local privilege escalation proof-of-concepts. Some are direct translations of public PoCs. Some are normalized so they build in the same place. Some are there because they show a useful variant of the same underlying failure mode. The point is not to publish “run this and get root” content. The point is to make the pattern visible.

And the pattern is uncomfortable:

Linux keeps giving unprivileged users ways to route readable file-backed pages through kernel subsystems that were written as if the memory was private.

That is where a read-only file becomes writable without going through normal filesystem permissions. If the file is a setuid-root binary, the gap between “cache corruption” and “root shell” can become very small.

What Happened

The repository groups a wave of Linux LPE research into one place. The bugs are not identical, but they rhyme. They all sit near high-performance kernel paths: splice, socket buffers, crypto transforms, network encapsulation, zerocopy, fixed buffers, request-key helpers, and privileged file descriptors.

Repo pathPublic trackingWhat it demonstrates
CopyFail/poc.cCVE-2026-31431 (reported CISA KEV)AF_ALG crypto path plus splice() causing page-cache writes through a file the user can only read. Public advisories report this CVE as KEV-listed; verify current status with CISA.
DirtyFrag/poc.cCVE-2026-43284, CVE-2026-43500A combined XFRM/ESP and RxRPC chain. One path targets a setuid binary; the fallback can touch /etc/passwd in page cache.
DirtyDecrypt/poc.cCommonly associated with CVE-2026-31635RxGK/AF_RXRPC in-place decryption touching page-cache backed fragments.
DirtyCBC/poc.cCommonly associated with CVE-2026-31635A chosen-plaintext RxGK/CBC variant of the same mutability class.
Fragnesia/poc.cCVE-2026-46300ESP-in-TCP/XFRM page-cache replacement using network encapsulation and splice().
FragnesiaV2/poc.cCVE-2026-46300A follow-up focused on skb_segment() and shared-fragment metadata being lost across segmentation.
PinTheft/poc.cCVE-2026-43494RDS zerocopy reference confusion plus io_uring fixed-buffer stale page pointers.
CIFSwitch/poc.cCVE-2026-46243A CIFS request_key()/cifs.upcall trust-boundary issue using namespaces and NSS loading.
SSH-Keysign-pwn/CVE-2026-46333pidfd_getfd() stealing sensitive file descriptors from privileged helpers during process exit.
CVE mappings are advisory context, not a replacement for vendor guidance. Kernel exposure depends on version, distro backports, build config, loaded modules, sysctls, LSM policy, and whether a feature is built in or loadable.

The thing that stands out is not that Linux had bugs. Linux is huge, old, performance-sensitive code. Bugs happen. The thing that stands out is how often the same mental model breaks:

“This memory is just packet data.”

or:

“This fragment is ours to decrypt in place.”

or:

“This page pointer is still valid because it was pinned earlier.”

Then the page turns out to be file-backed, shared, reused, stale, or reachable through a helper running with more privilege than the original user should have.

That is where the LPE happens.

The Page Cache Is the Target

The Linux page cache is simple to describe and hard to secure perfectly.

When a process reads a file, the kernel caches file contents in memory. Later reads can come from RAM instead of disk. Multiple processes can share the same cached pages. Executable loaders, memory maps, file reads, and kernel subsystems all meet here.

That is normally a feature. It is why Linux is fast.

But it becomes dangerous when a kernel path mutates a page it does not truly own.

A simplified vulnerable shape looks like this:

  1. An unprivileged user opens a readable sensitive file, often /usr/bin/su.
  2. The user routes bytes from that file into a pipe, socket, crypto interface, RxRPC path, XFRM/ESP path, or zerocopy path.
  3. A kernel subsystem treats the backing memory as private packet or buffer memory.
  4. The subsystem writes into it in place.
  5. The write lands in the page cache.
  6. A later execution or read observes the modified cached bytes.

The attacker did not open the file with write permission. They did not change the on-disk inode in the normal way. They made another kernel subsystem become the writer.

That is the part defenders need to internalize. These are not classic “chmod is wrong” or “sudoers is wrong” bugs. The filesystem permission check can be correct and still lose, because the write arrives from behind it.

Why Setuid Binaries Keep Appearing

Most of the page-cache PoCs target a readable setuid-root binary such as /usr/bin/su.

That is not random.

Setuid-root binaries have three properties attackers like:

PropertyWhy it matters
They are readable by normal usersThe attacker can get their pages into cache without write permission.
They execute with elevated effective UIDIf the cached executable page is replaced, execution can happen as root.
They exist on almost every Linux hostThe target is predictable and portable across distributions.

The PoC shape is usually not “write a new file and execute it.” It is more subtle:

  1. Read a page from a setuid-root binary.
  2. Abuse a kernel path to replace cached bytes with a tiny payload.
  3. Execute the same setuid-root binary.
  4. The kernel’s exec path sees the cached page.

Some PoCs only corrupt memory and not disk. That does not make them safe. A corrupted page cache can still be executed, served, or read until it is evicted, invalidated, or the machine is rebooted.

The Interesting Technical Bits

The best way to read the repository is not by asking “which one gives root fastest?” That is the least useful question.

Ask instead: which kernel assumption failed here?

CopyFail: crypto touched what it should have copied

CopyFail/poc.c is the small, sharp example. It uses AF_ALG, an AEAD construction, splice(), and a setuid-root target.

The important idea is that splice() can move file-backed pages around without copying their contents into userspace. That is exactly why it exists. But if the receiving path later mutates those pages as if they were private crypto buffers, the optimization becomes a write primitive.

This is the same kind of failure that makes page-cache bugs scary: performance paths avoid copies, then security depends on every later subsystem remembering the data may still be shared.

CopyFail is also a strong candidate to patch first: public advisories describe CVE-2026-31431 as listed in CISA’s Known Exploited Vulnerabilities catalog. If that holds for your environment, treat it as higher priority than a lab-only PoC. Confirm the current KEV status against CISA before acting on it.

DirtyFrag: two paths, same consequence

DirtyFrag/poc.c is more interesting because it combines two families:

  • an XFRM/ESP path that can patch a setuid-root binary in page cache
  • an RxRPC fallback that can rewrite selected bytes of /etc/passwd in page cache and then drive su

That matters operationally. If a mitigation only disables ESP but leaves RxRPC reachable, the host may still have an exposed path. If it only disables RxRPC but IPsec/ESP is reachable, the other side remains.

The defensive lesson is boring but important: mitigate the class and the reachable interfaces, not just the PoC filename.

Fragnesia: metadata loss is a security bug

The Fragnesia variants are a good reminder that a single bit of metadata can be the difference between “safe to mutate” and “this points into shared file cache.”

FragnesiaV2/poc.c focuses on segmentation behavior. Packet data moves through GRO, GSO, TCP, ESP-in-TCP, and XFRM logic. If shared-fragment metadata is stripped or not preserved, a later decrypt path may believe it owns memory that is actually shared.

This is the kernel version of a classic application bug: sanitize input at the edge, then lose the “untrusted” label before the dangerous operation.

DirtyDecrypt and DirtyCBC: decrypting in place is not always harmless

The RxGK examples show why “decrypt into the same buffer” is a loaded decision.

In-place crypto is normal. It is fast and common. But it is only safe if the destination buffer is private writable memory. If socket fragments point to page-cache backed data, the decrypt path needs copy-on-write discipline before it modifies anything.

The higher-level rule is simple:

Authenticate, copy, or prove ownership before mutation.

If the kernel cannot prove that a buffer is private, mutating it in place is a security decision, not just a performance decision.

PinTheft: not a page-cache write bug, but it lands in the same place

PinTheft/poc.c is different. It combines an RDS zerocopy reference problem with io_uring fixed buffers.

Instead of a crypto path writing into file cache directly, the chain is about page lifetime:

  1. Get a page pinned.
  2. Lose track of references on an error path.
  3. Keep a stale page pointer alive through io_uring.
  4. Reclaim that physical page as page cache for a setuid binary.
  5. Write through the stale pointer.

Different bug class, same final feeling: a page that should not be writable becomes writable.

There is an important version gate here. This chain keeps the cloned registered-buffer reference alive with full-range IORING_REGISTER_CLONE_BUFFERS, an io_uring operation that landed around Linux 6.12. So the Ubuntu 24.04 LTS GA kernel line (upstream 6.8) is not affected by PinTheft as written. Hosts running Ubuntu HWE, mainline, vendor, or custom kernels at 6.12 or newer are in scope. Check the running kernel with uname -r before deciding you are exposed — and remember the underlying RDS bug (CVE-2026-43494) still wants a patch regardless.

CIFSwitch: root helpers cannot trust userspace stories

CIFSwitch/poc.c is not a page-cache corruption bug. It is a trust-boundary bug around CIFS upcalls.

The public chain abuses a forged cifs.spnego request-key description. A normal request-key rule can launch cifs.upcall as root. Affected helper behavior trusts fields such as PID, UID, credential UID, and namespace target as if they came from legitimate kernel CIFS state. With namespace switching and NSS lookup in the mix, attacker-controlled libraries can enter the root helper path.

This is one of those bugs where the exploit reads almost absurdly specific, but the lesson is general:

Privileged helpers must treat userspace-supplied descriptions as hostile, even when the format normally comes from the kernel.

SSH-Keysign-pwn: process exit is still a security boundary

The SSH-Keysign-pwn/ directory covers another different class: file-descriptor theft from privileged helpers using pidfd_getfd() while the process is exiting.

The interesting race is that the target process can have task->mm == NULL while sensitive file descriptors are still open. If access checks incorrectly treat the exiting state as less sensitive, an attacker may duplicate a privileged descriptor before it disappears.

This is not about patching /usr/bin/su. It is about the lifetime of authority. A file descriptor is authority. If a privileged process still holds it, the kernel has to protect it until it is closed.

The Defensive Value of a PoC Repository

There is always a tension with public exploit code. Attackers use it. Defenders use it. Researchers need it. Vendors often wish it had waited.

For me, the useful part of this repository is not the raw exploit outcome. It is the forced clarity:

  • Which kernel interfaces are involved?
  • Which modules or configs make the path reachable?
  • Which syscalls and helper binaries appear before the privilege jump?
  • Which files become suspicious only when seen in sequence?
  • Which mitigations break legitimate workloads?

That is why the repo includes IoC.md. The indicators are more useful than a hash because these bugs are behavioral. A recompiled PoC will have a different checksum, but it still needs the same weird sequence of namespaces, socket families, keyrings, splice(), io_uring, or setuid execution.

What To Hunt For

Do not hunt for only the binary name. That catches lazy tests and misses variants.

Hunt for sequences.

SignalWhy it matters
unshare(CLONE_NEWUSER) followed by unshare(CLONE_NEWNET)Several chains create namespaces to gain namespaced capabilities.
Writes to uid_map, gid_map, and setgroupsCommon setup for user namespace based PoCs.
splice() or vmsplice() from /usr/bin/su, /bin/su, /usr/bin/passwd, or /usr/bin/pkexec into sockets or crypto fdsStrong page-cache exploitation pattern.
Normal users opening AF_ALG, AF_RXRPC, AF_RDS, or NETLINK_XFRMRare on many endpoints and central to several PoCs.
request_key("cifs.spnego", ...) from an unprivileged contextCIFSwitch-style trigger path.
cifs.upcall entering another process namespace or loading unexpected NSS modulesStrong CIFSwitch indicator.
io_uring_setup plus fixed-buffer registration and RDS activityPinTheft-style primitive combination.
Repeated pidfd_open() and pidfd_getfd() attempts against short-lived privileged helpersSSH-Keysign-pwn pattern.
Sudden execution of su, passwd, mount, chsh, or pkexec after unusual kernel-interface activityCommon final privilege transition.

The strongest detections are not single-event alerts. They are correlations:

1
normal user -> namespace setup -> rare kernel interface -> splice/vmsplice -> setuid binary execution -> uid transition

or:

1
normal user -> request_key(cifs.spnego) -> cifs.upcall as root -> namespace switch -> NSS module load

or:

1
normal user -> pidfd_open privileged helper -> repeated pidfd_getfd -> sensitive fd path observed

If your telemetry can express those chains, you are in a much better place than someone searching for dirtyfrag in process names.

Quick Exposure Checks

The first question is whether the relevant code is present, built in, or loadable.

On most distributions, start with the running kernel config:

1
2
grep -E 'CONFIG_CIFS=|CONFIG_CRYPTO_USER_API_AEAD=|CONFIG_INET_ESP=|CONFIG_INET6_ESP=|CONFIG_AF_RXRPC=|CONFIG_RXGK=|CONFIG_RDS=|CONFIG_RDS_TCP=|CONFIG_IO_URING=' /boot/config-$(uname -r) 2>/dev/null || \
zgrep -E 'CONFIG_CIFS=|CONFIG_CRYPTO_USER_API_AEAD=|CONFIG_INET_ESP=|CONFIG_INET6_ESP=|CONFIG_AF_RXRPC=|CONFIG_RXGK=|CONFIG_RDS=|CONFIG_RDS_TCP=|CONFIG_IO_URING=' /proc/config.gz 2>/dev/null

Read the result carefully:

ValueMeaning
=yBuilt into the kernel. You cannot remove it with rmmod.
=mAvailable as a loadable module. Autoload policy matters.
MissingUsually unavailable, unless the distro names or backports it differently.

Then check what is currently loaded:

1
lsmod | grep -E 'algif_aead|af_alg|cifs|esp4|esp6|xfrm_user|rxrpc|rds|rds_tcp'
Absence from lsmod is not proof of safety. A feature may be built into the kernel, or a normal user action may autoload the module later if policy allows it.

For module-based temporary mitigations, the repo documents specific block rules. The short version is:

  • Patch the kernel first.
  • Block unused module families only as a temporary workaround.
  • Rebuild initramfs where the distribution requires it.
  • Reboot to prove the policy is actually enforced.
  • Expect breakage if the host legitimately uses CIFS, IPsec, AFS/RxRPC, RDS, AF_ALG, or io_uring.

Why Patching Alone Is Easy To Misread

Kernel LPEs are especially annoying operationally because “package installed” does not mean “running kernel fixed.”

You can install the patched kernel and still be vulnerable until the machine boots into it:

1
2
uname -r
dpkg -l 'linux-image-*' 2>/dev/null | awk '/^ii/ {print $2}' | sort -V | tail -n1

On RPM-based systems, use the equivalent package query for installed kernel packages and compare it to uname -r.

This matters for these PoCs because a local user does not need a long exploit window. If the vulnerable kernel is still running after the patch is installed, the exploit path may still exist.

Live patching can help, but only if the vendor provides a live patch for the specific issue and the live patch is actually applied. For page-cache corruption and refcount bugs, do not assume live patchability. Verify.

Practical Response If You See This

If you detect one of these patterns on a real host, avoid the temptation to treat it as a simple failed exploit attempt.

Do this instead:

  1. Preserve volatile evidence: process list, loaded modules, audit logs, shell history, network connections, /proc context where possible.
  2. Identify the running kernel and compare it to vendor fixed versions.
  3. Check whether suspicious setuid binaries were executed after the kernel-interface activity.
  4. Look for credential access: /etc/shadow, /etc/passwd, SSH host keys, user SSH keys, sudoers changes.
  5. Look for persistence: new systemd units, cron entries, shell profile changes, authorized keys, sudoers drop-ins.
  6. Reboot or isolate only after evidence collection, unless the host is actively causing damage.

The uncomfortable part is that memory-only page-cache corruption may leave little disk evidence. That makes sequence-based telemetry more important than after-the-fact file hashes.

What I Learned Cleaning This Up

The repo started as a convenience project: put the PoCs in one place, make them build, document what each one shows. But after writing the README and IoCs, the more useful lesson was architectural:

The kernel has many paths that move data without copying it. Every one of those paths carries a security obligation.

Performance work tends to remove copies. Security work wants them back, or at least wants proof that mutation is safe. These bugs live in the space between those goals — normal systems code at high speed, under complex lifetime rules, with security boundaries hidden inside performance shortcuts. That is why they are worth studying even if you never run the PoCs.

Final Takeaway

The lesson from linux-lpe-pocs is not “Linux is broken.” That is too lazy.

The real lesson is sharper:

When the kernel imports user-reachable file-backed memory into high-performance subsystems, ownership has to be explicit. If a path cannot prove it owns writable private memory, it must not mutate it.

For defenders, the practical answer is just as direct:

  • keep kernels patched and actually rebooted
  • know which rare kernel interfaces are reachable by normal users
  • block unused module families where it makes sense
  • collect syscall-level telemetry
  • hunt for behavior chains, not PoC filenames
  • treat page-cache corruption as a real compromise path, even when disk looks clean

The shell is the flashy part. The page ownership bug is the part that matters.