Skip to content

feat(rules): Dirty Frag class β€” skb in-place crypto without cow gate#297

Merged
peaktwilight merged 1 commit into
mainfrom
feat/dirty-frag-rules
May 7, 2026
Merged

feat(rules): Dirty Frag class β€” skb in-place crypto without cow gate#297
peaktwilight merged 1 commit into
mainfrom
feat/dirty-frag-rules

Conversation

@peaktwilight

Copy link
Copy Markdown
Collaborator

Summary

  • Add three rules for the Dirty Frag kernel bug class disclosed in the oss-security advisory of 2026-05-07 (Hyunwoo Kim, @V4bel). Sibling of Dirty Pipe and Copy Fail; overwrites the frags[] slot of struct sk_buff via in-place crypto on splice-pinned pages.
  • Add C as a first-class language so foxguard can scan .c / .h files at all (it could not before this PR β€” there is no built-in C rule today, only the new YAML).
  • This is the foxguard half of the foxguard-first integration approach in pwnkit issue #263 β€” kernel/C scanning lives in foxguard; the pwnkit CLI delegates kernel variant-hunt to foxguard rules.

Rules

All three are loaded with foxguard --no-builtins --rules rules/kernel/dirty-frag-class:

Rule ID Pattern
kernel/dirty-frag/skb-inplace-skcipher-no-cow skcipher_request_set_crypt(req, sg, sg, …) then crypto_skcipher_decrypt(req) in the same function, with no dominating cow / unshare / make-writable / pskb_expand_head
kernel/dirty-frag/skb-inplace-aead-no-cow aead_request_set_crypt(req, sg, sg, …) then crypto_aead_decrypt(req) in the same function, with no dominating cow gate
kernel/dirty-frag/scatterwalk-store-on-shared-sgl aead_request_set_crypt(...) then scatterwalk_map_and_copy(…, /*out=*/1) in the same function β€” the secondary STORE primitive abused by Copy Fail and Dirty Frag in crypto_authenc_esn_decrypt

Calibration

Six fixtures (positive + negative per rule) model the calibration sites named in the advisory:

  • aead_no_cow_vulnerable.c ↔ net/ipv4/esp4.c::esp_input / net/ipv6/esp6.c::esp6_input (pre-patch)
  • skcipher_no_cow_vulnerable.c ↔ net/rxrpc/rxkad.c::rxkad_verify_packet_1 / net/rxrpc/conn_event.c::rxrpc_verify_response (pre-patch)
  • scatterwalk_store_vulnerable.c ↔ crypto/authencesn.c::crypto_authenc_esn_decrypt
  • *_safe.c fixtures model the same functions post-patch (skb_cow_data dominates) or with out=0 / non-aliased SGL.
$ foxguard --no-builtins --rules rules/kernel/dirty-frag-class tests/fixtures/kernel/dirty-frag/
  foxguard v0.8.0 Β· scanning...
  .../kernel/dirty-frag/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);

  .../kernel/dirty-frag/scatterwalk_store_vulnerable.c Β· 1 issue
    β–ˆ  CRITICAL  scatterwalk_map_and_copy STORE (out=1) on an in-place AEAD scatterlist (Dirty Frag class). …
    β–ˆ   semgrep/kernel/dirty-frag/scatterwalk-store-on-shared-sgl (CWE-787)  line 27:1
    β–ˆ   aead_request_set_crypt(req, sg, sg, cryptlen, iv);

  .../kernel/dirty-frag/skcipher_no_cow_vulnerable.c Β· 1 issue
    β–ˆ  CRITICAL  In-place skcipher decrypt on skb without a dominating cow/unshare gate (Dirty Frag class). …
    β–ˆ   semgrep/kernel/dirty-frag/skb-inplace-skcipher-no-cow (CWE-787)  line 28:1
    β–ˆ   skcipher_request_set_crypt(req, sg, sg, len, iv);

  3 issues  6 files Β· 0.01s

