Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pedit-cow (CVE-2026-46331)

Authorized reproduction, audit, mitigation, and detection harness for pedit COW: a net/sched act_pedit partial copy-on-write page-cache write in the Linux kernel that can be turned into unprivileged local root.

Project Goal

The primary objectives of this repository are to:

  1. Analyze & Test the Exploit: Safely reproduce, run, and audit the CVE-2026-46331 vulnerability under controlled conditions.
  2. Implement & Validate Mitigations: Apply and test defense-in-depth controls (such as sysctl limitations and kernel module-blocking scripts).
  3. Develop & Verify Runtime Detection: Compile and test an eBPF-based watchdog sensor that flags exploit attempts in real-time.

Warning

Authorized testing only. Do not run this against systems you do not own or do not have explicit written permission to test.

At A Glance

# Build the exploit (or run 'make' to build both exploit and watchdog)
make exploit

# Non-destructive exposure audit: exit 0 = expected mitigated, 2 = expected vulnerable
./exploit/peditcow --audit

# Demo/recording mode: pause 0.5s after each status line
./exploit/peditcow --slow

# Exploit run on a disposable vulnerable VM
echo id | timeout 25 ./exploit/peditcow        # expected success: uid=0(root) ...

Bug window: v5.18, starting at upstream culprit 899ee91156e5, through the mainline fix in v7.1-rc7. Distro backports can change the real exposure of an older-looking kernel, so use ./exploit/peditcow --audit and, for definitive validation, only test exploitability on disposable systems.

Why the Kernel is Vulnerable (CVE-2026-46331)

The vulnerability stems from a validation bypass in the Linux Traffic Control (tc) packet editing action (act_pedit):

  1. One-Time COW Range Validation: When executing a tc-pedit action, the kernel's tcf_pedit_act() function calls skb_ensure_writable() (or skb_cow()) once before resolving individual edit keys. This creates a writable, private clone of the packet data up to a pre-validated length.

  2. Relative Offset Calculation & IHL Inflation: An attacker defines a sequence of keys with relative offsets:

    • Key 1 (NETWORK-relative): Overwrites the IPv4 IHL (Internet Header Length) field, setting it to a maximum value of 15 (instead of the standard 5).
    • Key 2 (TCP-relative): Overwrites bytes in the transport layer. To find the TCP header, the kernel dynamically jumps forward by multiplying the IPv4 IHL value.
  3. The OOB Write: Because the IHL value was inflated after the initial write-boundary check was completed, the dynamically calculated offset for Key 2 resolves to a position far past the pre-validated COW boundary.

  4. Page Cache Injection: By using sendfile() or splice(), the target file's page cache (specifically setuid-root /bin/su) is placed directly into the socket buffer (skb). When act_pedit resolves Key 2 out-of-bounds, it writes directly into the shared page cache page in memory, corrupting the memory image of /bin/su without modifying the file on disk.

  5. System-Wide Exposure: Because the page cache is shared system-wide, the modified in-memory page of the binary remains cached globally. This means the hijacked version of su becomes available system-wide to any user executing it on the host, until the page cache is cleared or the system is rebooted.

The Exploit Payload

The exploit uses this primitive to overwrite the cached ELF entry point of /bin/su with a short setuid payload:

setgid(0)
setuid(0)
execve("/bin/sh")

When the parent process executes su, the setuid bit causes it to run with elevated privileges. Because the shared page cache holds the corrupted instructions, the binary runs the shellcode and spawns /bin/sh as root.

The primitive needs CAP_NET_ADMIN. The normal unprivileged route is:

unshare(CLONE_NEWUSER|CLONE_NEWNET)

Inside that new user namespace, the process maps its uid/gid to 0/0 and gains CAP_NET_ADMIN for its private network namespace.

Repository Layout

File Purpose
exploit/ Directory containing the privilege escalation exploit source files (packet_edit_meme.c, pedit_primitive.c, pedit_primitive.h).
mitigations/ Directory containing sysctl and module-blocking scripts (block-userns.sh, block-module.sh, and their reverts).
watchdog/ eBPF CO-RE detector for the pedit-CoW tc-pedit signature.

Audit Mode

Use audit mode when you want a read-only system assessment instead of an exploit attempt:

./exploit/peditcow --audit
./exploit/peditcow --check              # compatibility alias
./exploit/peditcow --check-mitigations  # compatibility alias

