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.
The primary objectives of this repository are to:
- Analyze & Test the Exploit: Safely reproduce, run, and audit the CVE-2026-46331 vulnerability under controlled conditions.
- Implement & Validate Mitigations: Apply and test defense-in-depth controls (such as sysctl limitations and kernel module-blocking scripts).
- 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.
# 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.
The vulnerability stems from a validation bypass in the Linux Traffic Control (tc) packet editing action (act_pedit):
-
One-Time COW Range Validation: When executing a
tc-peditaction, the kernel'stcf_pedit_act()function callsskb_ensure_writable()(orskb_cow()) once before resolving individual edit keys. This creates a writable, private clone of the packet data up to a pre-validated length. -
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 standard5). - 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.
- Key 1 (NETWORK-relative): Overwrites the IPv4 IHL (Internet Header Length) field, setting it to a maximum value of
-
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.
-
Page Cache Injection: By using
sendfile()orsplice(), the target file's page cache (specifically setuid-root/bin/su) is placed directly into the socket buffer (skb). Whenact_peditresolves Key 2 out-of-bounds, it writes directly into the shared page cache page in memory, corrupting the memory image of/bin/suwithout modifying the file on disk. -
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
subecomes available system-wide to any user executing it on the host, until the page cache is cleared or the system is rebooted.
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.
| 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. |
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 aliasAudit mode does not open /bin/su, install tc filters, or attempt the
page-cache overwrite. It checks:
uname()availability and kernel version against thev7.1fix threshold.kernel.unprivileged_userns_clonewhen 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/profilesis readable by this user. - If direct userns is blocked, whether common nested
aa-exectrampoline profiles such aspodman,rootlesskit, orslirp4netnsreopen the userns route.
Verdicts are intentionally phrased as expected exposure because distro backports and runtime policy can disagree with a simple version check.
Modern systems enforcing AppArmor restrictions can block direct unprivileged user namespace creation. However, AppArmor-enforced hosts often remain vulnerable due to permissive application profiles.
- 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 theuserns,rule to allow user namespace creation. - Transition Abuse (
aa-exec): If directunshare()is denied under the defaultunconfinedcontext, an attacker can useaa-execto transition execution into any loaded profile that possesses theuserns,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_ADMINto execute thetc-peditprimitive.
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-profileThat 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/profilesCheck whether profile files exist and contain userns,:
sudo grep -RsnE '(^|profile[[:space:]]+)podman|userns' /etc/apparmor.d /usr/share/apparmor 2>/dev/nullLoad useful profiles explicitly:
sudo apparmor_parser -r /etc/apparmor.d/podman /etc/apparmor.d/rootlesskit /etc/apparmor.d/slirp4netnsOr ask the service loader to process profiles again:
sudo systemctl reload apparmorIf 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.
Run exploitation only on a disposable VM in the vulnerable window:
make exploit
./exploit/peditcowFor screen recording or demos, use paced logging:
./exploit/peditcow --slowOn 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.
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/nullThe 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.
mitigations/block-userns.sh sets both userns knobs at runtime and persists them through a
/etc/sysctl.d/ drop-in:
kernel.unprivileged_userns_clone=0on Debian/Ubuntu.user.max_user_namespaces=0on 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-inImpact: 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.
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 beforeBefore 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.
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_peditvulnerability 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 viaaa-execunder 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.
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.
- 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-peditdo not inflate the IPv4 IHL field beyond the normal value of5while 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_watchSee watchdog/README.md for the safe functional trigger
that reproduces the signature without overwriting anything.
- Original Exploit & Primitive: Developed by sgkdev/packet_edit_meme, providing the foundational page-cache-write primitive.
- Exploit Harness Design: Heavily inspired by and adapted from Rafael Tinoco's security repository at rafaeldtinoco/security/exploits/peditcow.
For a deeper understanding of the behavioral and eBPF-based detection principles applied in this watchdog:
- Detecting CopyFail & DirtyFrag: Explores techniques for identifying anomalous packet modifications and out-of-bounds writes.
- Detecting the nftables catchall UAF (CVE-2026-23111): Details how behavioral tracking can catch complex logic vulnerabilities by looking outside of traditional isolation boundaries.
This project was developed by David with AI-assisted engineering.