Notes

Copy Fail and Dirty Frag: A Shared-Fragment Invariant

Copy Fail and Dirty Frag, three Linux CVEs in ten days, share one root cause: in-place crypto fast paths that write over paged fragments they do not own.

Why I wrote this

Three subsystems, same bug, ten days apart. Copy Fail and Dirty Frag are not three independent flaws, they are three instances of an invariant the kernel quietly stopped enforcing across its in-place crypto fast paths. The next one will look like this.

essay Updated July 18, 2026 17 min read

Between April 29 and May 8, 2026, three CVEs landed against the Linux kernel that all read like the same bug.

  • CVE-2026-31431 (Copy Fail). An in-place AEAD optimization in algif_aead lets an unprivileged user write four bytes into the page cache of any readable file by chaining a splice()d pipe page into the destination scatterlist of an authencesn decryption. Disclosed by Theori / Xint. Nine years old.
  • CVE-2026-43284 (xfrm-ESP / Dirty Frag). The IPsec ESP receive fast path decrypts in place over paged skb fragments without verifying that the underlying pages are kernel-owned. Pages attached via MSG_SPLICE_PAGES or sendfile() get plaintext written into them. Disclosed by Hyunwoo Kim. Nine years old.
  • CVE-2026-43500 (RxRPC / Dirty Frag). Same shape, different subsystem: RxRPC’s in-place decrypt path writes over paged fragments that originated as user-controlled pipe pages. Three years old.

Three subsystems, ten days, one bug.

The shared root cause is a single invariant the kernel’s in-place crypto fast paths assume but do not enforce: the paged fragments we are about to write over are kernel-owned. They are not: a splice(2) from a pipe, a sendfile(2) from a file, an MSG_SPLICE_PAGES send all attach page-cache references to a socket buffer’s fragment list. The crypto fast path then maps those pages, decrypts in place over them, and the kernel has just written attacker-controlled bytes into a page that an unprivileged process still holds a reference to. In the case of a setuid binary like /usr/bin/su, the kernel will load that page on the next execve(), without ever flushing the change to disk, because the page is never marked dirty.

This is the pattern essay. The three CVEs are case studies. The disclosures cover how each bug works. The more interesting question is why the same invariant fails in three subsystems at once, and what that says about which other in-place fast paths are next.

The pattern, stated upfront

A surprising amount of the modern Linux kernel’s networking and crypto fast paths use in-place operation: the decrypted (or encrypted) plaintext is written back over the same memory that held the ciphertext. In-place is a real performance win when the buffers are kernel-owned scratch memory, because it eliminates the allocation, the copy, and the cache traffic of a separate destination buffer.

In-place is a memory-safety bug when the buffers are not kernel-owned. The kernel has many ways to attach externally-owned pages to a socket buffer’s fragment list:

  • splice(2) from a pipe whose pages came from the page cache
  • sendfile(2), which uses an in-kernel file-to-file-descriptor transfer path and can feed related zero-copy machinery
  • MSG_SPLICE_PAGES on a sendmsg() call
  • AF_ALG sendmsg() with MSG_SPLICE_PAGES, which delivers pipe pages directly into the crypto API’s scatterlist

These mechanisms can produce externally shared fragments, including pipe pages and file-backed page-cache pages. They do not make every fragment a page-cache page. The exploit selects file-backed pages because changing the cached page of a privileged executable turns the write primitive into something useful. The vulnerable crypto path sees a paged fragment, maps it, and writes in place without first separating storage that may still be shared elsewhere.

If the user controls the plaintext (and in an AEAD with attacker-controlled associated data, they often do), they get a deterministic write into the page cache. If the page they wrote into backs a setuid binary, the next execve() of that binary loads the corrupted bytes from the page cache. Local privilege escalation in 732 bytes of Python.

That is the entire pattern. Now to the three instances.

Background: how externally-owned pages reach the crypto fast path

Three primitives matter.

Scatterlists. The kernel’s crypto API takes input and output as scatter-gather lists (struct scatterlist). A scatterlist is an array of (page, offset, length) tuples. Sources and destinations can each be a chain of scatterlists, and crypto fast paths walk them via scatterwalk_map_and_copy() and friends, which kmap_local_page() each entry to get a kernel-virtual pointer for the read or write.