3 of 6 fixtures flagged (the three positives), 3 clean (the three negatives).

What this rule cannot do (deferred to Coccinelle / CodeQL)

The advisory describes a path-sensitive bug β€” "no skb_cow_data (or equivalent) call dominates the in-place decrypt on the unsafe path". foxguard's Semgrep YAML bridge is the right tool for structural triage, but it cannot prove dominance of one call over another on every control-flow path. Specific limitations of these rules:

  • No backreferences in Rust regex. I cannot syntactically require arg2 == arg3 of *_request_set_crypt(req, src, dst, …) to confirm src == dst (the in-place property). The rules fire on any set_crypt then decrypt sequence inside a function. This will produce false positives on any set_crypt(req, src, dst, …) where src != dst (legitimate non-in-place crypto).
  • The cow-gate suppression is coarse. pattern-not-regex filters out a positive only when the cow regex overlaps the positive's match span. Since [^}]*? is greedy-lazy across the function body, this approximates "cow appears earlier in the same function" β€” but it is not a dominating-call analysis. A skb_cow_data call that is itself behind an unrelated branch will still suppress.
  • Macros and inline helpers are invisible. The kernel reaches the cow gate via pskb_expand_head, pskb_* macros, and inlined skb helpers. These rules enumerate the common direct names; macro-only call paths will be missed.
  • No taint to MSG_SPLICE_PAGES source. The bug requires the page provenance to trace back to userspace via splice / vmsplice + MSG_SPLICE_PAGES. These rules do not check that β€” they fire on the in-place idiom regardless of where the SGL came from. Expect false positives on TLS, dm-crypt, fscrypt, and offload-crypto paths.

These are deliberate trade-offs for a 1-day spike. The full path-sensitive variant-hunt for Dirty Frag class needs Coccinelle and/or CodeQL, planned as a separate issue per the pwnkit #263 plan (Β§3.1, Β§3.3).

C language enrollment

This PR adds the minimum needed to make the rules above runnable:

  • tree-sitter-c = "0.24" in Cargo.toml
  • Language::C enum variant in src/lib.rs
  • tree_sitter_c::LANGUAGE in src/engine/parser.rs
  • .c / .h extension detection in src/engine/scanner.rs
  • // comment marker in src/engine/scanner.rs::comment_markers
  • c language string in src/rules/semgrep_compat.rs::map_language
  • Display + gen_rules_ts arms (no built-in C rules; the website array is empty for now)

No built-in C rules ship in this PR. The scanner will not find anything in C files unless --rules points at a C-language YAML rule set (e.g. these three).

Test plan

  • cargo build β€” clean
  • cargo clippy -- -D warnings β€” clean
  • cargo fmt β€” clean
  • cargo test β€” 580 passing (430 unit + 8 + 109 + 3 + 15 + 1 + 12 + 6 + 6 new), 0 failed
  • cargo test --test kernel_dirty_frag β€” all 6 calibration tests pass (positive flags, negative ignored, per rule)
  • cargo test --test rule_inventory β€” generated www/src/data/rules.ts matches (no built-in C rules added, so no inventory drift)
  • foxguard --no-builtins --rules rules/kernel/dirty-frag-class tests/fixtures/kernel/dirty-frag/ β€” 3 findings on positives, 0 on negatives

Refs

Three Semgrep-YAML-bridge rules for the Dirty Frag bug class
(in-place skcipher/AEAD on skb without cow gate, plus the
scatterwalk_map_and_copy STORE primitive abused by Copy Fail and
Dirty Frag in crypto_authenc_esn_decrypt).

This is the foxguard half of the foxguard-first integration approach
proposed in pwnkit issue #263 β€” kernel/C scanning lives in foxguard,
the pwnkit CLI delegates kernel variant-hunt to foxguard rules.

