1 //! Support for implementing the [`RuntimeLinearMemory`] trait in terms of a 2 //! platform mmap primitive. 3 4 use crate::prelude::*; 5 use crate::runtime::vm::memory::RuntimeLinearMemory; 6 use crate::runtime::vm::{HostAlignedByteCount, Mmap, mmap::AlignedLength}; 7 use alloc::sync::Arc; 8 use wasmtime_environ::Tunables; 9 10 use super::MemoryBase; 11 12 /// A linear memory instance. 13 #[derive(Debug)] 14 pub struct MmapMemory { 15 // The underlying allocation. 16 mmap: Arc<Mmap<AlignedLength>>, 17 18 // The current length of this Wasm memory, in bytes. 19 // 20 // This region starts at `pre_guard_size` offset from the base of `mmap`. It 21 // is always accessible, which means that if the Wasm page size is smaller 22 // than the host page size, there may be some trailing region in the `mmap` 23 // that is accessible but should not be accessed. (We rely on explicit 24 // bounds checks in the compiled code to protect this region.) 25 len: usize, 26 27 // The optional maximum accessible size, in bytes, for this linear memory. 28 // 29 // Note that this maximum does not factor in guard pages, so this isn't the 30 // maximum size of the linear address space reservation for this memory. 31 // 32 // This is *not* always a multiple of the host page size, and 33 // `self.accessible()` may go past `self.maximum` when Wasm is using a small 34 // custom page size due to `self.accessible()`'s rounding up to the host 35 // page size. 36 maximum: Option<usize>, 37 38 // The amount of extra bytes to reserve whenever memory grows. This is 39 // specified so that the cost of repeated growth is amortized. 40 extra_to_reserve_on_growth: HostAlignedByteCount, 41 42 // Size in bytes of extra guard pages before the start and after the end to 43 // optimize loads and stores with constant offsets. 44 pre_guard_size: HostAlignedByteCount, 45 offset_guard_size: HostAlignedByteCount, 46 } 47 48 impl MmapMemory { 49 /// Create a new linear memory instance with specified minimum and maximum 50 /// number of wasm pages. new( ty: &wasmtime_environ::Memory, tunables: &Tunables, minimum: usize, maximum: Option<usize>, ) -> Result<Self>51 pub fn new( 52 ty: &wasmtime_environ::Memory, 53 tunables: &Tunables, 54 minimum: usize, 55 maximum: Option<usize>, 56 ) -> Result<Self> { 57 // It's a programmer error for these two configuration values to exceed 58 // the host available address space, so panic if such a configuration is 59 // found (mostly an issue for hypothetical 32-bit hosts). 60 // 61 // Also be sure to round up to the host page size for this value. 62 let offset_guard_bytes = 63 HostAlignedByteCount::new_rounded_up_u64(tunables.memory_guard_size) 64 .context("tunable.memory_guard_size overflows")?; 65 let pre_guard_bytes = if tunables.guard_before_linear_memory { 66 offset_guard_bytes 67 } else { 68 HostAlignedByteCount::ZERO 69 }; 70 71 // Calculate how much is going to be allocated for this linear memory in 72 // addition to how much extra space we're reserving to grow into. 73 // 74 // If the minimum size of this linear memory fits within the initial 75 // allocation (tunables.memory_reservation) then that's how many bytes 76 // are going to be allocated. If the maximum size of linear memory 77 // additionally fits within the entire allocation then there's no need 78 // to reserve any extra for growth. 79 // 80 // If the minimum size doesn't fit within this linear memory. 81 let mut alloc_bytes = tunables.memory_reservation; 82 let mut extra_to_reserve_on_growth = tunables.memory_reservation_for_growth; 83 let minimum_u64 = u64::try_from(minimum).unwrap(); 84 if minimum_u64 <= alloc_bytes { 85 if let Ok(max) = ty.maximum_byte_size() { 86 if max <= alloc_bytes { 87 extra_to_reserve_on_growth = 0; 88 } 89 } 90 } else { 91 alloc_bytes = minimum_u64.saturating_add(extra_to_reserve_on_growth); 92 } 93 94 // Convert `alloc_bytes` and `extra_to_reserve_on_growth` to 95 // page-aligned `usize` values. 96 let alloc_bytes = HostAlignedByteCount::new_rounded_up_u64(alloc_bytes) 97 .context("tunables.memory_reservation overflows")?; 98 let extra_to_reserve_on_growth = 99 HostAlignedByteCount::new_rounded_up_u64(extra_to_reserve_on_growth) 100 .context("tunables.memory_reservation_for_growth overflows")?; 101 102 let request_bytes = pre_guard_bytes 103 .checked_add(alloc_bytes) 104 .and_then(|i| i.checked_add(offset_guard_bytes)) 105 .with_context(|| format!("cannot allocate {minimum} with guard regions"))?; 106 107 let mmap = Mmap::accessible_reserved(HostAlignedByteCount::ZERO, request_bytes)?; 108 109 if minimum > 0 { 110 let accessible = HostAlignedByteCount::new_rounded_up(minimum)?; 111 // SAFETY: mmap is not in use right now so it's safe to make it accessible. 112 unsafe { 113 mmap.make_accessible(pre_guard_bytes, accessible)?; 114 } 115 } 116 117 Ok(Self { 118 mmap: try_new::<Arc<_>>(mmap)?, 119 len: minimum, 120 maximum, 121 pre_guard_size: pre_guard_bytes, 122 offset_guard_size: offset_guard_bytes, 123 extra_to_reserve_on_growth, 124 }) 125 } 126 127 /// Get the length of the accessible portion of the underlying `mmap`. This 128 /// is the same region as `self.len` but rounded up to a multiple of the 129 /// host page size. accessible(&self) -> HostAlignedByteCount130 fn accessible(&self) -> HostAlignedByteCount { 131 let accessible = HostAlignedByteCount::new_rounded_up(self.len) 132 .expect("accessible region always fits in usize"); 133 debug_assert!(accessible <= self.current_capacity()); 134 accessible 135 } 136 137 /// Get the amount to which this memory can grow. current_capacity(&self) -> HostAlignedByteCount138 fn current_capacity(&self) -> HostAlignedByteCount { 139 let mmap_len = self.mmap.len_aligned(); 140 mmap_len 141 .checked_sub(self.offset_guard_size) 142 .and_then(|i| i.checked_sub(self.pre_guard_size)) 143 .expect("guard regions fit in mmap.len") 144 } 145 } 146 147 impl RuntimeLinearMemory for MmapMemory { byte_size(&self) -> usize148 fn byte_size(&self) -> usize { 149 self.len 150 } 151 byte_capacity(&self) -> usize152 fn byte_capacity(&self) -> usize { 153 self.current_capacity().byte_count() 154 } 155 grow_to(&mut self, new_size: usize) -> Result<()>156 fn grow_to(&mut self, new_size: usize) -> Result<()> { 157 let new_accessible = HostAlignedByteCount::new_rounded_up(new_size)?; 158 let current_capacity = self.current_capacity(); 159 if new_accessible > current_capacity { 160 // If the new size of this heap exceeds the current size of the 161 // allocation we have, then this must be a dynamic heap. Use 162 // `new_size` to calculate a new size of an allocation, allocate it, 163 // and then copy over the memory from before. 164 let request_bytes = self 165 .pre_guard_size 166 .checked_add(new_accessible) 167 .and_then(|s| s.checked_add(self.extra_to_reserve_on_growth)) 168 .and_then(|s| s.checked_add(self.offset_guard_size)) 169 .context("overflow calculating size of memory allocation")?; 170 171 let mut new_mmap = 172 Mmap::accessible_reserved(HostAlignedByteCount::ZERO, request_bytes)?; 173 // SAFETY: new_mmap is not in use right now so it's safe to make it 174 // accessible. 175 unsafe { 176 new_mmap.make_accessible(self.pre_guard_size, new_accessible)?; 177 } 178 179 // This method has an exclusive reference to `self.mmap` and just 180 // created `new_mmap` so it should be safe to acquire references 181 // into both of them and copy between them. 182 unsafe { 183 let range = 184 self.pre_guard_size.byte_count()..(self.pre_guard_size.byte_count() + self.len); 185 let src = self.mmap.slice(range.clone()); 186 let dst = new_mmap.slice_mut(range); 187 dst.copy_from_slice(src); 188 } 189 190 self.mmap = Arc::new(new_mmap); 191 } else { 192 // If the new size of this heap fits within the existing allocation 193 // then all we need to do is to make the new pages accessible. This 194 // can happen either for "static" heaps which always hit this case, 195 // or "dynamic" heaps which have some space reserved after the 196 // initial allocation to grow into before the heap is moved in 197 // memory. 198 assert!(new_size <= current_capacity.byte_count()); 199 assert!(self.maximum.map_or(true, |max| new_size <= max)); 200 201 // If the Wasm memory's page size is smaller than the host's page 202 // size, then we might not need to actually change permissions, 203 // since we are forced to round our accessible range up to the 204 // host's page size. 205 if let Ok(difference) = new_accessible.checked_sub(self.accessible()) { 206 // SAFETY: the difference was previously inaccessible so we 207 // never handed out any references to within it. 208 unsafe { 209 self.mmap.make_accessible( 210 self.pre_guard_size 211 .checked_add(self.accessible()) 212 .context("overflow calculating new accessible region")?, 213 difference, 214 )?; 215 } 216 } 217 } 218 219 self.len = new_size; 220 221 Ok(()) 222 } 223 set_byte_size(&mut self, len: usize)224 fn set_byte_size(&mut self, len: usize) { 225 self.len = len; 226 } 227 base(&self) -> MemoryBase228 fn base(&self) -> MemoryBase { 229 MemoryBase::Mmap( 230 self.mmap 231 .offset(self.pre_guard_size) 232 .expect("pre_guard_size is in bounds"), 233 ) 234 } 235 vmmemory(&self) -> crate::vm::VMMemoryDefinition236 fn vmmemory(&self) -> crate::vm::VMMemoryDefinition { 237 let pre_guard_size = self.pre_guard_size.byte_count(); 238 assert!(pre_guard_size <= self.mmap.len()); 239 let pre_guard_size = isize::try_from(pre_guard_size).unwrap(); 240 let mmap = self.mmap.as_non_null(); 241 let base = unsafe { 242 // Safety: `pre_guard_size` is within the mmap allocation. 243 mmap.offset(pre_guard_size) 244 }; 245 246 crate::vm::VMMemoryDefinition { 247 base: base.into(), 248 current_length: self.len.into(), 249 } 250 } 251 } 252