Socket buffers (sk_buff) and paged fragments. An sk_buff carries packet data partly in a linear header and partly in a paged fragment list (skb_shinfo(skb)->frags). The fragment list lets the kernel zero-copy attach external pages to a packet, a TX path can splice file pages onto an outgoing packet, an RX path can hold pages allocated from a page pool, and certain syscalls let userspace seed the fragment list with its own pages.

Splice-style page passing. splice(2) moves pages between a pipe and a file or socket without copying. sendfile(2) is the same primitive in a different shape. MSG_SPLICE_PAGES is a sendmsg() flag that tells the kernel to attach pipe pages onto the outgoing skb’s fragment list rather than copying their contents. AF_ALG sockets, which expose the kernel crypto API to userspace, accept MSG_SPLICE_PAGES and feed those pipe pages into the crypto request’s source scatterlist.

The combination is what makes the bug class possible. The user opens a pipe, splices a page-cache page into it (e.g., a page from /usr/bin/su’s read-only mapping), then attaches that page into a socket buffer or crypto request via MSG_SPLICE_PAGES. The kernel sees a scatterlist or fragment list with that page in it. The fast path maps it. The fast path writes to it.

The fast path was supposed to check that the page belongs to it. The fast path does not.

Case 1: Copy Fail (CVE-2026-31431)

Disclosed by Theori / Xint on April 29, 2026, with a 732-byte Python proof-of-concept and a CVSS of 7.8. The bug had been in the kernel since 2017.

algif_aead is the AF_ALG implementation for AEAD ciphers. It exposes ciphers like aes-gcm and authencesn(hmac(sha256),cbc(aes)) to unprivileged userspace via a socket interface. In 2017, commit 72548b093ee3 introduced an in-place optimization: when the source data was already in a usable layout, the fast path set req->src = req->dst, sharing one scatterlist between input and output.

For most AEAD ciphers this was harmless. For the authencesn template, used by IPsec for Extended Sequence Number support, it was not. The authencesn decrypt path rearranges 64-bit sequence numbers in place during decryption, using the destination buffer as scratch space. From the disclosure:

It performs this rearrangement by using the caller’s destination buffer as scratch space.

Concretely, crypto_authenc_esn_decrypt() performs three scatterwalk_map_and_copy() operations:

  1. Read AAD bytes 0–7
  2. Overwrite dst[4..7] with seqno_hi
  3. Write seqno_lo at offset assoclen + cryptlen

In the in-place configuration, those writes land on the chained source scatterlist. When the source includes a page-cache page that was splice()d in via AF_ALG, the third write crosses from the user’s output buffer into the chained page-cache page. scatterwalk calls kmap_local_page() and writes four attacker-controlled bytes (taken from AAD bytes 4–7) directly into the kernel’s cached copy of the file. The page is never marked dirty, so the change never hits disk, but every subsequent read() or mmap() of the file sees the modified bytes, and so does the next execve().

The 732-byte PoC targets /usr/bin/su. It opens a pipe, splices su’s .text pages into it, opens an AF_ALG socket bound to authencesn(hmac(sha256),cbc(aes)), and for each four-byte chunk of shellcode constructs a sendmsg() + splice() pair that places the chunk in AAD bytes 4–7 with the corresponding offset into su. A recv() on the socket invokes crypto_authenc_esn_decrypt(), which writes the chunk to the targeted offset in the page cache. After enough chunks compose into a setuid-execve shellcode in su’s .text, the next invocation of su runs as UID 0.

The upstream fix, commit a664bf3d603d, reverts algif_aead to out-of-place operation: req->src is the TX scatterlist, req->dst is the RX buffer, no chaining between them. The maintainers’ note in the patch is the line that matters: there is no benefit in operating in-place in algif_aead since the source and destination come from different mappings. The 2017 optimization saved nothing because the input was always coming from userspace pages and the output was always going to a separate kernel buffer. It was a fast path that wasn’t even fast, it just opened a 4-byte page-cache write primitive for nine years.

Insight

The fast path that wasn’t fast

The most galling thing about Copy Fail is that the in-place optimization didn’t measurably help anything. Source and destination always came from different mappings. The performance argument that justified sharing the scatterlist was wrong from the start, and the security cost was a deterministic page-cache write primitive across every distro shipped since 2017. Performance optimizations that don’t measure the thing they’re optimizing are how the kernel accumulates security debt.

