Skip to main content

Copy Fail: 4 Bytes in the Page Cache Break Container Isolation on Every Major Distro

11 min readDora NodaDora Noda
Share
On this page

A 732-byte Python script, run by an unprivileged user, edits a setuid binary it was never allowed to write and walks away with root — on Ubuntu, RHEL, Debian, SUSE, Amazon Linux, and Arch, with no per-distro offsets, no version checks, and no recompilation. That is Copy Fail (CVE-2026-31431), disclosed in April 2026 by the Xint.io Code Research Team and Theori, and two facts make it worse than a routine local privilege escalation: it corrupts the kernel page cache, a host-wide resource shared across every container on the node, and it leaves the bytes on disk untouched, so file-integrity monitoring sees nothing.

If you run unrelated tenants on shared nodes, here is the verdict up front, with the evidence to follow: patch every node kernel now — CISA has it in the Known Exploited Vulnerabilities catalog. As stopgaps, block the AF_ALG socket family and set allowPrivilegeEscalation: false on tenant pods. And understand the uncomfortable part: user namespaces, RBAC, and default seccomp profiles do not stop this exploit. Only patching, or giving each tenant its own kernel, closes it.

The 30-second verdict

Copy Fail is a deterministic, race-free logic bug in the kernel's authencesn cryptographic template, reached through the AF_ALG crypto API (algif_aead) chained with splice(). An unprivileged local user triggers a controlled 4-byte write into the page cache of any readable file, then aims that write at a setuid binary or a shared library mapping and gets root. CVSS 7.8. Every kernel with CONFIG_CRYPTO_AUTHENC — essentially every major distribution shipped since 2017, kernels 4.x through 6.x — is affected until patched. Ubuntu 24.04 is vulnerable; Ubuntu 26.04 ships fixed.

For a multi-tenant fleet, the blast radius is what matters, and it lands in this order:

PriorityActionWhat it buys
1. Patch node kernels and rebootThe only complete fix on a shared kernel
2. Block AF_ALG (modprobe blacklist, seccomp)Removes the exploit's entry point until patched
3. allowPrivilegeEscalation: false on tenant podsno_new_privs stops the kernel honoring setuid bits
4. Read-only root filesystems, scheduling separationNarrows what a successful corruption can reach
5. Per-tenant kernels (Kata, gVisor, microVMs)The only structural fix for the whole bug class

Everything below is the receipts for that table: how the exploit works, why your existing boundaries miss it, the exact commands for each row, and what the last row costs.

How 4 bytes become root

The mechanism is a short chain with no race window to win and no heap grooming to get right. The attacker opens an AF_ALG socket against the authencesn template, drives an AEAD operation over attacker-chosen buffers, and uses splice() to steer the resulting write into the page cache of a target file the attacker can merely read. The write is exactly four bytes, fully controlled, at a chosen offset — deterministic on the first try.

Four bytes sounds too small to matter until you remember what lives in an ELF binary's first pages: entry points, program headers, and the dynamic section that decides which code runs at startup. The published proof of concept overwrites a few bytes of a setuid-root binary's in-memory image so that executing it — something any local user is allowed to do — runs attacker code as root. On a single-user box that is already game over. On a shared node it is worse, for a reason the next section covers.

The next nasty property is universality. Most kernel exploits need per-version structure offsets, which is why a typical local-root PoC ships with a table of kernel builds and fails on anything unlisted. Copy Fail corrupts page-cache contents through a stable API path, so the same 732 bytes work on Ubuntu 24.04, Debian 12, RHEL 10, and Amazon Linux 2023 without modification. The exploit the researchers published is genuinely one script for the entire installed base, which is also why CISA's catalog entry landed within about a week of disclosure with a "patch right now" note: the public exploit is reliable and works on almost all active Linux systems.

The nastiest property is invisibility. The page cache sits between the filesystem and every reader; corrupting a cached page changes what processes execute without changing a single byte on disk. After the attack, the file's hash on storage is pristine, rpm -V and AIDE-style integrity checks pass, and the corruption evaporates on reboot or page eviction — leaving no artifact except whatever the attacker did with root. If your detection story for node compromise starts with "we hash files on disk," this exploit walks past it.

Why your container boundaries don't stop it

Containers are not separate machines; they are processes on one shared kernel, and the page cache is one of the things they share. When two containers — or a container and the host — map the same file, they map the same cached pages. A 4-byte write into those pages from an unprivileged pod is instantly visible to every other consumer of that file on the node, including root-owned processes and other tenants' containers. Namespaces partition PIDs, mounts, and networks. Nothing partitions the page cache, because it was never a security boundary — until April 2026, nobody had a reliable write primitive against it.

Kubernetes then provides the execution context that turns corruption into escape. Container images share base layers across pods on a node, so corrupting a page of a widely-shared file (the published Kubernetes PoC uses /usr/sbin/ipset) poisons every pod mapping that layer. The escape path the researchers demonstrated aims at the kube-proxy DaemonSet: it runs privileged on every node, it is ubiquitous, and its image pull policy means the poisoned layer is the one already on disk. An unprivileged pod corrupts the shared page; the privileged daemonset executes the corrupted bytes; the attacker has node-level code execution. That PoC has been validated on Alibaba Cloud ACK, Amazon EKS, and Google GKE — managed control planes do not help, because the bug is in the node kernel underneath all of them.

This is the point where teams reach for the hardening they already shipped and discover the gap:

