memscope_rs/analysis/
memory_passport_tracker.rs

1//! Memory Passport Tracking System for FFI Boundary Memory Management
2//!
3//! This module implements comprehensive memory passport tracking for memory that crosses
4//! FFI boundaries, including lifecycle event recording and leak detection at shutdown.
5
6use crate::analysis::unsafe_ffi_tracker::StackFrame;
7use crate::core::types::{TrackingError, TrackingResult};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13/// Memory passport for tracking cross-FFI boundary memory
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct MemoryPassport {
16    /// Unique passport identifier
17    pub passport_id: String,
18    /// Memory allocation pointer
19    pub allocation_ptr: usize,
20    /// Size in bytes
21    pub size_bytes: usize,
22    /// Current status at program shutdown
23    pub status_at_shutdown: PassportStatus,
24    /// Complete lifecycle events recorded
25    pub lifecycle_events: Vec<PassportEvent>,
26    /// Creation timestamp
27    pub created_at: u64,
28    /// Last update timestamp
29    pub updated_at: u64,
30    /// Additional metadata
31    pub metadata: HashMap<String, String>,
32}
33
34/// Status of memory passport at program shutdown
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub enum PassportStatus {
37    /// Memory properly freed by Rust
38    FreedByRust,
39    /// Memory handed over to FFI and not returned
40    HandoverToFfi,
41    /// Memory freed by foreign code
42    FreedByForeign,
43    /// Memory reclaimed by Rust from FFI
44    ReclaimedByRust,
45    /// Memory still in foreign custody (confirmed leak)
46    InForeignCustody,
47    /// Status unknown or corrupted
48    Unknown,
49}
50
51/// Lifecycle event in memory passport
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct PassportEvent {
54    /// Event type
55    pub event_type: PassportEventType,
56    /// Timestamp of event
57    pub timestamp: u64,
58    /// Context where event occurred
59    pub context: String,
60    /// Call stack at event time
61    pub call_stack: Vec<StackFrame>,
62    /// Additional event metadata
63    pub metadata: HashMap<String, String>,
64    /// Event sequence number
65    pub sequence_number: u32,
66}
67
68/// Types of passport events for lifecycle tracking
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub enum PassportEventType {
71    /// Memory allocated in Rust
72    AllocatedInRust,
73    /// Memory handed over to FFI (HandoverToFfi)
74    HandoverToFfi,
75    /// Memory freed by foreign code (FreedByForeign)
76    FreedByForeign,
77    /// Memory reclaimed by Rust (ReclaimedByRust)
78    ReclaimedByRust,
79    /// Memory accessed across boundary
80    BoundaryAccess,
81    /// Memory ownership transferred
82    OwnershipTransfer,
83    /// Memory validation check
84    ValidationCheck,
85    /// Memory corruption detected
86    CorruptionDetected,
87}
88
89/// Memory passport tracker for comprehensive FFI boundary tracking
90pub struct MemoryPassportTracker {
91    /// Active memory passports
92    passports: Arc<Mutex<HashMap<usize, MemoryPassport>>>,
93    /// Passport creation sequence
94    sequence_counter: Arc<Mutex<u32>>,
95    /// Event sequence counter
96    event_sequence: Arc<Mutex<u32>>,
97    /// Tracker configuration
98    config: PassportTrackerConfig,
99    /// Statistics tracking
100    stats: Arc<Mutex<PassportTrackerStats>>,
101}
102
103/// Configuration for passport tracker
104#[derive(Debug, Clone)]
105pub struct PassportTrackerConfig {
106    /// Enable detailed event logging
107    pub detailed_logging: bool,
108    /// Maximum number of events per passport
109    pub max_events_per_passport: usize,
110    /// Enable automatic leak detection
111    pub enable_leak_detection: bool,
112    /// Enable passport validation
113    pub enable_validation: bool,
114    /// Maximum number of passports to track
115    pub max_passports: usize,
116}
117
118impl Default for PassportTrackerConfig {
119    fn default() -> Self {
120        Self {
121            detailed_logging: true,
122            max_events_per_passport: 100,
123            enable_leak_detection: true,
124            enable_validation: true,
125            max_passports: 10000,
126        }
127    }
128}
129
130/// Statistics for passport tracker
131#[derive(Debug, Clone, Default, Serialize, Deserialize)]
132pub struct PassportTrackerStats {
133    /// Total passports created
134    pub total_passports_created: usize,
135    /// Active passports
136    pub active_passports: usize,
137    /// Passports by status
138    pub passports_by_status: HashMap<String, usize>,
139    /// Total events recorded
140    pub total_events_recorded: usize,
141    /// Leaks detected at shutdown
142    pub leaks_detected: usize,
143    /// Validation failures
144    pub validation_failures: usize,
145    /// Tracker start time
146    pub tracker_start_time: u64,
147}
148
149/// Leak detection result
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct LeakDetectionResult {
152    /// Leaked passport IDs
153    pub leaked_passports: Vec<String>,
154    /// Total leaks detected
155    pub total_leaks: usize,
156    /// Leak details
157    pub leak_details: Vec<LeakDetail>,
158    /// Detection timestamp
159    pub detected_at: u64,
160}
161
162/// Details of a detected leak
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct LeakDetail {
165    /// Passport ID
166    pub passport_id: String,
167    /// Memory address
168    pub memory_address: usize,
169    /// Size of leaked memory
170    pub size_bytes: usize,
171    /// Last known context
172    pub last_context: String,
173    /// Time since last event
174    pub time_since_last_event: u64,
175    /// Lifecycle summary
176    pub lifecycle_summary: String,
177}
178
179impl MemoryPassportTracker {
180    /// Create new memory passport tracker
181    pub fn new(config: PassportTrackerConfig) -> Self {
182        tracing::info!("๐Ÿ“‹ Initializing Memory Passport Tracker");
183        tracing::info!("   โ€ข Detailed logging: {}", config.detailed_logging);
184        tracing::info!(
185            "   โ€ข Max events per passport: {}",
186            config.max_events_per_passport
187        );
188        tracing::info!("   โ€ข Leak detection: {}", config.enable_leak_detection);
189        tracing::info!("   โ€ข Validation: {}", config.enable_validation);
190
191        Self {
192            passports: Arc::new(Mutex::new(HashMap::new())),
193            sequence_counter: Arc::new(Mutex::new(0)),
194            event_sequence: Arc::new(Mutex::new(0)),
195            config,
196            stats: Arc::new(Mutex::new(PassportTrackerStats {
197                tracker_start_time: SystemTime::now()
198                    .duration_since(UNIX_EPOCH)
199                    .unwrap_or_default()
200                    .as_secs(),
201                ..Default::default()
202            })),
203        }
204    }
205
206    /// Create memory passport for FFI boundary tracking
207    pub fn create_passport(
208        &self,
209        allocation_ptr: usize,
210        size_bytes: usize,
211        initial_context: String,
212    ) -> TrackingResult<String> {
213        let passport_id = self.generate_passport_id(allocation_ptr)?;
214        let current_time = SystemTime::now()
215            .duration_since(UNIX_EPOCH)
216            .unwrap_or_default()
217            .as_secs();
218
219        // Create initial event
220        let initial_event = PassportEvent {
221            event_type: PassportEventType::AllocatedInRust,
222            timestamp: current_time,
223            context: initial_context,
224            call_stack: self.capture_call_stack()?,
225            metadata: HashMap::new(),
226            sequence_number: self.get_next_event_sequence(),
227        };
228
229        let passport = MemoryPassport {
230            passport_id: passport_id.clone(),
231            allocation_ptr,
232            size_bytes,
233            status_at_shutdown: PassportStatus::Unknown,
234            lifecycle_events: vec![initial_event],
235            created_at: current_time,
236            updated_at: current_time,
237            metadata: HashMap::new(),
238        };
239
240        // Store passport
241        if let Ok(mut passports) = self.passports.lock() {
242            // Check passport limit
243            if passports.len() >= self.config.max_passports {
244                return Err(TrackingError::ResourceExhausted(
245                    "Maximum passport limit reached".to_string(),
246                ));
247            }
248            passports.insert(allocation_ptr, passport);
249        } else {
250            return Err(TrackingError::LockContention(
251                "Failed to lock passports".to_string(),
252            ));
253        }
254
255        // Update statistics
256        self.update_stats_passport_created();
257
258        if self.config.detailed_logging {
259            tracing::info!(
260                "๐Ÿ“‹ Created passport: {} for 0x{:x} ({} bytes)",
261                passport_id,
262                allocation_ptr,
263                size_bytes
264            );
265        }
266
267        Ok(passport_id)
268    }
269
270    /// Record HandoverToFfi event
271    pub fn record_handover_to_ffi(
272        &self,
273        allocation_ptr: usize,
274        ffi_context: String,
275        function_name: String,
276    ) -> TrackingResult<()> {
277        let mut metadata = HashMap::new();
278        metadata.insert("ffi_function".to_string(), function_name);
279
280        self.record_passport_event(
281            allocation_ptr,
282            PassportEventType::HandoverToFfi,
283            ffi_context,
284            metadata,
285        )
286    }
287
288    /// Record FreedByForeign event
289    pub fn record_freed_by_foreign(
290        &self,
291        allocation_ptr: usize,
292        foreign_context: String,
293        free_function: String,
294    ) -> TrackingResult<()> {
295        let mut metadata = HashMap::new();
296        metadata.insert("free_function".to_string(), free_function);
297
298        self.record_passport_event(
299            allocation_ptr,
300            PassportEventType::FreedByForeign,
301            foreign_context,
302            metadata,
303        )
304    }
305
306    /// Record ReclaimedByRust event
307    pub fn record_reclaimed_by_rust(
308        &self,
309        allocation_ptr: usize,
310        rust_context: String,
311        reclaim_reason: String,
312    ) -> TrackingResult<()> {
313        let mut metadata = HashMap::new();
314        metadata.insert("reclaim_reason".to_string(), reclaim_reason);
315
316        self.record_passport_event(
317            allocation_ptr,
318            PassportEventType::ReclaimedByRust,
319            rust_context,
320            metadata,
321        )
322    }
323
324    /// Record general passport event
325    pub fn record_passport_event(
326        &self,
327        allocation_ptr: usize,
328        event_type: PassportEventType,
329        context: String,
330        metadata: HashMap<String, String>,
331    ) -> TrackingResult<()> {
332        let current_time = SystemTime::now()
333            .duration_since(UNIX_EPOCH)
334            .unwrap_or_default()
335            .as_secs();
336
337        let event = PassportEvent {
338            event_type: event_type.clone(),
339            timestamp: current_time,
340            context: context.clone(),
341            call_stack: self.capture_call_stack()?,
342            metadata,
343            sequence_number: self.get_next_event_sequence(),
344        };
345
346        if let Ok(mut passports) = self.passports.lock() {
347            if let Some(passport) = passports.get_mut(&allocation_ptr) {
348                // Limit events per passport
349                if passport.lifecycle_events.len() >= self.config.max_events_per_passport {
350                    passport.lifecycle_events.remove(0); // Remove oldest event
351                }
352
353                passport.lifecycle_events.push(event);
354                passport.updated_at = current_time;
355
356                // Update statistics
357                self.update_stats_event_recorded();
358
359                if self.config.detailed_logging {
360                    tracing::info!(
361                        "๐Ÿ“ Recorded {:?} event for passport 0x{:x} in context: {}",
362                        event_type,
363                        allocation_ptr,
364                        context
365                    );
366                }
367            } else {
368                return Err(TrackingError::InvalidPointer(format!(
369                    "No passport found for 0x{allocation_ptr:x}"
370                )));
371            }
372        } else {
373            return Err(TrackingError::LockContention(
374                "Failed to lock passports".to_string(),
375            ));
376        }
377
378        Ok(())
379    }
380
381    /// Detect InForeignCustody status and memory leaks at program shutdown
382    pub fn detect_leaks_at_shutdown(&self) -> LeakDetectionResult {
383        if !self.config.enable_leak_detection {
384            return LeakDetectionResult {
385                leaked_passports: Vec::new(),
386                total_leaks: 0,
387                leak_details: Vec::new(),
388                detected_at: SystemTime::now()
389                    .duration_since(UNIX_EPOCH)
390                    .unwrap_or_default()
391                    .as_secs(),
392            };
393        }
394
395        let mut leaked_passports = Vec::new();
396        let mut leak_details = Vec::new();
397        let current_time = SystemTime::now()
398            .duration_since(UNIX_EPOCH)
399            .unwrap_or_default()
400            .as_secs();
401
402        if let Ok(mut passports) = self.passports.lock() {
403            for (ptr, passport) in passports.iter_mut() {
404                // Determine final status based on lifecycle events
405                let final_status = self.determine_final_status(&passport.lifecycle_events);
406                passport.status_at_shutdown = final_status.clone();
407
408                // Check for leaks (InForeignCustody status)
409                if final_status == PassportStatus::InForeignCustody {
410                    leaked_passports.push(passport.passport_id.clone());
411
412                    // Create leak detail
413                    let last_event = passport.lifecycle_events.last();
414                    let last_context = last_event
415                        .map(|e| e.context.clone())
416                        .unwrap_or_else(|| "unknown".to_string());
417                    let time_since_last = last_event
418                        .map(|e| current_time.saturating_sub(e.timestamp))
419                        .unwrap_or(0);
420
421                    let lifecycle_summary =
422                        self.create_lifecycle_summary(&passport.lifecycle_events);
423
424                    leak_details.push(LeakDetail {
425                        passport_id: passport.passport_id.clone(),
426                        memory_address: *ptr,
427                        size_bytes: passport.size_bytes,
428                        last_context,
429                        time_since_last_event: time_since_last,
430                        lifecycle_summary,
431                    });
432
433                    tracing::warn!("๐Ÿšจ MEMORY LEAK DETECTED: Passport {} (0x{:x}, {} bytes) in foreign custody", 
434                        passport.passport_id, ptr, passport.size_bytes);
435                }
436            }
437
438            // Update statistics
439            if let Ok(mut stats) = self.stats.lock() {
440                stats.leaks_detected = leaked_passports.len();
441
442                // Update status counts
443                stats.passports_by_status.clear();
444                for passport in passports.values() {
445                    let status_key = format!("{:?}", passport.status_at_shutdown);
446                    *stats.passports_by_status.entry(status_key).or_insert(0) += 1;
447                }
448            }
449        }
450
451        let total_leaks = leaked_passports.len();
452
453        tracing::info!(
454            "๐Ÿ” Leak detection complete: {} leaks detected out of {} passports",
455            total_leaks,
456            self.get_passport_count()
457        );
458
459        LeakDetectionResult {
460            leaked_passports,
461            total_leaks,
462            leak_details,
463            detected_at: current_time,
464        }
465    }
466
467    /// Get passport by allocation pointer
468    pub fn get_passport(&self, allocation_ptr: usize) -> Option<MemoryPassport> {
469        self.passports.lock().ok()?.get(&allocation_ptr).cloned()
470    }
471
472    /// Get all passports
473    pub fn get_all_passports(&self) -> HashMap<usize, MemoryPassport> {
474        self.passports
475            .lock()
476            .unwrap_or_else(|_| {
477                tracing::error!("Failed to lock passports for reading");
478                std::process::exit(1);
479            })
480            .clone()
481    }
482
483    /// Get passports by status
484    pub fn get_passports_by_status(&self, status: PassportStatus) -> Vec<MemoryPassport> {
485        if let Ok(passports) = self.passports.lock() {
486            passports
487                .values()
488                .filter(|p| p.status_at_shutdown == status)
489                .cloned()
490                .collect()
491        } else {
492            Vec::new()
493        }
494    }
495
496    /// Get tracker statistics
497    pub fn get_stats(&self) -> PassportTrackerStats {
498        self.stats
499            .lock()
500            .unwrap_or_else(|_| {
501                tracing::error!("Failed to lock stats");
502                std::process::exit(1);
503            })
504            .clone()
505    }
506
507    /// Validate passport integrity
508    pub fn validate_passport(&self, allocation_ptr: usize) -> TrackingResult<bool> {
509        if !self.config.enable_validation {
510            return Ok(true);
511        }
512
513        if let Ok(passports) = self.passports.lock() {
514            if let Some(passport) = passports.get(&allocation_ptr) {
515                // Validate event sequence
516                let mut last_sequence = 0;
517                for event in &passport.lifecycle_events {
518                    if event.sequence_number <= last_sequence {
519                        self.update_stats_validation_failure();
520                        return Ok(false);
521                    }
522                    last_sequence = event.sequence_number;
523                }
524
525                // Validate timestamps
526                let mut last_timestamp = 0;
527                for event in &passport.lifecycle_events {
528                    if event.timestamp < last_timestamp {
529                        self.update_stats_validation_failure();
530                        return Ok(false);
531                    }
532                    last_timestamp = event.timestamp;
533                }
534
535                Ok(true)
536            } else {
537                Err(TrackingError::InvalidPointer(format!(
538                    "No passport found for 0x{allocation_ptr:x}",
539                )))
540            }
541        } else {
542            Err(TrackingError::LockContention(
543                "Failed to lock passports".to_string(),
544            ))
545        }
546    }
547
548    /// Clear all passports (for testing)
549    pub fn clear_all_passports(&self) {
550        if let Ok(mut passports) = self.passports.lock() {
551            passports.clear();
552        }
553
554        if let Ok(mut stats) = self.stats.lock() {
555            *stats = PassportTrackerStats {
556                tracker_start_time: stats.tracker_start_time,
557                ..Default::default()
558            };
559        }
560
561        tracing::info!("๐Ÿงน Cleared all passports");
562    }
563
564    // Private helper methods
565
566    fn generate_passport_id(&self, allocation_ptr: usize) -> TrackingResult<String> {
567        let sequence = if let Ok(mut counter) = self.sequence_counter.lock() {
568            *counter += 1;
569            *counter
570        } else {
571            return Err(TrackingError::LockContention(
572                "Failed to lock sequence counter".to_string(),
573            ));
574        };
575
576        let timestamp = SystemTime::now()
577            .duration_since(UNIX_EPOCH)
578            .unwrap_or_default()
579            .as_nanos();
580
581        Ok(format!(
582            "passport_{:x}_{:08x}_{}",
583            allocation_ptr,
584            sequence,
585            timestamp % 1000000
586        ))
587    }
588
589    fn get_next_event_sequence(&self) -> u32 {
590        if let Ok(mut counter) = self.event_sequence.lock() {
591            *counter += 1;
592            *counter
593        } else {
594            tracing::error!("Failed to lock event sequence counter");
595            0
596        }
597    }
598
599    fn capture_call_stack(&self) -> TrackingResult<Vec<StackFrame>> {
600        // Simplified call stack capture
601        // In a real implementation, this would use backtrace or similar
602        Ok(vec![StackFrame {
603            function_name: "memory_passport_tracker".to_string(),
604            file_name: Some("src/analysis/memory_passport_tracker.rs".to_string()),
605            line_number: Some(1),
606            is_unsafe: false,
607        }])
608    }
609
610    fn determine_final_status(&self, events: &[PassportEvent]) -> PassportStatus {
611        let mut has_handover = false;
612        let mut has_reclaim = false;
613        let mut has_foreign_free = false;
614
615        // Analyze events in chronological order
616        for event in events {
617            match event.event_type {
618                PassportEventType::HandoverToFfi => has_handover = true,
619                PassportEventType::ReclaimedByRust => {
620                    has_reclaim = true;
621                    has_handover = false; // Reset handover status
622                }
623                PassportEventType::FreedByForeign => {
624                    has_foreign_free = true;
625                    has_handover = false; // Reset handover status
626                }
627                _ => {}
628            }
629        }
630
631        // Determine final status based on event history
632        if has_handover && !has_reclaim && !has_foreign_free {
633            PassportStatus::InForeignCustody // This is a confirmed leak
634        } else if has_foreign_free {
635            PassportStatus::FreedByForeign
636        } else if has_reclaim {
637            PassportStatus::ReclaimedByRust
638        } else if has_handover {
639            PassportStatus::HandoverToFfi
640        } else {
641            PassportStatus::FreedByRust
642        }
643    }
644
645    fn create_lifecycle_summary(&self, events: &[PassportEvent]) -> String {
646        let event_types: Vec<String> = events
647            .iter()
648            .map(|e| format!("{:?}", e.event_type))
649            .collect();
650
651        if event_types.is_empty() {
652            "No events recorded".to_string()
653        } else {
654            format!("Events: {}", event_types.join(" -> "))
655        }
656    }
657
658    fn get_passport_count(&self) -> usize {
659        self.passports.lock().map(|p| p.len()).unwrap_or(0)
660    }
661
662    fn update_stats_passport_created(&self) {
663        if let Ok(mut stats) = self.stats.lock() {
664            stats.total_passports_created += 1;
665            stats.active_passports = self.get_passport_count();
666        }
667    }
668
669    fn update_stats_event_recorded(&self) {
670        if let Ok(mut stats) = self.stats.lock() {
671            stats.total_events_recorded += 1;
672        }
673    }
674
675    fn update_stats_validation_failure(&self) {
676        if let Ok(mut stats) = self.stats.lock() {
677            stats.validation_failures += 1;
678        }
679    }
680}
681
682impl Default for MemoryPassportTracker {
683    fn default() -> Self {
684        Self::new(PassportTrackerConfig::default())
685    }
686}
687
688/// Global memory passport tracker instance
689static GLOBAL_PASSPORT_TRACKER: std::sync::OnceLock<Arc<MemoryPassportTracker>> =
690    std::sync::OnceLock::new();
691
692/// Get global memory passport tracker instance
693pub fn get_global_passport_tracker() -> Arc<MemoryPassportTracker> {
694    GLOBAL_PASSPORT_TRACKER
695        .get_or_init(|| Arc::new(MemoryPassportTracker::new(PassportTrackerConfig::default())))
696        .clone()
697}
698
699/// Initialize global passport tracker with custom config
700pub fn initialize_global_passport_tracker(
701    config: PassportTrackerConfig,
702) -> Arc<MemoryPassportTracker> {
703    let tracker = Arc::new(MemoryPassportTracker::new(config));
704    if GLOBAL_PASSPORT_TRACKER.set(tracker.clone()).is_err() {
705        tracing::warn!("Global passport tracker already initialized");
706    }
707    tracker
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713
714    #[test]
715    fn test_passport_creation() {
716        let tracker = MemoryPassportTracker::new(PassportTrackerConfig::default());
717        let ptr = 0x1000;
718        let size = 1024;
719
720        let passport_id = tracker
721            .create_passport(ptr, size, "test_context".to_string())
722            .expect("Failed to create passport");
723        assert!(!passport_id.is_empty());
724
725        let passport = tracker.get_passport(ptr).expect("Failed to get passport");
726        assert_eq!(passport.allocation_ptr, ptr);
727        assert_eq!(passport.size_bytes, size);
728        assert_eq!(passport.lifecycle_events.len(), 1);
729        assert_eq!(
730            passport.lifecycle_events[0].event_type,
731            PassportEventType::AllocatedInRust
732        );
733    }
734
735    #[test]
736    fn test_handover_to_ffi() {
737        let tracker = MemoryPassportTracker::new(PassportTrackerConfig::default());
738        let ptr = 0x2000;
739
740        tracker
741            .create_passport(ptr, 512, "rust_context".to_string())
742            .expect("Failed to create passport");
743        tracker
744            .record_handover_to_ffi(ptr, "ffi_context".to_string(), "malloc".to_string())
745            .expect("Failed to record handover");
746
747        let passport = tracker.get_passport(ptr).expect("Failed to get passport");
748        assert_eq!(passport.lifecycle_events.len(), 2);
749        assert_eq!(
750            passport.lifecycle_events[1].event_type,
751            PassportEventType::HandoverToFfi
752        );
753    }
754
755    #[test]
756    fn test_leak_detection() {
757        let tracker = MemoryPassportTracker::new(PassportTrackerConfig::default());
758        let ptr = 0x3000;
759
760        // Create passport and hand over to FFI without reclaim
761        tracker
762            .create_passport(ptr, 256, "rust_context".to_string())
763            .expect("Failed to create passport");
764        tracker
765            .record_handover_to_ffi(ptr, "ffi_context".to_string(), "malloc".to_string())
766            .expect("Failed to record handover");
767
768        // Detect leaks at shutdown
769        let leak_result = tracker.detect_leaks_at_shutdown();
770
771        assert_eq!(leak_result.total_leaks, 1);
772        assert_eq!(leak_result.leaked_passports.len(), 1);
773        assert_eq!(leak_result.leak_details.len(), 1);
774        assert_eq!(leak_result.leak_details[0].memory_address, ptr);
775
776        // Verify passport status
777        let passport = tracker.get_passport(ptr).expect("Test operation failed");
778        assert_eq!(
779            passport.status_at_shutdown,
780            PassportStatus::InForeignCustody
781        );
782    }
783
784    #[test]
785    fn test_reclaim_prevents_leak() {
786        let tracker = MemoryPassportTracker::new(PassportTrackerConfig::default());
787        let ptr = 0x4000;
788
789        // Create passport, hand over to FFI, then reclaim
790        tracker
791            .create_passport(ptr, 128, "rust_context".to_string())
792            .expect("Failed to create passport");
793        tracker
794            .record_handover_to_ffi(ptr, "ffi_context".to_string(), "malloc".to_string())
795            .expect("Failed to record handover");
796        tracker
797            .record_reclaimed_by_rust(ptr, "rust_context".to_string(), "cleanup".to_string())
798            .expect("Failed to record reclaim");
799
800        // Detect leaks at shutdown
801        let leak_result = tracker.detect_leaks_at_shutdown();
802
803        assert_eq!(leak_result.total_leaks, 0);
804
805        // Verify passport status
806        let passport = tracker.get_passport(ptr).expect("Test operation failed");
807        assert_eq!(passport.status_at_shutdown, PassportStatus::ReclaimedByRust);
808    }
809
810    #[test]
811    fn test_passport_validation() {
812        let tracker = MemoryPassportTracker::new(PassportTrackerConfig::default());
813        let ptr = 0x5000;
814
815        tracker
816            .create_passport(ptr, 64, "test_context".to_string())
817            .expect("Failed to create passport");
818        tracker
819            .record_handover_to_ffi(ptr, "ffi_context".to_string(), "test_func".to_string())
820            .expect("Failed to record handover");
821
822        let is_valid = tracker
823            .validate_passport(ptr)
824            .expect("Failed to validate passport");
825        assert!(is_valid);
826    }
827}