Audit mode does not open /bin/su, install tc filters, or attempt the page-cache overwrite. It checks:

  • uname() availability and kernel version against the v7.1 fix threshold.
  • kernel.unprivileged_userns_clone when present.
  • user.max_user_namespaces.
  • Whether direct unshare(CLONE_NEWUSER|CLONE_NEWNET) works for this user.
  • Whether AppArmor securityfs is present.
  • Whether /sys/kernel/security/apparmor/profiles is readable by this user.
  • If direct userns is blocked, whether common nested aa-exec trampoline profiles such as podman, rootlesskit, or slirp4netns reopen the userns route.

Verdicts are intentionally phrased as expected exposure because distro backports and runtime policy can disagree with a simple version check.

AppArmor Trampoline Use Case

Modern systems enforcing AppArmor restrictions can block direct unprivileged user namespace creation. However, AppArmor-enforced hosts often remain vulnerable due to permissive application profiles.

Why AppArmor-Enforced Hosts Remain Vulnerable

  • Permissive Profiles: Applications like container runtimes and web browsers (podman, rootlesskit, slirp4netns, chrome, etc.) require user namespaces to isolate processes. Consequently, their loaded AppArmor profiles must explicitly declare the userns, rule to allow user namespace creation.
  • Transition Abuse (aa-exec): If direct unshare() is denied under the default unconfined context, an attacker can use aa-exec to transition execution into any loaded profile that possesses the userns, permission.
  • Namespace Access: Once running under the context of the permissive profile, the exploit gains the authority to create user and network namespaces. Inside the namespace, it maps its ID to root, obtaining CAP_NET_ADMIN to execute the tc-pedit primitive.

A profile file is not enough by itself; the profile must be loaded into the kernel.

Example profile shape:

profile podman /usr/bin/podman flags=(unconfined) {
  userns,
}

When direct unshare() is denied, peditcow probes loaded userns-capable profiles and re-execs itself with aa-exec if one opens the namespace route. On the tested AppArmor-enforced system, a single transition produced a stacked label that still denied userns:

podman//&unconfined (unconfined)

The working route was a nested transition:

aa-exec -p podman -- aa-exec -p podman -- ./exploit/peditcow --in-profile

That produced:

podman (unconfined)

and allowed unshare(CLONE_NEWUSER|CLONE_NEWNET).

Test the trampoline without running the exploit body:

aa-exec -p podman -- aa-exec -p podman -- sh -c 'cat /proc/self/attr/current; ./exploit/peditcow --probe-userns; echo exit=$?'

exit=0 means the trampoline opens the userns route. exit=1 means it is blocked.

Check whether relevant profiles are loaded:

sudo aa-status | grep -E 'podman|rootlesskit|slirp4netns'
sudo grep -E '^(podman|rootlesskit|slirp4netns) ' /sys/kernel/security/apparmor/profiles

Check whether profile files exist and contain userns,:

sudo grep -RsnE '(^|profile[[:space:]]+)podman|userns' /etc/apparmor.d /usr/share/apparmor 2>/dev/null

Load useful profiles explicitly:

sudo apparmor_parser -r /etc/apparmor.d/podman /etc/apparmor.d/rootlesskit /etc/apparmor.d/slirp4netns

Or ask the service loader to process profiles again:

sudo systemctl reload apparmor

If aa-exec says profile 'podman' does not exist, Podman may be installed and /etc/apparmor.d/podman may exist, but the profile is not loaded. Load it and retest the nested probe.

Reproduction

Run exploitation only on a disposable VM in the vulnerable window:

make exploit
./exploit/peditcow

For screen recording or demos, use paced logging:

./exploit/peditcow --slow

On a vulnerable host with a working userns route, the run verifies that /bin/su's cached entry contains the shellcode before execing su and printing the final [ VULNERABLE ] verdict.

Cleaning Up / Restoring /bin/su

Since the exploit corrupts the in-memory page cache of the /bin/su executable, the system is left in a compromised state where any user can run su to get root. To clear the dirty cache pages and force the kernel to reload the unmodified binary from disk, run:

# Flush page cache and force reload of su from disk
sudo sh -c 'echo 1 > /proc/sys/vm/drop_caches'
cat /bin/su > /dev/null

Mitigations

The real fix is a patched kernel: v7.1-rc7 or later, or the distro backport of the act_pedit fix. The controls below are defense-in-depth or temporary containment.

Warning

These controls alter shared host behavior and can break container, sandbox, or traffic-control workloads. Test in staging and have authorized IT apply them.

Option 1: disable unprivileged user namespaces

mitigations/block-userns.sh sets both userns knobs at runtime and persists them through a /etc/sysctl.d/ drop-in:

  • kernel.unprivileged_userns_clone=0 on Debian/Ubuntu.
  • user.max_user_namespaces=0 on generic kernels.

It backs up original values first so revert is exact.