Case 2: xfrm-ESP / Dirty Frag (CVE-2026-43284)

Disclosed by Hyunwoo Kim (@v4bel) on May 7, 2026, after the coordinated embargo was broken by external factors. At the point he published, no patch and no CVE existed yet; the CVE record went up the following day. Scoring is contested: kernel.org, the assigning CNA, rates it 8.8, while CISA and Red Hat both rate it 7.8. Introduced upstream in 2017.

The IPsec ESP receive path, esp4_input and esp6_input for IPv4 and IPv6 respectively, is a hot path. A packet arrives, the kernel walks the SPI to find the matching xfrm_state, allocates a request, and calls into the AEAD crypto API with the skb’s data as both source and destination. In-place decryption.

When the packet arrived through normal network reception, the skb’s paged fragments come from the kernel’s page pool, pages that the kernel allocated, owns, and will free. The in-place decrypt is safe.

When the packet arrived through a path that lets userspace seed the fragment list, and MSG_SPLICE_PAGES does exactly that, the paged fragments are page-cache or pipe pages. The fast path doesn’t check. It calls into scatterwalk over skb_shinfo(skb)->frags, maps each page, and writes the decrypted plaintext.

The exploitation primitive is the same as Copy Fail: a 4-byte STORE into the page cache of any readable file, controlled by what the user puts in the ciphertext + AAD. The mechanics are different, IPsec ESP rather than algif_aead, but the bug class is identical, and so is the consequence.

The chain to root: an attacker sets up an xfrm_state (which requires a network namespace they control, more on that under CVE-2026-43500), builds a crafted ESP packet whose paged fragments are spliced page-cache pages of /usr/bin/su, and sends the packet to themselves through a loopback or veth interface. The kernel’s RX path decrypts in place, the plaintext lands in su’s page cache, the next execve("/usr/bin/su") runs the corrupted bytes.

The patch is the same shape as Copy Fail’s: introduce a check that paged fragments are kernel-owned before allowing in-place decrypt, and fall back to a copy when they are not. The performance cost of the check is negligible. The cost of not having it was nine years of root.

Case 3: RxRPC / Dirty Frag (CVE-2026-43500)

Same shape, different subsystem, introduced in 2023 rather than 2017. RxRPC is the kernel’s implementation of the AFS distributed filesystem RPC protocol, a subsystem most users never knowingly touch. It is packaged by most distros but not loaded by default everywhere; RHEL omits the module entirely. That detail turns out to matter for the exploit chain.

The vulnerable path is RxRPC’s in-place decrypt of incoming jumbo packets. Same skb fragment list, same in-place crypto API call, same missing page-ownership check. The primitive is a little wider than Copy Fail’s: rxkad_verify_packet_1() performs an in-place single-block decryption over the first eight bytes of the packet payload, so this one lands an eight-byte write rather than four.

In the public exploit, the two primitives are not sequential steps where one unlocks the other. They are redundant alternatives, and each one covers the distros where the other fails.

The xfrm-ESP path needs CAP_NET_ADMIN to stand up an xfrm_state, which an unprivileged user obtains by entering a user namespace. Ubuntu restricts exactly that, via AppArmor profiles introduced in 23.10 and enabled by default from 24.04, so on a current Ubuntu the ESP primitive is out of reach. The RxRPC path needs no user namespace at all, but it does need rxrpc.ko present and loaded, and that is where the distros diverge in the other direction: Ubuntu loads the module by default, and RHEL’s default build does not ship it.

So each primitive fails on roughly the population where the other works. Ubuntu blocks the user namespace but hands you RxRPC; RHEL leaves user namespaces alone but never loads RxRPC. Shipping both primitives is what makes the result universal rather than conditional, and it means a fleet that hardened user-namespace creation and stopped there has bought nothing.

The patch is, again, the page-ownership check on paged fragments before in-place decrypt.

The shared invariant

The invariant is easier to state than to implement: do not decrypt in place over shared paged fragments. The actual RxRPC fix checks the skb properties that expose this state and unshares before entering the security operation. In simplified pseudocode:

if (skb_cloned(skb) || skb_has_frag_list(skb) || skb_has_shared_frag(skb))
    skb = unshare_or_linearize(skb);

