Blog

pedit COW (CVE-2026-46331): the Linux kernel overwrites a file’s cache — and a local user becomes root

LinuxKernel_CVE-2026-46331_pedit_COW
CVE / Linux / Security

pedit COW (CVE-2026-46331): the Linux kernel overwrites a file’s cache — and a local user becomes root

In late May 2026, a patch landed on the netdev mailing list with the subject “net/sched: fix pedit partial COW leading to page cache corruption.” Nothing alarming — a routine data-corruption fix in the traffic-control subsystem. No CVE, no security tag. The patch sat in the public archive for weeks. Then, on June 16, when it merged into mainline, the kernel CNA automatically assigned an identifier: CVE-2026-46331. The next day, June 17, a working exploit appeared on GitHub. Security teams that don’t track the netdev mailing list found out when customers started filing tickets.

CVE-2026-46331, nicknamed pedit COW, is a vulnerability in the Linux kernel’s traffic-control subsystem, specifically in the function tcf_pedit_act(). It is an out-of-bounds write (CWE-787) that lets an unprivileged local user write data into the page cache of any readable file — including the cached in-memory image of a setuid binary like /usr/bin/su. NVD had not finalized a CVSS score at publication; Red Hat rates the flaw as Important with a local attack vector, low complexity, and no user interaction required. A public working exploit exists for RHEL 10, Debian 13, and Ubuntu 24.04. Red Hat has shipped errata for RHEL 8, 9, and 10. Debian patched trixie. Ubuntu has not yet released a patched kernel for supported releases.

WHAT TC AND ACT_PEDIT ARE

The tc utility (traffic control) is the Linux kernel’s built-in tool for managing network traffic. It lets you shape queues, rate-limit bandwidth, set priorities, and — most relevant here — rewrite packet headers inside the kernel without touching userspace. Typical use cases include DSCP remarking, VLAN tag manipulation, and IP address rewriting without conntrack.

Header rewriting is handled by the act_pedit action (packet edit). It takes a list of keys — each key describes an offset into the packet and a value to write there. Offsets come in two flavors: static ones, known at configuration time, and typed ones whose actual offset is resolved at runtime based on the packet’s header structure — for example, whether an IP header is present. That distinction is what broke.

One more thing worth noting: act_pedit does not require root. An unprivileged user can create a user namespace, acquire network privileges (CAP_NET_ADMIN) inside it, and load the act_pedit module. That combination — user namespaces plus act_pedit — is the attack path.

HOW THE BUG WORKS

Before tcf_pedit_act() writes anything, it needs to make sure the target memory region is a private copy — one not shared with other processes. Think of a library book: anyone can read it, but if you want to scribble in the margins, the librarian first makes a photocopy so you don’t mark up the original. That’s copy-on-write (COW): before writing, the kernel makes a private copy of the relevant memory pages. This is done by calling skb_ensure_writable() with the byte range that’s about to be modified.

The problem is that this range is computed once, before the key loop begins, using the field tcfp_off_max_hint. That field holds the maximum offset across all static keys — the ones whose offsets are fixed at configuration time. But typed keys resolve their actual offset at runtime, per packet. If that runtime offset falls outside the range that was passed to skb_ensure_writable(), the corresponding memory page was never privately copied. The write goes into the shared page — directly into the page cache.

The fix moves skb_ensure_writable() inside the per-key loop so the writable range is recalculated using each key’s actual offset. It also adds overflow checking to the offset arithmetic and handles negative offsets — such as Ethernet header edits at ingress — through skb_cow().

HOW IT IS EXPLOITED

The attack relies on the page cache being global — a file’s pages in cache are shared among every process that reads it. If an attacker can write into a cached page, the next execve() of that file runs the poisoned in-memory image, not the original on disk. File integrity checks see nothing wrong; the file on disk hasn’t changed.

