Skip to main content

memscope_rs/analysis/unsafe_inference/
memory_view.rs

1//! Memory View - Safe Memory Access Layer
2//!
3//! Provides safe, bounds-checked access to memory content for type inference.
4//! All memory access goes through this layer - no raw pointer dereferencing.
5//!
6//! # ValidRegions
7//!
8//! Uses dynamic mmap region detection with static fallback:
9//! - Linux: Reads `/proc/self/maps` for precise regions
10//! - Other platforms: Uses static address range bounds
11
12use std::sync::RwLock;
13
14// Static fallback bounds for different platforms
15#[cfg(all(target_pointer_width = "64", target_os = "linux"))]
16const MAX_USER_ADDR: usize = 0x0000_7fff_ffff_f000;
17
18#[cfg(all(target_pointer_width = "64", target_os = "macos"))]
19const MAX_USER_ADDR: usize = 0x0000_7fff_ffff_f000;
20
21#[cfg(all(target_pointer_width = "64", target_os = "windows"))]
22const MAX_USER_ADDR: usize = 0x0000_7fff_ffff_0000;
23
24#[cfg(all(
25    target_pointer_width = "64",
26    not(any(target_os = "linux", target_os = "macos", target_os = "windows"))
27))]
28const MAX_USER_ADDR: usize = 0x0000_7fff_ffff_ffff;
29
30#[cfg(target_pointer_width = "32")]
31const MAX_USER_ADDR: usize = 0x7fff_ffff;
32
33// Windows-specific heap end addresses for 32-bit vs 64-bit
34#[cfg(all(target_os = "windows", target_pointer_width = "64"))]
35const MAX_HEAP_END: usize = 0x7FFF_FFFF_FFFF_FFFF;
36
37#[cfg(all(target_os = "windows", target_pointer_width = "32"))]
38const MAX_HEAP_END: usize = 0x7FFF_FFFF;
39
40const MIN_VALID_ADDR: usize = 0x1000;
41
42/// Represents a valid memory region from process memory map.
43#[derive(Clone, Debug)]
44pub struct MemoryRegion {
45    pub start: usize,
46    pub end: usize,
47}
48
49/// Valid memory regions for pointer validation.
50///
51/// Uses dynamic detection on supported platforms with static fallback.
52#[derive(Clone, Debug, Default)]
53pub struct ValidRegions {
54    regions: Vec<MemoryRegion>,
55    is_dynamic: bool,
56}
57
58impl ValidRegions {
59    /// Create empty regions (will use static bounds).
60    pub fn empty() -> Self {
61        Self {
62            regions: Vec::new(),
63            is_dynamic: false,
64        }
65    }
66
67    /// Create from a list of memory regions.
68    pub fn from_regions(mut regions: Vec<MemoryRegion>) -> Self {
69        // Sort regions by start address for partition_point to work correctly
70        regions.sort_by_key(|r| r.start);
71        Self {
72            regions,
73            is_dynamic: true,
74        }
75    }
76
77    /// Check if an address falls within valid regions.
78    ///
79    /// If dynamic regions are available, uses precise checking.
80    /// Otherwise, falls back to static bounds.
81    pub fn contains(&self, addr: usize) -> bool {
82        if addr <= MIN_VALID_ADDR {
83            return false;
84        }
85
86        if self.is_dynamic && !self.regions.is_empty() {
87            // Use partition_point to find the first region where start > addr
88            // Then check if the previous region contains addr
89            let idx = self.regions.partition_point(|region| region.start <= addr);
90
91            if idx > 0 {
92                let region = &self.regions[idx - 1];
93                return addr < region.end;
94            }
95            false
96        } else {
97            // Static fallback
98            addr < MAX_USER_ADDR
99        }
100    }
101
102    /// Get the number of regions.
103    pub fn len(&self) -> usize {
104        self.regions.len()
105    }
106
107    /// Check if regions are empty.
108    pub fn is_empty(&self) -> bool {
109        self.regions.is_empty()
110    }
111
112    /// Check if using dynamic detection.
113    pub fn is_dynamic(&self) -> bool {
114        self.is_dynamic
115    }
116
117    /// Debug dump regions to stderr
118    #[cfg(test)]
119    pub fn debug_dump(&self) {
120        eprintln!("ValidRegions (is_dynamic={}):", self.is_dynamic);
121        for (i, region) in self.regions.iter().enumerate() {
122            eprintln!("  Region {}: 0x{:x} - 0x{:x}", i, region.start, region.end);
123        }
124    }
125}
126
127/// Global cached valid regions.
128static VALID_REGIONS: RwLock<Option<ValidRegions>> = RwLock::new(None);
129
130/// Merge overlapping or adjacent memory regions.
131#[cfg(target_os = "linux")]
132fn merge_regions(regions: Vec<MemoryRegion>) -> Vec<MemoryRegion> {
133    if regions.is_empty() {
134        return regions;
135    }
136
137    let mut merged: Vec<MemoryRegion> = Vec::with_capacity(regions.len());
138    // Use first() for safer access pattern
139    let mut current = regions
140        .first()
141        .expect("regions should not be empty after is_empty check")
142        .clone();
143
144    for region in regions.into_iter().skip(1) {
145        if region.start < current.end {
146            current.end = current.end.max(region.end);
147        } else {
148            merged.push(current);
149            current = region;
150        }
151    }
152    merged.push(current);
153
154    merged
155}
156
157/// Get valid memory regions for the current process.
158///
159/// Platform-specific implementation:
160/// - Linux: Reads `/proc/self/maps`
161/// - Other: Returns empty (uses static bounds)
162#[cfg(target_os = "linux")]
163fn get_valid_regions_impl() -> ValidRegions {
164    use std::fs;
165
166    let content = match fs::read_to_string("/proc/self/maps") {
167        Ok(c) => c,
168        Err(_) => {
169            // Fallback: use conservative estimates if /proc/self/maps is unavailable
170            return get_conservative_regions();
171        }
172    };
173
174    let mut regions: Vec<MemoryRegion> = content
175        .lines()
176        .filter_map(|line| {
177            let parts: Vec<&str> = line.split_whitespace().collect();
178            if parts.is_empty() {
179                return None;
180            }
181
182            // Parse address range (e.g., "7f1234567000-7f1234568000")
183            let range: Vec<&str> = parts[0].split('-').collect();
184            let (start_str, end_str) = match (range.first(), range.get(1)) {
185                (Some(&s), Some(&e)) => (s, e),
186                _ => return None,
187            };
188
189            let start = usize::from_str_radix(start_str, 16).ok()?;
190            let end = usize::from_str_radix(end_str, 16).ok()?;
191
192            // Filter to readable regions only (r-- or r-x or rw-)
193            let perms = parts.get(1)?;
194            if !perms.starts_with('r') {
195                return None;
196            }
197
198            Some(MemoryRegion { start, end })
199        })
200        .collect();
201
202    // Sort by start address for binary search
203    regions.sort_by_key(|r| r.start);
204
205    // Merge overlapping/adjacent regions
206    regions = merge_regions(regions);
207
208    // If no regions found, use conservative estimates
209    if regions.is_empty() {
210        return get_conservative_regions();
211    }
212
213    // /proc/self/maps already includes stack, heap, and all mapped regions
214    // No need to add additional regions
215    ValidRegions::from_regions(regions)
216}
217
218/// Get conservative memory regions as fallback
219#[cfg(target_os = "linux")]
220fn get_conservative_regions() -> ValidRegions {
221    let regions = vec![MemoryRegion {
222        start: 0x10000,
223        end: 0x7FFF_FFFF_FFFF_FFFF, // x64 address space
224    }];
225
226    ValidRegions::from_regions(regions)
227}
228
229/// Get valid memory regions for the current process (Windows).
230///
231/// Uses a conservative approach to detect valid memory regions:
232/// - Single wide region covering entire user-space address range
233#[cfg(target_os = "windows")]
234fn get_valid_regions_impl() -> ValidRegions {
235    let mut regions = Vec::new();
236
237    // Add single wide region covering entire user-space
238    // Windows allocators can place memory anywhere in the address space
239    regions.push(MemoryRegion {
240        start: 0x10000,
241        end: MAX_HEAP_END, // Platform-specific: 64-bit or 32-bit
242    });
243
244    ValidRegions::from_regions(regions)
245}
246
247/// Get valid memory regions for the current process (macOS).
248///
249/// Uses a conservative approach to detect valid memory regions:
250/// - Stack region: 8MB below and above current stack pointer
251/// - Heap regions: multiple ranges to cover different allocators
252#[cfg(target_os = "macos")]
253fn get_valid_regions_impl() -> ValidRegions {
254    // Add very wide heap region to cover all possible allocations
255    // macOS can allocate memory in various ranges depending on the allocator
256    // This covers the entire user-space address range
257    let regions = vec![MemoryRegion {
258        start: 0x1000,              // Start from a low address
259        end: 0x7FFF_FFFF_FFFF_FFFF, // Up to max 64-bit address
260    }];
261
262    // Note: We use a single wide region instead of separate stack/heap regions
263    // because macOS allocators can place memory anywhere in the address space.
264    // The stack is already covered by this wide range.
265
266    ValidRegions::from_regions(regions)
267}
268
269/// Get valid memory regions for the current process (non-Linux, non-Windows, non-macOS).
270/// Uses conservative approach as fallback for unknown platforms.
271#[cfg(all(
272    not(target_os = "linux"),
273    not(target_os = "windows"),
274    not(target_os = "macos")
275))]
276fn get_valid_regions_impl() -> ValidRegions {
277    let mut regions = Vec::new();
278
279    // Add single wide region covering entire user-space
280    regions.push(MemoryRegion {
281        start: 0x10000,
282        end: 0x7FF_FFFFF_FFFF_FFFF,
283    });
284
285    ValidRegions::from_regions(regions)
286}
287
288/// Get cached valid regions, initializing if needed.
289pub fn get_valid_regions() -> ValidRegions {
290    // Fast path: check if already initialized
291    {
292        let read_guard = match VALID_REGIONS.read() {
293            Ok(guard) => guard,
294            Err(poisoned) => poisoned.into_inner(),
295        };
296        if read_guard.is_some() {
297            return read_guard
298                .as_ref()
299                .cloned()
300                .expect("VALID_REGIONS should be Some after is_some() check (read path)");
301        }
302    }
303
304    // Slow path: need to initialize
305    // Use write lock and double-check to prevent TOCTOU race
306    let mut write_guard = match VALID_REGIONS.write() {
307        Ok(guard) => guard,
308        Err(poisoned) => poisoned.into_inner(),
309    };
310
311    // Double-check after acquiring write lock
312    if write_guard.is_some() {
313        return write_guard
314            .as_ref()
315            .cloned()
316            .expect("VALID_REGIONS should be Some after is_some() check (write path)");
317    }
318
319    // Initialize while holding the write lock
320    let regions = get_valid_regions_impl();
321    *write_guard = Some(regions.clone());
322    regions
323}
324
325/// Check if a pointer value is valid using dynamic regions with static fallback.
326pub fn is_valid_ptr(p: usize) -> bool {
327    get_valid_regions().contains(p)
328}
329
330/// Check if a pointer value is valid using only static bounds.
331pub fn is_valid_ptr_static(p: usize) -> bool {
332    p > MIN_VALID_ADDR && p < MAX_USER_ADDR
333}
334
335/// Memory view for safe memory access.
336pub struct MemoryView<'a> {
337    data: &'a [u8],
338}
339
340/// Owned memory view that owns its data.
341///
342/// This is a non-reference version of `MemoryView` that owns the underlying
343/// buffer. Useful when the memory view needs to outlive the original scope.
344///
345/// # When to Use OwnedMemoryView vs MemoryView
346///
347/// | Scenario | Use |
348/// |----------|-----|
349/// | Temporary analysis within a function | `MemoryView<&[u8]>` |
350/// | Storing memory data for later use | `OwnedMemoryView` |
351/// | Returning memory data from a function | `OwnedMemoryView` |
352/// | Zero-copy analysis | `MemoryView<&[u8]>` |
353///
354/// # Lifetime Management
355///
356/// `OwnedMemoryView` owns its data via `Vec<u8>`, so it has no lifetime parameter.
357/// This means:
358/// - The data remains valid as long as the `OwnedMemoryView` exists
359/// - No need to worry about the underlying data being dropped
360/// - Slightly higher memory overhead due to ownership
361///
362/// # Example
363///
364/// ```rust
365/// use memscope_rs::analysis::unsafe_inference::OwnedMemoryView;
366///
367/// // Create from a vector (takes ownership)
368/// let data = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
369/// let view = OwnedMemoryView::new(data);
370///
371/// // Read values safely
372/// if let Some(value) = view.read_usize(0) {
373///     println!("First usize: {}", value);
374/// }
375///
376/// // Check bounds
377/// if let Some(byte) = view.read_u8(10) {
378///     println!("Byte at offset 10: {}", byte);
379/// } else {
380///     println!("Offset 10 out of bounds");
381/// }
382///
383/// // Access raw slice when needed
384/// let slice = view.as_slice();
385/// println!("Total bytes: {}", slice.len());
386/// ```
387///
388/// # Memory Safety
389///
390/// All read methods perform bounds checking and return `Option` types.
391/// This ensures safe access even with invalid offsets:
392///
393/// ```rust
394/// use memscope_rs::unsafe_inference::OwnedMemoryView;
395///
396/// let view = OwnedMemoryView::new(vec![0u8; 4]);
397///
398/// // This returns None (out of bounds)
399/// assert!(view.read_usize(0).is_none());
400///
401/// // This returns None (offset + size > len)
402/// assert!(view.read_usize(1).is_none());
403/// ```
404pub struct OwnedMemoryView {
405    data: Vec<u8>,
406}
407
408/// Allocate storage for raw process bytes, which can contain allocator metadata.
409pub(crate) fn scan_buffer(len: usize) -> Vec<u8> {
410    // Nano V2 treats a matching first-word free canary as an unallocated slot.
411    // Copying a stale scan back into its old address can restore that canary in
412    // a newly allocated slot. Keep scan buffers (including copies) outside Nano's
413    // <=256-byte regime; do not alter the captured bytes to hide the marker.
414    // See Apple libmalloc nanov2_pointer_size_inline / slot_freelist_cookie.
415    #[cfg(target_os = "macos")]
416    let capacity = len.max(257);
417    #[cfg(not(target_os = "macos"))]
418    let capacity = len;
419    Vec::with_capacity(capacity)
420}
421
422impl OwnedMemoryView {
423    pub(crate) fn copy_scan(data: &[u8]) -> Self {
424        let mut copy = scan_buffer(data.len());
425        copy.extend_from_slice(data);
426        Self::new(copy)
427    }
428
429    /// Create a new `OwnedMemoryView` from a `Vec<u8>`.
430    ///
431    /// This takes ownership of the vector, so no copying occurs.
432    ///
433    /// # Example
434    ///
435    /// ```rust
436    /// use memscope_rs::unsafe_inference::OwnedMemoryView;
437    /// let view = OwnedMemoryView::new(vec![1, 2, 3, 4]);
438    /// assert_eq!(view.len(), 4);
439    /// ```
440    pub fn new(data: Vec<u8>) -> Self {
441        Self { data }
442    }
443
444    /// Returns the length of the underlying data.
445    ///
446    /// # Example
447    ///
448    /// ```rust
449    /// use memscope_rs::unsafe_inference::OwnedMemoryView;
450    /// let view = OwnedMemoryView::new(vec![1, 2, 3]);
451    /// assert_eq!(view.len(), 3);
452    /// ```
453    pub fn len(&self) -> usize {
454        self.data.len()
455    }
456
457    /// Returns `true` if the underlying data is empty.
458    ///
459    /// # Example
460    ///
461    /// ```rust
462    /// use memscope_rs::unsafe_inference::OwnedMemoryView;
463    /// let view = OwnedMemoryView::new(vec![]);
464    /// assert!(view.is_empty());
465    /// ```
466    pub fn is_empty(&self) -> bool {
467        self.data.is_empty()
468    }
469
470    /// Read a `usize` value from the specified offset.
471    ///
472    /// Reads `std::mem::size_of::<usize>()` bytes starting at `offset`
473    /// and interprets them as a little-endian `usize`.
474    ///
475    /// Returns `None` if the read would exceed the buffer bounds.
476    ///
477    /// # Example
478    ///
479    /// ```rust
480    /// use memscope_rs::unsafe_inference::OwnedMemoryView;
481    /// let data = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
482    /// let view = OwnedMemoryView::new(data);
483    ///
484    /// if let Some(value) = view.read_usize(0) {
485    ///     println!("Value: 0x{:x}", value);
486    /// }
487    /// ```
488    pub fn read_usize(&self, offset: usize) -> Option<usize> {
489        let size = std::mem::size_of::<usize>();
490        if offset.saturating_add(size) > self.data.len() {
491            return None;
492        }
493        let mut buf = [0u8; 8];
494        buf[..size].copy_from_slice(&self.data[offset..offset + size]);
495        Some(usize::from_le_bytes(buf))
496    }
497
498    /// Read a single byte from the specified offset.
499    ///
500    /// Returns `None` if the offset is out of bounds.
501    ///
502    /// # Example
503    ///
504    /// ```rust
505    /// use memscope_rs::unsafe_inference::OwnedMemoryView;
506    /// let view = OwnedMemoryView::new(vec![0x10, 0x20, 0x30]);
507    ///
508    /// assert_eq!(view.read_u8(0), Some(0x10));
509    /// assert_eq!(view.read_u8(2), Some(0x30));
510    /// assert_eq!(view.read_u8(3), None); // out of bounds
511    /// ```
512    pub fn read_u8(&self, offset: usize) -> Option<u8> {
513        self.data.get(offset).copied()
514    }
515
516    /// Returns a slice of the underlying data.
517    ///
518    /// This provides direct access to the bytes without copying.
519    ///
520    /// # Example
521    ///
522    /// ```rust
523    /// use memscope_rs::unsafe_inference::OwnedMemoryView;
524    /// let view = OwnedMemoryView::new(vec![1, 2, 3, 4, 5]);
525    /// let slice = view.as_slice();
526    /// assert_eq!(slice, &[1, 2, 3, 4, 5]);
527    /// ```
528    pub fn as_slice(&self) -> &[u8] {
529        &self.data
530    }
531
532    /// Returns an iterator over chunks of the underlying data.
533    ///
534    /// Each chunk has at most `chunk_size` elements.
535    ///
536    /// # Example
537    ///
538    /// ```rust
539    /// use memscope_rs::unsafe_inference::OwnedMemoryView;
540    /// let view = OwnedMemoryView::new(vec![1, 2, 3, 4, 5, 6]);
541    /// let chunks: Vec<_> = view.chunks(2).collect();
542    /// assert_eq!(chunks, vec![&[1, 2][..], &[3, 4], &[5, 6]]);
543    /// ```
544    pub fn chunks(&self, chunk_size: usize) -> impl Iterator<Item = &[u8]> {
545        self.data.chunks(chunk_size)
546    }
547}
548
549impl<'a> MemoryView<'a> {
550    pub fn new(data: &'a [u8]) -> Self {
551        Self { data }
552    }
553
554    pub fn len(&self) -> usize {
555        self.data.len()
556    }
557
558    pub fn is_empty(&self) -> bool {
559        self.data.is_empty()
560    }
561
562    pub fn read_usize(&self, offset: usize) -> Option<usize> {
563        let size = std::mem::size_of::<usize>();
564        if offset.saturating_add(size) > self.data.len() {
565            return None;
566        }
567        let mut buf = [0u8; 8];
568        buf[..size].copy_from_slice(&self.data[offset..offset + size]);
569        Some(usize::from_le_bytes(buf))
570    }
571
572    pub fn read_u8(&self, offset: usize) -> Option<u8> {
573        self.data.get(offset).copied()
574    }
575
576    pub fn last_byte(&self) -> Option<u8> {
577        self.data.last().copied()
578    }
579
580    pub fn as_slice(&self) -> &'a [u8] {
581        self.data
582    }
583
584    pub fn chunks(&self, chunk_size: usize) -> impl Iterator<Item = &'a [u8]> {
585        self.data.chunks(chunk_size)
586    }
587}
588
589/// Count valid pointers in a memory view.
590pub fn count_valid_pointers(view: &MemoryView) -> usize {
591    let ptr_size = std::mem::size_of::<usize>();
592    let mut count = 0;
593    for chunk in view.chunks(ptr_size) {
594        if chunk.len() < ptr_size {
595            break;
596        }
597        // Use a buffer sized for the platform's pointer size
598        let mut buf = [0u8; 16]; // Max pointer size is 16 bytes (128-bit)
599        buf[..ptr_size].copy_from_slice(chunk);
600        let v = if ptr_size == 8 {
601            usize::from_le_bytes(buf[..8].try_into().unwrap())
602        } else {
603            usize::from_le_bytes({
604                let mut arr = [0u8; 8];
605                arr[..ptr_size].copy_from_slice(&buf[..ptr_size]);
606                arr
607            })
608        };
609        if is_valid_ptr(v) {
610            count += 1;
611        }
612    }
613    count
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    /// Objective: Owned scan storage preserves raw bytes while avoiding Nano slots.
621    /// Invariants: Empty, small and full-page copies retain their exact content.
622    #[test]
623    fn owned_scan_buffers_preserve_all_bytes() {
624        for len in [0, 1, 8, 128, 255, 256, 257, 4096] {
625            let bytes: Vec<_> = (0..len).map(|i| (i % 256) as u8).collect();
626            let copy = OwnedMemoryView::copy_scan(&bytes);
627            assert_eq!(
628                copy.as_slice(),
629                bytes,
630                "copy {len} bytes without modifying metadata"
631            );
632            #[cfg(target_os = "macos")]
633            assert!(
634                copy.data.capacity() > 256,
635                "scan ownership must stay outside Nano size classes"
636            );
637        }
638    }
639
640    #[test]
641    fn test_memory_view_read_usize() {
642        let data: [u8; 16] = [
643            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
644            0x0e, 0x0f,
645        ];
646        let view = MemoryView::new(&data);
647
648        let val0 = view.read_usize(0).unwrap();
649        let val8 = view.read_usize(8).unwrap();
650
651        assert_eq!(val0, 0x0706050403020100);
652        assert_eq!(val8, 0x0f0e0d0c0b0a0908);
653    }
654
655    #[test]
656    fn test_memory_view_bounds_check() {
657        let data = [0u8; 8];
658        let view = MemoryView::new(&data);
659
660        assert!(view.read_usize(0).is_some());
661        assert!(view.read_usize(1).is_none());
662        assert!(view.read_usize(8).is_none());
663    }
664
665    #[test]
666    #[cfg(target_os = "macos")]
667    fn test_is_valid_ptr_static() {
668        assert!(!is_valid_ptr_static(0));
669        assert!(!is_valid_ptr_static(0x1000));
670        assert!(is_valid_ptr_static(0x10000));
671        assert!(is_valid_ptr_static(0x7fff_ffff_0000));
672        assert!(!is_valid_ptr_static(0xffff_ffff_ffff_ffff));
673    }
674
675    #[test]
676    #[cfg(target_os = "macos")]
677    fn test_is_valid_ptr() {
678        // Should work with either dynamic or static
679        assert!(!is_valid_ptr(0));
680        assert!(!is_valid_ptr(0x1000));
681        // These should pass with static fallback
682        assert!(is_valid_ptr(0x10000));
683    }
684
685    #[test]
686    #[cfg(target_os = "macos")]
687    fn test_count_valid_pointers() {
688        let mut data = [0u8; 24];
689        let valid_ptr: usize = 0x10000;
690        data[..8].copy_from_slice(&valid_ptr.to_le_bytes());
691
692        let view = MemoryView::new(&data);
693        assert_eq!(count_valid_pointers(&view), 1);
694    }
695
696    #[test]
697    fn test_valid_regions_contains() {
698        let regions = ValidRegions::empty();
699        // Empty regions should use static bounds
700        assert!(regions.contains(0x10000));
701        assert!(!regions.contains(0));
702    }
703
704    #[test]
705    fn test_valid_regions_from_regions() {
706        let regions = ValidRegions::from_regions(vec![
707            MemoryRegion {
708                start: 0x1000,
709                end: 0x2000,
710            },
711            MemoryRegion {
712                start: 0x3000,
713                end: 0x4000,
714            },
715        ]);
716
717        assert!(regions.is_dynamic());
718        assert!(regions.contains(0x1500));
719        assert!(regions.contains(0x3500));
720        assert!(!regions.contains(0x2500));
721        assert!(!regions.contains(0x5000));
722    }
723
724    #[test]
725    fn test_get_valid_regions() {
726        let regions = get_valid_regions();
727        // Should return something (dynamic or static)
728        // Just verify it doesn't panic
729        let _ = regions.contains(0x10000);
730    }
731}