That is a model of the decision, not compilable kernel code. The named helpers and error handling vary by subsystem. The review question is whether every path that may write in place first establishes exclusive ownership of the storage it will modify.

The set of fast paths that might operate in place over paged fragments includes, at minimum, the ESP send path (the TX side of CVE-2026-43284), the AH integrity path, RxRPC’s encrypt path, KTLS’s crypto offload path, MACsec, TIPC’s authentication, and any AF_ALG mode that did not get reverted in the Copy Fail patch. Three siblings in ten days looks like the start of this bug class, not the end.

Warning

The audit surface is large and the invariant is invisible

A page does not carry a universal “safe for this subsystem to overwrite” flag. The disclosed fixes reason from skb sharing state and from the way a particular path acquired its fragments. Every in-place caller still has to establish the property it needs at its own boundary.

Why these lasted as long as they did

Copy Fail was nine years old. xfrm-ESP / Dirty Frag was nine years old. RxRPC / Dirty Frag was three years old. None of them were exotic, splice() into AF_ALG, MSG_SPLICE_PAGES into a socket, and yet none were caught by the existing audit infrastructure.

The reasons are familiar from Dirty COW, and they generalize:

  • The bug is invisible to fuzzers that exercise one subsystem at a time. AF_ALG fuzzing exercises the crypto API. Page-cache fuzzing exercises the mm subsystem. The bug only manifests when you compose them, splice page-cache pages into a crypto request, and most fuzzing harnesses do not compose subsystems that way.
  • The invariant is implicit. “Paged fragments must be kernel-owned” is not written down anywhere in the kernel source as a constraint that fast paths must respect. It is an assumption. Assumptions do not get checked by tooling.
  • The performance optimizations are sticky. In-place AEAD looked like a win in 2017. It was committed, it shipped, it accumulated dependent code. By the time anyone reviewed it adversarially, reverting it was a non-trivial change. Most of the time, no one reviews it adversarially.
  • The reachability gates moved. When algif_aead was written, AF_ALG was considered a relatively obscure interface. When MSG_SPLICE_PAGES was added, it expanded the reachability of every in-place path that already shipped, without anyone going back to re-audit them under the new threat model.

The fourth point is the one that generalizes worst. Every time the kernel adds a new way for userspace to attach pages to a socket buffer, MSG_ZEROCOPY, MSG_SPLICE_PAGES, future iterations, the reachability of every in-place fast path that already shipped expands. The audit work to confirm “still safe under the new attachment primitive” rarely happens, because the team that adds the new primitive is not the team that maintains the old fast path.

The same compounding shape that makes security platforms collapse under operational complexity makes the kernel’s audit posture decay over time. Invariants that were true under the original threat model quietly stop being true as new primitives are added, and nobody is paid to notice.

Detection

Patch inventory is the dependable control. Behavioral detection is possible, but it requires correlation that a pair of standalone Wazuh rules does not provide.

The earlier version of this article showed if_sid rules as if they correlated a non-root AF_ALG socket, a later bind to authencesn, and splice activity over time. They did not. if_sid relates decoded rule matches within Wazuh’s rule evaluation; it is not a general temporal join across syscalls and process identity. Audit records also do not reliably contain friendly strings such as family=38 unless the decoder or collection layer adds them.

A serious detector needs process-aware telemetry and a tested correlation window. For Copy Fail, the useful sequence includes an unprivileged process creating an AF_ALG socket, binding an affected AEAD template, moving file-backed data through a pipe, and repeatedly invoking the crypto operation before executing a privileged target. Dirty Frag has different setup paths through XFRM or RxRPC. Common uses of splice() alone are far too broad to alert at high severity.

An eBPF sensor or a purpose-built audit pipeline can retain PID, UID, namespace, socket family, bind name, file inode, and timing, then correlate the sequence. Validate it by replaying a known proof of concept in an isolated vulnerable VM and by measuring false positives from legitimate AF_ALG, IPsec, AFS, container, and file-transfer workloads.

Traditional file-integrity monitoring also has a blind spot. Reading a file normally may return the already modified page-cache contents, so comparing two ordinary reads does not give an independent disk reference. A trustworthy check needs known-good package content or another verified source, plus an acquisition method designed for the page-cache threat. Do not recommend dropping production caches as a generic detector; it is disruptive and can destroy the transient evidence you wanted to inspect.