Adds:
- C as a first-class Language (tree-sitter-c parser, .c/.h detection,
  // comment marker, semgrep-compat language map). No built-in C rules
  yet β€” only YAML loading via --rules.
- rules/kernel/dirty-frag-class/skb-inplace-skcipher-no-cow.yaml
- rules/kernel/dirty-frag-class/skb-inplace-aead-no-cow.yaml
- rules/kernel/dirty-frag-class/scatterwalk-store-on-shared-sgl.yaml
- 6 fixtures under tests/fixtures/kernel/dirty-frag/ (positive +
  negative for each rule), modeling esp_input, rxkad_verify_packet_1,
  and crypto_authenc_esn_decrypt pre/post the upstream patches
- tests/kernel_dirty_frag.rs (calibration test: each rule flags its
  positive fixture and ignores its negative fixture)

Calibration:
  foxguard --no-builtins --rules rules/kernel/dirty-frag-class \
           tests/fixtures/kernel/dirty-frag/
  β†’ 3 critical findings (one per *_vulnerable.c). Safe fixtures clean.

Path-sensitivity caveat: Rust's regex crate has no backreferences, so
these rules cannot syntactically require src == dst on set_crypt's
SGL args. The cow-gate suppression is a coarse same-file regex
overlap, not a dominating-call analysis. Definitive Dirty Frag class
matching needs Coccinelle (path-sensitive) or CodeQL (taint from
MSG_SPLICE_PAGES to crypto_*_decrypt). That work is deferred to a
follow-up issue per the issue #263 plan.

Refs:
- oss-security advisory 2026-05-07 (Hyunwoo Kim @V4bel)
- upstream ESP patch f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@peaktwilight has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 28 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

βŒ› How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 307092b3-15cc-455a-8767-767cca26a362

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3041fc7 and 70813a9.

β›” Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
πŸ“’ Files selected for processing (16)
  • Cargo.toml
  • rules/kernel/dirty-frag-class/scatterwalk-store-on-shared-sgl.yaml
  • rules/kernel/dirty-frag-class/skb-inplace-aead-no-cow.yaml
  • rules/kernel/dirty-frag-class/skb-inplace-skcipher-no-cow.yaml
  • src/bin/gen_rules_ts.rs
  • src/engine/parser.rs
  • src/engine/scanner.rs
  • src/lib.rs
  • src/rules/semgrep_compat.rs
  • tests/fixtures/kernel/dirty-frag/aead_no_cow_safe.c
  • tests/fixtures/kernel/dirty-frag/aead_no_cow_vulnerable.c
  • tests/fixtures/kernel/dirty-frag/scatterwalk_store_safe.c
  • tests/fixtures/kernel/dirty-frag/scatterwalk_store_vulnerable.c
  • tests/fixtures/kernel/dirty-frag/skcipher_no_cow_safe.c
  • tests/fixtures/kernel/dirty-frag/skcipher_no_cow_vulnerable.c
  • tests/kernel_dirty_frag.rs
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dirty-frag-rules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@peaktwilight peaktwilight merged commit 416e6e0 into main May 7, 2026
16 checks passed
@peaktwilight peaktwilight deleted the feat/dirty-frag-rules branch May 7, 2026 20:58
peaktwilight added a commit that referenced this pull request May 10, 2026
Blog post for foxguard.dev/blog covering PR #297: C language support
plus three structural rules under rules/kernel/dirty-frag-class/ for
the Dirty Frag class disclosed by Hyunwoo Kim (@V4bel) on oss-security
2026-05-07.

Frames the rules as structural triage (regex-shaped, not path-sensitive
proofs), shows one YAML rule verbatim, lines up the false-positive
caveats, and points at foxguard #295 (Coccinelle), #296 (CodeQL), and
pwnkit #263 (variant-hunt orchestrator) as the proper homes for
dominator analysis.
peaktwilight added a commit that referenced this pull request May 10, 2026
…er 1 + FP shapes) (#305)

* test(kernel/dirty-frag): expand fixtures + tighten negative-regex

Lifts kernel_dirty_frag test count from 6 to 12 by adding 6 synthetic
fixtures (3 Tier 1 sibling-site positives, 3 Tier 2 known-FP negatives)
and extends the cow-gate negative regex with two additional kernel
primitives.

New fixtures (tests/fixtures/kernel/dirty-frag/):
- ah_aead_no_cow_vulnerable.c β€” AH4/AH6 ICV-verify shape (must flag)
- ipcomp_skcipher_no_cow_vulnerable.c β€” IPComp decrypt shape (must flag)
- scatterwalk_authenc_store_vulnerable.c β€” non-ESN authenc STORE (must flag)
- tls_aead_cow_safe.c β€” kTLS-style cow via __skb_cow (must not flag)
- macsec_skcipher_cow_safe.c β€” MACsec via skb_copy_expand (must not flag)
- scatterwalk_inplace_read_safe.c β€” in-place AEAD + READ-back out=0
  (must not flag; tests literal-1 tightness of scatterwalk regex)

Negative-regex additions (skb-inplace-aead-no-cow,
skb-inplace-skcipher-no-cow):
- skb_cow β€” base form of skb_cow_data, used by some non-IPsec callers
- __skb_cow β€” internal helper exposed in skbuff.h, hit by kTLS receive
- skb_copy_expand β€” full reallocation, defeats shared-frag aliasing

Skipped on purpose:
- skb_clone β€” duplicates pointers, does NOT make the data writable;
  including it would over-suppress true positives.
- skb_share_check β€” only handles the cloned case, not non-linear
  shared frags from MSG_SPLICE_PAGES; same over-suppression risk.

All fixtures are minimal synthetic reproducers authored from the patch-
hunk shape (oss-security 2026-05-07, commit f4c50a4034e6); no kernel
source is copied. Refs foxguard PR #297, pwnkit issue #263.

* test(fixtures): drop kernel #include lines, keep clangd quiet

The 12 dirty-frag fixtures already self-declare their structs and externs;
the kernel headers (linux/skbuff.h, crypto/aead.h, crypto/skcipher.h,
crypto/scatterwalk.h) were redundant with the forward decls below them.
clangd flagged pp_file_not_found on every fixture because kernel headers
aren't on dev machines. These files are scanned as source text by
tree-sitter, never compiled, so removing the includes is a no-op for
foxguard tests (still 12/12) and a clear win for editor LSP noise.
peaktwilight added a commit that referenced this pull request May 29, 2026
Three Semgrep-YAML-bridge rules for the Dirty Frag bug class
(in-place skcipher/AEAD on skb without cow gate, plus the
scatterwalk_map_and_copy STORE primitive abused by Copy Fail and
Dirty Frag in crypto_authenc_esn_decrypt).

This is the foxguard half of the foxguard-first integration approach
proposed in pwnkit issue #263 β€” kernel/C scanning lives in foxguard,
the pwnkit CLI delegates kernel variant-hunt to foxguard rules.

Adds:
- C as a first-class Language (tree-sitter-c parser, .c/.h detection,
  // comment marker, semgrep-compat language map). No built-in C rules
  yet β€” only YAML loading via --rules.
- rules/kernel/dirty-frag-class/skb-inplace-skcipher-no-cow.yaml
- rules/kernel/dirty-frag-class/skb-inplace-aead-no-cow.yaml
- rules/kernel/dirty-frag-class/scatterwalk-store-on-shared-sgl.yaml
- 6 fixtures under tests/fixtures/kernel/dirty-frag/ (positive +
  negative for each rule), modeling esp_input, rxkad_verify_packet_1,
  and crypto_authenc_esn_decrypt pre/post the upstream patches
- tests/kernel_dirty_frag.rs (calibration test: each rule flags its
  positive fixture and ignores its negative fixture)

Calibration:
  foxguard --no-builtins --rules rules/kernel/dirty-frag-class \
           tests/fixtures/kernel/dirty-frag/
  β†’ 3 critical findings (one per *_vulnerable.c). Safe fixtures clean.

Path-sensitivity caveat: Rust's regex crate has no backreferences, so
these rules cannot syntactically require src == dst on set_crypt's
SGL args. The cow-gate suppression is a coarse same-file regex
overlap, not a dominating-call analysis. Definitive Dirty Frag class
matching needs Coccinelle (path-sensitive) or CodeQL (taint from
MSG_SPLICE_PAGES to crypto_*_decrypt). That work is deferred to a
follow-up issue per the issue #263 plan.

Refs:
- oss-security advisory 2026-05-07 (Hyunwoo Kim @V4bel)
- upstream ESP patch f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4
peaktwilight added a commit that referenced this pull request May 29, 2026
Blog post for foxguard.dev/blog covering PR #297: C language support
plus three structural rules under rules/kernel/dirty-frag-class/ for
the Dirty Frag class disclosed by Hyunwoo Kim (@V4bel) on oss-security
2026-05-07.

Frames the rules as structural triage (regex-shaped, not path-sensitive
proofs), shows one YAML rule verbatim, lines up the false-positive
caveats, and points at foxguard #295 (Coccinelle), #296 (CodeQL), and
pwnkit #263 (variant-hunt orchestrator) as the proper homes for
dominator analysis.
peaktwilight added a commit that referenced this pull request May 29, 2026
…er 1 + FP shapes) (#305)

* test(kernel/dirty-frag): expand fixtures + tighten negative-regex

Lifts kernel_dirty_frag test count from 6 to 12 by adding 6 synthetic
fixtures (3 Tier 1 sibling-site positives, 3 Tier 2 known-FP negatives)
and extends the cow-gate negative regex with two additional kernel
primitives.

New fixtures (tests/fixtures/kernel/dirty-frag/):
- ah_aead_no_cow_vulnerable.c β€” AH4/AH6 ICV-verify shape (must flag)
- ipcomp_skcipher_no_cow_vulnerable.c β€” IPComp decrypt shape (must flag)
- scatterwalk_authenc_store_vulnerable.c β€” non-ESN authenc STORE (must flag)
- tls_aead_cow_safe.c β€” kTLS-style cow via __skb_cow (must not flag)
- macsec_skcipher_cow_safe.c β€” MACsec via skb_copy_expand (must not flag)
- scatterwalk_inplace_read_safe.c β€” in-place AEAD + READ-back out=0
  (must not flag; tests literal-1 tightness of scatterwalk regex)

Negative-regex additions (skb-inplace-aead-no-cow,
skb-inplace-skcipher-no-cow):
- skb_cow β€” base form of skb_cow_data, used by some non-IPsec callers
- __skb_cow β€” internal helper exposed in skbuff.h, hit by kTLS receive
- skb_copy_expand β€” full reallocation, defeats shared-frag aliasing

Skipped on purpose:
- skb_clone β€” duplicates pointers, does NOT make the data writable;
  including it would over-suppress true positives.
- skb_share_check β€” only handles the cloned case, not non-linear
  shared frags from MSG_SPLICE_PAGES; same over-suppression risk.

All fixtures are minimal synthetic reproducers authored from the patch-
hunk shape (oss-security 2026-05-07, commit f4c50a4034e6); no kernel
source is copied. Refs foxguard PR #297, pwnkit issue #263.

* test(fixtures): drop kernel #include lines, keep clangd quiet

The 12 dirty-frag fixtures already self-declare their structs and externs;
the kernel headers (linux/skbuff.h, crypto/aead.h, crypto/skcipher.h,
crypto/scatterwalk.h) were redundant with the forward decls below them.
clangd flagged pp_file_not_found on every fixture because kernel headers
aren't on dev machines. These files are scanned as source text by
tree-sitter, never compiled, so removing the includes is a no-op for
foxguard tests (still 12/12) and a clear win for editor LSP noise.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant