Skip to main content

batuta/bug_hunter/
modes_fuzz.rs

1//! BH-04/05: Unsafe fuzzing and deep hunt modes.
2
3use super::types::*;
4use std::path::Path;
5
6/// Check if a source file contains #![forbid(unsafe_code)] in its first 50 lines.
7// SAFETY: no actual unsafe code -- checks if target crate forbids unsafe
8pub(super) fn source_forbids_unsafe(path: &Path) -> bool {
9    let Ok(content) = std::fs::read_to_string(path) else {
10        return false;
11    };
12    content.lines().take(50).any(|line| {
13        let t = line.trim();
14        t.starts_with("#![") && t.contains("forbid") && t.contains("unsafe_code")
15    })
16}
17
18/// Check if the crate forbids unsafe code (BH-19 fix).
19// SAFETY: no actual unsafe code -- checks if target crate forbids unsafe via lint attrs
20pub(super) fn crate_forbids_unsafe(project_path: &Path) -> bool {
21    for entry in ["src/lib.rs", "src/main.rs"] {
22        // SAFETY: no actual unsafe code -- delegating to source-level check
23        if source_forbids_unsafe(&project_path.join(entry)) {
24            return true;
25        }
26    }
27    if let Ok(content) = std::fs::read_to_string(project_path.join("Cargo.toml")) {
28        // SAFETY: no actual unsafe code -- checking Cargo.toml for unsafe_code lint config
29        if content.contains("unsafe_code") && content.contains("forbid") {
30            return true;
31        }
32    }
33    false
34}
35
36/// Scan a single file for unsafe blocks and dangerous operations within them.
37pub(super) fn scan_file_for_unsafe_blocks(
38    entry: &Path,
39    finding_id: &mut usize,
40    unsafe_inventory: &mut Vec<(std::path::PathBuf, usize)>,
41    result: &mut HuntResult,
42) {
43    let Ok(content) = std::fs::read_to_string(entry) else {
44        return;
45    };
46    let mut in_unsafe = false;
47    let mut unsafe_start = 0;
48
49    for (line_num, line) in content.lines().enumerate() {
50        let line_num = line_num + 1;
51
52        // SAFETY: no actual unsafe code -- scanning target file for unsafe blocks
53        if line.contains("unsafe ") && line.contains('{') {
54            in_unsafe = true;
55            unsafe_start = line_num;
56        }
57
58        // SAFETY: no actual unsafe code -- tracking state for unsafe block content analysis
59        if in_unsafe {
60            if line.contains('*') && (line.contains("ptr") || line.contains("as *")) {
61                *finding_id += 1;
62                unsafe_inventory.push((entry.to_path_buf(), line_num));
63                result.add_finding(
64                    Finding::new(
65                        format!("BH-UNSAFE-{:04}", finding_id),
66                        entry,
67                        line_num,
68                        "Pointer dereference in unsafe block",
69                    )
70                    .with_description(format!(
71                        "Unsafe block starting at line {}; potential fuzzing target",
72                        unsafe_start
73                    ))
74                    .with_severity(FindingSeverity::High)
75                    .with_category(DefectCategory::MemorySafety)
76                    .with_suspiciousness(0.75)
77                    .with_discovered_by(HuntMode::Fuzz)
78                    .with_evidence(FindingEvidence::fuzzing("N/A", "pointer_deref")),
79                );
80            }
81
82            if line.contains("transmute") {
83                *finding_id += 1;
84                result.add_finding(
85                    Finding::new(
86                        format!("BH-UNSAFE-{:04}", finding_id),
87                        entry,
88                        line_num,
89                        "Transmute in unsafe block",
90                    )
91                    .with_description(
92                        "std::mem::transmute bypasses type safety; high-priority fuzzing target",
93                    )
94                    .with_severity(FindingSeverity::Critical)
95                    .with_category(DefectCategory::MemorySafety)
96                    .with_suspiciousness(0.9)
97                    .with_discovered_by(HuntMode::Fuzz)
98                    .with_evidence(FindingEvidence::fuzzing("N/A", "transmute")),
99                );
100            }
101        }
102
103        // SAFETY: no actual unsafe code -- detecting end of unsafe block being scanned
104        if line.contains('}') && in_unsafe {
105            in_unsafe = false;
106        }
107    }
108}
109
110/// BH-04: Targeted unsafe Rust fuzzing (FourFuzz pattern)
111pub(super) fn run_fuzz_mode(project_path: &Path, config: &HuntConfig, result: &mut HuntResult) {
112    // SAFETY: no actual unsafe code -- checking if fuzz targets needed based on crate policy
113    if crate_forbids_unsafe(project_path) {
114        result.add_finding(
115            Finding::new(
116                "BH-FUZZ-SKIPPED",
117                project_path.join("src/lib.rs"),
118                1,
119                "Fuzz targets not needed - crate forbids unsafe code",
120            )
121            .with_description("Crate uses #![forbid(unsafe_code)], no unsafe blocks to fuzz")
122            .with_severity(FindingSeverity::Info)
123            .with_category(DefectCategory::ConfigurationErrors)
124            .with_suspiciousness(0.0)
125            .with_discovered_by(HuntMode::Fuzz),
126        );
127        return;
128    }
129
130    let mut unsafe_inventory = Vec::new();
131    let mut finding_id = 0;
132
133    for target in &config.targets {
134        let target_path = project_path.join(target);
135        for pattern in &[
136            format!("{}/*.rs", target_path.display()),
137            format!("{}/**/*.rs", target_path.display()),
138        ] {
139            if let Ok(entries) = glob::glob(pattern) {
140                for entry in entries.flatten() {
141                    scan_file_for_unsafe_blocks(
142                        &entry,
143                        &mut finding_id,
144                        &mut unsafe_inventory,
145                        result,
146                    );
147                }
148            }
149        }
150    }
151
152    let fuzz_dir = project_path.join("fuzz");
153    if !fuzz_dir.exists() {
154        result.add_finding(
155            Finding::new(
156                "BH-FUZZ-NOTARGETS",
157                project_path.join("Cargo.toml"),
158                1,
159                "No fuzz directory found",
160            )
161            .with_description(format!(
162                // SAFETY: no actual unsafe code -- format string referencing unsafe block count
163                "Create fuzz targets for {} identified unsafe blocks",
164                unsafe_inventory.len()
165            ))
166            .with_severity(FindingSeverity::Medium)
167            .with_category(DefectCategory::ConfigurationErrors)
168            .with_suspiciousness(0.4)
169            .with_discovered_by(HuntMode::Fuzz),
170        );
171    }
172
173    // SAFETY: no actual unsafe code -- computing fuzz coverage from unsafe inventory
174    result.stats.mode_stats.fuzz_coverage = if unsafe_inventory.is_empty() { 100.0 } else { 0.0 };
175}
176
177/// Scan a single file for deeply nested conditionals and complex boolean guards.
178pub(super) fn scan_file_for_deep_conditionals(
179    entry: &Path,
180    finding_id: &mut usize,
181    result: &mut HuntResult,
182) {
183    let Ok(content) = std::fs::read_to_string(entry) else {
184        return;
185    };
186    let mut complexity: usize = 0;
187    let mut complex_start: usize = 0;
188
189    for (line_num, line) in content.lines().enumerate() {
190        let line_num = line_num + 1;
191
192        if line.contains("if ") || line.contains("match ") {
193            complexity += 1;
194            if complexity == 1 {
195                complex_start = line_num;
196            }
197        }
198
199        if complexity >= 3 && line.contains("if ") {
200            *finding_id += 1;
201            result.add_finding(
202                Finding::new(
203                    format!("BH-DEEP-{:04}", *finding_id),
204                    entry,
205                    line_num,
206                    "Deeply nested conditional",
207                )
208                .with_description(format!(
209                    "Complexity {} starting at line {}; concolic execution recommended",
210                    complexity, complex_start
211                ))
212                .with_severity(FindingSeverity::Medium)
213                .with_category(DefectCategory::LogicErrors)
214                .with_suspiciousness(0.6)
215                .with_discovered_by(HuntMode::DeepHunt)
216                .with_evidence(FindingEvidence::concolic(format!("depth={}", complexity))),
217            );
218        }
219
220        if line.contains(" && ") && line.contains(" || ") {
221            *finding_id += 1;
222            result.add_finding(
223                Finding::new(
224                    format!("BH-DEEP-{:04}", *finding_id),
225                    entry,
226                    line_num,
227                    "Complex boolean guard",
228                )
229                .with_description("Mixed AND/OR logic; path explosion potential")
230                .with_severity(FindingSeverity::Medium)
231                .with_category(DefectCategory::LogicErrors)
232                .with_suspiciousness(0.55)
233                .with_discovered_by(HuntMode::DeepHunt)
234                .with_evidence(FindingEvidence::concolic("complex_guard")),
235            );
236        }
237
238        if line.contains('}') && complexity > 0 {
239            complexity -= 1;
240        }
241    }
242}
243
244/// BH-05: Hybrid concolic + SBFL (COTTONTAIL pattern)
245pub(super) fn run_deep_hunt_mode(
246    project_path: &Path,
247    config: &HuntConfig,
248    result: &mut HuntResult,
249) {
250    let mut finding_id = 0;
251
252    for target in &config.targets {
253        let target_path = project_path.join(target);
254        for pattern in &[
255            format!("{}/*.rs", target_path.display()),
256            format!("{}/**/*.rs", target_path.display()),
257        ] {
258            if let Ok(entries) = glob::glob(pattern) {
259                for entry in entries.flatten() {
260                    scan_file_for_deep_conditionals(&entry, &mut finding_id, result);
261                }
262            }
263        }
264    }
265
266    super::modes_hunt::run_hunt_mode(project_path, config, result);
267}