diff --git a/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs b/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs index cfbca292d..f54a721bc 100644 --- a/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs +++ b/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs @@ -101,6 +101,7 @@ impl Slot { pub struct Alloc { pub heap_accessible_size: usize, pub heap_inaccessible_size: usize, + pub heap_memory_size_limit: usize, pub slot: Option, pub region: Arc, } @@ -241,7 +242,7 @@ impl Alloc { } // The runtime sets a limit on how much of the heap can be backed by real memory. Don't let // the heap expand beyond that: - if self.heap_accessible_size + expand_pagealigned as usize > slot.limits.heap_memory_size { + if self.heap_accessible_size + expand_pagealigned as usize > self.heap_memory_size_limit { bail_limits_exceeded!( "expansion would exceed runtime-specified heap limit: {:?}", slot.limits @@ -396,7 +397,7 @@ impl Alloc { /// Runtime limits for the various memories that back a Lucet instance. /// /// Each value is specified in bytes, and must be evenly divisible by the host page size (4K). -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] #[repr(C)] pub struct Limits { /// Max size of the heap, which can be backed by real memory. (default 1M) diff --git a/lucet-runtime/lucet-runtime-internals/src/alloc/tests.rs b/lucet-runtime/lucet-runtime-internals/src/alloc/tests.rs index 4db699b0e..5f7f8b5b2 100644 --- a/lucet-runtime/lucet-runtime-internals/src/alloc/tests.rs +++ b/lucet-runtime/lucet-runtime-internals/src/alloc/tests.rs @@ -191,6 +191,46 @@ macro_rules! alloc_tests { assert_eq!(heap[new_heap_len - 1], 0xFF); } + /// This test exercises custom limits on the heap_memory_size. + /// In this scenario, the Region has a limit on the heap memory + /// size, but the instance has a smaller limit. Attempts to expand + /// the heap fail, but the existing heap can still be used. + #[test] + fn expand_past_spec_max_with_custom_limit() { + let region = TestRegion::create(10, &LIMITS).expect("region created"); + let module = MockModuleBuilder::new() + .with_heap_spec(THREE_PAGE_MAX_HEAP) + .build(); + let mut inst = region + .new_instance_builder(module.clone()) + .with_heap_size_limit((THREEPAGE_INITIAL_SIZE) as usize) + .build() + .expect("new_instance succeeds"); + + let heap_len = inst.alloc().heap_len(); + assert_eq!(heap_len, THREEPAGE_INITIAL_SIZE as usize); + + // Expand the heap within the Region limit but past the + // instance limit. + let new_heap_area = inst.alloc_mut().expand_heap( + (THREEPAGE_MAX_SIZE - THREEPAGE_INITIAL_SIZE) as u32, + module.as_ref(), + ); + assert!( + new_heap_area.is_err(), + "heap expansion past instance limit fails" + ); + + // Affirm that the existing heap can still be used. + let new_heap_len = inst.alloc().heap_len(); + assert_eq!(new_heap_len, heap_len); + + let heap = unsafe { inst.alloc_mut().heap_mut() }; + assert_eq!(heap[new_heap_len - 1], 0); + heap[new_heap_len - 1] = 0xFF; + assert_eq!(heap[new_heap_len - 1], 0xFF); + } + const EXPANDPASTLIMIT_INITIAL_SIZE: u64 = LIMITS_HEAP_MEM_SIZE as u64 - (64 * 1024); const EXPANDPASTLIMIT_MAX_SIZE: u64 = LIMITS_HEAP_MEM_SIZE as u64 + (64 * 1024); const EXPAND_PAST_LIMIT_SPEC: HeapSpec = HeapSpec { @@ -836,6 +876,90 @@ macro_rules! alloc_tests { Ok(_) => panic!("unexpected success"), } } + + /// This test exercises custom limits on the heap_memory_size. + /// In this scenario, the Region has a limit on the heap + /// memory size, but the instance has a larger limit. An + /// instance's custom limit must not exceed the Region's. + #[test] + fn reject_heap_memory_size_exceeds_region_limits() { + let region = TestRegion::create(1, &LIMITS).expect("region created"); + let module = MockModuleBuilder::new() + .with_heap_spec(THREE_PAGE_MAX_HEAP) + .build(); + let res = region + .new_instance_builder(module.clone()) + .with_heap_size_limit(&LIMITS.heap_memory_size * 2) + .build(); + + match res { + Err(Error::InvalidArgument( + "heap memory size requested for instance is larger than slot allows", + )) => (), + Err(e) => panic!("unexpected error: {}", e), + Ok(_) => panic!("unexpected success"), + } + } + + /// This test exercises custom limits on the heap_memory_size. + /// In this scenario, successfully create a custom-sized + /// instance, drop it, then create a default-sized instance to + /// affirm that a custom size doesn't somehow overwrite the + /// default size. + #[test] + fn custom_size_does_not_break_default() { + let region = TestRegion::create(1, &LIMITS).expect("region created"); + + // Build an instance that is has custom limits that are big + // enough to accommodate the HeapSpec. + let custom_inst = region + .new_instance_builder( + MockModuleBuilder::new() + .with_heap_spec(THREE_PAGE_MAX_HEAP) + .build(), + ) + .with_heap_size_limit((THREE_PAGE_MAX_HEAP.initial_size * 2) as usize) + .build() + .expect("new instance succeeds"); + + // Affirm that its heap is the expected size, the size + // specified in the HeapSpec. + let heap_len = custom_inst.alloc().heap_len(); + assert_eq!(heap_len, THREE_PAGE_MAX_HEAP.initial_size as usize); + drop(custom_inst); + + // Build a default heap-limited instance, to make sure the + // custom limits didn't break the defaults. + let default_inst = region + .new_instance( + MockModuleBuilder::new() + .with_heap_spec(SMALL_GUARD_HEAP) + .build(), + ) + .expect("new_instance succeeds"); + + // Affirm that its heap is the expected size. + let heap_len = default_inst.alloc().heap_len(); + assert_eq!(heap_len, SMALL_GUARD_HEAP.initial_size as usize); + } + + /// This test exercises custom limits on the heap_memory_size. + /// In this scenario, the HeapSpec has an expectation for the + /// initial heap memory size, but the instance's limit is too small. + #[test] + fn reject_heap_memory_size_exeeds_instance_limits() { + let region = TestRegion::create(1, &LIMITS).expect("region created"); + let res = region + .new_instance_builder( + MockModuleBuilder::new() + .with_heap_spec(THREE_PAGE_MAX_HEAP) + .build(), + ) + .with_heap_size_limit((THREE_PAGE_MAX_HEAP.initial_size / 2) as usize) + .build(); + + assert!(res.is_err(), "new_instance fails"); + } }; } diff --git a/lucet-runtime/lucet-runtime-internals/src/region.rs b/lucet-runtime/lucet-runtime-internals/src/region.rs index 5e633442d..1f067497b 100644 --- a/lucet-runtime/lucet-runtime-internals/src/region.rs +++ b/lucet-runtime/lucet-runtime-internals/src/region.rs @@ -57,6 +57,7 @@ pub trait RegionInternal: Send + Sync { &self, module: Arc, embed_ctx: CtxMap, + heap_memory_size_limit: usize, ) -> Result; /// Unmaps the heap, stack, and globals of an `Alloc`, while retaining the virtual address @@ -93,6 +94,7 @@ pub struct InstanceBuilder<'a> { region: &'a dyn RegionInternal, module: Arc, embed_ctx: CtxMap, + heap_memory_size_limit: usize, } impl<'a> InstanceBuilder<'a> { @@ -101,9 +103,19 @@ impl<'a> InstanceBuilder<'a> { region, module, embed_ctx: CtxMap::default(), + heap_memory_size_limit: region.get_limits().heap_memory_size, } } + /// Add a smaller, custom limit for the heap memory size to the built instance. + /// + /// This call is optional. Attempts to build a new instance fail if the + /// limit supplied by with_heap_size_limit() exceeds that of the region. + pub fn with_heap_size_limit(mut self, heap_memory_size_limit: usize) -> Self { + self.heap_memory_size_limit = heap_memory_size_limit; + self + } + /// Add an embedder context to the built instance. /// /// Up to one context value of any particular type may exist in the instance. If a context value @@ -120,6 +132,7 @@ impl<'a> InstanceBuilder<'a> { /// This function runs the guest code for the WebAssembly `start` section, and running any guest /// code is potentially unsafe; see [`Instance::run()`](struct.Instance.html#method.run). pub fn build(self) -> Result { - self.region.new_instance_with(self.module, self.embed_ctx) + self.region + .new_instance_with(self.module, self.embed_ctx, self.heap_memory_size_limit) } } diff --git a/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs b/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs index 66e1b3920..4ad313e6e 100644 --- a/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs +++ b/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs @@ -8,6 +8,7 @@ use libc::c_void; #[cfg(not(target_os = "linux"))] use libc::memset; use nix::sys::mman::{madvise, mmap, munmap, MapFlags, MmapAdvise, ProtFlags}; +use std::cmp::Ordering; use std::ptr; use std::sync::{Arc, RwLock, Weak}; @@ -78,11 +79,32 @@ impl RegionInternal for MmapRegion { &self, module: Arc, embed_ctx: CtxMap, + heap_memory_size_limit: usize, ) -> Result { + let custom_limits; + let mut limits = self.get_limits(); + // Affirm that the module, if instantiated, would not violate // any runtime memory limits. - let limits = self.get_limits(); - module.validate_runtime_spec(limits)?; + match heap_memory_size_limit.cmp(&limits.heap_memory_size) { + Ordering::Less => { + // The supplied heap_memory_size is smaller than + // default. Augment the limits with this custom value + // so that it may be validated. + custom_limits = Limits { + heap_memory_size: heap_memory_size_limit, + ..*limits + }; + limits = &custom_limits; + } + Ordering::Equal => (), + Ordering::Greater => { + return Err(Error::InvalidArgument( + "heap memory size requested for instance is larger than slot allows", + )) + } + } + module.validate_runtime_spec(&limits)?; let slot = self .freelist @@ -127,6 +149,7 @@ impl RegionInternal for MmapRegion { let alloc = Alloc { heap_accessible_size: 0, // the `reset` call in `new_instance_handle` will set this heap_inaccessible_size: slot.limits.heap_address_space_size, + heap_memory_size_limit, slot: Some(slot), region, };