use crate::capture::types::{TrackingError, TrackingResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedFfiFunction {
pub library_name: String,
pub function_name: String,
pub signature: Option<String>,
pub category: FfiFunctionCategory,
pub risk_level: FfiRiskLevel,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum FfiFunctionCategory {
MemoryManagement,
StringManipulation,
FileIO,
Network,
Cryptographic,
SystemCall,
Graphics,
Database,
UserLibrary,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum FfiRiskLevel {
VeryLow,
Low,
Medium,
High,
Critical,
}
pub struct FfiFunctionResolver {
function_database: Arc<Mutex<HashMap<String, ResolvedFfiFunction>>>,
library_mapping: Arc<Mutex<HashMap<String, String>>>,
stats: Arc<Mutex<ResolutionStats>>,
config: ResolverConfig,
}
#[derive(Debug, Clone)]
pub struct ResolverConfig {
pub enable_auto_discovery: bool,
pub enable_risk_assessment: bool,
pub enable_caching: bool,
pub max_cache_size: usize,
}
impl Default for ResolverConfig {
fn default() -> Self {
Self {
enable_auto_discovery: true,
enable_risk_assessment: true,
enable_caching: true,
max_cache_size: 1000,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResolutionStats {
pub total_attempts: usize,
pub successful_resolutions: usize,
pub failed_resolutions: usize,
pub cache_hits: usize,
pub functions_by_category: HashMap<String, usize>,
pub functions_by_risk: HashMap<String, usize>,
pub top_functions: Vec<(String, usize)>,
}
impl FfiFunctionResolver {
pub fn new(config: ResolverConfig) -> Self {
tracing::info!("🔍 Initializing FFI Function Resolver");
tracing::info!(" • Auto discovery: {}", config.enable_auto_discovery);
tracing::info!(" • Risk assessment: {}", config.enable_risk_assessment);
tracing::info!(" • Caching: {}", config.enable_caching);
let resolver = Self {
function_database: Arc::new(Mutex::new(HashMap::new())),
library_mapping: Arc::new(Mutex::new(HashMap::new())),
stats: Arc::new(Mutex::new(ResolutionStats::default())),
config,
};
resolver.initialize_known_functions();
resolver
}
pub fn resolve_function(
&self,
function_name: &str,
library_hint: Option<&str>,
) -> TrackingResult<ResolvedFfiFunction> {
self.update_stats_attempt();
if self.config.enable_caching {
if let Ok(db) = self.function_database.lock() {
if let Some(cached) = db.get(function_name) {
self.update_stats_cache_hit();
self.update_stats_success();
tracing::debug!("🔍 Cache hit for function: {}", function_name);
return Ok(cached.clone());
}
}
}
let resolved = if let Some(known) = self.get_known_function(function_name) {
known
} else if let Some(lib_hint) = library_hint {
self.resolve_with_library_hint(function_name, lib_hint)?
} else if self.config.enable_auto_discovery {
self.auto_discover_function(function_name)?
} else {
self.create_unknown_function(function_name)
};
if self.config.enable_caching {
self.cache_function(function_name, &resolved)?;
}
self.update_stats_success();
tracing::debug!(
"🔍 Resolved function: {} -> {}::{}",
function_name,
resolved.library_name,
resolved.function_name
);
Ok(resolved)
}
pub fn resolve_functions_batch(
&self,
function_names: &[String],
) -> Vec<TrackingResult<ResolvedFfiFunction>> {
function_names
.iter()
.map(|name| self.resolve_function(name, None))
.collect()
}
pub fn add_custom_function(
&self,
function_name: String,
resolved: ResolvedFfiFunction,
) -> TrackingResult<()> {
if let Ok(mut db) = self.function_database.lock() {
db.insert(function_name.clone(), resolved.clone());
if let Ok(mut mapping) = self.library_mapping.lock() {
mapping.insert(function_name.clone(), resolved.library_name.clone());
}
tracing::info!(
"📚 Added custom function: {} -> {}::{}",
function_name,
resolved.library_name,
resolved.function_name
);
Ok(())
} else {
Err(TrackingError::LockContention(
"Failed to lock function database".to_string(),
))
}
}
pub fn get_stats(&self) -> ResolutionStats {
if let Ok(stats) = self.stats.lock() {
stats.clone()
} else {
tracing::error!("Failed to lock resolution stats");
ResolutionStats::default()
}
}
pub fn clear_cache(&self) {
if let Ok(mut db) = self.function_database.lock() {
let initial_size = db.len();
db.retain(|_, func| self.is_builtin_function(func));
let cleared = initial_size - db.len();
tracing::info!("🧹 Cleared {} cached functions", cleared);
}
}
fn initialize_known_functions(&self) {
let known_functions = vec![
(
"malloc",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "malloc".to_string(),
signature: Some("void* malloc(size_t size)".to_string()),
category: FfiFunctionCategory::MemoryManagement,
risk_level: FfiRiskLevel::Medium,
metadata: [("description".to_string(), "Allocate memory".to_string())].into(),
},
),
(
"free",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "free".to_string(),
signature: Some("void free(void* ptr)".to_string()),
category: FfiFunctionCategory::MemoryManagement,
risk_level: FfiRiskLevel::High,
metadata: [(
"description".to_string(),
"Free allocated memory".to_string(),
)]
.into(),
},
),
(
"realloc",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "realloc".to_string(),
signature: Some("void* realloc(void* ptr, size_t size)".to_string()),
category: FfiFunctionCategory::MemoryManagement,
risk_level: FfiRiskLevel::High,
metadata: [("description".to_string(), "Reallocate memory".to_string())].into(),
},
),
(
"calloc",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "calloc".to_string(),
signature: Some("void* calloc(size_t num, size_t size)".to_string()),
category: FfiFunctionCategory::MemoryManagement,
risk_level: FfiRiskLevel::Medium,
metadata: [(
"description".to_string(),
"Allocate and zero memory".to_string(),
)]
.into(),
},
),
(
"strcpy",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "strcpy".to_string(),
signature: Some("char* strcpy(char* dest, const char* src)".to_string()),
category: FfiFunctionCategory::StringManipulation,
risk_level: FfiRiskLevel::Critical,
metadata: [(
"description".to_string(),
"Copy string (unsafe)".to_string(),
)]
.into(),
},
),
(
"strncpy",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "strncpy".to_string(),
signature: Some(
"char* strncpy(char* dest, const char* src, size_t n)".to_string(),
),
category: FfiFunctionCategory::StringManipulation,
risk_level: FfiRiskLevel::Medium,
metadata: [(
"description".to_string(),
"Copy string with length limit".to_string(),
)]
.into(),
},
),
(
"sprintf",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "sprintf".to_string(),
signature: Some("int sprintf(char* str, const char* format, ...)".to_string()),
category: FfiFunctionCategory::StringManipulation,
risk_level: FfiRiskLevel::Critical,
metadata: [(
"description".to_string(),
"Format string (unsafe)".to_string(),
)]
.into(),
},
),
(
"snprintf",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "snprintf".to_string(),
signature: Some(
"int snprintf(char* str, size_t size, const char* format, ...)".to_string(),
),
category: FfiFunctionCategory::StringManipulation,
risk_level: FfiRiskLevel::Low,
metadata: [("description".to_string(), "Safe format string".to_string())]
.into(),
},
),
(
"fopen",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "fopen".to_string(),
signature: Some(
"FILE* fopen(const char* filename, const char* mode)".to_string(),
),
category: FfiFunctionCategory::FileIO,
risk_level: FfiRiskLevel::Low,
metadata: [("description".to_string(), "Open file".to_string())].into(),
},
),
(
"fclose",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "fclose".to_string(),
signature: Some("int fclose(FILE* stream)".to_string()),
category: FfiFunctionCategory::FileIO,
risk_level: FfiRiskLevel::Low,
metadata: [("description".to_string(), "Close file".to_string())].into(),
},
),
(
"fork",
ResolvedFfiFunction {
library_name: "libc".to_string(),
function_name: "fork".to_string(),
signature: Some("pid_t fork(void)".to_string()),
category: FfiFunctionCategory::SystemCall,
risk_level: FfiRiskLevel::Medium,
metadata: [(
"description".to_string(),
"Create child process".to_string(),
)]
.into(),
},
),
];
if let Ok(mut db) = self.function_database.lock() {
if let Ok(mut mapping) = self.library_mapping.lock() {
for (name, func) in known_functions {
db.insert(name.to_string(), func.clone());
mapping.insert(name.to_string(), func.library_name);
}
}
}
tracing::info!("📚 Initialized {} known FFI functions", 11);
}
fn get_known_function(&self, function_name: &str) -> Option<ResolvedFfiFunction> {
if let Ok(db) = self.function_database.lock() {
db.get(function_name).cloned()
} else {
None
}
}
fn resolve_with_library_hint(
&self,
function_name: &str,
library_hint: &str,
) -> TrackingResult<ResolvedFfiFunction> {
let category = self.infer_category_from_name(function_name);
let risk_level = self.assess_risk_from_name(function_name, &category);
Ok(ResolvedFfiFunction {
library_name: library_hint.to_string(),
function_name: function_name.to_string(),
signature: None,
category,
risk_level,
metadata: HashMap::new(),
})
}
fn auto_discover_function(&self, function_name: &str) -> TrackingResult<ResolvedFfiFunction> {
let library_name = self.infer_library_from_name(function_name);
let category = self.infer_category_from_name(function_name);
let risk_level = self.assess_risk_from_name(function_name, &category);
Ok(ResolvedFfiFunction {
library_name,
function_name: function_name.to_string(),
signature: None,
category,
risk_level,
metadata: [("auto_discovered".to_string(), "true".to_string())].into(),
})
}
fn create_unknown_function(&self, function_name: &str) -> ResolvedFfiFunction {
ResolvedFfiFunction {
library_name: "unknown".to_string(),
function_name: function_name.to_string(),
signature: None,
category: FfiFunctionCategory::Unknown,
risk_level: FfiRiskLevel::Medium,
metadata: [("status".to_string(), "unresolved".to_string())].into(),
}
}
fn infer_library_from_name(&self, function_name: &str) -> String {
if function_name.starts_with("SSL_") || function_name.starts_with("crypto_") {
"libssl".to_string()
} else if function_name.starts_with("pthread_") {
"libpthread".to_string()
} else if function_name.starts_with("gl") || function_name.starts_with("GL") {
"libGL".to_string()
} else if function_name.starts_with("sqlite3_") {
"libsqlite3".to_string()
} else if ["malloc", "free", "printf", "scanf", "fopen", "fork"]
.iter()
.any(|&f| function_name.contains(f))
{
"libc".to_string()
} else {
"unknown".to_string()
}
}
fn infer_category_from_name(&self, function_name: &str) -> FfiFunctionCategory {
if ["malloc", "free", "realloc", "calloc"]
.iter()
.any(|&f| function_name.contains(f))
{
FfiFunctionCategory::MemoryManagement
} else if ["str", "sprintf", "printf"]
.iter()
.any(|&f| function_name.contains(f))
{
FfiFunctionCategory::StringManipulation
} else if ["fopen", "fread", "fwrite", "fclose"]
.iter()
.any(|&f| function_name.contains(f))
{
FfiFunctionCategory::FileIO
} else if ["socket", "connect", "send", "recv"]
.iter()
.any(|&f| function_name.contains(f))
{
FfiFunctionCategory::Network
} else if ["SSL_", "crypto_"]
.iter()
.any(|&f| function_name.starts_with(f))
{
FfiFunctionCategory::Cryptographic
} else if ["fork", "exec", "signal", "kill"]
.iter()
.any(|&f| function_name.contains(f))
{
FfiFunctionCategory::SystemCall
} else if ["gl", "GL", "Direct"]
.iter()
.any(|&f| function_name.contains(f))
{
FfiFunctionCategory::Graphics
} else if ["sqlite", "mysql", "postgres"]
.iter()
.any(|&f| function_name.contains(f))
{
FfiFunctionCategory::Database
} else {
FfiFunctionCategory::Unknown
}
}
fn assess_risk_from_name(
&self,
function_name: &str,
category: &FfiFunctionCategory,
) -> FfiRiskLevel {
if ["strcpy", "sprintf", "gets", "scanf"].contains(&function_name) {
return FfiRiskLevel::Critical;
}
if ["free", "realloc"].contains(&function_name) {
return FfiRiskLevel::High;
}
match category {
FfiFunctionCategory::MemoryManagement => FfiRiskLevel::Medium,
FfiFunctionCategory::StringManipulation => FfiRiskLevel::High,
FfiFunctionCategory::SystemCall => FfiRiskLevel::Medium,
FfiFunctionCategory::Cryptographic => FfiRiskLevel::Low,
FfiFunctionCategory::FileIO => FfiRiskLevel::Low,
FfiFunctionCategory::Network => FfiRiskLevel::Medium,
FfiFunctionCategory::Graphics => FfiRiskLevel::Low,
FfiFunctionCategory::Database => FfiRiskLevel::Low,
FfiFunctionCategory::UserLibrary => FfiRiskLevel::Medium,
FfiFunctionCategory::Unknown => FfiRiskLevel::Medium,
}
}
fn cache_function(
&self,
function_name: &str,
resolved: &ResolvedFfiFunction,
) -> TrackingResult<()> {
if let Ok(mut db) = self.function_database.lock() {
if db.len() >= self.config.max_cache_size {
let keys_to_remove: Vec<String> = db
.iter()
.filter(|(_, func)| !self.is_builtin_function(func))
.take(10)
.map(|(k, _)| k.clone())
.collect();
for key in keys_to_remove {
db.remove(&key);
}
}
db.insert(function_name.to_string(), resolved.clone());
Ok(())
} else {
Err(TrackingError::LockContention(
"Failed to lock function database".to_string(),
))
}
}
fn is_builtin_function(&self, func: &ResolvedFfiFunction) -> bool {
!func.metadata.contains_key("auto_discovered") && func.library_name != "unknown"
}
fn update_stats_attempt(&self) {
if let Ok(mut stats) = self.stats.lock() {
stats.total_attempts += 1;
}
}
fn update_stats_success(&self) {
if let Ok(mut stats) = self.stats.lock() {
stats.successful_resolutions += 1;
}
}
fn update_stats_cache_hit(&self) {
if let Ok(mut stats) = self.stats.lock() {
stats.cache_hits += 1;
}
}
}
impl Default for FfiFunctionResolver {
fn default() -> Self {
Self::new(ResolverConfig::default())
}
}
static GLOBAL_FFI_RESOLVER: std::sync::OnceLock<Arc<FfiFunctionResolver>> =
std::sync::OnceLock::new();
pub fn get_global_ffi_resolver() -> Arc<FfiFunctionResolver> {
GLOBAL_FFI_RESOLVER
.get_or_init(|| Arc::new(FfiFunctionResolver::new(ResolverConfig::default())))
.clone()
}
pub fn initialize_global_ffi_resolver(config: ResolverConfig) -> Arc<FfiFunctionResolver> {
let resolver = Arc::new(FfiFunctionResolver::new(config));
if GLOBAL_FFI_RESOLVER.set(resolver.clone()).is_err() {
tracing::warn!("Global FFI resolver already initialized");
}
resolver
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_function_resolution() {
let resolver = FfiFunctionResolver::new(ResolverConfig::default());
let malloc_result = resolver
.resolve_function("malloc", None)
.expect("Failed to resolve malloc");
assert_eq!(malloc_result.library_name, "libc");
assert_eq!(malloc_result.function_name, "malloc");
assert_eq!(
malloc_result.category,
FfiFunctionCategory::MemoryManagement
);
assert_eq!(malloc_result.risk_level, FfiRiskLevel::Medium);
}
#[test]
fn test_auto_discovery() {
let resolver = FfiFunctionResolver::new(ResolverConfig::default());
let ssl_result = resolver
.resolve_function("SSL_new", None)
.expect("Failed to resolve SSL_new");
assert_eq!(ssl_result.library_name, "libssl");
assert_eq!(ssl_result.category, FfiFunctionCategory::Cryptographic);
}
#[test]
fn test_library_hint() {
let resolver = FfiFunctionResolver::new(ResolverConfig::default());
let custom_result = resolver
.resolve_function("custom_func", Some("mylib"))
.expect("Failed to resolve custom function");
assert_eq!(custom_result.library_name, "mylib");
assert_eq!(custom_result.function_name, "custom_func");
}
#[test]
fn test_risk_assessment() {
let resolver = FfiFunctionResolver::new(ResolverConfig::default());
let strcpy_result = resolver
.resolve_function("strcpy", None)
.expect("Failed to resolve strcpy");
assert_eq!(strcpy_result.risk_level, FfiRiskLevel::Critical);
let snprintf_result = resolver
.resolve_function("snprintf", None)
.expect("Failed to resolve snprintf");
assert_eq!(snprintf_result.risk_level, FfiRiskLevel::Low);
}
#[test]
fn test_batch_resolution() {
let resolver = FfiFunctionResolver::new(ResolverConfig::default());
let functions = vec![
"malloc".to_string(),
"free".to_string(),
"strcpy".to_string(),
];
let results = resolver.resolve_functions_batch(&functions);
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r.is_ok()));
}
#[test]
fn test_custom_function() {
let resolver = FfiFunctionResolver::new(ResolverConfig::default());
let custom_func = ResolvedFfiFunction {
library_name: "mylib".to_string(),
function_name: "my_func".to_string(),
signature: Some("int my_func(void)".to_string()),
category: FfiFunctionCategory::UserLibrary,
risk_level: FfiRiskLevel::Low,
metadata: HashMap::new(),
};
resolver
.add_custom_function("my_func".to_string(), custom_func)
.expect("Failed to add custom function");
let resolved = resolver
.resolve_function("my_func", None)
.expect("Failed to resolve custom function");
assert_eq!(resolved.library_name, "mylib");
assert_eq!(resolved.category, FfiFunctionCategory::UserLibrary);
}
}