Skip to main content

mir_extractor/rules/
memory.rs

1//! Memory safety rules.
2//!
3//! Rules detecting memory safety issues:
4//! - Transmute operations
5//! - Uninitialized memory usage
6//! - Vec::set_len misuse
7//! - Raw pointer escapes
8//! - Box/Arc into_raw without proper cleanup
9//! - Null pointer transmutes
10//! - ZST pointer arithmetic
11
12use super::collect_matches;
13use super::utils::filter_entry;
14use crate::detect_truncating_len_casts;
15use crate::{
16    Confidence, Exploitability, Finding, MirFunction, MirPackage, Rule, RuleMetadata, RuleOrigin,
17    Severity,
18};
19use std::collections::{HashMap, HashSet};
20use std::ffi::OsStr;
21use std::fs;
22use std::path::Path;
23use walkdir::WalkDir;
24
25// ============================================================================
26// Helper Functions
27// ============================================================================
28
29#[derive(Default, Clone, Copy)]
30pub(crate) struct StringLiteralState {
31    in_normal_string: bool,
32    raw_hashes: Option<usize>,
33}
34
35const STRIP_STRING_INITIAL_CAPACITY: usize = 256;
36
37pub(crate) fn strip_string_literals(
38    mut state: StringLiteralState,
39    line: &str,
40) -> (String, StringLiteralState) {
41    let bytes = line.as_bytes();
42    let mut result = String::with_capacity(STRIP_STRING_INITIAL_CAPACITY);
43    let mut i = 0usize;
44
45    while i < bytes.len() {
46        if let Some(hashes) = state.raw_hashes {
47            result.push(' ');
48            if bytes[i] == b'"' {
49                let mut matched = true;
50                for k in 0..hashes {
51                    if i + 1 + k >= bytes.len() || bytes[i + 1 + k] != b'#' {
52                        matched = false;
53                        break;
54                    }
55                }
56                if matched {
57                    for _ in 0..hashes {
58                        result.push(' ');
59                    }
60                    state.raw_hashes = None;
61                    i += 1 + hashes;
62                    continue;
63                }
64            }
65            i += 1;
66            continue;
67        }
68
69        if state.in_normal_string {
70            result.push(' ');
71            if bytes[i] == b'\\' {
72                i += 1;
73                if i < bytes.len() {
74                    result.push(' ');
75                    i += 1;
76                    continue;
77                } else {
78                    break;
79                }
80            }
81            if bytes[i] == b'"' {
82                state.in_normal_string = false;
83            }
84            i += 1;
85            continue;
86        }
87
88        let ch = bytes[i];
89        if ch == b'"' {
90            state.in_normal_string = true;
91            result.push(' ');
92            i += 1;
93            continue;
94        }
95
96        if ch == b'r' {
97            let mut j = i + 1;
98            let mut hashes = 0usize;
99            while j < bytes.len() && bytes[j] == b'#' {
100                hashes += 1;
101                j += 1;
102            }
103            if j < bytes.len() && bytes[j] == b'"' {
104                state.raw_hashes = Some(hashes);
105                result.push(' ');
106                for _ in 0..hashes {
107                    result.push(' ');
108                }
109                result.push(' ');
110                i = j + 1;
111                continue;
112            }
113        }
114
115        if ch == b'\'' {
116            if i + 1 < bytes.len() {
117                let next = bytes[i + 1];
118                let looks_like_lifetime = next == b'_' || next.is_ascii_alphabetic();
119                let following = bytes.get(i + 2).copied();
120                if looks_like_lifetime && following != Some(b'\'') {
121                    result.push('\'');
122                    i += 1;
123                    continue;
124                }
125            }
126
127            let mut j = i + 1;
128            let mut escaped = false;
129            while j < bytes.len() {
130                if escaped {
131                    escaped = false;
132                    j += 1;
133                    continue;
134                }
135                if bytes[j] == b'\\' {
136                    escaped = true;
137                    j += 1;
138                    continue;
139                }
140                if bytes[j] == b'\'' {
141                    for _ in i..=j {
142                        result.push(' ');
143                    }
144                    i = j + 1;
145                    break;
146                }
147                j += 1;
148            }
149            if j >= bytes.len() {
150                result.push(ch as char);
151                i += 1;
152            }
153            continue;
154        }
155
156        result.push(ch as char);
157        i += 1;
158    }
159
160    (result, state)
161}
162
163pub(crate) fn looks_like_null_pointer_transmute(line: &str) -> bool {
164    let lower = line.to_lowercase();
165
166    if !lower.contains("transmute") {
167        return false;
168    }
169
170    // Skip internal compiler transmute casts
171    if lower.contains("(transmute)") {
172        return false;
173    }
174
175    if lower.contains("transmute(const 0") || lower.contains("transmute(0_") {
176        return true;
177    }
178
179    if (lower.contains("std::ptr::null") || lower.contains("::ptr::null"))
180        && lower.contains("transmute")
181    {
182        return true;
183    }
184
185    if lower.contains("null") && lower.contains("transmute") {
186        return true;
187    }
188
189    false
190}
191
192pub(crate) fn looks_like_zst_pointer_arithmetic(line: &str) -> bool {
193    let lower = line.to_lowercase();
194
195    let arithmetic_methods = [
196        "offset",
197        "add",
198        "sub",
199        "wrapping_offset",
200        "wrapping_add",
201        "wrapping_sub",
202        "offset_from",
203    ];
204
205    let has_arithmetic = arithmetic_methods
206        .iter()
207        .any(|method| lower.contains(method));
208    if !has_arithmetic {
209        return false;
210    }
211
212    // Unit type: *const () or *mut ()
213    if (lower.contains("*const ()") || lower.contains("*mut ()")) && has_arithmetic {
214        return true;
215    }
216
217    // PhantomData
218    if lower.contains("phantomdata") && has_arithmetic {
219        return true;
220    }
221
222    // PhantomPinned
223    if lower.contains("phantompinned") && has_arithmetic {
224        return true;
225    }
226
227    // Empty tuple/array patterns
228    if (lower.contains("*const [(); 0]") || lower.contains("*mut [(); 0]")) && has_arithmetic {
229        return true;
230    }
231
232    // ZST naming conventions
233    if (lower.contains("_zst") || lower.contains("zst_")) && has_arithmetic {
234        return true;
235    }
236
237    let empty_type_patterns = [
238        "emptystruct",
239        "emptyenum",
240        "emptytype",
241        "empty_struct",
242        "empty_enum",
243        "empty_type",
244        "unitstruct",
245        "unitenum",
246        "unittype",
247        "unit_struct",
248        "unit_enum",
249        "unit_type",
250        "markerstruct",
251        "markerenum",
252        "markertype",
253        "marker_struct",
254        "marker_enum",
255        "marker_type",
256        "zststruct",
257        "zstenum",
258        "zsttype",
259        "zst_struct",
260        "zst_enum",
261        "zst_type",
262    ];
263    if empty_type_patterns.iter().any(|p| lower.contains(p)) && has_arithmetic {
264        return true;
265    }
266
267    false
268}
269
270/// Check if text contains a word (case-insensitive, respecting word boundaries)
271pub(crate) fn text_contains_word_case_insensitive(text: &str, needle: &str) -> bool {
272    if needle.is_empty() {
273        return false;
274    }
275
276    let target = needle.to_lowercase();
277    text.to_lowercase()
278        .split(|c: char| !(c.is_alphanumeric() || c == '_'))
279        .any(|token| token == target)
280}
281
282/// Strip comments from a line, tracking block comment state
283pub(crate) fn strip_comments(line: &str, in_block_comment: &mut bool) -> String {
284    let mut result = String::with_capacity(line.len());
285    let bytes = line.as_bytes();
286    let mut idx = 0usize;
287
288    while idx < bytes.len() {
289        if *in_block_comment {
290            if bytes[idx] == b'*' && idx + 1 < bytes.len() && bytes[idx + 1] == b'/' {
291                *in_block_comment = false;
292                idx += 2;
293            } else {
294                idx += 1;
295            }
296            continue;
297        }
298
299        if bytes[idx] == b'/' && idx + 1 < bytes.len() {
300            match bytes[idx + 1] {
301                b'/' => break,
302                b'*' => {
303                    *in_block_comment = true;
304                    idx += 2;
305                    continue;
306                }
307                _ => {}
308            }
309        }
310
311        result.push(bytes[idx] as char);
312        idx += 1;
313    }
314    result
315}
316
317// ============================================================================
318// RUSTCOLA001: Box::into_raw
319// ============================================================================
320
321pub struct BoxIntoRawRule {
322    metadata: RuleMetadata,
323}
324
325impl BoxIntoRawRule {
326    pub fn new() -> Self {
327        Self {
328            metadata: RuleMetadata {
329                id: "RUSTCOLA001".to_string(),
330                name: "box-into-raw".to_string(),
331                short_description: "Conversion of managed pointer into raw pointer".to_string(),
332                full_description: "Detects conversions such as Box::into_raw that hand out raw pointers across FFI boundaries.".to_string(),
333                help_uri: None,
334                default_severity: Severity::Medium,
335                origin: RuleOrigin::BuiltIn,
336                cwe_ids: Vec::new(),
337                fix_suggestion: None,
338                exploitability: Exploitability::default(),
339            },
340        }
341    }
342
343    fn patterns() -> &'static [&'static str] {
344        &[
345            "Box::into_raw",
346            "CString::into_raw",
347            "Arc::into_raw",
348            ".into_raw()",
349        ]
350    }
351}
352
353impl Rule for BoxIntoRawRule {
354    fn metadata(&self) -> &RuleMetadata {
355        &self.metadata
356    }
357
358    fn evaluate(
359        &self,
360        package: &MirPackage,
361        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
362    ) -> Vec<Finding> {
363        if package.crate_name == "mir-extractor" {
364            return Vec::new();
365        }
366
367        let mut findings = Vec::new();
368
369        for function in &package.functions {
370            let evidence = collect_matches(&function.body, Self::patterns());
371            if evidence.is_empty() {
372                continue;
373            }
374
375            findings.push(Finding {
376                rule_id: self.metadata.id.clone(),
377                rule_name: self.metadata.name.clone(),
378                severity: self.metadata.default_severity,
379                message: format!(
380                    "Potential raw pointer escape via into_raw detected in `{}`",
381                    function.name
382                ),
383                function: function.name.clone(),
384                function_signature: function.signature.clone(),
385                evidence,
386                span: function.span.clone(),
387                confidence: Confidence::Medium,
388                cwe_ids: Vec::new(),
389                fix_suggestion: None,
390                code_snippet: None,
391                exploitability: Exploitability::default(),
392                exploitability_score: Exploitability::default().score(),
393            ..Default::default()
394            });
395        }
396
397        findings
398    }
399}
400
401// ============================================================================
402// RUSTCOLA002: std::mem::transmute
403// ============================================================================
404
405pub struct TransmuteRule {
406    metadata: RuleMetadata,
407}
408
409impl TransmuteRule {
410    pub fn new() -> Self {
411        Self {
412            metadata: RuleMetadata {
413                id: "RUSTCOLA002".to_string(),
414                name: "std-mem-transmute".to_string(),
415                short_description: "Usage of std::mem::transmute".to_string(),
416                full_description: "Highlights calls to std::mem::transmute, which may indicate unsafe type conversions that require careful review.".to_string(),
417                help_uri: None,
418                default_severity: Severity::High,
419                origin: RuleOrigin::BuiltIn,
420                cwe_ids: Vec::new(),
421                fix_suggestion: None,
422                exploitability: Exploitability::default(),
423            },
424        }
425    }
426
427    fn line_contains_transmute_call(line: &str) -> bool {
428        let mut search_start = 0usize;
429        while let Some(relative_idx) = line[search_start..].find("transmute") {
430            let idx = search_start + relative_idx;
431            let before_non_ws = line[..idx].chars().rev().find(|c| !c.is_whitespace());
432            if before_non_ws
433                .map(|c| c.is_alphanumeric() || c == '_')
434                .unwrap_or(false)
435            {
436                search_start = idx + "transmute".len();
437                continue;
438            }
439
440            let after = &line[idx + "transmute".len()..];
441            let after_trimmed = after.trim_start();
442
443            if after_trimmed.starts_with('(') || after_trimmed.starts_with("::<") {
444                return true;
445            }
446
447            search_start = idx + "transmute".len();
448        }
449
450        false
451    }
452
453    fn collect_transmute_lines(body: &[String]) -> Vec<String> {
454        let mut state = StringLiteralState::default();
455        let mut lines = Vec::new();
456
457        for raw_line in body {
458            let (sanitized, next_state) = strip_string_literals(state, raw_line);
459            state = next_state;
460
461            let trimmed = sanitized.trim_start();
462            if trimmed.starts_with("//") || trimmed.starts_with("/*") {
463                continue;
464            }
465
466            if Self::line_contains_transmute_call(&sanitized) {
467                lines.push(raw_line.trim().to_string());
468            }
469        }
470
471        lines
472    }
473}
474
475impl Rule for TransmuteRule {
476    fn metadata(&self) -> &RuleMetadata {
477        &self.metadata
478    }
479
480    fn evaluate(
481        &self,
482        package: &MirPackage,
483        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
484    ) -> Vec<Finding> {
485        let mut findings = Vec::new();
486        for function in &package.functions {
487            let transmute_lines = Self::collect_transmute_lines(&function.body);
488
489            if !transmute_lines.is_empty() {
490                findings.push(Finding {
491                    rule_id: self.metadata.id.clone(),
492                    rule_name: self.metadata.name.clone(),
493                    severity: self.metadata.default_severity,
494                    message: format!("Use of std::mem::transmute detected in `{}`", function.name),
495                    function: function.name.clone(),
496                    function_signature: function.signature.clone(),
497                    evidence: transmute_lines,
498                    span: function.span.clone(),
499                    confidence: Confidence::Medium,
500                    cwe_ids: Vec::new(),
501                    fix_suggestion: None,
502                    code_snippet: None,
503                    exploitability: Exploitability::default(),
504                    exploitability_score: Exploitability::default().score(),
505                ..Default::default()
506                });
507            }
508        }
509        findings
510    }
511}
512
513// ============================================================================
514// RUSTCOLA003: Unsafe Usage
515// ============================================================================
516
517pub struct UnsafeUsageRule {
518    metadata: RuleMetadata,
519}
520
521impl UnsafeUsageRule {
522    pub fn new() -> Self {
523        Self {
524            metadata: RuleMetadata {
525                id: "RUSTCOLA003".to_string(),
526                name: "unsafe-usage".to_string(),
527                short_description: "Unsafe function or block detected".to_string(),
528                full_description: "Flags functions marked unsafe or containing unsafe blocks, highlighting code that requires careful review.".to_string(),
529                help_uri: None,
530                default_severity: Severity::High,
531                origin: RuleOrigin::BuiltIn,
532                cwe_ids: Vec::new(),
533                fix_suggestion: None,
534                exploitability: Exploitability::default(),
535            },
536        }
537    }
538
539    fn gather_evidence(&self, function: &MirFunction) -> Vec<String> {
540        let mut evidence = Vec::new();
541        let mut seen = HashSet::new();
542
543        let (sanitized_sig, _) =
544            strip_string_literals(StringLiteralState::default(), &function.signature);
545        if text_contains_word_case_insensitive(&sanitized_sig, "unsafe") {
546            let sig = format!("signature: {}", function.signature.trim());
547            if seen.insert(sig.clone()) {
548                evidence.push(sig);
549            }
550        }
551
552        let mut state = StringLiteralState::default();
553        let mut in_block_comment = false;
554
555        for line in &function.body {
556            let (sanitized, next_state) = strip_string_literals(state, line);
557            state = next_state;
558
559            let without_comments = strip_comments(&sanitized, &mut in_block_comment);
560            if text_contains_word_case_insensitive(&without_comments, "unsafe") {
561                let entry = line.trim().to_string();
562                if seen.insert(entry.clone()) {
563                    evidence.push(entry);
564                }
565            }
566        }
567
568        evidence
569    }
570}
571
572impl Rule for UnsafeUsageRule {
573    fn metadata(&self) -> &RuleMetadata {
574        &self.metadata
575    }
576
577    fn evaluate(
578        &self,
579        package: &MirPackage,
580        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
581    ) -> Vec<Finding> {
582        let mut findings = Vec::new();
583        for function in &package.functions {
584            // Skip self-analysis
585            if package.crate_name == "mir-extractor" {
586                continue;
587            }
588
589            let evidence = self.gather_evidence(function);
590            if evidence.is_empty() {
591                continue;
592            }
593
594            findings.push(Finding {
595                rule_id: self.metadata.id.clone(),
596                rule_name: self.metadata.name.clone(),
597                severity: self.metadata.default_severity,
598                message: format!("Unsafe code detected in `{}`", function.name),
599                function: function.name.clone(),
600                function_signature: function.signature.clone(),
601                evidence,
602                span: function.span.clone(),
603                confidence: Confidence::Medium,
604                cwe_ids: Vec::new(),
605                fix_suggestion: None,
606                code_snippet: None,
607                exploitability: Exploitability::default(),
608                exploitability_score: Exploitability::default().score(),
609            ..Default::default()
610            });
611        }
612
613        findings
614    }
615}
616
617// ============================================================================
618// RUSTCOLA063: Null Pointer Transmute
619// ============================================================================
620
621pub struct NullPointerTransmuteRule {
622    metadata: RuleMetadata,
623}
624
625impl NullPointerTransmuteRule {
626    pub fn new() -> Self {
627        Self {
628            metadata: RuleMetadata {
629                id: "RUSTCOLA063".to_string(),
630                name: "null-pointer-transmute".to_string(),
631                short_description: "Null pointer transmuted to reference or function pointer".to_string(),
632                full_description: "Detects transmute operations involving null pointers, which cause undefined behavior. This includes transmuting zero/null to references, function pointers, or other non-nullable types. Use proper Option types or explicit null checks instead. Sonar RSPEC-7427 parity.".to_string(),
633                help_uri: Some("https://rules.sonarsource.com/rust/RSPEC-7427/".to_string()),
634                default_severity: Severity::High,
635                origin: RuleOrigin::BuiltIn,
636                cwe_ids: Vec::new(),
637                fix_suggestion: None,
638                exploitability: Exploitability::default(),
639            },
640        }
641    }
642}
643
644impl Rule for NullPointerTransmuteRule {
645    fn metadata(&self) -> &RuleMetadata {
646        &self.metadata
647    }
648
649    fn evaluate(
650        &self,
651        package: &MirPackage,
652        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
653    ) -> Vec<Finding> {
654        if package.crate_name == "mir-extractor" {
655            return Vec::new();
656        }
657
658        let mut findings = Vec::new();
659
660        for function in &package.functions {
661            if function.name.contains("NullPointerTransmuteRule")
662                || function.name.contains("looks_like_null_pointer_transmute")
663            {
664                continue;
665            }
666
667            let evidence: Vec<String> = function
668                .body
669                .iter()
670                .filter(|line| looks_like_null_pointer_transmute(line))
671                .map(|line| line.trim().to_string())
672                .collect();
673
674            if evidence.is_empty() {
675                continue;
676            }
677
678            findings.push(Finding {
679                rule_id: self.metadata.id.clone(),
680                rule_name: self.metadata.name.clone(),
681                severity: self.metadata.default_severity,
682                message: format!(
683                    "Null pointer transmuted to non-nullable type in `{}`",
684                    function.name
685                ),
686                function: function.name.clone(),
687                function_signature: function.signature.clone(),
688                evidence,
689                span: function.span.clone(),
690                confidence: Confidence::Medium,
691                cwe_ids: Vec::new(),
692                fix_suggestion: None,
693                code_snippet: None,
694                exploitability: Exploitability::default(),
695                exploitability_score: Exploitability::default().score(),
696            ..Default::default()
697            });
698        }
699
700        findings
701    }
702}
703
704// ============================================================================
705// RUSTCOLA064: ZST Pointer Arithmetic
706// ============================================================================
707
708pub struct ZSTPointerArithmeticRule {
709    metadata: RuleMetadata,
710}
711
712impl ZSTPointerArithmeticRule {
713    pub fn new() -> Self {
714        Self {
715            metadata: RuleMetadata {
716                id: "RUSTCOLA064".to_string(),
717                name: "zst-pointer-arithmetic".to_string(),
718                short_description: "Pointer arithmetic on zero-sized types".to_string(),
719                full_description: "Detects pointer arithmetic operations (offset, add, sub, etc.) on pointers to zero-sized types (ZSTs) like (), PhantomData, or empty structs. Such operations are usually undefined behavior since ZSTs have no meaningful memory layout. Sonar RSPEC-7428 parity.".to_string(),
720                help_uri: Some("https://rules.sonarsource.com/rust/RSPEC-7428/".to_string()),
721                default_severity: Severity::High,
722                origin: RuleOrigin::BuiltIn,
723                cwe_ids: Vec::new(),
724                fix_suggestion: None,
725                exploitability: Exploitability::default(),
726            },
727        }
728    }
729}
730
731impl Rule for ZSTPointerArithmeticRule {
732    fn metadata(&self) -> &RuleMetadata {
733        &self.metadata
734    }
735
736    fn evaluate(
737        &self,
738        package: &MirPackage,
739        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
740    ) -> Vec<Finding> {
741        if package.crate_name == "mir-extractor" {
742            return Vec::new();
743        }
744
745        let mut findings = Vec::new();
746
747        for function in &package.functions {
748            if function.name.contains("ZSTPointerArithmeticRule")
749                || function.name.contains("looks_like_zst_pointer_arithmetic")
750            {
751                continue;
752            }
753
754            let evidence: Vec<String> = function
755                .body
756                .iter()
757                .filter(|line| looks_like_zst_pointer_arithmetic(line))
758                .map(|line| line.trim().to_string())
759                .collect();
760
761            if evidence.is_empty() {
762                continue;
763            }
764
765            findings.push(Finding {
766                rule_id: self.metadata.id.clone(),
767                rule_name: self.metadata.name.clone(),
768                severity: self.metadata.default_severity,
769                message: format!(
770                    "Pointer arithmetic on zero-sized type detected in `{}`",
771                    function.name
772                ),
773                function: function.name.clone(),
774                function_signature: function.signature.clone(),
775                evidence,
776                span: function.span.clone(),
777                confidence: Confidence::Medium,
778                cwe_ids: Vec::new(),
779                fix_suggestion: None,
780                code_snippet: None,
781                exploitability: Exploitability::default(),
782                exploitability_score: Exploitability::default().score(),
783            ..Default::default()
784            });
785        }
786
787        findings
788    }
789}
790
791// ============================================================================
792// RUSTCOLA008: Vec::set_len
793// ============================================================================
794
795const VEC_SET_LEN_SYMBOL: &str = concat!("Vec", "::", "set", "_len");
796
797pub struct VecSetLenRule {
798    metadata: RuleMetadata,
799}
800
801impl VecSetLenRule {
802    pub fn new() -> Self {
803        Self {
804            metadata: RuleMetadata {
805                id: "RUSTCOLA008".to_string(),
806                name: "vec-set-len".to_string(),
807                short_description: format!("Potential misuse of {}", VEC_SET_LEN_SYMBOL),
808                full_description: format!(
809                    "Flags calls to {} which can lead to uninitialized memory exposure if not followed by proper writes.",
810                    VEC_SET_LEN_SYMBOL
811                ),
812                help_uri: None,
813                default_severity: Severity::High,
814                origin: RuleOrigin::BuiltIn,
815                cwe_ids: Vec::new(),
816                fix_suggestion: None,
817                exploitability: Exploitability::default(),
818            },
819        }
820    }
821
822    fn gather_evidence(&self, function: &MirFunction) -> Vec<String> {
823        let mut evidence = Vec::new();
824        let mut seen = HashSet::new();
825        let mut state = StringLiteralState::default();
826        let mut in_block_comment = false;
827
828        for line in &function.body {
829            let (sanitized, next_state) = strip_string_literals(state, line);
830            state = next_state;
831
832            let without_comments = strip_comments(&sanitized, &mut in_block_comment);
833            let trimmed = without_comments.trim_start();
834            if trimmed.starts_with("0x") || without_comments.contains('│') {
835                continue;
836            }
837
838            let has_call = without_comments.contains("set_len(");
839            let has_turbofish = without_comments.contains("set_len::<");
840            if has_call || has_turbofish {
841                let entry = line.trim().to_string();
842                if seen.insert(entry.clone()) {
843                    evidence.push(entry);
844                }
845            }
846        }
847
848        evidence
849    }
850}
851
852impl Rule for VecSetLenRule {
853    fn metadata(&self) -> &RuleMetadata {
854        &self.metadata
855    }
856
857    fn evaluate(
858        &self,
859        package: &MirPackage,
860        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
861    ) -> Vec<Finding> {
862        // Skip self-analysis
863        if package.crate_name == "mir-extractor" {
864            return Vec::new();
865        }
866
867        let mut findings = Vec::new();
868
869        for function in &package.functions {
870            let evidence = self.gather_evidence(function);
871            if evidence.is_empty() {
872                continue;
873            }
874
875            findings.push(Finding {
876                rule_id: self.metadata.id.clone(),
877                rule_name: self.metadata.name.clone(),
878                severity: self.metadata.default_severity,
879                message: format!(
880                    "{} used in `{}`; ensure elements are initialized",
881                    VEC_SET_LEN_SYMBOL, function.name
882                ),
883                function: function.name.clone(),
884                function_signature: function.signature.clone(),
885                evidence,
886                span: function.span.clone(),
887                confidence: Confidence::Medium,
888                cwe_ids: Vec::new(),
889                fix_suggestion: None,
890                code_snippet: None,
891                exploitability: Exploitability::default(),
892                exploitability_score: Exploitability::default().score(),
893            ..Default::default()
894            });
895        }
896
897        findings
898    }
899}
900
901// ============================================================================
902// RUSTCOLA009: MaybeUninit::assume_init
903// ============================================================================
904
905const MAYBE_UNINIT_TYPE_SYMBOL: &str = concat!("Maybe", "Uninit");
906const MAYBE_UNINIT_ASSUME_INIT_SYMBOL: &str = concat!("assume", "_init");
907
908pub struct MaybeUninitAssumeInitRule {
909    metadata: RuleMetadata,
910}
911
912impl MaybeUninitAssumeInitRule {
913    pub fn new() -> Self {
914        Self {
915            metadata: RuleMetadata {
916                id: "RUSTCOLA009".to_string(),
917                name: "maybeuninit-assume-init".to_string(),
918                short_description: format!(
919                    "{}::{} usage",
920                    MAYBE_UNINIT_TYPE_SYMBOL, MAYBE_UNINIT_ASSUME_INIT_SYMBOL
921                ),
922                full_description: format!(
923                    "Highlights {}::{} calls which require careful initialization guarantees.",
924                    MAYBE_UNINIT_TYPE_SYMBOL, MAYBE_UNINIT_ASSUME_INIT_SYMBOL
925                ),
926                help_uri: None,
927                default_severity: Severity::High,
928                origin: RuleOrigin::BuiltIn,
929                cwe_ids: Vec::new(),
930                fix_suggestion: None,
931                exploitability: Exploitability::default(),
932            },
933        }
934    }
935}
936
937impl Rule for MaybeUninitAssumeInitRule {
938    fn metadata(&self) -> &RuleMetadata {
939        &self.metadata
940    }
941
942    fn evaluate(
943        &self,
944        package: &MirPackage,
945        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
946    ) -> Vec<Finding> {
947        // Skip self-analysis
948        if package.crate_name == "mir-extractor" {
949            return Vec::new();
950        }
951
952        let mut findings = Vec::new();
953        let patterns = ["assume_init", "assume_init_ref"];
954
955        for function in &package.functions {
956            let evidence = collect_matches(&function.body, &patterns);
957            if evidence.is_empty() {
958                continue;
959            }
960
961            findings.push(Finding {
962                rule_id: self.metadata.id.clone(),
963                rule_name: self.metadata.name.clone(),
964                severity: self.metadata.default_severity,
965                message: format!(
966                    "{}::{} detected in `{}`",
967                    MAYBE_UNINIT_TYPE_SYMBOL, MAYBE_UNINIT_ASSUME_INIT_SYMBOL, function.name
968                ),
969                function: function.name.clone(),
970                function_signature: function.signature.clone(),
971                evidence,
972                span: function.span.clone(),
973                confidence: Confidence::Medium,
974                cwe_ids: Vec::new(),
975                fix_suggestion: None,
976                code_snippet: None,
977                exploitability: Exploitability::default(),
978                exploitability_score: Exploitability::default().score(),
979            ..Default::default()
980            });
981        }
982
983        findings
984    }
985}
986
987// ============================================================================
988// RUSTCOLA010: mem::uninitialized / mem::zeroed
989// ============================================================================
990
991const MEM_MODULE_SYMBOL: &str = concat!("mem");
992const MEM_UNINITIALIZED_SYMBOL: &str = concat!("uninitialized");
993const MEM_ZEROED_SYMBOL: &str = concat!("zeroed");
994
995pub struct MemUninitZeroedRule {
996    metadata: RuleMetadata,
997}
998
999impl MemUninitZeroedRule {
1000    pub fn new() -> Self {
1001        Self {
1002            metadata: RuleMetadata {
1003                id: "RUSTCOLA010".to_string(),
1004                name: "mem-uninit-zeroed".to_string(),
1005                short_description: format!(
1006                    "Use of {}::{} or {}::{}",
1007                    MEM_MODULE_SYMBOL,
1008                    MEM_UNINITIALIZED_SYMBOL,
1009                    MEM_MODULE_SYMBOL,
1010                    MEM_ZEROED_SYMBOL
1011                ),
1012                full_description: format!(
1013                    "Flags deprecated zero-initialization APIs such as {}::{} and {}::{} which can lead to undefined behavior on non-zero types.",
1014                    MEM_MODULE_SYMBOL,
1015                    MEM_UNINITIALIZED_SYMBOL,
1016                    MEM_MODULE_SYMBOL,
1017                    MEM_ZEROED_SYMBOL
1018                ),
1019                help_uri: None,
1020                default_severity: Severity::High,
1021                origin: RuleOrigin::BuiltIn,
1022                cwe_ids: Vec::new(),
1023                fix_suggestion: None,
1024                exploitability: Exploitability::default(),
1025            },
1026        }
1027    }
1028}
1029
1030impl Rule for MemUninitZeroedRule {
1031    fn metadata(&self) -> &RuleMetadata {
1032        &self.metadata
1033    }
1034
1035    fn evaluate(
1036        &self,
1037        package: &MirPackage,
1038        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1039    ) -> Vec<Finding> {
1040        // Skip self-analysis
1041        if package.crate_name == "mir-extractor" {
1042            return Vec::new();
1043        }
1044
1045        let mut findings = Vec::new();
1046        let patterns = [
1047            format!("{}::{}", MEM_MODULE_SYMBOL, MEM_UNINITIALIZED_SYMBOL),
1048            format!("{}::{}", MEM_MODULE_SYMBOL, MEM_ZEROED_SYMBOL),
1049            "::uninitialized()".to_string(),
1050            "::zeroed()".to_string(),
1051        ];
1052        let pattern_refs: Vec<_> = patterns.iter().map(|s| s.as_str()).collect();
1053
1054        for function in &package.functions {
1055            let evidence = collect_matches(&function.body, &pattern_refs);
1056            if evidence.is_empty() {
1057                continue;
1058            }
1059
1060            findings.push(Finding {
1061                rule_id: self.metadata.id.clone(),
1062                rule_name: self.metadata.name.clone(),
1063                severity: self.metadata.default_severity,
1064                message: format!(
1065                    "Deprecated zero-initialization detected in `{}` via {}::{} or {}::{}",
1066                    function.name,
1067                    MEM_MODULE_SYMBOL,
1068                    MEM_UNINITIALIZED_SYMBOL,
1069                    MEM_MODULE_SYMBOL,
1070                    MEM_ZEROED_SYMBOL
1071                ),
1072                function: function.name.clone(),
1073                function_signature: function.signature.clone(),
1074                evidence,
1075                span: function.span.clone(),
1076                confidence: Confidence::Medium,
1077                cwe_ids: Vec::new(),
1078                fix_suggestion: None,
1079                code_snippet: None,
1080                exploitability: Exploitability::default(),
1081                exploitability_score: Exploitability::default().score(),
1082            ..Default::default()
1083            });
1084        }
1085
1086        findings
1087    }
1088}
1089
1090// ============================================================================
1091// RUSTCOLA073: NonNull::new_unchecked
1092// ============================================================================
1093
1094pub struct NonNullNewUncheckedRule {
1095    metadata: RuleMetadata,
1096}
1097
1098impl NonNullNewUncheckedRule {
1099    pub fn new() -> Self {
1100        Self {
1101            metadata: RuleMetadata {
1102                id: "RUSTCOLA073".to_string(),
1103                name: "nonnull-new-unchecked".to_string(),
1104                short_description: "NonNull::new_unchecked usage without null check".to_string(),
1105                full_description: "Detects usage of NonNull::new_unchecked which assumes the pointer is non-null. Using this with a null pointer is undefined behavior.".to_string(),
1106                help_uri: None,
1107                default_severity: Severity::High,
1108                origin: RuleOrigin::BuiltIn,
1109                cwe_ids: Vec::new(),
1110                fix_suggestion: None,
1111                exploitability: Exploitability::default(),
1112            },
1113        }
1114    }
1115}
1116
1117impl Rule for NonNullNewUncheckedRule {
1118    fn metadata(&self) -> &RuleMetadata {
1119        &self.metadata
1120    }
1121
1122    fn evaluate(
1123        &self,
1124        package: &MirPackage,
1125        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1126    ) -> Vec<Finding> {
1127        // Skip analyzer's own crate to avoid self-referential warnings
1128        if package.crate_name == "mir-extractor" {
1129            return Vec::new();
1130        }
1131
1132        let mut findings = Vec::new();
1133
1134        for function in &package.functions {
1135            let evidence: Vec<String> = function
1136                .body
1137                .iter()
1138                .filter(|line| line.contains("NonNull") && line.contains("new_unchecked"))
1139                .map(|line| line.trim().to_string())
1140                .collect();
1141
1142            if !evidence.is_empty() {
1143                findings.push(Finding {
1144                    rule_id: self.metadata.id.clone(),
1145                    rule_name: self.metadata.name.clone(),
1146                    severity: self.metadata.default_severity,
1147                    message: format!("NonNull::new_unchecked usage in `{}`", function.name),
1148                    function: function.name.clone(),
1149                    function_signature: function.signature.clone(),
1150                    evidence,
1151                    span: function.span.clone(),
1152                    confidence: Confidence::Medium,
1153                    cwe_ids: Vec::new(),
1154                    fix_suggestion: None,
1155                    code_snippet: None,
1156                    exploitability: Exploitability::default(),
1157                    exploitability_score: Exploitability::default().score(),
1158                ..Default::default()
1159                });
1160            }
1161        }
1162
1163        findings
1164    }
1165}
1166
1167// ============================================================================
1168// RUSTCOLA078: mem::forget on guard types
1169// ============================================================================
1170
1171pub struct MemForgetGuardRule {
1172    metadata: RuleMetadata,
1173}
1174
1175impl MemForgetGuardRule {
1176    pub fn new() -> Self {
1177        Self {
1178            metadata: RuleMetadata {
1179                id: "RUSTCOLA078".to_string(),
1180                name: "mem-forget-guard".to_string(),
1181                short_description: "mem::forget on guard types".to_string(),
1182                full_description: "Detects mem::forget called on guard types (MutexGuard, RwLockGuard, etc.) which prevents the lock from being released, potentially causing deadlocks.".to_string(),
1183                help_uri: None,
1184                default_severity: Severity::High,
1185                origin: RuleOrigin::BuiltIn,
1186                cwe_ids: Vec::new(),
1187                fix_suggestion: None,
1188                exploitability: Exploitability::default(),
1189            },
1190        }
1191    }
1192}
1193
1194impl Rule for MemForgetGuardRule {
1195    fn metadata(&self) -> &RuleMetadata {
1196        &self.metadata
1197    }
1198
1199    fn evaluate(
1200        &self,
1201        package: &MirPackage,
1202        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1203    ) -> Vec<Finding> {
1204        // Skip analyzer's own crate to avoid self-referential warnings
1205        if package.crate_name == "mir-extractor" {
1206            return Vec::new();
1207        }
1208
1209        let mut findings = Vec::new();
1210        let guard_types = [
1211            "MutexGuard",
1212            "RwLockReadGuard",
1213            "RwLockWriteGuard",
1214            "RefMut",
1215            "Ref",
1216        ];
1217
1218        for function in &package.functions {
1219            // Check if function has mem::forget with guard types
1220            let has_forget = function
1221                .body
1222                .iter()
1223                .any(|line| line.contains("mem::forget"));
1224            if !has_forget {
1225                continue;
1226            }
1227
1228            let has_guard = function
1229                .body
1230                .iter()
1231                .any(|line| guard_types.iter().any(|g| line.contains(g)));
1232
1233            if has_guard {
1234                let evidence: Vec<String> = function
1235                    .body
1236                    .iter()
1237                    .filter(|line| {
1238                        line.contains("mem::forget") || guard_types.iter().any(|g| line.contains(g))
1239                    })
1240                    .map(|line| line.trim().to_string())
1241                    .collect();
1242
1243                findings.push(Finding {
1244                    rule_id: self.metadata.id.clone(),
1245                    rule_name: self.metadata.name.clone(),
1246                    severity: self.metadata.default_severity,
1247                    message: format!(
1248                        "mem::forget on guard type may cause deadlock in `{}`",
1249                        function.name
1250                    ),
1251                    function: function.name.clone(),
1252                    function_signature: function.signature.clone(),
1253                    evidence,
1254                    span: function.span.clone(),
1255                    confidence: Confidence::Medium,
1256                    cwe_ids: Vec::new(),
1257                    fix_suggestion: None,
1258                    code_snippet: None,
1259                    exploitability: Exploitability::default(),
1260                    exploitability_score: Exploitability::default().score(),
1261                ..Default::default()
1262                });
1263            }
1264        }
1265
1266        findings
1267    }
1268}
1269
1270// ============================================================================
1271// RUSTCOLA025: Static mut global detected
1272// ============================================================================
1273
1274pub struct StaticMutGlobalRule {
1275    metadata: RuleMetadata,
1276}
1277
1278impl StaticMutGlobalRule {
1279    pub fn new() -> Self {
1280        Self {
1281            metadata: RuleMetadata {
1282                id: "RUSTCOLA025".to_string(),
1283                name: "static-mut-global".to_string(),
1284                short_description: "Mutable static global detected".to_string(),
1285                full_description: "Flags uses of `static mut` globals, which are unsafe shared mutable state and can introduce data races or memory safety bugs.".to_string(),
1286                help_uri: None,
1287                default_severity: Severity::High,
1288                origin: RuleOrigin::BuiltIn,
1289                cwe_ids: Vec::new(),
1290                fix_suggestion: None,
1291                exploitability: Exploitability::default(),
1292            },
1293        }
1294    }
1295}
1296
1297impl Rule for StaticMutGlobalRule {
1298    fn metadata(&self) -> &RuleMetadata {
1299        &self.metadata
1300    }
1301
1302    fn evaluate(
1303        &self,
1304        package: &MirPackage,
1305        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1306    ) -> Vec<Finding> {
1307        if package.crate_name == "mir-extractor" {
1308            return Vec::new();
1309        }
1310
1311        let mut findings = Vec::new();
1312        let patterns = ["static mut "];
1313
1314        for function in &package.functions {
1315            let mut evidence = collect_matches(&function.body, &patterns);
1316            if evidence.is_empty() {
1317                continue;
1318            }
1319
1320            // If the signature itself declared a mutable static, include it for additional context.
1321            if function.signature.contains("static mut ") {
1322                evidence.push(function.signature.trim().to_string());
1323            }
1324
1325            findings.push(Finding {
1326                rule_id: self.metadata.id.clone(),
1327                rule_name: self.metadata.name.clone(),
1328                severity: self.metadata.default_severity,
1329                message: format!(
1330                    "Mutable static global detected in `{}`; prefer interior mutability or synchronization primitives",
1331                    function.name
1332                ),
1333                function: function.name.clone(),
1334                function_signature: function.signature.clone(),
1335                evidence,
1336                span: function.span.clone(),
1337                    confidence: Confidence::Medium,
1338                    cwe_ids: Vec::new(),
1339                    fix_suggestion: None,
1340                    code_snippet: None,
1341                exploitability: Exploitability::default(),
1342                exploitability_score: Exploitability::default().score(),
1343            ..Default::default()
1344            });
1345        }
1346
1347        findings
1348    }
1349}
1350
1351// ============================================================================
1352// RUSTCOLA095: Transmute changes reference lifetime
1353// ============================================================================
1354
1355pub struct TransmuteLifetimeChangeRule {
1356    metadata: RuleMetadata,
1357}
1358
1359impl TransmuteLifetimeChangeRule {
1360    pub fn new() -> Self {
1361        Self {
1362            metadata: RuleMetadata {
1363                id: "RUSTCOLA095".to_string(),
1364                name: "transmute-lifetime-change".to_string(),
1365                short_description: "Transmute changes reference lifetime".to_string(),
1366                full_description: "Using std::mem::transmute to change lifetime parameters of references is undefined behavior. It can create references that outlive the data they point to, leading to use-after-free. Use proper lifetime annotations or safe APIs instead.".to_string(),
1367                help_uri: Some("https://doc.rust-lang.org/std/mem/fn.transmute.html#examples".to_string()),
1368                default_severity: Severity::High,
1369                origin: RuleOrigin::BuiltIn,
1370                cwe_ids: Vec::new(),
1371                fix_suggestion: None,
1372                exploitability: Exploitability::default(),
1373            },
1374        }
1375    }
1376
1377    /// Extract the lifetime from a type annotation like "&'a str" or "&'static str"
1378    fn extract_lifetime(type_str: &str) -> Option<String> {
1379        if let Some(quote_pos) = type_str.find('\'') {
1380            let after_quote = &type_str[quote_pos + 1..];
1381            let end_pos = after_quote
1382                .find(|c: char| !c.is_alphanumeric() && c != '_')
1383                .unwrap_or(after_quote.len());
1384            if end_pos > 0 {
1385                return Some(format!("'{}", &after_quote[..end_pos]));
1386            }
1387        }
1388        None
1389    }
1390
1391    /// Check if two types differ only in lifetime parameters
1392    fn types_differ_in_lifetime(from_type: &str, to_type: &str) -> bool {
1393        let from_lifetime = Self::extract_lifetime(from_type);
1394        let to_lifetime = Self::extract_lifetime(to_type);
1395
1396        match (from_lifetime, to_lifetime) {
1397            (Some(from_lt), Some(to_lt)) => {
1398                if from_lt != to_lt {
1399                    let from_is_ref = from_type.contains('&');
1400                    let to_is_ref = to_type.contains('&');
1401                    return from_is_ref && to_is_ref;
1402                }
1403                false
1404            }
1405            (Some(_), None) | (None, Some(_)) => from_type.contains('&') && to_type.contains('&'),
1406            _ => false,
1407        }
1408    }
1409}
1410
1411impl Rule for TransmuteLifetimeChangeRule {
1412    fn metadata(&self) -> &RuleMetadata {
1413        &self.metadata
1414    }
1415
1416    fn evaluate(
1417        &self,
1418        package: &MirPackage,
1419        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1420    ) -> Vec<Finding> {
1421        if package.crate_name == "mir-extractor" {
1422            return Vec::new();
1423        }
1424
1425        let mut findings = Vec::new();
1426        let crate_root = Path::new(&package.crate_root);
1427
1428        if !crate_root.exists() {
1429            return findings;
1430        }
1431
1432        for entry in WalkDir::new(crate_root)
1433            .into_iter()
1434            .filter_entry(|e| filter_entry(e))
1435        {
1436            let entry = match entry {
1437                Ok(e) => e,
1438                Err(_) => continue,
1439            };
1440
1441            if !entry.file_type().is_file() {
1442                continue;
1443            }
1444
1445            let path = entry.path();
1446            if path.extension() != Some(OsStr::new("rs")) {
1447                continue;
1448            }
1449
1450            let rel_path = path
1451                .strip_prefix(crate_root)
1452                .unwrap_or(path)
1453                .to_string_lossy()
1454                .replace('\\', "/");
1455
1456            let content = match fs::read_to_string(path) {
1457                Ok(c) => c,
1458                Err(_) => continue,
1459            };
1460
1461            let lines: Vec<&str> = content.lines().collect();
1462            let mut current_fn_name = String::new();
1463
1464            for (idx, line) in lines.iter().enumerate() {
1465                let trimmed = line.trim();
1466
1467                if trimmed.contains("fn ") {
1468                    if let Some(fn_pos) = trimmed.find("fn ") {
1469                        let after_fn = &trimmed[fn_pos + 3..];
1470                        if let Some(paren_pos) = after_fn.find('(') {
1471                            current_fn_name = after_fn[..paren_pos].trim().to_string();
1472                        }
1473                    }
1474                }
1475
1476                if trimmed.starts_with("//")
1477                    || trimmed.starts_with("*")
1478                    || trimmed.starts_with("/*")
1479                {
1480                    continue;
1481                }
1482
1483                if trimmed.contains("transmute") {
1484                    // Pattern 1: transmute::<From, To>(...)
1485                    if let Some(turbofish_start) = trimmed.find("transmute::<") {
1486                        let after_turbofish = &trimmed[turbofish_start + 12..];
1487                        if let Some(end) = after_turbofish.find(">(") {
1488                            let types_str = &after_turbofish[..end];
1489                            let parts: Vec<&str> = types_str.split(',').collect();
1490                            if parts.len() == 2 {
1491                                let from_type = parts[0].trim();
1492                                let to_type = parts[1].trim();
1493
1494                                if Self::types_differ_in_lifetime(from_type, to_type) {
1495                                    let location = format!("{}:{}", rel_path, idx + 1);
1496                                    findings.push(Finding {
1497                                        rule_id: self.metadata.id.clone(),
1498                                        rule_name: self.metadata.name.clone(),
1499                                        severity: self.metadata.default_severity,
1500                                        message: format!(
1501                                            "Transmute changes lifetime in `{}`: {} -> {}. This can create dangling references.",
1502                                            current_fn_name, from_type, to_type
1503                                        ),
1504                                        function: location,
1505                                        function_signature: current_fn_name.clone(),
1506                                        evidence: vec![trimmed.to_string()],
1507                                        span: None,
1508                    ..Default::default()
1509                                    });
1510                                }
1511                            }
1512                        }
1513                    }
1514
1515                    // Pattern 2: Function signature shows lifetime extension
1516                    let mut fn_sig_line = String::new();
1517                    for back_idx in (0..=idx).rev() {
1518                        let back_line = lines[back_idx].trim();
1519                        if back_line.contains("fn ") && back_line.contains("->") {
1520                            fn_sig_line = back_line.to_string();
1521                            break;
1522                        }
1523                        if back_line.starts_with("pub fn ") || back_line.starts_with("fn ") {
1524                            if !back_line.contains("->") {
1525                                break;
1526                            }
1527                        }
1528                    }
1529
1530                    let sig_has_short_lifetime =
1531                        fn_sig_line.contains("'a") || fn_sig_line.contains("'b");
1532                    let sig_returns_static = fn_sig_line.contains("-> &'static")
1533                        || fn_sig_line.contains("-> StaticData");
1534
1535                    let is_actual_transmute =
1536                        trimmed.contains("transmute(") || trimmed.contains("transmute::<");
1537
1538                    if sig_has_short_lifetime && sig_returns_static && is_actual_transmute {
1539                        let already_reported = findings
1540                            .iter()
1541                            .any(|f| f.function == format!("{}:{}", rel_path, idx + 1));
1542                        if !already_reported {
1543                            let location = format!("{}:{}", rel_path, idx + 1);
1544                            findings.push(Finding {
1545                                rule_id: self.metadata.id.clone(),
1546                                rule_name: self.metadata.name.clone(),
1547                                severity: self.metadata.default_severity,
1548                                message: format!(
1549                                    "Transmute may extend lifetime to 'static in `{}`. This can create dangling references.",
1550                                    current_fn_name
1551                                ),
1552                                function: location,
1553                                function_signature: current_fn_name.clone(),
1554                                evidence: vec![trimmed.to_string()],
1555                                span: None,
1556                    ..Default::default()
1557                            });
1558                        }
1559                    }
1560
1561                    // Pattern 3: Struct with lifetime parameter transmuted to struct without
1562                    if let Some(turbofish_start) = trimmed.find("transmute::<") {
1563                        let after_turbofish = &trimmed[turbofish_start + 12..];
1564                        if let Some(end) = after_turbofish.find(">(") {
1565                            let types_str = &after_turbofish[..end];
1566                            if types_str.contains("<'") || types_str.contains("< '") {
1567                                let mut depth = 0;
1568                                let mut split_pos = None;
1569                                for (i, c) in types_str.char_indices() {
1570                                    match c {
1571                                        '<' => depth += 1,
1572                                        '>' => depth -= 1,
1573                                        ',' if depth == 0 => {
1574                                            split_pos = Some(i);
1575                                            break;
1576                                        }
1577                                        _ => {}
1578                                    }
1579                                }
1580
1581                                if let Some(pos) = split_pos {
1582                                    let from_type = types_str[..pos].trim();
1583                                    let to_type = types_str[pos + 1..].trim();
1584
1585                                    let from_has_lifetime = from_type.contains("'a")
1586                                        || from_type.contains("'b")
1587                                        || from_type.contains("'_");
1588                                    let to_has_static =
1589                                        !to_type.contains('\'') || to_type.contains("'static");
1590
1591                                    if from_has_lifetime && to_has_static {
1592                                        let already_reported = findings.iter().any(|f| {
1593                                            f.function == format!("{}:{}", rel_path, idx + 1)
1594                                        });
1595                                        if !already_reported {
1596                                            let location = format!("{}:{}", rel_path, idx + 1);
1597                                            findings.push(Finding {
1598                                                rule_id: self.metadata.id.clone(),
1599                                                rule_name: self.metadata.name.clone(),
1600                                                severity: self.metadata.default_severity,
1601                                                message: format!(
1602                                                    "Transmute changes struct lifetime in `{}`: {} -> {}. This can create dangling references.",
1603                                                    current_fn_name, from_type, to_type
1604                                                ),
1605                                                function: location,
1606                                                function_signature: current_fn_name.clone(),
1607                                                evidence: vec![trimmed.to_string()],
1608                                                span: None,
1609                    ..Default::default()
1610                                            });
1611                                        }
1612                                    }
1613                                }
1614                            }
1615                        }
1616                    }
1617                }
1618            }
1619        }
1620
1621        findings
1622    }
1623}
1624
1625// ============================================================================
1626// RUSTCOLA096: Raw pointer from local reference escapes function
1627// ============================================================================
1628
1629pub struct RawPointerEscapeRule {
1630    metadata: RuleMetadata,
1631}
1632
1633impl RawPointerEscapeRule {
1634    pub fn new() -> Self {
1635        Self {
1636            metadata: RuleMetadata {
1637                id: "RUSTCOLA096".to_string(),
1638                name: "raw-pointer-escape".to_string(),
1639                short_description: "Raw pointer from local reference escapes function".to_string(),
1640                full_description: "Casting a reference to a raw pointer (`as *const T` or `as *mut T`) and returning it or storing it beyond the reference's lifetime creates a dangling pointer. When the referenced data is dropped or moved, the pointer becomes invalid. Use Box::leak, 'static data, or ensure the caller manages the lifetime.".to_string(),
1641                help_uri: Some("https://doc.rust-lang.org/std/primitive.pointer.html".to_string()),
1642                default_severity: Severity::High,
1643                origin: RuleOrigin::BuiltIn,
1644                cwe_ids: Vec::new(),
1645                fix_suggestion: None,
1646                exploitability: Exploitability::default(),
1647            },
1648        }
1649    }
1650
1651    fn is_ptr_cast(line: &str) -> bool {
1652        line.contains("as *const")
1653            || line.contains("as *mut")
1654            || line.contains(".as_ptr()")
1655            || line.contains(".as_mut_ptr()")
1656    }
1657
1658    /// Check for unsafe dereference patterns that create references outliving pointee
1659    fn is_unsafe_deref_outlive(line: &str) -> bool {
1660        // Pattern: unsafe { &*ptr } or unsafe { &mut *ptr }
1661        (line.contains("&*") || line.contains("&mut *"))
1662            && (line.contains("unsafe") || line.contains("ptr"))
1663    }
1664
1665    /// Check for transmute patterns that extend lifetimes
1666    fn is_lifetime_transmute(line: &str) -> bool {
1667        line.contains("transmute")
1668            && (line.contains("&'") || line.contains("'static") || line.contains("'a"))
1669    }
1670
1671    fn is_return_context(lines: &[&str], idx: usize, ptr_var: &str) -> bool {
1672        let line = lines[idx].trim();
1673
1674        if line.starts_with("return ") && (line.contains("as *const") || line.contains("as *mut")) {
1675            return true;
1676        }
1677
1678        if (line.contains("as *const") || line.contains("as *mut") || line.contains(".as_ptr()"))
1679            && !line.ends_with(';')
1680            && !line.contains("let ")
1681        {
1682            return true;
1683        }
1684
1685        if !ptr_var.is_empty() {
1686            for check_line in lines.iter().skip(idx + 1).take(10) {
1687                let trimmed = check_line.trim();
1688                if trimmed.starts_with("return ") && trimmed.contains(ptr_var) {
1689                    return true;
1690                }
1691                if trimmed.contains(ptr_var) && !trimmed.ends_with(';') && trimmed.ends_with(')') {
1692                    return true;
1693                }
1694                if trimmed.starts_with(ptr_var) && !trimmed.ends_with(';') {
1695                    return true;
1696                }
1697            }
1698        }
1699
1700        false
1701    }
1702
1703    fn is_escape_via_store(lines: &[&str], idx: usize) -> bool {
1704        let line = lines[idx].trim();
1705
1706        if line.contains("ptr:") && (line.contains("as *const") || line.contains("as *mut")) {
1707            return true;
1708        }
1709
1710        if (line.starts_with("*") && line.contains(" = "))
1711            && (line.contains("as *const") || line.contains("as *mut"))
1712        {
1713            if line.contains("&") {
1714                return true;
1715            }
1716        }
1717
1718        if line.contains("GLOBAL") || line.contains("STATIC") {
1719            if line.contains("as *const") || line.contains("as *mut") {
1720                return true;
1721            }
1722        }
1723
1724        false
1725    }
1726
1727    fn is_safe_pattern(lines: &[&str], idx: usize, fn_context: &str) -> bool {
1728        let line = lines[idx].trim();
1729
1730        if fn_context.contains("fn ") && fn_context.contains("(&") {
1731            if !line.contains("let ") && (line.contains(" x ") || line.contains("(x)")) {
1732                return true;
1733            }
1734        }
1735
1736        if line.contains("Box::leak") {
1737            return true;
1738        }
1739
1740        if fn_context.contains("&'static str") {
1741            return true;
1742        }
1743
1744        if line.contains("(ptr,") && (fn_context.contains("Box<") || fn_context.contains("boxed")) {
1745            return true;
1746        }
1747
1748        if fn_context.contains("ManuallyDrop") {
1749            return true;
1750        }
1751
1752        if fn_context.contains("Pin<") {
1753            return true;
1754        }
1755
1756        if line.contains("unsafe {") && line.contains("*ptr") && !line.contains("return") {
1757            return true;
1758        }
1759
1760        let next_lines: String = lines[idx..std::cmp::min(idx + 5, lines.len())]
1761            .iter()
1762            .map(|s| *s)
1763            .collect::<Vec<&str>>()
1764            .join("\n");
1765        if next_lines.contains("unsafe { *ptr }") && !next_lines.contains("return ptr") {
1766            return true;
1767        }
1768
1769        false
1770    }
1771}
1772
1773impl Rule for RawPointerEscapeRule {
1774    fn metadata(&self) -> &RuleMetadata {
1775        &self.metadata
1776    }
1777
1778    fn evaluate(
1779        &self,
1780        package: &MirPackage,
1781        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1782    ) -> Vec<Finding> {
1783        if package.crate_name == "mir-extractor" {
1784            return Vec::new();
1785        }
1786
1787        let mut findings = Vec::new();
1788        let crate_root = Path::new(&package.crate_root);
1789
1790        if !crate_root.exists() {
1791            return findings;
1792        }
1793
1794        for entry in WalkDir::new(crate_root)
1795            .into_iter()
1796            .filter_entry(|e| filter_entry(e))
1797        {
1798            let entry = match entry {
1799                Ok(e) => e,
1800                Err(_) => continue,
1801            };
1802
1803            if !entry.file_type().is_file() {
1804                continue;
1805            }
1806
1807            let path = entry.path();
1808            if path.extension() != Some(OsStr::new("rs")) {
1809                continue;
1810            }
1811
1812            let rel_path = path
1813                .strip_prefix(crate_root)
1814                .unwrap_or(path)
1815                .to_string_lossy()
1816                .replace('\\', "/");
1817
1818            let content = match fs::read_to_string(path) {
1819                Ok(c) => c,
1820                Err(_) => continue,
1821            };
1822
1823            let lines: Vec<&str> = content.lines().collect();
1824            let mut current_fn_name = String::new();
1825            let mut current_fn_start = 0;
1826            let mut returns_ptr = false;
1827
1828            for (idx, line) in lines.iter().enumerate() {
1829                let trimmed = line.trim();
1830
1831                if trimmed.contains("fn ") {
1832                    if let Some(fn_pos) = trimmed.find("fn ") {
1833                        let after_fn = &trimmed[fn_pos + 3..];
1834                        if let Some(paren_pos) = after_fn.find('(') {
1835                            current_fn_name = after_fn[..paren_pos].trim().to_string();
1836                            current_fn_start = idx;
1837                            returns_ptr = trimmed.contains("-> *const")
1838                                || trimmed.contains("-> *mut")
1839                                || trimmed.contains("*const i32")
1840                                || trimmed.contains("*const u8")
1841                                || trimmed.contains("*const str");
1842                        }
1843                    }
1844                }
1845
1846                if trimmed.starts_with("//") || trimmed.starts_with("/*") {
1847                    continue;
1848                }
1849                if trimmed.starts_with("* ") || trimmed == "*" || trimmed.starts_with("*/") {
1850                    continue;
1851                }
1852
1853                if Self::is_ptr_cast(trimmed) {
1854                    let fn_context: String = lines[current_fn_start..=idx.min(lines.len() - 1)]
1855                        .iter()
1856                        .take(20)
1857                        .map(|s| *s)
1858                        .collect::<Vec<&str>>()
1859                        .join("\n");
1860
1861                    if Self::is_safe_pattern(&lines, idx, &fn_context) {
1862                        continue;
1863                    }
1864
1865                    let is_local_cast = trimmed.contains("&x ")
1866                        || trimmed.contains("&local")
1867                        || trimmed.contains("&temp")
1868                        || trimmed.contains("&s ")
1869                        || trimmed.contains("s.as_ptr()")
1870                        || trimmed.contains("s.as_str()")
1871                        || trimmed.contains("&v[");
1872
1873                    let mut ptr_var = String::new();
1874                    if trimmed.contains("let ") && trimmed.contains(" = ") {
1875                        if let Some(eq_pos) = trimmed.find(" = ") {
1876                            let before_eq = &trimmed[..eq_pos];
1877                            if let Some(let_pos) = before_eq.find("let ") {
1878                                ptr_var = before_eq[let_pos + 4..].trim().to_string();
1879                            }
1880                        }
1881                    }
1882
1883                    let escapes_via_return = Self::is_return_context(&lines, idx, &ptr_var);
1884                    let escapes_via_store = Self::is_escape_via_store(&lines, idx);
1885
1886                    let is_deref_assign = trimmed.starts_with("*") && trimmed.contains(" = &");
1887
1888                    // Enhanced: Check for unsafe { &*ptr } outliving pointee
1889                    let is_unsafe_deref = Self::is_unsafe_deref_outlive(trimmed);
1890                    let is_transmute_lifetime = Self::is_lifetime_transmute(trimmed);
1891
1892                    if ((returns_ptr || escapes_via_return || escapes_via_store) && is_local_cast)
1893                        || (is_deref_assign && is_local_cast)
1894                        || (is_unsafe_deref && escapes_via_return)
1895                        || (is_transmute_lifetime && (returns_ptr || escapes_via_return))
1896                    {
1897                        let location = format!("{}:{}", rel_path, idx + 1);
1898                        findings.push(Finding {
1899                            rule_id: self.metadata.id.clone(),
1900                            rule_name: self.metadata.name.clone(),
1901                            severity: self.metadata.default_severity,
1902                            message: format!(
1903                                "Raw pointer from local reference escapes function `{}`. This creates a dangling pointer when the local is dropped.",
1904                                current_fn_name
1905                            ),
1906                            function: location,
1907                            function_signature: current_fn_name.clone(),
1908                            evidence: vec![trimmed.to_string()],
1909                            span: None,
1910                    ..Default::default()
1911                        });
1912                    }
1913                }
1914            }
1915        }
1916
1917        findings
1918    }
1919}
1920
1921// ============================================================================
1922// RUSTCOLA038: Vec::set_len called on uninitialized vector (dataflow)
1923// ============================================================================
1924
1925pub struct VecSetLenMisuseRule {
1926    metadata: RuleMetadata,
1927}
1928
1929impl VecSetLenMisuseRule {
1930    pub fn new() -> Self {
1931        Self {
1932            metadata: RuleMetadata {
1933                id: "RUSTCOLA038".to_string(),
1934                name: "vec-set-len-misuse".to_string(),
1935                short_description: "Vec::set_len called on uninitialized vector".to_string(),
1936                full_description: "Detects Vec::set_len calls where the vector may not be fully initialized. Calling set_len without ensuring all elements are initialized leads to undefined behavior when accessing uninitialized memory. Use Vec::resize, Vec::resize_with, or manually initialize elements before calling set_len.".to_string(),
1937                help_uri: Some("https://doc.rust-lang.org/std/vec/struct.Vec.html#method.set_len".to_string()),
1938                default_severity: Severity::High,
1939                origin: RuleOrigin::BuiltIn,
1940                cwe_ids: Vec::new(),
1941                fix_suggestion: None,
1942                exploitability: Exploitability::default(),
1943            },
1944        }
1945    }
1946
1947    fn initialization_methods() -> &'static [&'static str] {
1948        &[
1949            ".push(",
1950            ".extend(",
1951            ".insert(",
1952            ".resize(",
1953            ".resize_with(",
1954            "Vec::from(",
1955            "vec![",
1956            ".clone()",
1957            ".to_vec()",
1958        ]
1959    }
1960}
1961
1962impl Rule for VecSetLenMisuseRule {
1963    fn metadata(&self) -> &RuleMetadata {
1964        &self.metadata
1965    }
1966
1967    fn evaluate(
1968        &self,
1969        package: &MirPackage,
1970        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1971    ) -> Vec<Finding> {
1972        if package.crate_name == "mir-extractor" {
1973            return Vec::new();
1974        }
1975
1976        let mut findings = Vec::new();
1977        let crate_root = Path::new(&package.crate_root);
1978
1979        if !crate_root.exists() {
1980            return findings;
1981        }
1982
1983        for entry in WalkDir::new(crate_root)
1984            .into_iter()
1985            .filter_entry(|e| filter_entry(e))
1986        {
1987            let entry = match entry {
1988                Ok(e) => e,
1989                Err(_) => continue,
1990            };
1991
1992            if !entry.file_type().is_file() {
1993                continue;
1994            }
1995
1996            let path = entry.path();
1997            if path.extension() != Some(OsStr::new("rs")) {
1998                continue;
1999            }
2000
2001            let rel_path = path
2002                .strip_prefix(crate_root)
2003                .unwrap_or(path)
2004                .to_string_lossy()
2005                .replace('\\', "/");
2006
2007            let content = match fs::read_to_string(path) {
2008                Ok(c) => c,
2009                Err(_) => continue,
2010            };
2011
2012            let lines: Vec<&str> = content.lines().collect();
2013
2014            for (idx, line) in lines.iter().enumerate() {
2015                let trimmed = line.trim();
2016
2017                if trimmed.contains(".set_len(") || trimmed.contains("::set_len(") {
2018                    let mut var_name = None;
2019
2020                    if let Some(pos) = trimmed.find(".set_len(") {
2021                        let before_set_len = &trimmed[..pos];
2022                        if let Some(last_word_start) = before_set_len
2023                            .rfind(|c: char| c.is_whitespace() || c == '(' || c == '{' || c == ';')
2024                        {
2025                            var_name = Some(&before_set_len[last_word_start + 1..]);
2026                        } else {
2027                            var_name = Some(before_set_len);
2028                        }
2029                    }
2030
2031                    if let Some(var) = var_name {
2032                        let mut found_initialization = false;
2033                        let lookback_limit = idx.saturating_sub(50);
2034
2035                        for prev_idx in (lookback_limit..idx).rev() {
2036                            let prev_line = lines[prev_idx];
2037
2038                            for init_method in Self::initialization_methods() {
2039                                if prev_line.contains(var) && prev_line.contains(init_method) {
2040                                    found_initialization = true;
2041                                    break;
2042                                }
2043                            }
2044
2045                            if prev_line.contains(var)
2046                                && (prev_line.contains("[") && prev_line.contains("]=")
2047                                    || prev_line.contains("ptr::write")
2048                                    || prev_line.contains(".as_mut_ptr()"))
2049                            {
2050                                found_initialization = true;
2051                                break;
2052                            }
2053
2054                            if prev_line.contains(var) && prev_line.contains("Vec::with_capacity") {
2055                                found_initialization = false;
2056                                break;
2057                            }
2058
2059                            if prev_line.trim().starts_with("fn ")
2060                                || prev_line.trim().starts_with("pub fn ")
2061                                || prev_line.trim().starts_with("async fn ")
2062                            {
2063                                break;
2064                            }
2065                        }
2066
2067                        if !found_initialization {
2068                            let location = format!("{}:{}", rel_path, idx + 1);
2069
2070                            findings.push(Finding {
2071                                rule_id: self.metadata.id.clone(),
2072                                rule_name: self.metadata.name.clone(),
2073                                severity: self.metadata.default_severity,
2074                                message: format!(
2075                                    "Vec::set_len called on potentially uninitialized vector `{}`",
2076                                    var
2077                                ),
2078                                function: location,
2079                                function_signature: var.to_string(),
2080                                evidence: vec![trimmed.to_string()],
2081                                span: None,
2082                                ..Default::default()
2083                            });
2084                        }
2085                    }
2086                }
2087            }
2088        }
2089
2090        findings
2091    }
2092}
2093
2094// ============================================================================
2095// RUSTCOLA022: Payload length cast to narrower integer
2096// ============================================================================
2097
2098pub struct LengthTruncationCastRule {
2099    metadata: RuleMetadata,
2100}
2101
2102impl LengthTruncationCastRule {
2103    pub fn new() -> Self {
2104        Self {
2105            metadata: RuleMetadata {
2106                id: "RUSTCOLA022".to_string(),
2107                name: "length-truncation-cast".to_string(),
2108                short_description: "Payload length cast to narrower integer".to_string(),
2109                full_description: "Detects casts or try_into conversions that shrink message length fields to 8/16/32-bit integers without bounds checks, potentially smuggling extra bytes past protocol parsers. See RUSTSEC-2024-0363 and RUSTSEC-2024-0365 for PostgreSQL wire protocol examples.".to_string(),
2110                help_uri: Some("https://rustsec.org/advisories/RUSTSEC-2024-0363.html".to_string()),
2111                default_severity: Severity::High,
2112                origin: RuleOrigin::BuiltIn,
2113                cwe_ids: Vec::new(),
2114                fix_suggestion: None,
2115                exploitability: Exploitability::default(),
2116            },
2117        }
2118    }
2119}
2120
2121impl Rule for LengthTruncationCastRule {
2122    fn metadata(&self) -> &RuleMetadata {
2123        &self.metadata
2124    }
2125
2126    fn evaluate(
2127        &self,
2128        package: &MirPackage,
2129        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2130    ) -> Vec<Finding> {
2131        if package.crate_name == "mir-extractor" {
2132            return Vec::new();
2133        }
2134
2135        let mut findings = Vec::new();
2136
2137        for function in &package.functions {
2138            let casts = detect_truncating_len_casts(function);
2139
2140            for cast in casts {
2141                let mut evidence = vec![cast.cast_line.clone()];
2142
2143                if !cast.source_vars.is_empty() {
2144                    evidence.push(format!("length sources: {}", cast.source_vars.join(", ")));
2145                }
2146
2147                for sink in &cast.sink_lines {
2148                    if !evidence.contains(sink) {
2149                        evidence.push(sink.clone());
2150                    }
2151                }
2152
2153                findings.push(Finding {
2154                    rule_id: self.metadata.id.clone(),
2155                    rule_name: self.metadata.name.clone(),
2156                    severity: self.metadata.default_severity,
2157                    message: format!(
2158                        "Potential length truncation before serialization in `{}`",
2159                        function.name
2160                    ),
2161                    function: function.name.clone(),
2162                    function_signature: function.signature.clone(),
2163                    evidence,
2164                    span: function.span.clone(),
2165                    confidence: Confidence::Medium,
2166                    cwe_ids: Vec::new(),
2167                    fix_suggestion: None,
2168                    code_snippet: None,
2169                    exploitability: Exploitability::default(),
2170                    exploitability_score: Exploitability::default().score(),
2171                ..Default::default()
2172                });
2173            }
2174        }
2175
2176        findings
2177    }
2178}
2179
2180// ============================================================================
2181// RUSTCOLA078: MaybeUninit::assume_init without preceding write (dataflow)
2182// ============================================================================
2183
2184pub struct MaybeUninitAssumeInitDataflowRule {
2185    metadata: RuleMetadata,
2186}
2187
2188impl MaybeUninitAssumeInitDataflowRule {
2189    pub fn new() -> Self {
2190        Self {
2191            metadata: RuleMetadata {
2192                id: "RUSTCOLA078".to_string(),
2193                name: "maybeuninit-assume-init-without-write".to_string(),
2194                short_description: "MaybeUninit::assume_init without preceding write".to_string(),
2195                full_description: "Detects MaybeUninit::assume_init() or assume_init_read() calls \
2196                    where no preceding MaybeUninit::write(), write_slice(), or ptr::write() \
2197                    initializes the data. Reading uninitialized memory is undefined behavior and \
2198                    can lead to crashes, data corruption, or security vulnerabilities. Always \
2199                    initialize MaybeUninit values before assuming them initialized."
2200                    .to_string(),
2201                help_uri: Some(
2202                    "https://doc.rust-lang.org/std/mem/union.MaybeUninit.html".to_string(),
2203                ),
2204                default_severity: Severity::High,
2205                origin: RuleOrigin::BuiltIn,
2206                cwe_ids: Vec::new(),
2207                fix_suggestion: None,
2208                exploitability: Exploitability::default(),
2209            },
2210        }
2211    }
2212
2213    fn uninit_creation_patterns() -> &'static [&'static str] {
2214        &[
2215            "MaybeUninit::uninit",
2216            "MaybeUninit::<",
2217            "uninit_array",
2218            "uninit(",
2219        ]
2220    }
2221
2222    fn init_patterns() -> &'static [&'static str] {
2223        &[
2224            ".write(",
2225            "::write(",
2226            "write_slice(",
2227            "ptr::write(",
2228            "ptr::write_bytes(",
2229            "ptr::copy(",
2230            "ptr::copy_nonoverlapping(",
2231            "as_mut_ptr()",
2232            "zeroed(",
2233            "MaybeUninit::new(",
2234        ]
2235    }
2236
2237    fn assume_init_patterns() -> &'static [&'static str] {
2238        &[
2239            "assume_init(",
2240            "assume_init_read(",
2241            "assume_init_ref(",
2242            "assume_init_mut(",
2243            "assume_init_drop(",
2244        ]
2245    }
2246
2247    fn analyze_uninit_flow(body: &[String]) -> Vec<(String, String)> {
2248        let mut uninitialized_vars: HashMap<String, String> = HashMap::new();
2249        let mut initialized_vars: HashSet<String> = HashSet::new();
2250        let mut unsafe_assumes: Vec<(String, String)> = Vec::new();
2251
2252        let creation_patterns = Self::uninit_creation_patterns();
2253        let init_patterns = Self::init_patterns();
2254        let assume_patterns = Self::assume_init_patterns();
2255
2256        for line in body {
2257            let trimmed = line.trim();
2258
2259            let is_creation = creation_patterns.iter().any(|p| trimmed.contains(p));
2260
2261            if is_creation && !trimmed.contains("zeroed") && !trimmed.contains("::new(") {
2262                if let Some(eq_pos) = trimmed.find(" = ") {
2263                    let target = trimmed[..eq_pos].trim();
2264                    if let Some(var) = target
2265                        .split(|c: char| !c.is_alphanumeric() && c != '_')
2266                        .find(|s| s.starts_with('_'))
2267                    {
2268                        uninitialized_vars.insert(var.to_string(), trimmed.to_string());
2269                    }
2270                }
2271            }
2272
2273            let is_init = init_patterns.iter().any(|p| trimmed.contains(p));
2274
2275            if is_init {
2276                for var in uninitialized_vars.keys() {
2277                    if trimmed.contains(var) {
2278                        initialized_vars.insert(var.clone());
2279                    }
2280                }
2281            }
2282
2283            let is_assume = assume_patterns.iter().any(|p| trimmed.contains(p));
2284
2285            if is_assume {
2286                for (var, creation_line) in &uninitialized_vars {
2287                    if trimmed.contains(var) && !initialized_vars.contains(var) {
2288                        unsafe_assumes.push((creation_line.clone(), trimmed.to_string()));
2289                    }
2290                }
2291            }
2292        }
2293
2294        unsafe_assumes
2295    }
2296}
2297
2298impl Rule for MaybeUninitAssumeInitDataflowRule {
2299    fn metadata(&self) -> &RuleMetadata {
2300        &self.metadata
2301    }
2302
2303    fn evaluate(
2304        &self,
2305        package: &MirPackage,
2306        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2307    ) -> Vec<Finding> {
2308        let mut findings = Vec::new();
2309
2310        for function in &package.functions {
2311            if function.name.contains("mir_extractor")
2312                || function.name.contains("mir-extractor")
2313                || function.name.contains("MaybeUninit")
2314            {
2315                continue;
2316            }
2317
2318            let unsafe_assumes = Self::analyze_uninit_flow(&function.body);
2319
2320            for (creation_line, assume_line) in unsafe_assumes {
2321                findings.push(Finding {
2322                    rule_id: self.metadata.id.clone(),
2323                    rule_name: self.metadata.name.clone(),
2324                    severity: self.metadata.default_severity,
2325                    message: format!(
2326                        "MaybeUninit::assume_init() called in `{}` without preceding initialization. \
2327                        Reading uninitialized memory is undefined behavior.",
2328                        function.name
2329                    ),
2330                    function: function.name.clone(),
2331                    function_signature: function.signature.clone(),
2332                    evidence: vec![
2333                        format!("Created: {}", creation_line),
2334                        format!("Assumed: {}", assume_line),
2335                    ],
2336                    span: function.span.clone(),
2337                    confidence: Confidence::Medium,
2338                    cwe_ids: Vec::new(),
2339                    fix_suggestion: None,
2340                    code_snippet: None,
2341                exploitability: Exploitability::default(),
2342                exploitability_score: Exploitability::default().score(),
2343                ..Default::default()
2344                });
2345            }
2346        }
2347
2348        findings
2349    }
2350}
2351
2352// ============================================================================
2353// RUSTCOLA082: Slice element size mismatch in transmute
2354// ============================================================================
2355
2356pub struct SliceElementSizeMismatchRule {
2357    metadata: RuleMetadata,
2358}
2359
2360impl SliceElementSizeMismatchRule {
2361    pub fn new() -> Self {
2362        Self {
2363            metadata: RuleMetadata {
2364                id: "RUSTCOLA082".to_string(),
2365                name: "slice-element-size-mismatch".to_string(),
2366                short_description: "Raw pointer to slice of different element size".to_string(),
2367                full_description: "Detects transmutes between slice types with different \
2368                    element sizes (e.g., &[u8] to &[u32]). This is unsound because the slice \
2369                    length field isn't adjusted for the size difference, causing the new slice \
2370                    to reference memory beyond the original allocation. Use slice::from_raw_parts \
2371                    or slice::align_to instead."
2372                    .to_string(),
2373                default_severity: Severity::High,
2374                origin: RuleOrigin::BuiltIn,
2375                cwe_ids: Vec::new(),
2376                fix_suggestion: None,
2377                help_uri: None,
2378                exploitability: Exploitability::default(),
2379            },
2380        }
2381    }
2382
2383    fn get_primitive_size(type_name: &str) -> Option<usize> {
2384        let inner = type_name
2385            .trim_start_matches('&')
2386            .trim_start_matches("mut ")
2387            .trim_start_matches("*const ")
2388            .trim_start_matches("*mut ")
2389            .trim_start_matches('[')
2390            .trim_end_matches(']');
2391
2392        match inner {
2393            "u8" | "i8" | "bool" => Some(1),
2394            "u16" | "i16" => Some(2),
2395            "u32" | "i32" | "f32" | "char" => Some(4),
2396            "u64" | "i64" | "f64" => Some(8),
2397            "u128" | "i128" => Some(16),
2398            "usize" | "isize" => Some(8),
2399            _ => None,
2400        }
2401    }
2402
2403    fn extract_slice_element_type(type_str: &str) -> Option<String> {
2404        let trimmed = type_str.trim();
2405
2406        if !trimmed.contains('[') || !trimmed.contains(']') {
2407            return None;
2408        }
2409
2410        let start = trimmed.find('[')? + 1;
2411        let end = trimmed.rfind(']')?;
2412
2413        if start >= end {
2414            return None;
2415        }
2416
2417        Some(trimmed[start..end].trim().to_string())
2418    }
2419
2420    fn is_slice_size_mismatch(
2421        from_type: &str,
2422        to_type: &str,
2423    ) -> Option<(String, String, usize, usize)> {
2424        let from_elem = Self::extract_slice_element_type(from_type)?;
2425        let to_elem = Self::extract_slice_element_type(to_type)?;
2426
2427        if from_elem == to_elem {
2428            return None;
2429        }
2430
2431        let from_size = Self::get_primitive_size(&from_elem);
2432        let to_size = Self::get_primitive_size(&to_elem);
2433
2434        match (from_size, to_size) {
2435            (Some(fs), Some(ts)) => {
2436                if fs == ts {
2437                    None
2438                } else {
2439                    Some((from_elem, to_elem, fs, ts))
2440                }
2441            }
2442            (None, None) => Some((from_elem, to_elem, 0, 0)),
2443            _ => Some((
2444                from_elem,
2445                to_elem,
2446                from_size.unwrap_or(0),
2447                to_size.unwrap_or(0),
2448            )),
2449        }
2450    }
2451
2452    fn is_vec_size_mismatch(
2453        from_type: &str,
2454        to_type: &str,
2455    ) -> Option<(String, String, usize, usize)> {
2456        let extract_vec_elem = |t: &str| -> Option<String> {
2457            if !t.contains("Vec<") {
2458                return None;
2459            }
2460            let start = t.find("Vec<")? + 4;
2461            let end = t.rfind('>')?;
2462            if start >= end {
2463                return None;
2464            }
2465            Some(t[start..end].trim().to_string())
2466        };
2467
2468        let from_elem = extract_vec_elem(from_type)?;
2469        let to_elem = extract_vec_elem(to_type)?;
2470
2471        if from_elem == to_elem {
2472            return None;
2473        }
2474
2475        let from_size = Self::get_primitive_size(&from_elem)?;
2476        let to_size = Self::get_primitive_size(&to_elem)?;
2477
2478        if from_size == to_size {
2479            return None;
2480        }
2481
2482        Some((from_elem, to_elem, from_size, to_size))
2483    }
2484
2485    fn parse_transmute_copy_line(line: &str) -> Option<(String, String)> {
2486        let trimmed = line.trim();
2487
2488        if !trimmed.contains("transmute_copy::<") {
2489            return None;
2490        }
2491
2492        let start = trimmed.find("transmute_copy::<")? + 17;
2493        let end = trimmed[start..].find(">")? + start;
2494
2495        let type_params = &trimmed[start..end];
2496
2497        let mut depth = 0;
2498        let mut split_pos = None;
2499        for (i, c) in type_params.char_indices() {
2500            match c {
2501                '<' => depth += 1,
2502                '>' => depth -= 1,
2503                ',' if depth == 0 => {
2504                    split_pos = Some(i);
2505                    break;
2506                }
2507                _ => {}
2508            }
2509        }
2510
2511        let split = split_pos?;
2512        let from_type = type_params[..split].trim().to_string();
2513        let to_type = type_params[split + 1..].trim().to_string();
2514
2515        Some((from_type, to_type))
2516    }
2517}
2518
2519impl Rule for SliceElementSizeMismatchRule {
2520    fn metadata(&self) -> &RuleMetadata {
2521        &self.metadata
2522    }
2523
2524    fn evaluate(
2525        &self,
2526        package: &MirPackage,
2527        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2528    ) -> Vec<Finding> {
2529        let mut findings = Vec::new();
2530
2531        for function in &package.functions {
2532            if function.signature.contains("#[test]") || function.name.contains("test") {
2533                continue;
2534            }
2535
2536            let mut var_types: HashMap<String, String> = HashMap::new();
2537
2538            if let Some(params_start) = function.signature.find('(') {
2539                if let Some(params_end) = function.signature.find(')') {
2540                    let params = &function.signature[params_start + 1..params_end];
2541                    for param in params.split(',') {
2542                        let param = param.trim();
2543                        if let Some(colon_pos) = param.find(':') {
2544                            let var_name = param[..colon_pos].trim();
2545                            let var_type = param[colon_pos + 1..].trim();
2546                            var_types.insert(var_name.to_string(), var_type.to_string());
2547                        }
2548                    }
2549                }
2550            }
2551
2552            for line in &function.body {
2553                let trimmed = line.trim();
2554
2555                if trimmed.starts_with("let ") {
2556                    let rest = trimmed
2557                        .trim_start_matches("let ")
2558                        .trim_start_matches("mut ");
2559                    if let Some(colon_pos) = rest.find(':') {
2560                        let var_name = rest[..colon_pos].trim();
2561                        let type_end = rest.find(';').unwrap_or(rest.len());
2562                        let var_type = rest[colon_pos + 1..type_end].trim();
2563                        var_types.insert(var_name.to_string(), var_type.to_string());
2564                    }
2565                }
2566            }
2567
2568            for line in &function.body {
2569                let trimmed = line.trim();
2570
2571                if let Some((from_type, to_type)) = Self::parse_transmute_copy_line(trimmed) {
2572                    if let Some((from_elem, to_elem, from_size, to_size)) =
2573                        Self::is_slice_size_mismatch(&from_type, &to_type)
2574                    {
2575                        findings.push(Finding {
2576                            rule_id: self.metadata.id.clone(),
2577                            rule_name: self.metadata.name.clone(),
2578                            severity: self.metadata.default_severity,
2579                            message: format!(
2580                                "transmute_copy between slices with different element sizes: \
2581                                [{}] ({} bytes) to [{}] ({} bytes). The slice length won't be \
2582                                adjusted, causing memory access beyond the original allocation. \
2583                                Use slice::from_raw_parts with adjusted length instead.",
2584                                from_elem, from_size, to_elem, to_size
2585                            ),
2586                            function: function.name.clone(),
2587                            function_signature: function.signature.clone(),
2588                            evidence: vec![trimmed.to_string()],
2589                            span: function.span.clone(),
2590                            confidence: Confidence::Medium,
2591                            cwe_ids: Vec::new(),
2592                            fix_suggestion: None,
2593                            code_snippet: None,
2594                            exploitability: Exploitability::default(),
2595                            exploitability_score: Exploitability::default().score(),
2596                        ..Default::default()
2597                        });
2598                        continue;
2599                    }
2600
2601                    if let Some((from_elem, to_elem, from_size, to_size)) =
2602                        Self::is_vec_size_mismatch(&from_type, &to_type)
2603                    {
2604                        findings.push(Finding {
2605                            rule_id: self.metadata.id.clone(),
2606                            rule_name: self.metadata.name.clone(),
2607                            severity: self.metadata.default_severity,
2608                            message: format!(
2609                                "transmute_copy between Vecs with different element sizes: \
2610                                Vec<{}> ({} bytes) to Vec<{}> ({} bytes). This corrupts the \
2611                                Vec's length and capacity fields.",
2612                                from_elem, from_size, to_elem, to_size
2613                            ),
2614                            function: function.name.clone(),
2615                            function_signature: function.signature.clone(),
2616                            evidence: vec![trimmed.to_string()],
2617                            span: function.span.clone(),
2618                            confidence: Confidence::Medium,
2619                            cwe_ids: Vec::new(),
2620                            fix_suggestion: None,
2621                            code_snippet: None,
2622                            exploitability: Exploitability::default(),
2623                            exploitability_score: Exploitability::default().score(),
2624                        ..Default::default()
2625                        });
2626                        continue;
2627                    }
2628                }
2629
2630                if trimmed.contains("(Transmute)") && trimmed.contains(" as ") {
2631                    let copy_move_pattern = if trimmed.contains("copy ") {
2632                        "copy "
2633                    } else if trimmed.contains("move ") {
2634                        "move "
2635                    } else {
2636                        continue;
2637                    };
2638
2639                    let as_pos = match trimmed.find(" as ") {
2640                        Some(p) => p,
2641                        None => continue,
2642                    };
2643
2644                    let transmute_pos = match trimmed.find("(Transmute)") {
2645                        Some(p) => p,
2646                        None => continue,
2647                    };
2648
2649                    let to_type = trimmed[as_pos + 4..transmute_pos].trim();
2650
2651                    let copy_pos = match trimmed.find(copy_move_pattern) {
2652                        Some(p) => p,
2653                        None => continue,
2654                    };
2655
2656                    let src_start = copy_pos + copy_move_pattern.len();
2657                    let src_end = as_pos;
2658                    let src_var = trimmed[src_start..src_end].trim();
2659
2660                    let from_type = match var_types.get(src_var) {
2661                        Some(t) => t.as_str(),
2662                        None => continue,
2663                    };
2664
2665                    if let Some((from_elem, to_elem, from_size, to_size)) =
2666                        Self::is_slice_size_mismatch(from_type, to_type)
2667                    {
2668                        let size_info = if from_size == 0 && to_size == 0 {
2669                            format!(
2670                                "Transmute between slices of different struct types: \
2671                                [{}] to [{}]. Different struct types likely have different sizes, \
2672                                causing the slice length to be incorrect.",
2673                                from_elem, to_elem
2674                            )
2675                        } else if from_size == 0 || to_size == 0 {
2676                            format!(
2677                                "Transmute between slices with different element types: \
2678                                [{}] to [{}]. The slice length won't be adjusted for size differences.",
2679                                from_elem, to_elem
2680                            )
2681                        } else {
2682                            format!(
2683                                "Transmute between slices with different element sizes: \
2684                                [{}] ({} bytes) to [{}] ({} bytes). The slice length won't be \
2685                                adjusted, causing memory access beyond the original allocation.",
2686                                from_elem, from_size, to_elem, to_size
2687                            )
2688                        };
2689
2690                        findings.push(Finding {
2691                            rule_id: self.metadata.id.clone(),
2692                            rule_name: self.metadata.name.clone(),
2693                            severity: self.metadata.default_severity,
2694                            message: format!(
2695                                "{} Use slice::from_raw_parts with adjusted length, or slice::align_to.",
2696                                size_info
2697                            ),
2698                            function: function.name.clone(),
2699                            function_signature: function.signature.clone(),
2700                            evidence: vec![trimmed.to_string()],
2701                            span: function.span.clone(),
2702                    confidence: Confidence::Medium,
2703                    cwe_ids: Vec::new(),
2704                    fix_suggestion: None,
2705                    code_snippet: None,
2706                exploitability: Exploitability::default(),
2707                exploitability_score: Exploitability::default().score(),
2708                        ..Default::default()
2709                        });
2710                    }
2711
2712                    if let Some((from_elem, to_elem, from_size, to_size)) =
2713                        Self::is_vec_size_mismatch(from_type, to_type)
2714                    {
2715                        findings.push(Finding {
2716                            rule_id: self.metadata.id.clone(),
2717                            rule_name: self.metadata.name.clone(),
2718                            severity: self.metadata.default_severity,
2719                            message: format!(
2720                                "Transmute between Vecs with different element sizes: \
2721                                Vec<{}> ({} bytes) to Vec<{}> ({} bytes). This corrupts the \
2722                                Vec's length and capacity fields, potentially causing memory \
2723                                corruption or use-after-free.",
2724                                from_elem, from_size, to_elem, to_size
2725                            ),
2726                            function: function.name.clone(),
2727                            function_signature: function.signature.clone(),
2728                            evidence: vec![trimmed.to_string()],
2729                            span: function.span.clone(),
2730                            confidence: Confidence::Medium,
2731                            cwe_ids: Vec::new(),
2732                            fix_suggestion: None,
2733                            code_snippet: None,
2734                            exploitability: Exploitability::default(),
2735                            exploitability_score: Exploitability::default().score(),
2736                        ..Default::default()
2737                        });
2738                    }
2739                }
2740            }
2741        }
2742
2743        findings
2744    }
2745}
2746
2747// ============================================================================
2748// RUSTCOLA083: slice::from_raw_parts with potentially invalid length
2749// ============================================================================
2750
2751pub struct SliceFromRawPartsRule {
2752    metadata: RuleMetadata,
2753}
2754
2755impl SliceFromRawPartsRule {
2756    pub fn new() -> Self {
2757        Self {
2758            metadata: RuleMetadata {
2759                id: "RUSTCOLA083".to_string(),
2760                name: "slice-from-raw-parts-length".to_string(),
2761                short_description: "slice::from_raw_parts with potentially invalid length"
2762                    .to_string(),
2763                full_description: "Detects calls to slice::from_raw_parts or from_raw_parts_mut \
2764                    where the length argument may exceed the actual allocation, causing undefined \
2765                    behavior. Common issues include using untrusted input for length, forgetting \
2766                    to divide byte length by element size, or using unvalidated external lengths. \
2767                    Ensure length is derived from a trusted source or properly validated."
2768                    .to_string(),
2769                default_severity: Severity::High,
2770                origin: RuleOrigin::BuiltIn,
2771                cwe_ids: Vec::new(),
2772                fix_suggestion: None,
2773                help_uri: None,
2774                exploitability: Exploitability::default(),
2775            },
2776        }
2777    }
2778
2779    fn is_trusted_length_source(var_name: &str, body: &[String]) -> bool {
2780        let body_str = body.join("\n");
2781
2782        if body_str.contains(&format!("{} = ", var_name)) {
2783            for line in body {
2784                if line.contains(&format!("{} = ", var_name)) {
2785                    if line.contains("::len(")
2786                        || line.contains(">::len(")
2787                        || line.contains(".len()")
2788                    {
2789                        return true;
2790                    }
2791                }
2792            }
2793        }
2794
2795        if var_name.contains("count") {
2796            if body_str.contains("Layout::array") || body_str.contains("with_capacity") {
2797                return true;
2798            }
2799        }
2800
2801        false
2802    }
2803
2804    fn has_length_validation(len_var: &str, body: &[String]) -> bool {
2805        let body_str = body.join("\n");
2806
2807        let comparison_patterns = [
2808            format!("Gt(copy {}", len_var),
2809            format!("Lt(copy {}", len_var),
2810            format!("Le(copy {}", len_var),
2811            format!("Ge(copy {}", len_var),
2812            format!("Gt(move {}", len_var),
2813            format!("Lt(move {}", len_var),
2814            format!("Le(move {}", len_var),
2815            format!("Ge(move {}", len_var),
2816        ];
2817
2818        for pattern in &comparison_patterns {
2819            if body_str.contains(pattern) {
2820                return true;
2821            }
2822        }
2823
2824        if body_str.contains("::min(") {
2825            if body_str.contains(&format!("copy {}", len_var))
2826                || body_str.contains(&format!("move {}", len_var))
2827            {
2828                for line in body {
2829                    if line.contains("::min(") && line.contains(len_var) {
2830                        return true;
2831                    }
2832                }
2833            }
2834        }
2835        if body_str.contains("saturating_") && body_str.contains(len_var) {
2836            for line in body {
2837                if line.contains("saturating_") && line.contains(len_var) {
2838                    return true;
2839                }
2840            }
2841        }
2842
2843        if body_str.contains("checked_") && body_str.contains(len_var) {
2844            for line in body {
2845                if line.contains("checked_") && line.contains(len_var) {
2846                    return true;
2847                }
2848            }
2849        }
2850
2851        for line in body {
2852            if line.contains("assert") {
2853                if line.contains(&format!("Le(copy {}", len_var))
2854                    || line.contains(&format!("Lt(copy {}", len_var))
2855                    || line.contains(&format!("Le(move {}", len_var))
2856                    || line.contains(&format!("Lt(move {}", len_var))
2857                {
2858                    return true;
2859                }
2860            }
2861        }
2862
2863        false
2864    }
2865
2866    fn is_large_constant(line: &str) -> Option<usize> {
2867        if let Some(const_pos) = line.rfind("const ") {
2868            let after_const = &line[const_pos + 6..];
2869            if let Some(usize_pos) = after_const.find("_usize") {
2870                let num_str = &after_const[..usize_pos];
2871                if let Ok(n) = num_str.trim().parse::<usize>() {
2872                    if n > 10000 {
2873                        return Some(n);
2874                    }
2875                }
2876            }
2877        }
2878        None
2879    }
2880
2881    fn is_untrusted_length_source(_len_var: &str, body: &[String]) -> bool {
2882        let body_str = body.join("\n");
2883
2884        if body_str.contains("env::var") || body_str.contains("var::<") {
2885            if body_str.contains("parse") {
2886                return true;
2887            }
2888        }
2889
2890        if body_str.contains("env::args")
2891            || body_str.contains("Args")
2892            || body_str.contains("args::<")
2893        {
2894            return true;
2895        }
2896
2897        if body_str.contains("stdin") || body_str.contains("Stdin") {
2898            return true;
2899        }
2900
2901        false
2902    }
2903
2904    fn is_dangerous_length_computation(len_var: &str, body: &[String]) -> Option<String> {
2905        let mut source_var = len_var.to_string();
2906        for line in body {
2907            let trimmed = line.trim();
2908            if trimmed.contains(&format!("{} = move ", len_var)) && trimmed.contains("as usize") {
2909                if let Some(start) = trimmed.find("move ") {
2910                    let after_move = &trimmed[start + 5..];
2911                    if let Some(end) = after_move.find(" as") {
2912                        source_var = after_move[..end].to_string();
2913                    }
2914                }
2915            }
2916        }
2917
2918        for line in body {
2919            let trimmed = line.trim();
2920
2921            if trimmed.contains(&format!("{} = MulWithOverflow", len_var))
2922                || trimmed.contains(&format!("{} = Mul(", len_var))
2923            {
2924                return Some(
2925                    "length computed from multiplication (may overflow or use wrong scale)"
2926                        .to_string(),
2927                );
2928            }
2929
2930            if trimmed.contains(&format!("{} =", len_var)) && trimmed.contains("offset_from") {
2931                return Some(
2932                    "length derived from pointer difference (end pointer may be invalid)"
2933                        .to_string(),
2934                );
2935            }
2936            if source_var != len_var
2937                && trimmed.contains(&format!("{} =", source_var))
2938                && trimmed.contains("offset_from")
2939            {
2940                return Some(
2941                    "length derived from pointer difference (end pointer may be invalid)"
2942                        .to_string(),
2943                );
2944            }
2945
2946            if trimmed.contains(&format!("{} = move (", len_var)) && trimmed.contains(".0: usize)")
2947            {
2948                let body_str = body.join("\n");
2949                if body_str.contains("MulWithOverflow") {
2950                    return Some(
2951                        "length computed from multiplication (may overflow or use wrong scale)"
2952                            .to_string(),
2953                    );
2954                }
2955            }
2956
2957            if trimmed.contains(&format!("{} = Layout::size", len_var)) {
2958                return Some(
2959                    "length from Layout::size() returns bytes, not element count".to_string(),
2960                );
2961            }
2962            if trimmed.contains(&format!("{} =", len_var)) && trimmed.contains("Layout::size") {
2963                return Some(
2964                    "length from Layout::size() returns bytes, not element count".to_string(),
2965                );
2966            }
2967
2968            if trimmed.contains(&format!("{} = Div(", len_var)) {
2969                if trimmed.contains("const 2_usize") {
2970                    return Some("length divided by 2 may not match element size".to_string());
2971                }
2972            }
2973        }
2974
2975        None
2976    }
2977
2978    fn parse_from_raw_parts_call(line: &str) -> Option<(String, String)> {
2979        if !line.contains("from_raw_parts") {
2980            return None;
2981        }
2982
2983        let call_start = if line.contains("from_raw_parts_mut") {
2984            line.find("from_raw_parts_mut")?
2985        } else {
2986            line.find("from_raw_parts")?
2987        };
2988
2989        let after_call = &line[call_start..];
2990
2991        let args_start = after_call.find('(')? + 1;
2992        let args_end = after_call.rfind(')')?;
2993
2994        if args_start >= args_end {
2995            return None;
2996        }
2997
2998        let args_str = &after_call[args_start..args_end];
2999
3000        let parts: Vec<&str> = args_str.split(',').collect();
3001        if parts.len() != 2 {
3002            return None;
3003        }
3004
3005        let ptr_arg = parts[0]
3006            .trim()
3007            .trim_start_matches("copy ")
3008            .trim_start_matches("move ")
3009            .to_string();
3010        let len_arg = parts[1]
3011            .trim()
3012            .trim_start_matches("copy ")
3013            .trim_start_matches("move ")
3014            .to_string();
3015
3016        Some((ptr_arg, len_arg))
3017    }
3018
3019    fn is_function_parameter(len_var: &str, signature: &str) -> bool {
3020        signature.contains(&format!("{}: usize", len_var))
3021            || signature.contains(&format!("{}: u64", len_var))
3022            || signature.contains(&format!("{}: u32", len_var))
3023    }
3024}
3025
3026impl Rule for SliceFromRawPartsRule {
3027    fn metadata(&self) -> &RuleMetadata {
3028        &self.metadata
3029    }
3030
3031    fn evaluate(
3032        &self,
3033        package: &MirPackage,
3034        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3035    ) -> Vec<Finding> {
3036        let mut findings = Vec::new();
3037
3038        for function in &package.functions {
3039            if function.signature.contains("#[test]") || function.name.contains("test") {
3040                continue;
3041            }
3042
3043            for line in &function.body {
3044                let trimmed = line.trim();
3045
3046                if !trimmed.contains("from_raw_parts") {
3047                    continue;
3048                }
3049
3050                if !trimmed.contains("->") || !trimmed.contains("(") {
3051                    continue;
3052                }
3053
3054                let (_ptr_var, len_var) = match Self::parse_from_raw_parts_call(trimmed) {
3055                    Some(p) => p,
3056                    None => continue,
3057                };
3058
3059                if let Some(large_len) = Self::is_large_constant(trimmed) {
3060                    findings.push(Finding {
3061                        rule_id: self.metadata.id.clone(),
3062                        rule_name: self.metadata.name.clone(),
3063                        severity: self.metadata.default_severity,
3064                        message: format!(
3065                            "slice::from_raw_parts called with large constant length {}. \
3066                            Ensure the pointer actually points to at least {} elements of \
3067                            memory. Large constant lengths often indicate bugs.",
3068                            large_len, large_len
3069                        ),
3070                        function: function.name.clone(),
3071                        function_signature: function.signature.clone(),
3072                        evidence: vec![trimmed.to_string()],
3073                        span: function.span.clone(),
3074                        confidence: Confidence::Medium,
3075                        cwe_ids: Vec::new(),
3076                        fix_suggestion: None,
3077                        code_snippet: None,
3078                        exploitability: Exploitability::default(),
3079                        exploitability_score: Exploitability::default().score(),
3080                    ..Default::default()
3081                    });
3082                    continue;
3083                }
3084
3085                if Self::is_trusted_length_source(&len_var, &function.body) {
3086                    continue;
3087                }
3088
3089                if Self::has_length_validation(&len_var, &function.body) {
3090                    continue;
3091                }
3092
3093                if Self::is_untrusted_length_source(&len_var, &function.body) {
3094                    findings.push(Finding {
3095                        rule_id: self.metadata.id.clone(),
3096                        rule_name: self.metadata.name.clone(),
3097                        severity: self.metadata.default_severity,
3098                        message: format!(
3099                            "slice::from_raw_parts length '{}' derived from untrusted source \
3100                            (environment variable, command-line argument, or user input). \
3101                            Validate length against allocation size before creating slice.",
3102                            len_var
3103                        ),
3104                        function: function.name.clone(),
3105                        function_signature: function.signature.clone(),
3106                        evidence: vec![trimmed.to_string()],
3107                        span: function.span.clone(),
3108                        confidence: Confidence::Medium,
3109                        cwe_ids: Vec::new(),
3110                        fix_suggestion: None,
3111                        code_snippet: None,
3112                        exploitability: Exploitability::default(),
3113                        exploitability_score: Exploitability::default().score(),
3114                    ..Default::default()
3115                    });
3116                    continue;
3117                }
3118
3119                if let Some(reason) =
3120                    Self::is_dangerous_length_computation(&len_var, &function.body)
3121                {
3122                    findings.push(Finding {
3123                        rule_id: self.metadata.id.clone(),
3124                        rule_name: self.metadata.name.clone(),
3125                        severity: self.metadata.default_severity,
3126                        message: format!(
3127                            "slice::from_raw_parts length '{}': {}. \
3128                            Verify the length correctly represents element count within the allocation.",
3129                            len_var, reason
3130                        ),
3131                        function: function.name.clone(),
3132                        function_signature: function.signature.clone(),
3133                        evidence: vec![trimmed.to_string()],
3134                        span: function.span.clone(),
3135                    confidence: Confidence::Medium,
3136                    cwe_ids: Vec::new(),
3137                    fix_suggestion: None,
3138                    code_snippet: None,
3139                exploitability: Exploitability::default(),
3140                exploitability_score: Exploitability::default().score(),
3141                    ..Default::default()
3142                    });
3143                    continue;
3144                }
3145
3146                if Self::is_function_parameter(&len_var, &function.signature) {
3147                    if function.signature.contains("NonNull<") {
3148                        continue;
3149                    }
3150
3151                    findings.push(Finding {
3152                        rule_id: self.metadata.id.clone(),
3153                        rule_name: self.metadata.name.clone(),
3154                        severity: Severity::Medium,
3155                        message: format!(
3156                            "slice::from_raw_parts length '{}' comes directly from function \
3157                            parameter without validation. If callers can pass arbitrary values, \
3158                            add bounds checking or document the safety requirements.",
3159                            len_var
3160                        ),
3161                        function: function.name.clone(),
3162                        function_signature: function.signature.clone(),
3163                        evidence: vec![trimmed.to_string()],
3164                        span: function.span.clone(),
3165                        confidence: Confidence::Medium,
3166                        cwe_ids: Vec::new(),
3167                        fix_suggestion: None,
3168                        code_snippet: None,
3169                        exploitability: Exploitability::default(),
3170                        exploitability_score: Exploitability::default().score(),
3171                    ..Default::default()
3172                    });
3173                }
3174            }
3175        }
3176
3177        findings
3178    }
3179}
3180
3181// ============================================================================
3182// RUSTCOLA101: Variance Transmute Unsound Rule
3183// ============================================================================
3184
3185/// Detects transmutes that violate Rust's variance rules, which can lead to
3186/// unsoundness. Common patterns include transmuting between &T and &mut T,
3187/// or between covariant and invariant types.
3188pub struct VarianceTransmuteUnsoundRule {
3189    metadata: RuleMetadata,
3190}
3191
3192impl VarianceTransmuteUnsoundRule {
3193    pub fn new() -> Self {
3194        Self {
3195            metadata: RuleMetadata {
3196                id: "RUSTCOLA101".to_string(),
3197                name: "variance-transmute-unsound".to_string(),
3198                short_description: "Transmutes violating variance rules".to_string(),
3199                full_description: "Detects transmutes that violate Rust's variance rules (e.g., &T to &mut T, \
3200                    *const T to *mut T, or invariant types like Cell/RefCell), which cause undefined behavior.".to_string(),
3201                help_uri: None,
3202                default_severity: Severity::High,
3203                origin: RuleOrigin::BuiltIn,
3204                cwe_ids: Vec::new(),
3205                fix_suggestion: None,
3206                exploitability: Exploitability::default(),
3207            },
3208        }
3209    }
3210
3211    /// Detects &T to &mut T transmute patterns
3212    fn is_ref_to_mut_transmute(line: &str) -> bool {
3213        if !line.contains("transmute") {
3214            return false;
3215        }
3216
3217        // Pattern: transmute::<&Foo, &mut Foo> or similar
3218        if let Some(transmute_start) = line.find("transmute") {
3219            let after_transmute = &line[transmute_start..];
3220            // Look for pattern where we go from & to &mut
3221            if after_transmute.contains("::<&") && after_transmute.contains("&mut") {
3222                return true;
3223            }
3224            // Look for explicit type annotations
3225            if after_transmute.contains("::<&") && !after_transmute.contains("&mut") {
3226                // Check if the result is being assigned to &mut
3227                let before_transmute = &line[..transmute_start];
3228                if before_transmute.contains("&mut") || before_transmute.contains(": &mut") {
3229                    return true;
3230                }
3231            }
3232        }
3233        false
3234    }
3235
3236    /// Detects *const T to *mut T transmute patterns
3237    fn is_const_to_mut_ptr_transmute(line: &str) -> bool {
3238        if !line.contains("transmute") {
3239            return false;
3240        }
3241
3242        if let Some(transmute_start) = line.find("transmute") {
3243            let after_transmute = &line[transmute_start..];
3244            // Pattern: transmute::<*const T, *mut T>
3245            if after_transmute.contains("*const") && after_transmute.contains("*mut") {
3246                // Make sure *const comes before *mut in type params
3247                if let (Some(const_pos), Some(mut_pos)) =
3248                    (after_transmute.find("*const"), after_transmute.find("*mut"))
3249                {
3250                    return const_pos < mut_pos;
3251                }
3252            }
3253        }
3254        false
3255    }
3256
3257    /// Detects transmutes involving invariant types (Cell, RefCell, UnsafeCell)
3258    fn is_invariant_type_transmute(line: &str) -> bool {
3259        if !line.contains("transmute") {
3260            return false;
3261        }
3262
3263        let invariant_types = ["Cell<", "RefCell<", "UnsafeCell<", "Mutex<", "RwLock<"];
3264
3265        for inv_type in invariant_types.iter() {
3266            if line.contains(inv_type) {
3267                return true;
3268            }
3269        }
3270        false
3271    }
3272}
3273
3274impl Rule for VarianceTransmuteUnsoundRule {
3275    fn metadata(&self) -> &RuleMetadata {
3276        &self.metadata
3277    }
3278
3279    fn evaluate(
3280        &self,
3281        package: &MirPackage,
3282        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3283    ) -> Vec<Finding> {
3284        let mut findings = Vec::new();
3285
3286        for function in &package.functions {
3287            for line in &function.body {
3288                let trimmed = line.trim();
3289
3290                // Skip comments
3291                if trimmed.starts_with("//") {
3292                    continue;
3293                }
3294
3295                // Check for &T to &mut T transmute
3296                if Self::is_ref_to_mut_transmute(trimmed) {
3297                    findings.push(Finding {
3298                        rule_id: self.metadata.id.clone(),
3299                        rule_name: self.metadata.name.clone(),
3300                        severity: Severity::High,
3301                        message: "Transmuting from immutable reference (&T) to mutable reference (&mut T) \
3302                            violates Rust's aliasing rules and is undefined behavior. Use interior \
3303                            mutability (Cell, RefCell, Mutex) instead.".to_string(),
3304                        function: function.name.clone(),
3305                        function_signature: function.signature.clone(),
3306                        evidence: vec![trimmed.to_string()],
3307                        span: function.span.clone(),
3308                    confidence: Confidence::Medium,
3309                    cwe_ids: Vec::new(),
3310                    fix_suggestion: None,
3311                    code_snippet: None,
3312                exploitability: Exploitability::default(),
3313                exploitability_score: Exploitability::default().score(),
3314                    ..Default::default()
3315                    });
3316                    continue;
3317                }
3318
3319                // Check for *const T to *mut T transmute
3320                if Self::is_const_to_mut_ptr_transmute(trimmed) {
3321                    findings.push(Finding {
3322                        rule_id: self.metadata.id.clone(),
3323                        rule_name: self.metadata.name.clone(),
3324                        severity: Severity::High,
3325                        message:
3326                            "Transmuting from *const T to *mut T can cause undefined behavior \
3327                            if the original data was immutable. Use ptr.cast_mut() (Rust 1.65+) \
3328                            or ensure the underlying data is actually mutable."
3329                                .to_string(),
3330                        function: function.name.clone(),
3331                        function_signature: function.signature.clone(),
3332                        evidence: vec![trimmed.to_string()],
3333                        span: function.span.clone(),
3334                        confidence: Confidence::Medium,
3335                        cwe_ids: Vec::new(),
3336                        fix_suggestion: None,
3337                        code_snippet: None,
3338                        exploitability: Exploitability::default(),
3339                        exploitability_score: Exploitability::default().score(),
3340                    ..Default::default()
3341                    });
3342                    continue;
3343                }
3344
3345                // Check for invariant type transmutes
3346                if Self::is_invariant_type_transmute(trimmed) {
3347                    findings.push(Finding {
3348                        rule_id: self.metadata.id.clone(),
3349                        rule_name: self.metadata.name.clone(),
3350                        severity: Severity::High,
3351                        message: "Transmuting invariant types (Cell, RefCell, UnsafeCell, Mutex, RwLock) \
3352                            can violate their safety invariants and cause undefined behavior. \
3353                            These types have special memory semantics that transmute bypasses.".to_string(),
3354                        function: function.name.clone(),
3355                        function_signature: function.signature.clone(),
3356                        evidence: vec![trimmed.to_string()],
3357                        span: function.span.clone(),
3358                    confidence: Confidence::Medium,
3359                    cwe_ids: Vec::new(),
3360                    fix_suggestion: None,
3361                    code_snippet: None,
3362                exploitability: Exploitability::default(),
3363                exploitability_score: Exploitability::default().score(),
3364                    ..Default::default()
3365                    });
3366                }
3367            }
3368        }
3369
3370        findings
3371    }
3372}
3373
3374// ============================================================================
3375// RUSTCOLA118: Returned Reference to Local Rule
3376// ============================================================================
3377
3378/// Detects patterns where a function returns a reference to a local variable,
3379/// which would be a use-after-free if the borrow checker didn't catch it.
3380///
3381/// In safe Rust, the compiler prevents this. But in unsafe code or through
3382/// certain patterns involving raw pointers, this can slip through.
3383pub struct ReturnedRefToLocalRule {
3384    metadata: RuleMetadata,
3385}
3386
3387impl ReturnedRefToLocalRule {
3388    pub fn new() -> Self {
3389        Self {
3390            metadata: RuleMetadata {
3391                id: "RUSTCOLA118".to_string(),
3392                name: "returned-ref-to-local".to_string(),
3393                short_description: "Reference to local variable returned".to_string(),
3394                full_description: "Detects patterns where a function may return a reference \
3395                    to a stack-allocated local variable. In unsafe code, this leads to \
3396                    use-after-free when the stack frame is deallocated."
3397                    .to_string(),
3398                help_uri: None,
3399                default_severity: Severity::High,
3400                origin: RuleOrigin::BuiltIn,
3401                cwe_ids: Vec::new(),
3402                fix_suggestion: None,
3403                exploitability: Exploitability::default(),
3404            },
3405        }
3406    }
3407
3408    /// Patterns that indicate returning a reference to a local
3409    fn dangerous_return_patterns() -> &'static [&'static str] {
3410        &[
3411            // Raw pointer from local then returned as reference
3412            "&*",
3413            "as *const",
3414            "as *mut",
3415            // Transmute of local address
3416            "transmute(&",
3417            "transmute::<&",
3418            // addr_of! macro on local
3419            "addr_of!(",
3420            "addr_of_mut!(",
3421        ]
3422    }
3423}
3424
3425impl Rule for ReturnedRefToLocalRule {
3426    fn metadata(&self) -> &RuleMetadata {
3427        &self.metadata
3428    }
3429
3430    fn evaluate(
3431        &self,
3432        package: &MirPackage,
3433        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3434    ) -> Vec<Finding> {
3435        let mut findings = Vec::new();
3436        let crate_root = Path::new(&package.crate_root);
3437
3438        if !crate_root.exists() {
3439            return findings;
3440        }
3441
3442        for entry in WalkDir::new(crate_root)
3443            .into_iter()
3444            .filter_entry(|e| filter_entry(e))
3445        {
3446            let entry = match entry {
3447                Ok(e) => e,
3448                Err(_) => continue,
3449            };
3450
3451            if !entry.file_type().is_file() {
3452                continue;
3453            }
3454
3455            let path = entry.path();
3456            if path.extension() != Some(OsStr::new("rs")) {
3457                continue;
3458            }
3459
3460            let rel_path = path
3461                .strip_prefix(crate_root)
3462                .unwrap_or(path)
3463                .to_string_lossy()
3464                .replace('\\', "/");
3465
3466            let content = match fs::read_to_string(path) {
3467                Ok(c) => c,
3468                Err(_) => continue,
3469            };
3470
3471            // Quick check: does file have unsafe blocks?
3472            if !content.contains("unsafe") {
3473                continue;
3474            }
3475
3476            let lines: Vec<&str> = content.lines().collect();
3477            let mut in_unsafe_block = false;
3478            let mut unsafe_depth = 0;
3479            let mut local_vars: HashSet<String> = HashSet::new();
3480            let mut current_fn_returns_ref = false;
3481            let mut current_fn_name = String::new();
3482
3483            for (idx, line) in lines.iter().enumerate() {
3484                let trimmed = line.trim();
3485
3486                // Skip comments
3487                if trimmed.starts_with("//") {
3488                    continue;
3489                }
3490
3491                // Track function signatures that return references
3492                if trimmed.starts_with("fn ")
3493                    || trimmed.starts_with("pub fn ")
3494                    || trimmed.starts_with("unsafe fn ")
3495                    || trimmed.starts_with("pub unsafe fn ")
3496                {
3497                    current_fn_returns_ref = trimmed.contains("-> &")
3498                        || trimmed.contains("-> *const")
3499                        || trimmed.contains("-> *mut");
3500                    // Extract function name
3501                    if let Some(fn_start) = trimmed.find("fn ") {
3502                        let after_fn = &trimmed[fn_start + 3..];
3503                        if let Some(paren) = after_fn.find('(') {
3504                            current_fn_name = after_fn[..paren].trim().to_string();
3505                        }
3506                    }
3507                    local_vars.clear();
3508                }
3509
3510                // Track unsafe blocks
3511                if trimmed.contains("unsafe {") || trimmed.contains("unsafe{") {
3512                    in_unsafe_block = true;
3513                    unsafe_depth = 1;
3514                } else if in_unsafe_block {
3515                    unsafe_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
3516                    unsafe_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
3517                    if unsafe_depth <= 0 {
3518                        in_unsafe_block = false;
3519                    }
3520                }
3521
3522                // Track local variable declarations
3523                if trimmed.starts_with("let ") {
3524                    if let Some(eq_pos) = trimmed.find('=') {
3525                        let var_part = &trimmed[4..eq_pos];
3526                        // Handle patterns like "let mut x", "let x: Type"
3527                        let var_name = var_part
3528                            .trim()
3529                            .trim_start_matches("mut ")
3530                            .split(':')
3531                            .next()
3532                            .map(|s| s.trim())
3533                            .unwrap_or("");
3534                        if !var_name.is_empty() && !var_name.contains('(') {
3535                            local_vars.insert(var_name.to_string());
3536                        }
3537                    }
3538                }
3539
3540                // Only check in unsafe blocks for functions returning references
3541                if in_unsafe_block && current_fn_returns_ref {
3542                    // Check for dangerous patterns
3543                    for pattern in Self::dangerous_return_patterns() {
3544                        if trimmed.contains(pattern) {
3545                            // Check if this involves a local variable
3546                            for var in &local_vars {
3547                                if trimmed.contains(var.as_str()) {
3548                                    let location = format!("{}:{}", rel_path, idx + 1);
3549
3550                                    findings.push(Finding {
3551                                        rule_id: self.metadata.id.clone(),
3552                                        rule_name: self.metadata.name.clone(),
3553                                        severity: self.metadata.default_severity,
3554                                        message: format!(
3555                                            "Potential return of reference to local variable '{}' in unsafe block. \
3556                                            When the function returns, the stack frame is deallocated, \
3557                                            leaving a dangling pointer. Pattern: '{}'",
3558                                            var, pattern
3559                                        ),
3560                                        function: format!("{} ({})", current_fn_name, location),
3561                                        function_signature: String::new(),
3562                                        evidence: vec![trimmed.to_string()],
3563                                        span: None,
3564                    ..Default::default()
3565                                    });
3566                                    break;
3567                                }
3568                            }
3569                        }
3570                    }
3571
3572                    // Special check: returning &*ptr where ptr is from a local
3573                    if trimmed.contains("return") && trimmed.contains("&*") {
3574                        let location = format!("{}:{}", rel_path, idx + 1);
3575                        findings.push(Finding {
3576                            rule_id: self.metadata.id.clone(),
3577                            rule_name: self.metadata.name.clone(),
3578                            severity: self.metadata.default_severity,
3579                            message: "Returning dereferenced raw pointer in unsafe block. \
3580                                Ensure the pointer does not point to stack-allocated memory \
3581                                that will be deallocated when the function returns."
3582                                .to_string(),
3583                            function: format!("{} ({})", current_fn_name, location),
3584                            function_signature: String::new(),
3585                            evidence: vec![trimmed.to_string()],
3586                            span: None,
3587                            ..Default::default()
3588                        });
3589                    }
3590                }
3591            }
3592        }
3593
3594        findings
3595    }
3596}
3597
3598// ============================================================================
3599// RUSTCOLA120: Self-Referential Struct Rule
3600// ============================================================================
3601
3602/// Detects patterns that create self-referential structs without proper Pin usage.
3603///
3604/// Self-referential structs (where a field contains a pointer/reference to another
3605/// field) are inherently dangerous because moving the struct invalidates the
3606/// internal pointer.
3607pub struct SelfReferentialStructRule {
3608    metadata: RuleMetadata,
3609}
3610
3611impl SelfReferentialStructRule {
3612    pub fn new() -> Self {
3613        Self {
3614            metadata: RuleMetadata {
3615                id: "RUSTCOLA120".to_string(),
3616                name: "self-referential-struct".to_string(),
3617                short_description: "Potential self-referential struct without Pin".to_string(),
3618                full_description: "Detects patterns that may create self-referential structs \
3619                    without proper Pin usage. When a struct contains a pointer to one of its \
3620                    own fields, moving the struct invalidates that pointer. Use Pin<Box<T>> \
3621                    or crates like 'ouroboros' or 'self_cell' for safe self-references."
3622                    .to_string(),
3623                help_uri: Some("https://doc.rust-lang.org/std/pin/index.html".to_string()),
3624                default_severity: Severity::High,
3625                origin: RuleOrigin::BuiltIn,
3626                cwe_ids: Vec::new(),
3627                fix_suggestion: None,
3628                exploitability: Exploitability::default(),
3629            },
3630        }
3631    }
3632
3633    /// Patterns indicating self-referential struct creation
3634    fn self_ref_patterns() -> &'static [&'static str] {
3635        &[
3636            // Taking address of own field
3637            "&self.",
3638            "addr_of!(self.",
3639            "addr_of_mut!(self.",
3640            // Raw pointer to self field
3641            "as *const Self",
3642            "as *mut Self",
3643            // Storing reference to self
3644            "self as *",
3645            "&mut self as *",
3646            "&self as *",
3647        ]
3648    }
3649}
3650
3651impl Rule for SelfReferentialStructRule {
3652    fn metadata(&self) -> &RuleMetadata {
3653        &self.metadata
3654    }
3655
3656    fn evaluate(
3657        &self,
3658        package: &MirPackage,
3659        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3660    ) -> Vec<Finding> {
3661        let mut findings = Vec::new();
3662        let crate_root = Path::new(&package.crate_root);
3663
3664        if !crate_root.exists() {
3665            return findings;
3666        }
3667
3668        for entry in WalkDir::new(crate_root)
3669            .into_iter()
3670            .filter_entry(|e| filter_entry(e))
3671        {
3672            let entry = match entry {
3673                Ok(e) => e,
3674                Err(_) => continue,
3675            };
3676
3677            if !entry.file_type().is_file() {
3678                continue;
3679            }
3680
3681            let path = entry.path();
3682            if path.extension() != Some(OsStr::new("rs")) {
3683                continue;
3684            }
3685
3686            let rel_path = path
3687                .strip_prefix(crate_root)
3688                .unwrap_or(path)
3689                .to_string_lossy()
3690                .replace('\\', "/");
3691
3692            let content = match fs::read_to_string(path) {
3693                Ok(c) => c,
3694                Err(_) => continue,
3695            };
3696
3697            // Quick check: file likely has self-referential patterns
3698            if !content.contains("*const")
3699                && !content.contains("*mut")
3700                && !content.contains("addr_of")
3701            {
3702                continue;
3703            }
3704
3705            let lines: Vec<&str> = content.lines().collect();
3706            let mut in_impl_block = false;
3707            let mut current_type = String::new();
3708            let mut in_unsafe = false;
3709
3710            for (idx, line) in lines.iter().enumerate() {
3711                let trimmed = line.trim();
3712
3713                // Skip comments
3714                if trimmed.starts_with("//") {
3715                    continue;
3716                }
3717
3718                // Track impl blocks
3719                if trimmed.starts_with("impl ") || trimmed.starts_with("impl<") {
3720                    in_impl_block = true;
3721                    // Extract type name
3722                    if let Some(for_pos) = trimmed.find(" for ") {
3723                        let after_for = &trimmed[for_pos + 5..];
3724                        current_type = after_for
3725                            .split(|c| c == '<' || c == ' ' || c == '{')
3726                            .next()
3727                            .unwrap_or("")
3728                            .to_string();
3729                    } else if let Some(impl_pos) = trimmed.find("impl ") {
3730                        let after_impl = &trimmed[impl_pos + 5..];
3731                        current_type = after_impl
3732                            .split(|c| c == '<' || c == ' ' || c == '{')
3733                            .next()
3734                            .unwrap_or("")
3735                            .to_string();
3736                    }
3737                }
3738
3739                // Track unsafe blocks
3740                if trimmed.contains("unsafe") {
3741                    in_unsafe = true;
3742                }
3743
3744                // Only flag in impl blocks where self-reference is meaningful
3745                if in_impl_block && in_unsafe {
3746                    for pattern in Self::self_ref_patterns() {
3747                        if trimmed.contains(pattern) {
3748                            // Check if this is being stored in a field (assignment to self.field)
3749                            let is_storing = trimmed.contains("self.")
3750                                && (trimmed.contains(" = ") || trimmed.contains("="));
3751
3752                            // Check if Pin is being used properly
3753                            let has_pin = content.contains("Pin<") || content.contains("pin!");
3754
3755                            if is_storing || !has_pin {
3756                                let location = format!("{}:{}", rel_path, idx + 1);
3757
3758                                findings.push(Finding {
3759                                    rule_id: self.metadata.id.clone(),
3760                                    rule_name: self.metadata.name.clone(),
3761                                    severity: self.metadata.default_severity,
3762                                    message: format!(
3763                                        "Potential self-referential pattern in type '{}' without Pin. \
3764                                        Creating a pointer to a struct's own field and storing it \
3765                                        creates a self-referential struct. Moving this struct will \
3766                                        invalidate the internal pointer. Use Pin<Box<{}>> to prevent \
3767                                        moves, or use 'ouroboros'/'self_cell' crates for safe self-references.",
3768                                        current_type, current_type
3769                                    ),
3770                                    function: location,
3771                                    function_signature: String::new(),
3772                                    evidence: vec![trimmed.to_string()],
3773                                    span: None,
3774                    ..Default::default()
3775                                });
3776                                break;
3777                            }
3778                        }
3779                    }
3780                }
3781
3782                // Reset on block end (simplified)
3783                if trimmed == "}" && in_impl_block {
3784                    // This is a simplification - proper tracking would need brace counting
3785                }
3786            }
3787        }
3788
3789        findings
3790    }
3791}
3792
3793// ============================================================================
3794// RUSTCOLA128: UnsafeCell Aliasing Violation Rule
3795// ============================================================================
3796
3797/// Detects potential UnsafeCell aliasing violations where multiple mutable
3798/// references may exist simultaneously, violating Rust's aliasing rules.
3799pub struct UnsafeCellAliasingRule {
3800    metadata: RuleMetadata,
3801}
3802
3803impl UnsafeCellAliasingRule {
3804    pub fn new() -> Self {
3805        Self {
3806            metadata: RuleMetadata {
3807                id: "RUSTCOLA128".to_string(),
3808                name: "unsafecell-aliasing-violation".to_string(),
3809                short_description: "Potential UnsafeCell aliasing violation".to_string(),
3810                full_description: "Detects patterns where UnsafeCell, Cell, or RefCell contents \
3811                    may be accessed through multiple mutable references simultaneously in unsafe \
3812                    code. This violates Rust's aliasing rules and causes undefined behavior. \
3813                    Ensure only one mutable reference exists at a time, or use proper interior \
3814                    mutability patterns."
3815                    .to_string(),
3816                help_uri: Some(
3817                    "https://doc.rust-lang.org/std/cell/struct.UnsafeCell.html".to_string(),
3818                ),
3819                default_severity: Severity::High,
3820                origin: RuleOrigin::BuiltIn,
3821                cwe_ids: Vec::new(),
3822                fix_suggestion: None,
3823                exploitability: Exploitability::default(),
3824            },
3825        }
3826    }
3827
3828    fn aliasing_patterns() -> Vec<(&'static str, &'static str)> {
3829        vec![
3830            (
3831                ".get()",
3832                "UnsafeCell::get() returns *mut T - ensure no aliasing",
3833            ),
3834            (
3835                "&mut *self.",
3836                "mutable dereference may alias with other refs",
3837            ),
3838            (
3839                "&mut *ptr",
3840                "raw pointer to mutable ref - check for aliases",
3841            ),
3842            (
3843                "as *mut",
3844                "casting to *mut - may create aliasing mutable refs",
3845            ),
3846            (".as_mut()", "as_mut() in unsafe may alias"),
3847            (
3848                "get_unchecked_mut",
3849                "unchecked mutable access - verify no aliasing",
3850            ),
3851        ]
3852    }
3853
3854    fn aliasing_contexts() -> Vec<&'static str> {
3855        vec!["UnsafeCell", "Cell<", "RefCell<", "*mut", "*const"]
3856    }
3857}
3858
3859impl Rule for UnsafeCellAliasingRule {
3860    fn metadata(&self) -> &RuleMetadata {
3861        &self.metadata
3862    }
3863
3864    fn evaluate(
3865        &self,
3866        package: &MirPackage,
3867        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3868    ) -> Vec<Finding> {
3869        // Skip self-analysis
3870        if package.crate_name == "mir-extractor" {
3871            return Vec::new();
3872        }
3873
3874        let mut findings = Vec::new();
3875        let crate_root = Path::new(&package.crate_root);
3876
3877        if !crate_root.exists() {
3878            return findings;
3879        }
3880
3881        for entry in WalkDir::new(crate_root)
3882            .into_iter()
3883            .filter_entry(filter_entry)
3884            .filter_map(Result::ok)
3885            .filter(|e| e.file_type().is_file())
3886        {
3887            let path = entry.path();
3888            if path.extension() != Some(OsStr::new("rs")) {
3889                continue;
3890            }
3891
3892            let rel_path = path
3893                .strip_prefix(crate_root)
3894                .unwrap_or(path)
3895                .to_string_lossy()
3896                .replace('\\', "/");
3897
3898            let content = match fs::read_to_string(path) {
3899                Ok(c) => c,
3900                Err(_) => continue,
3901            };
3902
3903            // Quick check: does this file use interior mutability?
3904            let has_interior_mut = Self::aliasing_contexts()
3905                .iter()
3906                .any(|ctx| content.contains(ctx));
3907
3908            if !has_interior_mut {
3909                continue;
3910            }
3911
3912            let lines: Vec<&str> = content.lines().collect();
3913            let mut in_unsafe = false;
3914            let mut unsafe_start = 0;
3915
3916            for (idx, line) in lines.iter().enumerate() {
3917                let trimmed = line.trim();
3918
3919                // Skip comments
3920                if trimmed.starts_with("//") {
3921                    continue;
3922                }
3923
3924                // Track unsafe blocks
3925                if trimmed.contains("unsafe {") || trimmed.contains("unsafe{") {
3926                    in_unsafe = true;
3927                    unsafe_start = idx;
3928                }
3929
3930                if in_unsafe {
3931                    // Check for aliasing patterns
3932                    for (pattern, description) in Self::aliasing_patterns() {
3933                        if trimmed.contains(pattern) {
3934                            // Check if there are multiple access patterns in the same unsafe block
3935                            let unsafe_block =
3936                                &lines[unsafe_start..=(idx + 5).min(lines.len() - 1)];
3937
3938                            let mut_access_count = unsafe_block
3939                                .iter()
3940                                .filter(|l| {
3941                                    l.contains("&mut")
3942                                        || l.contains("as *mut")
3943                                        || l.contains(".get()")
3944                                        || l.contains(".as_mut()")
3945                                })
3946                                .count();
3947
3948                            // Multiple mutable accesses in same block is suspicious
3949                            if mut_access_count >= 2 {
3950                                let location = format!("{}:{}", rel_path, idx + 1);
3951
3952                                findings.push(Finding {
3953                                    rule_id: self.metadata.id.clone(),
3954                                    rule_name: self.metadata.name.clone(),
3955                                    severity: self.metadata.default_severity,
3956                                    message: format!(
3957                                        "Potential aliasing violation: {}. Multiple mutable accesses \
3958                                        detected in same unsafe block. Ensure only one &mut reference \
3959                                        exists at a time to avoid undefined behavior.",
3960                                        description
3961                                    ),
3962                                    function: location,
3963                                    function_signature: String::new(),
3964                                    evidence: vec![trimmed.to_string()],
3965                                    span: None,
3966                    ..Default::default()
3967                                });
3968                                break;
3969                            }
3970                        }
3971                    }
3972
3973                    // Simple closing brace tracking
3974                    if trimmed == "}" {
3975                        in_unsafe = false;
3976                    }
3977                }
3978            }
3979        }
3980
3981        findings
3982    }
3983}
3984
3985// ============================================================================
3986// RUSTCOLA129: Lazy Initialization Panic Poison Rule
3987// ============================================================================
3988
3989/// Detects lazy initialization patterns that can panic and poison the lazy value,
3990/// causing all future accesses to fail or return corrupted state.
3991pub struct LazyInitPanicPoisonRule {
3992    metadata: RuleMetadata,
3993}
3994
3995impl LazyInitPanicPoisonRule {
3996    pub fn new() -> Self {
3997        Self {
3998            metadata: RuleMetadata {
3999                id: "RUSTCOLA129".to_string(),
4000                name: "lazy-init-panic-poison".to_string(),
4001                short_description: "Panic-prone code in lazy initialization".to_string(),
4002                full_description: "Detects lazy initialization (OnceLock, Lazy, OnceCell, lazy_static) \
4003                    with panic-prone code like unwrap(), expect(), or panic!(). If the initialization \
4004                    panics, the lazy value may be poisoned, causing all future accesses to fail or \
4005                    return incomplete state. Use fallible initialization patterns or handle errors \
4006                    gracefully.".to_string(),
4007                help_uri: Some("https://doc.rust-lang.org/std/sync/struct.OnceLock.html".to_string()),
4008                default_severity: Severity::Medium,
4009                origin: RuleOrigin::BuiltIn,
4010                cwe_ids: Vec::new(),
4011                fix_suggestion: None,
4012                exploitability: Exploitability::default(),
4013            },
4014        }
4015    }
4016
4017    fn lazy_patterns() -> Vec<(&'static str, &'static str)> {
4018        vec![
4019            ("OnceLock", "std::sync::OnceLock"),
4020            ("OnceCell", "once_cell::sync::OnceCell"),
4021            ("Lazy<", "once_cell::sync::Lazy"),
4022            ("lazy_static!", "lazy_static macro"),
4023            ("LazyLock", "std::sync::LazyLock"),
4024            (".get_or_init(", "lazy initialization closure"),
4025            (".get_or_try_init(", "fallible lazy init"),
4026            ("call_once(", "std::sync::Once::call_once"),
4027        ]
4028    }
4029
4030    fn panic_patterns() -> Vec<&'static str> {
4031        vec![
4032            ".unwrap()",
4033            ".expect(",
4034            "panic!(",
4035            "unreachable!(",
4036            "todo!(",
4037            "unimplemented!(",
4038            "assert!(",
4039            "assert_eq!(",
4040            "assert_ne!(",
4041        ]
4042    }
4043}
4044
4045impl Rule for LazyInitPanicPoisonRule {
4046    fn metadata(&self) -> &RuleMetadata {
4047        &self.metadata
4048    }
4049
4050    fn evaluate(
4051        &self,
4052        package: &MirPackage,
4053        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
4054    ) -> Vec<Finding> {
4055        // Skip self-analysis
4056        if package.crate_name == "mir-extractor" {
4057            return Vec::new();
4058        }
4059
4060        let mut findings = Vec::new();
4061        let crate_root = Path::new(&package.crate_root);
4062
4063        if !crate_root.exists() {
4064            return findings;
4065        }
4066
4067        for entry in WalkDir::new(crate_root)
4068            .into_iter()
4069            .filter_entry(filter_entry)
4070            .filter_map(Result::ok)
4071            .filter(|e| e.file_type().is_file())
4072        {
4073            let path = entry.path();
4074            if path.extension() != Some(OsStr::new("rs")) {
4075                continue;
4076            }
4077
4078            let rel_path = path
4079                .strip_prefix(crate_root)
4080                .unwrap_or(path)
4081                .to_string_lossy()
4082                .replace('\\', "/");
4083
4084            let content = match fs::read_to_string(path) {
4085                Ok(c) => c,
4086                Err(_) => continue,
4087            };
4088
4089            // Quick check: does this file use lazy initialization?
4090            let has_lazy = Self::lazy_patterns()
4091                .iter()
4092                .any(|(p, _)| content.contains(p));
4093
4094            if !has_lazy {
4095                continue;
4096            }
4097
4098            let lines: Vec<&str> = content.lines().collect();
4099            let mut in_lazy_init = false;
4100            let mut lazy_type = String::new();
4101            let mut lazy_start = 0;
4102            let mut brace_depth = 0;
4103
4104            for (idx, line) in lines.iter().enumerate() {
4105                let trimmed = line.trim();
4106
4107                // Skip comments
4108                if trimmed.starts_with("//") {
4109                    continue;
4110                }
4111
4112                // Detect lazy initialization patterns
4113                for (pattern, desc) in Self::lazy_patterns() {
4114                    if trimmed.contains(pattern) {
4115                        // Check if this is a definition with an initializer
4116                        if trimmed.contains("=")
4117                            || trimmed.contains("get_or_init")
4118                            || trimmed.contains("call_once")
4119                        {
4120                            in_lazy_init = true;
4121                            lazy_type = desc.to_string();
4122                            lazy_start = idx;
4123                            brace_depth = trimmed.matches('{').count() as i32
4124                                - trimmed.matches('}').count() as i32;
4125                        }
4126                    }
4127                }
4128
4129                // Track the initialization block
4130                if in_lazy_init {
4131                    brace_depth += trimmed.matches('{').count() as i32;
4132                    brace_depth -= trimmed.matches('}').count() as i32;
4133
4134                    // Look for panic patterns in the init block
4135                    for panic_pat in Self::panic_patterns() {
4136                        if trimmed.contains(panic_pat) {
4137                            let location = format!("{}:{}", rel_path, idx + 1);
4138
4139                            findings.push(Finding {
4140                                rule_id: self.metadata.id.clone(),
4141                                rule_name: self.metadata.name.clone(),
4142                                severity: self.metadata.default_severity,
4143                                message: format!(
4144                                    "Panic-prone code '{}' in {} initialization. If this panics, \
4145                                    the lazy value may be poisoned, causing all future accesses to \
4146                                    fail. Consider using fallible initialization (get_or_try_init) \
4147                                    or handling errors gracefully.",
4148                                    panic_pat.trim_end_matches('('),
4149                                    lazy_type
4150                                ),
4151                                function: location,
4152                                function_signature: String::new(),
4153                                evidence: vec![trimmed.to_string()],
4154                                span: None,
4155                                ..Default::default()
4156                            });
4157                            break;
4158                        }
4159                    }
4160
4161                    // End of init block
4162                    if brace_depth <= 0 && idx > lazy_start {
4163                        in_lazy_init = false;
4164                        lazy_type.clear();
4165                    }
4166                }
4167            }
4168        }
4169
4170        findings
4171    }
4172}
4173
4174/// Register all memory safety rules with the rule engine.
4175pub fn register_memory_rules(engine: &mut crate::RuleEngine) {
4176    engine.register_rule(Box::new(BoxIntoRawRule::new()));
4177    engine.register_rule(Box::new(TransmuteRule::new()));
4178    engine.register_rule(Box::new(UnsafeUsageRule::new()));
4179    engine.register_rule(Box::new(NullPointerTransmuteRule::new()));
4180    engine.register_rule(Box::new(ZSTPointerArithmeticRule::new()));
4181    engine.register_rule(Box::new(VecSetLenRule::new()));
4182    engine.register_rule(Box::new(MaybeUninitAssumeInitRule::new()));
4183    engine.register_rule(Box::new(MemUninitZeroedRule::new()));
4184    engine.register_rule(Box::new(NonNullNewUncheckedRule::new()));
4185    engine.register_rule(Box::new(MemForgetGuardRule::new()));
4186    // Advanced memory rules (dataflow-based)
4187    engine.register_rule(Box::new(StaticMutGlobalRule::new()));
4188    engine.register_rule(Box::new(TransmuteLifetimeChangeRule::new()));
4189    engine.register_rule(Box::new(RawPointerEscapeRule::new()));
4190    engine.register_rule(Box::new(VecSetLenMisuseRule::new()));
4191    engine.register_rule(Box::new(LengthTruncationCastRule::new()));
4192    engine.register_rule(Box::new(MaybeUninitAssumeInitDataflowRule::new()));
4193    engine.register_rule(Box::new(SliceElementSizeMismatchRule::new()));
4194    engine.register_rule(Box::new(SliceFromRawPartsRule::new()));
4195    engine.register_rule(Box::new(VarianceTransmuteUnsoundRule::new()));
4196    engine.register_rule(Box::new(ReturnedRefToLocalRule::new()));
4197    engine.register_rule(Box::new(SelfReferentialStructRule::new()));
4198    engine.register_rule(Box::new(UnsafeCellAliasingRule::new()));
4199    engine.register_rule(Box::new(LazyInitPanicPoisonRule::new()));
4200}