A kernel packet-editor that promised to make memory writable forgot to count the bytes — and the leftover writes land on cached copies of root binaries
tcf_pedit_act() in net/sched/act_pedit.c precomputes a single COW range using tcfp_off_max_hint and calls skb_ensure_writable() once before the per-key write loop. The hint doesn't include the runtime header offset that *typed* keys add at execution time, so a crafted pedit action writes past the COW'd region into the shared page cache. The end state: an attacker overwrites the in-memory page of a setuid binary (the public exploit targets /bin/su), and the next exec gives them root — all without ever touching the on-disk file. Affected: mainline Linux through June 16 2026, including kernels shipped on RHEL 10 (6.12.0-228.el10), RHEL 9 / 8 backports, Debian 13 (6.12.90+deb13.1), and Ubuntu 24.04.4 (6.17.0-22).
There is no NVD CVSS yet; Red Hat called it *Important*, which is the right neighborhood but underplays the operational reality. A working, author-signed PoC (Massimiliano Oldani, sgkdev/packet_edit_meme) landed within 24 hours of disclosure. It's a clean, deterministic LPE on every default-configured RHEL and Debian box with unprivileged user namespaces — which is most of them. Local-only is the only thing keeping this out of CRITICAL.
5 steps from start to impact.
Land local code execution as any unprivileged user
- any local user account or RCE foothold
- pure RCE-as-www-data on a hardened jump box may not have a writable tmp to drop the exploit
Enter a user namespace to acquire namespace-local CAP_NET_ADMIN
unshare(CLONE_NEWUSER|CLONE_NEWNET) to get CAP_NET_ADMIN inside the new netns. This is the gate: it requires kernel.unprivileged_userns_clone=1 (default on RHEL 10, Debian, mainstream distros). Ubuntu 24.04 layers an AppArmor profile that the public PoC already bypasses.unprivileged_userns_cloneenabled- no seccomp filter blocking
unshare/clone3
- hardened images (CIS L2, Bottlerocket, Talos) often disable unprivileged user namespaces
- containers with default Docker seccomp profile cannot
unshare(CLONE_NEWUSER)from inside the container
SYSCALL=unshare with CLONE_NEWUSER from non-root is a high-signal event most shops do not logConfigure a tc qdisc with a malicious pedit action
tc qdisc add + tc filter add action pedit with a typed key whose runtime header offset pushes the write past the precomputed COW boundary. act_pedit autoloads on first use.act_peditkernel module present (default in all major distro kernels)- no module-loading lockdown
modules_disabled=1or a strict module signing + denylist foract_peditblocks autoload- kernel-lockdown integrity mode does NOT block this — pedit is signed
act_pedit from a user-namespace context are anomalousTrigger the partial-COW write to corrupt a cached setuid binary page
/bin/su into the page cache, sends a crafted packet through the tc datapath, and the unbounded write lands on the cached page. Because the skb shares memory with the page cache for that read, the binary's in-memory image is rewritten with an exec-shellcode equivalent. The on-disk file is unchanged — AIDE/Tripwire see nothing.- a setuid-root binary cached in memory (su, sudo, pkexec, mount — pick any)
- SELinux in enforcing mode does NOT stop the write itself (page cache is below the LSM)
- tmpfs-only roots or read-only rootfs with
dm-verity(ChromeOS, Bottlerocket, Talos) defeat the cached-binary pivot
Execute the poisoned binary, get UID 0
su (or whichever target was poisoned). The kernel maps the corrupted page, jumps to the shellcode running as root, and returns a clean root shell. End state: full local privilege escalation, persistence trivially achievable.- the poisoned binary is executable by the attacker
- IMA-appraisal in enforce mode revalidates pages on exec and refuses — but almost nobody runs IMA-appraisal in prod
su → root child process tree; this is the best post-exploit signalThe supporting signals.
| In-the-wild status | No confirmed mass exploitation as of 2026-06-27, but a working public PoC has been live for 10 days. Treat as imminent. |
|---|---|
| Proof-of-concept | sgkdev/packet_edit_meme by Massimiliano Oldani, published 2026-06-17 — deterministic, ships ready-to-run. |
| EPSS | 0.00291 (low percentile) — EPSS lags local-LPE bugs badly; ignore it for prioritization. |
| CISA KEV | Not listed as of 2026-06-27. |
| CVSS vector | No NVD vector published. Realistic vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H → ~8.4 (scope-change because the page cache is shared across users). |
| Affected versions | Mainline Linux through 2026-06-16; RHEL 10 6.12.0-228.el10, RHEL 9 / 8 backport kernels, Debian 13 6.12.90+deb13.1, Ubuntu 24.04.4 6.17.0-22. Any kernel with act_pedit and unprivileged user namespaces. |
| Fixed versions | RHEL 10: RHSA-2026:27288, RHEL 9: RHSA-2026:27789, RHEL 8 / AlmaLinux 8: RHSA-2026:27353 / ALSA-2026:27353, Debian 13: DSA-6355-1 (2026-06-21), Ubuntu USN pending, mainline + stable backports merged 2026-06-16. |
| Exposure data | Not an internet-reachable bug — GreyNoise/Shodan irrelevant. Population at risk = every Linux host with unprivileged_userns_clone=1 and act_pedit loadable, i.e. *the majority of enterprise Linux*. |
| Disclosure | 2026-06-16 via the Linux kernel security tree commit + linux-cve-announce. |
| Reporter / fix | Patch authored by net/sched maintainers; PoC and write-up by Massimiliano Oldani. |
noisgate verdict.
This is a local privilege escalation with a public, turnkey PoC that converts any unprivileged shell into root on default-configured RHEL, Debian, and Ubuntu kernels. The single decisive factor is local-only attack vector — if it were remote it would be CRITICAL; because every chain starts from an existing foothold, HIGH is the correct bucket.
Why this verdict
- Public weaponized PoC within 24h of disclosure.
sgkdev/packet_edit_memeis a one-shot LPE, not a crash repro. The bar to use it isgit clone && make && ./exploit. - Default-configured exploit surface. RHEL 10 and Debian 13 ship with
unprivileged_userns_clone=1andact_peditautoloadable — no hardening checkbox to flip first. This is not a niche feature. - Friction adjustment — local only. AV:L is the one and only thing pulling this off CRITICAL. The attacker must already be on the box (RCE, stolen creds, malicious insider, CI runner). That's a real precondition and it puts the verdict at HIGH, not 10.0.
- Friction adjustment — container default-deny. Docker/Kubernetes default seccomp blocks
unshare(CLONE_NEWUSER)from inside a container, so the typical webapp-RCE-in-a-pod path does NOT auto-chain. Shared-tenancy bare-metal and VM hosts have no such gate. - Role multiplier — multi-tenant Linux (shared dev boxes, jump hosts, bastions, JupyterHub, HPC login nodes, university shells): chain succeeds, blast radius = full host compromise of a host that holds many users' credentials and SSH keys → lateral pivot to dozens of downstream systems. Floor = HIGH.
- Role multiplier — container hosts / Kubernetes worker nodes: if the attacker is *on the node* (not in a pod), chain succeeds and yields node-root → kubelet credentials → cluster-wide pivot. This is a high-value role but reaching the node requires a prior container escape, so it sets the ceiling at HIGH not CRITICAL.
- Role multiplier — single-tenant app servers and developer workstations: chain succeeds; impact is local root on one box. Still HIGH because of pivot potential to credentials cached on that host (cloud SDK keys, kubeconfig, browser cookies).
- No high-value role canonically owned by
act_pedit. Unlike a hypervisor or domain controller bug, this is a kernel LPE — the role multiplier doesn't push to a CRITICAL floor because the affected *component* isn't a fleet-control plane on its own.
Why not higher?
CRITICAL requires either remote exploitability or a chain that ends in fleet-scale compromise from a single act. This bug requires a pre-existing local foothold and ends in single-host root. The realistic CVSS tops out around 8.4 with scope-change accounting for page-cache cross-user impact — that's the top of HIGH, not CRITICAL.
Why not lower?
MEDIUM is wrong because there is a public, deterministic, weaponized PoC against default distro configurations and the chain reliably ends in root. EPSS of 0.00291 reflects scanner blindness to LPE bugs, not actual risk. Any environment with shared Linux hosts, CI runners, or untrusted local code (i.e. almost every enterprise) must treat this as urgent.
What to do — in priority order.
- Disable unprivileged user namespaces fleet-wide — Set
kernel.unprivileged_userns_clone=0(Debian/Ubuntu) oruser.max_user_namespaces=0(RHEL) via sysctl drop-in. Kills the namespace-local CAP_NET_ADMIN path that gates the exploit. Deploy within 3 days per noisgate mitigation SLA; verify with Ansible/Salt that the sysctl sticks across reboot. Caveat: breaks rootless Podman, Chrome sandbox, Flatpak — test on a workstation pilot ring first. - Blocklist the
act_peditkernel module — Dropinstall act_pedit /bin/falseinto/etc/modprobe.d/noisgate-cve-2026-46331.confandrmmod act_peditif already loaded. No legitimate workload outside of advanced QoS shaping uses pedit. Deploy within 3 days; verify withmodprobe -n -v act_peditshowing the install line. - Apply the vendor kernel patch and reboot — Stage RHSA-2026:27288 / 27789 / 27353, DSA-6355-1, and the corresponding Ubuntu USN through your normal kernel-rollout pipeline. Remediation deadline is 90 days per noisgate remediation SLA, but for any multi-tenant host (bastion, jump, dev shell, CI runner) accelerate to 14 days.
- Enable kpatch / kernel live patching where available — RHEL kpatch and Ubuntu Livepatch both ship this fix as a no-reboot patch. Best path for hosts you cannot reboot inside the 3-day mitigation window.
- Hunt for prior abuse in audit logs — Search auditd for
unshare(CLONE_NEWUSER|CLONE_NEWNET)followed bytc filter addfrom non-root UIDs in the last 30 days. Any hit on a production host is incident-response material.
- SELinux / AppArmor enforcing — the partial-COW write happens below the LSM hook; the kernel sees a legitimate skb write. Ubuntu's AppArmor profile narrows the exploit surface but the public PoC already bypasses it.
- File integrity monitoring (AIDE, Tripwire, OSSEC FIM) — the on-disk binary is never modified. Only the cached page is poisoned. FIM will not alert.
- Network controls (firewalls, segmentation, WAF, IDS) — this is a local bug. No packet ever leaves the host.
- Disabling root login or
su— the exploit picks any setuid-root binary cached in memory.sudo,pkexec,mount,passwdall work equally well as targets. - EDR signatures for the PoC binary name — recompile breaks the hash; the underlying primitive is generic.
Crowdsourced verification payload.
Run on each target Linux host as root (or via your config-mgmt agent). Example: sudo ./check-cve-2026-46331.sh. The script checks running kernel version against the patched kernel for the detected distro and reports VULNERABLE / PATCHED / UNKNOWN.
#!/usr/bin/env bash
# noisgate verification: CVE-2026-46331 (net/sched pedit partial COW)
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
KVER="$(uname -r)"
DISTRO="unknown"
if [ -r /etc/os-release ]; then
. /etc/os-release
DISTRO="${ID:-unknown}"
DVER="${VERSION_ID:-}"
fi
echo "[noisgate] kernel=$KVER distro=$DISTRO $DVER"
# Helper: compare dotted versions; returns 0 if $1 >= $2
ver_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | tail -1)" = "$1" ]; }
verdict="UNKNOWN"
reason="distro/kernel not in known-patched table"
case "$DISTRO" in
rhel|centos|almalinux|rocky)
# RHEL 10: fixed in RHSA-2026:27288 (>= 6.12.0-228.4.el10 or rebuild post-fix)
# RHEL 9: fixed in RHSA-2026:27789
# RHEL 8: fixed in RHSA-2026:27353
if echo "$KVER" | grep -q 'el10'; then
FIXED="6.12.0-228.4.el10"
elif echo "$KVER" | grep -q 'el9'; then
FIXED="5.14.0-580.el9"
elif echo "$KVER" | grep -q 'el8'; then
FIXED="4.18.0-620.el8"
else FIXED=""; fi
if [ -n "$FIXED" ]; then
RUN="$(echo "$KVER" | sed 's/\.\(x86_64\|aarch64\|ppc64le\|s390x\)$//')"
if ver_ge "$RUN" "$FIXED"; then verdict="PATCHED"; reason="$RUN >= $FIXED"; else verdict="VULNERABLE"; reason="$RUN < $FIXED"; fi
fi ;;
debian)
# Debian 13: DSA-6355-1, fixed kernel 6.12.90+deb13.2 or later
FIXED="6.12.90+deb13.2"
if ver_ge "$KVER" "$FIXED"; then verdict="PATCHED"; reason="$KVER >= $FIXED"; else verdict="VULNERABLE"; reason="$KVER < $FIXED"; fi ;;
ubuntu)
# Ubuntu 24.04: USN pending; treat anything < 6.17.0-23 as VULNERABLE
FIXED="6.17.0-23"
if ver_ge "$KVER" "$FIXED"; then verdict="PATCHED"; reason="$KVER >= $FIXED"; else verdict="VULNERABLE"; reason="$KVER < $FIXED"; fi ;;
esac
# Mitigation indicators (downgrade VULNERABLE -> MITIGATED-style note)
MITIGATED=0
USERNS="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || echo unknown)"
MAXNS="$(sysctl -n user.max_user_namespaces 2>/dev/null || echo unknown)"
if [ "$USERNS" = "0" ] || [ "$MAXNS" = "0" ]; then MITIGATED=1; fi
if grep -qsE '^(install|blacklist)\s+act_pedit' /etc/modprobe.d/*.conf 2>/dev/null; then MITIGATED=1; fi
echo "[noisgate] verdict=$verdict ($reason)"
echo "[noisgate] unprivileged_userns_clone=$USERNS max_user_namespaces=$MAXNS act_pedit_blocklisted=$( [ $MITIGATED -eq 1 ] && echo yes || echo no )"
case "$verdict" in
PATCHED) exit 0 ;;
VULNERABLE) [ $MITIGATED -eq 1 ] && echo '[noisgate] NOTE: kernel vulnerable but compensating control present'; exit 1 ;;
*) exit 2 ;;
esac
If you remember one thing.
kernel.unprivileged_userns_clone=0 and the act_pedit modprobe blocklist to every Linux host via your config-management plane — that's the noisgate mitigation SLA of 3 days for a HIGH-with-public-PoC bug, and you can move it that fast because both controls are sysctl/modprobe drop-ins. Stage RHSA-2026:27288/27789/27353, DSA-6355-1, and the Ubuntu USN into your kernel-rollout pipeline behind the noisgate remediation SLA of 90 days, but compress that to 14 days on every multi-tenant host (bastions, jump boxes, CI runners, dev shells, JupyterHub, HPC login nodes) where the LPE blast radius is worst. Use kpatch / Livepatch for the hosts you cannot reboot. Ignore EPSS on this one — it does not see local-LPE chains; the PoC is public and works out of the box.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.