Remediation

Patches first

All three CVEs have upstream kernel fixes shipped to mainline before public disclosure:

  • CVE-2026-31431 (Copy Fail): kernel commit a664bf3d603d, reverting algif_aead to out-of-place operation. Backported across the maintained stable branches.
  • CVE-2026-43284 (xfrm-ESP): patch series introducing page-ownership check in the ESP receive path. Backported across maintained kernel lines.
  • CVE-2026-43500 (RxRPC): parallel patch series, same shape, in the RxRPC receive path.

Use the distribution’s advisory and kernel package, then reboot into the fixed kernel. Generic commands cannot prove which backport a vendor shipped, but these begin the update on common systems:

# Ubuntu / Debian (install the distribution's current kernel metapackage)
sudo apt update
sudo apt full-upgrade
sudo reboot

# RHEL / Fedora
sudo dnf upgrade kernel
sudo reboot

# SUSE
sudo zypper patch
sudo reboot

Emergency mitigations if you cannot patch immediately

If patching must wait, first determine whether the affected functionality is modular, built in, currently loaded, and actually needed:

for module in algif_aead esp4 esp6 rxrpc; do
  printf '%-12s built=%s loaded=%s\n' \
    "$module" \
    "$(modinfo -n "$module" 2>/dev/null || printf 'unknown-or-built-in')" \
    "$(test -d "/sys/module/$module" && printf yes || printf no)"
done

For unused loadable modules, an install rule prevents future automatic loading more reliably than blacklist alone:

sudo tee /etc/modprobe.d/page-frag-cve-mitigation.conf >/dev/null <<'EOF'
install algif_aead /bin/false
install esp4 /bin/false
install esp6 /bin/false
install rxrpc /bin/false
EOF
sudo reboot

This can break userspace AF_ALG AEAD, IPsec, and AFS/RxRPC. It does nothing to code compiled into the kernel and does not unload an active module. Do not hide rmmod failures or force-remove a live networking or crypto module. Confirm the vendor’s documented mitigation, schedule a reboot, and verify /sys/module afterward. A patched kernel remains the preferred resolution.

Container sandboxes can also deny creation of AF_ALG sockets and unnecessary namespace or network-administration capabilities. Treat that as defense in depth. The exact Dirty Frag reachability depends on the kernel, modules, namespaces, and container policy, so a generic seccomp slogan is not a substitute for the fixed kernel.

Longer-term posture

The pattern says the next instance of this bug class will be the next in-place crypto fast path that someone audits, or that someone exploits, depending who gets there first. The defensive moves that pay forward:

  • Audit every in-place fast path you maintain for page-ownership checks. If you ship a kernel module that walks skb_shinfo(skb)->frags and writes to mapped pages, you are in scope.
  • Default to out-of-place when ownership is uncertain. In-place operation can be a material performance win on a hot path. New fast paths should earn it with measurements and an explicit proof that the destination is exclusively writable.
  • Treat new userspace page-attachment primitives as audit triggers. When the kernel adds the next MSG_* flag that lets userspace seed paged fragments, every existing in-place fast path becomes implicitly more reachable. The reachability change should trigger a re-audit. Today, it does not.
  • Give integrity checks an independent reference. Package manifests, verified artifacts, and measured boot can provide evidence that does not depend on rereading the same suspect cache state.

This is the same class of work the red-team-findings → engineering-fixes loop is built to absorb: a finding lands, the structural lesson is extracted, and the audit work that prevents the next sibling becomes scheduled engineering, not heroic incident response.

Audit hypotheses, not predictions

Three related disclosures justify a broader audit. They do not prove that the same invariant is currently missing from every other crypto or networking path. The following are review candidates because they combine in-place operations, skb fragments, or complex ownership transitions. They are hypotheses, not vulnerability claims:

  • KTLS receive and transmit ownership transitions
  • MACsec receive paths
  • TIPC authentication paths
  • ESP transmit paths
  • AF_ALG modes outside the reverted algif_aead path

The review task is to trace every possible fragment origin and show where exclusive writability is established. A path belongs on this list because it deserves that proof, not because this article has established a flaw.

Primary sources