1use crate::analysis::safety::engine::RiskAssessmentEngine;
2use crate::analysis::safety::types::*;
3use crate::analysis::unsafe_ffi_tracker::{RiskLevel, SafetyViolation, StackFrame};
4use crate::capture::types::{AllocationInfo, TrackingError, TrackingResult};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone)]
11pub struct SafetyAnalysisConfig {
12 pub detailed_risk_assessment: bool,
13 pub enable_passport_tracking: bool,
14 pub min_risk_level: RiskLevel,
15 pub max_reports: usize,
16 pub enable_dynamic_violations: bool,
17 pub strict_mutex_handling: bool,
18 pub max_mutex_poison_retries: usize,
19}
20
21impl Default for SafetyAnalysisConfig {
22 fn default() -> Self {
23 Self {
24 detailed_risk_assessment: true,
25 enable_passport_tracking: true,
26 min_risk_level: RiskLevel::Low,
27 max_reports: 1000,
28 enable_dynamic_violations: true,
29 strict_mutex_handling: false,
30 max_mutex_poison_retries: 3,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Default)]
36struct CircuitBreaker {
37 poison_count: usize,
38 last_poison_time: Option<u64>,
39 is_open: bool,
40}
41
42impl CircuitBreaker {
43 fn record_poison(&mut self, max_retries: usize) {
44 self.poison_count += 1;
45 self.last_poison_time = Some(get_current_timestamp());
46
47 if self.poison_count >= max_retries {
48 self.is_open = true;
49 }
50 }
51
52 fn is_tripped(&self) -> bool {
53 self.is_open
54 }
55
56 fn reset(&mut self) {
57 self.poison_count = 0;
58 self.last_poison_time = None;
59 self.is_open = false;
60 }
61
62 fn poison_count(&self) -> usize {
63 self.poison_count
64 }
65
66 #[allow(dead_code)]
67 fn last_poison_time(&self) -> Option<u64> {
68 self.last_poison_time
69 }
70}
71
72fn get_current_timestamp() -> u64 {
73 match SystemTime::now().duration_since(UNIX_EPOCH) {
74 Ok(duration) => duration.as_secs(),
75 Err(e) => {
76 tracing::error!(
77 "System clock error when getting timestamp: {}. Using 0 as timestamp.",
78 e
79 );
80 0
81 }
82 }
83}
84
85fn get_current_timestamp_nanos() -> u128 {
86 match SystemTime::now().duration_since(UNIX_EPOCH) {
87 Ok(duration) => duration.as_nanos(),
88 Err(e) => {
89 tracing::error!(
90 "System clock error when getting timestamp in nanos: {}. Using 0 as timestamp.",
91 e
92 );
93 0
94 }
95 }
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct SafetyAnalysisStats {
100 pub total_reports: usize,
101 pub reports_by_risk_level: HashMap<String, usize>,
102 pub total_passports: usize,
103 pub passports_by_status: HashMap<String, usize>,
104 pub dynamic_violations: usize,
105 pub analysis_start_time: u64,
106}
107
108pub struct SafetyAnalyzer {
109 unsafe_reports: Arc<Mutex<HashMap<String, UnsafeReport>>>,
110 memory_passports: Arc<Mutex<HashMap<usize, MemoryPassport>>>,
111 risk_engine: RiskAssessmentEngine,
112 config: SafetyAnalysisConfig,
113 stats: Arc<Mutex<SafetyAnalysisStats>>,
114 reports_circuit_breaker: Arc<Mutex<CircuitBreaker>>,
115 passports_circuit_breaker: Arc<Mutex<CircuitBreaker>>,
116 stats_circuit_breaker: Arc<Mutex<CircuitBreaker>>,
117}
118
119impl SafetyAnalyzer {
120 pub fn new(config: SafetyAnalysisConfig) -> Self {
121 tracing::info!("🔒 Initializing Safety Analyzer");
122 tracing::info!(
123 " • Detailed risk assessment: {}",
124 config.detailed_risk_assessment
125 );
126 tracing::info!(
127 " • Passport tracking: {}",
128 config.enable_passport_tracking
129 );
130 tracing::info!(" • Min risk level: {:?}", config.min_risk_level);
131
132 Self {
133 unsafe_reports: Arc::new(Mutex::new(HashMap::new())),
134 memory_passports: Arc::new(Mutex::new(HashMap::new())),
135 risk_engine: RiskAssessmentEngine::new(),
136 config,
137 stats: Arc::new(Mutex::new(SafetyAnalysisStats {
138 analysis_start_time: get_current_timestamp(),
139 ..Default::default()
140 })),
141 reports_circuit_breaker: Arc::new(Mutex::new(CircuitBreaker::default())),
142 passports_circuit_breaker: Arc::new(Mutex::new(CircuitBreaker::default())),
143 stats_circuit_breaker: Arc::new(Mutex::new(CircuitBreaker::default())),
144 }
145 }
146
147 fn lock_circuit_breaker<'a>(
148 breaker: &'a Arc<Mutex<CircuitBreaker>>,
149 name: &str,
150 ) -> TrackingResult<std::sync::MutexGuard<'a, CircuitBreaker>> {
151 breaker.lock().map_err(|e| {
152 let error_msg = format!("Mutex poisoned in {}: {}", name, e);
153 tracing::error!("{}", error_msg);
154 TrackingError::LockError(error_msg)
155 })
156 }
157
158 fn lock_reports(
159 &self,
160 ) -> TrackingResult<std::sync::MutexGuard<'_, HashMap<String, UnsafeReport>>> {
161 let circuit_breaker =
162 Self::lock_circuit_breaker(&self.reports_circuit_breaker, "reports_circuit_breaker")?;
163
164 if circuit_breaker.is_tripped() {
165 return Err(TrackingError::LockError(
166 "Circuit breaker tripped for unsafe_reports: too many mutex poison events"
167 .to_string(),
168 ));
169 }
170
171 drop(circuit_breaker);
172
173 match self.unsafe_reports.lock() {
174 Ok(guard) => {
175 if let Ok(mut cb) = Self::lock_circuit_breaker(
176 &self.reports_circuit_breaker,
177 "reports_circuit_breaker",
178 ) {
179 cb.reset();
180 }
181 Ok(guard)
182 }
183 Err(e) => {
184 let error_msg = format!("Mutex poisoned in unsafe_reports: {}", e);
185 tracing::error!("{}", error_msg);
186
187 if let Ok(mut circuit_breaker) = Self::lock_circuit_breaker(
188 &self.reports_circuit_breaker,
189 "reports_circuit_breaker",
190 ) {
191 circuit_breaker.record_poison(self.config.max_mutex_poison_retries);
192
193 if self.config.strict_mutex_handling || circuit_breaker.is_tripped() {
194 tracing::error!(
195 "Circuit breaker tripped for unsafe_reports after {} poison events",
196 circuit_breaker.poison_count()
197 );
198 return Err(TrackingError::LockError(error_msg));
199 } else {
200 tracing::warn!(
201 "Recovering from mutex poison in unsafe_reports (attempt {}/{})",
202 circuit_breaker.poison_count(),
203 self.config.max_mutex_poison_retries
204 );
205 }
206 }
207
208 Ok(e.into_inner())
209 }
210 }
211 }
212
213 fn lock_passports(
214 &self,
215 ) -> TrackingResult<std::sync::MutexGuard<'_, HashMap<usize, MemoryPassport>>> {
216 let circuit_breaker = Self::lock_circuit_breaker(
217 &self.passports_circuit_breaker,
218 "passports_circuit_breaker",
219 )?;
220
221 if circuit_breaker.is_tripped() {
222 return Err(TrackingError::LockError(
223 "Circuit breaker tripped for memory_passports: too many mutex poison events"
224 .to_string(),
225 ));
226 }
227
228 drop(circuit_breaker);
229
230 match self.memory_passports.lock() {
231 Ok(guard) => {
232 if let Ok(mut cb) = Self::lock_circuit_breaker(
233 &self.passports_circuit_breaker,
234 "passports_circuit_breaker",
235 ) {
236 cb.reset();
237 }
238 Ok(guard)
239 }
240 Err(e) => {
241 let error_msg = format!("Mutex poisoned in memory_passports: {}", e);
242 tracing::error!("{}", error_msg);
243
244 if let Ok(mut circuit_breaker) = Self::lock_circuit_breaker(
245 &self.passports_circuit_breaker,
246 "passports_circuit_breaker",
247 ) {
248 circuit_breaker.record_poison(self.config.max_mutex_poison_retries);
249
250 if self.config.strict_mutex_handling || circuit_breaker.is_tripped() {
251 tracing::error!(
252 "Circuit breaker tripped for memory_passports after {} poison events",
253 circuit_breaker.poison_count()
254 );
255 return Err(TrackingError::LockError(error_msg));
256 } else {
257 tracing::warn!(
258 "Recovering from mutex poison in memory_passports (attempt {}/{})",
259 circuit_breaker.poison_count(),
260 self.config.max_mutex_poison_retries
261 );
262 }
263 }
264
265 Ok(e.into_inner())
266 }
267 }
268 }
269
270 fn lock_stats(&self) -> TrackingResult<std::sync::MutexGuard<'_, SafetyAnalysisStats>> {
271 let circuit_breaker =
272 Self::lock_circuit_breaker(&self.stats_circuit_breaker, "stats_circuit_breaker")?;
273
274 if circuit_breaker.is_tripped() {
275 return Err(TrackingError::LockError(
276 "Circuit breaker tripped for stats: too many mutex poison events".to_string(),
277 ));
278 }
279
280 drop(circuit_breaker);
281
282 match self.stats.lock() {
283 Ok(guard) => {
284 if let Ok(mut cb) =
285 Self::lock_circuit_breaker(&self.stats_circuit_breaker, "stats_circuit_breaker")
286 {
287 cb.reset();
288 }
289 Ok(guard)
290 }
291 Err(e) => {
292 let error_msg = format!("Mutex poisoned in stats: {}", e);
293 tracing::error!("{}", error_msg);
294
295 if let Ok(mut circuit_breaker) =
296 Self::lock_circuit_breaker(&self.stats_circuit_breaker, "stats_circuit_breaker")
297 {
298 circuit_breaker.record_poison(self.config.max_mutex_poison_retries);
299
300 if self.config.strict_mutex_handling || circuit_breaker.is_tripped() {
301 tracing::error!(
302 "Circuit breaker tripped for stats after {} poison events",
303 circuit_breaker.poison_count()
304 );
305 return Err(TrackingError::LockError(error_msg));
306 } else {
307 tracing::warn!(
308 "Recovering from mutex poison in stats (attempt {}/{})",
309 circuit_breaker.poison_count(),
310 self.config.max_mutex_poison_retries
311 );
312 }
313 }
314
315 Ok(e.into_inner())
316 }
317 }
318 }
319
320 pub fn generate_unsafe_report(
321 &self,
322 source: UnsafeSource,
323 allocations: &[AllocationInfo],
324 violations: &[SafetyViolation],
325 ) -> TrackingResult<String> {
326 let report_id = self.generate_report_id(&source);
327
328 tracing::info!("🔍 Generating unsafe report: {}", report_id);
329
330 let memory_context = self.create_memory_context(allocations);
331 let call_stack = self.capture_call_stack()?;
332
333 let risk_assessment = if self.config.detailed_risk_assessment {
334 self.risk_engine
335 .assess_risk(&source, &memory_context, &call_stack)
336 } else {
337 self.create_basic_risk_assessment(&source)
338 };
339
340 if !self.should_generate_report(&risk_assessment.risk_level) {
341 return Ok(report_id);
342 }
343
344 let dynamic_violations = self.convert_safety_violations(violations);
345
346 let related_passports = if self.config.enable_passport_tracking {
347 self.find_related_passports(&source, allocations)
348 } else {
349 Vec::new()
350 };
351
352 let report = UnsafeReport {
353 report_id: report_id.clone(),
354 source,
355 risk_assessment: risk_assessment.clone(),
356 dynamic_violations,
357 related_passports,
358 memory_context,
359 generated_at: get_current_timestamp(),
360 };
361
362 let mut reports = self.lock_reports()?;
363 if reports.len() >= self.config.max_reports {
364 if let Some(oldest_id) = reports.keys().next().cloned() {
365 reports.remove(&oldest_id);
366 }
367 }
368 reports.insert(report_id.clone(), report);
369
370 self.update_stats(&report_id, &risk_assessment.risk_level);
371
372 tracing::info!(
373 "✅ Generated unsafe report: {} (risk: {:?})",
374 report_id,
375 risk_assessment.risk_level
376 );
377
378 Ok(report_id)
379 }
380
381 pub fn create_memory_passport(
382 &self,
383 allocation_ptr: usize,
384 size_bytes: usize,
385 initial_event: PassportEventType,
386 ) -> TrackingResult<String> {
387 if !self.config.enable_passport_tracking {
388 return Ok(String::new());
389 }
390
391 let passport_id = format!(
392 "passport_{:x}_{}",
393 allocation_ptr,
394 get_current_timestamp_nanos()
395 );
396
397 let call_stack = self.capture_call_stack()?;
398 let current_time = get_current_timestamp();
399
400 let initial_passport_event = PassportEvent {
401 event_type: initial_event,
402 timestamp: current_time,
403 context: "SafetyAnalyzer".to_string(),
404 call_stack,
405 metadata: HashMap::new(),
406 };
407
408 let memory_context = MemoryContext {
409 total_allocated: size_bytes,
410 active_allocations: 1,
411 memory_pressure: MemoryPressureLevel::Low,
412 allocation_patterns: Vec::new(),
413 };
414
415 let source = UnsafeSource::RawPointer {
416 operation: "passport_creation".to_string(),
417 location: format!("0x{allocation_ptr:x}"),
418 };
419
420 let risk_assessment = self.risk_engine.assess_risk(&source, &memory_context, &[]);
421
422 let passport = MemoryPassport {
423 passport_id: passport_id.clone(),
424 allocation_ptr,
425 size_bytes,
426 status_at_shutdown: PassportStatus::Unknown,
427 lifecycle_events: vec![initial_passport_event],
428 risk_assessment,
429 created_at: current_time,
430 updated_at: current_time,
431 };
432
433 let mut passports = self.lock_passports()?;
434 passports.insert(allocation_ptr, passport);
435
436 let mut stats = self.lock_stats()?;
437 stats.total_passports += 1;
438
439 tracing::info!(
440 "📋 Created memory passport: {} for 0x{:x}",
441 passport_id,
442 allocation_ptr
443 );
444
445 Ok(passport_id)
446 }
447
448 pub fn record_passport_event(
449 &self,
450 allocation_ptr: usize,
451 event_type: PassportEventType,
452 context: String,
453 ) -> TrackingResult<()> {
454 if !self.config.enable_passport_tracking {
455 return Ok(());
456 }
457
458 let call_stack = self.capture_call_stack()?;
459 let current_time = get_current_timestamp();
460
461 let event = PassportEvent {
462 event_type,
463 timestamp: current_time,
464 context,
465 call_stack,
466 metadata: HashMap::new(),
467 };
468
469 let mut passports = self.lock_passports()?;
470 if let Some(passport) = passports.get_mut(&allocation_ptr) {
471 passport.lifecycle_events.push(event);
472 passport.updated_at = current_time;
473
474 tracing::info!("📝 Recorded passport event for 0x{:x}", allocation_ptr);
475 }
476
477 Ok(())
478 }
479
480 pub fn finalize_passports_at_shutdown(&self) -> Vec<String> {
481 let mut leaked_passports = Vec::new();
482
483 let mut passports = match self.lock_passports() {
484 Ok(guard) => guard,
485 Err(e) => {
486 tracing::error!("Failed to lock passports during finalization: {}", e);
487 return leaked_passports;
488 }
489 };
490
491 for (ptr, passport) in passports.iter_mut() {
492 let final_status = self.determine_final_passport_status(&passport.lifecycle_events);
493 passport.status_at_shutdown = final_status.clone();
494
495 if matches!(final_status, PassportStatus::InForeignCustody) {
496 leaked_passports.push(passport.passport_id.clone());
497 tracing::warn!(
498 "🚨 Memory leak detected: passport {} (0x{:x}) in foreign custody",
499 passport.passport_id,
500 ptr
501 );
502 }
503 }
504
505 let status_counts: Vec<String> = passports
506 .values()
507 .map(|p| format!("{:?}", p.status_at_shutdown))
508 .collect();
509
510 drop(passports);
511
512 let mut stats = match self.lock_stats() {
513 Ok(guard) => guard,
514 Err(e) => {
515 tracing::error!("Failed to lock stats during finalization: {}", e);
516 return leaked_passports;
517 }
518 };
519 for status_key in status_counts {
520 *stats.passports_by_status.entry(status_key).or_insert(0) += 1;
521 }
522
523 tracing::info!(
524 "🏁 Finalized {} passports, {} leaks detected",
525 self.get_passport_count(),
526 leaked_passports.len()
527 );
528
529 leaked_passports
530 }
531
532 pub fn get_unsafe_reports(&self) -> HashMap<String, UnsafeReport> {
533 match self.lock_reports() {
534 Ok(guard) => guard.clone(),
535 Err(e) => {
536 tracing::error!("Failed to get unsafe reports: {}", e);
537 HashMap::new()
538 }
539 }
540 }
541
542 pub fn get_memory_passports(&self) -> HashMap<usize, MemoryPassport> {
543 match self.lock_passports() {
544 Ok(guard) => guard.clone(),
545 Err(e) => {
546 tracing::error!("Failed to get memory passports: {}", e);
547 HashMap::new()
548 }
549 }
550 }
551
552 pub fn get_stats(&self) -> SafetyAnalysisStats {
553 match self.lock_stats() {
554 Ok(guard) => guard.clone(),
555 Err(e) => {
556 tracing::error!("Failed to get stats: {}", e);
557 SafetyAnalysisStats::default()
558 }
559 }
560 }
561
562 fn generate_report_id(&self, source: &UnsafeSource) -> String {
563 let timestamp = get_current_timestamp_nanos();
564
565 let source_type = match source {
566 UnsafeSource::UnsafeBlock { .. } => "UB",
567 UnsafeSource::FfiFunction { .. } => "FFI",
568 UnsafeSource::RawPointer { .. } => "PTR",
569 UnsafeSource::Transmute { .. } => "TX",
570 };
571
572 format!("UNSAFE-{}-{}", source_type, timestamp % 1000000)
573 }
574
575 fn create_memory_context(&self, allocations: &[AllocationInfo]) -> MemoryContext {
576 let total_allocated = allocations.iter().map(|a| a.size).sum();
577 let active_allocations = allocations
578 .iter()
579 .filter(|a| a.timestamp_dealloc.is_none())
580 .count();
581
582 let memory_pressure = if total_allocated > 1024 * 1024 * 1024 {
583 MemoryPressureLevel::Critical
584 } else if total_allocated > 512 * 1024 * 1024 {
585 MemoryPressureLevel::High
586 } else if total_allocated > 256 * 1024 * 1024 {
587 MemoryPressureLevel::Medium
588 } else {
589 MemoryPressureLevel::Low
590 };
591
592 MemoryContext {
593 total_allocated,
594 active_allocations,
595 memory_pressure,
596 allocation_patterns: Vec::new(),
597 }
598 }
599
600 fn capture_call_stack(&self) -> TrackingResult<Vec<StackFrame>> {
601 Ok(vec![StackFrame {
602 function_name: "safety_analyzer".to_string(),
603 file_name: Some("src/analysis/safety_analyzer.rs".to_string()),
604 line_number: Some(1),
605 is_unsafe: false,
606 }])
607 }
608
609 fn create_basic_risk_assessment(&self, source: &UnsafeSource) -> RiskAssessment {
610 let (risk_level, risk_score) = match source {
611 UnsafeSource::UnsafeBlock { .. } => (RiskLevel::Medium, 50.0),
612 UnsafeSource::FfiFunction { .. } => (RiskLevel::Medium, 45.0),
613 UnsafeSource::RawPointer { .. } => (RiskLevel::High, 70.0),
614 UnsafeSource::Transmute { .. } => (RiskLevel::High, 65.0),
615 };
616
617 RiskAssessment {
618 risk_level,
619 risk_score,
620 risk_factors: Vec::new(),
621 confidence_score: 0.5,
622 mitigation_suggestions: vec!["Review unsafe operation for safety".to_string()],
623 assessment_timestamp: get_current_timestamp(),
624 }
625 }
626
627 fn should_generate_report(&self, risk_level: &RiskLevel) -> bool {
628 match (&self.config.min_risk_level, risk_level) {
629 (RiskLevel::Low, _) => true,
630 (RiskLevel::Medium, RiskLevel::Low) => false,
631 (RiskLevel::Medium, _) => true,
632 (RiskLevel::High, RiskLevel::Low | RiskLevel::Medium) => false,
633 (RiskLevel::High, _) => true,
634 (RiskLevel::Critical, RiskLevel::Critical) => true,
635 (RiskLevel::Critical, _) => false,
636 }
637 }
638
639 fn convert_safety_violations(&self, violations: &[SafetyViolation]) -> Vec<DynamicViolation> {
640 violations
641 .iter()
642 .map(|v| match v {
643 SafetyViolation::DoubleFree { timestamp, .. } => DynamicViolation {
644 violation_type: ViolationType::DoubleFree,
645 memory_address: 0,
646 memory_size: 0,
647 detected_at: (*timestamp as u64),
648 call_stack: Vec::new(),
649 severity: RiskLevel::Critical,
650 context: "Double free detected: memory was freed twice".to_string(),
651 },
652 SafetyViolation::InvalidFree {
653 attempted_pointer,
654 timestamp,
655 ..
656 } => DynamicViolation {
657 violation_type: ViolationType::InvalidAccess,
658 memory_address: *attempted_pointer,
659 memory_size: 0,
660 detected_at: (*timestamp as u64),
661 call_stack: Vec::new(),
662 severity: RiskLevel::High,
663 context: format!(
664 "Invalid free attempted at address 0x{:x}",
665 attempted_pointer
666 ),
667 },
668 SafetyViolation::PotentialLeak {
669 allocation_timestamp,
670 leak_detection_timestamp,
671 ..
672 } => DynamicViolation {
673 violation_type: ViolationType::MemoryLeak,
674 memory_address: 0,
675 memory_size: 0,
676 detected_at: (*leak_detection_timestamp as u64),
677 call_stack: Vec::new(),
678 severity: RiskLevel::Medium,
679 context: format!(
680 "Potential memory leak detected (allocated at timestamp {})",
681 allocation_timestamp
682 ),
683 },
684 SafetyViolation::CrossBoundaryRisk {
685 risk_level,
686 description,
687 ..
688 } => DynamicViolation {
689 violation_type: ViolationType::FfiBoundaryViolation,
690 memory_address: 0,
691 memory_size: 0,
692 detected_at: get_current_timestamp(),
693 call_stack: Vec::new(),
694 severity: risk_level.clone(),
695 context: description.clone(),
696 },
697 })
698 .collect()
699 }
700
701 fn find_related_passports(
702 &self,
703 _source: &UnsafeSource,
704 _allocations: &[AllocationInfo],
705 ) -> Vec<String> {
706 Vec::new()
707 }
708
709 fn update_stats(&self, _report_id: &str, risk_level: &RiskLevel) {
710 match self.lock_stats() {
711 Ok(mut stats) => {
712 stats.total_reports += 1;
713 let risk_key = format!("{risk_level:?}");
714 *stats.reports_by_risk_level.entry(risk_key).or_insert(0) += 1;
715 }
716 Err(e) => {
717 tracing::error!("Failed to update stats: {}", e);
718 }
719 }
720 }
721
722 fn determine_final_passport_status(&self, events: &[PassportEvent]) -> PassportStatus {
723 let mut has_handover = false;
724 let mut has_reclaim = false;
725 let mut has_foreign_free = false;
726
727 for event in events {
728 match event.event_type {
729 PassportEventType::HandoverToFfi => has_handover = true,
730 PassportEventType::ReclaimedByRust => has_reclaim = true,
731 PassportEventType::FreedByForeign => has_foreign_free = true,
732 _ => {}
733 }
734 }
735
736 if has_handover && !has_reclaim && !has_foreign_free {
737 PassportStatus::InForeignCustody
738 } else if has_foreign_free {
739 PassportStatus::FreedByForeign
740 } else if has_reclaim {
741 PassportStatus::ReclaimedByRust
742 } else if has_handover {
743 PassportStatus::HandoverToFfi
744 } else {
745 PassportStatus::FreedByRust
746 }
747 }
748
749 fn get_passport_count(&self) -> usize {
750 self.memory_passports.lock().map(|p| p.len()).unwrap_or(0)
751 }
752}
753
754impl Default for SafetyAnalyzer {
755 fn default() -> Self {
756 Self::new(SafetyAnalysisConfig::default())
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
767 fn test_safety_analyzer_default() {
768 let analyzer = SafetyAnalyzer::default();
769 let stats = analyzer.get_stats();
770 assert_eq!(
771 stats.total_reports, 0,
772 "New analyzer should have zero reports"
773 );
774 assert_eq!(
775 stats.total_passports, 0,
776 "New analyzer should have zero passports"
777 );
778 }
779
780 #[test]
783 fn test_safety_analyzer_custom_config() {
784 let config = SafetyAnalysisConfig {
785 detailed_risk_assessment: false,
786 enable_passport_tracking: false,
787 min_risk_level: RiskLevel::High,
788 max_reports: 100,
789 enable_dynamic_violations: false,
790 ..Default::default()
791 };
792 let analyzer = SafetyAnalyzer::new(config);
793 let stats = analyzer.get_stats();
794 assert_eq!(
795 stats.total_reports, 0,
796 "Custom config analyzer should start with zero reports"
797 );
798 }
799
800 #[test]
803 fn test_generate_unsafe_report_unsafe_block() {
804 let analyzer = SafetyAnalyzer::default();
805 let source = UnsafeSource::UnsafeBlock {
806 location: "test.rs:10".to_string(),
807 function: "test_fn".to_string(),
808 file_path: Some("test.rs".to_string()),
809 line_number: Some(10),
810 };
811
812 let result = analyzer.generate_unsafe_report(source, &[], &[]);
813 assert!(result.is_ok(), "Should generate report successfully");
814 let report_id = result.unwrap();
815 assert!(
816 report_id.starts_with("UNSAFE-UB-"),
817 "Report ID should start with UNSAFE-UB-"
818 );
819 }
820
821 #[test]
824 fn test_generate_unsafe_report_ffi() {
825 let analyzer = SafetyAnalyzer::default();
826 let source = UnsafeSource::FfiFunction {
827 library: "libc".to_string(),
828 function: "malloc".to_string(),
829 call_site: "test.rs:20".to_string(),
830 };
831
832 let result = analyzer.generate_unsafe_report(source, &[], &[]);
833 assert!(result.is_ok(), "Should generate FFI report successfully");
834 let report_id = result.unwrap();
835 assert!(
836 report_id.starts_with("UNSAFE-FFI-"),
837 "FFI report ID should start with UNSAFE-FFI-"
838 );
839
840 let reports = analyzer.get_unsafe_reports();
841 let report = reports
842 .get(&report_id)
843 .expect("Report should exist in reports map");
844
845 match &report.source {
846 UnsafeSource::FfiFunction {
847 library,
848 function,
849 call_site,
850 } => {
851 assert_eq!(
852 library, "libc",
853 "FFI report should contain correct library name"
854 );
855 assert_eq!(
856 function, "malloc",
857 "FFI report should contain correct function name"
858 );
859 assert_eq!(
860 call_site, "test.rs:20",
861 "FFI report should contain correct call site"
862 );
863 }
864 _ => panic!("Report source should be FfiFunction variant"),
865 }
866 }
867
868 #[test]
871 fn test_generate_unsafe_report_raw_pointer() {
872 let analyzer = SafetyAnalyzer::default();
873 let source = UnsafeSource::RawPointer {
874 operation: "dereference".to_string(),
875 location: "0x1000".to_string(),
876 };
877
878 let result = analyzer.generate_unsafe_report(source, &[], &[]);
879 assert!(
880 result.is_ok(),
881 "Should generate raw pointer report successfully"
882 );
883 let report_id = result.unwrap();
884 assert!(
885 report_id.starts_with("UNSAFE-PTR-"),
886 "PTR report ID should start with UNSAFE-PTR-"
887 );
888 }
889
890 #[test]
893 fn test_generate_unsafe_report_transmute() {
894 let analyzer = SafetyAnalyzer::default();
895 let source = UnsafeSource::Transmute {
896 from_type: "u8".to_string(),
897 to_type: "i8".to_string(),
898 location: "test.rs:30".to_string(),
899 };
900
901 let result = analyzer.generate_unsafe_report(source, &[], &[]);
902 assert!(
903 result.is_ok(),
904 "Should generate transmute report successfully"
905 );
906 let report_id = result.unwrap();
907 assert!(
908 report_id.starts_with("UNSAFE-TX-"),
909 "TX report ID should start with UNSAFE-TX-"
910 );
911 }
912
913 #[test]
916 fn test_create_memory_passport() {
917 let analyzer = SafetyAnalyzer::default();
918 let result =
919 analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
920 assert!(result.is_ok(), "Should create passport successfully");
921 let passport_id = result.unwrap();
922 assert!(
923 passport_id.starts_with("passport_"),
924 "Passport ID should start with passport_"
925 );
926
927 let stats = analyzer.get_stats();
928 assert_eq!(
929 stats.total_passports, 1,
930 "Should have one passport after creation"
931 );
932 }
933
934 #[test]
937 fn test_passport_tracking_disabled() {
938 let config = SafetyAnalysisConfig {
939 enable_passport_tracking: false,
940 ..Default::default()
941 };
942 let analyzer = SafetyAnalyzer::new(config);
943 let result =
944 analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
945 assert!(result.is_ok(), "Should return Ok even when disabled");
946 assert!(
947 result.unwrap().is_empty(),
948 "Should return empty string when disabled"
949 );
950 }
951
952 #[test]
955 fn test_record_passport_event() {
956 let analyzer = SafetyAnalyzer::default();
957 analyzer
958 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
959 .unwrap();
960
961 let result = analyzer.record_passport_event(
962 0x1000,
963 PassportEventType::HandoverToFfi,
964 "test_context".to_string(),
965 );
966 assert!(result.is_ok(), "Should record event successfully");
967
968 let passports = analyzer.get_memory_passports();
969 assert!(passports.contains_key(&0x1000), "Passport should exist");
970 let passport = passports.get(&0x1000).unwrap();
971 assert_eq!(passport.lifecycle_events.len(), 2, "Should have two events");
972 }
973
974 #[test]
977 fn test_finalize_passports_leak_detection() {
978 let analyzer = SafetyAnalyzer::default();
979 analyzer
980 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
981 .unwrap();
982 analyzer
983 .record_passport_event(
984 0x1000,
985 PassportEventType::HandoverToFfi,
986 "ffi_transfer".to_string(),
987 )
988 .unwrap();
989
990 let leaks = analyzer.finalize_passports_at_shutdown();
991 assert_eq!(
992 leaks.len(),
993 1,
994 "Should detect one leak for passport in foreign custody"
995 );
996 }
997
998 #[test]
1001 fn test_finalize_passports_no_leak() {
1002 let analyzer = SafetyAnalyzer::default();
1003 analyzer
1004 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1005 .unwrap();
1006 analyzer
1007 .record_passport_event(
1008 0x1000,
1009 PassportEventType::FreedByForeign,
1010 "freed".to_string(),
1011 )
1012 .unwrap();
1013
1014 let leaks = analyzer.finalize_passports_at_shutdown();
1015 assert!(
1016 leaks.is_empty(),
1017 "Should not detect leak for freed passport"
1018 );
1019 }
1020
1021 #[test]
1024 fn test_get_unsafe_reports() {
1025 let analyzer = SafetyAnalyzer::default();
1026 let source = UnsafeSource::UnsafeBlock {
1027 location: "test.rs".to_string(),
1028 function: "test".to_string(),
1029 file_path: None,
1030 line_number: None,
1031 };
1032 analyzer
1033 .generate_unsafe_report(source.clone(), &[], &[])
1034 .unwrap();
1035 analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1036
1037 let reports = analyzer.get_unsafe_reports();
1038 assert_eq!(reports.len(), 2, "Should have two reports");
1039 }
1040
1041 #[test]
1044 fn test_min_risk_level_filtering() {
1045 let config = SafetyAnalysisConfig {
1046 min_risk_level: RiskLevel::Critical,
1047 ..Default::default()
1048 };
1049 let analyzer = SafetyAnalyzer::new(config);
1050 let source = UnsafeSource::UnsafeBlock {
1051 location: "test.rs".to_string(),
1052 function: "test".to_string(),
1053 file_path: None,
1054 line_number: None,
1055 };
1056 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1057 assert!(result.is_ok(), "Should return Ok even when filtered");
1058 }
1059
1060 #[test]
1063 fn test_stats_update() {
1064 let analyzer = SafetyAnalyzer::default();
1065 let source = UnsafeSource::RawPointer {
1066 operation: "test".to_string(),
1067 location: "test".to_string(),
1068 };
1069 analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1070
1071 let stats = analyzer.get_stats();
1072 assert_eq!(stats.total_reports, 1, "Stats should show one report");
1073 assert!(
1074 !stats.reports_by_risk_level.is_empty(),
1075 "Should have risk level breakdown"
1076 );
1077 }
1078
1079 #[test]
1082 fn test_max_reports_limit() {
1083 let config = SafetyAnalysisConfig {
1084 max_reports: 2,
1085 ..Default::default()
1086 };
1087 let analyzer = SafetyAnalyzer::new(config);
1088 let source = UnsafeSource::UnsafeBlock {
1089 location: "test.rs".to_string(),
1090 function: "test".to_string(),
1091 file_path: None,
1092 line_number: None,
1093 };
1094
1095 analyzer
1096 .generate_unsafe_report(source.clone(), &[], &[])
1097 .unwrap();
1098 analyzer
1099 .generate_unsafe_report(source.clone(), &[], &[])
1100 .unwrap();
1101 analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1102
1103 let reports = analyzer.get_unsafe_reports();
1104 assert!(reports.len() <= 2, "Should not exceed max_reports limit");
1105 }
1106
1107 #[test]
1110 fn test_safety_config_default() {
1111 let config = SafetyAnalysisConfig::default();
1112 assert!(
1113 config.detailed_risk_assessment,
1114 "Detailed risk assessment should be enabled"
1115 );
1116 assert!(
1117 config.enable_passport_tracking,
1118 "Passport tracking should be enabled"
1119 );
1120 assert_eq!(config.max_reports, 1000, "Max reports should be 1000");
1121 }
1122
1123 #[test]
1126 fn test_risk_level_ordering() {
1127 assert!(matches!(RiskLevel::Low, RiskLevel::Low));
1128 assert!(matches!(RiskLevel::Medium, RiskLevel::Medium));
1129 assert!(matches!(RiskLevel::High, RiskLevel::High));
1130 assert!(matches!(RiskLevel::Critical, RiskLevel::Critical));
1131 }
1132
1133 #[test]
1136 fn test_passport_status_variants() {
1137 let statuses = vec![
1138 PassportStatus::FreedByRust,
1139 PassportStatus::HandoverToFfi,
1140 PassportStatus::FreedByForeign,
1141 PassportStatus::ReclaimedByRust,
1142 PassportStatus::InForeignCustody,
1143 PassportStatus::Unknown,
1144 ];
1145
1146 for status in &statuses {
1147 let debug_str = format!("{status:?}");
1148 assert!(
1149 !debug_str.is_empty(),
1150 "Status should have debug representation"
1151 );
1152 }
1153 }
1154
1155 #[test]
1158 fn test_passport_event_type_variants() {
1159 let event_types = vec![
1160 PassportEventType::AllocatedInRust,
1161 PassportEventType::HandoverToFfi,
1162 PassportEventType::FreedByForeign,
1163 PassportEventType::ReclaimedByRust,
1164 PassportEventType::BoundaryAccess,
1165 PassportEventType::OwnershipTransfer,
1166 ];
1167
1168 for event_type in &event_types {
1169 let debug_str = format!("{event_type:?}");
1170 assert!(
1171 !debug_str.is_empty(),
1172 "Event type should have debug representation"
1173 );
1174 }
1175 }
1176
1177 #[test]
1180 fn test_record_passport_event_non_existent() {
1181 let analyzer = SafetyAnalyzer::default();
1182 let result = analyzer.record_passport_event(
1183 0x9999,
1184 PassportEventType::HandoverToFfi,
1185 "test_context".to_string(),
1186 );
1187 assert!(
1188 result.is_ok(),
1189 "Should return Ok even for non-existent passport"
1190 );
1191
1192 let passports = analyzer.get_memory_passports();
1193 assert!(
1194 !passports.contains_key(&0x9999),
1195 "Non-existent passport should not be created"
1196 );
1197 }
1198
1199 #[test]
1202 fn test_generate_unsafe_report_with_allocations() {
1203 let analyzer = SafetyAnalyzer::default();
1204 let source = UnsafeSource::RawPointer {
1205 operation: "test".to_string(),
1206 location: "test.rs".to_string(),
1207 };
1208
1209 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1210 assert!(
1211 result.is_ok(),
1212 "Should generate report with empty allocations"
1213 );
1214 }
1215
1216 #[test]
1219 fn test_generate_unsafe_report_with_violations() {
1220 let analyzer = SafetyAnalyzer::default();
1221 let source = UnsafeSource::UnsafeBlock {
1222 location: "test.rs".to_string(),
1223 function: "test".to_string(),
1224 file_path: None,
1225 line_number: None,
1226 };
1227
1228 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1229 assert!(
1230 result.is_ok(),
1231 "Should generate report with empty violations"
1232 );
1233
1234 let report_id = result.unwrap();
1235 let reports = analyzer.get_unsafe_reports();
1236 let report = reports.get(&report_id).expect("Report should exist");
1237 assert_eq!(
1238 report.dynamic_violations.len(),
1239 0,
1240 "Should have no violations when empty"
1241 );
1242 }
1243
1244 #[test]
1247 fn test_determine_final_passport_status_scenarios() {
1248 let analyzer = SafetyAnalyzer::default();
1249
1250 let events_handover_only = vec![PassportEvent {
1251 event_type: PassportEventType::HandoverToFfi,
1252 timestamp: 1000,
1253 context: "test".to_string(),
1254 call_stack: vec![],
1255 metadata: HashMap::new(),
1256 }];
1257 let status = analyzer.determine_final_passport_status(&events_handover_only);
1258 assert!(
1259 matches!(status, PassportStatus::InForeignCustody),
1260 "Handover without reclaim or foreign free should be InForeignCustody"
1261 );
1262
1263 let events_reclaimed = vec![
1264 PassportEvent {
1265 event_type: PassportEventType::HandoverToFfi,
1266 timestamp: 1000,
1267 context: "test".to_string(),
1268 call_stack: vec![],
1269 metadata: HashMap::new(),
1270 },
1271 PassportEvent {
1272 event_type: PassportEventType::ReclaimedByRust,
1273 timestamp: 2000,
1274 context: "test".to_string(),
1275 call_stack: vec![],
1276 metadata: HashMap::new(),
1277 },
1278 ];
1279 let status = analyzer.determine_final_passport_status(&events_reclaimed);
1280 assert!(
1281 matches!(status, PassportStatus::ReclaimedByRust),
1282 "Reclaimed after handover should be ReclaimedByRust"
1283 );
1284
1285 let events_freed_by_foreign = vec![
1286 PassportEvent {
1287 event_type: PassportEventType::HandoverToFfi,
1288 timestamp: 1000,
1289 context: "test".to_string(),
1290 call_stack: vec![],
1291 metadata: HashMap::new(),
1292 },
1293 PassportEvent {
1294 event_type: PassportEventType::FreedByForeign,
1295 timestamp: 2000,
1296 context: "test".to_string(),
1297 call_stack: vec![],
1298 metadata: HashMap::new(),
1299 },
1300 ];
1301 let status = analyzer.determine_final_passport_status(&events_freed_by_foreign);
1302 assert!(
1303 matches!(status, PassportStatus::FreedByForeign),
1304 "Freed by foreign should be FreedByForeign"
1305 );
1306
1307 let events_no_handover = vec![PassportEvent {
1308 event_type: PassportEventType::AllocatedInRust,
1309 timestamp: 1000,
1310 context: "test".to_string(),
1311 call_stack: vec![],
1312 metadata: HashMap::new(),
1313 }];
1314 let status = analyzer.determine_final_passport_status(&events_no_handover);
1315 assert!(
1316 matches!(status, PassportStatus::FreedByRust),
1317 "No handover should be FreedByRust"
1318 );
1319 }
1320
1321 #[test]
1324 fn test_create_basic_risk_assessment() {
1325 let config = SafetyAnalysisConfig {
1326 detailed_risk_assessment: false,
1327 ..Default::default()
1328 };
1329 let analyzer = SafetyAnalyzer::new(config);
1330
1331 let source = UnsafeSource::RawPointer {
1332 operation: "test".to_string(),
1333 location: "test.rs".to_string(),
1334 };
1335 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1336 assert!(
1337 result.is_ok(),
1338 "Should generate report with basic assessment"
1339 );
1340
1341 let report_id = result.unwrap();
1342 let reports = analyzer.get_unsafe_reports();
1343 let report = reports.get(&report_id).expect("Report should exist");
1344 assert!(
1345 report.risk_assessment.risk_score > 0.0,
1346 "Basic assessment should have risk score"
1347 );
1348 }
1349
1350 #[test]
1353 fn test_should_generate_report_filtering() {
1354 let config = SafetyAnalysisConfig {
1355 min_risk_level: RiskLevel::High,
1356 ..Default::default()
1357 };
1358 let analyzer = SafetyAnalyzer::new(config);
1359
1360 let source = UnsafeSource::UnsafeBlock {
1361 location: "test.rs".to_string(),
1362 function: "test".to_string(),
1363 file_path: None,
1364 line_number: None,
1365 };
1366 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1367 assert!(result.is_ok(), "Should return Ok even when filtered");
1368
1369 let reports = analyzer.get_unsafe_reports();
1370 assert!(
1371 reports.is_empty(),
1372 "Report should be filtered out when below min risk level"
1373 );
1374 }
1375
1376 #[test]
1379 fn test_memory_pressure_levels() {
1380 let analyzer = SafetyAnalyzer::default();
1381
1382 let source = UnsafeSource::UnsafeBlock {
1383 location: "test.rs".to_string(),
1384 function: "test".to_string(),
1385 file_path: None,
1386 line_number: None,
1387 };
1388 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1389 assert!(result.is_ok(), "Should handle empty allocations");
1390 }
1391
1392 #[test]
1395 fn test_passport_multiple_events() {
1396 let analyzer = SafetyAnalyzer::default();
1397
1398 analyzer
1399 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1400 .unwrap();
1401
1402 analyzer
1403 .record_passport_event(
1404 0x1000,
1405 PassportEventType::HandoverToFfi,
1406 "transfer_to_ffi".to_string(),
1407 )
1408 .unwrap();
1409
1410 analyzer
1411 .record_passport_event(
1412 0x1000,
1413 PassportEventType::BoundaryAccess,
1414 "ffi_access".to_string(),
1415 )
1416 .unwrap();
1417
1418 analyzer
1419 .record_passport_event(
1420 0x1000,
1421 PassportEventType::ReclaimedByRust,
1422 "reclaimed".to_string(),
1423 )
1424 .unwrap();
1425
1426 let passports = analyzer.get_memory_passports();
1427 let passport = passports.get(&0x1000).expect("Passport should exist");
1428 assert_eq!(
1429 passport.lifecycle_events.len(),
1430 4,
1431 "Should have four events"
1432 );
1433 }
1434
1435 #[test]
1438 fn test_finalize_passports_mixed_statuses() {
1439 let analyzer = SafetyAnalyzer::default();
1440
1441 analyzer
1442 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1443 .unwrap();
1444 analyzer
1445 .record_passport_event(
1446 0x1000,
1447 PassportEventType::HandoverToFfi,
1448 "leaked".to_string(),
1449 )
1450 .unwrap();
1451
1452 analyzer
1453 .create_memory_passport(0x2000, 2048, PassportEventType::AllocatedInRust)
1454 .unwrap();
1455 analyzer
1456 .record_passport_event(
1457 0x2000,
1458 PassportEventType::FreedByForeign,
1459 "freed".to_string(),
1460 )
1461 .unwrap();
1462
1463 let leaks = analyzer.finalize_passports_at_shutdown();
1464 assert_eq!(leaks.len(), 1, "Should detect one leak");
1465
1466 let stats = analyzer.get_stats();
1467 assert!(
1468 stats.passports_by_status.contains_key("InForeignCustody"),
1469 "Stats should include InForeignCustody status"
1470 );
1471 assert!(
1472 stats.passports_by_status.contains_key("FreedByForeign"),
1473 "Stats should include FreedByForeign status"
1474 );
1475 }
1476
1477 #[test]
1480 fn test_enable_dynamic_violations_config() {
1481 let config = SafetyAnalysisConfig {
1482 enable_dynamic_violations: false,
1483 ..Default::default()
1484 };
1485 let analyzer = SafetyAnalyzer::new(config);
1486
1487 let source = UnsafeSource::UnsafeBlock {
1488 location: "test.rs".to_string(),
1489 function: "test".to_string(),
1490 file_path: None,
1491 line_number: None,
1492 };
1493 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1494 assert!(
1495 result.is_ok(),
1496 "Should generate report with dynamic violations disabled"
1497 );
1498 }
1499
1500 #[test]
1503 fn test_all_unsafe_source_variants() {
1504 let analyzer = SafetyAnalyzer::default();
1505
1506 let sources = vec![
1507 UnsafeSource::UnsafeBlock {
1508 location: "test.rs".to_string(),
1509 function: "test".to_string(),
1510 file_path: None,
1511 line_number: None,
1512 },
1513 UnsafeSource::FfiFunction {
1514 library: "libc".to_string(),
1515 function: "malloc".to_string(),
1516 call_site: "test.rs".to_string(),
1517 },
1518 UnsafeSource::RawPointer {
1519 operation: "test".to_string(),
1520 location: "test.rs".to_string(),
1521 },
1522 UnsafeSource::Transmute {
1523 from_type: "u8".to_string(),
1524 to_type: "i8".to_string(),
1525 location: "test.rs".to_string(),
1526 },
1527 ];
1528
1529 let mut report_ids = Vec::new();
1530 for source in sources {
1531 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1532 assert!(
1533 result.is_ok(),
1534 "Should generate report for all source types"
1535 );
1536 report_ids.push(result.unwrap());
1537 }
1538
1539 assert_eq!(report_ids.len(), 4, "Should have generated 4 reports");
1540 }
1541
1542 #[test]
1545 fn test_determine_final_passport_status_conflicting_events() {
1546 let analyzer = SafetyAnalyzer::default();
1547
1548 let events_conflict = vec![
1549 PassportEvent {
1550 event_type: PassportEventType::HandoverToFfi,
1551 timestamp: 1000,
1552 context: "test".to_string(),
1553 call_stack: vec![],
1554 metadata: HashMap::new(),
1555 },
1556 PassportEvent {
1557 event_type: PassportEventType::ReclaimedByRust,
1558 timestamp: 2000,
1559 context: "test".to_string(),
1560 call_stack: vec![],
1561 metadata: HashMap::new(),
1562 },
1563 PassportEvent {
1564 event_type: PassportEventType::FreedByForeign,
1565 timestamp: 3000,
1566 context: "test".to_string(),
1567 call_stack: vec![],
1568 metadata: HashMap::new(),
1569 },
1570 ];
1571 let status = analyzer.determine_final_passport_status(&events_conflict);
1572 assert!(
1573 matches!(status, PassportStatus::FreedByForeign),
1574 "When both reclaim and foreign_free exist, should prioritize foreign_free"
1575 );
1576 }
1577
1578 #[test]
1581 fn test_create_memory_passport_zero_size() {
1582 let analyzer = SafetyAnalyzer::default();
1583 let result = analyzer.create_memory_passport(0x1000, 0, PassportEventType::AllocatedInRust);
1584 assert!(result.is_ok(), "Should create passport with zero size");
1585
1586 let passports = analyzer.get_memory_passports();
1587 let passport = passports.get(&0x1000).expect("Passport should exist");
1588 assert_eq!(passport.size_bytes, 0, "Passport should have zero size");
1589 }
1590
1591 #[test]
1594 fn test_create_memory_passport_null_pointer() {
1595 let analyzer = SafetyAnalyzer::default();
1596 let result = analyzer.create_memory_passport(0x0, 1024, PassportEventType::AllocatedInRust);
1597 assert!(result.is_ok(), "Should create passport with null pointer");
1598
1599 let passports = analyzer.get_memory_passports();
1600 assert!(
1601 passports.contains_key(&0x0),
1602 "Passport with null pointer should exist"
1603 );
1604 }
1605
1606 #[test]
1609 fn test_create_memory_passport_duplicate_pointer() {
1610 let analyzer = SafetyAnalyzer::default();
1611
1612 analyzer
1613 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1614 .unwrap();
1615
1616 analyzer
1617 .create_memory_passport(0x1000, 2048, PassportEventType::AllocatedInRust)
1618 .unwrap();
1619
1620 let passports = analyzer.get_memory_passports();
1621 assert_eq!(
1622 passports.len(),
1623 1,
1624 "Duplicate pointer should overwrite previous passport"
1625 );
1626
1627 let passport = passports.get(&0x1000).expect("Passport should exist");
1628 assert_eq!(
1629 passport.size_bytes, 2048,
1630 "Should have size from second creation"
1631 );
1632
1633 let stats = analyzer.get_stats();
1634 assert_eq!(
1635 stats.total_passports, 2,
1636 "Stats should count both creation attempts"
1637 );
1638 }
1639
1640 #[test]
1643 fn test_max_reports_exact_boundary() {
1644 let config = SafetyAnalysisConfig {
1645 max_reports: 3,
1646 ..Default::default()
1647 };
1648 let analyzer = SafetyAnalyzer::new(config);
1649 let source = UnsafeSource::UnsafeBlock {
1650 location: "test.rs".to_string(),
1651 function: "test".to_string(),
1652 file_path: None,
1653 line_number: None,
1654 };
1655
1656 for _ in 0..3 {
1657 analyzer
1658 .generate_unsafe_report(source.clone(), &[], &[])
1659 .unwrap();
1660 }
1661
1662 let reports = analyzer.get_unsafe_reports();
1663 assert_eq!(reports.len(), 3, "Should have exactly max_reports count");
1664
1665 analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1666
1667 let reports = analyzer.get_unsafe_reports();
1668 assert!(
1669 reports.len() <= 3,
1670 "Should not exceed max_reports after adding one more"
1671 );
1672 }
1673
1674 #[test]
1677 fn test_risk_level_filtering_comprehensive() {
1678 let test_cases = vec![
1679 (RiskLevel::Low, 4),
1680 (RiskLevel::Medium, 4),
1681 (RiskLevel::High, 2),
1682 (RiskLevel::Critical, 0),
1683 ];
1684
1685 for (min_level, expected_count) in test_cases {
1686 let config = SafetyAnalysisConfig {
1687 min_risk_level: min_level.clone(),
1688 detailed_risk_assessment: false,
1689 ..Default::default()
1690 };
1691 let analyzer = SafetyAnalyzer::new(config);
1692
1693 let sources = vec![
1694 UnsafeSource::UnsafeBlock {
1695 location: "test.rs".to_string(),
1696 function: "test".to_string(),
1697 file_path: None,
1698 line_number: None,
1699 },
1700 UnsafeSource::FfiFunction {
1701 library: "libc".to_string(),
1702 function: "malloc".to_string(),
1703 call_site: "test.rs".to_string(),
1704 },
1705 UnsafeSource::RawPointer {
1706 operation: "test".to_string(),
1707 location: "test.rs".to_string(),
1708 },
1709 UnsafeSource::Transmute {
1710 from_type: "u8".to_string(),
1711 to_type: "i8".to_string(),
1712 location: "test.rs".to_string(),
1713 },
1714 ];
1715
1716 for source in sources.into_iter() {
1717 analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1718 }
1719
1720 let reports = analyzer.get_unsafe_reports();
1721 let actual_count = reports.len();
1722
1723 assert_eq!(
1724 actual_count, expected_count,
1725 "For min_level {:?}, expected {} reports but got {}",
1726 min_level, expected_count, actual_count
1727 );
1728 }
1729 }
1730
1731 #[test]
1734 fn test_risk_assessment_no_matching_factors() {
1735 let config = SafetyAnalysisConfig {
1736 detailed_risk_assessment: true,
1737 min_risk_level: RiskLevel::Low,
1738 ..Default::default()
1739 };
1740 let analyzer = SafetyAnalyzer::new(config);
1741
1742 let source = UnsafeSource::UnsafeBlock {
1743 location: "safe_location.rs".to_string(),
1744 function: "safe_function".to_string(),
1745 file_path: None,
1746 line_number: None,
1747 };
1748
1749 let result = analyzer.generate_unsafe_report(source, &[], &[]);
1750 assert!(result.is_ok(), "Should generate report");
1751
1752 let reports = analyzer.get_unsafe_reports();
1753 assert_eq!(reports.len(), 1, "Should have one report");
1754
1755 let report = reports.values().next().expect("Report should exist");
1756 assert!(
1757 matches!(report.risk_assessment.risk_level, RiskLevel::Low),
1758 "Risk level should be Low when no risk factors match (empty risk factors indicate low risk)"
1759 );
1760 }
1761
1762 #[test]
1765 fn test_record_passport_event_empty_context() {
1766 let analyzer = SafetyAnalyzer::default();
1767
1768 analyzer
1769 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1770 .unwrap();
1771
1772 let result =
1773 analyzer.record_passport_event(0x1000, PassportEventType::HandoverToFfi, String::new());
1774 assert!(result.is_ok(), "Should record event with empty context");
1775
1776 let passports = analyzer.get_memory_passports();
1777 let passport = passports.get(&0x1000).expect("Passport should exist");
1778 assert_eq!(passport.lifecycle_events.len(), 2, "Should have two events");
1779 }
1780
1781 #[test]
1784 fn test_stats_consistency() {
1785 let analyzer = SafetyAnalyzer::default();
1786
1787 let initial_stats = analyzer.get_stats();
1788 assert_eq!(initial_stats.total_reports, 0);
1789 assert_eq!(initial_stats.total_passports, 0);
1790
1791 analyzer
1792 .generate_unsafe_report(
1793 UnsafeSource::UnsafeBlock {
1794 location: "test.rs".to_string(),
1795 function: "test".to_string(),
1796 file_path: None,
1797 line_number: None,
1798 },
1799 &[],
1800 &[],
1801 )
1802 .unwrap();
1803
1804 analyzer
1805 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1806 .unwrap();
1807
1808 let stats = analyzer.get_stats();
1809 assert_eq!(stats.total_reports, 1, "Should have 1 report");
1810 assert_eq!(stats.total_passports, 1, "Should have 1 passport");
1811 assert!(
1812 !stats.reports_by_risk_level.is_empty(),
1813 "Should have risk level breakdown"
1814 );
1815 }
1816
1817 #[test]
1820 fn test_passport_lifecycle_all_event_types() {
1821 let analyzer = SafetyAnalyzer::default();
1822
1823 analyzer
1824 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1825 .unwrap();
1826
1827 let event_types = vec![
1828 PassportEventType::HandoverToFfi,
1829 PassportEventType::BoundaryAccess,
1830 PassportEventType::OwnershipTransfer,
1831 PassportEventType::ReclaimedByRust,
1832 ];
1833
1834 for event_type in event_types {
1835 let event_type_str = format!("{:?}", event_type);
1836 let result = analyzer.record_passport_event(0x1000, event_type, "test".to_string());
1837 assert!(
1838 result.is_ok(),
1839 "Should record event type {}",
1840 event_type_str
1841 );
1842 }
1843
1844 let passports = analyzer.get_memory_passports();
1845 let passport = passports.get(&0x1000).expect("Passport should exist");
1846 assert_eq!(
1847 passport.lifecycle_events.len(),
1848 5,
1849 "Should have initial event plus 4 recorded events"
1850 );
1851 }
1852
1853 #[test]
1856 fn test_report_id_uniqueness() {
1857 let analyzer = SafetyAnalyzer::default();
1858 let source = UnsafeSource::UnsafeBlock {
1859 location: "test.rs".to_string(),
1860 function: "test".to_string(),
1861 file_path: None,
1862 line_number: None,
1863 };
1864
1865 let mut report_ids = std::collections::HashSet::new();
1866 for _ in 0..100 {
1867 let report_id = analyzer
1868 .generate_unsafe_report(source.clone(), &[], &[])
1869 .unwrap();
1870 assert!(report_ids.insert(report_id), "Report ID should be unique");
1871 }
1872
1873 assert_eq!(report_ids.len(), 100, "Should have 100 unique report IDs");
1874 }
1875
1876 #[test]
1879 fn test_finalize_passports_empty_state() {
1880 let analyzer = SafetyAnalyzer::default();
1881 let leaks = analyzer.finalize_passports_at_shutdown();
1882 assert!(leaks.is_empty(), "Should have no leaks with empty state");
1883
1884 let stats = analyzer.get_stats();
1885 assert!(
1886 stats.passports_by_status.is_empty(),
1887 "Should have no passport status stats"
1888 );
1889 }
1890
1891 #[test]
1894 fn test_should_generate_report_all_combinations() {
1895 let test_cases = vec![
1896 (RiskLevel::Low, RiskLevel::Low, true),
1897 (RiskLevel::Low, RiskLevel::Medium, true),
1898 (RiskLevel::Low, RiskLevel::High, true),
1899 (RiskLevel::Low, RiskLevel::Critical, true),
1900 (RiskLevel::Medium, RiskLevel::Low, false),
1901 (RiskLevel::Medium, RiskLevel::Medium, true),
1902 (RiskLevel::Medium, RiskLevel::High, true),
1903 (RiskLevel::Medium, RiskLevel::Critical, true),
1904 (RiskLevel::High, RiskLevel::Low, false),
1905 (RiskLevel::High, RiskLevel::Medium, false),
1906 (RiskLevel::High, RiskLevel::High, true),
1907 (RiskLevel::High, RiskLevel::Critical, true),
1908 (RiskLevel::Critical, RiskLevel::Low, false),
1909 (RiskLevel::Critical, RiskLevel::Medium, false),
1910 (RiskLevel::Critical, RiskLevel::High, false),
1911 (RiskLevel::Critical, RiskLevel::Critical, true),
1912 ];
1913
1914 for (min_level, report_level, expected) in test_cases {
1915 let config = SafetyAnalysisConfig {
1916 min_risk_level: min_level.clone(),
1917 ..Default::default()
1918 };
1919 let analyzer = SafetyAnalyzer::new(config);
1920 let result = analyzer.should_generate_report(&report_level);
1921 assert_eq!(
1922 result, expected,
1923 "should_generate_report({:?}, {:?}) should be {}",
1924 min_level, report_level, expected
1925 );
1926 }
1927 }
1928
1929 #[test]
1932 fn test_create_basic_risk_assessment_all_sources() {
1933 let config = SafetyAnalysisConfig {
1934 detailed_risk_assessment: false,
1935 ..Default::default()
1936 };
1937 let analyzer = SafetyAnalyzer::new(config);
1938
1939 let test_cases = vec![
1940 (
1941 UnsafeSource::UnsafeBlock {
1942 location: "test.rs".to_string(),
1943 function: "test".to_string(),
1944 file_path: None,
1945 line_number: None,
1946 },
1947 RiskLevel::Medium,
1948 50.0,
1949 ),
1950 (
1951 UnsafeSource::FfiFunction {
1952 library: "libc".to_string(),
1953 function: "malloc".to_string(),
1954 call_site: "test.rs".to_string(),
1955 },
1956 RiskLevel::Medium,
1957 45.0,
1958 ),
1959 (
1960 UnsafeSource::RawPointer {
1961 operation: "dereference".to_string(),
1962 location: "test.rs".to_string(),
1963 },
1964 RiskLevel::High,
1965 70.0,
1966 ),
1967 (
1968 UnsafeSource::Transmute {
1969 from_type: "u8".to_string(),
1970 to_type: "i8".to_string(),
1971 location: "test.rs".to_string(),
1972 },
1973 RiskLevel::High,
1974 65.0,
1975 ),
1976 ];
1977
1978 for (source, expected_level, expected_score) in test_cases {
1979 let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1980 let reports = analyzer.get_unsafe_reports();
1981 let report = reports.get(&report_id).expect("Report should exist");
1982
1983 assert_eq!(
1984 report.risk_assessment.risk_level, expected_level,
1985 "Risk level should match for source"
1986 );
1987 assert_eq!(
1988 report.risk_assessment.risk_score, expected_score,
1989 "Risk score should match for source"
1990 );
1991 }
1992 }
1993
1994 #[test]
1997 fn test_max_reports_overflow() {
1998 let config = SafetyAnalysisConfig {
1999 max_reports: 5,
2000 ..Default::default()
2001 };
2002 let analyzer = SafetyAnalyzer::new(config);
2003 let source = UnsafeSource::UnsafeBlock {
2004 location: "test.rs".to_string(),
2005 function: "test".to_string(),
2006 file_path: None,
2007 line_number: None,
2008 };
2009
2010 for i in 0..10 {
2011 let result = analyzer.generate_unsafe_report(source.clone(), &[], &[]);
2012 assert!(result.is_ok(), "Should generate report {}", i);
2013 }
2014
2015 let reports = analyzer.get_unsafe_reports();
2016 assert!(reports.len() <= 5, "Should not exceed max_reports limit");
2017 }
2018
2019 #[test]
2022 fn test_create_memory_passport_large_pointer() {
2023 let analyzer = SafetyAnalyzer::default();
2024 let large_ptr = usize::MAX;
2025 let result =
2026 analyzer.create_memory_passport(large_ptr, 1024, PassportEventType::AllocatedInRust);
2027 assert!(result.is_ok(), "Should create passport with large pointer");
2028
2029 let passports = analyzer.get_memory_passports();
2030 assert!(
2031 passports.contains_key(&large_ptr),
2032 "Passport with large pointer should exist"
2033 );
2034 }
2035
2036 #[test]
2039 fn test_create_memory_passport_large_size() {
2040 let analyzer = SafetyAnalyzer::default();
2041 let large_size = usize::MAX;
2042 let result =
2043 analyzer.create_memory_passport(0x1000, large_size, PassportEventType::AllocatedInRust);
2044 assert!(result.is_ok(), "Should create passport with large size");
2045
2046 let passports = analyzer.get_memory_passports();
2047 let passport = passports.get(&0x1000).expect("Passport should exist");
2048 assert_eq!(
2049 passport.size_bytes, large_size,
2050 "Passport should have large size"
2051 );
2052 }
2053
2054 #[test]
2057 fn test_passport_event_sequence() {
2058 let analyzer = SafetyAnalyzer::default();
2059
2060 analyzer
2061 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
2062 .unwrap();
2063
2064 let events = vec![
2065 PassportEventType::BoundaryAccess,
2066 PassportEventType::OwnershipTransfer,
2067 PassportEventType::HandoverToFfi,
2068 PassportEventType::BoundaryAccess,
2069 PassportEventType::FreedByForeign,
2070 ];
2071
2072 for event_type in events {
2073 analyzer
2074 .record_passport_event(0x1000, event_type, "test".to_string())
2075 .unwrap();
2076 }
2077
2078 let passports = analyzer.get_memory_passports();
2079 let passport = passports.get(&0x1000).expect("Passport should exist");
2080 assert_eq!(
2081 passport.lifecycle_events.len(),
2082 6,
2083 "Should have initial event plus 5 recorded events"
2084 );
2085 }
2086
2087 #[test]
2090 fn test_generate_report_id_format() {
2091 let analyzer = SafetyAnalyzer::default();
2092
2093 let sources = vec![
2094 (
2095 UnsafeSource::UnsafeBlock {
2096 location: "test.rs".to_string(),
2097 function: "test".to_string(),
2098 file_path: None,
2099 line_number: None,
2100 },
2101 "UNSAFE-UB-",
2102 ),
2103 (
2104 UnsafeSource::FfiFunction {
2105 library: "libc".to_string(),
2106 function: "malloc".to_string(),
2107 call_site: "test.rs".to_string(),
2108 },
2109 "UNSAFE-FFI-",
2110 ),
2111 (
2112 UnsafeSource::RawPointer {
2113 operation: "test".to_string(),
2114 location: "test.rs".to_string(),
2115 },
2116 "UNSAFE-PTR-",
2117 ),
2118 (
2119 UnsafeSource::Transmute {
2120 from_type: "u8".to_string(),
2121 to_type: "i8".to_string(),
2122 location: "test.rs".to_string(),
2123 },
2124 "UNSAFE-TX-",
2125 ),
2126 ];
2127
2128 for (source, expected_prefix) in sources {
2129 let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2130 assert!(
2131 report_id.starts_with(expected_prefix),
2132 "Report ID should start with {}",
2133 expected_prefix
2134 );
2135 }
2136 }
2137
2138 #[test]
2141 fn test_stats_by_risk_level() {
2142 let analyzer = SafetyAnalyzer::default();
2143
2144 let sources = vec![
2145 UnsafeSource::RawPointer {
2146 operation: "test".to_string(),
2147 location: "test.rs".to_string(),
2148 },
2149 UnsafeSource::Transmute {
2150 from_type: "u8".to_string(),
2151 to_type: "i8".to_string(),
2152 location: "test.rs".to_string(),
2153 },
2154 UnsafeSource::UnsafeBlock {
2155 location: "test.rs".to_string(),
2156 function: "test".to_string(),
2157 file_path: None,
2158 line_number: None,
2159 },
2160 ];
2161
2162 for source in sources {
2163 analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2164 }
2165
2166 let stats = analyzer.get_stats();
2167 assert_eq!(stats.total_reports, 3, "Should have 3 reports");
2168 assert!(
2169 stats.reports_by_risk_level.contains_key("Low"),
2170 "Should have Low risk level reports"
2171 );
2172 }
2173
2174 #[test]
2177 fn test_determine_final_passport_status_edge_cases() {
2178 let analyzer = SafetyAnalyzer::default();
2179
2180 let events_only_reclaim = vec![PassportEvent {
2181 event_type: PassportEventType::ReclaimedByRust,
2182 timestamp: 1000,
2183 context: "test".to_string(),
2184 call_stack: vec![],
2185 metadata: HashMap::new(),
2186 }];
2187 let status = analyzer.determine_final_passport_status(&events_only_reclaim);
2188 assert!(
2189 matches!(status, PassportStatus::ReclaimedByRust),
2190 "Only reclaim event should result in ReclaimedByRust"
2191 );
2192
2193 let events_only_foreign_free = vec![PassportEvent {
2194 event_type: PassportEventType::FreedByForeign,
2195 timestamp: 1000,
2196 context: "test".to_string(),
2197 call_stack: vec![],
2198 metadata: HashMap::new(),
2199 }];
2200 let status = analyzer.determine_final_passport_status(&events_only_foreign_free);
2201 assert!(
2202 matches!(status, PassportStatus::FreedByForeign),
2203 "Only foreign free event should result in FreedByForeign"
2204 );
2205 }
2206
2207 #[test]
2210 fn test_analyzer_all_features_disabled() {
2211 let config = SafetyAnalysisConfig {
2212 detailed_risk_assessment: false,
2213 enable_passport_tracking: false,
2214 min_risk_level: RiskLevel::Low,
2215 max_reports: 10,
2216 enable_dynamic_violations: false,
2217 ..Default::default()
2218 };
2219 let analyzer = SafetyAnalyzer::new(config);
2220
2221 let source = UnsafeSource::UnsafeBlock {
2222 location: "test.rs".to_string(),
2223 function: "test".to_string(),
2224 file_path: None,
2225 line_number: None,
2226 };
2227
2228 let result = analyzer.generate_unsafe_report(source, &[], &[]);
2229 assert!(
2230 result.is_ok(),
2231 "Should generate report with all features disabled"
2232 );
2233
2234 let passport_result =
2235 analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
2236 assert!(
2237 passport_result.is_ok(),
2238 "Should return Ok when passport tracking disabled"
2239 );
2240 assert!(
2241 passport_result.unwrap().is_empty(),
2242 "Should return empty string when passport tracking disabled"
2243 );
2244 }
2245
2246 #[test]
2249 fn test_report_source_preservation() {
2250 let analyzer = SafetyAnalyzer::default();
2251
2252 let source = UnsafeSource::UnsafeBlock {
2253 location: "src/test.rs:42".to_string(),
2254 function: "test_function".to_string(),
2255 file_path: Some("src/test.rs".to_string()),
2256 line_number: Some(42),
2257 };
2258
2259 let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2260 let reports = analyzer.get_unsafe_reports();
2261 let report = reports.get(&report_id).expect("Report should exist");
2262
2263 match &report.source {
2264 UnsafeSource::UnsafeBlock {
2265 location,
2266 function,
2267 file_path,
2268 line_number,
2269 } => {
2270 assert_eq!(location, "src/test.rs:42");
2271 assert_eq!(function, "test_function");
2272 assert_eq!(file_path, &Some("src/test.rs".to_string()));
2273 assert_eq!(line_number, &Some(42));
2274 }
2275 _ => panic!("Report source should be UnsafeBlock"),
2276 }
2277 }
2278
2279 #[test]
2282 fn test_strict_mutex_handling_mode() {
2283 let config = SafetyAnalysisConfig {
2284 strict_mutex_handling: true,
2285 ..Default::default()
2286 };
2287 let analyzer = SafetyAnalyzer::new(config);
2288
2289 let source = UnsafeSource::UnsafeBlock {
2290 location: "test.rs".to_string(),
2291 function: "test".to_string(),
2292 file_path: None,
2293 line_number: None,
2294 };
2295
2296 let result = analyzer.generate_unsafe_report(source, &[], &[]);
2297 assert!(
2298 result.is_ok(),
2299 "Should generate report successfully in normal case"
2300 );
2301
2302 let passport_result =
2303 analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
2304 assert!(
2305 passport_result.is_ok(),
2306 "Should create passport successfully in normal case"
2307 );
2308 }
2309
2310 #[test]
2313 fn test_lenient_mutex_handling_mode() {
2314 let config = SafetyAnalysisConfig {
2315 strict_mutex_handling: false,
2316 ..Default::default()
2317 };
2318 let analyzer = SafetyAnalyzer::new(config);
2319
2320 let source = UnsafeSource::UnsafeBlock {
2321 location: "test.rs".to_string(),
2322 function: "test".to_string(),
2323 file_path: None,
2324 line_number: None,
2325 };
2326
2327 let result = analyzer.generate_unsafe_report(source, &[], &[]);
2328 assert!(
2329 result.is_ok(),
2330 "Should generate report successfully in lenient mode"
2331 );
2332
2333 let passport_result =
2334 analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
2335 assert!(
2336 passport_result.is_ok(),
2337 "Should create passport successfully in lenient mode"
2338 );
2339 }
2340
2341 #[test]
2344 fn test_mutex_handling_config_option() {
2345 let strict_config = SafetyAnalysisConfig {
2346 strict_mutex_handling: true,
2347 ..Default::default()
2348 };
2349 let strict_analyzer = SafetyAnalyzer::new(strict_config);
2350 assert!(
2351 strict_analyzer.config.strict_mutex_handling,
2352 "Strict mode should be enabled"
2353 );
2354
2355 let lenient_config = SafetyAnalysisConfig {
2356 strict_mutex_handling: false,
2357 ..Default::default()
2358 };
2359 let lenient_analyzer = SafetyAnalyzer::new(lenient_config);
2360 assert!(
2361 !lenient_analyzer.config.strict_mutex_handling,
2362 "Strict mode should be disabled"
2363 );
2364 }
2365
2366 #[test]
2369 fn test_getter_methods_error_handling() {
2370 let analyzer = SafetyAnalyzer::default();
2371
2372 let reports = analyzer.get_unsafe_reports();
2373 assert!(reports.is_empty(), "Should return empty map on success");
2374
2375 let passports = analyzer.get_memory_passports();
2376 assert!(passports.is_empty(), "Should return empty map on success");
2377
2378 let stats = analyzer.get_stats();
2379 assert_eq!(
2380 stats.total_reports, 0,
2381 "Should return default stats on success"
2382 );
2383 }
2384
2385 #[test]
2388 fn test_convert_safety_violation_double_free() {
2389 use crate::core::CallStackRef;
2390
2391 let analyzer = SafetyAnalyzer::default();
2392
2393 let call_stack = CallStackRef::new(1, Some(1));
2394 let violations = vec![SafetyViolation::DoubleFree {
2395 first_free_stack: call_stack.clone(),
2396 second_free_stack: call_stack.clone(),
2397 timestamp: 1000,
2398 }];
2399
2400 let source = UnsafeSource::RawPointer {
2401 operation: "test".to_string(),
2402 location: "test.rs".to_string(),
2403 };
2404
2405 let report_id = analyzer
2406 .generate_unsafe_report(source, &[], &violations)
2407 .unwrap();
2408 let reports = analyzer.get_unsafe_reports();
2409 let report = reports.get(&report_id).expect("Report should exist");
2410
2411 assert_eq!(
2412 report.dynamic_violations.len(),
2413 1,
2414 "Should have one dynamic violation"
2415 );
2416 let dv = &report.dynamic_violations[0];
2417 assert!(
2418 matches!(dv.violation_type, ViolationType::DoubleFree),
2419 "Violation type should be DoubleFree"
2420 );
2421 assert!(
2422 matches!(dv.severity, RiskLevel::Critical),
2423 "DoubleFree should have Critical severity"
2424 );
2425 }
2426
2427 #[test]
2430 fn test_convert_safety_violation_invalid_free() {
2431 use crate::core::CallStackRef;
2432
2433 let analyzer = SafetyAnalyzer::default();
2434
2435 let call_stack = CallStackRef::new(2, Some(1));
2436 let violations = vec![SafetyViolation::InvalidFree {
2437 attempted_pointer: 0x2000,
2438 stack: call_stack,
2439 timestamp: 2000,
2440 }];
2441
2442 let source = UnsafeSource::RawPointer {
2443 operation: "test".to_string(),
2444 location: "test.rs".to_string(),
2445 };
2446
2447 let report_id = analyzer
2448 .generate_unsafe_report(source, &[], &violations)
2449 .unwrap();
2450 let reports = analyzer.get_unsafe_reports();
2451 let report = reports.get(&report_id).expect("Report should exist");
2452
2453 let dv = &report.dynamic_violations[0];
2454 assert!(
2455 matches!(dv.violation_type, ViolationType::InvalidAccess),
2456 "Violation type should be InvalidAccess"
2457 );
2458 assert_eq!(
2459 dv.memory_address, 0x2000,
2460 "Memory address should match attempted pointer"
2461 );
2462 assert!(
2463 matches!(dv.severity, RiskLevel::High),
2464 "InvalidFree should have High severity"
2465 );
2466 }
2467
2468 #[test]
2471 fn test_convert_safety_violation_potential_leak() {
2472 use crate::core::CallStackRef;
2473
2474 let analyzer = SafetyAnalyzer::default();
2475
2476 let call_stack = CallStackRef::new(3, Some(1));
2477 let violations = vec![SafetyViolation::PotentialLeak {
2478 allocation_stack: call_stack,
2479 allocation_timestamp: 1000,
2480 leak_detection_timestamp: 5000,
2481 }];
2482
2483 let source = UnsafeSource::RawPointer {
2484 operation: "test".to_string(),
2485 location: "test.rs".to_string(),
2486 };
2487
2488 let report_id = analyzer
2489 .generate_unsafe_report(source, &[], &violations)
2490 .unwrap();
2491 let reports = analyzer.get_unsafe_reports();
2492 let report = reports.get(&report_id).expect("Report should exist");
2493
2494 let dv = &report.dynamic_violations[0];
2495 assert!(
2496 matches!(dv.violation_type, ViolationType::MemoryLeak),
2497 "Violation type should be MemoryLeak"
2498 );
2499 assert!(
2500 matches!(dv.severity, RiskLevel::Medium),
2501 "PotentialLeak should have Medium severity"
2502 );
2503 assert_eq!(
2504 dv.detected_at, 5000,
2505 "Detected at should match leak_detection_timestamp"
2506 );
2507 }
2508
2509 #[test]
2512 fn test_convert_safety_violation_cross_boundary() {
2513 use crate::core::CallStackRef;
2514
2515 let analyzer = SafetyAnalyzer::default();
2516
2517 let call_stack = CallStackRef::new(4, Some(1));
2518 let violations = vec![SafetyViolation::CrossBoundaryRisk {
2519 risk_level: RiskLevel::High,
2520 description: "FFI boundary violation".to_string(),
2521 stack: call_stack,
2522 }];
2523
2524 let source = UnsafeSource::FfiFunction {
2525 library: "libc".to_string(),
2526 function: "malloc".to_string(),
2527 call_site: "test.rs".to_string(),
2528 };
2529
2530 let report_id = analyzer
2531 .generate_unsafe_report(source, &[], &violations)
2532 .unwrap();
2533 let reports = analyzer.get_unsafe_reports();
2534 let report = reports.get(&report_id).expect("Report should exist");
2535
2536 let dv = &report.dynamic_violations[0];
2537 assert!(
2538 matches!(dv.violation_type, ViolationType::FfiBoundaryViolation),
2539 "Violation type should be FfiBoundaryViolation"
2540 );
2541 assert!(
2542 matches!(dv.severity, RiskLevel::High),
2543 "Severity should match original risk level"
2544 );
2545 assert_eq!(
2546 dv.context, "FFI boundary violation",
2547 "Context should match description"
2548 );
2549 }
2550
2551 #[test]
2554 fn test_convert_multiple_safety_violations() {
2555 use crate::core::CallStackRef;
2556
2557 let analyzer = SafetyAnalyzer::default();
2558
2559 let call_stack = CallStackRef::new(5, Some(1));
2560 let violations = vec![
2561 SafetyViolation::DoubleFree {
2562 first_free_stack: call_stack.clone(),
2563 second_free_stack: call_stack.clone(),
2564 timestamp: 1000,
2565 },
2566 SafetyViolation::InvalidFree {
2567 attempted_pointer: 0x2000,
2568 stack: call_stack.clone(),
2569 timestamp: 2000,
2570 },
2571 SafetyViolation::PotentialLeak {
2572 allocation_stack: call_stack,
2573 allocation_timestamp: 1000,
2574 leak_detection_timestamp: 5000,
2575 },
2576 ];
2577
2578 let source = UnsafeSource::UnsafeBlock {
2579 location: "test.rs".to_string(),
2580 function: "test".to_string(),
2581 file_path: None,
2582 line_number: None,
2583 };
2584
2585 let report_id = analyzer
2586 .generate_unsafe_report(source, &[], &violations)
2587 .unwrap();
2588 let reports = analyzer.get_unsafe_reports();
2589 let report = reports.get(&report_id).expect("Report should exist");
2590
2591 assert_eq!(
2592 report.dynamic_violations.len(),
2593 3,
2594 "Should have three dynamic violations"
2595 );
2596 }
2597
2598 #[test]
2601 fn test_create_memory_context_empty() {
2602 let analyzer = SafetyAnalyzer::default();
2603
2604 let source = UnsafeSource::RawPointer {
2605 operation: "test".to_string(),
2606 location: "test.rs".to_string(),
2607 };
2608
2609 let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2610 let reports = analyzer.get_unsafe_reports();
2611 let report = reports.get(&report_id).expect("Report should exist");
2612
2613 assert_eq!(
2614 report.memory_context.total_allocated, 0,
2615 "Total allocated should be 0 for empty allocations"
2616 );
2617 assert_eq!(
2618 report.memory_context.active_allocations, 0,
2619 "Active allocations should be 0 for empty allocations"
2620 );
2621 }
2622
2623 #[test]
2626 fn test_circuit_breaker_trip_threshold() {
2627 let mut breaker = CircuitBreaker::default();
2628
2629 assert!(!breaker.is_tripped(), "Should not be tripped initially");
2630
2631 breaker.record_poison(3);
2632 assert_eq!(breaker.poison_count(), 1);
2633 assert!(!breaker.is_tripped(), "Should not trip after 1 event");
2634
2635 breaker.record_poison(3);
2636 assert_eq!(breaker.poison_count(), 2);
2637 assert!(!breaker.is_tripped(), "Should not trip after 2 events");
2638
2639 breaker.record_poison(3);
2640 assert_eq!(breaker.poison_count(), 3);
2641 assert!(
2642 breaker.is_tripped(),
2643 "Should trip after reaching max_retries"
2644 );
2645 }
2646
2647 #[test]
2650 fn test_circuit_breaker_reset() {
2651 let mut breaker = CircuitBreaker::default();
2652
2653 breaker.record_poison(3);
2654 breaker.record_poison(3);
2655 breaker.record_poison(3);
2656
2657 assert!(breaker.is_tripped(), "Should be tripped");
2658
2659 breaker.reset();
2660
2661 assert!(!breaker.is_tripped(), "Should not be tripped after reset");
2662 assert_eq!(breaker.poison_count(), 0, "Poison count should be 0");
2663 assert!(
2664 breaker.last_poison_time().is_none(),
2665 "Last poison time should be None"
2666 );
2667 }
2668
2669 #[test]
2672 fn test_circuit_breaker_different_thresholds() {
2673 let mut breaker1 = CircuitBreaker::default();
2674 breaker1.record_poison(1);
2675 assert!(
2676 breaker1.is_tripped(),
2677 "Should trip immediately with max_retries=1"
2678 );
2679
2680 let mut breaker5 = CircuitBreaker::default();
2681 for _ in 0..4 {
2682 breaker5.record_poison(5);
2683 }
2684 assert!(
2685 !breaker5.is_tripped(),
2686 "Should not trip before reaching threshold"
2687 );
2688 breaker5.record_poison(5);
2689 assert!(breaker5.is_tripped(), "Should trip at exactly max_retries");
2690 }
2691
2692 #[test]
2695 fn test_get_current_timestamp() {
2696 let ts = get_current_timestamp();
2697 assert!(ts > 0, "Timestamp should be positive");
2698 assert!(
2699 ts > 1700000000,
2700 "Timestamp should be after 2023 (reasonable value)"
2701 );
2702 }
2703
2704 #[test]
2707 fn test_get_current_timestamp_nanos() {
2708 let ts_nanos = get_current_timestamp_nanos();
2709 let ts_secs = get_current_timestamp();
2710
2711 assert!(ts_nanos > 0, "Nanos timestamp should be positive");
2712 assert!(
2713 ts_nanos >= ts_secs as u128,
2714 "Nanos should be >= seconds timestamp"
2715 );
2716 }
2717
2718 #[test]
2721 fn test_record_passport_event_tracking_disabled() {
2722 let config = SafetyAnalysisConfig {
2723 enable_passport_tracking: false,
2724 ..Default::default()
2725 };
2726 let analyzer = SafetyAnalyzer::new(config);
2727
2728 let result = analyzer.record_passport_event(
2729 0x1000,
2730 PassportEventType::HandoverToFfi,
2731 "test".to_string(),
2732 );
2733
2734 assert!(result.is_ok(), "Should return Ok when tracking disabled");
2735 }
2736
2737 #[test]
2740 fn test_generate_report_with_passport_tracking() {
2741 let analyzer = SafetyAnalyzer::default();
2742
2743 analyzer
2744 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
2745 .unwrap();
2746
2747 let source = UnsafeSource::RawPointer {
2748 operation: "test".to_string(),
2749 location: "test.rs".to_string(),
2750 };
2751
2752 let result = analyzer.generate_unsafe_report(source, &[], &[]);
2753 assert!(
2754 result.is_ok(),
2755 "Should generate report with passport tracking"
2756 );
2757 }
2758
2759 #[test]
2762 fn test_safety_analysis_stats_serialization() {
2763 let stats = SafetyAnalysisStats {
2764 total_reports: 10,
2765 reports_by_risk_level: HashMap::from([("Low".to_string(), 5), ("High".to_string(), 5)]),
2766 total_passports: 3,
2767 passports_by_status: HashMap::from([("Active".to_string(), 3)]),
2768 dynamic_violations: 2,
2769 analysis_start_time: 1000,
2770 };
2771
2772 let json = serde_json::to_string(&stats).expect("Should serialize");
2773 let deserialized: SafetyAnalysisStats =
2774 serde_json::from_str(&json).expect("Should deserialize");
2775
2776 assert_eq!(deserialized.total_reports, 10, "Total reports should match");
2777 assert_eq!(
2778 deserialized.total_passports, 3,
2779 "Total passports should match"
2780 );
2781 }
2782
2783 #[test]
2786 fn test_safety_config_clone() {
2787 let config = SafetyAnalysisConfig {
2788 detailed_risk_assessment: false,
2789 enable_passport_tracking: false,
2790 min_risk_level: RiskLevel::High,
2791 max_reports: 500,
2792 enable_dynamic_violations: false,
2793 strict_mutex_handling: true,
2794 max_mutex_poison_retries: 5,
2795 };
2796
2797 let cloned = config.clone();
2798
2799 assert!(
2800 !cloned.detailed_risk_assessment,
2801 "Cloned detailed_risk_assessment should match"
2802 );
2803 assert!(
2804 !cloned.enable_passport_tracking,
2805 "Cloned enable_passport_tracking should match"
2806 );
2807 assert_eq!(cloned.max_reports, 500, "Cloned max_reports should match");
2808 assert_eq!(
2809 cloned.max_mutex_poison_retries, 5,
2810 "Cloned max_mutex_poison_retries should match"
2811 );
2812 }
2813
2814 #[test]
2817 fn test_finalize_passports_graceful_degradation() {
2818 let analyzer = SafetyAnalyzer::default();
2819
2820 analyzer
2821 .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
2822 .unwrap();
2823
2824 let leaks = analyzer.finalize_passports_at_shutdown();
2825 assert!(
2826 leaks.is_empty(),
2827 "Should have no leaks when passport is not in foreign custody"
2828 );
2829 }
2830}