The mechanics: the attacker opens a socket on the loopback interface, establishes a TCP connection to 127.0.0.1:4445, and configures a tc rule using act_pedit with a typed key whose runtime offset falls outside the COW range. Data is passed through sendfile() — and that choice is deliberate. Unlike the classic read() + write() pair, sendfile moves file pages directly from the page cache into the socket, bypassing userspace entirely. That’s what gives the kernel a direct view of the file’s actual cache pages as they pass through act_pedit. When the packet is processed, the write lands in the source file’s page cache instead of a private copy. An O_RDONLY file descriptor is enough — the file doesn’t need to be open for writing.

The target is any setuid binary readable by the attacker: /usr/bin/su, /usr/bin/passwd, or anything with the setuid bit set. After writing the payload into cache, the attacker runs the binary and gets a root shell — for as long as the poisoned image stays in cache. Dropping the page cache (echo 3 > /proc/sys/vm/drop_caches) clears the poisoned image, but does nothing about a root shell that’s already open.

WHAT HAPPENS NEXT — DEPENDS ON SYSTEM CONFIGURATION

Two conditions determine whether a system is exploitable: whether act_pedit can be loaded, and whether unprivileged user namespaces are open. On RHEL 10 and Debian 13, both are true by default — the exploit runs without any additional flags. On Ubuntu 24.04, AppArmor restricts unprivileged user namespace creation, but researchers confirmed a bypass through existing AppArmor profiles that still allow user namespaces — the attack works, just with a bit more setup. Ubuntu 26.04 blocks that path via its default AppArmor configuration, but the kernel underneath remains vulnerable.

On systems with unprivileged user namespaces disabled (user.max_user_namespaces=0 on RHEL, kernel.unprivileged_userns_clone=0 on Debian/Ubuntu) the public exploit doesn’t work — there’s no way for the attacker to get CAP_NET_ADMIN. That’s the primary workaround for systems that can’t patch immediately. The cost: rootless containers, some CI sandboxes, and sandboxed browsers break. On a production server without rootless Docker or Podman, that’s usually an acceptable trade.

TIMELINE

Late May 2026 — the patch appeared on the netdev mailing list as a data-corruption fix, with no CVE and no security framing. The exploitable technical details sat in a public archive for weeks. May 13 — CVE-2026-46331 was reserved by the kernel CNA. June 16 — merged into mainline, CVE published. June 17 — working PoC on GitHub. Red Hat shipped the first errata (RHSA-2026:27288 for RHEL 10) within days of the CVE going public. Debian released DSA-6355-1 for trixie. AlmaLinux published ALSA-2026:27353 for version 8.

TuxCare wrote in their report: “We did not catch this one through our own monitoring. It came in through a customer escalation — it was never on oss-security, and the kernel CNA stream is not currently on our watchlist.” That’s not a confession from one company — it’s a structural gap in how kernel security gets tracked. Upstream kernel patches frequently carry no security label, and CVE assignment happens at merge time, weeks after the fix is publicly readable. The gap between “patch is visible to anyone” and “security teams are aware” can be measured in weeks.

WHY IT MATTERS

pedit COW is the third page cache corruption LPE in the Linux kernel in 2026 — after Dirty Frag and Copy Fail. All three exploit the same principle: the kernel writes into a shared memory page instead of a private copy, poisoning the in-memory image of an executable. The COW mechanism has been in the kernel from the start, and each time it turns out that somewhere, the range was computed at the wrong time or in the wrong place.

Red Hat rates this as Important — local attack vector, no privilege requirement, no user interaction. In practice that means: an attacker who already has a shell on your server — through a vulnerable web service, a compromised container, any other vector — can use CVE-2026-46331 as a stepping stone to root. That’s especially relevant for CI/CD runners, build workers, shared research machines, and any system where multiple users or automated processes have local access.

There’s one more risk worth calling out: file integrity monitoring won’t catch this attack. The file on disk doesn’t change. AIDE, Tripwire, any FIM that works with on-disk file hashes will report nothing unusual. The poisoned image exists only in memory, until the cache is dropped or the system reboots. That makes the attack unusually clean — no disk artifacts, root access already in hand. For incident response, it means a filesystem forensics pass will tell you nothing about when the compromise happened. You’ll need network logs, auditd syscall records, or process metadata to reconstruct the timeline.

