1use 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#[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 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 if (lower.contains("*const ()") || lower.contains("*mut ()")) && has_arithmetic {
214 return true;
215 }
216
217 if lower.contains("phantomdata") && has_arithmetic {
219 return true;
220 }
221
222 if lower.contains("phantompinned") && has_arithmetic {
224 return true;
225 }
226
227 if (lower.contains("*const [(); 0]") || lower.contains("*mut [(); 0]")) && has_arithmetic {
229 return true;
230 }
231
232 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
270pub(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
282pub(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
317pub 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
401pub 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
513pub 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 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
617pub 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
704pub 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
791const 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 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
901const 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 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
987const 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 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
1090pub 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 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
1167pub 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 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 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
1270pub 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 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
1351pub 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 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 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 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 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 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
1625pub 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 fn is_unsafe_deref_outlive(line: &str) -> bool {
1660 (line.contains("&*") || line.contains("&mut *"))
1662 && (line.contains("unsafe") || line.contains("ptr"))
1663 }
1664
1665 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 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
1921pub 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
2094pub 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
2180pub 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
2352pub 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
2747pub 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
3181pub 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 fn is_ref_to_mut_transmute(line: &str) -> bool {
3213 if !line.contains("transmute") {
3214 return false;
3215 }
3216
3217 if let Some(transmute_start) = line.find("transmute") {
3219 let after_transmute = &line[transmute_start..];
3220 if after_transmute.contains("::<&") && after_transmute.contains("&mut") {
3222 return true;
3223 }
3224 if after_transmute.contains("::<&") && !after_transmute.contains("&mut") {
3226 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 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 if after_transmute.contains("*const") && after_transmute.contains("*mut") {
3246 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 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 if trimmed.starts_with("//") {
3292 continue;
3293 }
3294
3295 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 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 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
3374pub 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 fn dangerous_return_patterns() -> &'static [&'static str] {
3410 &[
3411 "&*",
3413 "as *const",
3414 "as *mut",
3415 "transmute(&",
3417 "transmute::<&",
3418 "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 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 if trimmed.starts_with("//") {
3488 continue;
3489 }
3490
3491 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 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 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 if trimmed.starts_with("let ") {
3524 if let Some(eq_pos) = trimmed.find('=') {
3525 let var_part = &trimmed[4..eq_pos];
3526 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 if in_unsafe_block && current_fn_returns_ref {
3542 for pattern in Self::dangerous_return_patterns() {
3544 if trimmed.contains(pattern) {
3545 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 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
3598pub 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 fn self_ref_patterns() -> &'static [&'static str] {
3635 &[
3636 "&self.",
3638 "addr_of!(self.",
3639 "addr_of_mut!(self.",
3640 "as *const Self",
3642 "as *mut Self",
3643 "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 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 if trimmed.starts_with("//") {
3715 continue;
3716 }
3717
3718 if trimmed.starts_with("impl ") || trimmed.starts_with("impl<") {
3720 in_impl_block = true;
3721 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 if trimmed.contains("unsafe") {
3741 in_unsafe = true;
3742 }
3743
3744 if in_impl_block && in_unsafe {
3746 for pattern in Self::self_ref_patterns() {
3747 if trimmed.contains(pattern) {
3748 let is_storing = trimmed.contains("self.")
3750 && (trimmed.contains(" = ") || trimmed.contains("="));
3751
3752 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 if trimmed == "}" && in_impl_block {
3784 }
3786 }
3787 }
3788
3789 findings
3790 }
3791}
3792
3793pub 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 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 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 if trimmed.starts_with("//") {
3921 continue;
3922 }
3923
3924 if trimmed.contains("unsafe {") || trimmed.contains("unsafe{") {
3926 in_unsafe = true;
3927 unsafe_start = idx;
3928 }
3929
3930 if in_unsafe {
3931 for (pattern, description) in Self::aliasing_patterns() {
3933 if trimmed.contains(pattern) {
3934 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 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 if trimmed == "}" {
3975 in_unsafe = false;
3976 }
3977 }
3978 }
3979 }
3980
3981 findings
3982 }
3983}
3984
3985pub 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 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 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 if trimmed.starts_with("//") {
4109 continue;
4110 }
4111
4112 for (pattern, desc) in Self::lazy_patterns() {
4114 if trimmed.contains(pattern) {
4115 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 if in_lazy_init {
4131 brace_depth += trimmed.matches('{').count() as i32;
4132 brace_depth -= trimmed.matches('}').count() as i32;
4133
4134 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 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
4174pub 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 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}