Skip to main content

lexe_enclave/
allocator.rs

1//! Basic heap allocator instrumentation for SGX enclaves.
2//!
3//! Non-SGX binaries should use something like `jemalloc`, which tracks many
4//! more useful stats more efficiently. The current SGX allocator is just
5//! `SpinMutex<Dlmalloc<_>>`, which is pretty awful for normal multi-threaded
6//! workloads.
7
8use std::{
9    alloc::{GlobalAlloc, Layout, System},
10    sync::atomic::{AtomicUsize, Ordering},
11};
12
13#[cfg(target_env = "sgx")]
14unsafe extern "C" {
15    /// A link-time constant for the fixed SGX enclave `heap-size` defined in
16    /// e.g. `node/Cargo.toml`.
17    ///
18    /// See:
19    /// - <https://github.com/rust-lang/rust/blob/main/library/std/src/sys/pal/sgx/abi/mem.rs#L18>
20    /// - <https://github.com/rust-lang/rust/blob/main/library/std/src/sys/pal/sgx/abi/entry.S#L48>
21    static HEAP_SIZE: usize;
22}
23
24/// Rust's [`System`] global allocator instrumented with allocation counters.
25///
26/// The counters track requested bytes, excluding allocator metadata, alignment
27/// padding, and fragmentation. All instances contribute to the same counters;
28/// install exactly one as the process global allocator.
29pub struct InstrumentedSystemAllocator {
30    _private: (),
31}
32
33/// Allocator metrics
34struct Counters {
35    current: AtomicUsize,
36    max: AtomicUsize,
37}
38
39/// Global allocator metrics counters
40static COUNTERS: Counters = Counters::new();
41
42/// A snapshot of the global allocator's requested-byte counters.
43#[derive(Copy, Clone, Debug, Eq, PartialEq)]
44pub struct AllocatorStats {
45    /// Current live bytes allocated on the heap.
46    pub current_bytes: usize,
47    /// The fixed SGX heap size, or `None` outside SGX.
48    pub heap_size: Option<usize>,
49    /// The max number of live bytes ever allocated during this process' life.
50    pub max_bytes: usize,
51}
52
53/// Returns a snapshot of the allocator stats.
54pub fn stats() -> AllocatorStats {
55    let (current_bytes, max_bytes) = COUNTERS.snapshot();
56
57    AllocatorStats {
58        current_bytes,
59        heap_size: heap_size(),
60        max_bytes,
61    }
62}
63
64/// The available memory we can heap allocate in.
65fn heap_size() -> Option<usize> {
66    cfg_if::cfg_if! {
67        if #[cfg(target_env = "sgx")] {
68            // SAFETY: `HEAP_SIZE` is a link-time constant for the SGX target.
69            Some(unsafe { HEAP_SIZE })
70        } else {
71            None
72        }
73    }
74}
75
76// --- impl InstrumentedSystemAllocator --- //
77
78impl InstrumentedSystemAllocator {
79    pub const fn new() -> Self {
80        Self { _private: () }
81    }
82}
83
84unsafe impl GlobalAlloc for InstrumentedSystemAllocator {
85    #[inline]
86    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
87        // SAFETY: The caller upholds `GlobalAlloc::alloc`'s contract.
88        let ptr = unsafe { System.alloc(layout) };
89        COUNTERS.record_allocation(ptr, layout.size());
90        ptr
91    }
92
93    #[inline]
94    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
95        // SAFETY: The caller upholds `GlobalAlloc::alloc_zeroed`'s contract.
96        let ptr = unsafe { System.alloc_zeroed(layout) };
97        COUNTERS.record_allocation(ptr, layout.size());
98        ptr
99    }
100
101    #[inline]
102    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
103        // SAFETY: The caller upholds `GlobalAlloc::dealloc`'s contract.
104        unsafe { System.dealloc(ptr, layout) };
105        COUNTERS.remove(layout.size());
106    }
107
108    #[inline]
109    unsafe fn realloc(
110        &self,
111        ptr: *mut u8,
112        layout: Layout,
113        new_size: usize,
114    ) -> *mut u8 {
115        // SAFETY: The caller upholds `GlobalAlloc::realloc`'s contract.
116        let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
117        COUNTERS.record_reallocation(new_ptr, layout.size(), new_size);
118        new_ptr
119    }
120}
121
122// --- impl Counters --- //
123
124impl Counters {
125    const fn new() -> Self {
126        Self {
127            current: AtomicUsize::new(0),
128            max: AtomicUsize::new(0),
129        }
130    }
131
132    #[inline]
133    fn record_allocation(&self, ptr: *mut u8, size: usize) {
134        if !ptr.is_null() {
135            self.add(size);
136        }
137    }
138
139    #[inline]
140    fn record_reallocation(
141        &self,
142        ptr: *mut u8,
143        old_size: usize,
144        new_size: usize,
145    ) {
146        if ptr.is_null() {
147            return;
148        }
149
150        if new_size > old_size {
151            self.add(new_size - old_size);
152        } else if new_size < old_size {
153            self.remove(old_size - new_size);
154        }
155    }
156
157    #[inline]
158    fn add(&self, size: usize) {
159        let current = self.current.fetch_add(size, Ordering::Relaxed) + size;
160        self.max.fetch_max(current, Ordering::Relaxed);
161    }
162
163    #[inline]
164    fn remove(&self, size: usize) {
165        self.current.fetch_sub(size, Ordering::Relaxed);
166    }
167
168    fn snapshot(&self) -> (usize, usize) {
169        let current = self.current.load(Ordering::Relaxed);
170        // `add` updates `current` first, so include an in-flight max update.
171        let max = self.max.load(Ordering::Relaxed).max(current);
172        (current, max)
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use std::ptr::{self, NonNull};
179
180    use super::*;
181
182    #[test]
183    fn tracks_allocation_lifecycle() {
184        let counters = Counters::new();
185        let ptr = NonNull::<u8>::dangling().as_ptr();
186
187        counters.record_allocation(ptr::null_mut(), 100);
188        assert_eq!(counters.snapshot(), (0, 0));
189
190        counters.record_allocation(ptr, 20);
191        counters.record_allocation(ptr, 30);
192        assert_eq!(counters.snapshot(), (50, 50));
193
194        counters.remove(20);
195        assert_eq!(counters.snapshot(), (30, 50));
196
197        counters.record_reallocation(ptr::null_mut(), 30, 100);
198        assert_eq!(counters.snapshot(), (30, 50));
199
200        counters.record_reallocation(ptr, 30, 60);
201        assert_eq!(counters.snapshot(), (60, 60));
202
203        counters.record_reallocation(ptr, 60, 10);
204        assert_eq!(counters.snapshot(), (10, 60));
205
206        counters.remove(10);
207        assert_eq!(counters.snapshot(), (0, 60));
208    }
209}