Skip to main content

zk_nalloc/
platform.rs

1//! Platform-specific memory allocation interface.
2//!
3//! This module provides an abstraction over the operating system's
4//! virtual memory allocation APIs:
5//! - **Linux**: `mmap` via `rustix` with huge pages and guard pages support
6//! - **macOS**: `mach_vm_allocate` via `mach2`
7//! - **Windows**: `VirtualAlloc` via direct FFI
8//! - **Other Unix**: `mmap` via `libc`
9
10use std::fmt;
11
12/// Error type for system memory allocation failures.
13#[derive(Debug, Clone, Copy)]
14pub struct AllocFailed {
15    /// The size that was requested.
16    pub requested_size: usize,
17    /// Platform-specific error code, if available.
18    pub error_code: Option<i32>,
19    /// Error kind for better diagnostics.
20    pub kind: AllocErrorKind,
21}
22
23/// Classification of allocation errors.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum AllocErrorKind {
26    /// Out of memory
27    OutOfMemory,
28    /// Invalid size or alignment
29    InvalidArgument,
30    /// Permission denied
31    PermissionDenied,
32    /// Huge pages not available
33    HugePagesUnavailable,
34    /// Memory lock failed
35    MlockFailed,
36    /// Unknown error
37    Unknown,
38}
39
40impl std::error::Error for AllocFailed {}
41
42impl fmt::Display for AllocFailed {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self.error_code {
45            Some(code) => write!(
46                f,
47                "Memory allocation failed ({:?}): requested {} bytes, error code {}",
48                self.kind, self.requested_size, code
49            ),
50            None => write!(
51                f,
52                "Memory allocation failed ({:?}): requested {} bytes",
53                self.kind, self.requested_size
54            ),
55        }
56    }
57}
58
59impl AllocFailed {
60    /// Create a new allocation failure error.
61    pub fn new(size: usize) -> Self {
62        Self {
63            requested_size: size,
64            error_code: None,
65            kind: AllocErrorKind::Unknown,
66        }
67    }
68
69    pub fn with_code(size: usize, code: i32) -> Self {
70        Self {
71            requested_size: size,
72            error_code: Some(code),
73            kind: AllocErrorKind::Unknown,
74        }
75    }
76
77    pub fn with_kind(size: usize, kind: AllocErrorKind) -> Self {
78        Self {
79            requested_size: size,
80            error_code: None,
81            kind,
82        }
83    }
84
85    pub fn out_of_memory(size: usize) -> Self {
86        Self::with_kind(size, AllocErrorKind::OutOfMemory)
87    }
88
89    pub fn huge_pages_unavailable(size: usize) -> Self {
90        Self::with_kind(size, AllocErrorKind::HugePagesUnavailable)
91    }
92
93    pub fn mlock_failed(size: usize) -> Self {
94        Self::with_kind(size, AllocErrorKind::MlockFailed)
95    }
96}
97
98/// Huge page size options (Linux only).
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum HugePageSize {
101    /// 2 MB huge pages (most common)
102    Size2MB,
103    /// 1 GB huge pages (requires explicit kernel configuration)
104    Size1GB,
105}
106
107impl HugePageSize {
108    pub fn bytes(&self) -> usize {
109        match self {
110            HugePageSize::Size2MB => 2 * 1024 * 1024,
111            HugePageSize::Size1GB => 1024 * 1024 * 1024,
112        }
113    }
114}
115
116/// Result of a guarded allocation.
117#[cfg(feature = "guard-pages")]
118pub struct GuardedAlloc {
119    /// The usable memory pointer (after the front guard page).
120    pub ptr: *mut u8,
121    /// Total allocation size including guard pages.
122    pub total_size: usize,
123    /// Usable size (excluding guard pages).
124    pub usable_size: usize,
125    /// Base pointer (start of front guard page).
126    pub base_ptr: *mut u8,
127}
128
129/// Platform-specific memory allocation functions.
130pub mod sys {
131    use super::*;
132
133    // ========================================================================
134    // Linux Implementation (using rustix)
135    // ========================================================================
136
137    /// Allocate `size` bytes of virtual memory from the OS.
138    #[cfg(target_os = "linux")]
139    #[inline]
140    pub fn alloc(size: usize) -> Result<*mut u8, AllocFailed> {
141        use rustix::mm::{mmap_anonymous, MapFlags, ProtFlags};
142        use std::ptr;
143
144        debug_assert!(size > 0);
145
146        unsafe {
147            match mmap_anonymous(
148                ptr::null_mut(),
149                size,
150                ProtFlags::READ | ProtFlags::WRITE,
151                MapFlags::PRIVATE | MapFlags::NORESERVE,
152            ) {
153                Ok(ptr) => {
154                    // Issue #8: Explicit null check for safety
155                    let ptr = ptr as *mut u8;
156                    if ptr.is_null() {
157                        return Err(AllocFailed::out_of_memory(size));
158                    }
159                    Ok(ptr)
160                }
161                Err(_) => Err(AllocFailed::out_of_memory(size)),
162            }
163        }
164    }
165
166    /// Allocate memory with huge pages (Linux only).
167    #[cfg(all(target_os = "linux", feature = "huge-pages"))]
168    pub fn alloc_huge(size: usize, huge_size: HugePageSize) -> Result<*mut u8, AllocFailed> {
169        use rustix::mm::{mmap_anonymous, MapFlags, ProtFlags};
170        use std::ptr;
171
172        debug_assert!(size > 0);
173
174        // Size must be aligned to huge page size
175        let page_size = huge_size.bytes();
176        let aligned_size = (size + page_size - 1) & !(page_size - 1);
177
178        // MAP_HUGETLB flags
179        let huge_flag = match huge_size {
180            HugePageSize::Size2MB => 21 << 26, // MAP_HUGE_2MB
181            HugePageSize::Size1GB => 30 << 26, // MAP_HUGE_1GB
182        };
183
184        unsafe {
185            // First try with huge pages
186            let flags = MapFlags::PRIVATE | MapFlags::from_bits_retain(0x40000 | huge_flag); // MAP_HUGETLB
187
188            match mmap_anonymous(
189                ptr::null_mut(),
190                aligned_size,
191                ProtFlags::READ | ProtFlags::WRITE,
192                flags,
193            ) {
194                Ok(ptr) => Ok(ptr as *mut u8),
195                Err(_) => Err(AllocFailed::huge_pages_unavailable(size)),
196            }
197        }
198    }
199
200    /// Allocate memory with guard pages at both ends.
201    #[cfg(all(target_os = "linux", feature = "guard-pages"))]
202    pub fn alloc_with_guards(size: usize) -> Result<GuardedAlloc, AllocFailed> {
203        use rustix::mm::{mmap_anonymous, mprotect, MapFlags, MprotectFlags, ProtFlags};
204        use std::ptr;
205
206        const PAGE_SIZE: usize = 4096;
207
208        // Allocate: guard_page + usable_memory + guard_page
209        let total_size = PAGE_SIZE + size + PAGE_SIZE;
210
211        unsafe {
212            let base = match mmap_anonymous(
213                ptr::null_mut(),
214                total_size,
215                ProtFlags::READ | ProtFlags::WRITE,
216                MapFlags::PRIVATE,
217            ) {
218                Ok(ptr) => ptr as *mut u8,
219                Err(_) => return Err(AllocFailed::out_of_memory(size)),
220            };
221
222            // Protect front guard page (no access)
223            if mprotect(base as *mut _, PAGE_SIZE, MprotectFlags::empty()).is_err() {
224                let _ = dealloc(base, total_size);
225                return Err(AllocFailed::with_kind(
226                    size,
227                    AllocErrorKind::PermissionDenied,
228                ));
229            }
230
231            // Protect rear guard page (no access)
232            let rear_guard = base.add(PAGE_SIZE + size);
233            if mprotect(rear_guard as *mut _, PAGE_SIZE, MprotectFlags::empty()).is_err() {
234                let _ = dealloc(base, total_size);
235                return Err(AllocFailed::with_kind(
236                    size,
237                    AllocErrorKind::PermissionDenied,
238                ));
239            }
240
241            Ok(GuardedAlloc {
242                ptr: base.add(PAGE_SIZE),
243                total_size,
244                usable_size: size,
245                base_ptr: base,
246            })
247        }
248    }
249
250    /// Lock memory to prevent swapping (for sensitive data).
251    #[cfg(all(target_os = "linux", feature = "mlock"))]
252    pub fn mlock(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
253        unsafe {
254            if libc::mlock(ptr as *const _, size) == 0 {
255                Ok(())
256            } else {
257                Err(AllocFailed::mlock_failed(size))
258            }
259        }
260    }
261
262    /// Unlock previously locked memory.
263    #[cfg(all(target_os = "linux", feature = "mlock"))]
264    pub fn munlock(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
265        unsafe {
266            if libc::munlock(ptr as *const _, size) == 0 {
267                Ok(())
268            } else {
269                Err(AllocFailed::mlock_failed(size))
270            }
271        }
272    }
273
274    /// Deallocate memory previously allocated with `alloc`.
275    #[cfg(target_os = "linux")]
276    #[inline]
277    pub fn dealloc(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
278        use rustix::mm::munmap;
279
280        if ptr.is_null() {
281            return Ok(());
282        }
283
284        unsafe {
285            match munmap(ptr as *mut _, size) {
286                Ok(()) => Ok(()),
287                Err(_) => Err(AllocFailed::new(size)),
288            }
289        }
290    }
291
292    // ========================================================================
293    // macOS Implementation (using mach2)
294    // ========================================================================
295
296    #[cfg(target_vendor = "apple")]
297    #[inline]
298    pub fn alloc(size: usize) -> Result<*mut u8, AllocFailed> {
299        use mach2::kern_return::KERN_SUCCESS;
300        use mach2::traps::mach_task_self;
301        use mach2::vm::mach_vm_allocate;
302        use mach2::vm_statistics::VM_FLAGS_ANYWHERE;
303        use mach2::vm_types::{mach_vm_address_t, mach_vm_size_t};
304
305        debug_assert!(size > 0);
306
307        let task = unsafe { mach_task_self() };
308        let mut address: mach_vm_address_t = 0;
309        let vm_size: mach_vm_size_t = size as mach_vm_size_t;
310
311        let retval = unsafe { mach_vm_allocate(task, &mut address, vm_size, VM_FLAGS_ANYWHERE) };
312
313        if retval == KERN_SUCCESS {
314            // Issue #8: Explicit null check for safety
315            let ptr = address as *mut u8;
316            if ptr.is_null() {
317                return Err(AllocFailed::out_of_memory(size));
318            }
319            Ok(ptr)
320        } else {
321            Err(AllocFailed::with_code(size, retval))
322        }
323    }
324
325    /// Lock memory on macOS.
326    #[cfg(all(target_vendor = "apple", feature = "mlock"))]
327    pub fn mlock(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
328        unsafe {
329            if libc::mlock(ptr as *const _, size) == 0 {
330                Ok(())
331            } else {
332                Err(AllocFailed::mlock_failed(size))
333            }
334        }
335    }
336
337    #[cfg(all(target_vendor = "apple", feature = "mlock"))]
338    pub fn munlock(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
339        unsafe {
340            if libc::munlock(ptr as *const _, size) == 0 {
341                Ok(())
342            } else {
343                Err(AllocFailed::mlock_failed(size))
344            }
345        }
346    }
347
348    /// Allocate with guard pages on macOS.
349    #[cfg(all(target_vendor = "apple", feature = "guard-pages"))]
350    pub fn alloc_with_guards(size: usize) -> Result<GuardedAlloc, AllocFailed> {
351        use mach2::kern_return::KERN_SUCCESS;
352        use mach2::traps::mach_task_self;
353        use mach2::vm::{mach_vm_allocate, mach_vm_protect};
354        use mach2::vm_prot::VM_PROT_NONE;
355        use mach2::vm_statistics::VM_FLAGS_ANYWHERE;
356        use mach2::vm_types::{mach_vm_address_t, mach_vm_size_t};
357
358        const PAGE_SIZE: usize = 4096;
359        let total_size = PAGE_SIZE + size + PAGE_SIZE;
360
361        let task = unsafe { mach_task_self() };
362        let mut address: mach_vm_address_t = 0;
363
364        let retval = unsafe {
365            mach_vm_allocate(
366                task,
367                &mut address,
368                total_size as mach_vm_size_t,
369                VM_FLAGS_ANYWHERE,
370            )
371        };
372
373        if retval != KERN_SUCCESS {
374            return Err(AllocFailed::out_of_memory(size));
375        }
376
377        let base = address as *mut u8;
378
379        unsafe {
380            // Protect front guard
381            let ret = mach_vm_protect(task, address, PAGE_SIZE as mach_vm_size_t, 0, VM_PROT_NONE);
382            if ret != KERN_SUCCESS {
383                let _ = dealloc(base, total_size);
384                return Err(AllocFailed::with_kind(
385                    size,
386                    AllocErrorKind::PermissionDenied,
387                ));
388            }
389
390            // Protect rear guard
391            let rear_addr = address + (PAGE_SIZE + size) as u64;
392            let ret = mach_vm_protect(
393                task,
394                rear_addr,
395                PAGE_SIZE as mach_vm_size_t,
396                0,
397                VM_PROT_NONE,
398            );
399            if ret != KERN_SUCCESS {
400                let _ = dealloc(base, total_size);
401                return Err(AllocFailed::with_kind(
402                    size,
403                    AllocErrorKind::PermissionDenied,
404                ));
405            }
406        }
407
408        Ok(GuardedAlloc {
409            ptr: unsafe { base.add(PAGE_SIZE) },
410            total_size,
411            usable_size: size,
412            base_ptr: base,
413        })
414    }
415
416    /// Deallocate memory previously allocated with `alloc`.
417    #[cfg(target_vendor = "apple")]
418    #[inline]
419    pub fn dealloc(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
420        use mach2::kern_return::KERN_SUCCESS;
421        use mach2::traps::mach_task_self;
422        use mach2::vm::mach_vm_deallocate;
423        use mach2::vm_types::mach_vm_size_t;
424
425        if ptr.is_null() {
426            return Ok(());
427        }
428
429        let task = unsafe { mach_task_self() };
430        let retval = unsafe { mach_vm_deallocate(task, ptr as u64, size as mach_vm_size_t) };
431
432        if retval == KERN_SUCCESS {
433            Ok(())
434        } else {
435            Err(AllocFailed::with_code(size, retval))
436        }
437    }
438
439    // ========================================================================
440    // Windows Implementation
441    // ========================================================================
442
443    #[cfg(target_os = "windows")]
444    #[inline]
445    pub fn alloc(size: usize) -> Result<*mut u8, AllocFailed> {
446        use std::ptr;
447
448        const MEM_COMMIT: u32 = 0x00001000;
449        const MEM_RESERVE: u32 = 0x00002000;
450        const PAGE_READWRITE: u32 = 0x04;
451
452        extern "system" {
453            fn VirtualAlloc(
454                lpAddress: *mut u8,
455                dwSize: usize,
456                flAllocationType: u32,
457                flProtect: u32,
458            ) -> *mut u8;
459        }
460
461        debug_assert!(size > 0);
462
463        let result = unsafe {
464            VirtualAlloc(
465                ptr::null_mut(),
466                size,
467                MEM_COMMIT | MEM_RESERVE,
468                PAGE_READWRITE,
469            )
470        };
471
472        if result.is_null() {
473            Err(AllocFailed::out_of_memory(size))
474        } else {
475            Ok(result)
476        }
477    }
478
479    /// Allocate with guard pages on Windows.
480    #[cfg(all(target_os = "windows", feature = "guard-pages"))]
481    pub fn alloc_with_guards(size: usize) -> Result<GuardedAlloc, AllocFailed> {
482        use std::ptr;
483
484        const MEM_COMMIT: u32 = 0x00001000;
485        const MEM_RESERVE: u32 = 0x00002000;
486        const PAGE_READWRITE: u32 = 0x04;
487        const PAGE_NOACCESS: u32 = 0x01;
488        const PAGE_SIZE: usize = 4096;
489
490        extern "system" {
491            fn VirtualAlloc(
492                lpAddress: *mut u8,
493                dwSize: usize,
494                flAllocationType: u32,
495                flProtect: u32,
496            ) -> *mut u8;
497            fn VirtualProtect(
498                lpAddress: *mut u8,
499                dwSize: usize,
500                flNewProtect: u32,
501                lpflOldProtect: *mut u32,
502            ) -> i32;
503        }
504
505        let total_size = PAGE_SIZE + size + PAGE_SIZE;
506
507        let base = unsafe {
508            VirtualAlloc(
509                ptr::null_mut(),
510                total_size,
511                MEM_COMMIT | MEM_RESERVE,
512                PAGE_READWRITE,
513            )
514        };
515
516        if base.is_null() {
517            return Err(AllocFailed::out_of_memory(size));
518        }
519
520        unsafe {
521            let mut old_protect: u32 = 0;
522
523            // Protect front guard
524            if VirtualProtect(base, PAGE_SIZE, PAGE_NOACCESS, &mut old_protect) == 0 {
525                let _ = dealloc(base, total_size);
526                return Err(AllocFailed::with_kind(
527                    size,
528                    AllocErrorKind::PermissionDenied,
529                ));
530            }
531
532            // Protect rear guard
533            let rear_guard = base.add(PAGE_SIZE + size);
534            if VirtualProtect(rear_guard, PAGE_SIZE, PAGE_NOACCESS, &mut old_protect) == 0 {
535                let _ = dealloc(base, total_size);
536                return Err(AllocFailed::with_kind(
537                    size,
538                    AllocErrorKind::PermissionDenied,
539                ));
540            }
541        }
542
543        Ok(GuardedAlloc {
544            ptr: unsafe { base.add(PAGE_SIZE) },
545            total_size,
546            usable_size: size,
547            base_ptr: base,
548        })
549    }
550
551    /// Lock memory on Windows.
552    #[cfg(all(target_os = "windows", feature = "mlock"))]
553    pub fn mlock(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
554        extern "system" {
555            fn VirtualLock(lpAddress: *mut u8, dwSize: usize) -> i32;
556        }
557
558        unsafe {
559            if VirtualLock(ptr, size) != 0 {
560                Ok(())
561            } else {
562                Err(AllocFailed::mlock_failed(size))
563            }
564        }
565    }
566
567    #[cfg(all(target_os = "windows", feature = "mlock"))]
568    pub fn munlock(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
569        extern "system" {
570            fn VirtualUnlock(lpAddress: *mut u8, dwSize: usize) -> i32;
571        }
572
573        unsafe {
574            if VirtualUnlock(ptr, size) != 0 {
575                Ok(())
576            } else {
577                Err(AllocFailed::mlock_failed(size))
578            }
579        }
580    }
581
582    /// Deallocate memory previously allocated with `alloc`.
583    #[cfg(target_os = "windows")]
584    #[inline]
585    pub fn dealloc(ptr: *mut u8, _size: usize) -> Result<(), AllocFailed> {
586        const MEM_RELEASE: u32 = 0x00008000;
587
588        extern "system" {
589            fn VirtualFree(lpAddress: *mut u8, dwSize: usize, dwFreeType: u32) -> i32;
590        }
591
592        if ptr.is_null() {
593            return Ok(());
594        }
595
596        // For MEM_RELEASE, dwSize must be 0
597        let result = unsafe { VirtualFree(ptr, 0, MEM_RELEASE) };
598
599        if result != 0 {
600            Ok(())
601        } else {
602            Err(AllocFailed::new(0))
603        }
604    }
605
606    // ========================================================================
607    // Unix Fallback (using libc mmap)
608    // ========================================================================
609
610    /// Fallback for other Unix-like systems.
611    #[cfg(all(
612        not(target_os = "linux"),
613        not(target_vendor = "apple"),
614        not(target_os = "windows"),
615        unix
616    ))]
617    #[inline]
618    pub fn alloc(size: usize) -> Result<*mut u8, AllocFailed> {
619        use libc::{mmap, MAP_ANON, MAP_FAILED, MAP_PRIVATE, PROT_READ, PROT_WRITE};
620        use std::ptr;
621
622        debug_assert!(size > 0);
623
624        let result = unsafe {
625            mmap(
626                ptr::null_mut(),
627                size,
628                PROT_READ | PROT_WRITE,
629                MAP_PRIVATE | MAP_ANON,
630                -1,
631                0,
632            )
633        };
634
635        if result == MAP_FAILED {
636            Err(AllocFailed::out_of_memory(size))
637        } else {
638            Ok(result as *mut u8)
639        }
640    }
641
642    /// Deallocate memory previously allocated with `alloc`.
643    #[cfg(all(
644        not(target_os = "linux"),
645        not(target_vendor = "apple"),
646        not(target_os = "windows"),
647        unix
648    ))]
649    #[inline]
650    pub fn dealloc(ptr: *mut u8, size: usize) -> Result<(), AllocFailed> {
651        use libc::munmap;
652
653        if ptr.is_null() {
654            return Ok(());
655        }
656
657        let result = unsafe { munmap(ptr as *mut _, size) };
658
659        if result == 0 {
660            Ok(())
661        } else {
662            Err(AllocFailed::new(size))
663        }
664    }
665
666    // ========================================================================
667    // Guard page deallocation helpers
668    // ========================================================================
669
670    /// Deallocate a guarded allocation.
671    #[cfg(feature = "guard-pages")]
672    pub fn dealloc_guarded(guarded: &GuardedAlloc) -> Result<(), AllocFailed> {
673        dealloc(guarded.base_ptr, guarded.total_size)
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    #[test]
682    fn test_alloc_dealloc_roundtrip() {
683        let size = 4096;
684        let ptr = sys::alloc(size).expect("allocation should succeed");
685
686        assert!(!ptr.is_null());
687
688        // Write to verify it's accessible
689        unsafe {
690            std::ptr::write_bytes(ptr, 0xAB, size);
691        }
692
693        sys::dealloc(ptr, size).expect("deallocation should succeed");
694    }
695
696    #[test]
697    fn test_large_allocation() {
698        let size = 64 * 1024 * 1024; // 64 MB
699        let ptr = sys::alloc(size).expect("large allocation should succeed");
700
701        assert!(!ptr.is_null());
702
703        // Touch first and last pages
704        unsafe {
705            *ptr = 0x42;
706            *ptr.add(size - 1) = 0x42;
707        }
708
709        sys::dealloc(ptr, size).expect("deallocation should succeed");
710    }
711
712    #[test]
713    fn test_alloc_failed_display() {
714        let err = AllocFailed::new(1024);
715        let msg = format!("{}", err);
716        assert!(msg.contains("1024"));
717    }
718
719    #[test]
720    fn test_alloc_error_kinds() {
721        let err = AllocFailed::out_of_memory(1024);
722        assert_eq!(err.kind, AllocErrorKind::OutOfMemory);
723
724        let err = AllocFailed::huge_pages_unavailable(1024);
725        assert_eq!(err.kind, AllocErrorKind::HugePagesUnavailable);
726
727        let err = AllocFailed::mlock_failed(1024);
728        assert_eq!(err.kind, AllocErrorKind::MlockFailed);
729    }
730
731    #[test]
732    #[cfg(all(target_os = "linux", feature = "guard-pages"))]
733    fn test_guard_pages() {
734        let size = 4096;
735        let guarded = sys::alloc_with_guards(size).expect("guarded allocation should succeed");
736
737        assert!(!guarded.ptr.is_null());
738        assert_eq!(guarded.usable_size, size);
739        assert!(guarded.total_size > size);
740
741        // Write to usable area
742        unsafe {
743            std::ptr::write_bytes(guarded.ptr, 0xAB, size);
744        }
745
746        sys::dealloc_guarded(&guarded).expect("deallocation should succeed");
747    }
748}