Dirty Frag is a bug class — here's the foxguard rule pack
We shipped C language support and three structural rules for the Dirty Frag class on the day the advisory landed. They're regex-shaped triage funnels, not proofs — but they catch the calibration sites and they're already in the npm release.
What dropped on Wednesday
On 2026-05-07, Hyunwoo Kim (@v4bel) posted Dirty Frag to oss-security. It’s not a single CVE — it’s a structural pattern. Userspace pins page-cache pages through splice / vmsplice with MSG_SPLICE_PAGES, the kernel parks those pages in an sk_buff fragment slot, and the receive-side AEAD or skcipher decrypts in place because nobody called skb_cow_data on the unsafe path. The decrypt writes plaintext back into the file’s page cache. It’s a sibling of Dirty Pipe (2022) and Copy Fail. The HN thread reached #4 with 560 points by the time we shipped this rule pack. The upstream ESP fix (f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4) extends the skip_cow gate with !skb_has_shared_frag(skb). RxRPC’s fix is still on lore as of today.
Our read: the splice-frag-then-in-place-crypto shape is going to recur in any subsystem that takes external pages and AEAD-decrypts them. Time to write rules.
What we shipped
PR #297 merged on the day the advisory landed. It adds C as a first-class scanner language (tree-sitter-c, Language::C, .c / .h extensions, Semgrep-compat languages: [c]) plus three rules under rules/kernel/dirty-frag-class/. Six calibration fixtures (three vulnerable, three safe) ship with it; cargo test --test kernel_dirty_frag is 6/6.
$ npx foxguard@latest --no-builtins \
--rules rules/kernel/dirty-frag-class/ \
tests/fixtures/kernel/dirty-frag/
foxguard v0.8.0 · scanning...
.../aead_no_cow_vulnerable.c · 1 issue
█ CRITICAL In-place AEAD decrypt on skb without a dominating cow/unshare gate (Dirty Frag class). …
█ semgrep/kernel/dirty-frag/skb-inplace-aead-no-cow (CWE-787) line 27:1
█ aead_request_set_crypt(req, sg, sg, len, iv);
.../scatterwalk_store_vulnerable.c · 1 issue
.../skcipher_no_cow_vulnerable.c · 1 issue
3 issues 6 files · 0.01s
Three positives, three clean negatives, one parser bump, and a rule directory you can point at any kernel tree.
The structural pattern, in foxguard’s language
Here’s skb-inplace-aead-no-cow.yaml verbatim. It’s the rule that flags the pre-patch ESP sites:
rules:
- id: kernel/dirty-frag/skb-inplace-aead-no-cow
pattern-regex: '(?ms)^\s*aead_request_set_crypt\s*\([^}]*?crypto_aead_decrypt\s*\('
pattern-not-regex: '(?s)\b(?:skb_cow_data|skb_copy|skb_unshare|skb_make_writable|pskb_expand_head)\s*\([^}]*?aead_request_set_crypt\s*\([^}]*?crypto_aead_decrypt\s*\('
message: |
In-place AEAD decrypt on skb without a dominating cow/unshare gate
(Dirty Frag class). Verify skb_cow_data / skb_unshare / skb_make_writable /
pskb_expand_head is reached on the unsafe path before
aead_request_set_crypt(req, sg, sg, ...) + crypto_aead_decrypt(req).
See oss-security 2026-05-07 advisory and pwnkit issue #263.
severity: ERROR
languages: [c]
metadata:
cwe: "CWE-787"
Two regexes, one logical AND-NOT. The positive looks for an aead_request_set_crypt call followed within the same C function body by crypto_aead_decrypt — [^}]*? is the cheap way to say “without leaving the brace block.” The negative says “if a cow / unshare / make-writable / expand-head call appears earlier in the same span, suppress.” That’s the entire reasoning the rule does. The skcipher rule is the same shape with skcipher_request_set_crypt / crypto_skcipher_decrypt. The scatterwalk rule flags the four-byte STORE primitive (scatterwalk_map_and_copy(..., out=1) after aead_request_set_crypt) that appears in crypto/authencesn.c::crypto_authenc_esn_decrypt.
This is structural triage, not a theorem. It survives renames and minor refactors, it catches the five calibration sites named in the advisory, and it runs at tree-sitter speed against a full kernel checkout.
What we deliberately don’t claim
This rule pack does not detect Dirty Frag. It flags the structural pattern. The difference matters:
- No backreferences. Rust regex can’t enforce
arg2 == arg3onaead_request_set_crypt(req, src, dst, …), so the rule fires on anyset_crypt→decryptsequence in a function. Legitimate non-in-place crypto (src != dst) trips the same regex. - The cow suppression is coarse.
pattern-not-regexfilters when the cow regex overlaps the positive’s span. That approximates “cow appears earlier in the same function” — it is not a dominating-call analysis. Askb_cow_databehind an unrelated branch still suppresses. - Macros are invisible. Tree-sitter sees the post-preprocessor source. The kernel reaches the cow gate through
pskb_*macros and inline helpers; macro-only paths get missed unless the direct name also appears. - No taint to the splice source. The actual bug requires the page provenance to come from
splice/vmsplice+MSG_SPLICE_PAGES. The rule fires on the in-place idiom regardless of where the SGL came from. Expect false positives on TLS, dm-crypt, fscrypt, and offload-crypto paths — they legitimately decrypt in place after a cow we can’t model.
We also haven’t run this against a real kernel checkout yet. The Tier 1 sibling list — net/ipv4/ah4.c, net/ipv6/ah6.c, net/ipv4/ipcomp.c, net/ipv6/ipcomp6.c, RxGK — is plausible-but-untested. If you point the pack at a tree and it lights up, that’s a triage funnel, not a finding. Don’t file CVEs off regex hits.
The plan beyond regex
Path-sensitive cow-gate analysis lives in two issues we have open:
- foxguard #295 — Coccinelle integration. Coccinelle’s
@@metavariables can expressarg2 == arg3directly, and SmPL has structural understanding of dominators. That’s where the in-place property gets a real proof. - foxguard #296 — CodeQL integration. CodeQL’s data-flow library can carry SGL provenance from
MSG_SPLICE_PAGESto the AEAD call, which is the actual bug-class invariant.
The variant-hunt orchestrator that walks a kernel tree, fans out to all three engines (foxguard regex, Coccinelle, CodeQL), and reconciles the hits is pwnkit #263. The foxguard rules are the cheap fast-pass — first sieve, not last word.
Try it
git clone --depth=1 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
npx foxguard@latest --no-builtins \
--rules rules/kernel/dirty-frag-class/ \
linux/
The rule files live at rules/kernel/dirty-frag-class/ on main. Calibration tests at tests/kernel_dirty_frag.rs are 6/6 against the included fixtures. PR #297 has the full rationale, and the original Dirty Frag write-up is at V4bel/dirtyfrag — credit where it’s due.
If you find a sibling site with this pack, open an issue. If you find a false positive, also open an issue — the negative-regex list is best-effort and grows.
foxguard is an open-source security scanner written in Rust. GitHub · foxguard.dev.