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 // We mix hint::spin_loop() (PAUSE/YIELD on x86) with periodic
252 // thread::yield_now() so the OS scheduler can run the thread that is
253 // actually performing the initialisation. Without the yield, on
254 // 2-CPU CI runners all waiting threads can starve the init thread.
255 for i in 0..MAX_CAS_RETRIES {
256 for _ in 0..SPIN_ITERATIONS {
257 std::hint::spin_loop();
258 }
259 // Every 10 outer iterations hand control back to the OS scheduler
260 // so the initialising thread gets CPU time.
261 if i % 10 == 9 {
262 std::thread::yield_now();
263 }
264 let state = self.init_state.load(Ordering::Acquire);
265
266 match state {
267 s if s == InitState::Initialized as u8 => {
268 return self.arenas.load(Ordering::Acquire);
269 }
270 s if s == InitState::Fallback as u8 => {
271 return null_mut();
272 }
273 _ => continue,
274 }
275 }
276
277 // Issue #2: Timeout - initialization is stuck or taking too long
278 // Fall back to system allocator rather than spinning forever
279 #[cfg(debug_assertions)]
280 eprintln!("[nalloc] Warning: Arena initialization timed out, using system allocator");
281 null_mut()
282 }
283
284 /// Check if NAlloc is operating in fallback mode (using system allocator).
285 #[must_use]
286 #[inline]
287 pub fn is_fallback_mode(&self) -> bool {
288 self.init_state.load(Ordering::Relaxed) == InitState::Fallback as u8
289 }
290
291 /// Check if NAlloc is fully initialized with arenas.
292 #[must_use]
293 #[inline]
294 pub fn is_initialized(&self) -> bool {
295 self.init_state.load(Ordering::Relaxed) == InitState::Initialized as u8
296 }
297
298 #[inline(always)]
299 fn get_arenas(&self) -> Option<&ArenaManager> {
300 let state = self.init_state.load(Ordering::Acquire);
301
302 if state == InitState::Initialized as u8 {
303 let ptr = self.arenas.load(Ordering::Acquire);
304 if !ptr.is_null() {
305 return Some(unsafe { &*ptr });
306 }
307 }
308
309 if state == InitState::Uninitialized as u8 || state == InitState::Initializing as u8 {
310 let ptr = self.init_arenas();
311 if !ptr.is_null() {
312 return Some(unsafe { &*ptr });
313 }
314 }
315
316 None
317 }
318
319 /// Access the witness arena directly.
320 ///
321 /// Use this for allocating sensitive private inputs that need
322 /// zero-initialization and secure wiping.
323 ///
324 /// # Panics
325 ///
326 /// Panics if arena initialization failed. Use `try_witness()` for
327 /// fallible access.
328 ///
329 /// # Example
330 ///
331 /// ```rust
332 /// use zk_nalloc::NAlloc;
333 ///
334 /// let alloc = NAlloc::new();
335 /// let witness = alloc.witness();
336 /// let secret_ptr = witness.alloc(256, 8);
337 /// assert!(!secret_ptr.is_null());
338 ///
339 /// // Securely wipe when done
340 /// unsafe { witness.secure_wipe(); }
341 /// ```
342 #[inline]
343 pub fn witness(&self) -> WitnessArena {
344 self.try_witness()
345 .expect("Arena initialization failed - use try_witness() for fallible access")
346 }
347
348 /// Try to access the witness arena.
349 ///
350 /// Returns `None` if arena initialization failed.
351 #[must_use]
352 #[inline]
353 pub fn try_witness(&self) -> Option<WitnessArena> {
354 self.get_arenas().map(|a| WitnessArena::new(a.witness()))
355 }
356
357 /// Access the polynomial arena directly.
358 ///
359 /// Use this for FFT/NTT-friendly polynomial coefficient vectors.
360 /// Provides 64-byte alignment by default for SIMD operations.
361 ///
362 /// # Panics
363 ///
364 /// Panics if arena initialization failed. Use `try_polynomial()` for
365 /// fallible access.
366 ///
367 /// # Example
368 ///
369 /// ```rust
370 /// use zk_nalloc::NAlloc;
371 ///
372 /// let alloc = NAlloc::new();
373 /// let poly = alloc.polynomial();
374 /// let coeffs = poly.alloc_fft_friendly(1024); // 1K coefficients
375 /// assert!(!coeffs.is_null());
376 /// assert_eq!((coeffs as usize) % 64, 0); // 64-byte aligned
377 /// ```
378 #[inline]
379 pub fn polynomial(&self) -> PolynomialArena {
380 self.try_polynomial()
381 .expect("Arena initialization failed - use try_polynomial() for fallible access")
382 }
383
384 /// Try to access the polynomial arena.
385 ///
386 /// Returns `None` if arena initialization failed.
387 #[must_use]
388 #[inline]
389 pub fn try_polynomial(&self) -> Option<PolynomialArena> {
390 self.get_arenas()
391 .map(|a| PolynomialArena::new(a.polynomial()))
392 }
393
394 /// Access the scratch arena directly.
395 ///
396 /// Use this for temporary computation space.
397 ///
398 /// # Panics
399 ///
400 /// Panics if arena initialization failed. Use `try_scratch()` for
401 /// fallible access.
402 #[inline]
403 pub fn scratch(&self) -> std::sync::Arc<BumpAlloc> {
404 self.try_scratch()
405 .expect("Arena initialization failed - use try_scratch() for fallible access")
406 }
407
408 /// Try to access the scratch arena.
409 ///
410 /// Returns `None` if arena initialization failed.
411 #[must_use]
412 #[inline]
413 pub fn try_scratch(&self) -> Option<std::sync::Arc<BumpAlloc>> {
414 self.get_arenas().map(|a| a.scratch())
415 }
416
417 /// Reset all arenas, freeing all allocated memory.
418 ///
419 /// The witness arena is securely wiped before reset.
420 ///
421 /// # Safety
422 /// This will invalidate all previously allocated memory.
423 ///
424 /// # Note
425 /// Does nothing if operating in fallback mode.
426 pub unsafe fn reset_all(&self) {
427 if let Some(arenas) = self.get_arenas() {
428 arenas.reset_all();
429 }
430 }
431
432 /// Get statistics about arena usage.
433 ///
434 /// Returns `None` if operating in fallback mode.
435 ///
436 /// Useful for monitoring memory consumption and tuning arena sizes.
437 #[must_use]
438 pub fn stats(&self) -> Option<ArenaStats> {
439 self.get_arenas().map(|a| a.stats())
440 }
441
442 /// Get statistics, returning default stats if in fallback mode.
443 #[must_use]
444 pub fn stats_or_default(&self) -> ArenaStats {
445 self.stats().unwrap_or(ArenaStats {
446 witness_used: 0,
447 witness_capacity: 0,
448 polynomial_used: 0,
449 polynomial_capacity: 0,
450 scratch_used: 0,
451 scratch_capacity: 0,
452 #[cfg(feature = "fallback")]
453 witness_fallback_bytes: 0,
454 #[cfg(feature = "fallback")]
455 polynomial_fallback_bytes: 0,
456 #[cfg(feature = "fallback")]
457 scratch_fallback_bytes: 0,
458 })
459 }
460}
461
462impl Default for NAlloc {
463 fn default() -> Self {
464 Self::new()
465 }
466}
467
468impl Drop for NAlloc {
469 fn drop(&mut self) {
470 // Only clean up if we successfully initialized arenas.
471 // Fallback mode never allocated an ArenaManager on the heap.
472 if *self.init_state.get_mut() == InitState::Initialized as u8 {
473 let ptr = *self.arenas.get_mut();
474 if !ptr.is_null() {
475 unsafe {
476 // Run ArenaManager's own Drop (securely wipes witness, unmaps arenas).
477 std::ptr::drop_in_place(ptr);
478 // Deallocate the heap slot we allocated in init_arenas().
479 let layout = Layout::new::<ArenaManager>();
480 System.dealloc(ptr as *mut u8, layout);
481 }
482 }
483 }
484 }
485}
486
487// Safety: NAlloc uses atomic operations for all shared state
488unsafe impl Send for NAlloc {}
489unsafe impl Sync for NAlloc {}
490
491unsafe impl GlobalAlloc for NAlloc {
492 #[inline(always)]
493 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
494 debug_assert!(layout.size() > 0);
495 debug_assert!(layout.align() > 0);
496 debug_assert!(layout.align().is_power_of_two());
497
498 // Try to use arenas
499 if let Some(arenas) = self.get_arenas() {
500 // Strategy:
501 // 1. Large allocations (>threshold) go to Polynomial Arena (likely vectors)
502 // 2. Smaller allocations go to Scratch Arena
503 // 3. User can explicitly use Witness Arena via NAlloc::witness()
504
505 if layout.size() > LARGE_ALLOC_THRESHOLD {
506 arenas.polynomial().alloc(layout.size(), layout.align())
507 } else {
508 arenas.scratch().alloc(layout.size(), layout.align())
509 }
510 } else {
511 // Fallback to system allocator
512 System.alloc(layout)
513 }
514 }
515
516 #[inline(always)]
517 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
518 // In fallback mode, we need to actually deallocate
519 if self.is_fallback_mode() {
520 System.dealloc(ptr, layout);
521 return;
522 }
523
524 // Issue #1: Check if this allocation came from fallback
525 // Arena allocations are within known address ranges; fallback allocations are not
526 if let Some(arenas) = self.get_arenas() {
527 let ptr_addr = ptr as usize;
528 if !arenas.contains_address(ptr_addr) {
529 // This was a fallback allocation - free it via system allocator
530 System.dealloc(ptr, layout);
531 return;
532 }
533 }
534
535 // For arena allocations, deallocation is a no-op.
536 // Memory is reclaimed by calling reset() on the arena.
537 }
538
539 #[inline(always)]
540 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
541 debug_assert!(!ptr.is_null());
542 debug_assert!(layout.size() > 0);
543 debug_assert!(new_size > 0);
544
545 let old_size = layout.size();
546
547 // If the new size is smaller or equal, just return the same pointer.
548 // (The bump allocator doesn't shrink.)
549 if new_size <= old_size {
550 return ptr;
551 }
552
553 // Allocate a new block
554 let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
555 let new_ptr = self.alloc(new_layout);
556
557 if new_ptr.is_null() {
558 return null_mut();
559 }
560
561 // Copy the old data
562 copy_nonoverlapping(ptr, new_ptr, old_size);
563
564 // Dealloc the old pointer (no-op for bump allocator, but semantically correct)
565 self.dealloc(ptr, layout);
566
567 new_ptr
568 }
569
570 #[inline(always)]
571 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
572 let ptr = self.alloc(layout);
573 if !ptr.is_null() {
574 // Note: mmap'd memory is already zeroed, but we zero anyway for
575 // recycled memory or if user specifically requested zeroed allocation.
576 std::ptr::write_bytes(ptr, 0, layout.size());
577 }
578 ptr
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use std::alloc::GlobalAlloc;
586
587 #[test]
588 fn test_global_alloc_api() {
589 let alloc = NAlloc::new();
590 let layout = Layout::from_size_align(1024, 8).unwrap();
591 unsafe {
592 let ptr = alloc.alloc(layout);
593 assert!(!ptr.is_null());
594 // Check that we can write to it
595 ptr.write(42);
596 assert_eq!(ptr.read(), 42);
597 }
598 }
599
600 #[test]
601 fn test_try_new() {
602 // This should succeed on any reasonable system
603 let result = NAlloc::try_new();
604 assert!(result.is_ok());
605
606 let alloc = result.unwrap();
607 assert!(alloc.is_initialized());
608 assert!(!alloc.is_fallback_mode());
609 }
610
611 #[test]
612 fn test_fallback_mode_detection() {
613 let alloc = NAlloc::new();
614 // Force initialization
615 let _ = alloc.stats();
616
617 // Should be initialized (not fallback) on a normal system
618 assert!(alloc.is_initialized() || alloc.is_fallback_mode());
619 }
620
621 #[test]
622 fn test_try_accessors() {
623 let alloc = NAlloc::new();
624
625 // These should return Some on a normal system
626 assert!(alloc.try_witness().is_some());
627 assert!(alloc.try_polynomial().is_some());
628 assert!(alloc.try_scratch().is_some());
629 }
630
631 #[test]
632 fn test_realloc() {
633 let alloc = NAlloc::new();
634 let layout = Layout::from_size_align(64, 8).unwrap();
635 unsafe {
636 let ptr = alloc.alloc(layout);
637 assert!(!ptr.is_null());
638
639 // Write some data
640 for i in 0..64 {
641 ptr.add(i).write(i as u8);
642 }
643
644 // Realloc to a larger size
645 let new_ptr = alloc.realloc(ptr, layout, 128);
646 assert!(!new_ptr.is_null());
647
648 // Verify data was copied
649 for i in 0..64 {
650 assert_eq!(new_ptr.add(i).read(), i as u8);
651 }
652 }
653 }
654
655 #[test]
656 fn test_alloc_zeroed() {
657 let alloc = NAlloc::new();
658 let layout = Layout::from_size_align(1024, 8).unwrap();
659 unsafe {
660 let ptr = alloc.alloc_zeroed(layout);
661 assert!(!ptr.is_null());
662
663 // Verify memory is zeroed
664 for i in 0..1024 {
665 assert_eq!(*ptr.add(i), 0);
666 }
667 }
668 }
669
670 #[test]
671 fn test_stats() {
672 let alloc = NAlloc::new();
673
674 // Trigger arena initialization with an allocation
675 let layout = Layout::from_size_align(1024, 8).unwrap();
676 unsafe {
677 let _ = alloc.alloc(layout);
678 }
679
680 let stats = alloc.stats();
681 assert!(stats.is_some());
682
683 let stats = stats.unwrap();
684 assert!(stats.scratch_used >= 1024);
685 assert!(stats.total_capacity() > 0);
686 }
687
688 #[test]
689 fn test_stats_or_default() {
690 let alloc = NAlloc::new();
691
692 // Should work even before initialization
693 let stats = alloc.stats_or_default();
694 // Just verify it doesn't panic
695 let _ = stats.total_capacity();
696 }
697
698 #[test]
699 fn test_large_allocation_routing() {
700 let alloc = NAlloc::new();
701
702 // Small allocation (<1MB) should go to scratch
703 let small_layout = Layout::from_size_align(1024, 8).unwrap();
704 unsafe {
705 let _ = alloc.alloc(small_layout);
706 }
707
708 let stats_after_small = alloc.stats().unwrap();
709 assert!(stats_after_small.scratch_used >= 1024);
710
711 // Large allocation (>1MB) should go to polynomial
712 let large_layout = Layout::from_size_align(2 * 1024 * 1024, 64).unwrap();
713 unsafe {
714 let _ = alloc.alloc(large_layout);
715 }
716
717 let stats_after_large = alloc.stats().unwrap();
718 assert!(stats_after_large.polynomial_used >= 2 * 1024 * 1024);
719 }
720
721 #[test]
722 fn test_drop_deallocates_arena_manager() {
723 // Verify that Drop runs without panic and actually frees the ArenaManager.
724 // If Drop is missing, valgrind/miri would catch the leak; here we test
725 // that drop_in_place + dealloc completes without UB or double-free.
726 {
727 let alloc = NAlloc::try_new().expect("NAlloc::try_new should succeed");
728 assert!(alloc.is_initialized());
729 // alloc drops here → Drop impl runs → ArenaManager is freed
730 }
731 // If we reach here without SIGSEGV / panic, the Drop impl is correct.
732 // Run a second init to confirm the heap is still healthy.
733 let alloc2 = NAlloc::try_new().expect("heap still healthy after previous drop");
734 assert!(alloc2.is_initialized());
735 }
736
737 #[test]
738 fn test_concurrent_init() {
739 use std::sync::Arc;
740 use std::thread;
741
742 let alloc = Arc::new(NAlloc::new());
743 let mut handles = vec![];
744
745 // Spawn multiple threads that try to initialize simultaneously
746 for _ in 0..8 {
747 let alloc = Arc::clone(&alloc);
748 handles.push(thread::spawn(move || {
749 let layout = Layout::from_size_align(64, 8).unwrap();
750 unsafe {
751 let ptr = alloc.alloc(layout);
752 assert!(!ptr.is_null());
753 }
754 }));
755 }
756
757 for h in handles {
758 h.join().unwrap();
759 }
760
761 // After all threads complete, should be in a consistent state
762 assert!(alloc.is_initialized() || alloc.is_fallback_mode());
763 }
764}