UPDATE

The most reliable path is to install the patched kernel and reboot. The reboot matters: it loads the fixed kernel and drops the page cache simultaneously, clearing any potentially poisoned pages. Before applying the module-blocking workaround, check whether act_pedit is actually in use on your system — if the output is empty, the module isn’t actively loaded:

tc actions list action pedit

Check your current kernel version with uname -r and compare it against the fixed versions for your distribution.

For RHEL 8, the fix is in RHSA-2026:27353; RHEL 8.8 EUS/TUS gets RHSA-2026:27355; RHEL 9 gets RHSA-2026:27789; RHEL 10 gets RHSA-2026:27288. AlmaLinux 8 has ALSA-2026:27353. Exact kernel package versions are listed on each errata page at access.redhat.com. For Debian 13 (trixie), the fix is in DSA-6355-1 — the vulnerable version is 6.12.90+deb13.1, the patched version is 6.12.94-1 or later. Ubuntu has not yet released patched kernels for supported releases — watch Ubuntu Security Notices (USN). The PoC author confirmed exploitation on RHEL 10.0 running 6.12.0-228.el10 and Debian 13 running 6.12.90+deb13.1 — both are vulnerable.

After updating, verify that the new kernel is actually running. On RHEL and AlmaLinux, list installed kernel versions — the active one is marked with an asterisk:

rpm -q kernel

On Debian and Ubuntu, check the running kernel version and confirm the patched package is installed:

uname -r
dpkg -l linux-image-* | grep ^ii
# RHEL / AlmaLinux
dnf update kernel && systemctl reboot

# Debian 13
apt update && apt install linux-image-$(uname -r) && reboot

If patching immediately isn’t possible, two workarounds break the exploit chain. The first: block act_pedit from loading if you don’t use tc pedit rules. Verify it’s not in use with lsmod | grep act_pedit — if the output is empty, block it:

echo 'install act_pedit /bin/true' | sudo tee /etc/modprobe.d/disable-act_pedit.conf

The second: disable unprivileged user namespaces. On Debian and Ubuntu both parameters are needed:

# Debian / Ubuntu
echo 'kernel.unprivileged_userns_clone=0' | sudo tee /etc/sysctl.d/99-userns.conf
echo 'user.max_user_namespaces=0' | sudo tee -a /etc/sysctl.d/99-userns.conf
sudo sysctl -p /etc/sysctl.d/99-userns.conf

On RHEL-family systems, one parameter is enough:

# RHEL / AlmaLinux
echo 'user.max_user_namespaces=0' | sudo tee /etc/sysctl.d/99-userns.conf
sudo sysctl -p /etc/sysctl.d/99-userns.conf

Before applying the second workaround, check that rootless Docker, Podman, or similar tools aren’t in use — they depend on unprivileged user namespaces. On a production server without rootless containers, the restriction is generally safe to apply.

CONCLUSIONS

pedit COW is the third page cache LPE in the Linux kernel in 2026, and the pattern is the same every time: a COW range computed in the wrong place, a write landing in a shared page, a cached binary poisoned in memory. Dirty Frag lived in IPsec. Copy Fail lived in crypto. Now act_pedit in tc. The same mistake in different places points to a systemic problem, not an isolated one.

CVE-2026-46331 arrived from a public patch on netdev with no security signal attached. The PoC dropped the day after the CVE went public. Between “patch readable by anyone” and “most security teams aware of the problem” lay several weeks. If your kernel security monitoring stops at oss-security subscriptions and vendor advisories, you’re getting this information late. The netdev mailing list, kernel git tags, and upstream stable notes are all sources worth tracking.

Patch the kernel, reboot. If your distribution hasn’t shipped a fix yet, block act_pedit or disable unprivileged user namespaces until the update lands.

Leave your thought here

Your email address will not be published. Required fields are marked *