Skip to content

Commit 416e6e0

Browse files
authored
feat(rules): add Dirty Frag class rules (kernel/dirty-frag/*) (#297)
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
1 parent 3041fc7 commit 416e6e0

17 files changed

Lines changed: 403 additions & 1 deletion

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tree-sitter-ruby = "0.23"
1919
tree-sitter-java = "0.23"
2020
tree-sitter-php = "0.24"
2121
tree-sitter-rust = "0.24"
22+
tree-sitter-c = "0.24"
2223
tree-sitter-c-sharp = "0.23"
2324
tree-sitter-swift = "0.7"
2425
tree-sitter-kotlin-sg = "0.4"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Dirty Frag class — scatterwalk_map_and_copy STORE on AEAD-shared SGL.
2+
#
3+
# Pattern: scatterwalk_map_and_copy(..., dst, off, len, /*out=*/1) where dst
4+
# is part of an in-place AEAD request set up earlier in the same function via
5+
# aead_request_set_crypt(req, sg, sg, ...). This is the secondary STORE
6+
# primitive that Copy Fail and Dirty Frag both abuse in
7+
# crypto_authenc_esn_decrypt (crypto/authencesn.c).
8+
#
9+
# This is a SYNTACTIC / structural rule — it does not prove dst aliases the
10+
# shared SGL via taint analysis. Path-sensitive analysis (CodeQL) is required
11+
# to confirm aliasing; see PR body for the deferred-work pointer.
12+
rules:
13+
- id: kernel/dirty-frag/scatterwalk-store-on-shared-sgl
14+
# Positive: aead_request_set_crypt(...) is followed (within the same
15+
# function body) by scatterwalk_map_and_copy(..., out=1). Rust regex has
16+
# no backreferences, so we cannot require arg2 == arg3 of set_crypt to
17+
# confirm the SGL is shared; manual triage / CodeQL confirms aliasing.
18+
pattern-regex: '(?ms)^\s*aead_request_set_crypt\s*\([^}]*?scatterwalk_map_and_copy\s*\([^,]+,[^,]+,[^,]+,[^,]+,\s*1\s*\)'
19+
message: |
20+
scatterwalk_map_and_copy STORE (out=1) on an in-place AEAD scatterlist
21+
(Dirty Frag class). The destination scatterlist is shared with the
22+
attacker-controllable source SGL via aead_request_set_crypt(req, sg, sg, ...);
23+
a 4-byte STORE lands in caller-pinned memory regardless of AEAD auth
24+
result. Calibration site: crypto/authencesn.c::crypto_authenc_esn_decrypt.
25+
See oss-security 2026-05-07 advisory and pwnkit issue #263.
26+
severity: ERROR
27+
languages: [c]
28+
metadata:
29+
cwe: "CWE-787"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Dirty Frag class — in-place AEAD decrypt on skb without cow gate.
2+
#
3+
# Pattern: aead_request_set_crypt(req, sg, sg, ...) followed by
4+
# crypto_aead_decrypt(req) within the same function body, with no
5+
# dominating cow / unshare / make-writable / pskb_expand_head call.
6+
#
7+
# Calibration sites (pre-patch): net/ipv4/esp4.c::esp_input,
8+
# net/ipv6/esp6.c::esp6_input. Patched by upstream commit
9+
# f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4 (extends skip_cow gate with
10+
# !skb_has_shared_frag()).
11+
#
12+
# This is a SYNTACTIC / structural rule — it does not prove the cow gate is
13+
# unreachable. Path-sensitive analysis (Coccinelle / CodeQL) is required for
14+
# definitive flagging; see PR body for the deferred-work pointer.
15+
rules:
16+
- id: kernel/dirty-frag/skb-inplace-aead-no-cow
17+
# Positive: aead_request_set_crypt(req, ?, ?, ...) followed (within the
18+
# same C function body) by crypto_aead_decrypt(req). Rust regex has no
19+
# backreferences so we cannot require arg2 == arg3 syntactically; manual
20+
# triage / Coccinelle confirms the in-place property.
21+
pattern-regex: '(?ms)^\s*aead_request_set_crypt\s*\([^}]*?crypto_aead_decrypt\s*\('
22+
# Negative: a cow / unshare / make-writable / expand-head call appears
23+
# BEFORE the in-place idiom within the same function body — suppress.
24+
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*\('
25+
message: |
26+
In-place AEAD decrypt on skb without a dominating cow/unshare gate
27+
(Dirty Frag class). Verify skb_cow_data / skb_unshare / skb_make_writable /
28+
pskb_expand_head is reached on the unsafe path before
29+
aead_request_set_crypt(req, sg, sg, ...) + crypto_aead_decrypt(req).
30+
See oss-security 2026-05-07 advisory and pwnkit issue #263.
31+
severity: ERROR
32+
languages: [c]
33+
metadata:
34+
cwe: "CWE-787"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Dirty Frag class — in-place skcipher decrypt on skb without cow gate.
2+
#
3+
# Pattern: skcipher_request_set_crypt(req, sg, sg, ...) followed by
4+
# crypto_skcipher_decrypt(req) within the same function body, with no
5+
# dominating cow / unshare / make-writable / pskb_expand_head call.
6+
#
7+
# Calibration sites (pre-patch): net/rxrpc/rxkad.c::rxkad_verify_packet_1.
8+
# Patched by upstream change to add data_len/nonlinear gate (see oss-security
9+
# advisory 2026-05-07).
10+
#
11+
# This is a SYNTACTIC / structural rule — it does not prove the cow gate is
12+
# unreachable. Path-sensitive analysis (Coccinelle / CodeQL) is required for
13+
# definitive flagging; see PR body for the deferred-work pointer.
14+
rules:
15+
- id: kernel/dirty-frag/skb-inplace-skcipher-no-cow
16+
# Positive: skcipher_request_set_crypt(req, ?, ?, ...) followed (within
17+
# the same C function body, bounded by `}`) by crypto_skcipher_decrypt(req).
18+
# Rust regex has no backreferences, so we cannot require arg2 == arg3
19+
# syntactically. Manual triage / Coccinelle confirms the in-place property.
20+
pattern-regex: '(?ms)^\s*skcipher_request_set_crypt\s*\([^}]*?crypto_skcipher_decrypt\s*\('
21+
# Negative: same idiom, but a cow / unshare / make-writable / expand-head
22+
# call appears BEFORE it within the same function body. Used to suppress
23+
# the post-patch / safe-fixture variant.
24+
pattern-not-regex: '(?s)\b(?:skb_cow_data|skb_copy|skb_unshare|skb_make_writable|pskb_expand_head)\s*\([^}]*?skcipher_request_set_crypt\s*\([^}]*?crypto_skcipher_decrypt\s*\('
25+
message: |
26+
In-place skcipher decrypt on skb without a dominating cow/unshare gate
27+
(Dirty Frag class). Verify skb_cow_data / skb_unshare / skb_make_writable /
28+
pskb_expand_head is reached on the unsafe path before
29+
skcipher_request_set_crypt(req, sg, sg, ...) + crypto_skcipher_decrypt(req).
30+
See oss-security 2026-05-07 advisory and pwnkit issue #263.
31+
severity: ERROR
32+
languages: [c]
33+
metadata:
34+
cwe: "CWE-787"

src/bin/gen_rules_ts.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ fn language_slug(language: Language) -> &'static str {
4444
Language::CSharp => "cs",
4545
Language::Swift => "swift",
4646
Language::Kotlin => "kt",
47+
Language::C => "c",
4748
Language::NginxConf => "nginxconf",
4849
Language::ApacheConf => "apacheconf",
4950
Language::HAProxyConf => "haproxyconf",
@@ -64,6 +65,7 @@ fn language_display_name(language: Language) -> &'static str {
6465
Language::CSharp => "C#",
6566
Language::Swift => "Swift",
6667
Language::Kotlin => "Kotlin",
68+
Language::C => "C",
6769
Language::NginxConf => "Nginx",
6870
Language::ApacheConf => "Apache",
6971
Language::HAProxyConf => "HAProxy",
@@ -84,6 +86,7 @@ fn language_array_name(language: Language) -> &'static str {
8486
Language::CSharp => "csharpRules",
8587
Language::Swift => "swiftRules",
8688
Language::Kotlin => "kotlinRules",
89+
Language::C => "cRules",
8790
Language::NginxConf => "nginxconfRules",
8891
Language::ApacheConf => "apacheconfRules",
8992
Language::HAProxyConf => "haproxyconfRules",

src/engine/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub fn parse_file(source: &str, language: Language) -> Option<tree_sitter::Tree>
1515
Language::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
1616
Language::Swift => tree_sitter_swift::LANGUAGE.into(),
1717
Language::Kotlin => tree_sitter_kotlin_sg::LANGUAGE.into(),
18+
Language::C => tree_sitter_c::LANGUAGE.into(),
1819
Language::NginxConf
1920
| Language::ApacheConf
2021
| Language::HAProxyConf

src/engine/scanner.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ pub fn detect_language(path: &Path) -> Option<Language> {
170170
"cs" => Some(Language::CSharp),
171171
"swift" => Some(Language::Swift),
172172
"kt" | "kts" => Some(Language::Kotlin),
173+
"c" | "h" => Some(Language::C),
173174
_ => None,
174175
}
175176
}
@@ -489,7 +490,8 @@ fn comment_markers(language: Language) -> &'static [&'static str] {
489490
| Language::Rust
490491
| Language::CSharp
491492
| Language::Swift
492-
| Language::Kotlin => &["//"],
493+
| Language::Kotlin
494+
| Language::C => &["//"],
493495
Language::NginxConf
494496
| Language::ApacheConf
495497
| Language::HAProxyConf

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub enum Language {
4747
CSharp,
4848
Swift,
4949
Kotlin,
50+
C,
5051
NginxConf,
5152
ApacheConf,
5253
HAProxyConf,
@@ -67,6 +68,7 @@ impl std::fmt::Display for Language {
6768
Language::CSharp => write!(f, "csharp"),
6869
Language::Swift => write!(f, "swift"),
6970
Language::Kotlin => write!(f, "kotlin"),
71+
Language::C => write!(f, "c"),
7072
Language::NginxConf => write!(f, "nginxconf"),
7173
Language::ApacheConf => write!(f, "apacheconf"),
7274
Language::HAProxyConf => write!(f, "haproxyconf"),

src/rules/semgrep_compat.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,7 @@ fn map_language(lang_str: &str) -> Option<Language> {
815815
"csharp" | "c#" | "cs" => Some(Language::CSharp),
816816
"swift" => Some(Language::Swift),
817817
"kotlin" | "kt" => Some(Language::Kotlin),
818+
"c" => Some(Language::C),
818819
_ => None,
819820
}
820821
}

0 commit comments

Comments
 (0)