memscope_rs/analysis/
ffi_function_resolver.rs

1//! FFI Function Name Resolution System
2//!
3//! This module provides enhanced FFI function name resolution to replace
4//! vague "potential_ffi_target" with specific function and library information.
5
6use crate::core::types::{TrackingError, TrackingResult};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10
11/// Resolved FFI function information
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ResolvedFfiFunction {
14    /// Library name (e.g., "libc", "libssl", "user_library")
15    pub library_name: String,
16    /// Function name (e.g., "malloc", "free", "SSL_new")
17    pub function_name: String,
18    /// Function signature if available
19    pub signature: Option<String>,
20    /// Function category
21    pub category: FfiFunctionCategory,
22    /// Risk level associated with this function
23    pub risk_level: FfiRiskLevel,
24    /// Additional metadata
25    pub metadata: HashMap<String, String>,
26}
27
28/// Categories of FFI functions
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30pub enum FfiFunctionCategory {
31    /// Memory management functions (malloc, free, realloc)
32    MemoryManagement,
33    /// String manipulation functions (strcpy, strcat, sprintf)
34    StringManipulation,
35    /// File I/O functions (fopen, fread, fwrite)
36    FileIO,
37    /// Network functions (socket, connect, send)
38    Network,
39    /// Cryptographic functions (SSL_*, crypto_*)
40    Cryptographic,
41    /// System calls (fork, exec, signal)
42    SystemCall,
43    /// Graphics/UI functions (OpenGL, DirectX, etc.)
44    Graphics,
45    /// Database functions
46    Database,
47    /// Custom user library functions
48    UserLibrary,
49    /// Unknown or unclassified functions
50    Unknown,
51}
52
53/// Risk levels for FFI functions
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
55pub enum FfiRiskLevel {
56    /// Very low risk, well-tested standard functions
57    VeryLow,
58    /// Low risk, standard library functions with good safety record
59    Low,
60    /// Medium risk, functions that require careful parameter validation
61    Medium,
62    /// High risk, functions known to be dangerous if misused
63    High,
64    /// Critical risk, functions that are inherently unsafe
65    Critical,
66}
67
68/// FFI function resolver with built-in knowledge base
69pub struct FfiFunctionResolver {
70    /// Known function database
71    function_database: Arc<Mutex<HashMap<String, ResolvedFfiFunction>>>,
72    /// Library mapping (function -> library)
73    library_mapping: Arc<Mutex<HashMap<String, String>>>,
74    /// Resolution statistics
75    stats: Arc<Mutex<ResolutionStats>>,
76    /// Configuration
77    config: ResolverConfig,
78}
79
80/// Configuration for FFI function resolver
81#[derive(Debug, Clone)]
82pub struct ResolverConfig {
83    /// Enable automatic function discovery
84    pub enable_auto_discovery: bool,
85    /// Enable risk assessment
86    pub enable_risk_assessment: bool,
87    /// Cache resolved functions
88    pub enable_caching: bool,
89    /// Maximum cache size
90    pub max_cache_size: usize,
91}
92
93impl Default for ResolverConfig {
94    fn default() -> Self {
95        Self {
96            enable_auto_discovery: true,
97            enable_risk_assessment: true,
98            enable_caching: true,
99            max_cache_size: 1000,
100        }
101    }
102}
103
104/// Statistics for function resolution
105#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106pub struct ResolutionStats {
107    /// Total resolution attempts
108    pub total_attempts: usize,
109    /// Successful resolutions
110    pub successful_resolutions: usize,
111    /// Failed resolutions
112    pub failed_resolutions: usize,
113    /// Cache hits
114    pub cache_hits: usize,
115    /// Functions by category
116    pub functions_by_category: HashMap<String, usize>,
117    /// Functions by risk level
118    pub functions_by_risk: HashMap<String, usize>,
119    /// Most frequently resolved functions
120    pub top_functions: Vec<(String, usize)>,
121}
122
123impl FfiFunctionResolver {
124    /// Create new FFI function resolver
125    pub fn new(config: ResolverConfig) -> Self {
126        tracing::info!("๐Ÿ” Initializing FFI Function Resolver");
127        tracing::info!("   โ€ข Auto discovery: {}", config.enable_auto_discovery);
128        tracing::info!("   โ€ข Risk assessment: {}", config.enable_risk_assessment);
129        tracing::info!("   โ€ข Caching: {}", config.enable_caching);
130
131        let resolver = Self {
132            function_database: Arc::new(Mutex::new(HashMap::new())),
133            library_mapping: Arc::new(Mutex::new(HashMap::new())),
134            stats: Arc::new(Mutex::new(ResolutionStats::default())),
135            config,
136        };
137
138        // Initialize with known functions
139        resolver.initialize_known_functions();
140        resolver
141    }
142
143    /// Resolve FFI function name and library
144    pub fn resolve_function(
145        &self,
146        function_name: &str,
147        library_hint: Option<&str>,
148    ) -> TrackingResult<ResolvedFfiFunction> {
149        self.update_stats_attempt();
150
151        // Check cache first
152        if self.config.enable_caching {
153            if let Ok(db) = self.function_database.lock() {
154                if let Some(cached) = db.get(function_name) {
155                    self.update_stats_cache_hit();
156                    self.update_stats_success();
157                    tracing::debug!("๐Ÿ” Cache hit for function: {}", function_name);
158                    return Ok(cached.clone());
159                }
160            }
161        }
162
163        // Try to resolve from known functions
164        let resolved = if let Some(known) = self.get_known_function(function_name) {
165            known
166        } else if let Some(lib_hint) = library_hint {
167            // Use library hint to create resolution
168            self.resolve_with_library_hint(function_name, lib_hint)?
169        } else if self.config.enable_auto_discovery {
170            // Try automatic discovery
171            self.auto_discover_function(function_name)?
172        } else {
173            // Create unknown function entry
174            self.create_unknown_function(function_name)
175        };
176
177        // Cache the result
178        if self.config.enable_caching {
179            self.cache_function(function_name, &resolved)?;
180        }
181
182        self.update_stats_success();
183        tracing::debug!(
184            "๐Ÿ” Resolved function: {} -> {}::{}",
185            function_name,
186            resolved.library_name,
187            resolved.function_name
188        );
189
190        Ok(resolved)
191    }
192
193    /// Resolve multiple functions in batch
194    pub fn resolve_functions_batch(
195        &self,
196        function_names: &[String],
197    ) -> Vec<TrackingResult<ResolvedFfiFunction>> {
198        function_names
199            .iter()
200            .map(|name| self.resolve_function(name, None))
201            .collect()
202    }
203
204    /// Add custom function to database
205    pub fn add_custom_function(
206        &self,
207        function_name: String,
208        resolved: ResolvedFfiFunction,
209    ) -> TrackingResult<()> {
210        if let Ok(mut db) = self.function_database.lock() {
211            db.insert(function_name.clone(), resolved.clone());
212
213            if let Ok(mut mapping) = self.library_mapping.lock() {
214                mapping.insert(function_name.clone(), resolved.library_name.clone());
215            }
216
217            tracing::info!(
218                "๐Ÿ“š Added custom function: {} -> {}::{}",
219                function_name,
220                resolved.library_name,
221                resolved.function_name
222            );
223            Ok(())
224        } else {
225            Err(TrackingError::LockContention(
226                "Failed to lock function database".to_string(),
227            ))
228        }
229    }
230
231    /// Get resolution statistics
232    pub fn get_stats(&self) -> ResolutionStats {
233        if let Ok(stats) = self.stats.lock() {
234            stats.clone()
235        } else {
236            tracing::error!("Failed to lock resolution stats");
237            ResolutionStats::default()
238        }
239    }
240
241    /// Clear function cache
242    pub fn clear_cache(&self) {
243        if let Ok(mut db) = self.function_database.lock() {
244            let initial_size = db.len();
245            db.retain(|_, func| self.is_builtin_function(func));
246            let cleared = initial_size - db.len();
247            tracing::info!("๐Ÿงน Cleared {} cached functions", cleared);
248        }
249    }
250
251    // Private helper methods
252
253    fn initialize_known_functions(&self) {
254        let known_functions = vec![
255            // Memory management functions
256            (
257                "malloc",
258                ResolvedFfiFunction {
259                    library_name: "libc".to_string(),
260                    function_name: "malloc".to_string(),
261                    signature: Some("void* malloc(size_t size)".to_string()),
262                    category: FfiFunctionCategory::MemoryManagement,
263                    risk_level: FfiRiskLevel::Medium,
264                    metadata: [("description".to_string(), "Allocate memory".to_string())].into(),
265                },
266            ),
267            (
268                "free",
269                ResolvedFfiFunction {
270                    library_name: "libc".to_string(),
271                    function_name: "free".to_string(),
272                    signature: Some("void free(void* ptr)".to_string()),
273                    category: FfiFunctionCategory::MemoryManagement,
274                    risk_level: FfiRiskLevel::High,
275                    metadata: [(
276                        "description".to_string(),
277                        "Free allocated memory".to_string(),
278                    )]
279                    .into(),
280                },
281            ),
282            (
283                "realloc",
284                ResolvedFfiFunction {
285                    library_name: "libc".to_string(),
286                    function_name: "realloc".to_string(),
287                    signature: Some("void* realloc(void* ptr, size_t size)".to_string()),
288                    category: FfiFunctionCategory::MemoryManagement,
289                    risk_level: FfiRiskLevel::High,
290                    metadata: [("description".to_string(), "Reallocate memory".to_string())].into(),
291                },
292            ),
293            (
294                "calloc",
295                ResolvedFfiFunction {
296                    library_name: "libc".to_string(),
297                    function_name: "calloc".to_string(),
298                    signature: Some("void* calloc(size_t num, size_t size)".to_string()),
299                    category: FfiFunctionCategory::MemoryManagement,
300                    risk_level: FfiRiskLevel::Medium,
301                    metadata: [(
302                        "description".to_string(),
303                        "Allocate and zero memory".to_string(),
304                    )]
305                    .into(),
306                },
307            ),
308            // String manipulation functions
309            (
310                "strcpy",
311                ResolvedFfiFunction {
312                    library_name: "libc".to_string(),
313                    function_name: "strcpy".to_string(),
314                    signature: Some("char* strcpy(char* dest, const char* src)".to_string()),
315                    category: FfiFunctionCategory::StringManipulation,
316                    risk_level: FfiRiskLevel::Critical,
317                    metadata: [(
318                        "description".to_string(),
319                        "Copy string (unsafe)".to_string(),
320                    )]
321                    .into(),
322                },
323            ),
324            (
325                "strncpy",
326                ResolvedFfiFunction {
327                    library_name: "libc".to_string(),
328                    function_name: "strncpy".to_string(),
329                    signature: Some(
330                        "char* strncpy(char* dest, const char* src, size_t n)".to_string(),
331                    ),
332                    category: FfiFunctionCategory::StringManipulation,
333                    risk_level: FfiRiskLevel::Medium,
334                    metadata: [(
335                        "description".to_string(),
336                        "Copy string with length limit".to_string(),
337                    )]
338                    .into(),
339                },
340            ),
341            (
342                "sprintf",
343                ResolvedFfiFunction {
344                    library_name: "libc".to_string(),
345                    function_name: "sprintf".to_string(),
346                    signature: Some("int sprintf(char* str, const char* format, ...)".to_string()),
347                    category: FfiFunctionCategory::StringManipulation,
348                    risk_level: FfiRiskLevel::Critical,
349                    metadata: [(
350                        "description".to_string(),
351                        "Format string (unsafe)".to_string(),
352                    )]
353                    .into(),
354                },
355            ),
356            (
357                "snprintf",
358                ResolvedFfiFunction {
359                    library_name: "libc".to_string(),
360                    function_name: "snprintf".to_string(),
361                    signature: Some(
362                        "int snprintf(char* str, size_t size, const char* format, ...)".to_string(),
363                    ),
364                    category: FfiFunctionCategory::StringManipulation,
365                    risk_level: FfiRiskLevel::Low,
366                    metadata: [("description".to_string(), "Safe format string".to_string())]
367                        .into(),
368                },
369            ),
370            // File I/O functions
371            (
372                "fopen",
373                ResolvedFfiFunction {
374                    library_name: "libc".to_string(),
375                    function_name: "fopen".to_string(),
376                    signature: Some(
377                        "FILE* fopen(const char* filename, const char* mode)".to_string(),
378                    ),
379                    category: FfiFunctionCategory::FileIO,
380                    risk_level: FfiRiskLevel::Low,
381                    metadata: [("description".to_string(), "Open file".to_string())].into(),
382                },
383            ),
384            (
385                "fclose",
386                ResolvedFfiFunction {
387                    library_name: "libc".to_string(),
388                    function_name: "fclose".to_string(),
389                    signature: Some("int fclose(FILE* stream)".to_string()),
390                    category: FfiFunctionCategory::FileIO,
391                    risk_level: FfiRiskLevel::Low,
392                    metadata: [("description".to_string(), "Close file".to_string())].into(),
393                },
394            ),
395            // System calls
396            (
397                "fork",
398                ResolvedFfiFunction {
399                    library_name: "libc".to_string(),
400                    function_name: "fork".to_string(),
401                    signature: Some("pid_t fork(void)".to_string()),
402                    category: FfiFunctionCategory::SystemCall,
403                    risk_level: FfiRiskLevel::Medium,
404                    metadata: [(
405                        "description".to_string(),
406                        "Create child process".to_string(),
407                    )]
408                    .into(),
409                },
410            ),
411        ];
412
413        if let Ok(mut db) = self.function_database.lock() {
414            if let Ok(mut mapping) = self.library_mapping.lock() {
415                for (name, func) in known_functions {
416                    db.insert(name.to_string(), func.clone());
417                    mapping.insert(name.to_string(), func.library_name);
418                }
419            }
420        }
421
422        tracing::info!("๐Ÿ“š Initialized {} known FFI functions", 11);
423    }
424
425    fn get_known_function(&self, function_name: &str) -> Option<ResolvedFfiFunction> {
426        if let Ok(db) = self.function_database.lock() {
427            db.get(function_name).cloned()
428        } else {
429            None
430        }
431    }
432
433    fn resolve_with_library_hint(
434        &self,
435        function_name: &str,
436        library_hint: &str,
437    ) -> TrackingResult<ResolvedFfiFunction> {
438        let category = self.infer_category_from_name(function_name);
439        let risk_level = self.assess_risk_from_name(function_name, &category);
440
441        Ok(ResolvedFfiFunction {
442            library_name: library_hint.to_string(),
443            function_name: function_name.to_string(),
444            signature: None,
445            category,
446            risk_level,
447            metadata: HashMap::new(),
448        })
449    }
450
451    fn auto_discover_function(&self, function_name: &str) -> TrackingResult<ResolvedFfiFunction> {
452        // Try to infer library from function name patterns
453        let library_name = self.infer_library_from_name(function_name);
454        let category = self.infer_category_from_name(function_name);
455        let risk_level = self.assess_risk_from_name(function_name, &category);
456
457        Ok(ResolvedFfiFunction {
458            library_name,
459            function_name: function_name.to_string(),
460            signature: None,
461            category,
462            risk_level,
463            metadata: [("auto_discovered".to_string(), "true".to_string())].into(),
464        })
465    }
466
467    fn create_unknown_function(&self, function_name: &str) -> ResolvedFfiFunction {
468        ResolvedFfiFunction {
469            library_name: "unknown".to_string(),
470            function_name: function_name.to_string(),
471            signature: None,
472            category: FfiFunctionCategory::Unknown,
473            risk_level: FfiRiskLevel::Medium,
474            metadata: [("status".to_string(), "unresolved".to_string())].into(),
475        }
476    }
477
478    fn infer_library_from_name(&self, function_name: &str) -> String {
479        // Common library patterns
480        if function_name.starts_with("SSL_") || function_name.starts_with("crypto_") {
481            "libssl".to_string()
482        } else if function_name.starts_with("pthread_") {
483            "libpthread".to_string()
484        } else if function_name.starts_with("gl") || function_name.starts_with("GL") {
485            "libGL".to_string()
486        } else if function_name.starts_with("sqlite3_") {
487            "libsqlite3".to_string()
488        } else if ["malloc", "free", "printf", "scanf", "fopen", "fork"]
489            .iter()
490            .any(|&f| function_name.contains(f))
491        {
492            "libc".to_string()
493        } else {
494            "unknown".to_string()
495        }
496    }
497
498    fn infer_category_from_name(&self, function_name: &str) -> FfiFunctionCategory {
499        if ["malloc", "free", "realloc", "calloc"]
500            .iter()
501            .any(|&f| function_name.contains(f))
502        {
503            FfiFunctionCategory::MemoryManagement
504        } else if ["str", "sprintf", "printf"]
505            .iter()
506            .any(|&f| function_name.contains(f))
507        {
508            FfiFunctionCategory::StringManipulation
509        } else if ["fopen", "fread", "fwrite", "fclose"]
510            .iter()
511            .any(|&f| function_name.contains(f))
512        {
513            FfiFunctionCategory::FileIO
514        } else if ["socket", "connect", "send", "recv"]
515            .iter()
516            .any(|&f| function_name.contains(f))
517        {
518            FfiFunctionCategory::Network
519        } else if ["SSL_", "crypto_"]
520            .iter()
521            .any(|&f| function_name.starts_with(f))
522        {
523            FfiFunctionCategory::Cryptographic
524        } else if ["fork", "exec", "signal", "kill"]
525            .iter()
526            .any(|&f| function_name.contains(f))
527        {
528            FfiFunctionCategory::SystemCall
529        } else if ["gl", "GL", "Direct"]
530            .iter()
531            .any(|&f| function_name.contains(f))
532        {
533            FfiFunctionCategory::Graphics
534        } else if ["sqlite", "mysql", "postgres"]
535            .iter()
536            .any(|&f| function_name.contains(f))
537        {
538            FfiFunctionCategory::Database
539        } else {
540            FfiFunctionCategory::Unknown
541        }
542    }
543
544    fn assess_risk_from_name(
545        &self,
546        function_name: &str,
547        category: &FfiFunctionCategory,
548    ) -> FfiRiskLevel {
549        // High-risk functions
550        if ["strcpy", "sprintf", "gets", "scanf"].contains(&function_name) {
551            return FfiRiskLevel::Critical;
552        }
553
554        // Medium-high risk functions
555        if ["free", "realloc"].contains(&function_name) {
556            return FfiRiskLevel::High;
557        }
558
559        // Category-based risk assessment
560        match category {
561            FfiFunctionCategory::MemoryManagement => FfiRiskLevel::Medium,
562            FfiFunctionCategory::StringManipulation => FfiRiskLevel::High,
563            FfiFunctionCategory::SystemCall => FfiRiskLevel::Medium,
564            FfiFunctionCategory::Cryptographic => FfiRiskLevel::Low,
565            FfiFunctionCategory::FileIO => FfiRiskLevel::Low,
566            FfiFunctionCategory::Network => FfiRiskLevel::Medium,
567            FfiFunctionCategory::Graphics => FfiRiskLevel::Low,
568            FfiFunctionCategory::Database => FfiRiskLevel::Low,
569            FfiFunctionCategory::UserLibrary => FfiRiskLevel::Medium,
570            FfiFunctionCategory::Unknown => FfiRiskLevel::Medium,
571        }
572    }
573
574    fn cache_function(
575        &self,
576        function_name: &str,
577        resolved: &ResolvedFfiFunction,
578    ) -> TrackingResult<()> {
579        if let Ok(mut db) = self.function_database.lock() {
580            // Check cache size limit
581            if db.len() >= self.config.max_cache_size {
582                // Remove some non-builtin entries
583                let keys_to_remove: Vec<String> = db
584                    .iter()
585                    .filter(|(_, func)| !self.is_builtin_function(func))
586                    .take(10)
587                    .map(|(k, _)| k.clone())
588                    .collect();
589
590                for key in keys_to_remove {
591                    db.remove(&key);
592                }
593            }
594
595            db.insert(function_name.to_string(), resolved.clone());
596            Ok(())
597        } else {
598            Err(TrackingError::LockContention(
599                "Failed to lock function database".to_string(),
600            ))
601        }
602    }
603
604    fn is_builtin_function(&self, func: &ResolvedFfiFunction) -> bool {
605        !func.metadata.contains_key("auto_discovered") && func.library_name != "unknown"
606    }
607
608    fn update_stats_attempt(&self) {
609        if let Ok(mut stats) = self.stats.lock() {
610            stats.total_attempts += 1;
611        }
612    }
613
614    fn update_stats_success(&self) {
615        if let Ok(mut stats) = self.stats.lock() {
616            stats.successful_resolutions += 1;
617        }
618    }
619
620    fn update_stats_cache_hit(&self) {
621        if let Ok(mut stats) = self.stats.lock() {
622            stats.cache_hits += 1;
623        }
624    }
625}
626
627impl Default for FfiFunctionResolver {
628    fn default() -> Self {
629        Self::new(ResolverConfig::default())
630    }
631}
632
633/// Global FFI function resolver instance
634static GLOBAL_FFI_RESOLVER: std::sync::OnceLock<Arc<FfiFunctionResolver>> =
635    std::sync::OnceLock::new();
636
637/// Get global FFI function resolver instance
638pub fn get_global_ffi_resolver() -> Arc<FfiFunctionResolver> {
639    GLOBAL_FFI_RESOLVER
640        .get_or_init(|| Arc::new(FfiFunctionResolver::new(ResolverConfig::default())))
641        .clone()
642}
643
644/// Initialize global FFI function resolver with custom config
645pub fn initialize_global_ffi_resolver(config: ResolverConfig) -> Arc<FfiFunctionResolver> {
646    let resolver = Arc::new(FfiFunctionResolver::new(config));
647    if GLOBAL_FFI_RESOLVER.set(resolver.clone()).is_err() {
648        tracing::warn!("Global FFI resolver already initialized");
649    }
650    resolver
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656
657    #[test]
658    fn test_known_function_resolution() {
659        let resolver = FfiFunctionResolver::new(ResolverConfig::default());
660
661        let malloc_result = resolver
662            .resolve_function("malloc", None)
663            .expect("Failed to resolve malloc");
664        assert_eq!(malloc_result.library_name, "libc");
665        assert_eq!(malloc_result.function_name, "malloc");
666        assert_eq!(
667            malloc_result.category,
668            FfiFunctionCategory::MemoryManagement
669        );
670        assert_eq!(malloc_result.risk_level, FfiRiskLevel::Medium);
671    }
672
673    #[test]
674    fn test_auto_discovery() {
675        let resolver = FfiFunctionResolver::new(ResolverConfig::default());
676
677        let ssl_result = resolver
678            .resolve_function("SSL_new", None)
679            .expect("Failed to resolve SSL_new");
680        assert_eq!(ssl_result.library_name, "libssl");
681        assert_eq!(ssl_result.category, FfiFunctionCategory::Cryptographic);
682    }
683
684    #[test]
685    fn test_library_hint() {
686        let resolver = FfiFunctionResolver::new(ResolverConfig::default());
687
688        let custom_result = resolver
689            .resolve_function("custom_func", Some("mylib"))
690            .expect("Failed to resolve custom function");
691        assert_eq!(custom_result.library_name, "mylib");
692        assert_eq!(custom_result.function_name, "custom_func");
693    }
694
695    #[test]
696    fn test_risk_assessment() {
697        let resolver = FfiFunctionResolver::new(ResolverConfig::default());
698
699        let strcpy_result = resolver
700            .resolve_function("strcpy", None)
701            .expect("Failed to resolve strcpy");
702        assert_eq!(strcpy_result.risk_level, FfiRiskLevel::Critical);
703
704        let snprintf_result = resolver
705            .resolve_function("snprintf", None)
706            .expect("Failed to resolve snprintf");
707        assert_eq!(snprintf_result.risk_level, FfiRiskLevel::Low);
708    }
709
710    #[test]
711    fn test_batch_resolution() {
712        let resolver = FfiFunctionResolver::new(ResolverConfig::default());
713
714        let functions = vec![
715            "malloc".to_string(),
716            "free".to_string(),
717            "strcpy".to_string(),
718        ];
719        let results = resolver.resolve_functions_batch(&functions);
720
721        assert_eq!(results.len(), 3);
722        assert!(results.iter().all(|r| r.is_ok()));
723    }
724
725    #[test]
726    fn test_custom_function() {
727        let resolver = FfiFunctionResolver::new(ResolverConfig::default());
728
729        let custom_func = ResolvedFfiFunction {
730            library_name: "mylib".to_string(),
731            function_name: "my_func".to_string(),
732            signature: Some("int my_func(void)".to_string()),
733            category: FfiFunctionCategory::UserLibrary,
734            risk_level: FfiRiskLevel::Low,
735            metadata: HashMap::new(),
736        };
737
738        resolver
739            .add_custom_function("my_func".to_string(), custom_func)
740            .expect("Failed to add custom function");
741
742        let resolved = resolver
743            .resolve_function("my_func", None)
744            .expect("Failed to resolve custom function");
745        assert_eq!(resolved.library_name, "mylib");
746        assert_eq!(resolved.category, FfiFunctionCategory::UserLibrary);
747    }
748}