sudo ./mitigations/block-userns.sh     # apply, log before/after state to /var/lib/peditcow/audit.log
sudo ./mitigations/unblock-userns.sh   # restore original sysctl values and remove the drop-in

Impact: breaks Docker Rootless, Chrome/Electron sandboxes, Flatpak, and any workload that creates user namespaces unprivileged. This is usually least disruptive on dedicated servers with no desktop/container sandbox workloads.

Option 2: block the vulnerable modules

mitigations/block-module.sh writes an autoload block for act_pedit and sch_ingress:

install act_pedit /bin/false
install sch_ingress /bin/false

It then attempts to unload any currently loaded copy. If refcount is greater than zero, live unload is skipped with a warning; the persistent autoload block is still written.

sudo ./mitigations/block-module.sh    # block autoload and unload when refcount is 0
sudo ./mitigations/unblock-module.sh  # remove the drop-in and reload modules present before

Before applying, check active use:

lsmod | grep -E 'act_pedit|sch_ingress'

The third column is the module refcount. A value greater than zero means some current tc setup is using the module, so live unload will be skipped until that setup is removed.

Module-blocking impact:

Component Effect
act_pedit unloaded tc actions that rewrite packet headers stop working or fail to install.
sch_ingress unloaded clsact and ingress qdiscs are unavailable; ingress/egress tc filters and BPF attachment points fail.
Autoload block Future kernel or explicit modprobe loads fail until mitigations/unblock-module.sh is run.

Not affected by module blocking: other qdiscs such as sch_htb, sch_fq, or sch_cake; other tc actions such as act_mirred, act_skbedit, or act_bpf; iptables/nftables; and unprivileged user namespaces.

Option 3: AppArmor/SELinux policy

Confining unprivileged user namespace creation can stop the plain route. However, this mitigation is not secure enough on its own:

  • Not a Kernel Fix: LSM policies only restrict the namespace setup vector; they do not patch the underlying act_pedit vulnerability in the kernel.
  • Permissive Profiles & Trampolines: Production hosts routinely load profiles for sandboxed apps (e.g. chrome, flatpak) or container runtimes (e.g. podman, rootlesskit). An attacker can pivot execution via aa-exec under any of these permissive contexts to gain user namespace permissions (see the AppArmor Trampoline section above).
  • Policy Complexity: Maintaining an airtight configuration that blocks all unprivileged namespace transitions without breaking legitimate containerized or sandboxed workloads is extremely difficult.

Therefore, AppArmor namespace restriction acts as a defense-in-depth hurdle rather than a solid boundary. For robust containment, disabling unprivileged namespaces globally (Option 1) or blocking/unloading the act_pedit module (Option 2) are much more reliable.

Detection

The high-confidence detection point is not the root shell; it is the abnormal operation the primitive cannot avoid: installing a tc-pedit action that inflates IPv4 IHL with a NETWORK-relative key and also carries TCP-relative write keys.

A reference eBPF CO-RE sensor lives in watchdog/. It hooks tcf_pedit_init, which is netns-global, so it sees filter installation even when the exploit performs all tc netlink inside an unshared CLONE_NEWNET namespace. The alert includes the real uid, netns inode, pedit key shape, and the task AppArmor label when it can still be read from /proc/<pid>/attr/current.

Why the Watchdog is Highly Effective

  • Pre-Execution Detection: Most security logging systems catch privilege escalations only after a root shell is spawned or setuid binaries are modified. The watchdog triggers during filter initialization (tcf_pedit_init) — intercepting the exploit's setup phase before the out-of-bounds write is executed.
  • Bypassing Namespace Isolation: Attackers run their traffic control commands inside a private network namespace (CLONE_NEWNET) to hide from user-space listeners in the root namespace. Because kernel-level eBPF kprobes are netns-global, our sensor remains fully visible to namespace-isolated operations.
  • Zero-Overhead on Live Traffic: The eBPF kprobes are attached only to the filter installation path (tcf_pedit_init), which is called once. The packet data path (tcf_pedit_act) is left untouched, guaranteeing zero performance impact on live network routing.
  • Near-Zero False Positives: Legitimate use cases for tc-pedit do not inflate the IPv4 IHL field beyond the normal value of 5 while carrying transport/TCP edit keys. This specific, anomalous configuration is unique to this page-cache write primitive.

To build and run the watchdog:

cd watchdog
make
sudo ./pedit_watch

See watchdog/README.md for the safe functional trigger that reproduces the signature without overwriting anything.

Credits & Further Reading

Prior Work & Acknowledgments

Detection Methodology & Research

For a deeper understanding of the behavioral and eBPF-based detection principles applied in this watchdog:


This project was developed by David with AI-assisted engineering.

About

PeditCOW Vuln POC / Audit

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages