lucet_runtime_internals/alloc/
mod.rs

1use crate::error::Error;
2use crate::module::Module;
3use crate::region::RegionInternal;
4use libc::c_void;
5use lucet_module::GlobalValue;
6use nix::unistd::{sysconf, SysconfVar};
7use std::sync::{Arc, Once, Weak};
8
9pub const HOST_PAGE_SIZE_EXPECTED: usize = 4096;
10static mut HOST_PAGE_SIZE: usize = 0;
11static HOST_PAGE_SIZE_INIT: Once = Once::new();
12
13/// Our host is Linux x86_64, which should always use a 4K page.
14///
15/// We double check the expected value using `sysconf` at runtime.
16pub fn host_page_size() -> usize {
17    unsafe {
18        HOST_PAGE_SIZE_INIT.call_once(|| match sysconf(SysconfVar::PAGE_SIZE) {
19            Ok(Some(sz)) => {
20                if sz as usize == HOST_PAGE_SIZE_EXPECTED {
21                    HOST_PAGE_SIZE = HOST_PAGE_SIZE_EXPECTED;
22                } else {
23                    panic!(
24                        "host page size was {}; expected {}",
25                        sz, HOST_PAGE_SIZE_EXPECTED
26                    );
27                }
28            }
29            _ => panic!("could not get host page size from sysconf"),
30        });
31        HOST_PAGE_SIZE
32    }
33}
34
35pub fn instance_heap_offset() -> usize {
36    1 * host_page_size()
37}
38
39/// A set of pointers into virtual memory that can be allocated into an `Alloc`.
40///
41/// The `'r` lifetime parameter represents the lifetime of the region that backs this virtual
42/// address space.
43///
44/// The memory layout in a `Slot` is meant to be reused in order to reduce overhead on region
45/// implementations. To back the layout with real memory, use `Region::allocate_runtime`.
46///
47/// To ensure a `Slot` can only be backed by one allocation at a time, it contains a mutex, but
48/// otherwise can be freely copied.
49#[repr(C)]
50pub struct Slot {
51    /// The beginning of the contiguous virtual memory chunk managed by this `Alloc`.
52    ///
53    /// The first part of this memory, pointed to by `start`, is always backed by real memory, and
54    /// is used to store the lucet_instance structure.
55    pub start: *mut c_void,
56
57    /// The next part of memory contains the heap and its guard pages.
58    ///
59    /// The heap is backed by real memory according to the `HeapSpec`. Guard pages trigger a sigsegv
60    /// when accessed.
61    pub heap: *mut c_void,
62
63    /// The stack comes after the heap.
64    ///
65    /// Because the stack grows downwards, we get the added safety of ensuring that stack overflows
66    /// go into the guard pages, if the `Limits` specify guard pages. The stack is always the size
67    /// given by `Limits.stack_pages`.
68    pub stack: *mut c_void,
69
70    /// The WebAssembly Globals follow the stack and a single guard page.
71    pub globals: *mut c_void,
72
73    /// The signal handler stack follows the globals.
74    ///
75    /// Having a separate signal handler stack allows the signal handler to run in situations where
76    /// the normal stack has grown into the guard page.
77    pub sigstack: *mut c_void,
78
79    /// Limits of the memory.
80    ///
81    /// Should not change through the lifetime of the `Alloc`.
82    pub limits: Limits,
83
84    pub region: Weak<dyn RegionInternal>,
85}
86
87// raw pointers require unsafe impl
88unsafe impl Send for Slot {}
89unsafe impl Sync for Slot {}
90
91impl Slot {
92    pub fn stack_top(&self) -> *mut c_void {
93        (self.stack as usize + self.limits.stack_size) as *mut c_void
94    }
95}
96
97/// The structure that manages the allocations backing an `Instance`.
98///
99/// `Alloc`s are not to be created directly, but rather are created by `Region`s during instance
100/// creation.
101pub struct Alloc {
102    pub heap_accessible_size: usize,
103    pub heap_inaccessible_size: usize,
104    pub slot: Option<Slot>,
105    pub region: Arc<dyn RegionInternal>,
106}
107
108impl Drop for Alloc {
109    fn drop(&mut self) {
110        // eprintln!("Alloc::drop()");
111        self.region.clone().drop_alloc(self);
112    }
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub enum AddrLocation {
117    Heap,
118    InaccessibleHeap,
119    StackGuard,
120    Stack,
121    Globals,
122    SigStackGuard,
123    SigStack,
124    Unknown,
125}
126
127impl AddrLocation {
128    /// If a fault occurs in this location, is it fatal to the entire process?
129    ///
130    /// This is currently a permissive baseline that only returns true for unknown locations and the
131    /// signal stack guard, in case a `Region` implementation uses faults to populate the accessible
132    /// locations like the heap and the globals.
133    pub fn is_fault_fatal(self) -> bool {
134        use AddrLocation::*;
135        match self {
136            SigStackGuard | Unknown => true,
137            _ => false,
138        }
139    }
140}
141
142impl Alloc {
143    /// Where in an `Alloc` does a particular address fall?
144    pub fn addr_location(&self, addr: *const c_void) -> AddrLocation {
145        let addr = addr as usize;
146
147        let heap_start = self.slot().heap as usize;
148        let heap_inaccessible_start = heap_start + self.heap_accessible_size;
149        let heap_inaccessible_end = heap_start + self.slot().limits.heap_address_space_size;
150
151        if (addr >= heap_start) && (addr < heap_inaccessible_start) {
152            return AddrLocation::Heap;
153        }
154        if (addr >= heap_inaccessible_start) && (addr < heap_inaccessible_end) {
155            return AddrLocation::InaccessibleHeap;
156        }
157
158        let stack_start = self.slot().stack as usize;
159        let stack_end = stack_start + self.slot().limits.stack_size;
160        let stack_guard_start = stack_start - host_page_size();
161
162        if (addr >= stack_guard_start) && (addr < stack_start) {
163            return AddrLocation::StackGuard;
164        }
165        if (addr >= stack_start) && (addr < stack_end) {
166            return AddrLocation::Stack;
167        }
168
169        let globals_start = self.slot().globals as usize;
170        let globals_end = globals_start + self.slot().limits.globals_size;
171
172        if (addr >= globals_start) && (addr < globals_end) {
173            return AddrLocation::Globals;
174        }
175
176        let sigstack_start = self.slot().sigstack as usize;
177        let sigstack_end = sigstack_start + self.slot().limits.signal_stack_size;
178        let sigstack_guard_start = sigstack_start - host_page_size();
179
180        if (addr >= sigstack_guard_start) && (addr < sigstack_start) {
181            return AddrLocation::SigStackGuard;
182        }
183        if (addr >= sigstack_start) && (addr < sigstack_end) {
184            return AddrLocation::SigStack;
185        }
186
187        AddrLocation::Unknown
188    }
189
190    pub fn expand_heap(&mut self, expand_bytes: u32, module: &dyn Module) -> Result<u32, Error> {
191        let slot = self.slot();
192
193        if expand_bytes == 0 {
194            // no expansion takes place, which is not an error
195            return Ok(self.heap_accessible_size as u32);
196        }
197
198        let host_page_size = host_page_size() as u32;
199
200        if self.heap_accessible_size as u32 % host_page_size != 0 {
201            lucet_bail!("heap is not page-aligned; this is a bug");
202        }
203
204        if expand_bytes > std::u32::MAX - host_page_size - 1 {
205            bail_limits_exceeded!("expanded heap would overflow address space");
206        }
207
208        // round the expansion up to a page boundary
209        let expand_pagealigned =
210            ((expand_bytes + host_page_size - 1) / host_page_size) * host_page_size;
211
212        // `heap_inaccessible_size` tracks the size of the allocation that is addressible but not
213        // accessible. We cannot perform an expansion larger than this size.
214        if expand_pagealigned as usize > self.heap_inaccessible_size {
215            bail_limits_exceeded!("expanded heap would overflow addressable memory");
216        }
217
218        // the above makes sure this expression does not underflow
219        let guard_remaining = self.heap_inaccessible_size - expand_pagealigned as usize;
220
221        if let Some(heap_spec) = module.heap_spec() {
222            // The compiler specifies how much guard (memory which traps on access) must be beyond the
223            // end of the accessible memory. We cannot perform an expansion that would make this region
224            // smaller than the compiler expected it to be.
225            if guard_remaining < heap_spec.guard_size as usize {
226                bail_limits_exceeded!("expansion would leave guard memory too small");
227            }
228
229            // The compiler indicates that the module has specified a maximum memory size. Don't let
230            // the heap expand beyond that:
231            if let Some(max_size) = heap_spec.max_size {
232                if self.heap_accessible_size + expand_pagealigned as usize > max_size as usize {
233                    bail_limits_exceeded!(
234                        "expansion would exceed module-specified heap limit: {:?}",
235                        max_size
236                    );
237                }
238            }
239        } else {
240            return Err(Error::NoLinearMemory("cannot expand heap".to_owned()));
241        }
242        // The runtime sets a limit on how much of the heap can be backed by real memory. Don't let
243        // the heap expand beyond that:
244        if self.heap_accessible_size + expand_pagealigned as usize > slot.limits.heap_memory_size {
245            bail_limits_exceeded!(
246                "expansion would exceed runtime-specified heap limit: {:?}",
247                slot.limits
248            );
249        }
250
251        let newly_accessible = self.heap_accessible_size;
252
253        self.region
254            .clone()
255            .expand_heap(slot, newly_accessible as u32, expand_pagealigned)?;
256
257        self.heap_accessible_size += expand_pagealigned as usize;
258        self.heap_inaccessible_size -= expand_pagealigned as usize;
259
260        Ok(newly_accessible as u32)
261    }
262
263    pub fn reset_heap(&mut self, module: &dyn Module) -> Result<(), Error> {
264        self.region.clone().reset_heap(self, module)
265    }
266
267    pub fn heap_len(&self) -> usize {
268        self.heap_accessible_size
269    }
270
271    pub fn slot(&self) -> &Slot {
272        self.slot
273            .as_ref()
274            .expect("alloc missing its slot before drop")
275    }
276
277    /// Return the heap as a byte slice.
278    pub unsafe fn heap(&self) -> &[u8] {
279        std::slice::from_raw_parts(self.slot().heap as *mut u8, self.heap_accessible_size)
280    }
281
282    /// Return the heap as a mutable byte slice.
283    pub unsafe fn heap_mut(&mut self) -> &mut [u8] {
284        std::slice::from_raw_parts_mut(self.slot().heap as *mut u8, self.heap_accessible_size)
285    }
286
287    /// Return the heap as a slice of 32-bit words.
288    pub unsafe fn heap_u32(&self) -> &[u32] {
289        assert!(self.slot().heap as usize % 4 == 0, "heap is 4-byte aligned");
290        assert!(
291            self.heap_accessible_size % 4 == 0,
292            "heap size is multiple of 4-bytes"
293        );
294        std::slice::from_raw_parts(self.slot().heap as *mut u32, self.heap_accessible_size / 4)
295    }
296
297    /// Return the heap as a mutable slice of 32-bit words.
298    pub unsafe fn heap_u32_mut(&mut self) -> &mut [u32] {
299        assert!(self.slot().heap as usize % 4 == 0, "heap is 4-byte aligned");
300        assert!(
301            self.heap_accessible_size % 4 == 0,
302            "heap size is multiple of 4-bytes"
303        );
304        std::slice::from_raw_parts_mut(self.slot().heap as *mut u32, self.heap_accessible_size / 4)
305    }
306
307    /// Return the heap as a slice of 64-bit words.
308    pub unsafe fn heap_u64(&self) -> &[u64] {
309        assert!(self.slot().heap as usize % 8 == 0, "heap is 8-byte aligned");
310        assert!(
311            self.heap_accessible_size % 8 == 0,
312            "heap size is multiple of 8-bytes"
313        );
314        std::slice::from_raw_parts(self.slot().heap as *mut u64, self.heap_accessible_size / 8)
315    }
316
317    /// Return the heap as a mutable slice of 64-bit words.
318    pub unsafe fn heap_u64_mut(&mut self) -> &mut [u64] {
319        assert!(self.slot().heap as usize % 8 == 0, "heap is 8-byte aligned");
320        assert!(
321            self.heap_accessible_size % 8 == 0,
322            "heap size is multiple of 8-bytes"
323        );
324        std::slice::from_raw_parts_mut(self.slot().heap as *mut u64, self.heap_accessible_size / 8)
325    }
326
327    /// Return the stack as a mutable byte slice.
328    ///
329    /// Since the stack grows down, `alloc.stack_mut()[0]` is the top of the stack, and
330    /// `alloc.stack_mut()[alloc.limits.stack_size - 1]` is the last byte at the bottom of the
331    /// stack.
332    pub unsafe fn stack_mut(&mut self) -> &mut [u8] {
333        std::slice::from_raw_parts_mut(self.slot().stack as *mut u8, self.slot().limits.stack_size)
334    }
335
336    /// Return the stack as a mutable slice of 64-bit words.
337    ///
338    /// Since the stack grows down, `alloc.stack_mut()[0]` is the top of the stack, and
339    /// `alloc.stack_mut()[alloc.limits.stack_size - 1]` is the last word at the bottom of the
340    /// stack.
341    pub unsafe fn stack_u64_mut(&mut self) -> &mut [u64] {
342        assert!(
343            self.slot().stack as usize % 8 == 0,
344            "stack is 8-byte aligned"
345        );
346        assert!(
347            self.slot().limits.stack_size % 8 == 0,
348            "stack size is multiple of 8-bytes"
349        );
350        std::slice::from_raw_parts_mut(
351            self.slot().stack as *mut u64,
352            self.slot().limits.stack_size / 8,
353        )
354    }
355
356    /// Return the globals as a slice.
357    pub unsafe fn globals(&self) -> &[GlobalValue] {
358        std::slice::from_raw_parts(
359            self.slot().globals as *const GlobalValue,
360            self.slot().limits.globals_size / std::mem::size_of::<GlobalValue>(),
361        )
362    }
363
364    /// Return the globals as a mutable slice.
365    pub unsafe fn globals_mut(&mut self) -> &mut [GlobalValue] {
366        std::slice::from_raw_parts_mut(
367            self.slot().globals as *mut GlobalValue,
368            self.slot().limits.globals_size / std::mem::size_of::<GlobalValue>(),
369        )
370    }
371
372    /// Return the sigstack as a mutable byte slice.
373    pub unsafe fn sigstack_mut(&mut self) -> &mut [u8] {
374        std::slice::from_raw_parts_mut(
375            self.slot().sigstack as *mut u8,
376            self.slot().limits.signal_stack_size,
377        )
378    }
379
380    pub fn mem_in_heap<T>(&self, ptr: *const T, len: usize) -> bool {
381        let start = ptr as usize;
382        let end = start + len;
383
384        let heap_start = self.slot().heap as usize;
385        let heap_end = heap_start + self.heap_accessible_size;
386
387        // TODO: check for off-by-ones
388        start <= end
389            && start >= heap_start
390            && start < heap_end
391            && end >= heap_start
392            && end <= heap_end
393    }
394}
395
396/// Runtime limits for the various memories that back a Lucet instance.
397///
398/// Each value is specified in bytes, and must be evenly divisible by the host page size (4K).
399#[derive(Clone, Debug)]
400#[repr(C)]
401pub struct Limits {
402    /// Max size of the heap, which can be backed by real memory. (default 1M)
403    pub heap_memory_size: usize,
404    /// Size of total virtual memory. (default 8G)
405    pub heap_address_space_size: usize,
406    /// Size of the guest stack. (default 128K)
407    pub stack_size: usize,
408    /// Size of the globals region in bytes; each global uses 8 bytes. (default 4K)
409    pub globals_size: usize,
410    /// Size of the signal stack in bytes. (default SIGSTKSZ for release builds, at least 12K for
411    /// debug builds; minimum MINSIGSTKSZ)
412    ///
413    /// This difference is to account for the greatly increased stack size usage in the signal
414    /// handler when running without optimizations.
415    ///
416    /// Note that debug vs. release mode is determined by `cfg(debug_assertions)`, so if you are
417    /// specifically enabling debug assertions in your release builds, the default signal stack may
418    /// be larger.
419    pub signal_stack_size: usize,
420}
421
422// this constant isn't exported by `libc` on Mac
423#[cfg(target_os = "macos")]
424pub const MINSIGSTKSZ: usize = 32 * 1024;
425
426#[cfg(not(target_os = "macos"))]
427pub const MINSIGSTKSZ: usize = libc::MINSIGSTKSZ;
428
429// on Linux, `SIGSTKSZ` is too small for the signal handler when compiled in debug mode
430#[cfg(all(debug_assertions, not(target_os = "macos")))]
431pub const DEFAULT_SIGNAL_STACK_SIZE: usize = 12 * 1024;
432
433// on Mac, `SIGSTKSZ` is way larger than we need; it would be nice to combine these debug cases once
434// `std::cmp::max` is a const fn
435#[cfg(all(debug_assertions, target_os = "macos"))]
436pub const DEFAULT_SIGNAL_STACK_SIZE: usize = libc::SIGSTKSZ;
437
438#[cfg(not(debug_assertions))]
439pub const DEFAULT_SIGNAL_STACK_SIZE: usize = libc::SIGSTKSZ;
440
441impl Limits {
442    pub const fn default() -> Limits {
443        Limits {
444            heap_memory_size: 16 * 64 * 1024,
445            heap_address_space_size: 0x200000000,
446            stack_size: 128 * 1024,
447            globals_size: 4096,
448            signal_stack_size: DEFAULT_SIGNAL_STACK_SIZE,
449        }
450    }
451}
452
453impl Limits {
454    pub fn total_memory_size(&self) -> usize {
455        // Memory is laid out as follows:
456        // * the instance (up to instance_heap_offset)
457        // * the heap, followed by guard pages
458        // * the stack (grows towards heap guard pages)
459        // * globals
460        // * one guard page (to catch signal stack overflow)
461        // * the signal stack
462
463        [
464            instance_heap_offset(),
465            self.heap_address_space_size,
466            host_page_size(),
467            self.stack_size,
468            self.globals_size,
469            host_page_size(),
470            self.signal_stack_size,
471        ]
472        .iter()
473        .try_fold(0usize, |acc, &x| acc.checked_add(x))
474        .expect("total_memory_size doesn't overflow")
475    }
476
477    /// Validate that the limits are aligned to page sizes, and that the stack is not empty.
478    pub fn validate(&self) -> Result<(), Error> {
479        if self.heap_memory_size % host_page_size() != 0 {
480            return Err(Error::InvalidArgument(
481                "memory size must be a multiple of host page size",
482            ));
483        }
484        if self.heap_address_space_size % host_page_size() != 0 {
485            return Err(Error::InvalidArgument(
486                "address space size must be a multiple of host page size",
487            ));
488        }
489        if self.heap_memory_size > self.heap_address_space_size {
490            return Err(Error::InvalidArgument(
491                "address space size must be at least as large as memory size",
492            ));
493        }
494        if self.stack_size % host_page_size() != 0 {
495            return Err(Error::InvalidArgument(
496                "stack size must be a multiple of host page size",
497            ));
498        }
499        if self.globals_size % host_page_size() != 0 {
500            return Err(Error::InvalidArgument(
501                "globals size must be a multiple of host page size",
502            ));
503        }
504        if self.stack_size <= 0 {
505            return Err(Error::InvalidArgument("stack size must be greater than 0"));
506        }
507        if self.signal_stack_size < MINSIGSTKSZ {
508            return Err(Error::InvalidArgument(
509                "signal stack size must be at least MINSIGSTKSZ (defined in <signal.h>)",
510            ));
511        }
512        if cfg!(debug_assertions) && self.signal_stack_size < 12 * 1024 {
513            return Err(Error::InvalidArgument(
514                "signal stack size must be at least 12KiB for debug builds",
515            ));
516        }
517        if self.signal_stack_size % host_page_size() != 0 {
518            return Err(Error::InvalidArgument(
519                "signal stack size must be a multiple of host page size",
520            ));
521        }
522        Ok(())
523    }
524}
525
526pub mod tests;