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