Six Weeks After Copy Fail: Analysis and What Comes Next
Six Weeks After Copy Fail — Analysis and What Comes Next
This post collects my analysis of Copy Fail and the vulnerabilities derived from it, work done as part of n4c activity, along with the thoughts that came out of it.
In the six weeks following the disclosure of Copy Fail on April 29, 2026, five local privilege escalation vulnerabilities that corrupt the Linux kernel page cache were published. Four of them fall into the same category, and on two occasions the patch that fixed an earlier vulnerability became a precondition for the next one.
Nine days passed between the disclosure of DirtyFrag and Fragnesia, which bypassed its patch, and another eight days to DirtyClone. Variants bypassing a patch were being published before that patch had even reached distributions.
Parts 1 and 2 cover what was confirmed during those six weeks. Parts 3 and 4 discuss what researchers and defenders should each prepare for if this pattern continues.
Parts 1 and 2 are based on published material and commits. Parts 3 and 4 are not verified conclusions but my personal views. I am still learning, so there may be gaps. If you have a different view or find something incorrect, I would appreciate hearing about it.
Part 1. What happened over six weeks
The root cause of Copy Fail
Copy Fail (CVE-2026-31431), disclosed on April 29, obtains root on major distributions using a 732-byte Python script. Affected versions span kernel 4.14 through 7.0-rc.
The exploitation conditions are as follows. No per-distribution offsets are required, no recompilation is needed, and because it is not a race condition, no repeated attempts are necessary. A failed attempt does not bring the system down.
Looking at the root cause, no individual commit is at fault. It is the result of three changes introduced at different times combining.
2011 — the authencesn AEAD template is added to support IPsec Extended Sequence Numbers. Rather than allocating a separate temporary buffer, it uses the caller-supplied destination buffer as scratch space. This was not a problem at the time. The only caller was the kernel's internal xfrm layer, and the buffer was an skb the kernel had allocated itself.
2015 — algif_aead exposes AEAD operations to userspace through AF_ALG. This was still safe. Because the input and output scatterlists were separate in an out-of-place structure, page cache pages delivered via splice() existed only on the read-only source side.
2017 — AEAD is switched to in-place operation for performance (72548b093ee3). Instead of copying the authentication tag region, it is linked to the end of the output scatterlist by reference using sg_chain(), followed by req->src = req->dst. With this change, a page cache page delivered by splice() ends up inside a writable destination.
When the three changes combine, the following path is created.
read-only setuid binary
↓ splice()
page cache page
↓ AF_ALG
crypto scatterlist — chained to end of destination via sg_chain
↓ authencesn scratch write
4 bytes written into the file's cached copy
HMAC verification fails and recvmsg() returns EBADMSG. The four bytes already written, however, are not rolled back. Because the target file, the offset, and the value written are all under the attacker's control, a setuid binary such as /usr/bin/su can be modified.
This vulnerability does not change the file on disk. The kernel does not mark the page dirty, so no writeback occurs, mtime does not change, and because the VFS write path is not taken, no log entry is produced. What is actually read at execution time is the page cache. CISA added the vulnerability to its KEV catalog on May 1, two days after disclosure.
Reviewed independently, none of the three commits reveals a problem. The vulnerability arose at the point where the conditions each commit assumed no longer held together.
When a patch created the next vulnerability
Around the time Copy Fail was disclosed, two separate vulnerabilities had already been reported to kernel maintainers with patches submitted to netdev. The six-week sequence that followed is below.
| Date |
Name |
CVE |
Location |
| 4/29 |
Copy Fail |
2026-31431 |
crypto / AF_ALG |
| 5/7 |
DirtyFrag |
2026-43284, 43500 |
xfrm-ESP, RxRPC |
| 5/13 |
Fragnesia |
2026-46300 |
XFRM ESP-in-TCP |
| 5/21 |
DirtyClone |
2026-43503 |
skb clone |
| 6/16–17 |
pedit COW |
2026-46331 |
net/sched |
Copy Fail and DirtyFrag are not in a patch-bypass relationship. One occurs in a crypto scatterlist, the other in network skb fragments. What the two share is a problem one level above: for performance, a reference to file-backed memory is handed to another subsystem, and the receiving side has no information that the memory belongs to a file.
The direct bypass lineage begins after DirtyFrag. The fix merged to mainline on May 4 (f4c50a4034e6) sets the SKBFL_SHARED_FRAG flag on spliced UDP packets, and makes code performing in-place decryption run COW first when it sees that flag.
Nine days later it was confirmed that skb_try_coalesce() does not carry this flag over when merging two packets. This is Fragnesia. The follow-up patch names DirtyFrag's fix commit f4c50a4034e6 in its Fixes: tag. The primitive also expanded from a fixed four bytes to an arbitrary-length write.
Eight days after that, the same problem was confirmed in __pskb_copy_fclone(). This is DirtyClone. A broad patch fixing the remaining helpers had already been submitted, but the issue was independently rediscovered before it was merged.
When an existing flag became a security boundary
The Fixes: tag on the Fragnesia patch names two commits. One is DirtyFrag's fix commit f4c50a4034e6; the other is the 2013 commit cef401de7be8.
This flag had existed since 2013. Its original purpose was not security but checksum correctness. Fragments arriving via vmsplice() or sendfile() could change contents during transmission, so the flag marked that they must be copied first if a TX checksum was going to be computed. If it was wrong, the worst outcome was an incorrect checksum.
What the DirtyFrag patch did was not create the flag, but begin using a flag that had served correctness purposes for thirteen years as the basis for a security decision.
The flag is also structured so that the unsafe side is the default when it is omitted. There are dozens of helpers in the kernel that copy, merge, or split skbs, and if the code to carry the flag over is not written, the value is simply 0. The compiler does not check it and the type system does not enforce it, so omitting it produces no warning.
Fragnesia and DirtyClone are cases confirmed in one such helper each. The May 16 broad patch mentioned earlier covered __pskb_copy_fclone(), skb_shift(), skb_segment(), and GRO-related functions together.
Part 2. Why it went undiscovered for nine years, and why it was found now
It was a type that automated tooling cannot detect
AF_ALG is a surface syzkaller has tested for a long time. algif_aead is likewise an area where syzbot reports appeared regularly. Even so, it went undiscovered for nine years.
The reason is that it is not a memory safety bug.
What KASAN detects is invalid memory access. But the target Copy Fail writes to is a valid page, properly mapped, with a correctly maintained reference count. It is merely a page that must not be written to; the access itself is legitimate.
From a sanitizer's perspective, therefore, there is nothing to detect. There is no crash, no report, and from a fuzzer's point of view it is an execution that returned an error code and terminated normally.
This bug does not crash, leaves no trace, and is correct when viewed at the level of individual code units. Most automated tooling currently in use depends on one of those three.
It was found in about an hour with three sentences of context
The discovery process has been published.
According to Theori's public writeup, the AF_ALG attack surface had already been mapped through earlier kernelCTF work. In that process it was established that combining AF_ALG with splice() lets an unprivileged user deliver page cache pages into the crypto subsystem, and a hypothesis was formed that the provenance of pages placed into a scatterlist was an insufficiently examined area.
The context provided to the automated analysis tool amounted to three sentences.
This is the linux crypto/ subsystem. Please examine all codepaths reachable from userspace syscalls. Note one key observation: splice() can deliver page-cache references of read-only files (including setuid binaries) to crypto TX scatterlists.
The scan completed roughly an hour later, and Copy Fail was the highest-severity output. The same scan surfaced additional high-severity vulnerabilities, including another privilege escalation bug, which are reportedly still in the responsible disclosure process.
This approach does not detect crashes; it evaluates the meaning of code. Who owns a given piece of memory is not a property observable through execution but one that must be inferred by reading code.
The Big Sleep research from Project Zero and DeepMind covers the same ground. Current LLMs are better suited to variant analysis, which starts from an already-discovered bug, than to fully open-ended search. The SQLite stack buffer underflow Big Sleep found was likewise missed by both OSS-Fuzz and the project's own fuzzing infrastructure.
Part 3. Changes I expect going forward
From here on this is not confirmed fact but my own speculation. That said, I find it difficult to treat these six weeks as an exceptional event.
What was replaced is search, not hypothesis formation
I do not think it is accurate to summarize Copy Fail's discovery as AI finding vulnerabilities. The three sentences supplied to the automated tool could only be written by someone who already understood the interaction between AF_ALG and splice(). Without those sentences, it is unknown whether Copy Fail would have surfaced as the highest-severity finding in the same scan.
What changed is that the cost of validating a single hypothesis across an entire subsystem dropped from days to about an hour. What that change leads to is the subject of the rest of this post.
One more point: a fix like DirtyFrag's, which assigns new responsibility to an existing condition, takes a single line of code, but from that moment every piece of code that has maintained that condition becomes security-relevant code. A full review of those does not usually follow. I believe that structure is behind Fragnesia and DirtyClone arriving nine and eight days apart.
The interval between vulnerabilities shrinks
When a patch is published, the following information is published with it: which functionality is connected to external input, what the developers judged dangerous, what condition is newly checked, and where other code that must follow the same rule lives.
The moment the DirtyFrag patch began trusting SKBFL_SHARED_FRAG, the question "is there another helper that does not preserve that flag?" was created. With that question in hand, the review scope narrows from the entire network stack to a few dozen functions that handle fragments. And reviewing a few dozen functions is a shape of work LLMs handle relatively well.
I take this to be why Fragnesia came nine days later and DirtyClone eight days after that. If this review cost falls further, the interval narrows further.
The gap between patch publication and working exploits shortens
pedit COW illustrates this.
That fix originally proceeded in the open on netdev as a data corruption bug patch, with no security framing and no CVE. A CVE was assigned on June 16, and a working PoC was published the next day. The information needed to build an exploit had been publicly available on the mailing list well before that.
Previously, analyzing a published patch and turning it into an exploit required considerable time and expertise, and that time functioned as response time for defenders. That time is shrinking.
Part 4. Response
For researchers
Hypothesis formation matters more than search. As broad code review gets cheaper, the ability to judge where to look becomes relatively more important. Copy Fail was found by an automated tool, but the hypothesis that page provenance in scatterlists was an unexamined area was not produced by the tool. Understanding of the subsystem remains the starting point.
Assume duplicate discovery. DirtyClone was independently rediscovered while a broad patch had already been submitted. As search costs fall, the same bug being found in several places at once becomes normal. For individual researchers, this means accounting for the possibility that a long investment has already been reported.
That said, the discovery of Fragnesia and DirtyClone started from understanding what condition SKBFL_SHARED_FRAG presupposes and when that condition began being used for security decisions. Extracting a rule from a patch, checking whether that rule is enforced by code, and finding other code that depends on the same rule still appear to be work handled by people.
Verification burden grows. Turning a search result into a confirmed vulnerability is separate work. You have to check whether the path is actually reachable, whether the required input and state can be constructed, whether it is a plain bug or crosses a security boundary, whether it duplicates an existing CVE, and whether a reproducible PoC exists. Reports submitted without reproduction and concrete impact only add review burden for maintainers.
For defenders
Tracking at the level of individual CVEs is not enough. Systems that applied the DirtyFrag patch were still vulnerable to Fragnesia, and applying Fragnesia's fix still left them vulnerable to DirtyClone. JFrog summarized it in their report as follows.
A system is only fully protected once the entire DirtyFrag vulnerability fix chain is applied.
What needs to be verified, then, is not a vulnerability name but CVE-2026-43284, 43500, 46300, and 43503 individually, along with the kernel version that carries the final fix. Stable and LTS branches that received only the initial mitigation without the follow-up patches are particularly at risk.
The same outcome can have a different cause. pedit COW corrupts the page cache in the same way but is not part of the SKBFL_SHARED_FRAG family. It is a separate bug in traffic-control's act_pedit, where the COW range calculation does not account for runtime offsets added by typed keys. Blocking the ESP module in response to the DirtyFrag family does not stop this one.
Verify that mitigations actually took effect. Module-blocking mitigations run without error in environments where the functionality is built into the kernel rather than shipped as a module, while blocking nothing. What matters is the result of applying a mitigation, not a record that it was applied.
Re-examine detection assumptions. Because this family does not modify files on disk, integrity checks that compare on-disk hashes and package verification will not detect it. This is not a configuration problem but a mismatch in what is being inspected. There are cases where a clean package verification result is not evidence that nothing was tampered with.
CVE notifications alone leave gaps. As with pedit COW, fixes sometimes proceed without a CVE being assigned. If the period immediately following patch publication is becoming the most dangerous window, the bottleneck moves from vulnerability discovery to patch deployment speed.
Published PoCs need verification. In this family as well, many of the repositories that appeared were other code with the name changed, or did not work at all. The basis for judgment should be kernel versions and the presence of upstream commits, not repositories.
Summary
The following is what I confirmed while following these six weeks, and what I came to think as a result.
A vulnerability can live between commits rather than in any one of them. Copy Fail was the result of changes from 2011, 2015, and 2017 combining, and each was fine at the time it was made. This type is not found by commit-level review.
When a fix creates a new precondition, that precondition needs review too. The DirtyFrag patch began using a flag that had served correctness purposes for thirteen years as the basis for a security decision, but the dozens of helpers that had been propagating that flag were not reviewed alongside it. Fragnesia and DirtyClone are the result. It is worth distinguishing whether a fix created structural enforcement or simply added one more rule for developers to follow.
Current automated tooling does not detect this type. Copy Fail does not crash, leaves no trace, and is correct at the level of individual code units. That is why it went undiscovered for nine years. A clean fuzzing result is not evidence that the code in question is sound.
Evaluating the meaning of code, by contrast, worked on this type. It was found with three sentences of context and about an hour of scanning. But those three sentences require understanding of the subsystem to write. What fell is the cost of search, not the difficulty of forming a hypothesis.
I therefore expect cases like this to appear more often. When a patch is published, what condition newly matters is revealed along with it, and finding code that does not honor that condition is narrow enough in scope to suit automation. That is why Fragnesia took nine days and DirtyClone eight, and if this cost falls further the intervals will shrink further. Cases like pedit COW, where a PoC follows a day after a fix that never received a CVE, should become more common as well.
For researchers, hypothesis formation and verification grow in weight. The value of broad code sweeps declines, while the ability to judge where to look and to confirm a result as a real vulnerability remains. At the same time, duplicate discovery will be routine, so being first is unlikely to remain a workable standard on its own.
For defenders, the unit of tracking and the basis of detection both need to be reset. The whole fix chain has to be verified rather than a single CVE, and mitigations do not carry over to vulnerabilities that share an outcome but not a cause. When a vulnerability does not modify files on disk, on-disk integrity checks will not see it, so it is worth confirming what existing checks actually inspect. And the bottleneck moves from vulnerability discovery to patch deployment speed.
In short, a patch is both the code that fixes a problem and information about where the same problem may remain. Attackers and defenders are looking at the same information, and the cost of interpreting it has fallen for both. The difference going forward will come from who interprets it first.
Thank you for reading to the end. If I have misunderstood something or if anything needs correction, I would appreciate hearing about it.
References