Skip to main content

zk_nalloc/
arena.rs

1//! Arena Manager for nalloc.
2//!
3//! The `ArenaManager` pre-allocates large, specialized memory pools
4//! during initialization. This avoids system call overhead during
5//! hot proof computation paths.
6
7use crate::bump::BumpAlloc;
8use crate::config::{POLY_ARENA_SIZE, SCRATCH_ARENA_SIZE, WITNESS_ARENA_SIZE};
9use crate::sys;
10
11use std::sync::Arc;
12
13/// Manages multiple specialized memory arenas.
14///
15/// Each arena is optimized for a specific purpose:
16/// - **Witness Arena**: For private ZK inputs, with secure wiping.
17/// - **Polynomial Arena**: For FFT/NTT coefficient vectors.
18/// - **Scratch Arena**: For temporary computation buffers.
19///
20/// # Drop Safety
21///
22/// The `ArenaManager` tracks the number of outstanding arena handles.
23/// On drop, it verifies that all handles have been released before
24/// deallocating memory. If handles are still in use, the memory is
25/// intentionally leaked to prevent use-after-free (with a warning).
26pub struct ArenaManager {
27    witness: Arc<BumpAlloc>,
28    polynomial: Arc<BumpAlloc>,
29    scratch: Arc<BumpAlloc>,
30    /// Raw pointers for deallocation (since we can't get them after Arc is dropped)
31    witness_ptr: *mut u8,
32    poly_ptr: *mut u8,
33    scratch_ptr: *mut u8,
34    /// Sizes for deallocation
35    witness_size: usize,
36    poly_size: usize,
37    scratch_size: usize,
38    /// Flag to indicate this manager uses guard pages
39    #[cfg(feature = "guard-pages")]
40    #[allow(dead_code)]
41    has_guard_pages: bool,
42}
43
44impl ArenaManager {
45    /// Create a new ArenaManager with default sizes.
46    ///
47    /// This will allocate a total of ~1.4 GB of virtual memory.
48    /// Note: On modern OSes, virtual memory is cheap; physical pages
49    /// are only allocated when touched.
50    pub fn new() -> Result<Self, crate::platform::AllocFailed> {
51        Self::with_sizes(WITNESS_ARENA_SIZE, POLY_ARENA_SIZE, SCRATCH_ARENA_SIZE)
52    }
53
54    /// Create a new ArenaManager with custom sizes.
55    ///
56    /// Use this for fine-tuned configurations based on your circuit size.
57    pub fn with_sizes(
58        witness_size: usize,
59        poly_size: usize,
60        scratch_size: usize,
61    ) -> Result<Self, crate::platform::AllocFailed> {
62        let witness_ptr = sys::alloc(witness_size)?;
63        let poly_ptr = sys::alloc(poly_size)?;
64        let scratch_ptr = sys::alloc(scratch_size)?;
65
66        Ok(Self {
67            witness: Arc::new(unsafe { BumpAlloc::new(witness_ptr, witness_size) }),
68            polynomial: Arc::new(unsafe { BumpAlloc::new(poly_ptr, poly_size) }),
69            scratch: Arc::new(unsafe { BumpAlloc::new(scratch_ptr, scratch_size) }),
70            witness_ptr,
71            poly_ptr,
72            scratch_ptr,
73            witness_size,
74            poly_size,
75            scratch_size,
76            #[cfg(feature = "guard-pages")]
77            has_guard_pages: false,
78        })
79    }
80
81    /// Create arenas with guard pages for buffer overflow protection.
82    #[cfg(feature = "guard-pages")]
83    pub fn with_guard_pages(
84        witness_size: usize,
85        poly_size: usize,
86        scratch_size: usize,
87    ) -> Result<Self, crate::platform::AllocFailed> {
88        let witness_guarded = sys::alloc_with_guards(witness_size)?;
89        let poly_guarded = sys::alloc_with_guards(poly_size)?;
90        let scratch_guarded = sys::alloc_with_guards(scratch_size)?;
91
92        Ok(Self {
93            witness: Arc::new(unsafe { BumpAlloc::new(witness_guarded.ptr, witness_size) }),
94            polynomial: Arc::new(unsafe { BumpAlloc::new(poly_guarded.ptr, poly_size) }),
95            scratch: Arc::new(unsafe { BumpAlloc::new(scratch_guarded.ptr, scratch_size) }),
96            witness_ptr: witness_guarded.base_ptr,
97            poly_ptr: poly_guarded.base_ptr,
98            scratch_ptr: scratch_guarded.base_ptr,
99            witness_size: witness_guarded.total_size,
100            poly_size: poly_guarded.total_size,
101            scratch_size: scratch_guarded.total_size,
102            has_guard_pages: true,
103        })
104    }
105
106    /// Lock witness memory to prevent swapping (important for sensitive data).
107    #[cfg(feature = "mlock")]
108    pub fn lock_witness(&self) -> Result<(), crate::platform::AllocFailed> {
109        sys::mlock(self.witness.base_ptr(), self.witness.capacity())
110    }
111
112    /// Unlock previously locked witness memory.
113    #[cfg(feature = "mlock")]
114    pub fn unlock_witness(&self) -> Result<(), crate::platform::AllocFailed> {
115        sys::munlock(self.witness.base_ptr(), self.witness.capacity())
116    }
117
118    /// Get a handle to the witness arena.
119    #[inline]
120    pub fn witness(&self) -> Arc<BumpAlloc> {
121        self.witness.clone()
122    }
123
124    /// Get a handle to the polynomial arena.
125    #[inline]
126    pub fn polynomial(&self) -> Arc<BumpAlloc> {
127        self.polynomial.clone()
128    }
129
130    /// Get a handle to the scratch arena.
131    #[inline]
132    pub fn scratch(&self) -> Arc<BumpAlloc> {
133        self.scratch.clone()
134    }
135
136    /// Reset all arenas.
137    ///
138    /// The witness arena is securely wiped (zeroed) before reset.
139    ///
140    /// # Safety
141    /// This will invalidate all memory previously allocated from these arenas.
142    /// The caller must ensure:
143    /// - No other thread is concurrently allocating from these arenas
144    /// - No references to arena memory exist
145    /// - No concurrent access to arena-allocated memory occurs during or after reset
146    pub unsafe fn reset_all(&self) {
147        self.witness.secure_reset();
148        self.polynomial.reset();
149        self.scratch.reset();
150    }
151
152    /// Get statistics about arena usage.
153    pub fn stats(&self) -> ArenaStats {
154        ArenaStats {
155            witness_used: self.witness.used(),
156            witness_capacity: self.witness.capacity(),
157            polynomial_used: self.polynomial.used(),
158            polynomial_capacity: self.polynomial.capacity(),
159            scratch_used: self.scratch.used(),
160            scratch_capacity: self.scratch.capacity(),
161            #[cfg(feature = "fallback")]
162            witness_fallback_bytes: self.witness.fallback_bytes(),
163            #[cfg(feature = "fallback")]
164            polynomial_fallback_bytes: self.polynomial.fallback_bytes(),
165            #[cfg(feature = "fallback")]
166            scratch_fallback_bytes: self.scratch.fallback_bytes(),
167        }
168    }
169
170    /// Check if all arena handles have been released.
171    ///
172    /// Returns true if this ArenaManager is the sole owner of all arenas.
173    pub fn is_sole_owner(&self) -> bool {
174        Arc::strong_count(&self.witness) == 1
175            && Arc::strong_count(&self.polynomial) == 1
176            && Arc::strong_count(&self.scratch) == 1
177    }
178
179    /// Get the reference counts for each arena (for debugging).
180    pub fn ref_counts(&self) -> (usize, usize, usize) {
181        (
182            Arc::strong_count(&self.witness),
183            Arc::strong_count(&self.polynomial),
184            Arc::strong_count(&self.scratch),
185        )
186    }
187
188    /// Check if an address falls within any of the arena memory ranges.
189    ///
190    /// Used by Issue #1 fix to distinguish arena allocations from fallback allocations.
191    /// Returns `true` if the address is within witness, polynomial, or scratch arena.
192    #[inline]
193    pub fn contains_address(&self, addr: usize) -> bool {
194        // Check witness arena range
195        let witness_start = self.witness_ptr as usize;
196        let witness_end = witness_start + self.witness_size;
197        if addr >= witness_start && addr < witness_end {
198            return true;
199        }
200
201        // Check polynomial arena range
202        let poly_start = self.poly_ptr as usize;
203        let poly_end = poly_start + self.poly_size;
204        if addr >= poly_start && addr < poly_end {
205            return true;
206        }
207
208        // Check scratch arena range
209        let scratch_start = self.scratch_ptr as usize;
210        let scratch_end = scratch_start + self.scratch_size;
211        if addr >= scratch_start && addr < scratch_end {
212            return true;
213        }
214
215        false
216    }
217}
218
219/// Statistics about arena memory usage.
220#[derive(Debug, Clone, Copy)]
221pub struct ArenaStats {
222    pub witness_used: usize,
223    pub witness_capacity: usize,
224    pub polynomial_used: usize,
225    pub polynomial_capacity: usize,
226    pub scratch_used: usize,
227    pub scratch_capacity: usize,
228    #[cfg(feature = "fallback")]
229    pub witness_fallback_bytes: usize,
230    #[cfg(feature = "fallback")]
231    pub polynomial_fallback_bytes: usize,
232    #[cfg(feature = "fallback")]
233    pub scratch_fallback_bytes: usize,
234}
235
236impl ArenaStats {
237    /// Total memory currently in use.
238    pub fn total_used(&self) -> usize {
239        self.witness_used + self.polynomial_used + self.scratch_used
240    }
241
242    /// Total memory capacity across all arenas.
243    pub fn total_capacity(&self) -> usize {
244        self.witness_capacity + self.polynomial_capacity + self.scratch_capacity
245    }
246
247    /// Total bytes allocated via fallback (only with `fallback` feature).
248    #[cfg(feature = "fallback")]
249    pub fn total_fallback_bytes(&self) -> usize {
250        self.witness_fallback_bytes + self.polynomial_fallback_bytes + self.scratch_fallback_bytes
251    }
252}
253
254impl Drop for ArenaManager {
255    /// # Safety Note: ref-count check is not atomic with deallocation
256    ///
257    /// The ref-count read and the subsequent deallocation are two separate
258    /// operations with no lock between them.  In theory, another thread could
259    /// clone an `Arc<BumpAlloc>` handle between the check and the dealloc,
260    /// causing a use-after-free.
261    ///
262    /// **In practice this cannot occur** because `ArenaManager::drop` is only
263    /// reachable when `NAlloc` is dropped (`&mut self` ⇒ exclusive access).
264    /// At that point no thread can obtain new `WitnessArena` / `PolynomialArena`
265    /// handles from this `NAlloc`, so the ref counts are stable.
266    ///
267    /// The check therefore serves as a debug-mode invariant assertion, not as
268    /// a concurrent-safety mechanism.
269    fn drop(&mut self) {
270        // SAFETY CHECK: Verify we are the sole owner of all arenas
271        // If not, we cannot safely deallocate the memory as it may still be in use
272
273        let (witness_refs, poly_refs, scratch_refs) = self.ref_counts();
274
275        if witness_refs > 1 || poly_refs > 1 || scratch_refs > 1 {
276            // CRITICAL: Other references exist! We must leak the memory to prevent
277            // use-after-free. This is a bug in the caller's code but we handle it safely.
278            eprintln!(
279                "[nalloc] WARNING: ArenaManager dropped with outstanding references! \
280                 witness={}, polynomial={}, scratch={}. Memory will be leaked to prevent \
281                 use-after-free. This is a bug in your code - ensure all arena handles \
282                 are dropped before the ArenaManager.",
283                witness_refs - 1,
284                poly_refs - 1,
285                scratch_refs - 1
286            );
287
288            // Intentionally leak by not deallocating
289            return;
290        }
291
292        // We are the sole owner - safe to deallocate
293        // First, securely wipe witness data
294        unsafe {
295            self.witness.secure_reset();
296        }
297
298        // Best-effort deallocation - log errors but don't panic
299        if let Err(e) = sys::dealloc(self.witness_ptr, self.witness_size) {
300            eprintln!(
301                "[nalloc] Warning: Failed to deallocate witness arena: {}",
302                e
303            );
304        }
305        if let Err(e) = sys::dealloc(self.poly_ptr, self.poly_size) {
306            eprintln!(
307                "[nalloc] Warning: Failed to deallocate polynomial arena: {}",
308                e
309            );
310        }
311        if let Err(e) = sys::dealloc(self.scratch_ptr, self.scratch_size) {
312            eprintln!(
313                "[nalloc] Warning: Failed to deallocate scratch arena: {}",
314                e
315            );
316        }
317    }
318}
319
320// Safety: ArenaManager uses Arc internally for thread-safe sharing
321unsafe impl Send for ArenaManager {}
322unsafe impl Sync for ArenaManager {}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_arena_manager_creation() {
330        // Use smaller sizes for testing
331        let manager = ArenaManager::with_sizes(1024 * 1024, 2 * 1024 * 1024, 1024 * 1024).unwrap();
332
333        let stats = manager.stats();
334        assert_eq!(stats.witness_capacity, 1024 * 1024);
335        assert_eq!(stats.polynomial_capacity, 2 * 1024 * 1024);
336        assert_eq!(stats.scratch_capacity, 1024 * 1024);
337        assert_eq!(stats.total_used(), 0);
338    }
339
340    #[test]
341    fn test_arena_stats() {
342        let manager = ArenaManager::with_sizes(1024 * 1024, 2 * 1024 * 1024, 1024 * 1024).unwrap();
343
344        // Allocate some memory
345        let _ = manager.witness().alloc(1024, 8);
346        let _ = manager.polynomial().alloc(2048, 64);
347        let _ = manager.scratch().alloc(512, 8);
348
349        let stats = manager.stats();
350        assert!(stats.witness_used >= 1024);
351        assert!(stats.polynomial_used >= 2048);
352        assert!(stats.scratch_used >= 512);
353    }
354
355    #[test]
356    fn test_drop_deallocates() {
357        // This test verifies that Drop runs without panicking
358        {
359            let _manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
360            // manager goes out of scope here, triggering Drop
361        }
362        // If we get here without crashing, deallocation worked
363    }
364
365    #[test]
366    fn test_sole_owner_check() {
367        let manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
368
369        // Initially we should be sole owner
370        assert!(manager.is_sole_owner());
371
372        // Take a reference
373        let _witness_handle = manager.witness();
374
375        // Now we're not the sole owner
376        assert!(!manager.is_sole_owner());
377
378        // Drop the handle
379        drop(_witness_handle);
380
381        // We're sole owner again
382        assert!(manager.is_sole_owner());
383    }
384
385    #[test]
386    fn test_ref_counts() {
387        let manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
388
389        let (w, p, s) = manager.ref_counts();
390        assert_eq!((w, p, s), (1, 1, 1));
391
392        let _w1 = manager.witness();
393        let _w2 = manager.witness();
394        let _p1 = manager.polynomial();
395
396        let (w, p, s) = manager.ref_counts();
397        assert_eq!((w, p, s), (3, 2, 1));
398    }
399
400    #[test]
401    #[cfg(all(target_os = "linux", feature = "guard-pages"))]
402    fn test_guard_pages_creation() {
403        let manager =
404            ArenaManager::with_guard_pages(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
405
406        // Verify we can allocate
407        let ptr = manager.witness().alloc(1024, 8);
408        assert!(!ptr.is_null());
409
410        // Write to verify it's accessible
411        unsafe {
412            std::ptr::write_bytes(ptr, 0xAB, 1024);
413        }
414    }
415
416    #[test]
417    #[cfg(feature = "mlock")]
418    fn test_mlock() {
419        let manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
420
421        // This may fail on systems without mlock permissions, so we just
422        // check that it doesn't panic
423        let _ = manager.lock_witness();
424        let _ = manager.unlock_witness();
425    }
426}