Skip to main content

mir_extractor/rules/
concurrency.rs

1//! Concurrency safety rules.
2//!
3//! Rules detecting concurrency issues:
4//! - Mutex guard held across await (RUSTCOLA094)
5//! - Blocking operations in async context (RUSTCOLA037, RUSTCOLA093)
6//! - Unsafe Send/Sync bounds (RUSTCOLA015)
7//! - Non-thread-safe test patterns (RUSTCOLA074)
8//! - Underscore lock guard (RUSTCOLA030)
9//! - Broadcast unsync payload (RUSTCOLA023)
10//! - Panic in Drop (RUSTCOLA040)
11//! - Unwrap in Poll (RUSTCOLA041)
12
13use super::filter_entry;
14use super::utils::{strip_string_literals, StringLiteralState};
15use crate::detect_broadcast_unsync_payloads;
16use crate::{
17    Confidence, Exploitability, Finding, MirPackage, Rule, RuleMetadata, RuleOrigin, Severity,
18};
19use std::collections::HashSet;
20use std::ffi::OsStr;
21use std::fs;
22use std::path::Path;
23use walkdir::WalkDir;
24
25// ============================================================================
26// RUSTCOLA074: Non-Thread-Safe Test Rule
27// ============================================================================
28
29/// Detects test functions that use non-thread-safe types like Rc, RefCell,
30/// Cell, or raw pointers in ways that could cause issues when tests run in parallel.
31pub struct NonThreadSafeTestRule {
32    metadata: RuleMetadata,
33}
34
35impl NonThreadSafeTestRule {
36    pub fn new() -> Self {
37        Self {
38            metadata: RuleMetadata {
39                id: "RUSTCOLA074".to_string(),
40                name: "non-thread-safe-test".to_string(),
41                short_description: "Test function uses non-thread-safe types".to_string(),
42                full_description: "Detects test functions that use non-thread-safe types like Rc, RefCell, \
43                    Cell, or raw pointers in ways that could cause issues when tests run in parallel. \
44                    The Rust test framework runs tests concurrently by default, and using !Send or !Sync \
45                    types with shared state (like static variables) can lead to data races or undefined \
46                    behavior. Consider using thread-safe alternatives (Arc, Mutex, AtomicCell) or marking \
47                    tests that require serialization with #[serial].".to_string(),
48                help_uri: Some("https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html".to_string()),
49                default_severity: Severity::Medium,
50                origin: RuleOrigin::BuiltIn,
51                cwe_ids: Vec::new(),
52                fix_suggestion: None,
53                exploitability: Exploitability::default(),
54            },
55        }
56    }
57
58    /// Non-Send/Sync type patterns
59    fn non_thread_safe_patterns() -> &'static [&'static str] {
60        &[
61            "Rc<",
62            "Rc::",
63            "RefCell<",
64            "RefCell::",
65            "Cell<",
66            "Cell::",
67            "UnsafeCell<",
68            "UnsafeCell::",
69            "*const ",
70            "*mut ",
71        ]
72    }
73
74    /// Check if function name indicates it's a test
75    fn is_test_function(name: &str, signature: &str) -> bool {
76        let looks_like_test_name = name.contains("::test_")
77            || name.starts_with("test_")
78            || name.contains("::tests::")
79            || name.ends_with("_test");
80
81        let no_params = signature.contains("fn()")
82            || signature.contains("fn ()")
83            || (signature.contains('(') && signature.contains("()"));
84
85        looks_like_test_name && no_params
86    }
87
88    /// Check if function body uses non-thread-safe types
89    fn uses_non_thread_safe_types(body: &[String]) -> Vec<String> {
90        let mut evidence = Vec::new();
91        let patterns = Self::non_thread_safe_patterns();
92
93        for line in body {
94            let trimmed = line.trim();
95            if trimmed.starts_with("//") {
96                continue;
97            }
98
99            for pattern in patterns {
100                if trimmed.contains(pattern) {
101                    evidence.push(trimmed.to_string());
102                    break;
103                }
104            }
105        }
106
107        evidence
108    }
109
110    /// Check if test accesses static/global state
111    fn accesses_static_state(body: &[String]) -> bool {
112        body.iter().any(|line| {
113            let trimmed = line.trim();
114            trimmed.contains("static ")
115                || trimmed.contains("lazy_static!")
116                || trimmed.contains("thread_local!")
117                || trimmed.contains("GLOBAL")
118                || trimmed.contains("STATE")
119        })
120    }
121}
122
123impl Rule for NonThreadSafeTestRule {
124    fn metadata(&self) -> &RuleMetadata {
125        &self.metadata
126    }
127
128    fn evaluate(
129        &self,
130        package: &MirPackage,
131        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
132    ) -> Vec<Finding> {
133        let mut findings = Vec::new();
134
135        for function in &package.functions {
136            if !Self::is_test_function(&function.name, &function.signature) {
137                continue;
138            }
139
140            let non_thread_safe_usage = Self::uses_non_thread_safe_types(&function.body);
141
142            if !non_thread_safe_usage.is_empty() {
143                let severity = if Self::accesses_static_state(&function.body) {
144                    Severity::High
145                } else {
146                    self.metadata.default_severity
147                };
148
149                let limited_evidence: Vec<_> = non_thread_safe_usage.into_iter().take(5).collect();
150
151                findings.push(Finding {
152                    rule_id: self.metadata.id.clone(),
153                    rule_name: self.metadata.name.clone(),
154                    severity,
155                    message: format!(
156                        "Test function `{}` uses non-thread-safe types (Rc, RefCell, Cell, raw pointers). \
157                        Tests run in parallel by default; consider using thread-safe alternatives or #[serial].",
158                        function.name
159                    ),
160                    function: function.name.clone(),
161                    function_signature: function.signature.clone(),
162                    evidence: limited_evidence,
163                    span: function.span.clone(),
164                    confidence: Confidence::Medium,
165                    cwe_ids: Vec::new(),
166                    fix_suggestion: None,
167                    code_snippet: None,
168                exploitability: Exploitability::default(),
169                exploitability_score: Exploitability::default().score(),
170                ..Default::default()
171                });
172            }
173        }
174
175        findings
176    }
177}
178
179// ============================================================================
180// RUSTCOLA037: Blocking Sleep in Async Rule
181// ============================================================================
182
183/// Detects std::thread::sleep and other blocking sleep calls inside async functions.
184pub struct BlockingSleepInAsyncRule {
185    metadata: RuleMetadata,
186}
187
188impl BlockingSleepInAsyncRule {
189    pub fn new() -> Self {
190        Self {
191            metadata: RuleMetadata {
192                id: "RUSTCOLA037".to_string(),
193                name: "blocking-sleep-in-async".to_string(),
194                short_description: "Blocking sleep in async function".to_string(),
195                full_description: "Detects std::thread::sleep and other blocking sleep calls inside async functions. \
196                    Blocking sleep in async contexts can stall the executor and prevent other tasks from running, \
197                    potentially causing denial-of-service. Use async sleep (tokio::time::sleep, async_std::task::sleep, etc.) instead.".to_string(),
198                help_uri: Some("https://www.jetbrains.com/help/inspectopedia/RsSleepInsideAsyncFunction.html".to_string()),
199                default_severity: Severity::Medium,
200                origin: RuleOrigin::BuiltIn,
201                cwe_ids: Vec::new(),
202                fix_suggestion: None,
203                exploitability: Exploitability::default(),
204            },
205        }
206    }
207
208    fn blocking_sleep_patterns() -> &'static [&'static str] {
209        &["std::thread::sleep", "thread::sleep", "::thread::sleep"]
210    }
211}
212
213impl Rule for BlockingSleepInAsyncRule {
214    fn metadata(&self) -> &RuleMetadata {
215        &self.metadata
216    }
217
218    fn evaluate(
219        &self,
220        package: &MirPackage,
221        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
222    ) -> Vec<Finding> {
223        if package.crate_name == "mir-extractor" {
224            return Vec::new();
225        }
226
227        let mut findings = Vec::new();
228        let crate_root = Path::new(&package.crate_root);
229
230        if !crate_root.exists() {
231            return findings;
232        }
233
234        for entry in WalkDir::new(crate_root)
235            .into_iter()
236            .filter_entry(|e| filter_entry(e))
237        {
238            let entry = match entry {
239                Ok(e) => e,
240                Err(_) => continue,
241            };
242
243            if !entry.file_type().is_file() {
244                continue;
245            }
246
247            let path = entry.path();
248            if path.extension() != Some(OsStr::new("rs")) {
249                continue;
250            }
251
252            let rel_path = path
253                .strip_prefix(crate_root)
254                .unwrap_or(path)
255                .to_string_lossy()
256                .replace('\\', "/");
257
258            let content = match fs::read_to_string(path) {
259                Ok(c) => c,
260                Err(_) => continue,
261            };
262
263            let lines: Vec<&str> = content.lines().collect();
264
265            let mut in_async_fn = false;
266            let mut async_fn_start = 0;
267            let mut brace_depth = 0;
268            let mut async_fn_name = String::new();
269
270            for (idx, line) in lines.iter().enumerate() {
271                let trimmed = line.trim();
272
273                if trimmed.contains("async fn ") {
274                    in_async_fn = true;
275                    async_fn_start = idx;
276                    brace_depth = 0;
277
278                    if let Some(fn_pos) = trimmed.find("fn ") {
279                        let after_fn = &trimmed[fn_pos + 3..];
280                        if let Some(paren_pos) = after_fn.find('(') {
281                            async_fn_name = after_fn[..paren_pos].trim().to_string();
282                        }
283                    }
284                }
285
286                if in_async_fn {
287                    brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
288                    brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
289
290                    for pattern in Self::blocking_sleep_patterns() {
291                        if trimmed.contains(pattern) {
292                            let location = format!("{}:{}", rel_path, idx + 1);
293
294                            findings.push(Finding::new(
295                                self.metadata.id.clone(),
296                                self.metadata.name.clone(),
297                                self.metadata.default_severity,
298                                format!(
299                                    "Blocking sleep in async function `{}` can stall executor",
300                                    async_fn_name
301                                ),
302                                location,
303                                async_fn_name.clone(),
304                                vec![trimmed.to_string()],
305                                None,
306                            ));
307                        }
308                    }
309
310                    if brace_depth <= 0 && idx > async_fn_start {
311                        in_async_fn = false;
312                    }
313                }
314            }
315        }
316
317        findings
318    }
319}
320
321// ============================================================================
322// RUSTCOLA093: Blocking Operations in Async Rule
323// ============================================================================
324
325/// Detects blocking operations inside async functions that can stall the async executor.
326pub struct BlockingOpsInAsyncRule {
327    metadata: RuleMetadata,
328}
329
330impl BlockingOpsInAsyncRule {
331    pub fn new() -> Self {
332        Self {
333            metadata: RuleMetadata {
334                id: "RUSTCOLA093".to_string(),
335                name: "blocking-ops-in-async".to_string(),
336                short_description: "Blocking operation in async function".to_string(),
337                full_description: "Detects blocking operations inside async functions that can stall the async executor. \
338                    This includes std::sync::Mutex::lock(), std::fs::* operations, std::net::* operations, and blocking I/O. \
339                    These operations block the current thread, preventing the async runtime from executing other tasks. \
340                    Use async alternatives (tokio::sync::Mutex, tokio::fs, tokio::net) or wrap blocking ops in spawn_blocking/block_in_place.".to_string(),
341                help_uri: None,
342                default_severity: Severity::Medium,
343                origin: RuleOrigin::BuiltIn,
344                cwe_ids: Vec::new(),
345                fix_suggestion: None,
346                exploitability: Exploitability::default(),
347            },
348        }
349    }
350
351    /// Blocking patterns to detect with their categories
352    fn blocking_patterns() -> Vec<(&'static str, &'static str, &'static str)> {
353        vec![
354            // Pattern, Category, Recommendation
355            (
356                ".lock().unwrap()",
357                "sync_mutex",
358                "Use tokio::sync::Mutex::lock().await instead",
359            ),
360            (
361                ".lock().expect(",
362                "sync_mutex",
363                "Use tokio::sync::Mutex::lock().await instead",
364            ),
365            (
366                "mutex.lock()",
367                "sync_mutex",
368                "Use tokio::sync::Mutex::lock().await instead",
369            ),
370            (
371                "fs::read_to_string(",
372                "blocking_fs",
373                "Use tokio::fs::read_to_string().await instead",
374            ),
375            (
376                "fs::read(",
377                "blocking_fs",
378                "Use tokio::fs::read().await instead",
379            ),
380            (
381                "fs::write(",
382                "blocking_fs",
383                "Use tokio::fs::write().await instead",
384            ),
385            (
386                "fs::remove_file(",
387                "blocking_fs",
388                "Use tokio::fs::remove_file().await instead",
389            ),
390            (
391                "fs::create_dir(",
392                "blocking_fs",
393                "Use tokio::fs::create_dir().await instead",
394            ),
395            (
396                "fs::create_dir_all(",
397                "blocking_fs",
398                "Use tokio::fs::create_dir_all().await instead",
399            ),
400            (
401                "fs::metadata(",
402                "blocking_fs",
403                "Use tokio::fs::metadata().await instead",
404            ),
405            (
406                "fs::File::open(",
407                "blocking_fs",
408                "Use tokio::fs::File::open().await instead",
409            ),
410            (
411                "fs::File::create(",
412                "blocking_fs",
413                "Use tokio::fs::File::create().await instead",
414            ),
415            (
416                "File::open(",
417                "blocking_fs",
418                "Use tokio::fs::File::open().await instead",
419            ),
420            (
421                "File::create(",
422                "blocking_fs",
423                "Use tokio::fs::File::create().await instead",
424            ),
425            (
426                "TcpStream::connect(",
427                "blocking_net",
428                "Use tokio::net::TcpStream::connect().await instead",
429            ),
430            (
431                "TcpListener::bind(",
432                "blocking_net",
433                "Use tokio::net::TcpListener::bind().await instead",
434            ),
435            (
436                "UdpSocket::bind(",
437                "blocking_net",
438                "Use tokio::net::UdpSocket::bind().await instead",
439            ),
440            (
441                "stdin().read_line(",
442                "blocking_io",
443                "Use tokio::io::stdin() with AsyncBufReadExt instead",
444            ),
445            (
446                "stdin().read(",
447                "blocking_io",
448                "Use tokio::io::stdin() with AsyncReadExt instead",
449            ),
450            (
451                "reqwest::blocking::",
452                "blocking_http",
453                "Use reqwest async API (reqwest::get, Client::new()) instead",
454            ),
455        ]
456    }
457
458    /// Patterns that indicate the blocking op is wrapped safely
459    fn safe_wrappers() -> &'static [&'static str] {
460        &[
461            "spawn_blocking",
462            "block_in_place",
463            "tokio::task::spawn_blocking",
464            "tokio::task::block_in_place",
465            "actix_web::web::block",
466        ]
467    }
468}
469
470impl Rule for BlockingOpsInAsyncRule {
471    fn metadata(&self) -> &RuleMetadata {
472        &self.metadata
473    }
474
475    fn evaluate(
476        &self,
477        package: &MirPackage,
478        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
479    ) -> Vec<Finding> {
480        if package.crate_name == "mir-extractor" {
481            return Vec::new();
482        }
483
484        let mut findings = Vec::new();
485        let crate_root = Path::new(&package.crate_root);
486
487        if !crate_root.exists() {
488            return findings;
489        }
490
491        for entry in WalkDir::new(crate_root)
492            .into_iter()
493            .filter_entry(|e| filter_entry(e))
494        {
495            let entry = match entry {
496                Ok(e) => e,
497                Err(_) => continue,
498            };
499
500            if !entry.file_type().is_file() {
501                continue;
502            }
503
504            let path = entry.path();
505            if path.extension() != Some(OsStr::new("rs")) {
506                continue;
507            }
508
509            let rel_path = path
510                .strip_prefix(crate_root)
511                .unwrap_or(path)
512                .to_string_lossy()
513                .replace('\\', "/");
514
515            let content = match fs::read_to_string(path) {
516                Ok(c) => c,
517                Err(_) => continue,
518            };
519
520            let lines: Vec<&str> = content.lines().collect();
521
522            let mut in_async_fn = false;
523            let mut async_fn_start = 0;
524            let mut brace_depth = 0;
525            let mut async_fn_name = String::new();
526            let mut in_safe_wrapper = false;
527            let mut safe_wrapper_depth = 0;
528
529            for (idx, line) in lines.iter().enumerate() {
530                let trimmed = line.trim();
531
532                if trimmed.contains("async fn ") || trimmed.contains("async move") {
533                    if trimmed.contains("async fn ") {
534                        in_async_fn = true;
535                        async_fn_start = idx;
536                        brace_depth = 0;
537
538                        if let Some(fn_pos) = trimmed.find("fn ") {
539                            let after_fn = &trimmed[fn_pos + 3..];
540                            if let Some(paren_pos) = after_fn.find('(') {
541                                async_fn_name = after_fn[..paren_pos].trim().to_string();
542                            }
543                        }
544                    }
545                }
546
547                if in_async_fn {
548                    for wrapper in Self::safe_wrappers() {
549                        if trimmed.contains(wrapper) {
550                            in_safe_wrapper = true;
551                            safe_wrapper_depth = brace_depth;
552                        }
553                    }
554
555                    brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
556                    brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
557
558                    if in_safe_wrapper && brace_depth <= safe_wrapper_depth {
559                        in_safe_wrapper = false;
560                    }
561
562                    if in_safe_wrapper {
563                        if brace_depth <= 0 && idx > async_fn_start {
564                            in_async_fn = false;
565                        }
566                        continue;
567                    }
568
569                    for (pattern, category, recommendation) in Self::blocking_patterns() {
570                        if trimmed.contains(pattern) {
571                            if trimmed.contains(".await") || trimmed.contains("tokio::") {
572                                continue;
573                            }
574                            if trimmed.starts_with("//")
575                                || trimmed.starts_with("*")
576                                || trimmed.starts_with("/*")
577                            {
578                                continue;
579                            }
580
581                            let fn_content: String = lines[async_fn_start..=idx]
582                                .iter()
583                                .copied()
584                                .collect::<Vec<&str>>()
585                                .join("\n");
586                            if fn_content.contains("tokio::sync::Mutex")
587                                && pattern.contains(".lock")
588                            {
589                                continue;
590                            }
591
592                            let location = format!("{}:{}", rel_path, idx + 1);
593                            let message = match category {
594                                "sync_mutex" => format!(
595                                    "Blocking std::sync::Mutex in async function `{}`. {}",
596                                    async_fn_name, recommendation
597                                ),
598                                "blocking_fs" => format!(
599                                    "Blocking filesystem operation in async function `{}`. {}",
600                                    async_fn_name, recommendation
601                                ),
602                                "blocking_net" => format!(
603                                    "Blocking network operation in async function `{}`. {}",
604                                    async_fn_name, recommendation
605                                ),
606                                "blocking_io" => format!(
607                                    "Blocking I/O in async function `{}`. {}",
608                                    async_fn_name, recommendation
609                                ),
610                                "blocking_http" => format!(
611                                    "Blocking HTTP client in async function `{}`. {}",
612                                    async_fn_name, recommendation
613                                ),
614                                _ => format!(
615                                    "Blocking operation in async function `{}`",
616                                    async_fn_name
617                                ),
618                            };
619
620                            findings.push(Finding {
621                                rule_id: self.metadata.id.clone(),
622                                rule_name: self.metadata.name.clone(),
623                                severity: self.metadata.default_severity,
624                                message,
625                                function: location,
626                                function_signature: async_fn_name.clone(),
627                                evidence: vec![trimmed.to_string()],
628                                span: None,
629                                ..Default::default()
630                            });
631                        }
632                    }
633
634                    if brace_depth <= 0 && idx > async_fn_start {
635                        in_async_fn = false;
636                    }
637                }
638            }
639        }
640
641        findings
642    }
643}
644
645// ============================================================================
646// RUSTCOLA094: Mutex Guard Across Await Rule
647// ============================================================================
648
649/// Detects MutexGuard/RwLockGuard held across await points which can cause deadlocks.
650pub struct MutexGuardAcrossAwaitRule {
651    metadata: RuleMetadata,
652}
653
654impl MutexGuardAcrossAwaitRule {
655    pub fn new() -> Self {
656        Self {
657            metadata: RuleMetadata {
658                id: "RUSTCOLA094".to_string(),
659                name: "mutex-guard-across-await".to_string(),
660                short_description: "MutexGuard held across await point".to_string(),
661                full_description: "Holding a std::sync::MutexGuard or RwLockGuard across an .await point can cause deadlocks. \
662                    When the async task yields, another task on the same thread may try to acquire the same lock, \
663                    leading to deadlock. Use tokio::sync::Mutex or drop the guard before awaiting.".to_string(),
664                help_uri: Some("https://rust-lang.github.io/rust-clippy/master/index.html#await_holding_lock".to_string()),
665                default_severity: Severity::High,
666                origin: RuleOrigin::BuiltIn,
667                cwe_ids: Vec::new(),
668                fix_suggestion: None,
669                exploitability: Exploitability::default(),
670            },
671        }
672    }
673
674    fn guard_patterns() -> &'static [(&'static str, &'static str)] {
675        &[
676            (".lock().unwrap()", "MutexGuard"),
677            (".lock().expect(", "MutexGuard"),
678            (".lock()?", "MutexGuard"),
679            (".read().unwrap()", "RwLockReadGuard"),
680            (".read().expect(", "RwLockReadGuard"),
681            (".read()?", "RwLockReadGuard"),
682            (".write().unwrap()", "RwLockWriteGuard"),
683            (".write().expect(", "RwLockWriteGuard"),
684            (".write()?", "RwLockWriteGuard"),
685        ]
686    }
687
688    fn safe_guard_patterns() -> &'static [&'static str] {
689        &[
690            "tokio::sync::Mutex",
691            "tokio::sync::RwLock",
692            "async_std::sync::Mutex",
693            "async_std::sync::RwLock",
694            "futures::lock::Mutex",
695        ]
696    }
697}
698
699impl Rule for MutexGuardAcrossAwaitRule {
700    fn metadata(&self) -> &RuleMetadata {
701        &self.metadata
702    }
703
704    fn evaluate(
705        &self,
706        package: &MirPackage,
707        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
708    ) -> Vec<Finding> {
709        if package.crate_name == "mir-extractor" {
710            return Vec::new();
711        }
712
713        let mut findings = Vec::new();
714        let crate_root = Path::new(&package.crate_root);
715
716        if !crate_root.exists() {
717            return findings;
718        }
719
720        for entry in WalkDir::new(crate_root)
721            .into_iter()
722            .filter_entry(|e| filter_entry(e))
723        {
724            let entry = match entry {
725                Ok(e) => e,
726                Err(_) => continue,
727            };
728
729            if !entry.file_type().is_file() {
730                continue;
731            }
732
733            let path = entry.path();
734            if path.extension() != Some(OsStr::new("rs")) {
735                continue;
736            }
737
738            let rel_path = path
739                .strip_prefix(crate_root)
740                .unwrap_or(path)
741                .to_string_lossy()
742                .replace('\\', "/");
743
744            let content = match fs::read_to_string(path) {
745                Ok(c) => c,
746                Err(_) => continue,
747            };
748
749            let lines: Vec<&str> = content.lines().collect();
750
751            let mut in_async_fn = false;
752            let mut async_fn_start = 0;
753            let mut brace_depth = 0;
754            let mut async_fn_name = String::new();
755
756            for (idx, line) in lines.iter().enumerate() {
757                let trimmed = line.trim();
758
759                if trimmed.contains("async fn ") || trimmed.contains("async move") {
760                    if trimmed.contains("async fn ") {
761                        in_async_fn = true;
762                        async_fn_start = idx;
763                        brace_depth = 0;
764
765                        if let Some(fn_pos) = trimmed.find("fn ") {
766                            let after_fn = &trimmed[fn_pos + 3..];
767                            if let Some(paren_pos) = after_fn.find('(') {
768                                async_fn_name = after_fn[..paren_pos].trim().to_string();
769                            }
770                        }
771                    }
772                }
773
774                if in_async_fn {
775                    brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
776                    brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
777
778                    for (pattern, guard_type) in Self::guard_patterns() {
779                        if trimmed.contains(pattern) {
780                            if trimmed.contains(".await") {
781                                continue;
782                            }
783                            if trimmed.starts_with("//")
784                                || trimmed.starts_with("*")
785                                || trimmed.starts_with("/*")
786                            {
787                                continue;
788                            }
789
790                            let fn_content: String = lines
791                                [async_fn_start..=std::cmp::min(idx + 50, lines.len() - 1)]
792                                .iter()
793                                .copied()
794                                .collect::<Vec<&str>>()
795                                .join("\n");
796
797                            let uses_async_mutex = Self::safe_guard_patterns()
798                                .iter()
799                                .any(|p| fn_content.contains(p));
800                            if uses_async_mutex {
801                                continue;
802                            }
803
804                            let mut inner_brace_depth = 0;
805                            let mut has_await_after = false;
806                            let mut await_line = 0;
807
808                            for (later_idx, later_line) in lines[idx..].iter().enumerate() {
809                                let later_trimmed = later_line.trim();
810                                inner_brace_depth +=
811                                    later_trimmed.chars().filter(|&c| c == '{').count() as i32;
812                                inner_brace_depth -=
813                                    later_trimmed.chars().filter(|&c| c == '}').count() as i32;
814
815                                if later_trimmed.contains("drop(") {
816                                    break;
817                                }
818
819                                if later_idx > 0 && later_trimmed.contains(".await") {
820                                    has_await_after = true;
821                                    await_line = idx + later_idx + 1;
822                                    break;
823                                }
824
825                                if inner_brace_depth < 0 {
826                                    break;
827                                }
828
829                                if later_idx > 30 {
830                                    break;
831                                }
832                            }
833
834                            if has_await_after {
835                                let location = format!("{}:{}", rel_path, idx + 1);
836                                findings.push(Finding {
837                                    rule_id: self.metadata.id.clone(),
838                                    rule_name: self.metadata.name.clone(),
839                                    severity: self.metadata.default_severity,
840                                    message: format!(
841                                        "{} held across .await in async function `{}` (line {}). \
842                                        This can cause deadlocks. Drop the guard before awaiting or use tokio::sync::Mutex.",
843                                        guard_type, async_fn_name, await_line
844                                    ),
845                                    function: location,
846                                    function_signature: async_fn_name.clone(),
847                                    evidence: vec![trimmed.to_string()],
848                                    span: None,
849                    ..Default::default()
850                                });
851                            }
852                        }
853                    }
854
855                    if brace_depth <= 0 && idx > async_fn_start {
856                        in_async_fn = false;
857                    }
858                }
859            }
860        }
861
862        findings
863    }
864}
865
866// ============================================================================
867// RUSTCOLA030: Underscore Lock Guard Rule
868// ============================================================================
869
870/// Detects lock guards assigned to `_`, which immediately drops the guard.
871pub struct UnderscoreLockGuardRule {
872    metadata: RuleMetadata,
873}
874
875impl UnderscoreLockGuardRule {
876    pub fn new() -> Self {
877        Self {
878            metadata: RuleMetadata {
879                id: "RUSTCOLA030".to_string(),
880                name: "underscore-lock-guard".to_string(),
881                short_description: "Lock guard immediately discarded via underscore binding".to_string(),
882                full_description: "Detects lock guards (Mutex::lock, RwLock::read/write, etc.) assigned to `_`, which immediately drops the guard and releases the lock before the critical section executes, creating race conditions.".to_string(),
883                help_uri: Some("https://rust-lang.github.io/rust-clippy/master/index.html#/let_underscore_lock".to_string()),
884                default_severity: Severity::High,
885                origin: RuleOrigin::BuiltIn,
886                cwe_ids: Vec::new(),
887                fix_suggestion: None,
888                exploitability: Exploitability::default(),
889            },
890        }
891    }
892
893    fn lock_method_patterns() -> &'static [&'static str] {
894        &[
895            "::lock(",
896            "::read(",
897            "::write(",
898            "::try_lock(",
899            "::try_read(",
900            "::try_write(",
901            "::blocking_lock(",
902            "::blocking_read(",
903            "::blocking_write(",
904        ]
905    }
906}
907
908impl Rule for UnderscoreLockGuardRule {
909    fn metadata(&self) -> &RuleMetadata {
910        &self.metadata
911    }
912
913    fn evaluate(
914        &self,
915        package: &MirPackage,
916        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
917    ) -> Vec<Finding> {
918        if package.crate_name == "mir-extractor" {
919            return Vec::new();
920        }
921
922        let mut findings = Vec::new();
923
924        for function in &package.functions {
925            // Step 1: Collect all MIR variables that have debug mappings
926            // Variables with debug mappings are named bindings like "let guard = ..." or "let _guard = ..."
927            // Variables WITHOUT debug mappings are wildcard patterns like "let _ = ..."
928            let mut named_vars: HashSet<String> = HashSet::new();
929
930            for line in &function.body {
931                let trimmed = line.trim();
932                // Pattern: "debug VAR_NAME => _N;"
933                if trimmed.starts_with("debug ") && trimmed.contains(" => ") {
934                    // Extract the _N part (MIR variable)
935                    if let Some(arrow_pos) = trimmed.find(" => ") {
936                        let var_part = trimmed[arrow_pos + 4..].trim().trim_end_matches(';').trim();
937                        if var_part.starts_with('_')
938                            && var_part
939                                .chars()
940                                .nth(1)
941                                .map_or(false, |c| c.is_ascii_digit())
942                        {
943                            named_vars.insert(var_part.to_string());
944                        }
945                    }
946                }
947            }
948
949            let body_lines: Vec<&str> = function.body.iter().map(|s| s.as_str()).collect();
950
951            // Step 2: Find lock acquisitions and trace to guard type
952            // Track: lock_result -> guard_var (via unwrap/expect) -> drop
953            for (i, line) in body_lines.iter().enumerate() {
954                let trimmed = line.trim();
955
956                // Check if the RHS contains a lock acquisition
957                let has_lock_call = Self::lock_method_patterns()
958                    .iter()
959                    .any(|pattern| trimmed.contains(pattern));
960
961                if !has_lock_call {
962                    continue;
963                }
964
965                // Parse the assignment: "_N = ..."
966                if !trimmed.contains(" = ") {
967                    continue;
968                }
969
970                let lock_result_var = trimmed.split(" = ").next().map(|s| s.trim()).unwrap_or("");
971
972                // Skip if not a MIR variable (_N format)
973                if !lock_result_var.starts_with('_')
974                    || !lock_result_var
975                        .chars()
976                        .nth(1)
977                        .map_or(false, |c| c.is_ascii_digit())
978                {
979                    continue;
980                }
981
982                // Case 1: Direct drop of lock result (no unwrap)
983                // Pattern: _N = mutex.lock() then drop(_N)
984                let drop_pattern = format!("drop({})", lock_result_var);
985                let has_direct_drop = body_lines
986                    .iter()
987                    .skip(i + 1)
988                    .take(15)
989                    .any(|future_line| future_line.trim().starts_with(&drop_pattern));
990
991                if has_direct_drop && !named_vars.contains(lock_result_var) {
992                    findings.push(Finding {
993                        rule_id: self.metadata.id.clone(),
994                        rule_name: self.metadata.name.clone(),
995                        severity: self.metadata.default_severity,
996                        message: format!(
997                            "Lock guard assigned to `_` in `{}`, immediately releasing the lock",
998                            function.name
999                        ),
1000                        function: function.name.clone(),
1001                        function_signature: function.signature.clone(),
1002                        evidence: vec![trimmed.to_string()],
1003                        span: function.span.clone(),
1004                        confidence: Confidence::Medium,
1005                        cwe_ids: Vec::new(),
1006                        fix_suggestion: None,
1007                        code_snippet: None,
1008                        exploitability: Exploitability::default(),
1009                        exploitability_score: Exploitability::default().score(),
1010                    ..Default::default()
1011                    });
1012                    continue;
1013                }
1014
1015                // Case 2: Unwrap then drop
1016                // Pattern: _N = mutex.lock() then _M = ...unwrap(move _N) then drop(_M)
1017                let unwrap_source_pattern = format!("(move {})", lock_result_var);
1018                for future_line in body_lines.iter().skip(i + 1).take(15) {
1019                    let future_trimmed = future_line.trim();
1020
1021                    // Look for unwrap/expect of the lock result
1022                    if (future_trimmed.contains("unwrap") || future_trimmed.contains("expect"))
1023                        && future_trimmed.contains(&unwrap_source_pattern)
1024                        && future_trimmed.contains(" = ")
1025                    {
1026                        let guard_var = future_trimmed
1027                            .split(" = ")
1028                            .next()
1029                            .map(|s| s.trim())
1030                            .unwrap_or("");
1031
1032                        // Check if guard_var is unnamed and immediately dropped
1033                        if guard_var.starts_with('_')
1034                            && guard_var
1035                                .chars()
1036                                .nth(1)
1037                                .map_or(false, |c| c.is_ascii_digit())
1038                            && !named_vars.contains(guard_var)
1039                        {
1040                            let guard_drop_pattern = format!("drop({})", guard_var);
1041                            let has_guard_drop = body_lines
1042                                .iter()
1043                                .any(|line| line.trim().starts_with(&guard_drop_pattern));
1044
1045                            if has_guard_drop {
1046                                findings.push(Finding {
1047                                    rule_id: self.metadata.id.clone(),
1048                                    rule_name: self.metadata.name.clone(),
1049                                    severity: self.metadata.default_severity,
1050                                    message: format!(
1051                                        "Lock guard assigned to `_` in `{}`, immediately releasing the lock",
1052                                        function.name
1053                                    ),
1054                                    function: function.name.clone(),
1055                                    function_signature: function.signature.clone(),
1056                                    evidence: vec![trimmed.to_string(), future_trimmed.to_string()],
1057                                    span: function.span.clone(),
1058                    confidence: Confidence::Medium,
1059                    cwe_ids: Vec::new(),
1060                    fix_suggestion: None,
1061                    code_snippet: None,
1062                exploitability: Exploitability::default(),
1063                exploitability_score: Exploitability::default().score(),
1064                                ..Default::default()
1065                                });
1066                                break;
1067                            }
1068                        }
1069                    }
1070                }
1071            }
1072        }
1073
1074        findings
1075    }
1076}
1077
1078// ============================================================================
1079// RUSTCOLA023: Broadcast Unsync Payload Rule
1080// ============================================================================
1081
1082/// Detects tokio broadcast channels with !Sync payloads.
1083pub struct BroadcastUnsyncPayloadRule {
1084    metadata: RuleMetadata,
1085}
1086
1087impl BroadcastUnsyncPayloadRule {
1088    pub fn new() -> Self {
1089        Self {
1090            metadata: RuleMetadata {
1091                id: "RUSTCOLA023".to_string(),
1092                name: "tokio-broadcast-unsync-payload".to_string(),
1093                short_description: "Tokio broadcast carries !Sync payload".to_string(),
1094                full_description: "Warns when `tokio::sync::broadcast` channels are instantiated for types like `Rc`/`RefCell` that are not Sync, enabling unsound clones to cross thread boundaries. See RUSTSEC-2025-0023 for details.".to_string(),
1095                help_uri: Some("https://rustsec.org/advisories/RUSTSEC-2025-0023.html".to_string()),
1096                default_severity: Severity::High,
1097                origin: RuleOrigin::BuiltIn,
1098                cwe_ids: Vec::new(),
1099                fix_suggestion: None,
1100                exploitability: Exploitability::default(),
1101            },
1102        }
1103    }
1104}
1105
1106impl Rule for BroadcastUnsyncPayloadRule {
1107    fn metadata(&self) -> &RuleMetadata {
1108        &self.metadata
1109    }
1110
1111    fn evaluate(
1112        &self,
1113        package: &MirPackage,
1114        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1115    ) -> Vec<Finding> {
1116        let mut findings = Vec::new();
1117
1118        for function in &package.functions {
1119            let usages = detect_broadcast_unsync_payloads(function);
1120
1121            for usage in usages {
1122                findings.push(Finding {
1123                    rule_id: self.metadata.id.clone(),
1124                    rule_name: self.metadata.name.clone(),
1125                    severity: self.metadata.default_severity,
1126                    message: format!(
1127                        "Broadcast channel instantiated with !Sync payload in `{}`",
1128                        function.name
1129                    ),
1130                    function: function.name.clone(),
1131                    function_signature: function.signature.clone(),
1132                    evidence: vec![usage.line.clone()],
1133                    span: function.span.clone(),
1134                    confidence: Confidence::Medium,
1135                    cwe_ids: Vec::new(),
1136                    fix_suggestion: None,
1137                    code_snippet: None,
1138                    exploitability: Exploitability::default(),
1139                    exploitability_score: Exploitability::default().score(),
1140                ..Default::default()
1141                });
1142            }
1143        }
1144
1145        findings
1146    }
1147}
1148
1149// ============================================================================
1150// RUSTCOLA040: Panic In Drop Rule
1151// ============================================================================
1152
1153/// Detects panic!, unwrap(), or expect() in Drop implementations.
1154pub struct PanicInDropRule {
1155    metadata: RuleMetadata,
1156}
1157
1158impl PanicInDropRule {
1159    pub fn new() -> Self {
1160        Self {
1161            metadata: RuleMetadata {
1162                id: "RUSTCOLA040".to_string(),
1163                name: "panic-in-drop".to_string(),
1164                short_description: "panic! or unwrap in Drop implementation".to_string(),
1165                full_description: "Detects panic!, unwrap(), or expect() calls inside Drop trait implementations. Panicking during stack unwinding causes the process to abort, which can mask the original error and make debugging difficult. Drop implementations should handle errors gracefully or use logging instead of panicking.".to_string(),
1166                help_uri: Some("https://doc.rust-lang.org/nomicon/exception-safety.html".to_string()),
1167                default_severity: Severity::Medium,
1168                origin: RuleOrigin::BuiltIn,
1169                cwe_ids: Vec::new(),
1170                fix_suggestion: None,
1171                exploitability: Exploitability::default(),
1172            },
1173        }
1174    }
1175
1176    fn panic_patterns() -> &'static [&'static str] {
1177        &[
1178            "panic!",
1179            ".unwrap()",
1180            ".expect(",
1181            "unreachable!",
1182            "unimplemented!",
1183            "todo!",
1184        ]
1185    }
1186}
1187
1188impl Rule for PanicInDropRule {
1189    fn metadata(&self) -> &RuleMetadata {
1190        &self.metadata
1191    }
1192
1193    fn evaluate(
1194        &self,
1195        package: &MirPackage,
1196        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1197    ) -> Vec<Finding> {
1198        if package.crate_name == "mir-extractor" {
1199            return Vec::new();
1200        }
1201
1202        let mut findings = Vec::new();
1203        let crate_root = Path::new(&package.crate_root);
1204
1205        if !crate_root.exists() {
1206            return findings;
1207        }
1208
1209        for entry in WalkDir::new(crate_root)
1210            .into_iter()
1211            .filter_entry(|e| filter_entry(e))
1212        {
1213            let entry = match entry {
1214                Ok(e) => e,
1215                Err(_) => continue,
1216            };
1217
1218            if !entry.file_type().is_file() {
1219                continue;
1220            }
1221
1222            let path = entry.path();
1223            if path.extension() != Some(OsStr::new("rs")) {
1224                continue;
1225            }
1226
1227            let rel_path = path
1228                .strip_prefix(crate_root)
1229                .unwrap_or(path)
1230                .to_string_lossy()
1231                .replace('\\', "/");
1232
1233            let content = match fs::read_to_string(path) {
1234                Ok(c) => c,
1235                Err(_) => continue,
1236            };
1237
1238            let lines: Vec<&str> = content.lines().collect();
1239
1240            // Track Drop implementation boundaries
1241            let mut in_drop_impl = false;
1242            let mut drop_impl_start = 0;
1243            let mut brace_depth = 0;
1244            let mut drop_type_name = String::new();
1245
1246            for (idx, line) in lines.iter().enumerate() {
1247                let trimmed = line.trim();
1248
1249                // Detect Drop impl start
1250                if trimmed.contains("impl") && trimmed.contains("Drop") && trimmed.contains("for") {
1251                    in_drop_impl = true;
1252                    drop_impl_start = idx;
1253                    brace_depth = 0;
1254
1255                    // Extract type name
1256                    if let Some(for_pos) = trimmed.find("for ") {
1257                        let after_for = &trimmed[for_pos + 4..];
1258                        if let Some(space_pos) =
1259                            after_for.find(|c: char| c.is_whitespace() || c == '{')
1260                        {
1261                            drop_type_name = after_for[..space_pos].trim().to_string();
1262                        } else {
1263                            drop_type_name = after_for.trim().to_string();
1264                        }
1265                    }
1266                }
1267
1268                if in_drop_impl {
1269                    brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
1270                    brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
1271
1272                    // Check for panic patterns
1273                    for pattern in Self::panic_patterns() {
1274                        if trimmed.contains(pattern) {
1275                            // Skip commented lines
1276                            if !trimmed.starts_with("//") {
1277                                let location = format!("{}:{}", rel_path, idx + 1);
1278
1279                                findings.push(Finding {
1280                                    rule_id: self.metadata.id.clone(),
1281                                    rule_name: self.metadata.name.clone(),
1282                                    severity: self.metadata.default_severity,
1283                                    message: format!(
1284                                        "Panic in Drop implementation for `{}` can cause abort during unwinding",
1285                                        drop_type_name
1286                                    ),
1287                                    function: location,
1288                                    function_signature: drop_type_name.clone(),
1289                                    evidence: vec![trimmed.to_string()],
1290                                    span: None,
1291                    ..Default::default()
1292                                });
1293                            }
1294                        }
1295                    }
1296
1297                    // If brace depth returns to 0, we've exited the Drop impl
1298                    if brace_depth <= 0 && idx > drop_impl_start {
1299                        in_drop_impl = false;
1300                    }
1301                }
1302            }
1303        }
1304
1305        findings
1306    }
1307}
1308
1309// ============================================================================
1310// RUSTCOLA041: Unwrap In Poll Rule
1311// ============================================================================
1312
1313/// Detects unwrap(), expect(), or panic! in Future::poll implementations.
1314pub struct UnwrapInPollRule {
1315    metadata: RuleMetadata,
1316}
1317
1318impl UnwrapInPollRule {
1319    pub fn new() -> Self {
1320        Self {
1321            metadata: RuleMetadata {
1322                id: "RUSTCOLA041".to_string(),
1323                name: "unwrap-in-poll".to_string(),
1324                short_description: "unwrap or panic in Future::poll implementation".to_string(),
1325                full_description: "Detects unwrap(), expect(), or panic! calls inside Future::poll implementations. Panicking in poll() can stall async executors, cause runtime hangs, and make debugging async code difficult. Poll implementations should propagate errors using Poll::Ready(Err(...)) or use defensive patterns like match/if-let.".to_string(),
1326                help_uri: Some("https://rust-lang.github.io/async-book/02_execution/03_wakeups.html".to_string()),
1327                default_severity: Severity::Medium,
1328                origin: RuleOrigin::BuiltIn,
1329                cwe_ids: Vec::new(),
1330                fix_suggestion: None,
1331                exploitability: Exploitability::default(),
1332            },
1333        }
1334    }
1335
1336    fn panic_patterns() -> &'static [&'static str] {
1337        &[
1338            "panic!",
1339            ".unwrap()",
1340            ".expect(",
1341            "unreachable!",
1342            "unimplemented!",
1343            "todo!",
1344        ]
1345    }
1346}
1347
1348impl Rule for UnwrapInPollRule {
1349    fn metadata(&self) -> &RuleMetadata {
1350        &self.metadata
1351    }
1352
1353    fn evaluate(
1354        &self,
1355        package: &MirPackage,
1356        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1357    ) -> Vec<Finding> {
1358        if package.crate_name == "mir-extractor" {
1359            return Vec::new();
1360        }
1361
1362        let mut findings = Vec::new();
1363        let crate_root = Path::new(&package.crate_root);
1364
1365        if !crate_root.exists() {
1366            return findings;
1367        }
1368
1369        for entry in WalkDir::new(crate_root)
1370            .into_iter()
1371            .filter_entry(|e| filter_entry(e))
1372        {
1373            let entry = match entry {
1374                Ok(e) => e,
1375                Err(_) => continue,
1376            };
1377
1378            if !entry.file_type().is_file() {
1379                continue;
1380            }
1381
1382            let path = entry.path();
1383            if path.extension() != Some(OsStr::new("rs")) {
1384                continue;
1385            }
1386
1387            let rel_path = path
1388                .strip_prefix(crate_root)
1389                .unwrap_or(path)
1390                .to_string_lossy()
1391                .replace('\\', "/");
1392
1393            let content = match fs::read_to_string(path) {
1394                Ok(c) => c,
1395                Err(_) => continue,
1396            };
1397
1398            let lines: Vec<&str> = content.lines().collect();
1399
1400            // Track Future impl and poll method boundaries
1401            let mut in_future_impl = false;
1402            let mut in_poll_method = false;
1403            let mut poll_start = 0;
1404            let mut brace_depth = 0;
1405            let mut impl_brace_depth = 0;
1406            let mut future_type_name = String::new();
1407
1408            for (idx, line) in lines.iter().enumerate() {
1409                let trimmed = line.trim();
1410
1411                // Detect Future impl start
1412                if !in_future_impl
1413                    && trimmed.contains("impl")
1414                    && trimmed.contains("Future")
1415                    && trimmed.contains("for")
1416                {
1417                    in_future_impl = true;
1418                    impl_brace_depth = 0;
1419
1420                    // Extract type name
1421                    if let Some(for_pos) = trimmed.find("for ") {
1422                        let after_for = &trimmed[for_pos + 4..];
1423                        if let Some(space_pos) =
1424                            after_for.find(|c: char| c.is_whitespace() || c == '{')
1425                        {
1426                            future_type_name = after_for[..space_pos].trim().to_string();
1427                        } else {
1428                            future_type_name = after_for.trim().to_string();
1429                        }
1430                    }
1431                }
1432
1433                if in_future_impl {
1434                    impl_brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
1435                    impl_brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
1436
1437                    // Detect poll method start
1438                    if !in_poll_method
1439                        && (trimmed.contains("fn poll") || trimmed.contains("fn poll("))
1440                    {
1441                        in_poll_method = true;
1442                        poll_start = idx;
1443                        brace_depth = 0;
1444                    }
1445
1446                    if in_poll_method {
1447                        brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
1448                        brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
1449
1450                        // Check for panic patterns
1451                        for pattern in Self::panic_patterns() {
1452                            if trimmed.contains(pattern) {
1453                                // Skip commented lines
1454                                if !trimmed.starts_with("//") {
1455                                    let location = format!("{}:{}", rel_path, idx + 1);
1456
1457                                    findings.push(Finding {
1458                                        rule_id: self.metadata.id.clone(),
1459                                        rule_name: self.metadata.name.clone(),
1460                                        severity: self.metadata.default_severity,
1461                                        message: format!(
1462                                            "Panic in Future::poll for `{}` can stall async executor",
1463                                            future_type_name
1464                                        ),
1465                                        function: location,
1466                                        function_signature: future_type_name.clone(),
1467                                        evidence: vec![trimmed.to_string()],
1468                                        span: None,
1469                    ..Default::default()
1470                                    });
1471                                }
1472                            }
1473                        }
1474
1475                        // If brace depth returns to 0, we've exited the poll method
1476                        if brace_depth <= 0 && idx > poll_start {
1477                            in_poll_method = false;
1478                        }
1479                    }
1480
1481                    // If impl brace depth returns to 0, we've exited the Future impl
1482                    if impl_brace_depth <= 0 && idx > 0 {
1483                        in_future_impl = false;
1484                    }
1485                }
1486            }
1487        }
1488
1489        findings
1490    }
1491}
1492
1493// ============================================================================
1494// RUSTCOLA015: Unsafe Send/Sync Bounds
1495// ============================================================================
1496
1497/// Detects unsafe implementations of Send/Sync for generic types that do not
1498/// constrain their generic parameters, which can reintroduce thread-safety bugs.
1499pub struct UnsafeSendSyncBoundsRule {
1500    metadata: RuleMetadata,
1501}
1502
1503impl UnsafeSendSyncBoundsRule {
1504    pub fn new() -> Self {
1505        Self {
1506            metadata: RuleMetadata {
1507                id: "RUSTCOLA015".to_string(),
1508                name: "unsafe-send-sync-bounds".to_string(),
1509                short_description: "Unsafe Send/Sync impl without generic bounds".to_string(),
1510                full_description: "Detects unsafe implementations of Send/Sync for generic types that do not constrain their generic parameters, which can reintroduce thread-safety bugs.".to_string(),
1511                help_uri: None,
1512                default_severity: Severity::High,
1513                origin: RuleOrigin::BuiltIn,
1514                cwe_ids: Vec::new(),
1515                fix_suggestion: None,
1516                exploitability: Exploitability::default(),
1517            },
1518        }
1519    }
1520
1521    fn has_required_bounds(block_text: &str, trait_name: &str) -> bool {
1522        let trait_marker = format!(" {trait_name} for");
1523        let Some(for_idx) = block_text.find(&trait_marker) else {
1524            return true;
1525        };
1526        let before_for = &block_text[..for_idx];
1527        let Some((generics_text, generic_names)) = Self::extract_generic_params(before_for) else {
1528            return true;
1529        };
1530
1531        if generic_names.is_empty() {
1532            return true;
1533        }
1534
1535        let generic_set: HashSet<String> = generic_names.iter().cloned().collect();
1536        let mut satisfied: HashSet<String> = HashSet::new();
1537
1538        for (name, bounds) in Self::parse_inline_bounds(&generics_text) {
1539            if !generic_set.contains(&name) {
1540                continue;
1541            }
1542
1543            if bounds
1544                .iter()
1545                .any(|bound| Self::bound_matches_trait(bound, trait_name))
1546            {
1547                satisfied.insert(name.clone());
1548            }
1549        }
1550
1551        if let Some(where_clauses) = Self::extract_where_clauses(block_text) {
1552            for (name, bounds) in where_clauses {
1553                if !generic_set.contains(&name) {
1554                    continue;
1555                }
1556
1557                if bounds
1558                    .iter()
1559                    .any(|bound| Self::bound_matches_trait(bound, trait_name))
1560                {
1561                    satisfied.insert(name);
1562                }
1563            }
1564        }
1565
1566        generic_names
1567            .into_iter()
1568            .all(|name| satisfied.contains(&name))
1569    }
1570
1571    fn extract_generic_params(before_for: &str) -> Option<(String, Vec<String>)> {
1572        let start = before_for.find('<')?;
1573        let end_offset = before_for[start..].find('>')?;
1574        let generics_text = before_for[start + 1..start + end_offset].trim().to_string();
1575
1576        let mut names = Vec::new();
1577        for param in generics_text.split(',') {
1578            if let Some(name) = Self::normalize_generic_name(param) {
1579                names.push(name);
1580            }
1581        }
1582
1583        Some((generics_text, names))
1584    }
1585
1586    fn parse_inline_bounds(generics_text: &str) -> Vec<(String, Vec<String>)> {
1587        generics_text
1588            .split(',')
1589            .filter_map(|param| {
1590                let Some(name) = Self::normalize_generic_name(param) else {
1591                    return None;
1592                };
1593
1594                let trimmed = param.trim();
1595                let mut parts = trimmed.splitn(2, ':');
1596                parts.next()?;
1597                let bounds = parts
1598                    .next()
1599                    .map(|rest| Self::split_bounds(rest))
1600                    .unwrap_or_default();
1601
1602                Some((name, bounds))
1603            })
1604            .collect()
1605    }
1606
1607    fn normalize_generic_name(token: &str) -> Option<String> {
1608        let token = token.trim();
1609        if token.is_empty() {
1610            return None;
1611        }
1612
1613        if token.starts_with("const ") {
1614            return None;
1615        }
1616
1617        if token.starts_with('\'') {
1618            return None;
1619        }
1620
1621        let ident = token
1622            .split(|c: char| c == ':' || c == '=' || c.is_whitespace())
1623            .next()
1624            .unwrap_or("")
1625            .trim();
1626
1627        if ident.is_empty() {
1628            None
1629        } else {
1630            Some(ident.to_string())
1631        }
1632    }
1633
1634    fn extract_where_clauses(block_text: &str) -> Option<Vec<(String, Vec<String>)>> {
1635        let where_idx = block_text.find(" where ")?;
1636        let after_where = &block_text[where_idx + " where ".len()..];
1637        let end_idx = after_where
1638            .find('{')
1639            .or_else(|| after_where.find(';'))
1640            .unwrap_or(after_where.len());
1641        let clauses = after_where[..end_idx].trim();
1642        if clauses.is_empty() {
1643            return Some(Vec::new());
1644        }
1645
1646        let mut result = Vec::new();
1647        for predicate in clauses.split(',') {
1648            let pred = predicate.trim();
1649            if pred.is_empty() {
1650                continue;
1651            }
1652
1653            let mut parts = pred.splitn(2, ':');
1654            let ident = parts.next().unwrap_or("").trim();
1655            if ident.is_empty() {
1656                continue;
1657            }
1658
1659            let bounds = parts
1660                .next()
1661                .map(|rest| Self::split_bounds(rest))
1662                .unwrap_or_default();
1663            result.push((ident.to_string(), bounds));
1664        }
1665
1666        Some(result)
1667    }
1668
1669    fn split_bounds(bounds: &str) -> Vec<String> {
1670        bounds
1671            .split('+')
1672            .map(|part| {
1673                part.trim()
1674                    .trim_start_matches('?')
1675                    .trim_start_matches("~const ")
1676                    .trim_end_matches(|c| matches!(c, ',' | '{' | ';'))
1677                    .to_string()
1678            })
1679            .filter(|part| !part.is_empty())
1680            .collect()
1681    }
1682
1683    fn scan_string_state(
1684        state: StringLiteralState,
1685        line: &str,
1686    ) -> (bool, String, StringLiteralState) {
1687        let (sanitized, state_after) = strip_string_literals(state, line);
1688        let has_impl = sanitized.contains("unsafe impl")
1689            && (sanitized.contains(" Send for") || sanitized.contains(" Sync for"));
1690        (has_impl, sanitized, state_after)
1691    }
1692
1693    fn bound_matches_trait(bound: &str, trait_name: &str) -> bool {
1694        let normalized = bound.trim();
1695        if normalized.is_empty() {
1696            return false;
1697        }
1698
1699        let normalized = normalized
1700            .trim_start_matches("dyn ")
1701            .trim_start_matches("impl ");
1702
1703        if normalized == trait_name {
1704            return true;
1705        }
1706
1707        if normalized.ends_with(trait_name)
1708            && normalized
1709                .trim_end_matches(trait_name)
1710                .trim_end()
1711                .ends_with("::")
1712        {
1713            return true;
1714        }
1715
1716        if let Some(start) = normalized.find('<') {
1717            let (path, generics) = normalized.split_at(start);
1718            if Self::bound_matches_trait(path.trim_end_matches('<'), trait_name) {
1719                return generics
1720                    .trim_matches(|c| c == '<' || c == '>')
1721                    .split(',')
1722                    .any(|part| Self::bound_matches_trait(part, trait_name));
1723            }
1724        }
1725
1726        if let Some(idx) = normalized.find('<') {
1727            let inner = normalized[idx + 1..].trim_end_matches('>').trim();
1728            if inner.starts_with("*const") || inner.starts_with("*mut") || inner.starts_with('&') {
1729                return true;
1730            }
1731        }
1732
1733        let tokens: Vec<_> = normalized
1734            .split(|c: char| c == ':' || c == '+' || c == ',' || c.is_whitespace())
1735            .filter(|token| !token.is_empty())
1736            .collect();
1737
1738        if tokens.iter().any(|token| token == &trait_name) {
1739            return true;
1740        }
1741
1742        if trait_name == "Send"
1743            && tokens
1744                .iter()
1745                .any(|token| *token == "Sync" || token.ends_with("::Sync"))
1746        {
1747            return true;
1748        }
1749
1750        if trait_name == "Sync"
1751            && tokens
1752                .iter()
1753                .any(|token| *token == "Send" || token.ends_with("::Send"))
1754        {
1755            return true;
1756        }
1757
1758        false
1759    }
1760}
1761
1762impl Rule for UnsafeSendSyncBoundsRule {
1763    fn metadata(&self) -> &RuleMetadata {
1764        &self.metadata
1765    }
1766
1767    fn evaluate(
1768        &self,
1769        package: &MirPackage,
1770        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1771    ) -> Vec<Finding> {
1772        let mut findings = Vec::new();
1773        let crate_root = Path::new(&package.crate_root);
1774
1775        if !crate_root.exists() {
1776            return findings;
1777        }
1778
1779        for entry in WalkDir::new(crate_root)
1780            .into_iter()
1781            .filter_entry(|e| filter_entry(e))
1782        {
1783            let entry = match entry {
1784                Ok(e) => e,
1785                Err(_) => continue,
1786            };
1787
1788            if !entry.file_type().is_file() {
1789                continue;
1790            }
1791
1792            if entry.path().extension().and_then(OsStr::to_str) != Some("rs") {
1793                continue;
1794            }
1795
1796            let Ok(source) = fs::read_to_string(entry.path()) else {
1797                continue;
1798            };
1799
1800            let rel_path = entry
1801                .path()
1802                .strip_prefix(crate_root)
1803                .unwrap_or_else(|_| entry.path())
1804                .to_string_lossy()
1805                .replace('\\', "/");
1806
1807            let lines: Vec<&str> = source.lines().collect();
1808            let mut idx = 0usize;
1809            let mut string_state = StringLiteralState::default();
1810
1811            while idx < lines.len() {
1812                let line = lines[idx];
1813                let (has_impl, sanitized_line, mut state_after_line) =
1814                    Self::scan_string_state(string_state, line);
1815                let trimmed_sanitized = sanitized_line.trim();
1816
1817                if !has_impl {
1818                    string_state = state_after_line;
1819                    idx += 1;
1820                    continue;
1821                }
1822
1823                let mut block_lines = Vec::new();
1824                let trimmed_first = line.trim();
1825                if !trimmed_first.is_empty() {
1826                    block_lines.push(trimmed_first.to_string());
1827                }
1828
1829                let mut block_complete =
1830                    trimmed_sanitized.contains('{') || trimmed_sanitized.ends_with(';');
1831
1832                let mut j = idx;
1833                while !block_complete && j + 1 < lines.len() {
1834                    let next_line = lines[j + 1];
1835                    let (next_has_impl, next_sanitized, next_state) =
1836                        Self::scan_string_state(state_after_line, next_line);
1837                    let trimmed_original = next_line.trim();
1838                    let trimmed_sanitized_next = next_sanitized.trim();
1839                    let mut appended = false;
1840
1841                    if !trimmed_original.is_empty() {
1842                        block_lines.push(trimmed_original.to_string());
1843                        appended = true;
1844                    }
1845
1846                    state_after_line = next_state;
1847                    block_complete = trimmed_sanitized_next.contains('{')
1848                        || trimmed_sanitized_next.ends_with(';');
1849
1850                    if next_has_impl {
1851                        if appended {
1852                            block_lines.pop();
1853                        }
1854                        break;
1855                    }
1856
1857                    j += 1;
1858                }
1859
1860                let block_text = block_lines.join(" ");
1861                let trait_name = if block_text.contains(" Send for") {
1862                    "Send"
1863                } else if block_text.contains(" Sync for") {
1864                    "Sync"
1865                } else {
1866                    string_state = state_after_line;
1867                    idx = j + 1;
1868                    continue;
1869                };
1870
1871                if !Self::has_required_bounds(&block_text, trait_name) {
1872                    let location = format!("{}:{}", rel_path, idx + 1);
1873                    findings.push(Finding::new(
1874                        self.metadata.id.clone(),
1875                        self.metadata.name.clone(),
1876                        self.metadata.default_severity,
1877                        format!("Unsafe impl of {trait_name} without generic bounds"),
1878                        location,
1879                        block_lines
1880                            .first()
1881                            .cloned()
1882                            .unwrap_or_else(|| trait_name.to_string()),
1883                        block_lines.clone(),
1884                        None,
1885                    ));
1886                }
1887
1888                string_state = state_after_line;
1889                idx = j + 1;
1890            }
1891        }
1892
1893        findings
1894    }
1895}
1896
1897// ============================================================================
1898// RUSTCOLA115: Non-Cancellation-Safe Select Rule
1899// ============================================================================
1900
1901/// Detects use of potentially non-cancellation-safe futures in select! macro.
1902///
1903/// When using `select!`, if a branch is not chosen, its future is dropped.
1904/// If that future has made partial progress (e.g., partially read from a channel),
1905/// that progress is lost. This can lead to data loss or unexpected behavior.
1906pub struct NonCancellationSafeSelectRule {
1907    metadata: RuleMetadata,
1908}
1909
1910impl NonCancellationSafeSelectRule {
1911    pub fn new() -> Self {
1912        Self {
1913            metadata: RuleMetadata {
1914                id: "RUSTCOLA115".to_string(),
1915                name: "non-cancellation-safe-select".to_string(),
1916                short_description: "Potentially non-cancellation-safe future in select!".to_string(),
1917                full_description: "Detects use of potentially non-cancellation-safe futures in select! macro. \
1918                    When using tokio::select!, futures::select!, or similar macros, if a branch is not chosen, \
1919                    its future is dropped. If the future has made partial progress (e.g., started reading from \
1920                    a buffered stream), that progress is lost. Common non-cancellation-safe patterns include: \
1921                    - read_line() / read_until() on buffered streams \
1922                    - recv_many() on channels \
1923                    - Futures that modify internal state before awaiting \
1924                    Consider using cancellation-safe alternatives or restructuring the code.".to_string(),
1925                help_uri: Some("https://docs.rs/tokio/latest/tokio/macro.select.html#cancellation-safety".to_string()),
1926                default_severity: Severity::Medium,
1927                origin: RuleOrigin::BuiltIn,
1928                cwe_ids: Vec::new(),
1929                fix_suggestion: None,
1930                exploitability: Exploitability::default(),
1931            },
1932        }
1933    }
1934
1935    /// Patterns that are known to be non-cancellation-safe
1936    fn non_cancellation_safe_patterns() -> &'static [(&'static str, &'static str)] {
1937        &[
1938            (
1939                "read_line(",
1940                "BufRead::read_line is not cancellation safe - partial reads are lost",
1941            ),
1942            (
1943                "read_until(",
1944                "BufRead::read_until is not cancellation safe - partial reads are lost",
1945            ),
1946            (
1947                "read_exact(",
1948                "read_exact is not cancellation safe - partial reads are lost",
1949            ),
1950            (
1951                "recv_many(",
1952                "recv_many is not cancellation safe - some messages may be lost",
1953            ),
1954            (
1955                "read_to_end(",
1956                "read_to_end is not cancellation safe - partial reads are lost",
1957            ),
1958            (
1959                "read_to_string(",
1960                "read_to_string is not cancellation safe - partial reads are lost",
1961            ),
1962            (
1963                "copy(",
1964                "io::copy is not cancellation safe - partial copy progress is lost",
1965            ),
1966            (
1967                "copy_buf(",
1968                "io::copy_buf is not cancellation safe - partial copy progress is lost",
1969            ),
1970        ]
1971    }
1972
1973    /// Patterns that indicate we're in a select! context
1974    fn select_macro_patterns() -> &'static [&'static str] {
1975        &[
1976            "select!",
1977            "tokio::select!",
1978            "futures::select!",
1979            "futures_util::select!",
1980        ]
1981    }
1982}
1983
1984impl Rule for NonCancellationSafeSelectRule {
1985    fn metadata(&self) -> &RuleMetadata {
1986        &self.metadata
1987    }
1988
1989    fn evaluate(
1990        &self,
1991        package: &MirPackage,
1992        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1993    ) -> Vec<Finding> {
1994        if package.crate_name == "mir-extractor" {
1995            return Vec::new();
1996        }
1997
1998        let mut findings = Vec::new();
1999        let crate_root = Path::new(&package.crate_root);
2000
2001        if !crate_root.exists() {
2002            return findings;
2003        }
2004
2005        for entry in WalkDir::new(crate_root)
2006            .into_iter()
2007            .filter_entry(|e| filter_entry(e))
2008        {
2009            let entry = match entry {
2010                Ok(e) => e,
2011                Err(_) => continue,
2012            };
2013
2014            if !entry.file_type().is_file() {
2015                continue;
2016            }
2017
2018            let path = entry.path();
2019            if path.extension() != Some(OsStr::new("rs")) {
2020                continue;
2021            }
2022
2023            let rel_path = path
2024                .strip_prefix(crate_root)
2025                .unwrap_or(path)
2026                .to_string_lossy()
2027                .replace('\\', "/");
2028
2029            let content = match fs::read_to_string(path) {
2030                Ok(c) => c,
2031                Err(_) => continue,
2032            };
2033
2034            let lines: Vec<&str> = content.lines().collect();
2035
2036            // Look for select! macro usage
2037            let mut in_select = false;
2038            let mut select_start_line = 0;
2039            let mut brace_depth = 0;
2040
2041            for (idx, line) in lines.iter().enumerate() {
2042                let trimmed = line.trim();
2043
2044                // Skip comments
2045                if trimmed.starts_with("//") {
2046                    continue;
2047                }
2048
2049                // Check for select! macro start
2050                for pattern in Self::select_macro_patterns() {
2051                    if trimmed.contains(pattern) && !in_select {
2052                        in_select = true;
2053                        select_start_line = idx;
2054                        brace_depth = 0;
2055                    }
2056                }
2057
2058                if in_select {
2059                    brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
2060                    brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
2061
2062                    // Check for non-cancellation-safe patterns within select!
2063                    for (pattern, reason) in Self::non_cancellation_safe_patterns() {
2064                        if trimmed.contains(pattern) {
2065                            let location = format!("{}:{}", rel_path, idx + 1);
2066
2067                            findings.push(Finding {
2068                                rule_id: self.metadata.id.clone(),
2069                                rule_name: self.metadata.name.clone(),
2070                                severity: self.metadata.default_severity,
2071                                message: format!(
2072                                    "Non-cancellation-safe operation `{}` used in select! macro. {}",
2073                                    pattern.trim_end_matches('('),
2074                                    reason
2075                                ),
2076                                function: location,
2077                                function_signature: String::new(),
2078                                evidence: vec![trimmed.to_string()],
2079                                span: None,
2080                    ..Default::default()
2081                            });
2082                        }
2083                    }
2084
2085                    // End of select! macro
2086                    if brace_depth <= 0 && idx > select_start_line {
2087                        in_select = false;
2088                    }
2089                }
2090            }
2091        }
2092
2093        findings
2094    }
2095}
2096
2097// ============================================================================
2098// RUSTCOLA111: Missing Sync Bound on Clone Rule
2099// ============================================================================
2100
2101/// Detects concurrent data structures that clone values without requiring Sync bound.
2102///
2103/// This pattern was found in tokio broadcast channels (RUSTSEC-2025-0023) where
2104/// cloning a value in a concurrent context without `Sync` bound can cause data races.
2105/// When multiple threads clone the same inner value simultaneously without synchronization,
2106/// undefined behavior can occur.
2107pub struct MissingSyncBoundOnCloneRule {
2108    metadata: RuleMetadata,
2109}
2110
2111impl MissingSyncBoundOnCloneRule {
2112    pub fn new() -> Self {
2113        Self {
2114            metadata: RuleMetadata {
2115                id: "RUSTCOLA111".to_string(),
2116                name: "missing-sync-bound-on-clone".to_string(),
2117                short_description: "Clone in concurrent context without Sync bound".to_string(),
2118                full_description: "Detects implementations that clone values in concurrent contexts \
2119                    without requiring the `Sync` trait bound. This pattern was found in tokio's \
2120                    broadcast channel (RUSTSEC-2025-0023) where concurrent cloning without Sync \
2121                    can cause data races. Channels and shared data structures that clone inner \
2122                    values should require `T: Clone + Send + Sync` instead of just `T: Clone + Send`.".to_string(),
2123                help_uri: Some("https://rustsec.org/advisories/RUSTSEC-2025-0023.html".to_string()),
2124                default_severity: Severity::High,
2125                origin: RuleOrigin::BuiltIn,
2126                cwe_ids: Vec::new(),
2127                fix_suggestion: None,
2128                exploitability: Exploitability::default(),
2129            },
2130        }
2131    }
2132}
2133
2134impl Rule for MissingSyncBoundOnCloneRule {
2135    fn metadata(&self) -> &RuleMetadata {
2136        &self.metadata
2137    }
2138
2139    fn evaluate(
2140        &self,
2141        package: &MirPackage,
2142        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2143    ) -> Vec<Finding> {
2144        if package.crate_name == "mir-extractor" {
2145            return Vec::new();
2146        }
2147
2148        let mut findings = Vec::new();
2149        let crate_root = Path::new(&package.crate_root);
2150
2151        if !crate_root.exists() {
2152            return findings;
2153        }
2154
2155        for entry in WalkDir::new(crate_root)
2156            .into_iter()
2157            .filter_entry(|e| filter_entry(e))
2158        {
2159            let entry = match entry {
2160                Ok(e) => e,
2161                Err(_) => continue,
2162            };
2163
2164            if !entry.file_type().is_file() {
2165                continue;
2166            }
2167
2168            let path = entry.path();
2169            if path.extension() != Some(OsStr::new("rs")) {
2170                continue;
2171            }
2172
2173            let rel_path = path
2174                .strip_prefix(crate_root)
2175                .unwrap_or(path)
2176                .to_string_lossy()
2177                .replace('\\', "/");
2178
2179            let content = match fs::read_to_string(path) {
2180                Ok(c) => c,
2181                Err(_) => continue,
2182            };
2183
2184            let lines: Vec<&str> = content.lines().collect();
2185
2186            // Track impl blocks that use Clone + Send but not Sync
2187            // and also use unsafe impl Sync or have channel-like names
2188            for (idx, line) in lines.iter().enumerate() {
2189                let trimmed = line.trim();
2190
2191                // Skip comments
2192                if trimmed.starts_with("//") {
2193                    continue;
2194                }
2195
2196                // Pattern 1: unsafe impl Sync for types that clone without Sync bound
2197                if trimmed.contains("unsafe impl") && trimmed.contains("Sync") {
2198                    // Check if this might be a channel or shared structure
2199                    let is_channel_like = trimmed.contains("Sender")
2200                        || trimmed.contains("Receiver")
2201                        || trimmed.contains("Channel")
2202                        || trimmed.contains("Broadcast")
2203                        || trimmed.contains("Queue")
2204                        || trimmed.contains("Buffer");
2205
2206                    if is_channel_like {
2207                        // Look for Clone + Send without Sync in nearby context
2208                        let context_start = idx.saturating_sub(20);
2209                        let context_end = (idx + 20).min(lines.len());
2210
2211                        let has_clone_send = lines[context_start..context_end]
2212                            .iter()
2213                            .any(|l| l.contains("Clone") && l.contains("Send"));
2214                        let has_sync_bound = lines[context_start..context_end]
2215                            .iter()
2216                            .any(|l| l.contains(": Sync") || l.contains("+ Sync"));
2217
2218                        if has_clone_send && !has_sync_bound {
2219                            let location = format!("{}:{}", rel_path, idx + 1);
2220
2221                            findings.push(Finding::new(
2222                                self.metadata.id.clone(),
2223                                self.metadata.name.clone(),
2224                                self.metadata.default_severity,
2225                                "unsafe impl Sync for channel-like type with Clone + Send \
2226                                    but no Sync bound. This may allow data races when cloning. \
2227                                    Consider adding `T: Sync` bound."
2228                                    .to_string(),
2229                                location,
2230                                String::new(),
2231                                vec![trimmed.to_string()],
2232                                None,
2233                            ));
2234                        }
2235                    }
2236                }
2237
2238                // Pattern 2: impl blocks with Clone + Send but not Sync for channel types
2239                if trimmed.starts_with("impl")
2240                    && trimmed.contains("Clone + Send")
2241                    && !trimmed.contains("Sync")
2242                {
2243                    let is_channel_like = trimmed.contains("Sender")
2244                        || trimmed.contains("Receiver")
2245                        || trimmed.contains("Channel")
2246                        || trimmed.contains("Broadcast");
2247
2248                    if is_channel_like {
2249                        let location = format!("{}:{}", rel_path, idx + 1);
2250
2251                        findings.push(Finding::new(
2252                            self.metadata.id.clone(),
2253                            self.metadata.name.clone(),
2254                            self.metadata.default_severity,
2255                            "Channel implementation with Clone + Send but missing Sync \
2256                                bound. Concurrent cloning may cause data races. \
2257                                Consider `T: Clone + Send + Sync`."
2258                                .to_string(),
2259                            location,
2260                            String::new(),
2261                            vec![trimmed.to_string()],
2262                            None,
2263                        ));
2264                    }
2265                }
2266            }
2267        }
2268
2269        findings
2270    }
2271}
2272
2273// ============================================================================
2274// RUSTCOLA112: Pin Contract Violation Rule
2275// ============================================================================
2276
2277/// Detects potential Pin contract violations through unsplit/reconstruction patterns.
2278///
2279/// Based on RUSTSEC-2023-0005 where ReadHalf::unsplit could violate Pin contract
2280/// for !Unpin types. When split IO types are unsplit, pinned data may be moved
2281/// incorrectly, leading to use-after-free.
2282pub struct PinContractViolationRule {
2283    metadata: RuleMetadata,
2284}
2285
2286impl PinContractViolationRule {
2287    pub fn new() -> Self {
2288        Self {
2289            metadata: RuleMetadata {
2290                id: "RUSTCOLA112".to_string(),
2291                name: "pin-contract-violation".to_string(),
2292                short_description: "Potential Pin contract violation through unsplit".to_string(),
2293                full_description: "Detects potential Pin contract violations through unsplit or \
2294                    reconstruction patterns. When split IO types are recombined using unsplit(), \
2295                    pinned data for !Unpin types may be moved incorrectly. Also detects unsafe \
2296                    Pin::new_unchecked followed by potential moves. Based on RUSTSEC-2023-0005."
2297                    .to_string(),
2298                help_uri: Some("https://rustsec.org/advisories/RUSTSEC-2023-0005.html".to_string()),
2299                default_severity: Severity::High,
2300                origin: RuleOrigin::BuiltIn,
2301                cwe_ids: Vec::new(),
2302                fix_suggestion: None,
2303                exploitability: Exploitability::default(),
2304            },
2305        }
2306    }
2307}
2308
2309impl Rule for PinContractViolationRule {
2310    fn metadata(&self) -> &RuleMetadata {
2311        &self.metadata
2312    }
2313
2314    fn evaluate(
2315        &self,
2316        package: &MirPackage,
2317        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2318    ) -> Vec<Finding> {
2319        if package.crate_name == "mir-extractor" {
2320            return Vec::new();
2321        }
2322
2323        let mut findings = Vec::new();
2324        let crate_root = Path::new(&package.crate_root);
2325
2326        if !crate_root.exists() {
2327            return findings;
2328        }
2329
2330        for entry in WalkDir::new(crate_root)
2331            .into_iter()
2332            .filter_entry(|e| filter_entry(e))
2333        {
2334            let entry = match entry {
2335                Ok(e) => e,
2336                Err(_) => continue,
2337            };
2338
2339            if !entry.file_type().is_file() {
2340                continue;
2341            }
2342
2343            let path = entry.path();
2344            if path.extension() != Some(OsStr::new("rs")) {
2345                continue;
2346            }
2347
2348            let rel_path = path
2349                .strip_prefix(crate_root)
2350                .unwrap_or(path)
2351                .to_string_lossy()
2352                .replace('\\', "/");
2353
2354            let content = match fs::read_to_string(path) {
2355                Ok(c) => c,
2356                Err(_) => continue,
2357            };
2358
2359            let lines: Vec<&str> = content.lines().collect();
2360
2361            for (idx, line) in lines.iter().enumerate() {
2362                let trimmed = line.trim();
2363
2364                // Skip comments
2365                if trimmed.starts_with("//") {
2366                    continue;
2367                }
2368
2369                // Pattern 1: unsplit() calls on IO types
2370                if trimmed.contains(".unsplit(") {
2371                    let location = format!("{}:{}", rel_path, idx + 1);
2372
2373                    findings.push(Finding {
2374                        rule_id: self.metadata.id.clone(),
2375                        rule_name: self.metadata.name.clone(),
2376                        severity: self.metadata.default_severity,
2377                        message: "Use of unsplit() may violate Pin contract for !Unpin types. \
2378                            When recombining split IO halves, pinned data may be moved incorrectly. \
2379                            Ensure the underlying type is Unpin or use appropriate synchronization.".to_string(),
2380                        function: location,
2381                        function_signature: String::new(),
2382                        evidence: vec![trimmed.to_string()],
2383                        span: None,
2384                    ..Default::default()
2385                    });
2386                }
2387
2388                // Pattern 2: Pin::new_unchecked in unsafe blocks
2389                if trimmed.contains("Pin::new_unchecked") {
2390                    let location = format!("{}:{}", rel_path, idx + 1);
2391
2392                    findings.push(Finding::new(
2393                        self.metadata.id.clone(),
2394                        self.metadata.name.clone(),
2395                        Severity::Medium,
2396                        "Use of Pin::new_unchecked requires ensuring the pinned value \
2397                            is never moved. Verify the value is not moved after pinning."
2398                            .to_string(),
2399                        location,
2400                        String::new(),
2401                        vec![trimmed.to_string()],
2402                        None,
2403                    ));
2404                }
2405            }
2406        }
2407
2408        findings
2409    }
2410}
2411
2412// ============================================================================
2413// RUSTCOLA113: Oneshot Race After Close Rule
2414// ============================================================================
2415
2416/// Detects potential race conditions when using oneshot channels after close().
2417///
2418/// Based on RUSTSEC-2021-0124 where concurrent close(), send(), and recv()
2419/// operations on a oneshot channel could cause data races.
2420pub struct OneshotRaceAfterCloseRule {
2421    metadata: RuleMetadata,
2422}
2423
2424impl OneshotRaceAfterCloseRule {
2425    pub fn new() -> Self {
2426        Self {
2427            metadata: RuleMetadata {
2428                id: "RUSTCOLA113".to_string(),
2429                name: "oneshot-race-after-close".to_string(),
2430                short_description: "Potential race condition with oneshot channel close()"
2431                    .to_string(),
2432                full_description:
2433                    "Detects patterns where oneshot::Receiver::close() may be called \
2434                    concurrently with send() or recv()/await operations. This pattern can cause \
2435                    data races as the sender and receiver may concurrently access shared memory. \
2436                    Based on RUSTSEC-2021-0124."
2437                        .to_string(),
2438                help_uri: Some("https://rustsec.org/advisories/RUSTSEC-2021-0124.html".to_string()),
2439                default_severity: Severity::High,
2440                origin: RuleOrigin::BuiltIn,
2441                cwe_ids: Vec::new(),
2442                fix_suggestion: None,
2443                exploitability: Exploitability::default(),
2444            },
2445        }
2446    }
2447}
2448
2449impl Rule for OneshotRaceAfterCloseRule {
2450    fn metadata(&self) -> &RuleMetadata {
2451        &self.metadata
2452    }
2453
2454    fn evaluate(
2455        &self,
2456        package: &MirPackage,
2457        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2458    ) -> Vec<Finding> {
2459        if package.crate_name == "mir-extractor" {
2460            return Vec::new();
2461        }
2462
2463        let mut findings = Vec::new();
2464        let crate_root = Path::new(&package.crate_root);
2465
2466        if !crate_root.exists() {
2467            return findings;
2468        }
2469
2470        for entry in WalkDir::new(crate_root)
2471            .into_iter()
2472            .filter_entry(|e| filter_entry(e))
2473        {
2474            let entry = match entry {
2475                Ok(e) => e,
2476                Err(_) => continue,
2477            };
2478
2479            if !entry.file_type().is_file() {
2480                continue;
2481            }
2482
2483            let path = entry.path();
2484            if path.extension() != Some(OsStr::new("rs")) {
2485                continue;
2486            }
2487
2488            let rel_path = path
2489                .strip_prefix(crate_root)
2490                .unwrap_or(path)
2491                .to_string_lossy()
2492                .replace('\\', "/");
2493
2494            let content = match fs::read_to_string(path) {
2495                Ok(c) => c,
2496                Err(_) => continue,
2497            };
2498
2499            // Check if file uses oneshot channels
2500            if !content.contains("oneshot::") && !content.contains("oneshot::{") {
2501                continue;
2502            }
2503
2504            let lines: Vec<&str> = content.lines().collect();
2505            let mut has_close_call = false;
2506            let mut close_line = 0;
2507
2508            for (idx, line) in lines.iter().enumerate() {
2509                let trimmed = line.trim();
2510
2511                // Skip comments
2512                if trimmed.starts_with("//") {
2513                    continue;
2514                }
2515
2516                // Track .close() calls on receivers
2517                if trimmed.contains(".close()") {
2518                    has_close_call = true;
2519                    close_line = idx + 1;
2520                }
2521
2522                // If we've seen a close() and see send/recv in spawn context, flag it
2523                if has_close_call {
2524                    let in_spawn_context = lines[..idx]
2525                        .iter()
2526                        .rev()
2527                        .take(10)
2528                        .any(|l| l.contains("spawn") || l.contains("thread::"));
2529
2530                    if in_spawn_context {
2531                        if trimmed.contains(".send(") || trimmed.contains(".try_recv(") {
2532                            let location = format!("{}:{}", rel_path, idx + 1);
2533
2534                            findings.push(Finding {
2535                                rule_id: self.metadata.id.clone(),
2536                                rule_name: self.metadata.name.clone(),
2537                                severity: self.metadata.default_severity,
2538                                message: format!(
2539                                    "Oneshot channel operation after close() at line {}. \
2540                                    Concurrent close(), send(), and recv() operations can cause data races. \
2541                                    Ensure proper synchronization between channel halves.",
2542                                    close_line
2543                                ),
2544                                function: location,
2545                                function_signature: String::new(),
2546                                evidence: vec![trimmed.to_string()],
2547                                span: None,
2548                    ..Default::default()
2549                            });
2550                        }
2551                    }
2552                }
2553            }
2554        }
2555
2556        findings
2557    }
2558}
2559
2560// ============================================================================
2561// RUSTCOLA109: Async-Signal-Unsafe in Handler Rule
2562// ============================================================================
2563
2564/// Detects async-signal-unsafe operations inside signal handlers.
2565///
2566/// Signal handlers have strict requirements about what operations are safe.
2567/// Many common operations (heap allocation, I/O, locking) are NOT safe in
2568/// signal handlers and can cause deadlocks or corruption.
2569pub struct AsyncSignalUnsafeInHandlerRule {
2570    metadata: RuleMetadata,
2571}
2572
2573impl AsyncSignalUnsafeInHandlerRule {
2574    pub fn new() -> Self {
2575        Self {
2576            metadata: RuleMetadata {
2577                id: "RUSTCOLA109".to_string(),
2578                name: "async-signal-unsafe-in-handler".to_string(),
2579                short_description: "Async-signal-unsafe operation in signal handler".to_string(),
2580                full_description: "Detects async-signal-unsafe operations inside signal handlers. \
2581                    Signal handlers run asynchronously and interrupt normal execution, so only \
2582                    'async-signal-safe' functions may be called. Unsafe operations include: \
2583                    heap allocation (Box, Vec, String, format!), I/O (println!, eprintln!), \
2584                    locking (Mutex, RwLock), and most standard library functions."
2585                    .to_string(),
2586                help_uri: Some(
2587                    "https://man7.org/linux/man-pages/man7/signal-safety.7.html".to_string(),
2588                ),
2589                default_severity: Severity::High,
2590                origin: RuleOrigin::BuiltIn,
2591                cwe_ids: Vec::new(),
2592                fix_suggestion: None,
2593                exploitability: Exploitability::default(),
2594            },
2595        }
2596    }
2597
2598    /// Operations that are NOT async-signal-safe
2599    fn unsafe_operations() -> &'static [(&'static str, &'static str)] {
2600        &[
2601            ("println!", "I/O operations are not async-signal-safe"),
2602            ("eprintln!", "I/O operations are not async-signal-safe"),
2603            ("print!", "I/O operations are not async-signal-safe"),
2604            ("eprint!", "I/O operations are not async-signal-safe"),
2605            ("format!", "format! allocates memory, not async-signal-safe"),
2606            ("vec!", "vec! allocates memory, not async-signal-safe"),
2607            ("Box::new", "Box allocates memory, not async-signal-safe"),
2608            ("Vec::new", "Vec may allocate, not async-signal-safe"),
2609            ("String::new", "String may allocate, not async-signal-safe"),
2610            (".to_string()", "to_string allocates, not async-signal-safe"),
2611            (".to_owned()", "to_owned allocates, not async-signal-safe"),
2612            ("Mutex::new", "Creating locks in handlers is dangerous"),
2613            (".lock()", "Locking is not async-signal-safe"),
2614            (".read()", "RwLock read is not async-signal-safe"),
2615            (".write()", "RwLock write is not async-signal-safe"),
2616            ("std::io::", "Most std::io is not async-signal-safe"),
2617            ("std::fs::", "File operations are not async-signal-safe"),
2618        ]
2619    }
2620}
2621
2622impl Rule for AsyncSignalUnsafeInHandlerRule {
2623    fn metadata(&self) -> &RuleMetadata {
2624        &self.metadata
2625    }
2626
2627    fn evaluate(
2628        &self,
2629        package: &MirPackage,
2630        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2631    ) -> Vec<Finding> {
2632        if package.crate_name == "mir-extractor" {
2633            return Vec::new();
2634        }
2635
2636        let mut findings = Vec::new();
2637        let crate_root = Path::new(&package.crate_root);
2638
2639        if !crate_root.exists() {
2640            return findings;
2641        }
2642
2643        for entry in WalkDir::new(crate_root)
2644            .into_iter()
2645            .filter_entry(|e| filter_entry(e))
2646        {
2647            let entry = match entry {
2648                Ok(e) => e,
2649                Err(_) => continue,
2650            };
2651
2652            if !entry.file_type().is_file() {
2653                continue;
2654            }
2655
2656            let path = entry.path();
2657            if path.extension() != Some(OsStr::new("rs")) {
2658                continue;
2659            }
2660
2661            let rel_path = path
2662                .strip_prefix(crate_root)
2663                .unwrap_or(path)
2664                .to_string_lossy()
2665                .replace('\\', "/");
2666
2667            let content = match fs::read_to_string(path) {
2668                Ok(c) => c,
2669                Err(_) => continue,
2670            };
2671
2672            // Check if file uses signal handling
2673            if !content.contains("signal")
2674                && !content.contains("Signal")
2675                && !content.contains("ctrlc")
2676                && !content.contains("SIGINT")
2677                && !content.contains("SIGTERM")
2678                && !content.contains("SIGHUP")
2679            {
2680                continue;
2681            }
2682
2683            let lines: Vec<&str> = content.lines().collect();
2684            let mut in_signal_handler = false;
2685            let mut handler_start_line = 0;
2686            let mut brace_depth = 0;
2687
2688            for (idx, line) in lines.iter().enumerate() {
2689                let trimmed = line.trim();
2690
2691                // Skip comments
2692                if trimmed.starts_with("//") {
2693                    continue;
2694                }
2695
2696                // Detect signal handler registration patterns
2697                let is_handler_start = trimmed.contains("signal::")
2698                    || trimmed.contains("ctrlc::set_handler")
2699                    || trimmed.contains("set_handler")
2700                    || (trimmed.contains("signal") && trimmed.contains("move ||"))
2701                    || (trimmed.contains("signal") && trimmed.contains("move |"))
2702                    || trimmed.contains("SigAction::new");
2703
2704                if is_handler_start && !in_signal_handler {
2705                    in_signal_handler = true;
2706                    handler_start_line = idx;
2707                    brace_depth = 0;
2708                }
2709
2710                if in_signal_handler {
2711                    brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
2712                    brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
2713
2714                    // Check for unsafe operations within handler
2715                    for (pattern, reason) in Self::unsafe_operations() {
2716                        if trimmed.contains(pattern) {
2717                            let location = format!("{}:{}", rel_path, idx + 1);
2718
2719                            findings.push(Finding {
2720                                rule_id: self.metadata.id.clone(),
2721                                rule_name: self.metadata.name.clone(),
2722                                severity: self.metadata.default_severity,
2723                                message: format!(
2724                                    "Async-signal-unsafe operation `{}` in signal handler (started at line {}). {}",
2725                                    pattern, handler_start_line + 1, reason
2726                                ),
2727                                function: location,
2728                                function_signature: String::new(),
2729                                evidence: vec![trimmed.to_string()],
2730                                span: None,
2731                    ..Default::default()
2732                            });
2733                        }
2734                    }
2735
2736                    // End of handler closure
2737                    if brace_depth <= 0 && idx > handler_start_line {
2738                        in_signal_handler = false;
2739                    }
2740                }
2741            }
2742        }
2743
2744        findings
2745    }
2746}
2747
2748// ============================================================================
2749// Registration
2750// ============================================================================
2751// RUSTCOLA100: OnceCell TOCTOU Race Rule
2752// ============================================================================
2753
2754/// Detects potential TOCTOU (Time-of-check to time-of-use) races with OnceCell.
2755///
2756/// Pattern: Checking OnceCell::get() then calling get_or_init() based on result
2757/// creates a race window where another thread may initialize between check and use.
2758pub struct OnceCellTocTouRule {
2759    metadata: RuleMetadata,
2760}
2761
2762impl OnceCellTocTouRule {
2763    pub fn new() -> Self {
2764        Self {
2765            metadata: RuleMetadata {
2766                id: "RUSTCOLA100".to_string(),
2767                name: "oncecell-toctou-race".to_string(),
2768                short_description: "Potential TOCTOU race with OnceCell".to_string(),
2769                full_description: "Detects patterns where OnceCell::get() is checked before \
2770                    calling get_or_init(). This creates a TOCTOU race: another thread may \
2771                    initialize the cell between the check and the use. Use get_or_init() \
2772                    directly without pre-checking, or use get_or_try_init() for fallible init."
2773                    .to_string(),
2774                help_uri: Some(
2775                    "https://doc.rust-lang.org/std/cell/struct.OnceCell.html".to_string(),
2776                ),
2777                default_severity: Severity::Medium,
2778                origin: RuleOrigin::BuiltIn,
2779                cwe_ids: Vec::new(),
2780                fix_suggestion: None,
2781                exploitability: Exploitability::default(),
2782            },
2783        }
2784    }
2785
2786    /// Patterns that indicate TOCTOU with OnceCell/OnceLock
2787    fn toctou_patterns() -> &'static [(&'static str, &'static str)] {
2788        &[
2789            (
2790                ".get().is_none()",
2791                "checking get().is_none() then initializing",
2792            ),
2793            (".get().is_some()", "checking get().is_some() before using"),
2794            ("if let None = ", "pattern matching None before get_or_init"),
2795            ("if cell.get() == None", "comparing get() to None"),
2796            ("match .get() {", "matching on get() result"),
2797        ]
2798    }
2799}
2800
2801impl Rule for OnceCellTocTouRule {
2802    fn metadata(&self) -> &RuleMetadata {
2803        &self.metadata
2804    }
2805
2806    fn evaluate(
2807        &self,
2808        package: &MirPackage,
2809        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2810    ) -> Vec<Finding> {
2811        if package.crate_name == "mir-extractor" {
2812            return Vec::new();
2813        }
2814
2815        let mut findings = Vec::new();
2816        let crate_root = Path::new(&package.crate_root);
2817
2818        if !crate_root.exists() {
2819            return findings;
2820        }
2821
2822        for entry in WalkDir::new(crate_root)
2823            .into_iter()
2824            .filter_entry(|e| filter_entry(e))
2825        {
2826            let entry = match entry {
2827                Ok(e) => e,
2828                Err(_) => continue,
2829            };
2830
2831            if !entry.file_type().is_file() {
2832                continue;
2833            }
2834
2835            let path = entry.path();
2836            if path.extension() != Some(OsStr::new("rs")) {
2837                continue;
2838            }
2839
2840            let rel_path = path
2841                .strip_prefix(crate_root)
2842                .unwrap_or(path)
2843                .to_string_lossy()
2844                .replace('\\', "/");
2845
2846            let content = match fs::read_to_string(path) {
2847                Ok(c) => c,
2848                Err(_) => continue,
2849            };
2850
2851            // Quick check: does file use OnceCell or OnceLock?
2852            if !content.contains("OnceCell")
2853                && !content.contains("OnceLock")
2854                && !content.contains("once_cell")
2855            {
2856                continue;
2857            }
2858
2859            let lines: Vec<&str> = content.lines().collect();
2860
2861            for (idx, line) in lines.iter().enumerate() {
2862                let trimmed = line.trim();
2863
2864                // Skip comments
2865                if trimmed.starts_with("//") {
2866                    continue;
2867                }
2868
2869                // Check for TOCTOU patterns
2870                for (pattern, description) in Self::toctou_patterns() {
2871                    if trimmed.contains(pattern) {
2872                        // Look ahead for get_or_init within 5 lines
2873                        let has_init_nearby = lines[idx..]
2874                            .iter()
2875                            .take(5)
2876                            .any(|l| l.contains("get_or_init") || l.contains("set("));
2877
2878                        if has_init_nearby {
2879                            let location = format!("{}:{}", rel_path, idx + 1);
2880
2881                            findings.push(Finding {
2882                                rule_id: self.metadata.id.clone(),
2883                                rule_name: self.metadata.name.clone(),
2884                                severity: self.metadata.default_severity,
2885                                message: format!(
2886                                    "Potential TOCTOU race: {} followed by initialization. \
2887                                    Another thread may initialize between check and use. \
2888                                    Use get_or_init() directly without pre-checking.",
2889                                    description
2890                                ),
2891                                function: location,
2892                                function_signature: String::new(),
2893                                evidence: vec![trimmed.to_string()],
2894                                span: None,
2895                                ..Default::default()
2896                            });
2897                        }
2898                    }
2899                }
2900            }
2901        }
2902
2903        findings
2904    }
2905}
2906
2907// ============================================================================
2908// RUSTCOLA117: Panic While Holding Lock Rule
2909// ============================================================================
2910
2911/// Detects panic-prone operations while holding a MutexGuard or RwLockGuard.
2912///
2913/// Panicking while holding a lock poisons the mutex, making it permanently
2914/// unusable for other threads. This can cause cascading failures.
2915pub struct PanicWhileHoldingLockRule {
2916    metadata: RuleMetadata,
2917}
2918
2919impl PanicWhileHoldingLockRule {
2920    pub fn new() -> Self {
2921        Self {
2922            metadata: RuleMetadata {
2923                id: "RUSTCOLA117".to_string(),
2924                name: "panic-while-holding-lock".to_string(),
2925                short_description: "Potential panic while holding lock".to_string(),
2926                full_description: "Detects panic-prone operations (unwrap, expect, assert, \
2927                    indexing) while a MutexGuard or RwLockGuard is held. Panicking while \
2928                    holding a lock poisons the mutex, making it unusable for other threads \
2929                    and causing cascading failures."
2930                    .to_string(),
2931                help_uri: Some(
2932                    "https://doc.rust-lang.org/std/sync/struct.Mutex.html#poisoning".to_string(),
2933                ),
2934                default_severity: Severity::Medium,
2935                origin: RuleOrigin::BuiltIn,
2936                cwe_ids: Vec::new(),
2937                fix_suggestion: None,
2938                exploitability: Exploitability::default(),
2939            },
2940        }
2941    }
2942
2943    /// Patterns that can cause panics
2944    fn panic_patterns() -> &'static [&'static str] {
2945        &[
2946            ".unwrap()",
2947            ".expect(",
2948            "panic!",
2949            "assert!",
2950            "assert_eq!",
2951            "assert_ne!",
2952            "unreachable!",
2953            "unimplemented!",
2954            "todo!",
2955        ]
2956    }
2957
2958    /// Patterns that acquire locks
2959    fn lock_patterns() -> &'static [&'static str] {
2960        &[
2961            ".lock()",
2962            ".read()",
2963            ".write()",
2964            ".try_lock()",
2965            ".try_read()",
2966            ".try_write()",
2967        ]
2968    }
2969}
2970
2971impl Rule for PanicWhileHoldingLockRule {
2972    fn metadata(&self) -> &RuleMetadata {
2973        &self.metadata
2974    }
2975
2976    fn evaluate(
2977        &self,
2978        package: &MirPackage,
2979        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
2980    ) -> Vec<Finding> {
2981        if package.crate_name == "mir-extractor" {
2982            return Vec::new();
2983        }
2984
2985        let mut findings = Vec::new();
2986        let crate_root = Path::new(&package.crate_root);
2987
2988        if !crate_root.exists() {
2989            return findings;
2990        }
2991
2992        for entry in WalkDir::new(crate_root)
2993            .into_iter()
2994            .filter_entry(|e| filter_entry(e))
2995        {
2996            let entry = match entry {
2997                Ok(e) => e,
2998                Err(_) => continue,
2999            };
3000
3001            if !entry.file_type().is_file() {
3002                continue;
3003            }
3004
3005            let path = entry.path();
3006            if path.extension() != Some(OsStr::new("rs")) {
3007                continue;
3008            }
3009
3010            let rel_path = path
3011                .strip_prefix(crate_root)
3012                .unwrap_or(path)
3013                .to_string_lossy()
3014                .replace('\\', "/");
3015
3016            let content = match fs::read_to_string(path) {
3017                Ok(c) => c,
3018                Err(_) => continue,
3019            };
3020
3021            // Quick check: does file use Mutex or RwLock?
3022            if !content.contains("Mutex") && !content.contains("RwLock") {
3023                continue;
3024            }
3025
3026            let lines: Vec<&str> = content.lines().collect();
3027            let mut in_lock_scope = false;
3028            let mut lock_start_line = 0;
3029            let mut brace_depth_at_lock = 0;
3030            let mut current_brace_depth = 0;
3031
3032            for (idx, line) in lines.iter().enumerate() {
3033                let trimmed = line.trim();
3034
3035                // Skip comments
3036                if trimmed.starts_with("//") {
3037                    continue;
3038                }
3039
3040                // Track brace depth
3041                current_brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
3042                current_brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
3043
3044                // Detect lock acquisition
3045                for pattern in Self::lock_patterns() {
3046                    if trimmed.contains(pattern) && trimmed.contains("let ") {
3047                        in_lock_scope = true;
3048                        lock_start_line = idx;
3049                        brace_depth_at_lock = current_brace_depth;
3050                    }
3051                }
3052
3053                // Check for panic patterns while lock is held
3054                if in_lock_scope {
3055                    for pattern in Self::panic_patterns() {
3056                        if trimmed.contains(pattern) {
3057                            let location = format!("{}:{}", rel_path, idx + 1);
3058
3059                            findings.push(Finding {
3060                                rule_id: self.metadata.id.clone(),
3061                                rule_name: self.metadata.name.clone(),
3062                                severity: self.metadata.default_severity,
3063                                message: format!(
3064                                    "Panic-prone operation `{}` while holding lock (acquired at line {}). \
3065                                    Panicking will poison the mutex, making it unusable. \
3066                                    Consider using fallible alternatives or dropping the guard first.",
3067                                    pattern.trim_end_matches('('), lock_start_line + 1
3068                                ),
3069                                function: location,
3070                                function_signature: String::new(),
3071                                evidence: vec![trimmed.to_string()],
3072                                span: None,
3073                    ..Default::default()
3074                            });
3075                        }
3076                    }
3077
3078                    // End of lock scope (simplified: when we return to or below the lock depth)
3079                    if current_brace_depth < brace_depth_at_lock {
3080                        in_lock_scope = false;
3081                    }
3082                }
3083            }
3084        }
3085
3086        findings
3087    }
3088}
3089
3090// ============================================================================
3091// RUSTCOLA119: Closure Escaping References Rule
3092// ============================================================================
3093
3094/// Detects closures that capture references and may escape their scope,
3095/// particularly when passed to spawn/thread functions or stored in
3096/// longer-lived contexts.
3097pub struct ClosureEscapingRefsRule {
3098    metadata: RuleMetadata,
3099}
3100
3101impl ClosureEscapingRefsRule {
3102    pub fn new() -> Self {
3103        Self {
3104            metadata: RuleMetadata {
3105                id: "RUSTCOLA119".to_string(),
3106                name: "closure-escaping-refs".to_string(),
3107                short_description: "Closure may capture escaping references".to_string(),
3108                full_description: "Detects closures passed to spawn/thread functions that \
3109                    capture local references. These closures outlive the captured references, \
3110                    leading to use-after-free. Use move closures or Arc for shared ownership."
3111                    .to_string(),
3112                help_uri: None,
3113                default_severity: Severity::High,
3114                origin: RuleOrigin::BuiltIn,
3115                cwe_ids: Vec::new(),
3116                fix_suggestion: None,
3117                exploitability: Exploitability::default(),
3118            },
3119        }
3120    }
3121
3122    /// Functions that take closures that outlive the current scope
3123    fn escaping_closure_receivers() -> &'static [&'static str] {
3124        &[
3125            "thread::spawn",
3126            "spawn(",
3127            "spawn_blocking(",
3128            "spawn_local(",
3129            "task::spawn",
3130            "tokio::spawn",
3131            "async_std::spawn",
3132            "rayon::spawn",
3133            "thread::Builder",
3134            ".spawn(",
3135            "std::thread::spawn",
3136        ]
3137    }
3138
3139    /// Patterns indicating a closure captures a reference
3140    fn ref_capture_patterns() -> &'static [&'static str] {
3141        &[
3142            // Non-move closure with explicit ref capture
3143            "|&",
3144            "| &",
3145            // Patterns that suggest borrowing in closure body
3146            ".as_ref()",
3147            ".borrow()",
3148        ]
3149    }
3150}
3151
3152impl Rule for ClosureEscapingRefsRule {
3153    fn metadata(&self) -> &RuleMetadata {
3154        &self.metadata
3155    }
3156
3157    fn evaluate(
3158        &self,
3159        package: &MirPackage,
3160        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3161    ) -> Vec<Finding> {
3162        let mut findings = Vec::new();
3163        let crate_root = Path::new(&package.crate_root);
3164
3165        if !crate_root.exists() {
3166            return findings;
3167        }
3168
3169        for entry in WalkDir::new(crate_root)
3170            .into_iter()
3171            .filter_entry(|e| filter_entry(e))
3172        {
3173            let entry = match entry {
3174                Ok(e) => e,
3175                Err(_) => continue,
3176            };
3177
3178            if !entry.file_type().is_file() {
3179                continue;
3180            }
3181
3182            let path = entry.path();
3183            if path.extension() != Some(OsStr::new("rs")) {
3184                continue;
3185            }
3186
3187            let rel_path = path
3188                .strip_prefix(crate_root)
3189                .unwrap_or(path)
3190                .to_string_lossy()
3191                .replace('\\', "/");
3192
3193            let content = match fs::read_to_string(path) {
3194                Ok(c) => c,
3195                Err(_) => continue,
3196            };
3197
3198            // Quick check: does file use spawn-like functions?
3199            let has_spawn = Self::escaping_closure_receivers()
3200                .iter()
3201                .any(|p| content.contains(p));
3202            if !has_spawn {
3203                continue;
3204            }
3205
3206            let lines: Vec<&str> = content.lines().collect();
3207
3208            for (idx, line) in lines.iter().enumerate() {
3209                let trimmed = line.trim();
3210
3211                // Skip comments
3212                if trimmed.starts_with("//") {
3213                    continue;
3214                }
3215
3216                // Check for spawn-like function calls
3217                for spawn_fn in Self::escaping_closure_receivers() {
3218                    if trimmed.contains(spawn_fn) {
3219                        // Check if it's NOT a move closure (which would be safe)
3220                        let has_closure = trimmed.contains('|');
3221                        let is_move_closure =
3222                            trimmed.contains("move |") || trimmed.contains("move|");
3223
3224                        if has_closure && !is_move_closure {
3225                            // Non-move closure passed to spawn - potential issue
3226                            let location = format!("{}:{}", rel_path, idx + 1);
3227
3228                            findings.push(Finding {
3229                                rule_id: self.metadata.id.clone(),
3230                                rule_name: self.metadata.name.clone(),
3231                                severity: Severity::Medium,
3232                                message: format!(
3233                                    "Non-move closure passed to '{}'. Closures passed to spawn \
3234                                    functions must use 'move' to take ownership of captured \
3235                                    variables, otherwise captured references may dangle. \
3236                                    Add 'move' keyword: `move |...| {{ ... }}`",
3237                                    spawn_fn.trim_end_matches('(')
3238                                ),
3239                                function: location,
3240                                function_signature: String::new(),
3241                                evidence: vec![trimmed.to_string()],
3242                                span: None,
3243                                ..Default::default()
3244                            });
3245                        }
3246
3247                        // Also check for reference capture patterns even in move closures
3248                        // (which can still be problematic with explicit &var)
3249                        if has_closure {
3250                            for ref_pattern in Self::ref_capture_patterns() {
3251                                if trimmed.contains(ref_pattern) {
3252                                    let location = format!("{}:{}", rel_path, idx + 1);
3253
3254                                    findings.push(Finding {
3255                                        rule_id: self.metadata.id.clone(),
3256                                        rule_name: self.metadata.name.clone(),
3257                                        severity: Severity::High,
3258                                        message: format!(
3259                                            "Closure passed to spawn may capture reference (pattern: '{}'). \
3260                                            References captured in spawn closures will dangle when the \
3261                                            spawning function returns. Use Arc or clone the data instead.",
3262                                            ref_pattern
3263                                        ),
3264                                        function: location,
3265                                        function_signature: String::new(),
3266                                        evidence: vec![trimmed.to_string()],
3267                                        span: None,
3268                    ..Default::default()
3269                                    });
3270                                    break;
3271                                }
3272                            }
3273                        }
3274                    }
3275                }
3276            }
3277        }
3278
3279        findings
3280    }
3281}
3282
3283// ============================================================================
3284// RUSTCOLA121: Executor Starvation Rule
3285// ============================================================================
3286
3287/// Detects CPU-bound operations in async functions that may starve the executor.
3288///
3289/// Long-running synchronous operations in async code block the executor thread,
3290/// preventing other tasks from making progress. Use spawn_blocking or yield points.
3291pub struct ExecutorStarvationRule {
3292    metadata: RuleMetadata,
3293}
3294
3295impl ExecutorStarvationRule {
3296    pub fn new() -> Self {
3297        Self {
3298            metadata: RuleMetadata {
3299                id: "RUSTCOLA121".to_string(),
3300                name: "executor-starvation".to_string(),
3301                short_description: "CPU-bound work in async context".to_string(),
3302                full_description: "Detects CPU-bound operations (tight loops, heavy computation) \
3303                    in async functions that may starve the executor. Long-running synchronous \
3304                    work blocks the executor thread. Use tokio::task::spawn_blocking or \
3305                    add yield points with tokio::task::yield_now()."
3306                    .to_string(),
3307                help_uri: Some("https://tokio.rs/tokio/topics/bridging".to_string()),
3308                default_severity: Severity::Medium,
3309                origin: RuleOrigin::BuiltIn,
3310                cwe_ids: Vec::new(),
3311                fix_suggestion: None,
3312                exploitability: Exploitability::default(),
3313            },
3314        }
3315    }
3316
3317    /// Patterns indicating CPU-bound work
3318    fn cpu_bound_patterns() -> &'static [(&'static str, &'static str)] {
3319        &[
3320            // Tight loops without yield
3321            ("loop {", "CPU-bound loop without yield_now() or await"),
3322            ("while true", "Infinite loop without yield"),
3323            ("for _ in 0..", "Large iteration without yield"),
3324            // Heavy computation patterns
3325            (".par_iter()", "Rayon parallel iteration blocks executor"),
3326            // Cryptographic operations
3327            ("hash(", "Cryptographic hashing is CPU-bound"),
3328            ("encrypt(", "Encryption is CPU-bound"),
3329            ("decrypt(", "Decryption is CPU-bound"),
3330            // Compression
3331            ("compress(", "Compression is CPU-bound"),
3332            ("decompress(", "Decompression is CPU-bound"),
3333            ("GzEncoder", "Gzip encoding is CPU-bound"),
3334            ("GzDecoder", "Gzip decoding is CPU-bound"),
3335        ]
3336    }
3337}
3338
3339impl Rule for ExecutorStarvationRule {
3340    fn metadata(&self) -> &RuleMetadata {
3341        &self.metadata
3342    }
3343
3344    fn evaluate(
3345        &self,
3346        package: &MirPackage,
3347        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3348    ) -> Vec<Finding> {
3349        let mut findings = Vec::new();
3350        let crate_root = Path::new(&package.crate_root);
3351
3352        if !crate_root.exists() {
3353            return findings;
3354        }
3355
3356        for entry in WalkDir::new(crate_root)
3357            .into_iter()
3358            .filter_entry(|e| filter_entry(e))
3359        {
3360            let entry = match entry {
3361                Ok(e) => e,
3362                Err(_) => continue,
3363            };
3364
3365            if !entry.file_type().is_file() {
3366                continue;
3367            }
3368
3369            let path = entry.path();
3370            if path.extension() != Some(OsStr::new("rs")) {
3371                continue;
3372            }
3373
3374            let rel_path = path
3375                .strip_prefix(crate_root)
3376                .unwrap_or(path)
3377                .to_string_lossy()
3378                .replace('\\', "/");
3379
3380            let content = match fs::read_to_string(path) {
3381                Ok(c) => c,
3382                Err(_) => continue,
3383            };
3384
3385            // Quick check: does file have async functions?
3386            if !content.contains("async fn") && !content.contains("async move") {
3387                continue;
3388            }
3389
3390            let lines: Vec<&str> = content.lines().collect();
3391            let mut in_async_fn = false;
3392            let mut async_fn_name = String::new();
3393            let mut brace_depth = 0;
3394            let mut async_start_depth = 0;
3395
3396            for (idx, line) in lines.iter().enumerate() {
3397                let trimmed = line.trim();
3398
3399                // Skip comments
3400                if trimmed.starts_with("//") {
3401                    continue;
3402                }
3403
3404                // Track async function entry
3405                if (trimmed.contains("async fn ") || trimmed.contains("async move")) && !in_async_fn
3406                {
3407                    in_async_fn = true;
3408                    async_start_depth = brace_depth;
3409                    // Extract function name
3410                    if let Some(fn_pos) = trimmed.find("fn ") {
3411                        let after_fn = &trimmed[fn_pos + 3..];
3412                        async_fn_name = after_fn
3413                            .split(|c| c == '(' || c == '<')
3414                            .next()
3415                            .unwrap_or("")
3416                            .trim()
3417                            .to_string();
3418                    }
3419                }
3420
3421                // Track brace depth
3422                brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
3423                brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
3424
3425                // Check if we've exited the async function
3426                if in_async_fn && brace_depth <= async_start_depth && trimmed.contains('}') {
3427                    in_async_fn = false;
3428                    async_fn_name.clear();
3429                }
3430
3431                // Check for CPU-bound patterns in async context
3432                if in_async_fn {
3433                    // Skip if there's already a spawn_blocking nearby
3434                    if trimmed.contains("spawn_blocking") || trimmed.contains("block_in_place") {
3435                        continue;
3436                    }
3437
3438                    for (pattern, description) in Self::cpu_bound_patterns() {
3439                        if trimmed.contains(pattern) {
3440                            // Check if there's an await or yield nearby (within 5 lines)
3441                            let start_idx = idx.saturating_sub(5);
3442                            let end_idx = (idx + 5).min(lines.len() - 1);
3443                            let has_nearby_yield = lines[start_idx..=end_idx]
3444                                .iter()
3445                                .any(|l| l.contains(".await") || l.contains("yield_now"));
3446
3447                            if !has_nearby_yield {
3448                                let location = format!("{}:{}", rel_path, idx + 1);
3449
3450                                findings.push(Finding {
3451                                    rule_id: self.metadata.id.clone(),
3452                                    rule_name: self.metadata.name.clone(),
3453                                    severity: self.metadata.default_severity,
3454                                    message: format!(
3455                                        "Potential executor starvation in async function '{}': {}. \
3456                                        Consider using tokio::task::spawn_blocking() for CPU-bound work \
3457                                        or adding yield points with tokio::task::yield_now().await",
3458                                        async_fn_name, description
3459                                    ),
3460                                    function: location,
3461                                    function_signature: String::new(),
3462                                    evidence: vec![trimmed.to_string()],
3463                                    span: None,
3464                    ..Default::default()
3465                                });
3466                            }
3467                        }
3468                    }
3469                }
3470            }
3471        }
3472
3473        findings
3474    }
3475}
3476
3477// ============================================================================
3478// RUSTCOLA122: Async Drop Correctness Rule
3479// ============================================================================
3480
3481/// Detects async resources that may be dropped without proper cleanup.
3482/// Common patterns: TcpStream, File, database connections in async context
3483/// without explicit close/shutdown.
3484pub struct AsyncDropCorrectnessRule {
3485    metadata: RuleMetadata,
3486}
3487
3488impl AsyncDropCorrectnessRule {
3489    pub fn new() -> Self {
3490        Self {
3491            metadata: RuleMetadata {
3492                id: "RUSTCOLA122".to_string(),
3493                name: "async-drop-correctness".to_string(),
3494                short_description: "Async resource may be dropped without cleanup".to_string(),
3495                full_description: "Detects async resources (network connections, file handles, \
3496                    database connections) that may be dropped without explicit cleanup. In async \
3497                    code, Drop::drop() is synchronous and cannot await cleanup operations. Use \
3498                    explicit .shutdown(), .close(), or .flush() before dropping async resources."
3499                    .to_string(),
3500                help_uri: None,
3501                default_severity: Severity::Medium,
3502                origin: RuleOrigin::BuiltIn,
3503                cwe_ids: Vec::new(),
3504                fix_suggestion: None,
3505                exploitability: Exploitability::default(),
3506            },
3507        }
3508    }
3509
3510    /// Async resource types that need explicit cleanup
3511    fn async_resource_types() -> &'static [(&'static str, &'static str)] {
3512        &[
3513            ("TcpStream", "Use .shutdown() before dropping"),
3514            ("TcpListener", "Use .shutdown() or explicit close"),
3515            ("UnixStream", "Use .shutdown() before dropping"),
3516            (
3517                "File",
3518                "Use .flush().await and .sync_all().await before dropping",
3519            ),
3520            ("BufWriter", "Use .flush().await before dropping"),
3521            ("BufReader", "Ensure underlying reader is properly closed"),
3522            ("Pool", "Use .close().await for connection pools"),
3523            (
3524                "Client",
3525                "Use .close() or explicit shutdown for HTTP clients",
3526            ),
3527            ("Connection", "Close database connections explicitly"),
3528            ("Sender", "Drop sender explicitly or use .closed().await"),
3529            ("WebSocket", "Use .close().await before dropping"),
3530        ]
3531    }
3532
3533    /// Cleanup methods that indicate proper resource handling
3534    fn cleanup_methods() -> &'static [&'static str] {
3535        &[
3536            ".shutdown()",
3537            ".close()",
3538            ".flush()",
3539            ".sync_all()",
3540            ".sync_data()",
3541            "drop(",
3542            "std::mem::drop(",
3543            ".abort()",
3544            ".cancel()",
3545        ]
3546    }
3547}
3548
3549impl Rule for AsyncDropCorrectnessRule {
3550    fn metadata(&self) -> &RuleMetadata {
3551        &self.metadata
3552    }
3553
3554    fn evaluate(
3555        &self,
3556        package: &MirPackage,
3557        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3558    ) -> Vec<Finding> {
3559        if package.crate_name == "mir-extractor" {
3560            return Vec::new();
3561        }
3562
3563        let mut findings = Vec::new();
3564        let crate_root = Path::new(&package.crate_root);
3565
3566        if !crate_root.exists() {
3567            return findings;
3568        }
3569
3570        for entry in WalkDir::new(crate_root)
3571            .into_iter()
3572            .filter_entry(|e| filter_entry(e))
3573        {
3574            let entry = match entry {
3575                Ok(e) => e,
3576                Err(_) => continue,
3577            };
3578
3579            if !entry.file_type().is_file() {
3580                continue;
3581            }
3582
3583            let path = entry.path();
3584            if path.extension() != Some(std::ffi::OsStr::new("rs")) {
3585                continue;
3586            }
3587
3588            let rel_path = path
3589                .strip_prefix(crate_root)
3590                .unwrap_or(path)
3591                .to_string_lossy()
3592                .replace('\\', "/");
3593
3594            let content = match std::fs::read_to_string(path) {
3595                Ok(c) => c,
3596                Err(_) => continue,
3597            };
3598
3599            let lines: Vec<&str> = content.lines().collect();
3600            let mut in_async_fn = false;
3601            let mut async_fn_name = String::new();
3602            let mut resource_declarations: Vec<(usize, String, String)> = Vec::new(); // (line, var_name, type)
3603
3604            for (idx, line) in lines.iter().enumerate() {
3605                let trimmed = line.trim();
3606
3607                // Skip comments
3608                if trimmed.starts_with("//") {
3609                    continue;
3610                }
3611
3612                // Track async function entry
3613                if trimmed.contains("async fn ") {
3614                    in_async_fn = true;
3615                    if let Some(fn_pos) = trimmed.find("fn ") {
3616                        let after_fn = &trimmed[fn_pos + 3..];
3617                        async_fn_name = after_fn
3618                            .split(|c| c == '(' || c == '<')
3619                            .next()
3620                            .unwrap_or("")
3621                            .trim()
3622                            .to_string();
3623                    }
3624                    resource_declarations.clear();
3625                }
3626
3627                if !in_async_fn {
3628                    continue;
3629                }
3630
3631                // Check for async resource declarations
3632                for (resource_type, advice) in Self::async_resource_types() {
3633                    if trimmed.contains(resource_type)
3634                        && (trimmed.contains("let ") || trimmed.contains("mut "))
3635                        && (trimmed.contains(".await")
3636                            || trimmed.contains("::new")
3637                            || trimmed.contains("::connect"))
3638                    {
3639                        // Extract variable name
3640                        if let Some(let_pos) = trimmed.find("let ") {
3641                            let after_let = &trimmed[let_pos + 4..];
3642                            let var_name: String = after_let
3643                                .split(|c: char| c == ':' || c == '=' || c.is_whitespace())
3644                                .next()
3645                                .unwrap_or("")
3646                                .replace("mut", "")
3647                                .trim()
3648                                .to_string();
3649                            if !var_name.is_empty() {
3650                                resource_declarations.push((idx, var_name, (*advice).to_string()));
3651                            }
3652                        }
3653                    }
3654                }
3655
3656                // Check if resources are properly cleaned up
3657                for cleanup in Self::cleanup_methods() {
3658                    if trimmed.contains(cleanup) {
3659                        // Remove declarations that are cleaned up
3660                        resource_declarations.retain(|(_, var, _)| !trimmed.contains(var.as_str()));
3661                    }
3662                }
3663
3664                // Function end - check for uncleaned resources
3665                if trimmed == "}" && in_async_fn {
3666                    // Check remaining uncleaned resources
3667                    for (decl_line, var_name, advice) in &resource_declarations {
3668                        let location = format!("{}:{}", rel_path, decl_line + 1);
3669                        findings.push(Finding {
3670                            rule_id: self.metadata.id.clone(),
3671                            rule_name: self.metadata.name.clone(),
3672                            severity: self.metadata.default_severity,
3673                            message: format!(
3674                                "Async resource '{}' in function '{}' may be dropped without cleanup. {}",
3675                                var_name, async_fn_name, advice
3676                            ),
3677                            function: location,
3678                            function_signature: async_fn_name.clone(),
3679                            evidence: vec![lines.get(*decl_line).unwrap_or(&"").to_string()],
3680                            span: None,
3681                    ..Default::default()
3682                        });
3683                    }
3684                    in_async_fn = false;
3685                    resource_declarations.clear();
3686                }
3687            }
3688        }
3689
3690        findings
3691    }
3692}
3693
3694// ============================================================================
3695// RUSTCOLA124: Panic in Drop Implementation Rule
3696// ============================================================================
3697
3698/// Detects panic-prone code in Drop implementations.
3699/// Panicking in Drop can cause double-panic and abort.
3700pub struct PanicInDropImplRule {
3701    metadata: RuleMetadata,
3702}
3703
3704impl PanicInDropImplRule {
3705    pub fn new() -> Self {
3706        Self {
3707            metadata: RuleMetadata {
3708                id: "RUSTCOLA124".to_string(),
3709                name: "panic-in-drop-impl".to_string(),
3710                short_description: "Panic-prone code in Drop implementation".to_string(),
3711                full_description: "Detects panic-prone operations (unwrap, expect, panic!, \
3712                    assert!, indexing) in Drop implementations. If Drop panics during stack \
3713                    unwinding from another panic, the process will abort. Use Option::take(), \
3714                    logging, or ignore errors in Drop."
3715                    .to_string(),
3716                help_uri: Some("https://doc.rust-lang.org/std/ops/trait.Drop.html".to_string()),
3717                default_severity: Severity::High,
3718                origin: RuleOrigin::BuiltIn,
3719                cwe_ids: Vec::new(),
3720                fix_suggestion: None,
3721                exploitability: Exploitability::default(),
3722            },
3723        }
3724    }
3725
3726    /// Panic-prone patterns to detect in Drop
3727    fn panic_patterns() -> &'static [(&'static str, &'static str)] {
3728        &[
3729            (
3730                ".unwrap()",
3731                "Use .ok(), .unwrap_or_default(), or log errors instead",
3732            ),
3733            (
3734                ".expect(",
3735                "Use .ok(), .unwrap_or_default(), or log errors instead",
3736            ),
3737            (
3738                "panic!(",
3739                "Never panic in Drop - log error or silently ignore",
3740            ),
3741            ("unreachable!(", "Replace with logging or silent handling"),
3742            (
3743                "unimplemented!(",
3744                "Implement proper cleanup or silently skip",
3745            ),
3746            ("todo!(", "Complete implementation before production use"),
3747            ("assert!(", "Use debug_assert! or remove assertion"),
3748            ("assert_eq!(", "Use debug_assert_eq! or remove assertion"),
3749            ("assert_ne!(", "Use debug_assert_ne! or remove assertion"),
3750            ("[", "Use .get() instead of direct indexing to avoid panic"),
3751        ]
3752    }
3753}
3754
3755impl Rule for PanicInDropImplRule {
3756    fn metadata(&self) -> &RuleMetadata {
3757        &self.metadata
3758    }
3759
3760    fn evaluate(
3761        &self,
3762        package: &MirPackage,
3763        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3764    ) -> Vec<Finding> {
3765        if package.crate_name == "mir-extractor" {
3766            return Vec::new();
3767        }
3768
3769        let mut findings = Vec::new();
3770        let crate_root = Path::new(&package.crate_root);
3771
3772        if !crate_root.exists() {
3773            return findings;
3774        }
3775
3776        for entry in WalkDir::new(crate_root)
3777            .into_iter()
3778            .filter_entry(|e| filter_entry(e))
3779        {
3780            let entry = match entry {
3781                Ok(e) => e,
3782                Err(_) => continue,
3783            };
3784
3785            if !entry.file_type().is_file() {
3786                continue;
3787            }
3788
3789            let path = entry.path();
3790            if path.extension() != Some(std::ffi::OsStr::new("rs")) {
3791                continue;
3792            }
3793
3794            let rel_path = path
3795                .strip_prefix(crate_root)
3796                .unwrap_or(path)
3797                .to_string_lossy()
3798                .replace('\\', "/");
3799
3800            let content = match std::fs::read_to_string(path) {
3801                Ok(c) => c,
3802                Err(_) => continue,
3803            };
3804
3805            let lines: Vec<&str> = content.lines().collect();
3806            let mut in_drop_impl = false;
3807            let mut drop_type_name = String::new();
3808            let mut brace_depth = 0;
3809            let mut drop_start_depth = 0;
3810
3811            for (idx, line) in lines.iter().enumerate() {
3812                let trimmed = line.trim();
3813
3814                // Skip comments
3815                if trimmed.starts_with("//") {
3816                    continue;
3817                }
3818
3819                // Detect impl Drop for Type
3820                if trimmed.contains("impl") && trimmed.contains("Drop") && trimmed.contains("for") {
3821                    in_drop_impl = true;
3822                    drop_start_depth = brace_depth;
3823                    // Extract type name
3824                    if let Some(for_pos) = trimmed.find("for ") {
3825                        let after_for = &trimmed[for_pos + 4..];
3826                        drop_type_name = after_for
3827                            .split(|c: char| c == '<' || c == '{' || c.is_whitespace())
3828                            .next()
3829                            .unwrap_or("")
3830                            .trim()
3831                            .to_string();
3832                    }
3833                }
3834
3835                // Track brace depth
3836                brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
3837                brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
3838
3839                // Check if we've exited the Drop impl
3840                if in_drop_impl && brace_depth <= drop_start_depth && trimmed.contains('}') {
3841                    in_drop_impl = false;
3842                    drop_type_name.clear();
3843                }
3844
3845                // Check for panic patterns in Drop
3846                if in_drop_impl {
3847                    for (pattern, advice) in Self::panic_patterns() {
3848                        if trimmed.contains(pattern) {
3849                            // Filter out false positives
3850                            // Skip if pattern is in a comment on same line
3851                            if let Some(comment_pos) = trimmed.find("//") {
3852                                if trimmed
3853                                    .find(pattern)
3854                                    .map(|p| p > comment_pos)
3855                                    .unwrap_or(false)
3856                                {
3857                                    continue;
3858                                }
3859                            }
3860
3861                            // For indexing, check it's actual array access not a range
3862                            if *pattern == "[" {
3863                                if !trimmed.contains("][")
3864                                    && !trimmed.contains("[..]")
3865                                    && trimmed.contains("[")
3866                                    && trimmed.contains("]")
3867                                    && !trimmed.contains(".get(")
3868                                {
3869                                    // Likely array indexing
3870                                } else {
3871                                    continue;
3872                                }
3873                            }
3874
3875                            let location = format!("{}:{}", rel_path, idx + 1);
3876                            findings.push(Finding {
3877                                rule_id: self.metadata.id.clone(),
3878                                rule_name: self.metadata.name.clone(),
3879                                severity: self.metadata.default_severity,
3880                                message: format!(
3881                                    "Panic-prone code in Drop impl for '{}': {}. {}",
3882                                    drop_type_name, pattern, advice
3883                                ),
3884                                function: location,
3885                                function_signature: format!("Drop for {}", drop_type_name),
3886                                evidence: vec![trimmed.to_string()],
3887                                span: None,
3888                                ..Default::default()
3889                            });
3890                            break; // One finding per line
3891                        }
3892                    }
3893                }
3894            }
3895        }
3896
3897        findings
3898    }
3899}
3900
3901// ============================================================================
3902// RUSTCOLA125: Spawned Task Panic Propagation Rule
3903// ============================================================================
3904
3905/// Detects spawned tasks without panic handling.
3906/// Panics in spawned tasks are silently swallowed by default.
3907pub struct SpawnedTaskPanicRule {
3908    metadata: RuleMetadata,
3909}
3910
3911impl SpawnedTaskPanicRule {
3912    pub fn new() -> Self {
3913        Self {
3914            metadata: RuleMetadata {
3915                id: "RUSTCOLA125".to_string(),
3916                name: "spawned-task-panic-propagation".to_string(),
3917                short_description: "Spawned task may silently swallow panics".to_string(),
3918                full_description: "Detects spawned tasks (tokio::spawn, async_std::spawn, etc.) \
3919                    without panic handling. By default, panics in spawned tasks are silently \
3920                    swallowed when the JoinHandle is dropped. Use .await on JoinHandle, \
3921                    catch_unwind, or panic hooks to detect task panics."
3922                    .to_string(),
3923                help_uri: Some("https://docs.rs/tokio/latest/tokio/task/fn.spawn.html".to_string()),
3924                default_severity: Severity::Medium,
3925                origin: RuleOrigin::BuiltIn,
3926                cwe_ids: Vec::new(),
3927                fix_suggestion: None,
3928                exploitability: Exploitability::default(),
3929            },
3930        }
3931    }
3932
3933    /// Spawn functions that need panic handling
3934    fn spawn_functions() -> &'static [&'static str] {
3935        &[
3936            "tokio::spawn(",
3937            "spawn(",
3938            "task::spawn(",
3939            "async_std::spawn(",
3940            "spawn_blocking(",
3941            "spawn_local(",
3942            "thread::spawn(",
3943            "rayon::spawn(",
3944        ]
3945    }
3946
3947    /// Patterns that indicate proper panic handling
3948    fn panic_handling_patterns() -> &'static [&'static str] {
3949        &[
3950            ".await",
3951            "join!(",
3952            "try_join!(",
3953            "JoinSet",
3954            "catch_unwind",
3955            "AssertUnwindSafe",
3956            "panic::set_hook",
3957            ".abort()",
3958            "JoinHandle",
3959            "let handle =",
3960            "let _ = handle",
3961        ]
3962    }
3963}
3964
3965impl Rule for SpawnedTaskPanicRule {
3966    fn metadata(&self) -> &RuleMetadata {
3967        &self.metadata
3968    }
3969
3970    fn evaluate(
3971        &self,
3972        package: &MirPackage,
3973        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
3974    ) -> Vec<Finding> {
3975        if package.crate_name == "mir-extractor" {
3976            return Vec::new();
3977        }
3978
3979        let mut findings = Vec::new();
3980        let crate_root = Path::new(&package.crate_root);
3981
3982        if !crate_root.exists() {
3983            return findings;
3984        }
3985
3986        for entry in WalkDir::new(crate_root)
3987            .into_iter()
3988            .filter_entry(|e| filter_entry(e))
3989        {
3990            let entry = match entry {
3991                Ok(e) => e,
3992                Err(_) => continue,
3993            };
3994
3995            if !entry.file_type().is_file() {
3996                continue;
3997            }
3998
3999            let path = entry.path();
4000            if path.extension() != Some(std::ffi::OsStr::new("rs")) {
4001                continue;
4002            }
4003
4004            let rel_path = path
4005                .strip_prefix(crate_root)
4006                .unwrap_or(path)
4007                .to_string_lossy()
4008                .replace('\\', "/");
4009
4010            let content = match std::fs::read_to_string(path) {
4011                Ok(c) => c,
4012                Err(_) => continue,
4013            };
4014
4015            let lines: Vec<&str> = content.lines().collect();
4016
4017            for (idx, line) in lines.iter().enumerate() {
4018                let trimmed = line.trim();
4019
4020                // Skip comments and test code
4021                if trimmed.starts_with("//") || trimmed.contains("#[test]") {
4022                    continue;
4023                }
4024
4025                // Check for spawn calls
4026                for spawn_fn in Self::spawn_functions() {
4027                    if trimmed.contains(spawn_fn) {
4028                        // Check if the spawn result is handled
4029                        let has_panic_handling = {
4030                            // Check current line and next 5 lines for handling
4031                            let check_range = idx..std::cmp::min(idx + 6, lines.len());
4032                            let context: String = lines[check_range]
4033                                .iter()
4034                                .map(|s| *s)
4035                                .collect::<Vec<&str>>()
4036                                .join("\n");
4037
4038                            Self::panic_handling_patterns()
4039                                .iter()
4040                                .any(|pattern| context.contains(pattern))
4041                        };
4042
4043                        // Also check if spawn is assigned to a variable (implies later handling)
4044                        let is_assigned = trimmed.contains("let ")
4045                            && trimmed.contains(" = ")
4046                            && !trimmed.contains("let _ =");
4047
4048                        if !has_panic_handling && !is_assigned {
4049                            let location = format!("{}:{}", rel_path, idx + 1);
4050                            findings.push(Finding::new(self.metadata.id.clone(), self.metadata.name.clone(), self.metadata.default_severity, format!(
4051                                    "Spawned task without panic handling. Panics will be silently swallowed. \
4052                                    Consider using .await on the JoinHandle or adding a panic hook."
4053                                ), location, String::new(), vec![trimmed.to_string()], None));
4054                        }
4055                        break;
4056                    }
4057                }
4058            }
4059        }
4060
4061        findings
4062    }
4063}
4064
4065// ============================================================================
4066// Registration
4067// ============================================================================
4068
4069/// Register all concurrency rules with the rule engine.
4070pub fn register_concurrency_rules(engine: &mut crate::RuleEngine) {
4071    engine.register_rule(Box::new(NonThreadSafeTestRule::new()));
4072    engine.register_rule(Box::new(BlockingSleepInAsyncRule::new()));
4073    engine.register_rule(Box::new(BlockingOpsInAsyncRule::new()));
4074    engine.register_rule(Box::new(MutexGuardAcrossAwaitRule::new()));
4075    engine.register_rule(Box::new(UnderscoreLockGuardRule::new()));
4076    engine.register_rule(Box::new(BroadcastUnsyncPayloadRule::new()));
4077    engine.register_rule(Box::new(PanicInDropRule::new()));
4078    engine.register_rule(Box::new(UnwrapInPollRule::new()));
4079    engine.register_rule(Box::new(UnsafeSendSyncBoundsRule::new()));
4080    engine.register_rule(Box::new(NonCancellationSafeSelectRule::new()));
4081    engine.register_rule(Box::new(MissingSyncBoundOnCloneRule::new()));
4082    engine.register_rule(Box::new(PinContractViolationRule::new()));
4083    engine.register_rule(Box::new(OneshotRaceAfterCloseRule::new()));
4084    engine.register_rule(Box::new(AsyncSignalUnsafeInHandlerRule::new()));
4085    engine.register_rule(Box::new(OnceCellTocTouRule::new()));
4086    engine.register_rule(Box::new(PanicWhileHoldingLockRule::new()));
4087    engine.register_rule(Box::new(ClosureEscapingRefsRule::new()));
4088    engine.register_rule(Box::new(ExecutorStarvationRule::new()));
4089    engine.register_rule(Box::new(AsyncDropCorrectnessRule::new()));
4090    engine.register_rule(Box::new(PanicInDropImplRule::new()));
4091    engine.register_rule(Box::new(SpawnedTaskPanicRule::new()));
4092}