FFI Boundary Security
The Foreign Function Interface (FFI) boundary represents the most security-critical component of Cypheron Core, defining the transition between memory-safe Rust and potentially unsafe C implementations.
For complete technical analysis, see Architecture Documentation.
Trust Boundary Model
The FFI boundary creates a clear separation between trusted and untrusted zones:
┌─────────────────────────────────────────────────────────────┐
│ TRUSTED ZONE │
│ • Rust Application Code - Type & Memory Safe │
│ • Cypheron Safe Wrappers - Input Validation │
│ • Buffer Management - Lifetime Control │
├═════════════════════════════════════════════════════════════┤
│ FFI SECURITY BOUNDARY │
├═════════════════════════════════════════════════════════════┤
│ UNTRUSTED ZONE │
│ • NIST C Reference Code - Manual Memory Management │
│ • Potential Undefined Behavior - Platform Specific │
└─────────────────────────────────────────────────────────────┘
Data Flow Security
Inbound Path (Rust → C)
- Input Validation: All parameters validated against algorithm specifications
- Buffer Preparation: Memory allocated with exact required sizes
- Pointer Safety: Raw pointers derived only from valid Rust references
- Length Verification: Buffer sizes cross-checked against C function expectations
Outbound Path (C → Rust)
- Return Code Verification: All C function return values checked
- Output Validation: Generated data verified for proper initialization
- Size Consistency: Output lengths validated against expected algorithm outputs
- Memory Transfer: C-generated data safely transferred to Rust ownership
Memory Ownership Model
Pre-Call State
- Rust allocates and owns all input and output buffers
- Buffer sizes calculated from algorithm-specific constants
- Pointers derived from valid Rust slice references
During C Execution
- Temporary shared access granted via raw pointers
- Rust retains ownership but cannot access during execution
- C code operates within provided buffer boundaries
Post-Call State
- Full ownership returns to Rust immediately
- C-modified buffers validated for proper initialization
- Sensitive data securely zeroized via Drop traits
Safety Guarantees
Buffer Boundary Protection
- All buffer accesses validated before FFI calls
- C functions receive exact sizes via separate length parameters
- No C function can access memory beyond provided boundaries
Type Safety Maintenance
- Raw pointers used only for duration of C function calls
- All data marshalling preserves Rust type invariants
- No C pointers retained beyond function call scope
Error Handling Isolation
- C function errors isolated and converted to Rust error types
- No C error state can compromise Rust memory safety
- Failed operations trigger secure cleanup of sensitive data
Concurrency Safety
- FFI calls protected by appropriate synchronization primitives
- No shared mutable state accessible across FFI boundary
- Thread-local storage used for algorithm-specific contexts
Example: Safe FFI Pattern
#![allow(unused)] fn main() { pub fn ml_kem_keypair() -> Result<(PublicKey, SecretKey), Error> { // 1. Allocate buffers in Rust let mut pk = [0u8; ML_KEM_768_PUBLIC_KEY_BYTES]; let mut sk = [0u8; ML_KEM_768_SECRET_KEY_BYTES]; // 2. Validate buffer sizes assert_eq!(pk.len(), ML_KEM_768_PUBLIC_KEY_BYTES); assert_eq!(sk.len(), ML_KEM_768_SECRET_KEY_BYTES); // 3. Call C function with temporary access let result = unsafe { pqcrystals_kyber768_ref_keypair( pk.as_mut_ptr(), // Temporary pointer access sk.as_mut_ptr(), // Rust maintains ownership ) }; // 4. Validate C function success if result != 0 { return Err(Error::KeygenFailed); } // 5. Verify output initialization (C code populated buffers) if pk.iter().all(|&b| b == 0) || sk.iter().all(|&b| b == 0) { return Err(Error::InvalidOutput); } // 6. Transfer ownership back to Rust types Ok((PublicKey::new(pk), SecretKey::new(sk))) } }
Security Testing
The FFI boundary undergoes comprehensive testing:
- Buffer Boundary Testing: Verify no out-of-bounds access
- Type Safety Validation: Ensure proper data marshalling
- Error Injection: Test error handling paths
- Fuzzing: Automated robustness testing with malformed inputs
- Memory Safety Analysis: AddressSanitizer and Valgrind testing
For detailed implementation analysis including specific safety invariants for each unsafe block, see the Unsafe Code Guide.