Skip to main content

memscope_rs/analysis/unsafe_inference/
engine.rs

1//! Unsafe Type Inference Engine v2
2//!
3//! Heuristic-based type inference for FFI/unsafe memory allocations.
4//!
5//! # Design Principles
6//!
7//! - **Six-dimensional signal model**: Size, Layout, Content, Stack, Lifetime, ValidPtr
8//! - **Memory safe**: No raw pointer dereferencing, all access through MemoryView
9//! - **O(n) complexity**: ~5-50ms for 1M allocations depending on enabled features
10//!
11//! # Phase 1 Features
12//!
13//! - Enhanced size heuristic with power-of-two signal
14//! - UTF-8 validation for String detection
15//! - Enhanced CString detection with ASCII ratio
16//! - Shannon entropy analysis for binary data
17//! - Zero-fill detection
18#![allow(dead_code)]
19
20use super::memory_view::MemoryView;
21use super::policy::{InferenceDecision, InferencePolicy};
22use super::scoring::{
23    candidate_table, content_analysis, lifetime_analysis, pointer_heuristic, size_heuristic,
24    stack_trace_analysis, vec_string_layout, Score,
25};
26
27/// Inferred type category for unsafe memory allocations.
28///
29/// Each variant represents a common pattern in FFI/unsafe code.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum TypeKind {
32    /// Rust Vec<T>: heap-allocated growable array (ptr, len, cap)
33    Vec,
34    /// Rust String: UTF-8 encoded growable string (ptr, len, cap)
35    String,
36    /// C-style null-terminated string
37    CString,
38    /// Raw pointer: *mut T, *const T, Box<T>
39    Pointer,
40    /// Fat pointer: &[T], &str, dyn Trait (data_ptr + metadata)
41    FatPtr,
42    /// Raw byte buffer: [u8], compressed/encrypted data
43    Buffer,
44    /// C struct with multiple pointer fields
45    CStruct,
46    /// Unknown type
47    Unknown,
48}
49
50impl std::fmt::Display for TypeKind {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            TypeKind::Vec => write!(f, "Vec<_>"),
54            TypeKind::String => write!(f, "String"),
55            TypeKind::CString => write!(f, "CString"),
56            TypeKind::Pointer => write!(f, "*mut c_void"),
57            TypeKind::FatPtr => write!(f, "&[T]"),
58            TypeKind::Buffer => write!(f, "[u8]"),
59            TypeKind::CStruct => write!(f, "CStruct"),
60            TypeKind::Unknown => write!(f, "unknown"),
61        }
62    }
63}
64
65/// Type inference result with a heuristic score.
66///
67/// The score is a sum of heuristic signals, **not** a probability: it is neither
68/// bounded by 100 nor meaningful as a percentage. A guess is only produced when
69/// [`InferencePolicy`] accepts the evidence; otherwise the kind is
70/// [`TypeKind::Unknown`] and the score is 0.
71#[derive(Clone, Copy, Debug)]
72pub struct TypeGuess {
73    /// Inferred type category
74    pub kind: TypeKind,
75    /// Heuristic score sum for the accepted candidate (not a percentage)
76    pub confidence: u16,
77    /// Method used for inference (for debugging/display)
78    pub method: InferenceMethod,
79}
80
81/// Method used for type inference.
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub enum InferenceMethod {
84    #[default]
85    Unknown,
86    SizeHeuristic,
87    LayoutDetection,
88    Utf8Validation,
89    ContentAnalysis,
90    EntropyAnalysis,
91    StackTraceAnalysis,
92    LifetimeAnalysis,
93    Combined,
94}
95
96impl TypeGuess {
97    /// Create a new type guess with the given kind and confidence.
98    pub fn new(kind: TypeKind, confidence: u16) -> Self {
99        Self {
100            kind,
101            confidence,
102            method: InferenceMethod::Combined,
103        }
104    }
105
106    /// Create a new type guess with method information.
107    pub fn with_method(kind: TypeKind, confidence: u16, method: InferenceMethod) -> Self {
108        Self {
109            kind,
110            confidence,
111            method,
112        }
113    }
114
115    /// Create an unknown type guess.
116    pub fn unknown() -> Self {
117        Self {
118            kind: TypeKind::Unknown,
119            confidence: 0,
120            method: InferenceMethod::Unknown,
121        }
122    }
123
124    /// Display the type guess with its heuristic score.
125    ///
126    /// The value is a score sum, not a probability, so it is never rendered as a
127    /// percentage.
128    pub fn display_with_confidence(&self) -> String {
129        if self.confidence == 0 {
130            return "-".to_string();
131        }
132        format!("{} (score {})", self.kind, self.confidence)
133    }
134}
135
136/// Main inference engine for unsafe type detection.
137///
138/// # Example
139///
140/// ```
141/// use memscope_rs::analysis::unsafe_inference::{
142///     UnsafeInferenceEngine, TypeGuess, TypeKind,
143/// };
144///
145/// let memory = vec![0u8; 24];
146/// let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 24);
147/// println!("Inferred: {} (score {})", guess.kind, guess.confidence);
148/// ```
149pub struct UnsafeInferenceEngine;
150
151impl UnsafeInferenceEngine {
152    /// Infer type from a single memory view.
153    pub fn infer_single(view: &MemoryView, size: usize) -> TypeGuess {
154        infer(view, size, None, None, None)
155    }
156
157    /// Infer type from raw bytes.
158    pub fn infer_from_bytes(data: &[u8], size: usize) -> TypeGuess {
159        let view = MemoryView::new(data);
160        infer(&view, size, None, None, None)
161    }
162
163    /// Infer type with full context.
164    pub fn infer_with_context(
165        data: &[u8],
166        size: usize,
167        stack_trace: Option<&[String]>,
168        alloc_time: Option<u64>,
169        dealloc_time: Option<u64>,
170    ) -> TypeGuess {
171        let view = MemoryView::new(data);
172        infer(&view, size, stack_trace, alloc_time, dealloc_time)
173    }
174
175    /// Infer with the default policy, keeping the abstain reason.
176    ///
177    /// Unlike [`Self::infer_from_bytes`] this returns the full decision, so a
178    /// caller can explain *why* no type was reported instead of silently showing
179    /// an empty result.
180    pub fn decide_from_bytes(data: &[u8], size: usize) -> InferenceDecision {
181        let view = MemoryView::new(data);
182        infer_decision(&view, size, None, None, None, &InferencePolicy::PRELIMINARY)
183    }
184
185    /// Run inference with an explicit acceptance policy.
186    pub fn decide_with_policy(
187        data: &[u8],
188        size: usize,
189        policy: &InferencePolicy,
190    ) -> InferenceDecision {
191        let view = MemoryView::new(data);
192        infer_decision(&view, size, None, None, None, policy)
193    }
194
195    /// Run inference on multiple records.
196    pub fn run(records: &mut [InferenceRecord]) {
197        for record in records.iter_mut() {
198            if let Some(ref memory) = record.memory {
199                let view = MemoryView::new(memory);
200                record.inferred = Some(infer(
201                    &view,
202                    record.size,
203                    record.stack_trace.as_deref(),
204                    record.alloc_time,
205                    record.dealloc_time,
206                ));
207            }
208        }
209    }
210}
211
212/// Record for batch inference.
213pub struct InferenceRecord {
214    /// Memory address of the allocation
215    pub ptr: usize,
216    /// Size in bytes
217    pub size: usize,
218    /// Optional memory content snapshot
219    pub memory: Option<Vec<u8>>,
220    /// Optional stack trace at allocation time
221    pub stack_trace: Option<Vec<String>>,
222    /// Optional allocation timestamp (nanoseconds)
223    pub alloc_time: Option<u64>,
224    /// Optional deallocation timestamp (nanoseconds)
225    pub dealloc_time: Option<u64>,
226    /// Inference result
227    pub inferred: Option<TypeGuess>,
228}
229
230impl InferenceRecord {
231    /// Create a new inference record with minimal information.
232    pub fn new(ptr: usize, size: usize, memory: Option<Vec<u8>>) -> Self {
233        Self {
234            ptr,
235            size,
236            memory,
237            stack_trace: None,
238            alloc_time: None,
239            dealloc_time: None,
240            inferred: None,
241        }
242    }
243
244    /// Create a new inference record with full information.
245    pub fn with_context(
246        ptr: usize,
247        size: usize,
248        memory: Option<Vec<u8>>,
249        stack_trace: Option<Vec<String>>,
250        alloc_time: Option<u64>,
251        dealloc_time: Option<u64>,
252    ) -> Self {
253        Self {
254            ptr,
255            size,
256            memory,
257            stack_trace,
258            alloc_time,
259            dealloc_time,
260            inferred: None,
261        }
262    }
263}
264
265/// Main inference function combining all heuristic dimensions.
266fn infer(
267    view: &MemoryView,
268    size: usize,
269    stack_trace: Option<&[String]>,
270    alloc_time: Option<u64>,
271    dealloc_time: Option<u64>,
272) -> TypeGuess {
273    decision_to_guess(&infer_decision(
274        view,
275        size,
276        stack_trace,
277        alloc_time,
278        dealloc_time,
279        &InferencePolicy::PRELIMINARY,
280    ))
281}
282
283/// Run every heuristic dimension and apply the acceptance policy.
284fn infer_decision(
285    view: &MemoryView,
286    size: usize,
287    stack_trace: Option<&[String]>,
288    alloc_time: Option<u64>,
289    dealloc_time: Option<u64>,
290    policy: &InferencePolicy,
291) -> InferenceDecision {
292    let mut score = Score::default();
293
294    // Dimension 1: Size heuristic with power-of-two signal
295    size_heuristic(size, &mut score);
296
297    // Dimension 2: Layout detection (ptr/len/cap structure)
298    let header_only =
299        vec_string_layout(view, &mut score) && view.len() == 3 * std::mem::size_of::<usize>();
300
301    // Dimension 3: Content analysis (CString, entropy, zero-fill)
302    // Header bytes describe a pointer and lengths, not the pointed-to payload.
303    // ASLR can make the address prefix look like printable NUL-terminated text.
304    if !header_only {
305        content_analysis(view, &mut score);
306    }
307
308    // Dimension 4: Pointer-based heuristics (buffer vs cstruct)
309    pointer_heuristic(view, &mut score);
310
311    // Dimension 5: Stack trace analysis (optional, high signal)
312    stack_trace_analysis(stack_trace, &mut score);
313
314    // Dimension 6: Lifetime analysis (optional, auxiliary signal)
315    lifetime_analysis(alloc_time, dealloc_time, &mut score);
316
317    finalize(&score, policy)
318}
319
320/// Method that produced a given kind, so an accepted decision can report its
321/// strongest dimension without threading it through the decision struct.
322fn method_for(kind: TypeKind) -> InferenceMethod {
323    match kind {
324        TypeKind::Vec => InferenceMethod::LayoutDetection,
325        TypeKind::String => InferenceMethod::Utf8Validation,
326        TypeKind::CString => InferenceMethod::ContentAnalysis,
327        TypeKind::Pointer | TypeKind::FatPtr => InferenceMethod::SizeHeuristic,
328        TypeKind::Buffer => InferenceMethod::EntropyAnalysis,
329        TypeKind::CStruct => InferenceMethod::Combined,
330        TypeKind::Unknown => InferenceMethod::Unknown,
331    }
332}
333
334/// Project an accepted decision into the legacy `TypeGuess` shape.
335fn decision_to_guess(decision: &InferenceDecision) -> TypeGuess {
336    match decision.predicted {
337        Some(kind) => TypeGuess::with_method(kind, decision.raw_score, method_for(kind)),
338        None => TypeGuess::unknown(),
339    }
340}
341
342/// Finalize inference by applying the acceptance policy to the score vector.
343///
344/// A type is only returned when the winner reaches `min_score` **and** leads the
345/// runner-up by `min_margin`; otherwise the decision abstains and records why.
346fn finalize(score: &Score, policy: &InferencePolicy) -> InferenceDecision {
347    let mut table = candidate_table(score);
348    table.sort_by_key(|candidate| std::cmp::Reverse(candidate.1));
349
350    let (best_kind, best_score, _) = table[0];
351    let (runner_kind, runner_score, _) = table[1];
352
353    if best_score < policy.min_score {
354        return InferenceDecision {
355            predicted: None,
356            best_candidate: best_kind,
357            raw_score: best_score,
358            runner_up_score: runner_score,
359            abstain_reason: Some(format!(
360                "best candidate {best_kind} scored {best_score}, below min_score {}",
361                policy.min_score
362            )),
363        };
364    }
365
366    if best_score.saturating_sub(runner_score) < policy.min_margin {
367        return InferenceDecision {
368            predicted: None,
369            best_candidate: best_kind,
370            raw_score: best_score,
371            runner_up_score: runner_score,
372            abstain_reason: Some(format!(
373                "ambiguous: {best_kind} scored {best_score} vs {runner_kind} {runner_score}, \
374                 margin below min_margin {}",
375                policy.min_margin
376            )),
377        };
378    }
379
380    InferenceDecision {
381        predicted: Some(best_kind),
382        best_candidate: best_kind,
383        raw_score: best_score,
384        runner_up_score: runner_score,
385        abstain_reason: None,
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    fn create_vec_memory(ptr: usize, len: usize, cap: usize) -> Vec<u8> {
394        let mut data = vec![0u8; 24];
395        data[..8].copy_from_slice(&ptr.to_le_bytes());
396        data[8..16].copy_from_slice(&len.to_le_bytes());
397        data[16..24].copy_from_slice(&cap.to_le_bytes());
398        data
399    }
400
401    fn create_string_memory(ptr: usize, len: usize, cap: usize) -> Vec<u8> {
402        create_vec_memory(ptr, len, cap)
403    }
404
405    fn create_cstring_memory(content: &[u8]) -> Vec<u8> {
406        let mut data = content.to_vec();
407        data.push(0);
408        data
409    }
410
411    #[test]
412    #[cfg(target_os = "macos")]
413    fn test_infer_vec_with_large_capacity() {
414        let memory = create_vec_memory(0x10000, 10, 100);
415        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 24);
416
417        assert_eq!(guess.kind, TypeKind::Vec);
418        assert!(guess.confidence >= 60);
419    }
420
421    #[test]
422    #[cfg(target_os = "macos")]
423    fn test_infer_vec_with_power_of_two_capacity() {
424        let memory = create_vec_memory(0x10000, 50, 64);
425        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 24);
426
427        // Power-of-two capacity should boost Vec score
428        assert!(guess.kind == TypeKind::Vec || guess.kind == TypeKind::String);
429    }
430
431    #[test]
432    #[cfg(target_os = "macos")]
433    fn test_infer_string_with_small_spare() {
434        let memory = create_string_memory(0x10000, 10, 12);
435        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 24);
436
437        // Small spare capacity should favor String
438        assert!(guess.kind == TypeKind::String || guess.kind == TypeKind::Vec);
439    }
440
441    #[test]
442    fn test_infer_cstring_printable() {
443        let memory = create_cstring_memory(b"hello world");
444        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 12);
445
446        assert_eq!(guess.kind, TypeKind::CString);
447        assert!(guess.confidence >= 70);
448    }
449
450    #[test]
451    fn test_infer_cstring_mixed_content() {
452        let memory = create_cstring_memory(&[0x30, 0x31, 0x80, 0x90, 0x20]);
453        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 6);
454
455        // Mixed content should have lower CString score
456        assert!(guess.confidence < 70);
457    }
458
459    #[test]
460    fn test_infer_pointer_size_8() {
461        let memory = [0u8; 8];
462        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 8);
463
464        assert_eq!(guess.kind, TypeKind::Pointer);
465        assert!(guess.confidence >= 30);
466    }
467
468    #[test]
469    #[cfg(target_os = "macos")]
470    fn test_infer_fat_ptr_size_16() {
471        // Fat pointer: data_ptr + metadata (e.g., length for &[T])
472        // Create a pattern that looks like a fat pointer: valid ptr + small length
473        let mut memory = [0u8; 16];
474        let ptr: usize = 0x10000;
475        let len: usize = 100;
476        memory[0..8].copy_from_slice(&ptr.to_le_bytes());
477        memory[8..16].copy_from_slice(&len.to_le_bytes());
478
479        // A 16-byte block with a valid pointer is a FatPtr candidate, but the
480        // zero-filled tail scores the buffer dimension just as high. The engine
481        // must abstain instead of resolving the tie by table order.
482        let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, 16);
483        assert!(
484            !decision.is_accepted(),
485            "a FatPtr/buffer tie must abstain, got {:?}",
486            decision.predicted
487        );
488        assert!(
489            decision.abstain_reason.is_some(),
490            "abstaining must always carry a reason"
491        );
492    }
493
494    #[test]
495    #[cfg(target_os = "macos")]
496    fn test_infer_cstruct_multiple_pointers() {
497        let mut memory = vec![1u8; 40];
498        let ptr1: usize = 0x10000;
499        let ptr2: usize = 0x20000;
500        memory[0..8].copy_from_slice(&ptr1.to_le_bytes());
501        memory[24..32].copy_from_slice(&ptr2.to_le_bytes());
502
503        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 40);
504
505        assert_eq!(guess.kind, TypeKind::CStruct);
506        assert!(guess.confidence >= 30);
507    }
508
509    #[test]
510    fn test_power_of_two_signal() {
511        // Size 64 (power of two) should get Vec/Buffer boost
512        let memory = vec![0u8; 64];
513        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 64);
514
515        // Should have some confidence from power-of-two signal
516        assert!(guess.confidence > 0 || guess.kind == TypeKind::Unknown);
517    }
518
519    #[test]
520    fn test_zero_filled_buffer() {
521        let memory = [0u8; 64];
522        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 64);
523
524        // Zero-filled should boost buffer or cstruct
525        assert!(
526            guess.kind == TypeKind::Buffer
527                || guess.kind == TypeKind::CStruct
528                || guess.kind == TypeKind::Unknown
529        );
530    }
531
532    #[test]
533    fn test_type_guess_display() {
534        let guess = TypeGuess::new(TypeKind::Vec, 85);
535        assert_eq!(
536            guess.display_with_confidence(),
537            "Vec<_> (score 85)",
538            "the heuristic score must not be rendered as a percentage"
539        );
540
541        let unknown = TypeGuess::unknown();
542        assert_eq!(unknown.display_with_confidence(), "-");
543    }
544
545    #[test]
546    fn test_run_on_records() {
547        let vec_memory = create_vec_memory(0x10000, 10, 100);
548        let mut records = vec![
549            InferenceRecord::new(0x1000, 24, Some(vec_memory)),
550            InferenceRecord::new(0x2000, 8, None),
551        ];
552
553        UnsafeInferenceEngine::run(&mut records);
554
555        assert!(records[0].inferred.is_some());
556        assert!(records[1].inferred.is_none());
557    }
558
559    #[test]
560    fn test_multiple_nulls_not_cstring() {
561        // Multiple null bytes should reduce CString score
562        let memory = [
563            b'h', b'e', b'l', b'l', b'o', 0, b'w', b'o', b'r', b'l', b'd', 0,
564        ];
565        let single_nul =
566            UnsafeInferenceEngine::decide_from_bytes(&create_cstring_memory(b"hello world"), 12);
567        let multiple_nuls = UnsafeInferenceEngine::decide_from_bytes(&memory, 12);
568
569        // A second NUL means the buffer is not one terminated string, so the
570        // CString evidence must drop below the single-terminator case.
571        assert!(
572            multiple_nuls.raw_score < single_nul.raw_score,
573            "multiple NULs must lower the CString evidence ({} vs {})",
574            multiple_nuls.raw_score,
575            single_nul.raw_score
576        );
577    }
578
579    #[test]
580    fn test_utf8_validation_printable_string() {
581        // Valid UTF-8 with high printable ratio
582        let memory = b"Hello, World! This is a test string.".to_vec();
583        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, memory.len());
584
585        // Should be strongly identified as String due to UTF-8 validation
586        assert_eq!(guess.kind, TypeKind::String);
587        assert!(guess.confidence >= 90);
588        assert_eq!(guess.method, InferenceMethod::Utf8Validation);
589    }
590
591    #[test]
592    fn test_utf8_validation_non_printable() {
593        // Valid UTF-8 but with non-printable characters
594        let memory = vec![0xC2, 0x80, 0xC2, 0x81, 0xC2, 0x82]; // Valid UTF-8 control chars
595        let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, memory.len());
596
597        // Valid UTF-8 made of control characters is neither a String nor evidence
598        // for a Vec: the engine must abstain instead of inventing a type.
599        assert!(
600            !decision.is_accepted(),
601            "an undecidable block must not be reported as a concrete type, got {:?}",
602            decision.predicted
603        );
604        assert!(
605            decision.abstain_reason.is_some(),
606            "the abstain reason must be reported"
607        );
608    }
609
610    /// Objective: Verify that non-ASCII text is not pushed towards Vec by an
611    /// ASCII-only printable check (plan §11.2).
612    /// Invariants: a UTF-8 CJK string is accepted as String.
613    #[test]
614    fn test_utf8_validation_cjk_string() {
615        let memory = "内存分析工具:数据准确性验证".as_bytes().to_vec();
616        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, memory.len());
617
618        assert_eq!(
619            guess.kind,
620            TypeKind::String,
621            "CJK text is text: an ASCII-only printable ratio must not turn it into Vec"
622        );
623    }
624
625    #[test]
626    fn test_utf8_validation_invalid() {
627        // Invalid UTF-8 sequence
628        let memory = vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA];
629        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, memory.len());
630
631        // Should not be String due to invalid UTF-8
632        assert_ne!(guess.kind, TypeKind::String);
633    }
634
635    #[test]
636    fn test_large_data_buffer_boost() {
637        // Data > 4KB should get buffer boost without entropy calculation
638        let memory = vec![0u8; 5000];
639        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 5000);
640
641        // Should be buffer due to size
642        assert!(guess.kind == TypeKind::Buffer || guess.kind == TypeKind::CStruct);
643    }
644
645    #[test]
646    fn test_inference_method_tracking() {
647        // Test that inference method is properly tracked
648        let string_memory = b"test string content".to_vec();
649        let guess = UnsafeInferenceEngine::infer_from_bytes(&string_memory, string_memory.len());
650
651        // String should use Utf8Validation method
652        assert_eq!(guess.kind, TypeKind::String);
653        assert_eq!(guess.method, InferenceMethod::Utf8Validation);
654    }
655
656    #[test]
657    fn test_stack_trace_vec_detection() {
658        let memory = vec![0u8; 24];
659        let stack = vec![
660            "alloc::vec::Vec::push".to_string(),
661            "my_app::process".to_string(),
662        ];
663        let guess =
664            UnsafeInferenceEngine::infer_with_context(&memory, 24, Some(&stack), None, None);
665
666        // Stack trace with Vec::push should boost Vec score
667        assert!(guess.kind == TypeKind::Vec || guess.kind == TypeKind::Unknown);
668    }
669
670    #[test]
671    fn test_stack_trace_string_detection() {
672        let memory = b"test".to_vec();
673        let stack = vec!["alloc::string::String::push".to_string()];
674        let guess = UnsafeInferenceEngine::infer_with_context(&memory, 4, Some(&stack), None, None);
675
676        // Stack trace with String::push should boost String score
677        assert!(guess.kind == TypeKind::String || guess.kind == TypeKind::Unknown);
678    }
679
680    #[test]
681    fn test_stack_trace_cstring_detection() {
682        let memory = b"hello\0".to_vec();
683        let stack = vec!["std::ffi::c_str::CString::new".to_string()];
684        let guess = UnsafeInferenceEngine::infer_with_context(&memory, 6, Some(&stack), None, None);
685
686        // Stack trace with CString::new should boost CString score
687        assert!(guess.kind == TypeKind::CString || guess.kind == TypeKind::String);
688    }
689
690    #[test]
691    fn test_stack_trace_ffi_detection() {
692        let memory = vec![0u8; 64];
693        let stack = vec!["libc::malloc".to_string()];
694        let guess =
695            UnsafeInferenceEngine::infer_with_context(&memory, 64, Some(&stack), None, None);
696
697        // Stack trace with malloc should boost CStruct/Buffer score
698        assert!(
699            guess.kind == TypeKind::CStruct
700                || guess.kind == TypeKind::Buffer
701                || guess.kind == TypeKind::Unknown
702        );
703    }
704
705    #[test]
706    fn test_lifetime_transient_allocation() {
707        let memory = b"test".to_vec();
708        let alloc_time = Some(1000);
709        let dealloc_time = Some(1_000_500); // 0.5ms lifetime
710        let guess =
711            UnsafeInferenceEngine::infer_with_context(&memory, 4, None, alloc_time, dealloc_time);
712
713        // Transient allocation should boost String/Vec score
714        assert!(guess.confidence > 0 || guess.kind == TypeKind::Unknown);
715    }
716
717    #[test]
718    fn test_lifetime_long_lived() {
719        let memory = vec![0u8; 64];
720        let alloc_time = Some(1000);
721        let dealloc_time = Some(15_000_000_000); // 15s lifetime
722        let guess =
723            UnsafeInferenceEngine::infer_with_context(&memory, 64, None, alloc_time, dealloc_time);
724
725        // Long-lived allocation should boost Buffer score
726        assert!(guess.confidence > 0 || guess.kind == TypeKind::Unknown);
727    }
728
729    #[test]
730    fn test_combined_stack_and_lifetime() {
731        let memory = vec![0u8; 24];
732        let stack = vec!["alloc::vec::Vec::new".to_string()];
733        let alloc_time = Some(1000);
734        let dealloc_time = Some(1_000_500); // 0.5ms
735        let guess = UnsafeInferenceEngine::infer_with_context(
736            &memory,
737            24,
738            Some(&stack),
739            alloc_time,
740            dealloc_time,
741        );
742
743        // Combined signals should give higher confidence
744        assert!(guess.confidence > 0 || guess.kind == TypeKind::Unknown);
745    }
746
747    #[test]
748    fn test_inference_record_with_context() {
749        let memory = Some(vec![0u8; 24]);
750        let stack = Some(vec!["alloc::vec::Vec::new".to_string()]);
751        let record =
752            InferenceRecord::with_context(0x1000, 24, memory, stack, Some(1000), Some(2000));
753
754        assert_eq!(record.ptr, 0x1000);
755        assert_eq!(record.size, 24);
756        assert!(record.stack_trace.is_some());
757        assert!(record.alloc_time.is_some());
758        assert!(record.dealloc_time.is_some());
759    }
760}
761
762/// Real data tests using actual type memory layouts.
763#[cfg(test)]
764mod real_data_tests {
765    use super::*;
766
767    /// Get memory representation of a real Vec.
768    fn vec_to_memory<T>(v: &Vec<T>) -> Vec<u8> {
769        let ptr = v.as_ptr() as usize;
770        let len = v.len();
771        let cap = v.capacity();
772        let mut memory = vec![0u8; 24];
773        memory[..8].copy_from_slice(&ptr.to_le_bytes());
774        memory[8..16].copy_from_slice(&len.to_le_bytes());
775        memory[16..24].copy_from_slice(&cap.to_le_bytes());
776        memory
777    }
778
779    /// Get memory representation of a real String.
780    fn string_to_memory(s: &String) -> Vec<u8> {
781        let ptr = s.as_ptr() as usize;
782        let len = s.len();
783        let cap = s.capacity();
784        let mut memory = vec![0u8; 24];
785        memory[..8].copy_from_slice(&ptr.to_le_bytes());
786        memory[8..16].copy_from_slice(&len.to_le_bytes());
787        memory[16..24].copy_from_slice(&cap.to_le_bytes());
788        memory
789    }
790
791    /// Get memory representation of a Box.
792    /// Assert that the engine either answers within `allowed` or abstains *with a
793    /// reason*.
794    ///
795    /// Real heap bytes are not always identifiable (a container header without its
796    /// payload, a pointer that happens to look like text), so "no confident answer"
797    /// is a valid outcome — but a confident answer outside `allowed` is not.
798    fn assert_kind_or_abstain(
799        decision: &crate::analysis::unsafe_inference::InferenceDecision,
800        allowed: &[TypeKind],
801    ) {
802        match decision.predicted {
803            Some(kind) => assert!(
804                allowed.contains(&kind),
805                "unexpected confident classification {kind:?}, allowed {allowed:?}"
806            ),
807            None => assert!(
808                decision.abstain_reason.is_some(),
809                "abstaining must always carry a reason"
810            ),
811        }
812    }
813
814    fn box_to_memory<T>(b: &T) -> Vec<u8> {
815        let ptr = b as *const T as usize;
816        let mut memory = vec![0u8; 8];
817        memory[..8].copy_from_slice(&ptr.to_le_bytes());
818        memory
819    }
820
821    #[test]
822    fn test_real_vec_i32() {
823        let v = vec![1i32, 2, 3, 4, 5];
824        let memory = vec_to_memory(&v);
825        let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, 24);
826        assert_kind_or_abstain(&decision, &[TypeKind::Vec, TypeKind::String]);
827    }
828
829    #[test]
830    fn test_real_vec_u8() {
831        let v = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
832        let memory = vec_to_memory(&v);
833        let guess = UnsafeInferenceEngine::infer_from_bytes(&memory, 24);
834
835        // Vec<u8> has same layout as String
836        // A `[ptr, len, cap]` header alone is indistinguishable between `Vec<u8>` and
837        // `String`; the discriminating information is in the payload, which this view
838        // does not contain. Accepting either *or* abstaining with a reason is the
839        // honest outcome — inventing certainty is not.
840        let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, 24);
841        assert_kind_or_abstain(&decision, &[TypeKind::Vec, TypeKind::String]);
842        let _ = guess;
843    }
844
845    #[test]
846    fn test_real_string() {
847        let s = String::from("Hello, World!");
848        let memory = string_to_memory(&s);
849        // Address bytes can also resemble text. Like Vec<u8>, an untyped header
850        // may require abstention; a confident unrelated classification is an error.
851        let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, 24);
852        assert_kind_or_abstain(&decision, &[TypeKind::String, TypeKind::Vec]);
853        // macOS uses static address bounds. Linux instead checks mapped regions,
854        // so this synthetic, never-dereferenced address is only a macOS fixture.
855        #[cfg(target_os = "macos")]
856        {
857            let mut memory = memory;
858            memory[..8].copy_from_slice(&0x6000_4342_4140u64.to_le_bytes());
859            let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, 24);
860            assert_kind_or_abstain(&decision, &[TypeKind::String, TypeKind::Vec]);
861        }
862    }
863
864    #[test]
865    fn test_real_string_with_capacity() {
866        let mut s = String::with_capacity(100);
867        s.push_str("Hello");
868        let memory = string_to_memory(&s);
869        let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, 24);
870        assert_kind_or_abstain(&decision, &[TypeKind::Vec, TypeKind::String]);
871    }
872
873    #[test]
874    fn test_real_box_i32() {
875        let b = Box::new(42i32);
876        let memory = box_to_memory(&*b);
877        // The bytes here are a live heap address, so they vary run to run: any of
878        // the 8-byte layouts is plausible, and the engine is allowed to abstain
879        // when the signals tie. What must never happen is a confident answer with
880        // no supporting evidence, so the decision is what gets asserted.
881        let decision = UnsafeInferenceEngine::decide_from_bytes(&memory, 8);
882        assert_kind_or_abstain(
883            &decision,
884            &[
885                TypeKind::Pointer,
886                TypeKind::Vec,
887                TypeKind::String,
888                TypeKind::CString,
889                TypeKind::Buffer,
890            ],
891        );
892    }
893
894    #[test]
895    fn test_real_string_content() {
896        let s = "Hello, World! This is a test string for type inference.";
897        let guess = UnsafeInferenceEngine::infer_from_bytes(s.as_bytes(), s.len());
898
899        assert_eq!(guess.kind, TypeKind::String);
900        assert!(guess.confidence >= 90);
901    }
902
903    #[test]
904    fn test_real_cstring_content() {
905        let cstr = std::ffi::CString::new("Hello, C World!").unwrap();
906        let bytes = cstr.as_bytes_with_nul();
907        let guess = UnsafeInferenceEngine::infer_from_bytes(bytes, bytes.len());
908
909        assert_eq!(guess.kind, TypeKind::CString);
910        assert!(guess.confidence >= 70);
911    }
912
913    #[test]
914    fn test_real_binary_data() {
915        let binary: Vec<u8> = (0..=255).collect();
916        let guess = UnsafeInferenceEngine::infer_from_bytes(&binary, binary.len());
917
918        // High entropy binary data: `[u8]` and `Vec<u8>` describe the same memory, so
919        // either name is acceptable, as is an abstention with a reason.
920        let decision = UnsafeInferenceEngine::decide_from_bytes(&binary, binary.len());
921        assert_kind_or_abstain(&decision, &[TypeKind::Buffer, TypeKind::Vec]);
922        let _ = guess;
923    }
924
925    #[test]
926    fn test_real_zero_filled() {
927        let zeros = vec![0u8; 1024];
928        let guess = UnsafeInferenceEngine::infer_from_bytes(&zeros, 1024);
929
930        // Zero-filled large data
931        assert!(guess.kind == TypeKind::Buffer || guess.kind == TypeKind::CStruct);
932    }
933
934    #[test]
935    fn test_real_vec_with_stack_trace() {
936        let v = vec![1i32, 2, 3, 4, 5];
937        let memory = vec_to_memory(&v);
938        let stack = vec!["alloc::vec::Vec::push".to_string()];
939        let guess =
940            UnsafeInferenceEngine::infer_with_context(&memory, 24, Some(&stack), None, None);
941
942        assert_eq!(guess.kind, TypeKind::Vec);
943        // Stack trace should boost confidence
944        assert!(guess.confidence >= 80);
945    }
946
947    #[test]
948    fn test_real_string_with_stack_trace() {
949        let s = String::from("Hello");
950        let memory = string_to_memory(&s);
951        let stack = vec!["alloc::string::String::push_str".to_string()];
952        let guess =
953            UnsafeInferenceEngine::infer_with_context(&memory, 24, Some(&stack), None, None);
954
955        assert!(guess.kind == TypeKind::String || guess.kind == TypeKind::Vec);
956    }
957
958    #[test]
959    #[cfg(target_os = "macos")]
960    fn test_real_struct_with_pointers() {
961        struct TestStruct {
962            _ptr1: *const u8,
963            _ptr2: *const u8,
964            _value: u64,
965        }
966
967        let s = TestStruct {
968            _ptr1: &0u8,
969            _ptr2: &1u8,
970            _value: 42,
971        };
972
973        let memory = unsafe {
974            std::slice::from_raw_parts(
975                &s as *const TestStruct as *const u8,
976                std::mem::size_of::<TestStruct>(),
977            )
978        };
979
980        let guess = UnsafeInferenceEngine::infer_from_bytes(memory, memory.len());
981
982        // Struct is 24 bytes (2 pointers + 1 u64), same as Vec/String layout
983        // So it could be detected as Vec, String, or CStruct depending on pointer values
984        assert!(
985            guess.kind == TypeKind::CStruct
986                || guess.kind == TypeKind::Buffer
987                || guess.kind == TypeKind::Pointer
988                || guess.kind == TypeKind::Vec
989                || guess.kind == TypeKind::String
990                || guess.kind == TypeKind::Unknown,
991            "Got {:?}",
992            guess.kind
993        );
994    }
995}