Control you may already haveStops Copy Fail?Why not
User namespaces (GA in Kubernetes 1.36)NoRemaps UIDs; the exploit needs no privilege, only a local unprivileged user
RBAC / fine-grained kubelet authorizationNoRestricts API access; the attack never touches the API server
Runtime-default seccomp profileNoDoes not block AF_ALG sockets
Container-only seccomp / SELinux / AppArmorNoThe write lands in host-wide kernel state; container-scoped policy is the wrong layer
Read-only root filesystemPartiallyLimits payload options but the corrupted page is still executed
Disk file-integrity monitoringNoOn-disk bytes never change

Read that table as a statement about bug class, not just this CVE. Anything that treats the kernel as a trust boundary between mutually untrusting tenants — which is exactly what bin-packing unrelated tenants onto shared nodes does — inherits every future page-cache-class bug the same way. Patching fixes this instance; the architecture decides how much the next one costs you.

What to do on your fleet today

Start with the patch, because everything else is a speed bump. The upstream fix is mainline commit a664bf3d603d plus its follow-ups; every major distro has shipped patched kernels, and livepatch services (KernelCare, Canonical Livepatch, kpatch) can cover nodes you cannot reboot immediately. Track patch level per node, not per cluster — one unpatched node is one escape away from a tenant incident. The distro-native checks are the source of truth:

bash
# Debian/Ubuntu: is this node covered for the CVE?
ubuntu-security-status --cves 2>/dev/null | grep -A2 2026-31431
# RHEL/Fedora/Rocky/Alma: is a fixed kernel installed?
dnf updateinfo list cves | grep CVE-2026-31431
# SUSE:
zypper patch-check
# Livepatch state, if you patched without rebooting:
canonical-livepatch status; kpatch list

If the fleet cannot be fully patched today, block the entry point. The exploit needs AF_ALG sockets, which almost no tenant workload legitimately uses — and blocking them is safe for IPsec/XFRM traffic, which goes through the kernel crypto API directly rather than AF_ALG. On the node:

bash
# Refuse to load the vulnerable module (then reboot, or rmmod where unused)
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf
echo "blacklist algif_aead" | sudo tee -a /etc/modprobe.d/disable-algif.conf

And at the container layer, ship a seccomp profile that denies the AF_ALG socket family to tenant pods, alongside allowPrivilegeEscalation: false, which enables no_new_privs and stops the kernel from honoring setuid bits on execve() — cutting the exploit's favorite path from corruption to root even if the write itself still lands:

yaml
securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  seccompProfile:
    type: Localhost
    localhostProfile: tenant-no-afalg.json

The remaining rows are defense in depth, and should be read honestly as such. Read-only root filesystems for privileged containers (kube-proxy included) limit what a corrupted page can reach but do not prevent the corruption. Scheduling separation — keeping untrusted tenant pods off nodes that run privileged daemonsets with shared base images — shrinks the set of high-value execution contexts sharing a page cache with attacker code. Image-layer provenance matters more than it used to: the escape depends on attacker and victim mapping the same layer, so knowing exactly which layers your privileged daemonsets share with tenant images tells you which nodes are one corruption away from escalation. None of these survives contact with an unpatched kernel and a determined tenant; all of them buy time while the patch rolls out.

One operational note for owned-hardware fleets: this is the CVE that justifies treating kernel version as a first-class fleet dimension, the same way you already treat Kubernetes version. If your node provisioning cannot answer "which nodes are still on a kernel older than the Copy Fail fix" in one query, that query is the thing to build this week — the next page-cache-class bug will want the same answer.

What actually closes the hole

Every mitigation above shares one assumption: all tenants share one kernel, and we are trying to make that sharing safe. Copy Fail is the counterexample that keeps working. The only fix that survives the next bug in this class is to stop sharing the kernel between mutually untrusting tenants — one kernel per tenant, so a page-cache write in tenant A's kernel corrupts tenant A's pages and nothing else.

In practice that means a hypervisor-isolated node pool for untrusted workloads: Kata Containers, gVisor, or Firecracker-based microVMs, where each pod (or tenant) gets its own guest kernel or syscall-interposition layer and a container-escape primitive lands in a virtualized kernel, not the host's. The sandbox research community has already run this exact experiment against Copy Fail as a class discriminator: container shares host kernel, escape succeeds; microVM with its own guest kernel or gVisor with Sentry interposition, contained. That is about as clean an architectural verdict as CVEs ever produce.

The honest cost side: per-pod microVMs add measurable memory overhead (a guest kernel per sandbox, typically hundreds of megabytes), cold-start latency versus runc, and a second runtime to operate, monitor, and keep patched — the guest kernel has its own CVE feed. gVisor trades some of that overhead for a compatibility surface: most workloads run unmodified, but syscalls outside Sentry's implemented set fail, and GPU passthrough for agent-sandbox inference workloads is a real limitation. For a small self-hosted fleet, the pragmatic shape is tiered: keep mutually-trusting first-party workloads on runc, and put untrusted multi-tenant workloads — other people's code, agent sandboxes, anything that executes tenant-supplied binaries — on the isolated pool. That is more runtime machinery than a three-node fleet wants to babysit, which is precisely why it should be a deliberate roadmap item with a named owner rather than a "someday" bullet.

The decision rule is simple to state and uncomfortable to apply: if a workload's threat model includes "the tenant is the attacker," a shared kernel is the wrong floor for it, patched or not. Patching resets the clock; only kernel-per-tenant changes the game.

The kernel is shared fate

Copy Fail will be remembered less for its mechanism — a crypto-template logic bug, now fixed — than for what it proved about the architecture most multi-tenant platforms still run: the kernel page cache was always shared, we just never had a reliable write primitive against it, so we planned as if sharing were safe. The 732-byte script ended that. Patch every node, block AF_ALG where you cannot patch yet, track kernel versions like you track Kubernetes versions, and put the tenants you do not trust on kernels they do not share.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex