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