Skip to main content

zk_nalloc/
lib.rs

1//! nalloc: A ZK-Proof optimized memory allocator.
2//!
3//! This crate provides a high-performance, deterministic memory allocator
4//! specifically designed for Zero-Knowledge proof systems. It is framework-agnostic
5//! and works with any ZK system: Halo2, Plonky2, Risc0, SP1, Miden, Cairo, Arkworks, etc.
6//!
7//! # Features
8//!
9//! - **Arena-based allocation**: Pre-reserved memory pools for different workload types
10//! - **Bump allocation**: O(1) allocation via atomic pointer increment
11//! - **Security-first**: Volatile secure wiping for witness data
12//! - **Cache-optimized**: 64-byte alignment for FFT/NTT SIMD operations
13//! - **Cross-platform**: Linux, macOS, Windows, and Unix support
14//! - **Zero ZK dependencies**: Pure memory primitive, no framework lock-in
15//! - **Fallback support**: Gracefully falls back to system allocator when arena exhausted
16//!
17//! # Cargo Features
18//!
19//! - `fallback` (default): Fall back to system allocator when arena is exhausted
20//! - `huge-pages`: Enable Linux 2MB/1GB huge page support
21//! - `guard-pages`: Add guard pages at arena boundaries for overflow detection
22//! - `mlock`: Lock witness memory to prevent swapping (security)
23//!
24//! # Usage
25//!
26//! As a global allocator:
27//! ```rust,no_run
28//! use zk_nalloc::NAlloc;
29//!
30//! #[global_allocator]
31//! static ALLOC: NAlloc = NAlloc::new();
32//!
33//! fn main() {
34//!     let data = vec![0u64; 1000];
35//!     println!("Allocated {} elements", data.len());
36//! }
37//! ```
38//!
39//! Using specialized arenas directly:
40//! ```rust
41//! use zk_nalloc::NAlloc;
42//!
43//! let alloc = NAlloc::new();
44//! let witness = alloc.witness();
45//! let ptr = witness.alloc(1024, 8);
46//! assert!(!ptr.is_null());
47//!
48//! // Securely wipe when done
49//! unsafe { witness.secure_wipe(); }
50//! ```
51
52pub mod arena;
53pub mod bump;
54pub mod config;
55pub mod platform;
56pub mod polynomial;
57pub mod witness;
58
59pub use arena::{ArenaManager, ArenaStats};
60pub use bump::BumpAlloc;
61pub use config::*;
62pub use platform::sys;
63#[cfg(feature = "guard-pages")]
64pub use platform::GuardedAlloc;
65#[cfg(feature = "huge-pages")]
66pub use platform::HugePageSize;
67pub use platform::{AllocErrorKind, AllocFailed};
68pub use polynomial::PolynomialArena;
69pub use witness::WitnessArena;
70
71use std::alloc::{GlobalAlloc, Layout, System};
72use std::ptr::{copy_nonoverlapping, null_mut};
73use std::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
74
75/// Initialization state for NAlloc.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[repr(u8)]
78enum InitState {
79    /// Not yet initialized
80    Uninitialized = 0,
81    /// Currently being initialized by another thread
82    Initializing = 1,
83    /// Successfully initialized with arenas
84    Initialized = 2,
85    /// Failed to initialize, using system allocator fallback
86    Fallback = 3,
87}
88
89/// The global ZK-optimized allocator.
90///
91/// `NAlloc` provides a drop-in replacement for the standard Rust global allocator,
92/// with special optimizations for ZK-Proof workloads.
93///
94/// # Memory Strategy
95///
96/// - **Large allocations (>1MB)**: Routed to Polynomial Arena (FFT vectors)
97/// - **Small allocations**: Routed to Scratch Arena (temporary buffers)
98/// - **Witness data**: Use `NAlloc::witness()` for security-critical allocations
99///
100/// # Thread Safety
101///
102/// This allocator uses lock-free atomic operations for initialization and
103/// allocation. It's safe to use from multiple threads concurrently.
104///
105/// # Fallback Behavior
106///
107/// If arena initialization fails (e.g., out of memory), NAlloc gracefully
108/// falls back to the system allocator rather than panicking. This ensures
109/// your application continues to function even under memory pressure.
110#[must_use]
111pub struct NAlloc {
112    /// Pointer to the ArenaManager (null until initialized)
113    arenas: AtomicPtr<ArenaManager>,
114    /// Initialization state
115    init_state: AtomicU8,
116}
117
118impl NAlloc {
119    /// Create a new `NAlloc` instance.
120    ///
121    /// The arenas are lazily initialized on the first allocation.
122    pub const fn new() -> Self {
123        Self {
124            arenas: AtomicPtr::new(null_mut()),
125            init_state: AtomicU8::new(InitState::Uninitialized as u8),
126        }
127    }
128
129    /// Try to create NAlloc and initialize arenas immediately.
130    ///
131    /// Returns an error if arena allocation fails, allowing the caller
132    /// to handle the failure gracefully.
133    #[must_use]
134    pub fn try_new() -> Result<Self, AllocFailed> {
135        let nalloc = Self::new();
136        nalloc.try_init()?;
137        Ok(nalloc)
138    }
139
140    /// Try to initialize arenas.
141    ///
142    /// Returns Ok if initialization succeeds or was already done.
143    /// Returns Err if initialization fails.
144    fn try_init(&self) -> Result<(), AllocFailed> {
145        let state = self.init_state.load(Ordering::Acquire);
146
147        match state {
148            s if s == InitState::Initialized as u8 => Ok(()),
149            s if s == InitState::Fallback as u8 => {
150                Err(AllocFailed::with_kind(0, AllocErrorKind::OutOfMemory))
151            }
152            _ => {
153                let ptr = self.init_arenas();
154                if ptr.is_null() {
155                    Err(AllocFailed::with_kind(0, AllocErrorKind::OutOfMemory))
156                } else {
157                    Ok(())
158                }
159            }
160        }
161    }
162
163    /// Initialize the arenas if not already done.
164    ///
165    /// This uses a spin-lock pattern with atomic state to prevent
166    /// recursive allocation issues and handle initialization failures gracefully.
167    #[cold]
168    #[inline(never)]
169    fn init_arenas(&self) -> *mut ArenaManager {
170        // Fast path: already initialized
171        let state = self.init_state.load(Ordering::Acquire);
172        if state == InitState::Initialized as u8 {
173            return self.arenas.load(Ordering::Acquire);
174        }
175        if state == InitState::Fallback as u8 {
176            return null_mut();
177        }
178
179        // Try to acquire initialization lock
180        if self
181            .init_state
182            .compare_exchange(
183                InitState::Uninitialized as u8,
184                InitState::Initializing as u8,
185                Ordering::AcqRel,
186                Ordering::Relaxed,
187            )
188            .is_ok()
189        {
190            // We won the race - initialize
191            match ArenaManager::new() {
192                Ok(manager) => {
193                    // Use system allocator to avoid recursive allocation
194                    let layout = Layout::new::<ArenaManager>();
195                    let raw = unsafe { System.alloc(layout) as *mut ArenaManager };
196
197                    if raw.is_null() {
198                        // Failed to allocate manager struct - enter fallback mode
199                        eprintln!("[nalloc] Warning: Failed to allocate ArenaManager struct, using system allocator");
200                        self.init_state
201                            .store(InitState::Fallback as u8, Ordering::Release);
202                        return null_mut();
203                    }
204
205                    unsafe {
206                        std::ptr::write(raw, manager);
207                    }
208                    self.arenas.store(raw, Ordering::Release);
209                    self.init_state
210                        .store(InitState::Initialized as u8, Ordering::Release);
211                    return raw;
212                }
213                Err(e) => {
214                    // Arena allocation failed - enter fallback mode
215                    eprintln!(
216                        "[nalloc] Warning: Arena initialization failed ({}), using system allocator",
217                        e
218                    );
219                    self.init_state
220                        .store(InitState::Fallback as u8, Ordering::Release);
221                    return null_mut();
222                }
223            }
224        }
225
226        // Another thread is initializing - spin wait with timeout (Issue #2)
227        // Inner SPIN_ITERATIONS hint loops yield the CPU before each state check,
228        // avoiding unnecessary memory bus traffic on contended cache lines.
229        for _ in 0..MAX_CAS_RETRIES {
230            for _ in 0..SPIN_ITERATIONS {
231                std::hint::spin_loop();
232            }
233            let state = self.init_state.load(Ordering::Acquire);
234
235            match state {
236                s if s == InitState::Initialized as u8 => {
237                    return self.arenas.load(Ordering::Acquire);
238                }
239                s if s == InitState::Fallback as u8 => {
240                    return null_mut();
241                }
242                _ => continue,
243            }
244        }
245
246        // Issue #2: Timeout - initialization is stuck or taking too long
247        // Fall back to system allocator rather than spinning forever
248        #[cfg(debug_assertions)]
249        eprintln!("[nalloc] Warning: Arena initialization timed out, using system allocator");
250        null_mut()
251    }
252
253    /// Check if NAlloc is operating in fallback mode (using system allocator).
254    #[must_use]
255    #[inline]
256    pub fn is_fallback_mode(&self) -> bool {
257        self.init_state.load(Ordering::Relaxed) == InitState::Fallback as u8
258    }
259
260    /// Check if NAlloc is fully initialized with arenas.
261    #[must_use]
262    #[inline]
263    pub fn is_initialized(&self) -> bool {
264        self.init_state.load(Ordering::Relaxed) == InitState::Initialized as u8
265    }
266
267    #[inline(always)]
268    fn get_arenas(&self) -> Option<&ArenaManager> {
269        let state = self.init_state.load(Ordering::Acquire);
270
271        if state == InitState::Initialized as u8 {
272            let ptr = self.arenas.load(Ordering::Acquire);
273            if !ptr.is_null() {
274                return Some(unsafe { &*ptr });
275            }
276        }
277
278        if state == InitState::Uninitialized as u8 || state == InitState::Initializing as u8 {
279            let ptr = self.init_arenas();
280            if !ptr.is_null() {
281                return Some(unsafe { &*ptr });
282            }
283        }
284
285        None
286    }
287
288    /// Access the witness arena directly.
289    ///
290    /// Use this for allocating sensitive private inputs that need
291    /// zero-initialization and secure wiping.
292    ///
293    /// # Panics
294    ///
295    /// Panics if arena initialization failed. Use `try_witness()` for
296    /// fallible access.
297    ///
298    /// # Example
299    ///
300    /// ```rust
301    /// use zk_nalloc::NAlloc;
302    ///
303    /// let alloc = NAlloc::new();
304    /// let witness = alloc.witness();
305    /// let secret_ptr = witness.alloc(256, 8);
306    /// assert!(!secret_ptr.is_null());
307    ///
308    /// // Securely wipe when done
309    /// unsafe { witness.secure_wipe(); }
310    /// ```
311    #[inline]
312    pub fn witness(&self) -> WitnessArena {
313        self.try_witness()
314            .expect("Arena initialization failed - use try_witness() for fallible access")
315    }
316
317    /// Try to access the witness arena.
318    ///
319    /// Returns `None` if arena initialization failed.
320    #[must_use]
321    #[inline]
322    pub fn try_witness(&self) -> Option<WitnessArena> {
323        self.get_arenas().map(|a| WitnessArena::new(a.witness()))
324    }
325
326    /// Access the polynomial arena directly.
327    ///
328    /// Use this for FFT/NTT-friendly polynomial coefficient vectors.
329    /// Provides 64-byte alignment by default for SIMD operations.
330    ///
331    /// # Panics
332    ///
333    /// Panics if arena initialization failed. Use `try_polynomial()` for
334    /// fallible access.
335    ///
336    /// # Example
337    ///
338    /// ```rust
339    /// use zk_nalloc::NAlloc;
340    ///
341    /// let alloc = NAlloc::new();
342    /// let poly = alloc.polynomial();
343    /// let coeffs = poly.alloc_fft_friendly(1024); // 1K coefficients
344    /// assert!(!coeffs.is_null());
345    /// assert_eq!((coeffs as usize) % 64, 0); // 64-byte aligned
346    /// ```
347    #[inline]
348    pub fn polynomial(&self) -> PolynomialArena {
349        self.try_polynomial()
350            .expect("Arena initialization failed - use try_polynomial() for fallible access")
351    }
352
353    /// Try to access the polynomial arena.
354    ///
355    /// Returns `None` if arena initialization failed.
356    #[must_use]
357    #[inline]
358    pub fn try_polynomial(&self) -> Option<PolynomialArena> {
359        self.get_arenas()
360            .map(|a| PolynomialArena::new(a.polynomial()))
361    }
362
363    /// Access the scratch arena directly.
364    ///
365    /// Use this for temporary computation space.
366    ///
367    /// # Panics
368    ///
369    /// Panics if arena initialization failed. Use `try_scratch()` for
370    /// fallible access.
371    #[inline]
372    pub fn scratch(&self) -> std::sync::Arc<BumpAlloc> {
373        self.try_scratch()
374            .expect("Arena initialization failed - use try_scratch() for fallible access")
375    }
376
377    /// Try to access the scratch arena.
378    ///
379    /// Returns `None` if arena initialization failed.
380    #[must_use]
381    #[inline]
382    pub fn try_scratch(&self) -> Option<std::sync::Arc<BumpAlloc>> {
383        self.get_arenas().map(|a| a.scratch())
384    }
385
386    /// Reset all arenas, freeing all allocated memory.
387    ///
388    /// The witness arena is securely wiped before reset.
389    ///
390    /// # Safety
391    /// This will invalidate all previously allocated memory.
392    ///
393    /// # Note
394    /// Does nothing if operating in fallback mode.
395    pub unsafe fn reset_all(&self) {
396        if let Some(arenas) = self.get_arenas() {
397            arenas.reset_all();
398        }
399    }
400
401    /// Get statistics about arena usage.
402    ///
403    /// Returns `None` if operating in fallback mode.
404    ///
405    /// Useful for monitoring memory consumption and tuning arena sizes.
406    #[must_use]
407    pub fn stats(&self) -> Option<ArenaStats> {
408        self.get_arenas().map(|a| a.stats())
409    }
410
411    /// Get statistics, returning default stats if in fallback mode.
412    #[must_use]
413    pub fn stats_or_default(&self) -> ArenaStats {
414        self.stats().unwrap_or(ArenaStats {
415            witness_used: 0,
416            witness_capacity: 0,
417            polynomial_used: 0,
418            polynomial_capacity: 0,
419            scratch_used: 0,
420            scratch_capacity: 0,
421            #[cfg(feature = "fallback")]
422            witness_fallback_bytes: 0,
423            #[cfg(feature = "fallback")]
424            polynomial_fallback_bytes: 0,
425            #[cfg(feature = "fallback")]
426            scratch_fallback_bytes: 0,
427        })
428    }
429}
430
431impl Default for NAlloc {
432    fn default() -> Self {
433        Self::new()
434    }
435}
436
437impl Drop for NAlloc {
438    fn drop(&mut self) {
439        // Only clean up if we successfully initialized arenas.
440        // Fallback mode never allocated an ArenaManager on the heap.
441        if *self.init_state.get_mut() == InitState::Initialized as u8 {
442            let ptr = *self.arenas.get_mut();
443            if !ptr.is_null() {
444                unsafe {
445                    // Run ArenaManager's own Drop (securely wipes witness, unmaps arenas).
446                    std::ptr::drop_in_place(ptr);
447                    // Deallocate the heap slot we allocated in init_arenas().
448                    let layout = Layout::new::<ArenaManager>();
449                    System.dealloc(ptr as *mut u8, layout);
450                }
451            }
452        }
453    }
454}
455
456// Safety: NAlloc uses atomic operations for all shared state
457unsafe impl Send for NAlloc {}
458unsafe impl Sync for NAlloc {}
459
460unsafe impl GlobalAlloc for NAlloc {
461    #[inline(always)]
462    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
463        debug_assert!(layout.size() > 0);
464        debug_assert!(layout.align() > 0);
465        debug_assert!(layout.align().is_power_of_two());
466
467        // Try to use arenas
468        if let Some(arenas) = self.get_arenas() {
469            // Strategy:
470            // 1. Large allocations (>threshold) go to Polynomial Arena (likely vectors)
471            // 2. Smaller allocations go to Scratch Arena
472            // 3. User can explicitly use Witness Arena via NAlloc::witness()
473
474            if layout.size() > LARGE_ALLOC_THRESHOLD {
475                arenas.polynomial().alloc(layout.size(), layout.align())
476            } else {
477                arenas.scratch().alloc(layout.size(), layout.align())
478            }
479        } else {
480            // Fallback to system allocator
481            System.alloc(layout)
482        }
483    }
484
485    #[inline(always)]
486    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
487        // In fallback mode, we need to actually deallocate
488        if self.is_fallback_mode() {
489            System.dealloc(ptr, layout);
490            return;
491        }
492
493        // Issue #1: Check if this allocation came from fallback
494        // Arena allocations are within known address ranges; fallback allocations are not
495        if let Some(arenas) = self.get_arenas() {
496            let ptr_addr = ptr as usize;
497            if !arenas.contains_address(ptr_addr) {
498                // This was a fallback allocation - free it via system allocator
499                System.dealloc(ptr, layout);
500                return;
501            }
502        }
503
504        // For arena allocations, deallocation is a no-op.
505        // Memory is reclaimed by calling reset() on the arena.
506    }
507
508    #[inline(always)]
509    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
510        debug_assert!(!ptr.is_null());
511        debug_assert!(layout.size() > 0);
512        debug_assert!(new_size > 0);
513
514        let old_size = layout.size();
515
516        // If the new size is smaller or equal, just return the same pointer.
517        // (The bump allocator doesn't shrink.)
518        if new_size <= old_size {
519            return ptr;
520        }
521
522        // Allocate a new block
523        let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
524        let new_ptr = self.alloc(new_layout);
525
526        if new_ptr.is_null() {
527            return null_mut();
528        }
529
530        // Copy the old data
531        copy_nonoverlapping(ptr, new_ptr, old_size);
532
533        // Dealloc the old pointer (no-op for bump allocator, but semantically correct)
534        self.dealloc(ptr, layout);
535
536        new_ptr
537    }
538
539    #[inline(always)]
540    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
541        let ptr = self.alloc(layout);
542        if !ptr.is_null() {
543            // Note: mmap'd memory is already zeroed, but we zero anyway for
544            // recycled memory or if user specifically requested zeroed allocation.
545            std::ptr::write_bytes(ptr, 0, layout.size());
546        }
547        ptr
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use std::alloc::GlobalAlloc;
555
556    #[test]
557    fn test_global_alloc_api() {
558        let alloc = NAlloc::new();
559        let layout = Layout::from_size_align(1024, 8).unwrap();
560        unsafe {
561            let ptr = alloc.alloc(layout);
562            assert!(!ptr.is_null());
563            // Check that we can write to it
564            ptr.write(42);
565            assert_eq!(ptr.read(), 42);
566        }
567    }
568
569    #[test]
570    fn test_try_new() {
571        // This should succeed on any reasonable system
572        let result = NAlloc::try_new();
573        assert!(result.is_ok());
574
575        let alloc = result.unwrap();
576        assert!(alloc.is_initialized());
577        assert!(!alloc.is_fallback_mode());
578    }
579
580    #[test]
581    fn test_fallback_mode_detection() {
582        let alloc = NAlloc::new();
583        // Force initialization
584        let _ = alloc.stats();
585
586        // Should be initialized (not fallback) on a normal system
587        assert!(alloc.is_initialized() || alloc.is_fallback_mode());
588    }
589
590    #[test]
591    fn test_try_accessors() {
592        let alloc = NAlloc::new();
593
594        // These should return Some on a normal system
595        assert!(alloc.try_witness().is_some());
596        assert!(alloc.try_polynomial().is_some());
597        assert!(alloc.try_scratch().is_some());
598    }
599
600    #[test]
601    fn test_realloc() {
602        let alloc = NAlloc::new();
603        let layout = Layout::from_size_align(64, 8).unwrap();
604        unsafe {
605            let ptr = alloc.alloc(layout);
606            assert!(!ptr.is_null());
607
608            // Write some data
609            for i in 0..64 {
610                ptr.add(i).write(i as u8);
611            }
612
613            // Realloc to a larger size
614            let new_ptr = alloc.realloc(ptr, layout, 128);
615            assert!(!new_ptr.is_null());
616
617            // Verify data was copied
618            for i in 0..64 {
619                assert_eq!(new_ptr.add(i).read(), i as u8);
620            }
621        }
622    }
623
624    #[test]
625    fn test_alloc_zeroed() {
626        let alloc = NAlloc::new();
627        let layout = Layout::from_size_align(1024, 8).unwrap();
628        unsafe {
629            let ptr = alloc.alloc_zeroed(layout);
630            assert!(!ptr.is_null());
631
632            // Verify memory is zeroed
633            for i in 0..1024 {
634                assert_eq!(*ptr.add(i), 0);
635            }
636        }
637    }
638
639    #[test]
640    fn test_stats() {
641        let alloc = NAlloc::new();
642
643        // Trigger arena initialization with an allocation
644        let layout = Layout::from_size_align(1024, 8).unwrap();
645        unsafe {
646            let _ = alloc.alloc(layout);
647        }
648
649        let stats = alloc.stats();
650        assert!(stats.is_some());
651
652        let stats = stats.unwrap();
653        assert!(stats.scratch_used >= 1024);
654        assert!(stats.total_capacity() > 0);
655    }
656
657    #[test]
658    fn test_stats_or_default() {
659        let alloc = NAlloc::new();
660
661        // Should work even before initialization
662        let stats = alloc.stats_or_default();
663        // Just verify it doesn't panic
664        let _ = stats.total_capacity();
665    }
666
667    #[test]
668    fn test_large_allocation_routing() {
669        let alloc = NAlloc::new();
670
671        // Small allocation (<1MB) should go to scratch
672        let small_layout = Layout::from_size_align(1024, 8).unwrap();
673        unsafe {
674            let _ = alloc.alloc(small_layout);
675        }
676
677        let stats_after_small = alloc.stats().unwrap();
678        assert!(stats_after_small.scratch_used >= 1024);
679
680        // Large allocation (>1MB) should go to polynomial
681        let large_layout = Layout::from_size_align(2 * 1024 * 1024, 64).unwrap();
682        unsafe {
683            let _ = alloc.alloc(large_layout);
684        }
685
686        let stats_after_large = alloc.stats().unwrap();
687        assert!(stats_after_large.polynomial_used >= 2 * 1024 * 1024);
688    }
689
690    #[test]
691    fn test_drop_deallocates_arena_manager() {
692        // Verify that Drop runs without panic and actually frees the ArenaManager.
693        // If Drop is missing, valgrind/miri would catch the leak; here we test
694        // that drop_in_place + dealloc completes without UB or double-free.
695        {
696            let alloc = NAlloc::try_new().expect("NAlloc::try_new should succeed");
697            assert!(alloc.is_initialized());
698            // alloc drops here → Drop impl runs → ArenaManager is freed
699        }
700        // If we reach here without SIGSEGV / panic, the Drop impl is correct.
701        // Run a second init to confirm the heap is still healthy.
702        let alloc2 = NAlloc::try_new().expect("heap still healthy after previous drop");
703        assert!(alloc2.is_initialized());
704    }
705
706    #[test]
707    fn test_concurrent_init() {
708        use std::sync::Arc;
709        use std::thread;
710
711        let alloc = Arc::new(NAlloc::new());
712        let mut handles = vec![];
713
714        // Spawn multiple threads that try to initialize simultaneously
715        for _ in 0..8 {
716            let alloc = Arc::clone(&alloc);
717            handles.push(thread::spawn(move || {
718                let layout = Layout::from_size_align(64, 8).unwrap();
719                unsafe {
720                    let ptr = alloc.alloc(layout);
721                    assert!(!ptr.is_null());
722                }
723            }));
724        }
725
726        for h in handles {
727            h.join().unwrap();
728        }
729
730        // After all threads complete, should be in a consistent state
731        assert!(alloc.is_initialized() || alloc.is_fallback_mode());
732    }
733}