
pedit COW
CVE-2026-46331 (nicknamed “pedit COW”) is a local Linux kernel privilege-escalation flaw in the traffic-control subsystem. An unprivileged user (in an unprivileged network namespace) can configure the act_pedit (packet editor) filter to trigger a partial copy-on-write (COW) write into the page cache. In effect, the kernel writes attacker-controlled data into a file’s in-memory image without marking the page private, corrupting the cached copy of that file. Crucially, the exploit requires only CAP_NET_ADMIN (obtainable in a user namespace) and does not modify the on-disk file. In practice, a working proof-of-concept (PoC) called packet_edit_meme was published on June 17, 2026, demonstrating how to overwrite the page-cache image of a setuid binary (e.g. /bin/su) to spawn a root shell. The vulnerability stems from incorrect COW-range calculation in tcf_pedit_act() and has been fixed upstream (June 4, 2026) by moving the writable-region check into the per-key loop.
act_pedit. Unpatched stable releases (including many distro kernels) are vulnerable.tc pedit filter, and overwrites the ELF entry point of a setuid binary in memory with shellcode.skb_ensure_writable() inside the key loop). As a workaround, block or unload the act_pedit module or disable unprivileged user namespaces (e.g. sysctl user.max_user_namespaces=0). After mitigation, drop caches (echo 3 > /proc/sys/vm/drop_caches) to evict any poisoned pages.This report provides a detailed technical analysis of CVE-2026-46331: its cause, exploitation, detection, and remediation strategies, with references to vendor advisories, CVEs, and the public exploit.
Definition: CVE-2026-46331 is an out-of-bounds write bug in the Linux kernel’s Traffic Control (net/sched) subsystem, specifically in the act_pedit (packet editor) action. The function tcf_pedit_act() computes a “copy-on-write” range for packet-edit operations before iterating over typed keys, using a static hint tcfp_off_max_hint. However, some keys (e.g. TCP/UDP header edits) determine their final byte offset only at runtime. The code never re-checks writability for these dynamic offsets. As a result, writes can occur outside the pre-COW’d region: part of the packet write is never made private, leading to a partial COW. This erroneous write propagates into the shared page-cache memory of a file (if the packet buffers happen to reference file pages), corrupting the cached file image.
Background: The Linux packet editor (pedit) action allows administrators to rewrite arbitrary bytes within packet headers (link, network, or transport layers) as packets traverse a configured tc filter. It works by specifying an offset (possibly anchored to a header) and a 32-bit value/mask. Internally, pedit operates on socket-buffers (sk_buff) and must make the target packet memory writable before modifying it (via skb_ensure_writable() in COW fashion). Ideally, the kernel should clone (private-copy) any shared pages before writing to avoid altering memory used elsewhere.
Root Cause: In tcf_pedit_act(), the code mistakenly calculates the writable range only once upfront, using tcfp_off_max_hint (the maximum static offset). This hint does not include any runtime header offset that typed keys add when the packet is being processed. Keys like TCP or UDP can compute an offset based on the position of the IP header at runtime (for example, if an earlier key shifts the network header). Thus, during the per-key loop, the actual offset for a key may exceed the range that was pre-allocated as writable. The code then writes into packet memory via skb_store_bits(), but since the page beyond the pre-COW’d region was not made private, the write corrupts a page that is still shared with the page cache. In short, “calculating the writable packet range too early” causes an out-of-bounds, cross-page write. Negative offsets (e.g. editing Ethernet headers on ingress) are also mishandled, and even offset_valid() lacked a guard for INT_MIN, compounding the flaw.
Why It Happens: This bug is essentially a logic error in copy-on-write range calculation. The kernel assumed the static maximum offset (known at load time) was sufficient for all edits. It failed to update the COW range when keys with dynamic offsets were actually applied. After a series of queued edits, the final write could lie outside the pre-checked region. Because packet buffers may reference memory-mapped file pages (e.g. via zero-copy mechanisms), this “partial COW” write can reach the page cache of a file on disk. In practice, the packet editor action may receive pages from a sendfile or splice; thus a single packet filter operation can indirectly write attacker-chosen data into a file’s in-memory image, without altering the disk.
Components and Data Flow: The vulnerable code resides in the Linux net/sched subsystem (act_pedit.c). When a packet matches a configured pedit rule, tcf_pedit_act() is invoked. Internally it calls skb_ensure_writable(skb, X) exactly once, where X = tcfp_off_max_hint. This makes the first X bytes of the packet private (COW’d). Then, in a loop over each key (edit operation), it computes the key’s actual write offset by adding the runtime header offset to the key’s specified offset, and writes a 32-bit value into the packet. In pseudocode:
u32 off_max = action->tcfp_off_max_hint;
skb_ensure_writable(skb, off_max);
for (i = 0; i < num_keys; i++) {
u32 hdr_off = compute_header_offset(skb, key[i].hdr_type);
u32 write_off = hdr_off + key[i].offset;
skb_store_bits(skb, write_off, &key[i].value, 4);
}
Because hdr_off is computed only when processing each key, the initial skb_ensure_writable() call did not account for it. If hdr_off + key[i].offset exceeds off_max, the code falls back to skb_store_bits() on fragments rather than the main linear area, meaning it writes into a page not made private. That is the failure point.
Attack Surface: The only interface needed is the tc filter with a pedit action, which normally requires the CAP_NET_ADMIN capability. However, ordinary users can obtain CAP_NET_ADMIN within a private network namespace (user namespace cloning) without real privileges. Thus, an unprivileged user can enter a user+net namespace and create a tc pedit rule on loopback. The write occurs when a packet is processed (the attacker typically generates traffic on loopback to trigger it). The trust boundary (user vs kernel) is crossed because the kernel trusted its own COW setup, but the user-supplied offsets broke that assumption.
Internal Mechanism: On the kernel side, the vulnerability manifests as an out-of-bounds write (CWE-787). It corrupts kernel memory that is mapped into user space (file page cache). Specifically, it can overwrite the contents of any file page that happens to be mapped into the socket buffer. In the proof-of-concept, /bin/su is mmapped by sending it into the socket buffer, so the exploit flips its entry point bytes in memory. This does not modify the on-disk file, but any subsequent execution of that binary reads the poisoned image from the cache. The blog analysis notes:
“Because the skb can reference zero-copy pages pulled in via sendfile, that out-of-bounds write can land in shared page-cache memory backing a real file. The kernel believes it has made the packet memory safe to modify; in reality, the later write reaches outside the region it actually privatized.”
Trust Boundaries: The kernel wrongly assumed that skb_ensure_writable() (fast-path COW) would guarantee safety for all subsequent writes. It did not re-check for each key. The user only controls packet filter configuration and packet content; the kernel granted that (through network namespaces). Once that trust was breached, the write escaped into file-backed memory that should have been protected.
The root cause is incorrect COW-range calculation in the pedit action. In code terms, a single skb_ensure_writable() was called with a length based on tcfp_off_max_hint, then inside the loop the actual offsets could exceed this. A small patch (May 2026) fixes it by moving skb_ensure_writable() inside the loop, after the true offset is known, and by adding checks and special handling for negative offsets. In other words:
skb_ensure_writable(skb, action->tcfp_off_max_hint);
for each key:
// compute offset (hdr_off + key_offset)
skb_store_bits(skb, write_off, ...);
for each key:
// compute offset (hdr_off + key_offset)
skb_ensure_writable(skb, write_off + 3);
skb_store_bits(skb, write_off, ...);
Additionally, the fix ensures that for negative offsets (Ethernet header edits) it uses skb_cow() on headroom, and guards against INT_MIN cases. The commit message (stack.watch summary) states: “Fix by moving skb_ensure_writable() inside the per-key loop where the actual write offset is known, and add overflow checking on the offset arithmetic.”.
Thus, why it exists: during code review or design, the per-key re-calculation was overlooked. The static hint optimization bypassed the need to re-evaluate per key. It appears to be an honest bug rather than a malicious oversight, but its effect is severe because it violates the COW assumption. As TuxCare notes, this bug was merged under the guise of a routine “data corruption” fix, without immediate security context.
The vulnerability was introduced by kernel commit 8b796475fd78 (May 2022) and remained unnoticed until early 2026. According to sources, the fix (commit 899ee91156e5 on May 31, 2026) was submitted to the netdev mailing list as an ordinary data-corruption patch. The kernel maintainers merged the fix (net-7.1-rc7) on June 4, 2026. Only on June 16, 2026 was CVE-2026-46331 formally assigned (about two weeks after the patch appeared). A fully weaponized public exploit appeared on June 17, 2026 (the packet_edit_meme PoC).
In practice, the sequence was:
Multiple parties noticed the bug by the open patch. For example, Massimiliano Oldani (cybersecurity researcher) published a detailed write-up and exploit shortly after, noting that “a public, working proof-of-concept exploit named packet_edit_meme appeared on GitHub within 24 hours of CVE assignment”. CloudLinux, TuxCare, and SentinelOne published analyses once the PoC was public and CVEs assigned. The Debian security tracker and PT DBugs also summarized the issue and available advisories (see References).
A realistic attack requires minimal preconditions:
Attacker capabilities: A local unprivileged user on the target machine. The user must be able to create a new user namespace with network namespace (via unshare(CLONE_NEWUSER|CLONE_NEWNET)), which grants CAP_NET_ADMIN inside that namespace without real root privileges. Unprivileged user namespaces are enabled by default on many kernels (e.g. RHEL, Debian) and can be re-enabled on Ubuntu with an aa-exec workaround.
Target conditions: The target must be running a vulnerable Linux kernel (approx. 5.18–7.1-rc6) with the act_pedit module available. If act_pedit is built-in or already loaded, it is immediately exploitable. If it is a module, it auto-loads when a tc pedit rule is configured. The target should not have applied the upstream patch. Notably, it is not necessary for the attacker to have write access to any file; the exploit works by writing through packet filters.
Attack chain:
unshare --map-root-user --net --pid bash to create a new user+net namespace. This grants CAP_NET_ADMIN in that namespace (user mapped to root inside).ifconfig lo up) and optionally spawns a listener (e.g. nc -l 127.0.0.1 9999). This provides a packet flow to use for TC actions.Impact: If successful, the attacker gains full root privileges locally. The exploit can be done in one command and is deterministic. Additionally, corruption of arbitrary file-backed pages could cause denial-of-service (system crash) if used differently. The published PoC specifically overwrote /bin/su’s entrypoint with shellcode, but any file the attacker can map could be targeted. The chain requires no special timing or race and has been demonstrated on many distros (RHEL, Ubuntu, Debian, etc.).
A public exploit, packet_edit_meme, is available on GitHub (sgkdev/packet_edit_meme) and targets /bin/su. We describe its essential logic without destructive payloads:
/* Pseudocode outline of the exploit (simplified) */
int main() {
/* 1. Identify a setuid binary (su) and its ELF entry offset */
int fd = open("/bin/su", O_RDONLY);
long entry = elf_entry_offset(fd);
if (entry < 0) abort();
printf("Target %s (UID=%d), entry offset 0x%lx\n", "/bin/su", getuid(), entry);
/* 2. Unshare user+net namespace to get CAP_NET_ADMIN locally */
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) abort();
/* Map UID/GID to root (handled via /proc/self/uid_map, /gid_map) */
// (omit details: write "0 <uid> 1" to /proc/self/uid_map and gid_map, and deny setgroups)
/* 3. Setup environment: bring up loopback and listener */
if (system("ip link set lo up") < 0) abort();
if (system("nc -l 127.0.0.1 9999 &") < 0) abort();
/* 4. Configure a tc pedit action via netlink (simplified) */
// Assume 'pedit_write' sends a packet-edit command to the kernel.
// The key offsets below are chosen such that they exceed the initial COW range.
char shellcode[/*size=48*/] = {
// (assembly for setgid(0); setuid(0); execve("/bin/sh").., padded to 36 or 48 bytes)
};
size_t total = sizeof(shellcode), sent = 0;
while (sent < total) {
int chunk = min(PEDIT_MAX_WRITE, total - sent);
/* Issue TC pedit action to write next chunk */
if (pedit_write(fd, entry + sent, &shellcode[sent], chunk) != 0) {
fprintf(stderr, "pedit_write failed\n");
exit(1);
}
sent += chunk;
}
/* 5. Trigger execution of su (in original namespace) */
execl("/bin/su", "su", NULL); // This will run the poisoned binary as root
return 0;
}
This pseudocode illustrates the flow: open /bin/su, unshare namespaces to gain CAP_NET_ADMIN, configure loopback and TC pedit rules, then call a function pedit_write(fd, offset, data, len) (in the actual PoC this uses netlink calls under the hood) to overwrite the target’s page cache. Finally, the binary is executed, spawning a root shell.
The actual PoC is more elaborate (handling UID/GID maps, network listening, and syscall-level shellcode bytes), but the core concept is as above. We emphasize not to run this exploit except in a safe test environment, and not to target any real system. The above is for demonstration only.
Note: If no public safe-to-use PoC existed, we would explicitly state so. In this case, the PoC is public, and we describe it conceptually. We have omitted the raw shellcode bytes and actual netlink details for brevity and safety.
Entry Point: The attacker must first obtain CAP_NET_ADMIN. Typically, this means creating a user+network namespace (unshare) from an unprivileged process, which grants namespace-local CAP_NET_ADMIN.
Initial Access: Within this namespace, the attacker can use normal tools (ip, tc) to configure traffic control. The kernel’s act_pedit code path is now reachable for packets.
Trigger: The attacker sets up a tc filter ... action pedit on the loopback interface. This filter matches packets (e.g. 0-match) and specifies one or more typed keys with header types (IP, TCP) and offsets. The offsets are chosen so that after the kernel computes the header base inside the loop, the final write offset exceeds the initial COW range.
Exploitation: When a packet matching the filter is processed, the kernel calls tcf_pedit_act(). It performs an insufficient skb_ensure_writable() and then iterates keys. For at least one key, the write lands on a page that was not cloned to private copy. This causes an out-of-bounds write into the shared page cache. If the socket buffer was prepared to reference pages of a file (via sendfile/splice), that write corrupts those file pages.
Post-Exploitation: The attacker’s shellcode has been written into the target binary’s pagecache (e.g. /bin/su). The attacker (in the original namespace) then executes . The kernel reads the in-memory image (with the injected payload) and runs the shellcode, giving the attacker a root shell. At this point, full system compromise has occurred.
This flow is summarized diagrammatically:
flowchart LR
A[Attacker (unprivileged user)] --> B[Unshare into user+net namespace<br>(gains CAP_NET_ADMIN)]
B --> C[Configure TC pedit filter on lo]
C --> D{Packet processing by kernel}
D --> E[act_pedit computes wrong COW range]
E --> F[skb_store_bits writes beyond COW'd region]
F --> G[Page cache of target file is corrupted]
G --> H[Attacker executes poisoned setuid binary]
H --> I[Root shell obtained]
act_pedit module appears in lsmod unexpectedly on systems that don’t normally use tc pedit. (E.g. lsmod | grep act_pedit being non-empty on web servers.)tc usage: Unusual tc commands or netlink messages from unprivileged processes. Auditing logs may show CAP_NET_ADMIN granted to a non-root process.netstat -tulnp showing nc or custom listener on 127.0.0.1 could be a sign.tcf_pedit_act, skb_ensure_writable, or soft lockups during heavy traffic on loopback or errors in tc processing. (These would be unusual and indicative of corruption.)For example, one IOA is corrupted files in page cache: a triage checklist might include verifying in-memory file content vs disk, especially for setuid binaries after heavy tc activity. Another is new namespace creation: monitoring calls to unshare(CLONE_NEWUSER|CLONE_NEWNET) could be flagged. In short, defenders should watch for any of: act_pedit usage, userns usage, and sudden modifications of executables in RAM.
To detect exploitation attempts:
tc configurations or netlink messages that add an act_pedit filter. For instance, Sigma rules could look for events containing TCA_ACT_KIND: pedit or similar. Monitor audit logs for capset CAP_NET_ADMIN from non-root processes, or writes to /proc/*/uid_map./bin/su (or other sensitive binaries) and suddenly executing them in tandem with namespace/unshare syscalls. Alert on any process that both opens a setuid binary and creates a userns.skb_ensure_writable() cannot be fooled (though no known built-in check exists).In summary, defenders should log and audit user namespace usage, tc commands, and module loads. One key approach: reject or log any invocation of tc pedit by untrusted users. On compromised hosts, check if /etc/modprobe.d/disable-act_pedit.conf has been applied (it should be pre-emptively).
Apply Patches: The primary fix is a kernel update. All major distributions have released updates in June 2026. Upgrading to a patched kernel (Linux 7.1.0 or later, or distro backports) is the definitive solution.
Configuration Changes: If patching is not immediately possible, implement mitigations:
Disable act_pedit: If your workloads do not require tc pedit, blacklist the module. For example:
echo 'install act_pedit /bin/true' | sudo tee /etc/modprobe.d/disable-act_pedit.conf
lsmod | grep -w act_pedit && sudo rmmod act_pedit
This ensures the action cannot be loaded. (This is recommended by CloudLinux and TuxCare.) Do not apply on hosts that legitimately use tc pedit.
Restrict User Namespaces: Remove the unprivileged namespace attack vector. On RHEL/Alma/Debian:
sudo sysctl -w user.max_user_namespaces=0
echo 'user.max_user_namespaces = 0' | sudo tee /etc/sysctl.d/99-pedit-cow.conf
On Ubuntu 22.04+:
sudo sysctl -w kernel.unprivileged_userns_clone=0
echo 'kernel.unprivileged_userns_clone = 0' | sudo tee /etc/sysctl.d/99-pedit-cow.conf
This prevents unprivileged users from creating the necessary user namespace to gain CAP_NET_ADMIN. Note: disabling namespaces may break rootless containers and some sandboxed applications.
Drop Page Cache (Containment): If you suspect the exploit ran, the in-memory copies of binaries may be poisoned. Immediately drop caches to evict them:
sudo sh -c "echo 3 > /proc/sys/vm/drop_caches"
This forces pages to be reloaded from disk. If an attacker already had root, dropping caches won’t remove any persistence they installed. Treat such hosts as compromised.
Long-term remediation involves ensuring that all affected systems are on updated kernels. Kernel packages that contain the fix should be installed and systems rebooted. For containers or systems unable to reboot, consider livepatch solutions (e.g. KernelCare) which have prepared patches.
Additionally, system design should assume that user-space accessible kernel interfaces can change over time. Restricting CAP_NET_ADMIN and filtering tc usage are good practices beyond this bug.
If a compromise occurred, rebuild the system. The vulnerability poisons the page cache only, but an attacker with root may have done other malicious actions; forensic validation is needed. Do not rely on file-integrity scans after the exploit, because as noted the PoC leaves on-disk files intact. Rebooting and patching is the safe remediation path.
Given these factors, a typical CVSS v3.1 vector is AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H, yielding a Base Score of 6.0 (Medium). Note however that CVSS does not capture that this vulnerability grants privilege escalation to root, which in practice is critical. (Some sources computed CVSSv4 for similar bugs; e.g. PT DBugs lists 8.5 on CVSSv4.)
The severity is often rated as Important/Critical by vendors. Red Hat’s advisory for this CVE labels it Important, and AWS marks it Medium (CVSS 6.0). In any case, because root is gained, the practical risk is highest on multi-user or shared systems.
This vulnerability belongs to a family of page-cache poisoning bugs. Other notable CVEs include:
splice() on a pipe could write to page cache beyond COW boundaries. It also allowed overwriting files in memory (without disk change)./proc/self/mem copy-on-write that allowed local write to read-only mappings.Each of these involves a kernel fast-path writing to memory it believed it exclusively owned, but did not. CVE-2026-46331 is unique in that it occurs in net/sched pedit action and leverages user namespaces to bypass privilege restrictions. Unlike DirtyPipe or Dirty COW, no auxiliary privileged process (like a misbehaving system) is needed – a single unprivileged user can trigger it.
All references are from reputable sources (vendor advisories, published analyses, CVE/NVD entries).
act_pedit, calculating writability too early enabled page-cache corruption.tc command or netlink, the attacker creates a qdisc and filter on lo that matches all packets (e.g. match u32 0 0) and attaches a pedit action with specially crafted keys. Each key has a dynamic header type (e.g. IP header for an L4 offset) and an offset chosen so that the actual write position (header start + offset) lies just beyond the range skb_ensure_writable() covered.echo '' > /dev/udp/127.0.0.1/53) to trigger the filter. The kernel calls tcf_pedit_act(), allocates a COW range, then iterates the keys. At least one key’s write falls outside the pre-COW’d region, causing the write to go into the shared pagecache./bin/su into the socket via sendfile or similar, so the packet buffer references that file’s pages. The out-of-bounds write then corrupts the in-memory copy of /bin/su (specifically the ELF entry point)./bin/su). Because the kernel has inadvertently injected shellcode that does setgid(0); setuid(0); execve("/bin/sh"), running su drops a root shell. The file on disk was never altered, so no on-disk file integrity tools will show a change./bin/suImpact: The attacker gains root privileges. Confidential data could be overwritten but not leaked directly. Integrity is completely broken (the attacker can change any file’s memory image). Availability can also be affected (miswriting critical pages could crash processes or the system). CVSS metrics: Medium overall (CVSS 3.1=6.0), but actual impact is severe local root.
auditd/proc/[pid]/uid_mapLeast Privilege: Audit and restrict who can use tc. The exploit only needs CAP_NET_ADMIN; ensure only trusted admins have this capability. Use RBAC or containerization to limit capability grants.
Network Controls: While not directly networkable, ensure loopback usage is monitored. Firewalling 127.0.0.1 is impractical, but ensure that only localhost traffic is used for TC manipulations.
Vendor Advisories: Refer to official advisories for your OS. Red Hat has RHSA-2026:27354 (and related) for RHEL 8/9/10, Debian has DSA-6355-1, Ubuntu’s CVE page lists fixed kernels, etc. (See References.)