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    fn drop(&mut self) {
256        // SAFETY CHECK: Verify we are the sole owner of all arenas
257        // If not, we cannot safely deallocate the memory as it may still be in use
258
259        let (witness_refs, poly_refs, scratch_refs) = self.ref_counts();
260
261        if witness_refs > 1 || poly_refs > 1 || scratch_refs > 1 {
262            // CRITICAL: Other references exist! We must leak the memory to prevent
263            // use-after-free. This is a bug in the caller's code but we handle it safely.
264            eprintln!(
265                "[nalloc] WARNING: ArenaManager dropped with outstanding references! \
266                 witness={}, polynomial={}, scratch={}. Memory will be leaked to prevent \
267                 use-after-free. This is a bug in your code - ensure all arena handles \
268                 are dropped before the ArenaManager.",
269                witness_refs - 1,
270                poly_refs - 1,
271                scratch_refs - 1
272            );
273
274            // Intentionally leak by not deallocating
275            return;
276        }
277
278        // We are the sole owner - safe to deallocate
279        // First, securely wipe witness data
280        unsafe {
281            self.witness.secure_reset();
282        }
283
284        // Best-effort deallocation - log errors but don't panic
285        if let Err(e) = sys::dealloc(self.witness_ptr, self.witness_size) {
286            eprintln!(
287                "[nalloc] Warning: Failed to deallocate witness arena: {}",
288                e
289            );
290        }
291        if let Err(e) = sys::dealloc(self.poly_ptr, self.poly_size) {
292            eprintln!(
293                "[nalloc] Warning: Failed to deallocate polynomial arena: {}",
294                e
295            );
296        }
297        if let Err(e) = sys::dealloc(self.scratch_ptr, self.scratch_size) {
298            eprintln!(
299                "[nalloc] Warning: Failed to deallocate scratch arena: {}",
300                e
301            );
302        }
303    }
304}
305
306// Safety: ArenaManager uses Arc internally for thread-safe sharing
307unsafe impl Send for ArenaManager {}
308unsafe impl Sync for ArenaManager {}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_arena_manager_creation() {
316        // Use smaller sizes for testing
317        let manager = ArenaManager::with_sizes(1024 * 1024, 2 * 1024 * 1024, 1024 * 1024).unwrap();
318
319        let stats = manager.stats();
320        assert_eq!(stats.witness_capacity, 1024 * 1024);
321        assert_eq!(stats.polynomial_capacity, 2 * 1024 * 1024);
322        assert_eq!(stats.scratch_capacity, 1024 * 1024);
323        assert_eq!(stats.total_used(), 0);
324    }
325
326    #[test]
327    fn test_arena_stats() {
328        let manager = ArenaManager::with_sizes(1024 * 1024, 2 * 1024 * 1024, 1024 * 1024).unwrap();
329
330        // Allocate some memory
331        let _ = manager.witness().alloc(1024, 8);
332        let _ = manager.polynomial().alloc(2048, 64);
333        let _ = manager.scratch().alloc(512, 8);
334
335        let stats = manager.stats();
336        assert!(stats.witness_used >= 1024);
337        assert!(stats.polynomial_used >= 2048);
338        assert!(stats.scratch_used >= 512);
339    }
340
341    #[test]
342    fn test_drop_deallocates() {
343        // This test verifies that Drop runs without panicking
344        {
345            let _manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
346            // manager goes out of scope here, triggering Drop
347        }
348        // If we get here without crashing, deallocation worked
349    }
350
351    #[test]
352    fn test_sole_owner_check() {
353        let manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
354
355        // Initially we should be sole owner
356        assert!(manager.is_sole_owner());
357
358        // Take a reference
359        let _witness_handle = manager.witness();
360
361        // Now we're not the sole owner
362        assert!(!manager.is_sole_owner());
363
364        // Drop the handle
365        drop(_witness_handle);
366
367        // We're sole owner again
368        assert!(manager.is_sole_owner());
369    }
370
371    #[test]
372    fn test_ref_counts() {
373        let manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
374
375        let (w, p, s) = manager.ref_counts();
376        assert_eq!((w, p, s), (1, 1, 1));
377
378        let _w1 = manager.witness();
379        let _w2 = manager.witness();
380        let _p1 = manager.polynomial();
381
382        let (w, p, s) = manager.ref_counts();
383        assert_eq!((w, p, s), (3, 2, 1));
384    }
385
386    #[test]
387    #[cfg(all(target_os = "linux", feature = "guard-pages"))]
388    fn test_guard_pages_creation() {
389        let manager =
390            ArenaManager::with_guard_pages(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
391
392        // Verify we can allocate
393        let ptr = manager.witness().alloc(1024, 8);
394        assert!(!ptr.is_null());
395
396        // Write to verify it's accessible
397        unsafe {
398            std::ptr::write_bytes(ptr, 0xAB, 1024);
399        }
400    }
401
402    #[test]
403    #[cfg(feature = "mlock")]
404    fn test_mlock() {
405        let manager = ArenaManager::with_sizes(1024 * 1024, 1024 * 1024, 1024 * 1024).unwrap();
406
407        // This may fail on systems without mlock permissions, so we just
408        // check that it doesn't panic
409        let _ = manager.lock_witness();
410        let _ = manager.unlock_witness();
411    }
412}